LCOV - code coverage report
Current view: top level - dashql-app/src/platform/logger - logger.ts Coverage Total Hit
Test: DashQL 915828b Lines: 80.4 % 168 135
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import { LogBuffer, LogLevel, LogRecord, TraceInfo } from "./log_buffer.js";
       2              : import { createChildSpan, injectTraceContext, TraceContext, TRACE_ID_KEY, SPAN_ID_KEY, PARENT_SPAN_ID_KEY } from "./trace_context.js";
       3              : 
       4              : /// A helper for log statistics
       5            1 : export class LogStatistics {
       6              :     /// The max width of the target attribute
       7            1 :     public maxTargetWidth: number;
       8              :     /// The max width of the message attribute
       9            1 :     public maxMessageWidth: number;
      10              : 
      11            1 :     constructor() {
      12            1 :         this.maxTargetWidth = 0;
      13            1 :         this.maxMessageWidth = 0;
      14            1 :     }
      15              : 
      16              :     /// Push a log record
      17            1 :     public push(record: LogRecord) {
      18            1 :         this.maxTargetWidth = Math.max(record.target.length, this.maxTargetWidth);
      19            1 :         this.maxMessageWidth = Math.max(record.message.length, this.maxMessageWidth);
      20            1 :     }
      21            1 : }
      22              : 
      23            1 : export function stringifyError(e: unknown): string {
      24            1 :     if (e instanceof Error) {
      25            1 :         return e.message;
      26              :     }
      27            1 :     if (e && typeof e === 'object') {
      28            1 :         try {
      29            1 :             const message = (e as { message?: unknown }).message;
      30            1 :             if (typeof message === 'string' && message.length > 0) return message;
      31            1 :         } catch {
      32              :             // Fall through when an exotic object's message getter throws.
      33            0 :         }
      34            0 :         try {
      35            0 :             return JSON.stringify(e);
      36            0 :         } catch {
      37            0 :             return Object.prototype.toString.call(e);
      38            0 :         }
      39            1 :     }
      40            0 :     return String(e);
      41            1 : }
      42              : 
      43            1 : const CONTEXT_KEY = "context";
      44              : 
      45              : /// Helper to parse string to number safely
      46            1 : function parseNumberOrUndefined(value: string | null | undefined): number | undefined {
      47            1 :     if (typeof value !== 'string') return undefined;
      48            1 :     const parsed = parseInt(value, 10);
      49            1 :     return isNaN(parsed) ? undefined : parsed;
      50            1 : }
      51              : 
      52              : /// Helper to extract context and trace fields from keyValues
      53            1 : function extractContextAndTraceFields(keyValues: Record<string, string | null | undefined>): {
      54              :     context: string | null;
      55              :     tracing: TraceInfo | null;
      56              :     filteredKeyValues: Record<string, string | null | undefined>;
      57            1 : } {
      58            1 :     const context = typeof keyValues[CONTEXT_KEY] === 'string' ? keyValues[CONTEXT_KEY] : null;
      59            1 :     const traceId = parseNumberOrUndefined(keyValues[TRACE_ID_KEY]);
      60            1 :     const spanId = parseNumberOrUndefined(keyValues[SPAN_ID_KEY]);
      61            1 :     const parentSpanId = parseNumberOrUndefined(keyValues[PARENT_SPAN_ID_KEY]);
      62              : 
      63              :     // Create trace info if we have at least traceId and spanId
      64            1 :     const tracing: TraceInfo | null = (traceId !== undefined && spanId !== undefined) ? {
      65            1 :         traceId,
      66            1 :         spanId,
      67            1 :         parentSpanId: parentSpanId ?? null,
      68            1 :     } : null;
      69              : 
      70              :     // Create a new object without context and trace fields
      71            1 :     const filteredKeyValues: Record<string, string | null | undefined> = {};
      72            1 :     for (const [key, value] of Object.entries(keyValues)) {
      73            1 :         if (key !== CONTEXT_KEY && key !== TRACE_ID_KEY && key !== SPAN_ID_KEY && key !== PARENT_SPAN_ID_KEY) {
      74            1 :             filteredKeyValues[key] = value;
      75              :         }
      76              :     }
      77              : 
      78            1 :     return { context, tracing, filteredKeyValues };
      79            1 : }
      80              : 
      81              : /// Structural logger interface satisfied by both Logger and TracedLogger.
      82              : /// Functions that just emit logs (and don't touch buffer/statistics/destroy)
      83              : /// should accept this type so either a raw or traced logger can be passed in.
      84              : export interface LoggerLike {
      85              :     trace(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void;
      86              :     debug(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void;
      87              :     info(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void;
      88              :     warn(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void;
      89              :     error(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void;
      90              :     exception(error: any): void;
      91              : }
      92              : 
      93            1 : export class LoggableException extends Error {
      94              :     /// The keyValues
      95            1 :     keyValues: Record<string, string | null | undefined>;
      96              :     /// The target
      97            1 :     target?: string;
      98              : 
      99            1 :     constructor(message: string, keyValues: Record<string, string | null | undefined> = {}, target?: string) {
     100            1 :         super(message);
     101            1 :         this.keyValues = keyValues;
     102            1 :         this.target = target;
     103            1 :     }
     104            1 : }
     105              : 
     106            1 : function buildRecord(level: LogLevel, message: string, target: string | undefined, keyValues: Record<string, string | null | undefined>): LogRecord {
     107            1 :     const { context, tracing, filteredKeyValues } = extractContextAndTraceFields(keyValues);
     108            1 :     return {
     109            1 :         timestamp: Date.now(),
     110            1 :         level,
     111            1 :         target: target ?? "pwa:unknown",
     112            1 :         message,
     113            1 :         context,
     114            1 :         tracing,
     115            1 :         keyValues: filteredKeyValues,
     116            1 :     };
     117            1 : }
     118              : 
     119              : /// A platform logger
     120            1 : export abstract class Logger {
     121              :     /// The pending log messages
     122            1 :     protected pendingRecords: LogRecord[];
     123              :     /// The output log buffer.
     124              :     /// To be populated by `flushPendingRecords`
     125            1 :     protected outputBuffer: LogBuffer;
     126              :     /// The log statistics
     127            1 :     protected logStatistics: LogStatistics;
     128              : 
     129            1 :     constructor() {
     130            1 :         this.pendingRecords = [];
     131            1 :         this.outputBuffer = new LogBuffer();
     132            1 :         this.logStatistics = new LogStatistics();
     133            1 :     }
     134              : 
     135              :     /// Destroy the logger
     136              :     public abstract destroy(): void;
     137              :     /// Helper to flush pending records
     138              :     protected abstract flushPendingRecords(): void;
     139              :     /// Access the log buffer
     140            1 :     public get buffer() { return this.outputBuffer; }
     141              :     /// Access the log statistics
     142            0 :     public get statistics() { return this.logStatistics; }
     143              : 
     144              :     /// Bind a trace context and return a logger that tags every record with it
     145            1 :     public withTrace(ctx: TraceContext): TracedLogger {
     146            1 :         return new TracedLogger(this, ctx);
     147            1 :     }
     148              : 
     149              :     /// Push a log record
     150            1 :     public push(entry: LogRecord): void {
     151            1 :         this.pendingRecords.push(entry);
     152            1 :         this.logStatistics.push(entry);
     153            1 :         this.flushPendingRecords();
     154            1 :     }
     155              :     /// Log a trace message
     156            0 :     public trace(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     157            0 :         const entry = buildRecord(LogLevel.Trace, message, target, keyValues);
     158            0 :         this.pendingRecords.push(entry);
     159            0 :         this.logStatistics.push(entry);
     160            0 :         this.flushPendingRecords();
     161            0 :     }
     162              :     /// Log an debug message
     163            1 :     public debug(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     164            1 :         const entry = buildRecord(LogLevel.Debug, message, target, keyValues);
     165            1 :         this.pendingRecords.push(entry);
     166            1 :         this.logStatistics.push(entry);
     167            1 :         this.flushPendingRecords();
     168            1 :     }
     169              :     /// Log an info message
     170            1 :     public info(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     171            1 :         const entry = buildRecord(LogLevel.Info, message, target, keyValues);
     172            1 :         this.pendingRecords.push(entry);
     173            1 :         this.logStatistics.push(entry);
     174            1 :         this.flushPendingRecords();
     175            1 :     }
     176              :     /// Log a warning message
     177            1 :     public warn(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     178            1 :         const entry = buildRecord(LogLevel.Warn, message, target, keyValues);
     179            1 :         this.pendingRecords.push(entry);
     180            1 :         this.logStatistics.push(entry);
     181            1 :         this.flushPendingRecords();
     182            1 :     }
     183              :     /// Log an error message
     184            1 :     public error(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     185            1 :         const entry = buildRecord(LogLevel.Error, message, target, keyValues);
     186            1 :         this.pendingRecords.push(entry);
     187            1 :         this.logStatistics.push(entry);
     188            1 :         this.flushPendingRecords();
     189            1 :     }
     190              :     /// Log an exception
     191            0 :     public exception(error: any) {
     192            0 :         if (error instanceof LoggableException) {
     193            0 :             this.error(error.message, error.keyValues, error.target);
     194              :         } else {
     195            0 :             this.error(stringifyError(error), {});
     196              :         }
     197            0 :     }
     198            1 : }
     199              : 
     200              : /// A logger view that tags every record with a fixed trace context
     201            1 : export class TracedLogger {
     202            1 :     constructor(
     203            1 :         private readonly base: Logger,
     204            1 :         public readonly context: TraceContext,
     205            1 :     ) { }
     206              : 
     207              :     /// The underlying unwrapped logger (for callees that don't carry a trace)
     208            0 :     public get logger(): Logger { return this.base; }
     209              :     /// Access the log buffer
     210            0 :     public get buffer() { return this.base.buffer; }
     211              :     /// Access the log statistics
     212            0 :     public get statistics() { return this.base.statistics; }
     213              : 
     214              :     /// Derive a child logger with a new span under the same trace
     215            1 :     public childSpan(): TracedLogger {
     216            1 :         return new TracedLogger(this.base, createChildSpan(this.context));
     217            1 :     }
     218              :     /// Bind a different trace context
     219            0 :     public withTrace(ctx: TraceContext): TracedLogger {
     220            0 :         return new TracedLogger(this.base, ctx);
     221            0 :     }
     222              : 
     223            1 :     private tag(keyValues: Record<string, string | null | undefined>): Record<string, string | null | undefined> {
     224            1 :         const tagged = { ...keyValues };
     225            1 :         injectTraceContext(tagged, this.context);
     226            1 :         return tagged;
     227            1 :     }
     228              : 
     229            0 :     public trace(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     230            0 :         this.base.trace(message, this.tag(keyValues), target);
     231            0 :     }
     232            1 :     public debug(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     233            1 :         this.base.debug(message, this.tag(keyValues), target);
     234            1 :     }
     235            1 :     public info(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     236            1 :         this.base.info(message, this.tag(keyValues), target);
     237            1 :     }
     238            1 :     public warn(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     239            1 :         this.base.warn(message, this.tag(keyValues), target);
     240            1 :     }
     241            1 :     public error(message: string, keyValues: Record<string, string | null | undefined>, target?: string): void {
     242            1 :         this.base.error(message, this.tag(keyValues), target);
     243            1 :     }
     244            0 :     public exception(error: any) {
     245            0 :         if (error instanceof LoggableException) {
     246            0 :             this.error(error.message, error.keyValues, error.target);
     247              :         } else {
     248            0 :             this.error(stringifyError(error), {});
     249              :         }
     250            0 :     }
     251            1 : }
        

Generated by: LCOV version 2.0-1