logger.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. // === Types ===
  2. export type LogLevel = "debug" | "info" | "warn" | "error";
  3. export interface LogEntry {
  4. readonly level: LogLevel;
  5. readonly tag: string;
  6. readonly message: string;
  7. readonly timestamp: string;
  8. readonly ctx?: Record<string, unknown>;
  9. }
  10. export interface LogSink {
  11. readonly write: (entry: LogEntry) => void;
  12. }
  13. export interface Logger {
  14. readonly debug: (msg: string, ctx?: Record<string, unknown>) => void;
  15. readonly info: (msg: string, ctx?: Record<string, unknown>) => void;
  16. readonly warn: (msg: string, ctx?: Record<string, unknown>) => void;
  17. readonly error: (msg: string, ctx?: Record<string, unknown>) => void;
  18. readonly child: (tag: string, extraCtx?: Record<string, unknown>) => Logger;
  19. }
  20. // === Level Ordering ===
  21. const LEVEL_ORDER: Record<LogLevel, number> = {
  22. debug: 0,
  23. info: 1,
  24. warn: 2,
  25. error: 3,
  26. };
  27. // === ANSI Colors ===
  28. const COLORS: Record<LogLevel, string> = {
  29. debug: "\x1b[90m", // gray
  30. info: "\x1b[36m", // cyan
  31. warn: "\x1b[33m", // yellow
  32. error: "\x1b[31m", // red
  33. };
  34. const RESET = "\x1b[0m";
  35. // === Built-in Sinks ===
  36. export function createStderrSink(options: {
  37. readonly minLevel?: LogLevel;
  38. readonly enableColors?: boolean;
  39. }): LogSink {
  40. const minLevel = options.minLevel ?? "info";
  41. const enableColors = options.enableColors ?? (process.stderr.isTTY ?? false);
  42. const minOrder = LEVEL_ORDER[minLevel];
  43. return {
  44. write(entry: LogEntry): void {
  45. if (LEVEL_ORDER[entry.level] < minOrder) return;
  46. const levelTag = entry.level.toUpperCase().padEnd(5);
  47. const prefix = `[${entry.tag}]`;
  48. if (enableColors) {
  49. const color = COLORS[entry.level];
  50. process.stderr.write(
  51. `${color}${levelTag}${RESET} ${prefix} ${entry.message}\n`,
  52. );
  53. } else {
  54. process.stderr.write(`${levelTag} ${prefix} ${entry.message}\n`);
  55. }
  56. },
  57. };
  58. }
  59. export function createJsonLineSink(writable: NodeJS.WritableStream): LogSink {
  60. return {
  61. write(entry: LogEntry): void {
  62. writable.write(JSON.stringify(entry) + "\n");
  63. },
  64. };
  65. }
  66. export const nullSink: LogSink = {
  67. write(): void {},
  68. };
  69. // === Factory ===
  70. export function createLogger(options: {
  71. readonly tag: string;
  72. readonly sinks: ReadonlyArray<LogSink>;
  73. readonly minLevel?: LogLevel;
  74. readonly baseCtx?: Record<string, unknown>;
  75. }): Logger {
  76. const { tag, sinks, baseCtx } = options;
  77. function emit(level: LogLevel, msg: string, ctx?: Record<string, unknown>): void {
  78. const entry: LogEntry = {
  79. level,
  80. tag,
  81. message: msg,
  82. timestamp: new Date().toISOString(),
  83. ...(ctx || baseCtx
  84. ? { ctx: { ...baseCtx, ...ctx } }
  85. : {}),
  86. };
  87. for (const sink of sinks) {
  88. sink.write(entry);
  89. }
  90. }
  91. return {
  92. debug: (msg, ctx) => emit("debug", msg, ctx),
  93. info: (msg, ctx) => emit("info", msg, ctx),
  94. warn: (msg, ctx) => emit("warn", msg, ctx),
  95. error: (msg, ctx) => emit("error", msg, ctx),
  96. child(childTag, extraCtx) {
  97. return createLogger({
  98. tag: childTag,
  99. sinks,
  100. baseCtx: { ...baseCtx, ...extraCtx },
  101. });
  102. },
  103. };
  104. }