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 : }
|