All files / server/src/utils logger.ts

59.37% Statements 19/32
0% Branches 0/18
66.66% Functions 4/6
63.33% Lines 19/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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          5x       5x 5x               5x 53630x                         53630x 53630x           53630x           5x 4x 4x             4x 4x 4x   4x     5x                                     2x       5x                                     2x                    
import { createLogger, format, transports } from "winston";
import type { TransformableInfo } from "logform";
import _WinstonTransport from "winston-transport-sentry-node";
// Node.js ESM CJS interop: package uses exports.default, so the class is wrapped as { default: Class }
const WinstonTransport = (
  _WinstonTransport as unknown as { default: typeof _WinstonTransport }
).default;
import { config } from "shared/config";
 
const SPLAT = Symbol.for("splat");
const LEVEL = Symbol.for("level");
 
// Log errors by passing the Error directly: `logger.error(new Error("..."))`, or wrap an
// underlying error as the cause: `logger.error(new Error("context", { cause: err }))`
//
// Surfaces the Error onto an enumerable `error` key because winston-transport-sentry-node
// only captures a real exception when it finds an Error among `info`'s enumerable values,
// and winston-transport's `Object.assign` copy drops an Error's non-enumerable `message`/`stack`
const surfaceError = format((info) => {
  if (info instanceof Error) {
    // sole-arg Error: winston makes `info` the Error itself. Return a fresh info so we don't
    // mutate the caller's Error or create a circular self-reference through `error`
    const next: TransformableInfo = {
      level: info.level,
      message: info.message,
      error: info,
    };
    (next as Record<symbol, unknown>)[LEVEL] = (
      info as Record<symbol, unknown>
    )[LEVEL];
    return next;
  }
  const args = (info as Record<symbol, unknown>)[SPLAT];
  if (Array.isArray(args)) {
    const error = args.find((arg: unknown) => arg instanceof Error);
    if (error) {
      info.error = error;
    }
  }
  return info;
});
 
// Build the human-readable message for console/file logs: the full stack plus the whole
// `cause` chain when the log carries an Error (Sentry instead gets the raw Error object via
// the transport, so this is only for the text logs)
const renderMessage = (info: TransformableInfo): string => {
  const error = info.error;
  if (error instanceof Error) {
    let text = error.stack ?? String(error);
    for (let cause = error.cause; cause instanceof Error; cause = cause.cause) {
      text += `\nCaused by: ${cause.stack ?? String(cause)}`;
    }
    return text;
  }
  const { message } = info;
  if (typeof message === "string") {
    return message;
  }
  return message == null ? "" : JSON.stringify(message);
};
 
const consoleOutputFormat = config.server().consoleLogFormatJson
  ? format.combine(
      format.timestamp(),
      format.splat(),
      format.printf((info) => {
        return JSON.stringify({
          timestamp: info.timestamp,
          level: info.level,
          message: renderMessage(info),
        });
      }),
    )
  : format.combine(
      format.colorize(),
      format.timestamp({
        format: "HH:mm:ss",
      }),
      format.splat(),
      format.printf((info) => {
        return `${String(info.timestamp)} ${info.level}: ${renderMessage(info)}`;
      }),
    );
 
export const logger = createLogger({
  handleExceptions: true,
  handleRejections: true,
  format: surfaceError(),
 
  transports: [
    new transports.Console({
      level: config.server().logLevel,
      format: consoleOutputFormat,
    }),
 
    new WinstonTransport({
      // Sentry SDK is already initialized in instrument.ts
      skipSentryInit: true,
      level: "error",
      format: format.combine(
        format.timestamp(),
        format.splat(),
        format.printf((info) => {
          return JSON.stringify({
            timestamp: info.timestamp,
            level: info.level,
            message: renderMessage(info),
          });
        }),
      ),
    }),
  ],
});