Stage discovered the wrong default at runtime: tcp-ingestion's compiled default REDIS_TELEMETRY_STREAM is 'telemetry:teltonika' but processor's was 'telemetry:t', so the two services were talking past each other — tcp-ingestion publishing to one stream, processor reading another empty one. The deploy stack now pins both to the same value via a shared env var, but the processor's compiled default should also match so local development and the integration test stay aligned with reality. Changes: - src/config/load.ts — default changed to 'telemetry:teltonika' - .env.example — same - test/config.test.ts — default-value assertion updated - planning docs (ROADMAP, phase-1 README, tasks 03/08/10, phase-3 README) — occurrences of 'telemetry:t' replaced with 'telemetry:teltonika' The deploy stack remains the single source of truth via the shared REDIS_TELEMETRY_STREAM env var. Compiled defaults are belt-and-braces.
4.9 KiB
Task 1.3 — Configuration & logging
Phase: 1 — Throughput pipeline
Status: 🟩 Done
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:teltonika |
Must match tcp-ingestion's REDIS_TELEMETRY_STREAM. Pinned via the deploy-stack shared env var so the two services cannot drift from each other. |
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
Landed in 290a08e. Implemented src/config/load.ts (zod schema, loadConfig) and src/observability/logger.ts (createLogger, pino-pretty in dev). 37 config test cases covering all defaults, missing required vars, URL protocol validation, and bounded numeric checks. Wired into src/main.ts. Divergence from tcp-ingestion config: INSTANCE_ID defaults to a fixed 'processor-1' string rather than a random UUID prefix; rationale: operator-visible name is more useful than randomness in a containerised environment where the instance name can be set deterministically.