-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathconsola.ts
More file actions
440 lines (391 loc) · 13.2 KB
/
consola.ts
File metadata and controls
440 lines (391 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
import type { Client } from '../client';
import { getClient } from '../currentScopes';
import { _INTERNAL_captureLog } from '../logs/internal';
import { createConsoleTemplateAttributes, formatConsoleArgs, hasConsoleSubstitutions } from '../logs/utils';
import type { LogSeverityLevel } from '../types-hoist/log';
import { isPlainObject } from '../utils/is';
import { normalize } from '../utils/normalize';
/**
* Result of extracting structured attributes from console arguments.
*/
interface ExtractAttributesResult {
/**
* The log message to use for the log entry, typically constructed from the console arguments.
*/
message?: string;
/**
* The parameterized template string which is added as `sentry.message.template` attribute if applicable.
*/
messageTemplate?: string;
/**
* Remaining arguments to process as attributes with keys like `sentry.message.parameter.0`, `sentry.message.parameter.1`, etc.
*/
messageParameters?: unknown[];
/**
* Additional attributes to add to the log.
*/
attributes?: Record<string, unknown>;
}
/**
* Options for the Sentry Consola reporter.
*/
interface ConsolaReporterOptions {
/**
* Use this option to filter which levels should be captured. By default, all levels are captured.
*
* @example
* ```ts
* const sentryReporter = Sentry.createConsolaReporter({
* // Only capture error and warn logs
* levels: ['error', 'warn'],
* });
* consola.addReporter(sentryReporter);
* ```
*/
levels?: Array<LogSeverityLevel>;
/**
* Optionally provide a specific Sentry client instance to use for capturing logs.
* If not provided, the current client will be retrieved using `getClient()`.
*
* This is useful when you want to use specific client options for log normalization
* or when working with multiple client instances.
*
* @example
* ```ts
* const sentryReporter = Sentry.createConsolaReporter({
* client: myCustomClient,
* });
* ```
*/
client?: Client;
}
export interface ConsolaReporter {
log: (logObj: ConsolaLogObject) => void;
}
/**
* Represents a log object that Consola reporters receive.
*
* This interface matches the structure of log objects passed to Consola reporters.
* See: https://github.com/unjs/consola#custom-reporters
*
* @example
* ```ts
* const reporter = {
* log(logObj: ConsolaLogObject) {
* console.log(`[${logObj.type}] ${logObj.message || logObj.args?.join(' ')}`);
* }
* };
* consola.addReporter(reporter);
* ```
*/
export interface ConsolaLogObject {
/**
* Allows additional custom properties to be set on the log object. These properties will be captured as log attributes.
*
* Additional properties are set when passing a single object with a `message` (`consola.[type]({ message: '', ... })`) or if the reporter is called directly
*
* @example
* ```ts
* const reporter = Sentry.createConsolaReporter();
* reporter.log({
* type: 'info',
* message: 'User action',
* userId: 123,
* sessionId: 'abc-123'
* });
* // Will create attributes: `userId` and `sessionId`
* ```
*/
[key: string]: unknown;
/**
* The numeric log level (0-5) or null.
*
* Consola log levels:
* - 0: Fatal and Error
* - 1: Warnings
* - 2: Normal logs
* - 3: Informational logs, success, fail, ready, start, box, ...
* - 4: Debug logs
* - 5: Trace logs
* - null: Some special types like 'verbose'
*
* See: https://github.com/unjs/consola/blob/main/README.md#log-level
*/
level?: number | null;
/**
* The log type/method name (e.g., 'error', 'warn', 'info', 'debug', 'trace', 'success', 'fail', etc.).
*
* Consola built-in types include:
* - Standard: silent, fatal, error, warn, log, info, success, fail, ready, start, box, debug, trace, verbose
* - Custom types can also be defined
*
* See: https://github.com/unjs/consola/blob/main/README.md#log-types
*/
type?: string;
/**
* An optional tag/scope for the log entry.
*
* Tags are created using `consola.withTag('scope')` and help categorize logs.
*
* @example
* ```ts
* const scopedLogger = consola.withTag('auth');
* scopedLogger.info('User logged in'); // tag will be 'auth'
* ```
*
* See: https://github.com/unjs/consola/blob/main/README.md#withtagtag
*/
tag?: string;
/**
* The raw arguments passed to the log method.
*
* These args are typically formatted into the final `message`. In Consola reporters, `message` is not provided. See: https://github.com/unjs/consola/issues/406#issuecomment-3684792551
*
* @example
* ```ts
* consola.info('Hello', 'world', { user: 'john' });
* // args = ['Hello', 'world', { user: 'john' }]
* ```
*
* @example
* ```ts
* // `message` is a reserved property in Consola
* consola.log({ message: 'Hello' });
* // args = ['Hello']
* ```
*/
args?: unknown[];
/**
* The timestamp when the log was created.
*
* This is automatically set by Consola when the log is created.
*/
date?: Date;
/**
* The formatted log message.
*
* When provided, this is the final formatted message. When not provided,
* the message should be constructed from the `args` array.
*
* Note: In reporters, `message` is typically undefined. It is primarily for
* `consola.[type]({ message: 'xxx' })` usage and is normalized into `args` before
* reporters receive the log object. See: https://github.com/unjs/consola/issues/406#issuecomment-3684792551
*/
message?: string;
}
const DEFAULT_CAPTURED_LEVELS: Array<LogSeverityLevel> = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
/**
* Creates a new Sentry reporter for Consola that forwards logs to Sentry. Requires the `enableLogs` option to be enabled.
*
* **Note: This integration supports Consola v3.x only.** The reporter interface and log object structure
* may differ in other versions of Consola.
*
* @param options - Configuration options for the reporter.
* @returns A Consola reporter that can be added to consola instances.
*
* @example
* ```ts
* import * as Sentry from '@sentry/node';
* import { consola } from 'consola';
*
* Sentry.init({
* enableLogs: true,
* });
*
* const sentryReporter = Sentry.createConsolaReporter({
* // Optional: filter levels to capture
* levels: ['error', 'warn', 'info'],
* });
*
* consola.addReporter(sentryReporter);
*
* // Now consola logs will be captured by Sentry
* consola.info('This will be sent to Sentry');
* consola.error('This error will also be sent to Sentry');
* ```
*/
export function createConsolaReporter(options: ConsolaReporterOptions = {}): ConsolaReporter {
const levels = new Set(options.levels ?? DEFAULT_CAPTURED_LEVELS);
const providedClient = options.client;
return {
log(logObj: ConsolaLogObject) {
// We need to exclude certain known properties from being added as additional attributes
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { type, level, message: consolaMessage, args, tag, date: _date, ...rest } = logObj;
// Get client - use provided client or current client
const client = providedClient || getClient();
if (!client) {
return;
}
// Determine the log severity level
const logSeverityLevel = getLogSeverityLevel(type, level);
// Early exit if this level should not be captured
if (!levels.has(logSeverityLevel)) {
return;
}
const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions();
const attributes: Record<string, unknown> = {};
// Build attributes
for (const [key, value] of Object.entries(rest)) {
attributes[key] = normalize(value, normalizeDepth, normalizeMaxBreadth);
}
attributes['sentry.origin'] = 'auto.log.consola';
if (tag) {
attributes['consola.tag'] = tag;
}
if (type) {
attributes['consola.type'] = type;
}
// Only add level if it's a valid number (not null/undefined)
if (level != null && typeof level === 'number') {
attributes['consola.level'] = level;
}
const extractionResult = processExtractedAttributes(
defaultExtractAttributes(args, normalizeDepth, normalizeMaxBreadth),
normalizeDepth,
normalizeMaxBreadth,
);
if (extractionResult?.attributes) {
Object.assign(attributes, extractionResult.attributes);
}
_INTERNAL_captureLog({
level: logSeverityLevel,
message:
extractionResult?.message ||
consolaMessage ||
(args && formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth)) ||
'',
attributes,
});
},
};
}
// Mapping from consola log types to Sentry log severity levels
const CONSOLA_TYPE_TO_LOG_SEVERITY_LEVEL_MAP: Record<string, LogSeverityLevel> = {
// Consola built-in types
silent: 'trace',
fatal: 'fatal',
error: 'error',
warn: 'warn',
log: 'info',
info: 'info',
success: 'info',
fail: 'error',
ready: 'info',
start: 'info',
box: 'info',
debug: 'debug',
trace: 'trace',
verbose: 'debug',
// Custom types that might exist
critical: 'fatal',
notice: 'info',
};
// Mapping from consola log levels (numbers) to Sentry log severity levels
const CONSOLA_LEVEL_TO_LOG_SEVERITY_LEVEL_MAP: Record<number, LogSeverityLevel> = {
0: 'fatal', // Fatal and Error
1: 'warn', // Warnings
2: 'info', // Normal logs
3: 'info', // Informational logs, success, fail, ready, start, ...
4: 'debug', // Debug logs
5: 'trace', // Trace logs
};
/**
* Determines the log severity level from Consola type and level.
*
* @param type - The Consola log type (e.g., 'error', 'warn', 'info')
* @param level - The Consola numeric log level (0-5) or null for some types like 'verbose'
* @returns The corresponding Sentry log severity level
*/
function getLogSeverityLevel(type?: string, level?: number | null): LogSeverityLevel {
// Handle special case for verbose logs (level can be null with infinite level in Consola)
if (type === 'verbose') {
return 'debug';
}
// Handle silent logs - these should be at trace level
if (type === 'silent') {
return 'trace';
}
// First try to map by type (more specific)
if (type) {
const mappedLevel = CONSOLA_TYPE_TO_LOG_SEVERITY_LEVEL_MAP[type];
if (mappedLevel) {
return mappedLevel;
}
}
// Fallback to level mapping (handle null level)
if (typeof level === 'number') {
const mappedLevel = CONSOLA_LEVEL_TO_LOG_SEVERITY_LEVEL_MAP[level];
if (mappedLevel) {
return mappedLevel;
}
}
// Default fallback
return 'info';
}
/**
* Extracts structured attributes from console arguments. If the first argument is a plain object, its properties are extracted as attributes.
*/
function defaultExtractAttributes(
args: unknown[] | undefined,
normalizeDepth: number,
normalizeMaxBreadth: number,
): ExtractAttributesResult {
if (!args?.length) {
return { message: '' };
}
// Message looks like how consola logs the message to the console (all args stringified and joined)
const message = formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth);
const firstArg = args[0];
if (isPlainObject(firstArg)) {
// Remaining args start from index 2 i f we used second arg as message, otherwise from index 1
const remainingArgsStartIndex = typeof args[1] === 'string' ? 2 : 1;
const remainingArgs = args.slice(remainingArgsStartIndex);
return {
message,
// Object content from first arg is added as attributes
attributes: firstArg,
// Add remaining args as message parameters
messageParameters: remainingArgs,
};
} else {
const followingArgs = args.slice(1);
const shouldAddTemplateAttr =
followingArgs.length > 0 && typeof firstArg === 'string' && !hasConsoleSubstitutions(firstArg);
return {
message,
messageTemplate: shouldAddTemplateAttr ? firstArg : undefined,
messageParameters: shouldAddTemplateAttr ? followingArgs : undefined,
};
}
}
/**
* Processes extracted attributes by normalizing them and preparing message parameter attributes if a template is present.
*/
function processExtractedAttributes(
extractionResult: ExtractAttributesResult,
normalizeDepth: number,
normalizeMaxBreadth: number,
): { message: string | undefined; attributes: Record<string, unknown> } {
const { message, attributes, messageTemplate, messageParameters } = extractionResult;
const messageParamAttributes: Record<string, unknown> = {};
if (messageTemplate && messageParameters) {
const templateAttrs = createConsoleTemplateAttributes(messageTemplate, messageParameters);
for (const [key, value] of Object.entries(templateAttrs)) {
messageParamAttributes[key] = key.startsWith('sentry.message.parameter.')
? normalize(value, normalizeDepth, normalizeMaxBreadth)
: value;
}
} else if (messageParameters && messageParameters.length > 0) {
messageParameters.forEach((arg, index) => {
messageParamAttributes[`sentry.message.parameter.${index}`] = normalize(arg, normalizeDepth, normalizeMaxBreadth);
});
}
return {
message: message,
attributes: {
...normalize(attributes, normalizeDepth, normalizeMaxBreadth),
...messageParamAttributes,
},
};
}