Files
zeekay 0730def03a Opt every service out of automated sync until its file is true here
Every file that opted in described the DigitalOcean estate, so CD
applying it put a pod in front of static sites the edge already serves
from disk. Automation stays per service; a file re-opts in once what it
declares has been shown to run on this cluster.
2026-09-03 16:22:24 -07:00

307 lines
11 KiB
YAML

# Lux's node-local o11y collector — the same one hanzo-k8s runs.
#
# This replaces node-exporter (9 pods) and the cadvisor scrape that
# lux-validator-vmagent performed. Lux now produces OTLZ like everything else
# and writes it to the o11y plane directly, so there is one telemetry standard
# in the estate rather than one per cluster.
#
# The ONLY difference from hanzo's agent is the endpoint: cloud's ZAP sockets
# are a ClusterIP over there, so from here they are reached through the `otlz`
# internal LoadBalancer on the shared DigitalOcean VPC. Everything else —
# image, receivers, processors, pipelines — is deliberately identical. If this
# file ever needs a Lux-specific receiver or a different wire, that is the
# signal that the plane has grown a second way of doing something.
replicas: 1
workload:
kind: DaemonSet
selectorLabels:
app: otel-agent
containerName: otel-agent
image:
# Pinned to the same build hanzo runs, and it must stay in step: this SHA is
# the first with per-pod ZAP node identity (aeff4d2) and lowercase span kind
# (9aab666). ghcr.io/hanzoai/otel-collector publishes no resolvable semver,
# so a tag would not pin anything.
repository: ghcr.io/hanzoai/otel-collector
#
# ⚠️ THIS SHA IS A PIN, NOT A WARNING SIGN — and the next bump is wanted.
# otel-collector main CONTAINS this build (`git merge-base --is-ancestor
# e321e94 origin/main` is true), and this build CARRIES the restart fix:
# plane needs: nodeID gained a per-process boot nonce (otel-collector e321e94),
# so a collector that restarts inside its pod arrives as a NEW ZAP peer instead
# of reclaiming an identity the receiver still holds and being refused with EOF
# forever. Four incidents on 2026-08-08 each ended with a manual delete of the
# pod; that is exactly what the nonce removes. Build main and move these pins.
#
# (An earlier revision of this comment said main had diverged onto an OTLP proto
# wire the receiver could not read, and called it measured. It was measured
# against a STALE LOCAL CHECKOUT, not the remote — origin/main has the
# MetricBatch wire and luxfi/zap, same as this image. Check origin, never a
# local main.)
tag: sha-e321e94
pullPolicy: IfNotPresent
imagePullSecrets: []
# EMPTY ON PURPOSE — the chart defaults this to `ghcr-secret`, and on THIS
# cluster that credential is user Darkhorse7stars, whose token the ghcr token
# endpoint answers 403. ghcr.io/hanzoai/otel-collector is public: an
# anonymous pull of this exact tag returns 200, and a credential-free pod on
# a lux node pulls it in seconds. Sending a rejected credential turns a public
# image into ImagePullBackOff, which is how all nine agents and the gateway
# sat unschedulable while the image was freely available the whole time.
#
# (hanzo-k8s has a secret of the SAME NAME holding hanzo-dev, which does
# authenticate — so the chart default is right there and wrong here. lux's
# ghcr-secret being dead is a separate problem: nothing new can be pulled on
# this cluster from a private hanzoai package, and everything still running
# is running from node cache.)
partOf: o11y
component: observability
args:
- --config=/etc/otel/otel-agent.yaml
env:
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
# kubeletstats dials the node's ADDRESS. Node names are not DNS records on
# DOKS, so ${K8S_NODE_NAME} there fails every interval with "no such host"
# while the collector otherwise looks healthy.
- name: K8S_NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: OTEL_RESOURCE_ATTRIBUTES
value: k8s.node.name=$(K8S_NODE_NAME)
rbac:
create: true
serviceAccountName: otel-agent
clusterRules:
# k8sattributes: resolve pod/namespace/workload metadata.
- apiGroups: ['']
resources: ['pods', 'namespaces', 'nodes']
verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']
resources: ['replicasets', 'deployments', 'daemonsets', 'statefulsets']
verbs: ['get', 'list', 'watch']
# kubeletstats: this node's kubelet summary API.
- apiGroups: ['']
resources: ['nodes/stats', 'nodes/proxy']
verbs: ['get']
tolerations:
- operator: Exists
ports:
- containerPort: 13133
name: health
- containerPort: 4317
name: otlp-grpc
- containerPort: 4318
name: otlp-http
resources:
requests:
cpu: 50m
memory: 192Mi
ephemeral-storage: 512Mi
limits:
cpu: 500m
memory: 1Gi
readinessProbe:
httpGet:
path: /
port: 13133
initialDelaySeconds: 5
periodSeconds: 15
volumeMounts:
- name: config
mountPath: /etc/otel
readOnly: true
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
- name: checkpoint
mountPath: /var/lib/otel-agent
- name: hostfs
mountPath: /hostfs
readOnly: true
mountPropagation: HostToContainer
volumes:
- name: config
configMap:
name: otel-agent-config
- name: varlogpods
hostPath:
path: /var/log/pods
type: Directory
- name: checkpoint
hostPath:
path: /var/lib/otel-agent
type: DirectoryOrCreate
- name: hostfs
hostPath:
path: /
type: Directory
configMaps:
- name: otel-agent-config
data:
otel-agent.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
filelog:
include:
- /var/log/pods/*/*/*.log
exclude:
- /var/log/pods/lux-system_otel-agent-*/**/*.log
start_at: end
include_file_path: true
include_file_name: false
retry_on_failure:
enabled: true
operators:
- type: container
add_metadata_from_filepath: true
# Host metrics — what node-exporter used to answer, at a rate this
# datastore can carry. 300s, not 30s: the OTLZ exporter emits one
# message per resource and cloud turns each into one INSERT, fanning
# out through seven tables. At 30s across 45 agents that helped push
# event.metric_30m over its part ceiling and the server rejected every
# metric write for an hour. Five-minute resolution is the right trade —
# the finest rollup in this store is five minutes wide.
hostmetrics:
collection_interval: 300s
root_path: /hostfs
scrapers:
cpu:
load:
memory:
paging:
disk:
filesystem:
network:
# [node] ONLY: pod and container each model as their own resource,
# which is one INSERT each.
kubeletstats:
collection_interval: 300s
auth_type: serviceAccount
# The node's IP, not its name — node names are not DNS on DOKS.
endpoint: https://${env:K8S_NODE_IP}:10250
insecure_skip_verify: true
metric_groups: [node]
processors:
memory_limiter:
check_interval: 5s
limit_mib: 800
spike_limit_mib: 160
k8sattributes:
auth_type: serviceAccount
passthrough: false
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.pod.uid
- k8s.deployment.name
- k8s.statefulset.name
- k8s.daemonset.name
- k8s.node.name
- k8s.container.name
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.uid
- sources:
- from: connection
resourcedetection:
detectors: [env, system]
timeout: 2s
override: false
resource:
attributes:
# Both clusters write to ONE store, so the cluster has to be on
# the series or a lux node and a hanzo node are indistinguishable.
# vmagent carried this as -remoteWrite.label=cluster=lux-k8s; this
# is the same fact under its OpenTelemetry name.
- key: k8s.cluster.name
value: lux-k8s
action: upsert
- key: service.namespace
value: lux
action: upsert
- key: deployment.environment
value: production
action: upsert
batch:
timeout: 2s
send_batch_size: 2048
send_batch_max_size: 4096
# Metrics get their OWN batch settings, far coarser than logs.
#
# Every insert into event.metric fans out through the fill_metric_5m
# and fill_metric_30m materialized views, so ONE insert becomes THREE
# parts. At batch/timeout 2s across 42 collectors that is ~60 inserts
# a second and ~180 parts a second, and ClickHouse merges cannot keep
# up: event.metric_30m reached 3,015 parts averaging 3.91 KiB and the
# server began answering code 252 "Too many parts" to EVERY metric
# write — killing app telemetry that had been fine for months, not
# just the new host metrics.
#
# Fewer, larger inserts is the fix. 60s/16384 cuts the part rate by
# ~30x and costs a minute of latency on data whose finest rollup is
# five minutes wide.
batch/metrics:
timeout: 60s
send_batch_size: 8192
send_batch_max_size: 16384
exporters:
# One exporter per signal: cloud binds one socket per signal, and the
# ZAP node identity derives from the component ID, so two exporters
# sharing a name would be two nodes claiming one identity and the
# second would be refused.
zap/logs:
endpoint: 10.124.0.71:4318
retry_on_failure:
enabled: true
zap/traces:
endpoint: 10.124.0.71:4317
retry_on_failure:
enabled: true
zap/metrics:
endpoint: 10.124.0.71:4319
retry_on_failure:
enabled: true
extensions:
health_check:
endpoint: 0.0.0.0:13133
service:
extensions: [health_check]
pipelines:
logs:
receivers: [filelog, otlp]
processors: [memory_limiter, k8sattributes, resourcedetection, resource, batch]
exporters: [zap/logs]
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resourcedetection, resource, batch]
exporters: [zap/traces]
metrics:
receivers: [hostmetrics, kubeletstats]
processors: [memory_limiter, k8sattributes, resourcedetection, resource, batch/metrics]
exporters: [zap/metrics]
telemetry:
logs:
level: info
encoding: json
cd:
automated: false