logger-factory.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import type { Logger } from 'pino';
  2. import pino from 'pino';
  3. import { parseEnvLevels } from './env-var-parser';
  4. import { resolveLevel } from './level-resolver';
  5. import {
  6. createBrowserOptions,
  7. createNodeTransportOptions,
  8. } from './transport-factory';
  9. import type { LoggerConfig, LoggerFactoryOptions } from './types';
  10. const isBrowser =
  11. typeof window !== 'undefined' && typeof window.document !== 'undefined';
  12. let moduleConfig: LoggerConfig = { default: 'info' };
  13. let envOverrides: Omit<LoggerConfig, 'default'> = {};
  14. const loggerCache = new Map<string, Logger<string>>();
  15. // Shared root logger. pino.transport() is called once here so that all
  16. // namespace loggers share a single Worker thread (pino's performance model).
  17. let rootLogger: Logger<string> | null = null;
  18. function assertRootLogger(
  19. logger: Logger<string> | null,
  20. ): asserts logger is Logger<string> {
  21. if (logger == null) {
  22. throw new Error(
  23. 'rootLogger is not initialized. Call initializeLoggerFactory() first.',
  24. );
  25. }
  26. }
  27. /**
  28. * Initialize the logger factory with configuration.
  29. * Creates the pino transport and root logger ONCE so that all namespace
  30. * loggers share a single Worker thread — preserving pino's performance model.
  31. * Must be called once at application startup before any loggerFactory() calls.
  32. * Subsequent calls clear the cache and create a fresh root logger.
  33. */
  34. export function initializeLoggerFactory(options: LoggerFactoryOptions): void {
  35. moduleConfig = options.config;
  36. envOverrides = parseEnvLevels();
  37. loggerCache.clear();
  38. const isProduction = process.env.NODE_ENV === 'production';
  39. if (isBrowser) {
  40. // Browser: no Worker thread involved; use pino's built-in browser mode.
  41. // Root level is 'trace' so each child can apply its own resolved level.
  42. const { browser } = createBrowserOptions(isProduction);
  43. rootLogger = pino({ level: 'trace', browser }) as Logger<string>;
  44. } else {
  45. // Node.js: call pino.transport() ONCE here.
  46. // Every subsequent loggerFactory() call uses rootLogger.child() which
  47. // shares this single Worker thread rather than spawning a new one.
  48. const { transport } = createNodeTransportOptions(isProduction);
  49. rootLogger = (
  50. transport != null
  51. ? pino({ level: 'trace' }, pino.transport(transport))
  52. : pino({ level: 'trace' })
  53. ) as Logger<string>;
  54. }
  55. }
  56. /**
  57. * Create or retrieve a cached pino logger for the given namespace.
  58. * Returns a child of the shared root logger so the Worker thread is reused.
  59. */
  60. export function loggerFactory(name: string): Logger<string> {
  61. const cached = loggerCache.get(name);
  62. if (cached != null) {
  63. return cached;
  64. }
  65. if (rootLogger == null) {
  66. // Auto-initialize with default config if the caller skipped the explicit init.
  67. initializeLoggerFactory({ config: moduleConfig });
  68. }
  69. assertRootLogger(rootLogger);
  70. const level = resolveLevel(name, moduleConfig, envOverrides);
  71. // child() shares the root logger's transport — no new Worker thread spawned.
  72. const logger = rootLogger.child({ name });
  73. logger.level = level;
  74. loggerCache.set(name, logger);
  75. return logger;
  76. }