@chidchanun/bcp 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ import {
2
+ requestId,
3
+ requestMethod,
4
+ requestUrl,
5
+ } from "./request-context.js";
6
+
7
+ export type LogLevel =
8
+ | "debug"
9
+ | "info"
10
+ | "warn"
11
+ | "error"
12
+ | "silent";
13
+
14
+ export type LogFormat =
15
+ | "pretty"
16
+ | "json";
17
+
18
+ export type LogFields =
19
+ Record<string, unknown>;
20
+
21
+ export interface LoggerOptions {
22
+ name?: string;
23
+ level?: LogLevel;
24
+ format?: LogFormat;
25
+ bindings?: LogFields;
26
+ write?: (
27
+ line: string,
28
+ level: Exclude<LogLevel, "silent">
29
+ ) => void;
30
+ now?: () => Date;
31
+ }
32
+
33
+ export interface Logger {
34
+ debug(
35
+ message: string,
36
+ fields?: LogFields
37
+ ): void;
38
+ info(
39
+ message: string,
40
+ fields?: LogFields
41
+ ): void;
42
+ warn(
43
+ message: string,
44
+ fields?: LogFields
45
+ ): void;
46
+ error(
47
+ message: string,
48
+ fields?: LogFields
49
+ ): void;
50
+ child(
51
+ bindings: LogFields
52
+ ): Logger;
53
+ }
54
+
55
+ interface ResolvedLoggerOptions {
56
+ name: string;
57
+ level?: LogLevel;
58
+ format?: LogFormat;
59
+ bindings: LogFields;
60
+ write?: LoggerOptions["write"];
61
+ now: () => Date;
62
+ }
63
+
64
+ const LOG_LEVEL_WEIGHT:
65
+ Record<LogLevel, number> = {
66
+ debug: 10,
67
+ info: 20,
68
+ warn: 30,
69
+ error: 40,
70
+ silent: Number.POSITIVE_INFINITY,
71
+ };
72
+
73
+ export function createLogger(
74
+ options: LoggerOptions = {}
75
+ ): Logger {
76
+ const resolved:
77
+ ResolvedLoggerOptions = {
78
+ name:
79
+ normalizeLoggerName(
80
+ options.name
81
+ ) ??
82
+ "bcp",
83
+ level:
84
+ options.level,
85
+ format:
86
+ options.format,
87
+ bindings: {
88
+ ...options.bindings,
89
+ },
90
+ write:
91
+ options.write,
92
+ now:
93
+ options.now ??
94
+ (() => new Date()),
95
+ };
96
+
97
+ return createLoggerInstance(
98
+ resolved
99
+ );
100
+ }
101
+
102
+ export const logger =
103
+ createLogger();
104
+
105
+ export async function requestLogger(
106
+ bindings: LogFields = {}
107
+ ): Promise<Logger> {
108
+ const [
109
+ id,
110
+ method,
111
+ url,
112
+ ] = await Promise.all([
113
+ requestId(),
114
+ requestMethod(),
115
+ requestUrl(),
116
+ ]);
117
+
118
+ return logger.child({
119
+ ...bindings,
120
+ requestId:
121
+ id,
122
+ method,
123
+ path:
124
+ `${url.pathname}${url.search}`,
125
+ });
126
+ }
127
+
128
+ export async function attachRequestId(
129
+ response: Response
130
+ ): Promise<Response> {
131
+ const id =
132
+ await requestId();
133
+ const responseHeaders =
134
+ new Headers(
135
+ response.headers
136
+ );
137
+
138
+ responseHeaders.set(
139
+ "x-request-id",
140
+ id
141
+ );
142
+
143
+ return new Response(
144
+ response.body,
145
+ {
146
+ status:
147
+ response.status,
148
+ statusText:
149
+ response.statusText,
150
+ headers:
151
+ responseHeaders,
152
+ }
153
+ );
154
+ }
155
+
156
+ function createLoggerInstance(
157
+ options: ResolvedLoggerOptions
158
+ ): Logger {
159
+ const write = (
160
+ level: Exclude<LogLevel, "silent">,
161
+ message: string,
162
+ fields: LogFields = {}
163
+ ): void => {
164
+ const configuredLevel =
165
+ options.level ??
166
+ resolveEnvironmentLogLevel();
167
+
168
+ if (
169
+ LOG_LEVEL_WEIGHT[level] <
170
+ LOG_LEVEL_WEIGHT[configuredLevel]
171
+ ) {
172
+ return;
173
+ }
174
+
175
+ const record = {
176
+ timestamp:
177
+ options.now()
178
+ .toISOString(),
179
+ level,
180
+ logger:
181
+ options.name,
182
+ message,
183
+ ...options.bindings,
184
+ ...fields,
185
+ };
186
+
187
+ const format =
188
+ options.format ??
189
+ resolveEnvironmentLogFormat();
190
+ const line =
191
+ format === "json"
192
+ ? stringifyLogRecord(
193
+ record
194
+ )
195
+ : formatPrettyLogRecord(
196
+ record
197
+ );
198
+
199
+ if (options.write) {
200
+ options.write(
201
+ line,
202
+ level
203
+ );
204
+ return;
205
+ }
206
+
207
+ writeConsoleLine(
208
+ line,
209
+ level
210
+ );
211
+ };
212
+
213
+ return {
214
+ debug(
215
+ message,
216
+ fields
217
+ ) {
218
+ write(
219
+ "debug",
220
+ message,
221
+ fields
222
+ );
223
+ },
224
+ info(
225
+ message,
226
+ fields
227
+ ) {
228
+ write(
229
+ "info",
230
+ message,
231
+ fields
232
+ );
233
+ },
234
+ warn(
235
+ message,
236
+ fields
237
+ ) {
238
+ write(
239
+ "warn",
240
+ message,
241
+ fields
242
+ );
243
+ },
244
+ error(
245
+ message,
246
+ fields
247
+ ) {
248
+ write(
249
+ "error",
250
+ message,
251
+ fields
252
+ );
253
+ },
254
+ child(
255
+ bindings
256
+ ) {
257
+ return createLoggerInstance({
258
+ ...options,
259
+ bindings: {
260
+ ...options.bindings,
261
+ ...bindings,
262
+ },
263
+ });
264
+ },
265
+ };
266
+ }
267
+
268
+ export function resolveEnvironmentLogLevel():
269
+ LogLevel {
270
+ return normalizeLogLevel(
271
+ process.env.BCP_LOG_LEVEL
272
+ ) ??
273
+ "info";
274
+ }
275
+
276
+ export function resolveEnvironmentLogFormat():
277
+ LogFormat {
278
+ return normalizeLogFormat(
279
+ process.env.BCP_LOG_FORMAT
280
+ ) ??
281
+ "pretty";
282
+ }
283
+
284
+ function normalizeLogLevel(
285
+ value:
286
+ string | undefined
287
+ ): LogLevel | null {
288
+ if (!value) {
289
+ return null;
290
+ }
291
+
292
+ const normalized =
293
+ value
294
+ .trim()
295
+ .toLowerCase();
296
+
297
+ return (
298
+ normalized === "debug" ||
299
+ normalized === "info" ||
300
+ normalized === "warn" ||
301
+ normalized === "error" ||
302
+ normalized === "silent"
303
+ )
304
+ ? normalized
305
+ : null;
306
+ }
307
+
308
+ function normalizeLogFormat(
309
+ value:
310
+ string | undefined
311
+ ): LogFormat | null {
312
+ if (!value) {
313
+ return null;
314
+ }
315
+
316
+ const normalized =
317
+ value
318
+ .trim()
319
+ .toLowerCase();
320
+
321
+ return (
322
+ normalized === "pretty" ||
323
+ normalized === "json"
324
+ )
325
+ ? normalized
326
+ : null;
327
+ }
328
+
329
+ function normalizeLoggerName(
330
+ value:
331
+ string | undefined
332
+ ): string | null {
333
+ if (!value) {
334
+ return null;
335
+ }
336
+
337
+ const normalized =
338
+ value.trim();
339
+
340
+ return normalized.length > 0
341
+ ? normalized
342
+ : null;
343
+ }
344
+
345
+ function formatPrettyLogRecord(
346
+ record: Record<string, unknown>
347
+ ): string {
348
+ const {
349
+ timestamp,
350
+ level,
351
+ logger: loggerName,
352
+ message,
353
+ ...fields
354
+ } = record;
355
+ const suffix =
356
+ Object.keys(fields).length > 0
357
+ ? " " +
358
+ Object.entries(fields)
359
+ .map(
360
+ ([key, value]) =>
361
+ `${key}=${formatPrettyValue(value)}`
362
+ )
363
+ .join(" ")
364
+ : "";
365
+
366
+ return (
367
+ `[${String(timestamp)}] ` +
368
+ `[${String(level).toUpperCase()}] ` +
369
+ `[${String(loggerName)}] ` +
370
+ `${String(message)}${suffix}`
371
+ );
372
+ }
373
+
374
+ function formatPrettyValue(
375
+ value: unknown
376
+ ): string {
377
+ if (
378
+ typeof value === "string"
379
+ ) {
380
+ return /\s/.test(value)
381
+ ? (
382
+ JSON.stringify(
383
+ value
384
+ ) ??
385
+ "\"\""
386
+ )
387
+ : value;
388
+ }
389
+
390
+ if (
391
+ typeof value === "number" ||
392
+ typeof value === "boolean" ||
393
+ value === null ||
394
+ value === undefined
395
+ ) {
396
+ return String(value);
397
+ }
398
+
399
+ return stringifyLogRecord(
400
+ value
401
+ );
402
+ }
403
+
404
+ function stringifyLogRecord(
405
+ value: unknown
406
+ ): string {
407
+ const seen =
408
+ new WeakSet<object>();
409
+ const serialized =
410
+ JSON.stringify(
411
+ value,
412
+ (_key, item) => {
413
+ if (
414
+ item instanceof Error
415
+ ) {
416
+ return {
417
+ name:
418
+ item.name,
419
+ message:
420
+ item.message,
421
+ stack:
422
+ item.stack,
423
+ cause:
424
+ item.cause,
425
+ };
426
+ }
427
+
428
+ if (
429
+ typeof item === "bigint"
430
+ ) {
431
+ return item.toString();
432
+ }
433
+
434
+ if (
435
+ item !== null &&
436
+ typeof item === "object"
437
+ ) {
438
+ if (seen.has(item)) {
439
+ return "[Circular]";
440
+ }
441
+
442
+ seen.add(item);
443
+ }
444
+
445
+ return item;
446
+ }
447
+ );
448
+
449
+ return serialized ??
450
+ "undefined";
451
+ }
452
+
453
+ function writeConsoleLine(
454
+ line: string,
455
+ level: Exclude<LogLevel, "silent">
456
+ ): void {
457
+ if (level === "error") {
458
+ console.error(
459
+ line
460
+ );
461
+ return;
462
+ }
463
+
464
+ if (level === "warn") {
465
+ console.warn(
466
+ line
467
+ );
468
+ return;
469
+ }
470
+
471
+ console.log(
472
+ line
473
+ );
474
+ }