c314ba0902
ROADMAP.md establishes status legend, architectural anchors pointing at the wiki, and seven non-negotiable design rules — most importantly the core/domain boundary that protects Phase 1 from Phase 2 churn, the schema-authority split (positions hypertable owned here; everything else owned by Directus), and idempotent-writes via (device_id, ts) ON CONFLICT. Phase 1 (throughput pipeline) is fully detailed across 11 task files: scaffold, core types + sentinel decoder, config + logging, Postgres hypertable, Redis Stream consumer, per-device LRU state, batched writer, main wiring, observability, integration test, Dockerfile + Gitea CI. Observability is in Phase 1 (not deferred) — lesson learned from tcp-ingestion task 1.10. Phases 2-4 are stub READMEs. Phase 2 (domain logic) blocks on Directus schema decisions and lists those open questions explicitly. Phase 3 (production hardening) and Phase 4 (future) sketch the task shape.
4.3 KiB
4.3 KiB
Task 1.3 — Configuration & logging
Phase: 1 — Throughput pipeline
Status: ⬜ Not started
Depends on: 1.1
Wiki refs: docs/wiki/entities/processor.md
Goal
Validate environment variables on startup with zod, build the pino root logger with the same conventions as tcp-ingestion (ISO timestamps, string level labels, instance_id base field), and fail fast with a readable error message if config is invalid.
Deliverables
src/config/load.tsexporting:loadConfig(): Config— readsprocess.env, runs zod parse, returns a typedConfig. Throws on invalid input with a multi-line message that names every invalid field.Configtype derived from the zod schema.
src/observability/logger.tsexporting:createLogger({ level, nodeEnv, instanceId }): Logger— pino root logger with base fieldsservice: 'processor',instance_id. ISO timestamps viapino.stdTimeFunctions.isoTime. Level formatter that emits"level":"info"not"level":30. InnodeEnv === 'development', use the pino-pretty transport.type Loggerre-exported frompino.
- Wire both into
src/main.ts:loadConfig()→createLogger()→logger.info('processor starting')→ exit 0 (still a stub; consumer wiring lands in 1.8).
Specification
Environment variables
| Var | Required | Default | Notes |
|---|---|---|---|
NODE_ENV |
no | production |
development enables pino-pretty |
INSTANCE_ID |
no | processor-1 |
Used in metrics + log base field |
LOG_LEVEL |
no | info |
trace / debug / info / warn / error |
REDIS_URL |
yes | — | e.g. redis://redis:6379 |
POSTGRES_URL |
yes | — | e.g. postgres://user:pass@db:5432/trm |
REDIS_TELEMETRY_STREAM |
no | telemetry:t |
Must match tcp-ingestion's REDIS_TELEMETRY_STREAM |
REDIS_CONSUMER_GROUP |
no | processor |
All Processor instances join this group |
REDIS_CONSUMER_NAME |
no | ${INSTANCE_ID} |
Unique per instance — defaults to instance id |
METRICS_PORT |
no | 9090 |
HTTP server port for /metrics, /healthz, /readyz |
BATCH_SIZE |
no | 100 |
Max records per XREADGROUP call |
BATCH_BLOCK_MS |
no | 5000 |
BLOCK timeout on XREADGROUP when stream is empty |
WRITE_BATCH_SIZE |
no | 50 |
Max rows per Postgres INSERT |
DEVICE_STATE_LRU_CAP |
no | 10000 |
Max devices kept in memory; LRU eviction beyond this |
Validation rules
- All defaults must be expressed in the zod schema with
.default(...)so the parsedConfigis fully typed and never hasundefinedfor an optional field. - Numeric env vars must be coerced from string and bounded:
BATCH_SIZE1–10000,BATCH_BLOCK_MS0–60000,WRITE_BATCH_SIZE1–1000,DEVICE_STATE_LRU_CAP100–1_000_000. REDIS_URLandPOSTGRES_URLmust parse as URLs with the expected protocol (redis:orrediss:;postgres:orpostgresql:).LOG_LEVELmust be one of pino's accepted levels.
Logger conventions
Match tcp-ingestion/src/observability/logger.ts line for line where applicable. Future-you grepping across services should see the same shape:
const formatters = { level: (label: string) => ({ level: label }) };
if (nodeEnv === 'development') {
return pino({ level, base, timestamp: pino.stdTimeFunctions.isoTime, formatters,
transport: { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } } });
}
return pino({ level, base, timestamp: pino.stdTimeFunctions.isoTime, formatters });
Acceptance criteria
pnpm testcovers config validation: missing required vars throw with the right message; invalid URLs throw; bounded numerics throw on out-of-range values.- Running with valid env emits a single
processor startinginfo log withservice=processorandinstance_id=processor-1base fields. - Running with
NODE_ENV=developmentproduces colorized output via pino-pretty. - Running with
NODE_ENV=productionproduces JSON output with ISOtimeand stringlevel.
Risks / open questions
REDIS_CONSUMER_NAMEdefaulting toINSTANCE_IDmeansINSTANCE_IDmust be unique per instance for safe consumer-group operation. Document this in.env.exampleso operators don't accidentally run two instances with the sameINSTANCE_ID.
Done
(Fill in once complete: commit SHA, brief notes.)