This directory contains the Kubernetes (K8s) configuration and Helm chart for a lightweight, self-hosted observability stack based on OpenObserve and OpenTelemetry. The chart wraps the official OpenObserve standalone and OpenTelemetry Collector charts with reusable defaults.
Most shared prerequisites are covered in the root-level README. This document focuses on the storage, secrets and deployment requirements specific to observability.
The chart deploys:
node/pod/container metricsEvery node Cluster singleton
┌─────────────────────────┐ ┌─────────────────────────┐
│ OTel agent DaemonSet │ │ OTel cluster collector │
│ │ │ │
│ • container stdout logs │ │ • cluster metrics │
│ • host CPU/memory │ │ • Kubernetes events │
│ • pod/container metrics │ │ • application OTLP │
└────────────┬────────────┘ └────────────┬────────────┘
│ OTLP/HTTP │ OTLP/HTTP
└─────────────────┬────────────────────┘
▼
┌──────────────────────┐
│ OpenObserve + UI │
├──────────────────────┤
│ metadata → Postgres │
│ telemetry → S3 │
└──────────────────────┘
OpenObserve uses the existing CloudNativePG cluster for metadata and the existing SeaweedFS S3 endpoint for Parquet telemetry files. Its /data directory is an ephemeral emptyDir used only for short-lived WAL/cache data; the container’s ephemeral-storage limit bounds it. No new PVC is needed.
A pod or node loss can discard the small WAL window that has not yet reached SeaweedFS – we accept this risk for now. The chart uses short flush intervals to bound that risk.
postgres-cluster-rw.databases.svc.cluster.local:5432 if you haven’t changed the storage chart values.http://seaweedfs-s3.storage.svc.cluster.local:8333 with bucket openobserve if you haven’t changed any storage chart values.The chart does not create either storage tenant because doing so requires privileged database and object-store credentials that the runtime pods should not have.
Create a dedicated role and logical database on the existing cluster. Do not reuse an application’s database or schema.
CREATE ROLE openobserve LOGIN PASSWORD '<strong-random-password>';
CREATE DATABASE openobserve OWNER openobserve;
The resulting DSN has this shape:
postgresql://openobserve:<url-encoded-password>@postgres-cluster-rw.databases.svc.cluster.local:5432/openobserve
Store the complete DSN in Doppler as ZO_META_POSTGRES_DSN. Password characters with URI meaning must be URL-encoded.
Create the openobserve bucket with credentials that can read, write and delete objects in that bucket. For example, port-forward SeaweedFS and use an S3-compatible CLI from your workstation:
kubectl --namespace storage port-forward service/seaweedfs-s3 8333:8333
AWS_ACCESS_KEY_ID='<access-key>' \
AWS_SECRET_ACCESS_KEY='<secret-key>' \
aws --endpoint-url http://127.0.0.1:8333 s3 mb s3://openobserve
The default 30-day retention deletes expired telemetry through OpenObserve’s compactor. Monitor SeaweedFS capacity and expand its existing volume before it fills.
The default secrets.provider=doppler configuration expects a Doppler project named observability with these keys:
| Key | Purpose |
|---|---|
ZO_ROOT_USER_EMAIL |
Initial OpenObserve administrator and Collector ingestion username |
ZO_ROOT_USER_PASSWORD |
OpenObserve administrator and Collector ingestion password |
ZO_META_POSTGRES_DSN |
Dedicated PostgreSQL metadata database DSN |
ZO_S3_ACCESS_KEY |
SeaweedFS access key for the dedicated bucket |
ZO_S3_SECRET_KEY |
SeaweedFS secret key for the dedicated bucket |
Generate a separate Doppler service token for the target config and pass it as secrets.doppler.token. Doppler creates the observability-secrets K8s Secret; each component reads only the keys it needs.
If Doppler is disabled:
secrets:
provider: none
managedSecretName: observability-secrets
Create observability-secrets manually with exactly the five keys above before installation.
Important wrapper values:
| Value | Default | Meaning |
|---|---|---|
global.clusterName |
kubernetes |
Cluster identity attached to every signal |
secrets.provider |
doppler |
doppler or none |
secrets.managedSecretName |
observability-secrets |
Secret materialized for selective component access |
ingress.enabled |
true |
Expose only the OpenObserve UI |
ingress.domain.* |
observability.example.com |
UI hostname |
openobserve.config.ZO_COMPACT_DATA_RETENTION_DAYS |
30 |
Retention for logs, metrics and traces |
openobserve.config.ZO_S3_* |
SeaweedFS/S3 defaults | Object-store connection and bucket |
dashboards.enabled |
true |
Import the pinned Kubernetes dashboards |
The default resource requests across a four-node cluster are approximately 640 MiB memory and 270m CPU:
There are no node selectors or workload placement constraints. The node Collector tolerates all taints because it must run on every node; Kubernetes schedules the other two pods normally.
This chart uses only pinned official images from its upstream dependencies. It does not build or require a local image.
Publish the chart through the existing chart pipeline, then install or upgrade the cluster-level Helm release directly:
helm upgrade --install observability appifyhub/observability \
--namespace observability \
--create-namespace \
--set global.clusterName='<cluster-name>' \
--set secrets.doppler.project='<doppler-project>' \
--set secrets.doppler.config='<doppler-config>' \
--set secrets.doppler.token='<doppler-service-token>' \
--set ingress.domain.base='<base-domain>' \
--set ingress.domain.prefix='<subdomain>' \
--set ingress.tls.enabled=true \
--set-string ingress.annotations."traefik\.ingress\.kubernetes\.io/router\.entrypoints"=websecure \
--set-string ingress.annotations."traefik\.ingress\.kubernetes\.io/router\.tls"=true \
--set-string ingress.annotations."traefik\.ingress\.kubernetes\.io/router\.tls\.certresolver"=letsencrypt
Do not commit Doppler service tokens or runtime secrets to this repository.
OpenObserve provides one UI for:
The post-install/upgrade job idempotently imports pinned upstream revisions of:
These dashboards cover node, namespace and pod CPU, memory, disk and network usage. The chart supplies both the standard k8s.cluster.name resource attribute and the upstream dashboards’ legacy k8s.cluster compatibility attribute.
An application dashboard and saved error view are intentionally not guessed before real application telemetry exists. Add them after the instrumented backend is deployed and the actual stream schema is visible. Alerts are also not provisioned until a notification destination is selected; an alert without a usable destination creates false confidence.
Container stdout appears in the k8s_logs stream and Kubernetes events in k8s_events. Application JSON records are parsed into fields while ordinary CRI/Uvicorn records remain searchable.
Instrumented applications send metrics and traces to the singleton Collector, never directly to OpenObserve:
otel-cluster.observability.svc.cluster.local:4317 # OTLP/gRPC
http://otel-cluster.observability.svc.cluster.local:4318 # OTLP/HTTP
A typical backend deployment uses:
env:
- name: OTEL_SDK_DISABLED
value: "false"
- name: OTEL_SERVICE_NAME
value: the-agent
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-cluster.observability.svc.cluster.local:4317
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: grpc
- name: OTEL_LOGS_EXPORTER
value: none
- name: OTEL_TRACES_SAMPLER
value: parentbased_always_on
- name: OTEL_RESOURCE_ATTRIBUTES
value: deployment.environment.name=production
Logs continue through stdout and the node Collector; disabling the application’s OTLP log exporter prevents duplication.
After the user-managed deployment, read-only checks include:
kubectl --namespace observability get pods
kubectl --namespace observability get services
kubectl --namespace observability get jobs
kubectl --namespace observability top pods
Expected workloads on a four-node cluster:
openobserve StatefulSet: 1 podotel-agent DaemonSet: 4 podsotel-cluster Deployment: 1 podOpen the configured UI, select the default organization and verify:
k8s_logs receives recent container records.k8s_events receives Kubernetes events.k8s_node_cpu_usage and k8s_pod_memory_rss exist.The complete observability system is intentionally in the same monitored cluster. A cluster-wide outage therefore also makes monitoring unavailable – but this is an accepted cost/simplicity trade-off for this simple cluster example and not an external uptime monitor.
Chart uninstall removes OpenObserve and Collector workloads but does not remove the external PostgreSQL database or SeaweedFS objects. Deleting those persistent stores is a separate, destructive action.
OpenObserve’s local /data is ephemeral. A restart can lose only telemetry not yet flushed to SeaweedFS; it cannot remove already persisted Parquet data or PostgreSQL metadata.
The wrapper pins:
0.92.2 / image v0.92.20.172.0 / image 0.159.06e2fc4d11b844f45a25a7cfd0cf445b58424158dReview upstream release notes, render the complete chart and inspect configuration changes before updating these pins.