From 477fabfef8747ba189a02071ac1bde8fee57451b Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Thu, 30 Apr 2026 19:30:15 +0200 Subject: [PATCH] Sharpen pilot logging: ISO timestamps, level labels, transport-error classification, per-frame info - Emit ISO-8601 timestamps and string level labels (info/warn/...) so Portainer's log viewer renders seconds and human-readable levels. - Classify ETIMEDOUT/ECONNRESET/EPIPE/ENOTCONN as info one-liners rather than warns with stack traces. These are routine on cellular. - Add an info "frame ingested" line per accepted AVL frame so device activity is visible at info level until task 1.10 wires up prom-client. --- src/adapters/teltonika/index.ts | 20 ++++++++++++++++++++ src/observability/logger.ts | 15 +++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/adapters/teltonika/index.ts b/src/adapters/teltonika/index.ts index ecb74c3..faf3dc7 100644 --- a/src/adapters/teltonika/index.ts +++ b/src/adapters/teltonika/index.ts @@ -134,6 +134,19 @@ export function createTeltonikaAdapter(options: TeltonikaAdapterOptions): Adapte 'malformed frame; dropping connection', ); } + } else if ( + err instanceof Error && + 'code' in err && + // Routine on cellular: NAT timeouts, carrier RST, half-closed pipes. + // Surface as info (one-liner, no stack) so warns mean something. + ['ETIMEDOUT', 'ECONNRESET', 'EPIPE', 'ENOTCONN'].includes( + (err as NodeJS.ErrnoException).code as string, + ) + ) { + sessionLogger.info( + { code: (err as NodeJS.ErrnoException).code }, + 'session ended (transport error)', + ); } else { sessionLogger.warn({ err }, 'unexpected error reading frame; dropping connection'); } @@ -204,6 +217,13 @@ export function createTeltonikaAdapter(options: TeltonikaAdapterOptions): Adapte result: 'ok', }); + // Pilot-stage visibility into per-frame ingest. Downgrade to debug + // once task 1.10 wires up prom-client and frames_total is scrapeable. + sessionLogger.info( + { codec: codecLabel, records: result.recordCount }, + 'frame ingested', + ); + // ACK: 4-byte big-endian record count const ack = Buffer.alloc(4); ack.writeUInt32BE(result.recordCount, 0); diff --git a/src/observability/logger.ts b/src/observability/logger.ts index 5809e64..a9b654e 100644 --- a/src/observability/logger.ts +++ b/src/observability/logger.ts @@ -22,10 +22,19 @@ export function createLogger(options: { instance_id: instanceId, }; + // Emit `"level":"info"` instead of pino's default `"level":30` so log + // viewers (Portainer, etc.) 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: { @@ -37,6 +46,8 @@ export function createLogger(options: { }); } - // Production and test: plain JSON — fast, no extra deps - return pino({ level, base }); + // Production and test: plain JSON — fast, no extra deps. + // ISO-8601 string timestamps (vs default epoch-ms) survive downstream + // log renderers (Portainer, Docker --timestamps) without losing seconds. + return pino({ level, base, timestamp: pino.stdTimeFunctions.isoTime, formatters }); }