Implement Phase 1 tasks 1.1-1.4 (scaffold + core types + config + Postgres)
Scaffold mirrors tcp-ingestion conventions: ESM, strict TS, pnpm, vitest
with unit/integration split, ESLint flat config with no-floating-promises
+ no-misused-promises + import/no-restricted-paths (the new src/core/ →
src/domain/ boundary that protects Phase 1 from Phase 2 churn).
Core types in src/core/types.ts (Position, StreamRecord, DeviceState,
Metrics, AttributeValue) — Position is byte-equivalent to tcp-ingestion's
output. Codec in src/core/codec.ts implements sentinel reversal:
{__bigint:"..."} → bigint, {__buffer_b64:"..."} → Buffer, ISO timestamp
string → Date. CodecError surfaces malformed payload reasons with the
failing field named.
Config in src/config/load.ts (zod schema, all 13 env vars with defaults
and bounded numerics). Logger in src/observability/logger.ts matches
tcp-ingestion exactly: ISO timestamps, string level labels, pino-pretty
in development.
Postgres in src/db/: createPool with sane defaults and application_name,
connectWithRetry mirroring the ioredis retry pattern, a 30-line
migration runner using a schema_migrations table, and 0001_positions.sql
with the hypertable + (device_id, ts) unique index + ts DESC index.
Migration runner unit-tested against a mocked pg.Pool; the real
TimescaleDB round-trip is deferred to task 1.10 per spec.
Verification: typecheck, lint, build all clean; 73 unit tests passing
across 4 files. import/no-restricted-paths verified live by temporarily
adding a forbidden src/domain/ import.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import pino from 'pino';
|
||||
import type { Logger } from 'pino';
|
||||
|
||||
export type { Logger };
|
||||
|
||||
/**
|
||||
* Builds the root pino logger. Called once at startup with config values.
|
||||
*
|
||||
* In development, pino-pretty is used for human-readable output (lazy transport
|
||||
* so it is never required in production paths). In test/production, raw JSON is
|
||||
* emitted — fast and parseable by log aggregators (Portainer, Loki, etc.).
|
||||
*/
|
||||
export function createLogger(options: {
|
||||
level: string;
|
||||
nodeEnv: string;
|
||||
instanceId: string;
|
||||
}): Logger {
|
||||
const { level, nodeEnv, instanceId } = options;
|
||||
|
||||
const base = {
|
||||
service: 'processor',
|
||||
instance_id: instanceId,
|
||||
};
|
||||
|
||||
// Emit `"level":"info"` instead of pino's default numeric `"level":30` so
|
||||
// log viewers show a human-readable label rather than the numeric level.
|
||||
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',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Production and test: plain JSON — fast, no extra deps.
|
||||
// ISO-8601 string timestamps (vs default epoch-ms) survive downstream
|
||||
// log renderers without losing precision.
|
||||
return pino({ level, base, timestamp: pino.stdTimeFunctions.isoTime, formatters });
|
||||
}
|
||||
Reference in New Issue
Block a user