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