Просмотр исходного кода

Update bunyan-format to use pino-pretty for enhanced log formatting and adjust extra fields output

Yuki Takei 2 дней назад
Родитель
Сommit
67110e28d2

+ 2 - 1
packages/logger/src/transports/bunyan-format.spec.ts

@@ -88,7 +88,8 @@ describe('bunyan-format transport', () => {
 
     const output = chunks.join('');
     expect(output).toContain('hello');
-    expect(output).toContain('\n    {"extra":"value"}');
+    // pino-pretty formats extra fields as `key: "value"` on a new indented line
+    expect(output).toContain('\n    extra: "value"');
   });
 
   it('appends extra fields inline when singleLine is true', async () => {

+ 41 - 76
packages/logger/src/transports/bunyan-format.ts

@@ -1,4 +1,5 @@
 import { Writable } from 'node:stream';
+import { prettyFactory } from 'pino-pretty';
 
 interface BunyanFormatOptions {
   singleLine?: boolean;
@@ -6,100 +7,64 @@ interface BunyanFormatOptions {
   destination?: NodeJS.WritableStream;
 }
 
-const LEVELS: Record<number, string> = {
-  10: 'TRACE',
-  20: 'DEBUG',
-  30: 'INFO',
-  40: 'WARN',
-  50: 'ERROR',
-  60: 'FATAL',
-};
+const ANAI_COLORS = ['gray', 'green', 'yellow', 'red'] as const;
 
-// ANSI color codes by level
-const COLORS: Record<number, string> = {
-  10: '\x1b[90m', // gray for TRACE
-  20: '\x1b[36m', // cyan for DEBUG
-  30: '\x1b[32m', // green for INFO
-  40: '\x1b[33m', // yellow for WARN
-  50: '\x1b[31m', // red for ERROR
-  60: '\x1b[31m', // red for FATAL
+const LEVEL_SETTINGS: Record<
+  number,
+  { label: string; color: (typeof ANAI_COLORS)[number] }
+> = {
+  10: {
+    label: 'TRACE',
+    color: 'gray',
+  },
+  20: { label: 'DEBUG', color: 'gray' },
+  30: { label: 'INFO', color: 'green' },
+  40: { label: 'WARN', color: 'yellow' },
+  50: { label: 'ERROR', color: 'red' },
+  60: { label: 'FATAL', color: 'red' },
 };
-const RESET = '\x1b[0m';
-
-/**
- * Format a log object into bunyan-format "short" style:
- *   HH:mm:ss.SSSZ LEVEL name: message
- */
-function formatLine(
-  log: Record<string, unknown>,
-  singleLine: boolean,
-  colorize: boolean,
-): string {
-  const time = new Date(log.time as number).toISOString().slice(11);
-  const level = log.level as number;
-  const label = (LEVELS[level] ?? 'INFO').padStart(5);
-  const name = (log.name as string) ?? '';
-  const msg = (log.msg as string) ?? '';
-
-  const color = colorize ? (COLORS[level] ?? '') : '';
-  const reset = colorize ? RESET : '';
-
-  let line = `${color}${time} ${label} ${name}:${reset} ${msg}`;
-
-  // Extra fields (exclude standard pino fields)
-  const extras: Record<string, unknown> = {};
-  for (const [key, val] of Object.entries(log)) {
-    if (
-      key !== 'level' &&
-      key !== 'time' &&
-      key !== 'msg' &&
-      key !== 'name' &&
-      key !== 'pid' &&
-      key !== 'hostname'
-    ) {
-      extras[key] = val;
-    }
-  }
-
-  if (Object.keys(extras).length > 0) {
-    const extraStr = JSON.stringify(extras);
-    if (singleLine) {
-      line += ` ${extraStr}`;
-    } else {
-      line += `\n    ${extraStr}`;
-    }
-  }
-
-  return `${line}\n`;
-}
 
 /**
  * Custom pino transport producing bunyan-format "short" mode output.
  * Format: HH:mm:ss.SSSZ LEVEL name: message
  *
  * Development only — this module is never imported in production.
- * Uses fs.writeSync(1, ...) to write directly to stdout fd, bypassing
- * thread-stream's stdout interception in Worker threads.
  */
 // biome-ignore lint/style/noDefaultExport: pino transports require a default export for thread-stream Worker loading
 export default (opts: BunyanFormatOptions) => {
   const singleLine = opts.singleLine ?? false;
-  const colorize = opts.colorize ?? !process.env.NO_COLOR;
-  const destination = opts.destination;
+  const destination = opts.destination ?? process.stdout;
+
+  const pretty = prettyFactory({
+    colorize: opts.colorize ?? !process.env.NO_COLOR,
+    ignore: 'pid,hostname,name',
+    translateTime: false,
+    singleLine,
+    // Suppress pino-pretty's default time and level rendering; we handle them in messageFormat
+    customPrettifiers: { time: () => '', level: () => '' },
+    messageFormat: (log, messageKey, _levelLabel, { colors }) => {
+      const time = new Date(log.time as number).toISOString().slice(11);
+      const levelNum = log.level as number;
+      const label = LEVEL_SETTINGS[levelNum]?.label ?? 'INFO';
+      const name = (log.name as string) ?? '';
+      const msg = String(log[messageKey] ?? '');
 
-  const out = destination ?? process.stdout;
+      const padding = ' '.repeat(Math.max(0, 5 - label.length));
+      const c = colors as unknown as Record<string, (s: string) => string>;
+      const levelColor =
+        c[LEVEL_SETTINGS[levelNum]?.color ?? 'reset'] ?? String;
+
+      return `${c.gray(time)} ${levelColor(`${padding}${label}`)} ${c.white(`${name}:`)} ${msg}`;
+    },
+  });
 
   return new Writable({
     write(chunk, _encoding, callback) {
-      const text = typeof chunk === 'string' ? chunk : chunk.toString();
-      // thread-stream may batch multiple JSON lines into one chunk
-      for (const line of text.split('\n')) {
-        if (line.length === 0) continue;
+      for (const line of chunk.toString().split('\n').filter(Boolean)) {
         try {
-          const log = JSON.parse(line);
-          out.write(formatLine(log, singleLine, colorize));
+          destination.write(pretty(JSON.parse(line)) ?? '');
         } catch {
-          out.write(`${line}\n`);
+          destination.write(`${line}\n`);
         }
       }
       callback();