Add ClickHouse + Vector + NATS JetStream centralized logging (with S3 raw archive) #296
Reference in New Issue
Block a user
Delete Branch "benvin/clickhouse-vector-logging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Metrics already land in VictoriaMetrics, but there is no centralized log store. This stands up the logs pillar: capture all logs from (a) k8s pods and (b) puppet-managed VMs into ClickHouse, with a durable NATS JetStream bus in the middle so logs survive a ClickHouse outage, can be replayed after a bad transform, and fan out to independent consumers. A third consumer archives selected raw logs to S3 (Ceph RGW) for long-horizon replay beyond the JetStream window. The puppet-side Vector rollout is a later task — this PR makes sure a reachable VM ingestion endpoint exists.
Topology
edge (publishers) → JetStream → consumers → sinksloggingns): 3-replica cluster, file storage oncephrbd-fast-delete(50Gi/node). Deliberately separate from app messaging (streamstack runs its own NATS in its own repo) for blast-radius isolation. StreamLOGS(subjectslogs.>,retention=limits, S2-compressed, 3d / 130 GiB). Durable consumers = independent offsets.vector-agent(DaemonSet): tails every node's pod logs (incl. control-plane) → JetStreamlogs.k8s.<ns>.<container>.vector-vm-ingest(Deployment): HTTPS/NDJSON front door behind thelogs-ingestGateway → JetStreamlogs.vm.<host>. (Chosen over exposing NATS TCP to ~143 VMs: keeps VM shipping to a simple TLS POST while still gaining JetStream durability; direct-NATS-for-VMs noted as an alternative.)vector-aggregator(StatefulSet): pulls the whole stream via durable consumertransform, routes by subject, normalises intologs.raw, and is the sole ClickHouse writer. Disk buffer shrunk to 2GiB/5Gi PVC (JetStream is the real outage buffer now).vector-archiver(Deployment): its own durable consumerarchiver(independent offsets — archive lag can never stall ClickHouse) writes raw, pre-transform events to a Ceph RGW bucket as gzipped NDJSON, keyedraw/<subject>/YYYY/MM/DD/. Default subject filterlogs.k8s.vault.>(Vault audit) — configurable via the bootstrap Job'sARCHIVE_SUBJECTS.ClickHouseInstallation(200Gi RBD),logs.rawMergeTree, 30d TTL, idempotent PostSync schema Job.Streams / consumers / auth
natsCLI). Runbook lines for both replay directions are in the Job's header comment.log-producer(publishlogs.>only),log-consumer(pull + ack only),log-admin(bootstrap). Passwords from Vault (nats-authSecret, env-var expansion in the server config). S3 creds from thecephrgw-operatorBucketAccessSecret.S3 / retention
ObjectStoreUser+Bucket(logs-archive, retainOnDelete) +BucketAccess(read-write) via the in-estate cephrgw-operator. aws_s3 sink →https://s3.ceph.unkin.net(path-style, trusts the reflectedvault-ca-cert). Object retention is an RGW-side bucket lifecycle policy (the operator doesn't manage lifecycle) — flagged as an operational knob, not invented here.Replay runbook
nats consumer rm LOGS transform, re-run the bootstrap Job (recreates at DeliverAll) — ornats consumer edit/--replayfrom a seq/time.aws_s3source or a one-shot Job); the archive is the replay source beyond JetStream's window.Validation
kustomize build --enable-helmclean;kubeconform(k8s 1.33.7) all valid — clickhouse-system 22, logging 38 (incl.ClickHouseInstallationvia datreeio and theceph.unkin.netCRDs via local schemas added underschemas/), apps/base 10.pre-commit(yamllint, check-json, no-plain-secrets) clean.vector testpasses the transform-tier + VM-ingest unit tests;vector validatepasses the agent + archiver configs.Known upstream caveat
Vector's NATS JetStream source has an open reliability issue (vectordotdev/vector#24932: consumer can stall after a NATS "lame duck"/reconnect). Recovery is a pod restart of the affected consumer; noted for the runbook.
Prerequisites (manual, one-time)
No terraform-vault change needed (templated
defaultk8s auth policy already grants theloggingnamespace KV path). Thevault-ca-certSecret is reflected intologgingby the existing reflector. RGW bucket + creds are provisioned by cephrgw-operator from the CRs in this PR.Open decisions (defaults chosen, flag to change)
logs.k8s.vault.>(Vault audit). Candidates to add:logs.k8s.authentik.>,logs.k8s.kanidm.>, VM auth roles — please confirm the exact security set.timberio/vector:0.57.0-debian+natsio/nats-box:0.18.0(Docker Hub) — mirror if runners restrict egress.https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Update: images via artifactapi, DHI, stateless transform tier
Depends on unkin/terraform-artifactapi#16 (dockerhub allowlist patterns) — merge that first or images won't pull.
Image table (all pulled through
artifactapi.k8s.syd1.au.unkin.net/dockerhub/…)DHI decision: Docker Hardened Images exist for clickhouse-server and vector, but they're subscription-gated and served from a private Docker org namespace (authenticated pull) — not reachable via the estate's anonymous artifactapi
dockerhubproxy, and no DHI org/remote exists here. Their shell-less nature would also break thebashheredoc in the ClickHouse schema Job and the shell-basedvector-testCI step. So: upstream official through artifactapi, using vectordistroless-libcfor runtime pods. Adopting DHI later would need a Docker Business subscription + an authenticated artifactapi remote for the DHI namespace.Transform tier is now a stateless Deployment
Was a StatefulSet with a disk buffer/PVC; now a Deployment with no PVC and an in-memory buffer — JetStream is the sole durability layer. Added a CPU HPA (min 2 / max 8).
Ack / backpressure design (important caveat): Vector's NATS source has
acknowledgements: no— it acks the JetStream message on receipt, not after the ClickHouse sink confirms. So end-to-end "sink-failure-must-not-ack" isn't achievable with the current source. What we get instead: the ClickHouse sink usesbuffer.when_full=block, so on a ClickHouse outage the memory buffer fills, back-pressure stops the pull source, and unpulled messages stay in JetStream and are redelivered. The only at-risk window is the in-memory buffer (2000 events) of already-pulled events if a pod is killed mid-outage. This is the accepted trade for a stateless, autoscalable tier. HPA is safe because JetStream pull consumers distribute work across N replicas on the single durable consumertransform. (If stronger delivery is needed later: reintroduce a StatefulSet+disk buffer, or wait for upstream end-to-end-ack support on the nats source — vectordotdev/vector.)Update: 7d retention, tunable limits ConfigMap, honest sizing
Retention → 7 days (
max_age=168h), stillretention=limits/discard=old: the transform tier and the archiver each have their own durable consumer and independently see every message — reading never deletes; only max_age/max_bytes evict.Stream limits live in a ConfigMap (
nats-stream-limits:max_age,max_bytes,dupe_window). Thenats-bootstrapPostSync Job reads them and does an idempotent create-or-UPDATE (nats stream add||nats stream edit). How a change propagates: the ConfigMap keeps its kustomize content-hash suffix, so editing a value renames the ConfigMap and rewrites the Job'sconfigMapKeyRefs → the hook Job's spec changes → Argo re-runs it (on top of PostSync hooks running every sync withhook-delete-policy=BeforeHookCreation) →nats stream editapplies the new limits. No manualnatssurgery. Verified against a real nats-server: create (7d), idempotent re-run, and amax_agechange (168h→24h) all applied; all flags incl.--compression=s2accepted by nats CLI v0.2.3.Honest 7d sizing (stated assumption — please sanity-check against real volume):
max_bytes = 300 GiB(headroom over the 230 GiB estimate). PVC = 400Gi/node oncephrbd-fast-delete(max_bytes + file-store WAL/index/overhead, safely under). 3 replicas ⇒ 1.2 TiB provisioned.discard=oldtruncates retention below 7d rather than silently overflowing. Raising retention/volume requires bumping bothmax_bytes(ConfigMap) and the file-store PVC (values-nats.yaml) together — the PVC is not a live-tunable knob.Replay window in the runbook is now 7d (beyond that → the S3 archive).
Update: retention cut to 3 days (both stores), PVCs shrunk
Ben: 1.2 TiB is too much. Both stores now retain 3 days; long-term retention lives exclusively in the encrypted S3 archive (the archiver's configured subjects) — everything else is gone after 3d. That's the accepted design.
LOGSmax_age=72h(3d)max_bytes=130 GiBlogs.rawTTL 3 DAYNATS math: ~33 GiB/day compressed (S2) × 3d ≈ 100 GiB →
max_bytes130 GiB (headroom) under a 180Gi PVC.ClickHouse math: ~130 GiB/day raw, LZ4/ZSTD ~6× ⇒ ~20-25 GiB/day ⇒ ~60-75 GiB/3d; +merge headroom ⇒ 150Gi PVC.
logs.rawis the only table.The retention knobs remain in the
nats-stream-limitsConfigMap (max_age/max_bytes/dupe_window) — tunable without redeploy; the ClickHouse TTL is in the bootstrap DDL.⚠️ PVC-shrink caveat: this is a plan-time change — the stack is not deployed yet, so shrinking PVCs is clean. If it were already deployed, PVCs cannot shrink in place (a StatefulSet/CHI PVC resize-down needs a recreate/migration, not an edit).
Add ClickHouse + Vector centralized logging stackto Add ClickHouse + Vector + NATS JetStream centralized logging (with S3 raw archive)