The header still claimed the ZAP wire was zap-proto/http. That claim is what
sent two days of debugging into version bumps of a library the receiver does not
speak at any version, and 65dd0de corrected CLAUDE.md and the code but left this
file saying it. A doc that names the wrong protocol is not stale, it is a bug
with a long fuse — so it is fixed here and the reason is written down.
Adds the two rules whose violation cost the fleet its telemetry this week:
- a ZAP NodeID must be unique per PROCESS, because luxzap admits exactly one
connection per identity and refuses the rest with a bare EOF
- span kind goes on the wire lowercase, or every kind silently reads internal
Both are things the next person cannot infer from the code they will be looking
at, and both fail without an error.
Co-authored-by: Hanzo Dev <[email protected]>
3.5 KiB
otel-collector
Hanzo O11y Otel Collector
Hanzo OpenTelemetry Collector distro. OTLP in → ZAP out → hanzoai/datastore
(ClickHouse). Adds receiver/zapreceiver + exporter/zapexporter over upstream.
The wire is a luxfi/zap envelope — NOT zap-proto/http
This line used to say zap-proto/http, and that was wrong. The o11y receivers
decode a JSON batch inside a luxfi/zap envelope, dispatched on a MsgType in
the upper 8 bits of the flags field (spans=1, metrics=2, logs=3). Those are
different protocols, so nothing arrived and nothing errored — the receiver's zap
node simply never saw a type it recognised and the handler never ran. Two
zap-proto version bumps were spent chasing it before the docs were suspected.
It is corrected here because the doc WAS the bug the second time around.
One ZAP identity per sender — do not derive it from config
luxzap keys its connection table by peer NodeID and admits exactly ONE
connection per identity; a duplicate is refused by closing before the handshake
reply, which the dialer reports as a bare EOF.
So a NodeID derived from anything that is the same in every copy of a config —
the exporter's component ID, most obviously — silently caps the whole fleet at
one connected sender. Measured 2026-08-01: 25 otel-agent pods all claiming
otel-agent-zap, one connected, 24 refused, ~88% of the fleet's logs discarded
while every pod reported Healthy and the errors read like a network fault.
zapexporter.nodeID() now derives it from hostname + component ID and there
is deliberately no config knob, since a knob is just a way to set one value on
every node. If you add another ZAP sender anywhere, give it a per-process
identity or it will take the fleet's connection from the agents.
Span kind goes on the wire LOWERCASE
pdata renders SpanKind title-cased ("Server"); the receiver matches server
or SPAN_KIND_SERVER and defaults everything else to internal. Sent
title-cased, every span of every kind stored as internal — no error, trace still
renders, only the entry/exit distinction quietly gone. translateSpan lowercases;
TestSpanKindIsTheReceiversVocabulary keeps it that way.
CI = root hanzo.yml + .github/workflows/cicd.yml importing hanzoai/ci
hanzo.yml(root, NO leading dot) declaresimages:/test:/deploy:/kms:..github/workflows/cicd.ymlis ~7 lines:uses: hanzoai/ci/.github/workflows/build.yml@main+secrets: inherit.- Runs on our self-hosted arc pool (
hanzo-build-linux-amd64) — no GitHub-hosted minutes. GHCR push = automatic workflow token; KMS = deployKUBECONFIGonly. - Two self-building images (multi-stage, compile in-image, no CI pre-build):
collector→cmd/o11yotelcollector/Dockerfile.selfbuild→ghcr.io/hanzoai/otel-collector:zap-native-latest+:sha-<short>-amd64-zap-native.schema-migrator→cmd/o11yschemamigrator/Dockerfile.selfbuild→ghcr.io/hanzoai/o11y-schema-migrator:latest+:sha-<short>-amd64.
- The
zap-nativetag = the collector build that includes zapreceiver + zapexporter. Deploy manifests (config ConfigMap with thezapreceiver on :4319 + theotel-agentlog DaemonSet that ships over ZAP) live in hanzoai/universeinfra/k8s/monitoring+infra/k8s/o11y.
This is the going-forward pattern for ALL native-PaaS repos. Onboard = add
hanzo.yml + cicd.yml; nothing else. The legacy hanzoai/.github
docker-build.yml and o11y/primus.workflows reusables were removed (broken:
runtime-validation jobs: 0, and primus is inaccessible).