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

            Line data    Source code
       1            1 : export enum LogLevel {
       2            1 :     Trace = 1,
       3            1 :     Debug = 2,
       4            1 :     Info = 3,
       5            1 :     Warn = 4,
       6            1 :     Error = 5,
       7            1 : }
       8              : 
       9            1 : export function getLogLevelName(level: LogLevel): string {
      10            1 :     switch (level) {
      11            1 :         case LogLevel.Trace: return "trace";
      12            1 :         case LogLevel.Debug: return "debug";
      13            1 :         case LogLevel.Info: return "info";
      14            1 :         case LogLevel.Warn: return "warn";
      15            1 :         case LogLevel.Error: return "error";
      16            1 :     }
      17            1 : }
      18              : 
      19            1 : export function parseLogLevel(text: string): LogLevel | null {
      20            1 :     switch (text) {
      21            1 :         case "trace": return LogLevel.Trace;
      22            1 :         case "debug": return LogLevel.Debug;
      23            1 :         case "info": return LogLevel.Info;
      24            1 :         case "warn": return LogLevel.Warn;
      25            1 :         case "error": return LogLevel.Error;
      26            1 :         default:
      27            1 :             return null;
      28            1 :     }
      29            1 : }
      30              : 
      31              : /// Trace information for a log record
      32              : export interface TraceInfo {
      33              :     /// The trace ID
      34              :     traceId: number;
      35              :     /// The span ID
      36              :     spanId: number;
      37              :     /// The parent span ID (null if no parent)
      38              :     parentSpanId: number | null;
      39              : }
      40              : 
      41              : /// A log record
      42              : export interface LogRecord {
      43              :     /// The timestamp
      44              :     timestamp: number;
      45              :     /// The log level
      46              :     level: LogLevel;
      47              :     /// The target
      48              :     target: string;
      49              :     /// The message
      50              :     message: string;
      51              :     /// Context information (if present)
      52              :     context: string | null;
      53              :     /// Tracing information (if present)
      54              :     tracing: TraceInfo | null;
      55              :     /// The log details
      56              :     keyValues: Record<string, string | null | undefined>;
      57              : }
      58              : 
      59            1 : const TARGET_CHUNK_SIZE = 1024;
      60              : 
      61            1 : class FrozenLogChunk {
      62              :     /// The entries of the chunk
      63            1 :     readonly entries: LogRecord[];
      64              :     /// Minimum trace ID in this chunk (undefined if no traces)
      65            1 :     readonly minTraceId: number | null;
      66              :     /// Maximum trace ID in this chunk (undefined if no traces)
      67            1 :     readonly maxTraceId: number | null;
      68              : 
      69            1 :     constructor(entries: LogRecord[]) {
      70            1 :         this.entries = entries;
      71              : 
      72              :         // Compute min and max trace IDs
      73            1 :         let min: number | null = null;
      74            1 :         let max: number | null = null;
      75              : 
      76            1 :         for (const entry of entries) {
      77            1 :             if (entry.tracing?.traceId !== undefined) {
      78            1 :                 const traceId = entry.tracing.traceId;
      79            1 :                 if (min === null || traceId < min) {
      80            1 :                     min = traceId;
      81              :                 }
      82            1 :                 if (max === null || traceId > max) {
      83            1 :                     max = traceId;
      84              :                 }
      85            1 :             }
      86              :         }
      87              : 
      88            1 :         this.minTraceId = min;
      89            1 :         this.maxTraceId = max;
      90            1 :     }
      91            1 : }
      92              : 
      93              : type LogObserver = (buffer: LogBuffer) => void;
      94              : type LogRecordObserver = (record: LogRecord) => void;
      95              : 
      96            1 : export class LogBuffer {
      97              :     /// Internal version counter
      98            1 :     protected version_: number;
      99              :     /// The last entries
     100            1 :     protected lastEntries_: LogRecord[];
     101              :     /// The frozen chunks
     102            1 :     protected frozenChunks_: FrozenLogChunk[];
     103              :     /// The log observers
     104            1 :     protected logObservers: Set<LogObserver>;
     105              :     /// The trace record observers (map from trace ID to set of observers)
     106            1 :     protected traceObservers: Map<number, Set<LogRecordObserver>>;
     107              :     /// The minimum log level
     108            1 :     protected minLogLevel: LogLevel;
     109              : 
     110            1 :     constructor() {
     111            1 :         this.version_ = 1;
     112            1 :         this.lastEntries_ = [];
     113            1 :         this.frozenChunks_ = [];
     114            1 :         this.logObservers = new Set();
     115            1 :         this.traceObservers = new Map();
     116            1 :         this.minLogLevel = LogLevel.Info;
     117            1 :     }
     118              : 
     119              :     /// Get the current version
     120            1 :     public get version(): number { return this.version_; }
     121              :     /// Get the total amount of log entries
     122            1 :     public get length(): number { return this.lastEntries_.length + this.frozenChunks_.length * TARGET_CHUNK_SIZE; }
     123              :     /// Get the observers
     124            1 :     public get observers(): Set<LogObserver> { return this.logObservers; }
     125              : 
     126              :     /// Set the minimum log level
     127            1 :     public setMinLogLevel(level: LogLevel) {
     128            1 :         this.minLogLevel = level;
     129            1 :     }
     130              : 
     131              :     /// Get the minimum log level
     132            1 :     public getMinLogLevel(): LogLevel {
     133            1 :         return this.minLogLevel;
     134            1 :     }
     135              : 
     136              :     /// Subscribe to log events
     137            1 :     public subscribe(observer: LogObserver, callWhenRegistering: boolean = false) {
     138            1 :         this.logObservers.add(observer);
     139            1 :         if (callWhenRegistering) {
     140            1 :             observer(this);
     141              :         }
     142            1 :     }
     143              : 
     144              :     /// Subscribe to trace log events for a specific trace ID
     145            1 :     public subscribeTrace(traceId: number, observer: LogRecordObserver) {
     146            1 :         let observers = this.traceObservers.get(traceId);
     147            1 :         if (!observers) {
     148            1 :             observers = new Set();
     149            1 :             this.traceObservers.set(traceId, observers);
     150            1 :         }
     151            1 :         observers.add(observer);
     152            1 :     }
     153              : 
     154              :     /// Unsubscribe from trace log events
     155            1 :     public unsubscribeTrace(traceId: number, observer: LogRecordObserver) {
     156            1 :         const observers = this.traceObservers.get(traceId);
     157            1 :         if (observers) {
     158            1 :             observers.delete(observer);
     159            1 :             if (observers.size === 0) {
     160            1 :                 this.traceObservers.delete(traceId);
     161              :             }
     162            1 :         }
     163            1 :     }
     164              : 
     165              :     /// Push an entry
     166            1 :     public push(entry: LogRecord) {
     167              :         // Ignore the entry
     168            1 :         if (entry.level < this.minLogLevel) {
     169            1 :             return;
     170              :         }
     171              : 
     172              :         // Buffer entries before compacting
     173            1 :         this.lastEntries_.push(entry);
     174              :         // Freeze full chunks, at() and length expect exactly TARGET_CHUNK_SIZE entries per chunk
     175            1 :         if (this.lastEntries_.length >= TARGET_CHUNK_SIZE) {
     176            1 :             this.frozenChunks_.push(new FrozenLogChunk(this.lastEntries_));
     177            1 :             this.lastEntries_ = [];
     178            1 :         }
     179              :         // Bump the version
     180            1 :         this.version_ += 1;
     181              :         // Notify all observers
     182            1 :         for (const observer of this.logObservers) {
     183            1 :             observer(this);
     184              :         }
     185              :         // Notify trace observers for this specific trace ID
     186            1 :         if (entry.tracing) {
     187            1 :             const observers = this.traceObservers.get(entry.tracing.traceId);
     188            1 :             if (observers) {
     189            1 :                 for (const observer of observers) {
     190            1 :                     observer(entry);
     191              :                 }
     192              :             }
     193            1 :         }
     194            1 :     }
     195              : 
     196              :     /// Get at position
     197            1 :     public at(index: number): LogRecord | null {
     198            1 :         const frozenEntries = this.frozenChunks_.length * TARGET_CHUNK_SIZE
     199            1 :         if (index < frozenEntries) {
     200            1 :             const chunkIndex = Math.floor(index / TARGET_CHUNK_SIZE);
     201            1 :             const indexInChunk = index - (chunkIndex * TARGET_CHUNK_SIZE);
     202            1 :             if (chunkIndex >= this.frozenChunks_.length) {
     203            1 :                 return null;
     204              :             }
     205            1 :             const chunk = this.frozenChunks_[chunkIndex];
     206            1 :             if (indexInChunk >= chunk.entries.length) {
     207            1 :                 return null;
     208              :             }
     209            1 :             return chunk.entries[indexInChunk];
     210            1 :         }
     211            1 :         const pendingIndex = index - frozenEntries;
     212            1 :         if (pendingIndex < this.lastEntries_.length) {
     213            1 :             return this.lastEntries_[pendingIndex];
     214              :         }
     215            1 :         return null;
     216            1 :     }
     217              : 
     218              :     /// Collect all log records for a specific trace ID
     219            1 :     public collectTraceLogs(traceId: number): LogRecord[] {
     220            1 :         const results: LogRecord[] = [];
     221              : 
     222              :         // Scan frozen chunks, using min/max trace IDs to skip irrelevant chunks
     223            1 :         for (const chunk of this.frozenChunks_) {
     224              :             // Skip chunk if trace ID is outside its range
     225            1 :             if (chunk.minTraceId !== null && chunk.maxTraceId !== null) {
     226            1 :                 if (traceId < chunk.minTraceId || traceId > chunk.maxTraceId) {
     227            1 :                     continue;
     228              :                 }
     229              :             }
     230              : 
     231              :             // Scan entries in this chunk
     232            1 :             for (const entry of chunk.entries) {
     233            1 :                 if (entry.tracing?.traceId === traceId) {
     234            1 :                     results.push(entry);
     235              :                 }
     236              :             }
     237            1 :         }
     238              : 
     239              :         // Scan pending entries
     240            1 :         for (const entry of this.lastEntries_) {
     241            1 :             if (entry.tracing?.traceId === traceId) {
     242            1 :                 results.push(entry);
     243              :             }
     244              :         }
     245              : 
     246            1 :         return results;
     247            1 :     }
     248            1 : }
        

Generated by: LCOV version 2.0-1