collector
OTLP/HTTP receiver plus normalizer. collector.py takes POST /v1/logs|metrics|traces (JSON or protobuf, optional gzip), redacts content, hashes identity and writes one raw record per request. normalize.py turns raw logs/traces into events (source otel) at <data dir>/events/otel.jsonl. Rules and worked examples: METHOD.md (N-* IDs).
harness --OTLP--> collector.py --sink--> <data>/otel/<signal>-<UTC day>.jsonl --normalize.py--> <data>/events/otel.jsonl
Run
python3 02-measure/collector/collector.py # 127.0.0.1:6979 (OTM_PORT or config collector.port)
OTM_PORT=7861 python3 02-measure/collector/collector.py --out /tmp/otm # scratch instance, other data root
python3 02-measure/collector/normalize.py --days 7 --harness codex # --in RAW_DIR, --out EVENTS_DIR
curl 127.0.0.1:6979/health # {"ok", "protobuf", "auth", "sink"}
Point a harness at it with OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:6979. Responses: 200 {}, 400 bad body, 401 missing bearer, 413 over MAX_BODY, 500 sink error.
Tests: python3 -m unittest discover -s 02-measure/tests -p test_collector.py. Check output: python3 02-measure/schemas/check.py <file> raw|event.
Reconfigure
01-deploy/config.json -> collector: sink (connector name), bind, port (null = OTM_PORT or 6979), token_env (env var holding a bearer token; null = no auth), hash_identity (bool).
Registry: SINKS in connectors.py, name -> write(record: dict, cfg: dict) -> None; raise to answer 500. Existing: files (default), stdout, forward_otlp (re-sends redacted OTLP JSON to $OTM_FORWARD_URL).
Worked example, print records instead of writing files: set "collector": {"sink": "stdout"} and run python3 02-measure/collector/collector.py > raw.jsonl. Adding a sink foo: write def foo(record, cfg) in connectors.py and add "foo": foo to SINKS.
Pitfalls: every stage has a connectors.py; collector.py loads its siblings by file path (_load), so do the same in tests. Bodies that cannot be decoded are dropped and summarized as undecoded (N-undecoded); KEEP_RAW_UNDECODED opts into unredacted data_b64. Claude Code's ~/.claude/settings.json env overrides process env, so a per-run OTEL_EXPORTER_OTLP_ENDPOINT is ignored when the settings set one.
Adapt, trade-offs, decisions for the human
- Adapt: bind address, port, auth (
token_env) and sink to where the data should live (01-deploy/); aforward_otlpsink or an existing OTel backend can replace the file store. - Trade-offs: stdlib only, a single process, files per UTC day. Simple to read and move, but no retention, rotation or HA. Redaction and identity hashing happen at ingest, so raw content is never stored and cannot be recovered later.
- Decisions for the human: where it runs and who can reach it; whether to hash identity (
hash_identity); whether to keep undecodable bodies (KEEP_RAW_UNDECODED, unredacted).