@trojs/logger 0.5.15 → 0.5.17

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@trojs/logger",
3
3
  "description": "Winston logger for TroJS",
4
- "version": "0.5.15",
4
+ "version": "0.5.17",
5
5
  "author": {
6
6
  "name": "Pieter Wigboldus",
7
7
  "url": "https://trojs.org/"
package/src/logger.js CHANGED
@@ -3,15 +3,9 @@ import makeLoggers from './loggers/index.js'
3
3
 
4
4
  /**
5
5
  * @typedef {import('./models/schemas/logger.js').Logger} LoggerType
6
- * @typedef {import('./models/enums/level.js').LevelType} LevelType
7
6
  */
8
7
 
9
- /** @type {LoggerType[]} */
10
- const defaultLoggers = [
11
- {
12
- type: 'console'
13
- }
14
- ]
8
+ const defaultLoggers = [{ type: 'console' }]
15
9
 
16
10
  const levels = {
17
11
  fatal: 0,
@@ -23,20 +17,102 @@ const levels = {
23
17
  }
24
18
 
25
19
  /**
26
- * Create the logger
27
- * @param {object} config
28
- * @param {LoggerType=} config.loggers
29
- * @param {string?=} config.level
30
- * @param {object?=} config.meta
31
- * @returns {winston.Logger}
20
+ * Creates a Winston logger instance with custom log levels and transports.
21
+ * Also attaches global process event handlers for uncaught exceptions, unhandled rejections, and warnings.
22
+ * @param {object} [options={}] - Logger configuration options.
23
+ * @param {Array<{[key: string]: string}>} [options.loggers=defaultLoggers] - Array of logger transport configurations.
24
+ * @param {string} [options.level='info'] - Minimum log level for the logger.
25
+ * @param {object} [options.meta={}] - Default metadata to include in all log messages.
26
+ * @returns {LoggerType} Winston logger instance with custom level wrappers.
27
+ * These handlers will log errors and warnings using the logger, and are only attached once per process.
28
+ * @example
29
+ * import createLogger from './logger.js';
30
+ * const logger = createLogger({ level: 'debug', meta: { service: 'api' } });
31
+ * logger.info('Service started');
32
32
  */
33
- export default ({ loggers = defaultLoggers, level = 'info', meta = {} }) => {
33
+ export default ({ loggers = defaultLoggers, level = 'info', meta = {} } = {}) => {
34
34
  const winstonLoggers = makeLoggers({ winston, loggers })
35
35
 
36
- return winston.createLogger({
36
+ const logger = winston.createLogger({
37
37
  level,
38
38
  levels,
39
39
  defaultMeta: meta,
40
40
  transports: winstonLoggers
41
41
  })
42
+
43
+ const wrapLevel = (lvl) => {
44
+ const orig = logger[lvl].bind(logger)
45
+ logger[lvl] = (first, ...rest) => {
46
+ if (first instanceof Error) {
47
+ const info = {
48
+ level: lvl,
49
+ message: first.message || first.toString(),
50
+ error: first,
51
+ stack: first.stack
52
+ }
53
+ if (rest[0] && typeof rest[0] === 'object') {
54
+ Object.assign(info, rest[0])
55
+ }
56
+ return logger.log(info)
57
+ }
58
+ return orig(first, ...rest)
59
+ }
60
+ }
61
+
62
+ ;['fatal', 'error', 'warn', 'info', 'debug', 'trace'].forEach((lvl) => {
63
+ if (typeof logger[lvl] === 'function') wrapLevel(lvl)
64
+ })
65
+
66
+ if (!process.__trojsLoggerHandlersAttached) {
67
+ process.__trojsLoggerHandlersAttached = true
68
+
69
+ process.on('uncaughtException', (err) => {
70
+ try {
71
+ logger.error(err instanceof Error ? err : new Error(String(err)))
72
+ } catch {
73
+ // eslint-disable-next-line no-console
74
+ console.error('UNCAUGHT_EXCEPTION', err)
75
+ }
76
+ })
77
+
78
+ process.on('unhandledRejection', (reason) => {
79
+ let err
80
+ if (reason instanceof Error) {
81
+ err = reason
82
+ } else if (typeof reason === 'string') {
83
+ err = new Error(reason)
84
+ } else {
85
+ try {
86
+ err = new Error(JSON.stringify(reason))
87
+ } catch {
88
+ err = new Error(String(reason))
89
+ }
90
+ }
91
+ try {
92
+ logger.error(err)
93
+ } catch {
94
+ // eslint-disable-next-line no-console
95
+ console.error('UNHANDLED_REJECTION', err)
96
+ }
97
+ })
98
+
99
+ process.on('warning', (warning) => {
100
+ try {
101
+ logger.warn(
102
+ warning instanceof Error
103
+ ? warning
104
+ : (
105
+ new Error(
106
+ `${warning.name}: ${warning.message}\n${warning.stack || ''}`
107
+ )
108
+ )
109
+ )
110
+ } catch {
111
+ // eslint-disable-next-line no-console
112
+ console.warn('PROCESS_WARNING', warning)
113
+ }
114
+ })
115
+ }
116
+
117
+ return logger
42
118
  }
@@ -1,59 +1,130 @@
1
1
  import stackDriver from '../helpers/stackdriver.js'
2
2
 
3
+ const SYMBOL_MESSAGE = Symbol.for('message')
4
+
3
5
  export default ({ winston, logger }) => {
4
6
  const defaultLevel = 'trace'
7
+ const stackTrace = logger?.debug ?? false
5
8
 
6
- const jsonFormatter = winston.format.combine(
7
- winston.format.errors({ stack: logger?.debug ?? false }),
8
- winston.format((info) => {
9
- if (!info.message) {
10
- if (info instanceof Error) {
11
- // Prefer the raw message; fall back to toString
12
- info.message = info.message || info.toString()
13
- } else {
14
- // Avoid {"level":"error"} as a message if level is the only key
15
- const clone = { ...info }
16
- delete clone.level
17
- const keys = Object.keys(clone)
18
- info.message = keys.length > 0 ? JSON.stringify(clone) : ''
9
+ const stackHead = (stack) =>
10
+ stack ? (stack.split('\n')[0] || '').trim() : ''
11
+
12
+ const ensureErrorProps = winston.format((info) => {
13
+ if (info instanceof Error) {
14
+ info.message = info.message || info.toString()
15
+ }
16
+ return info
17
+ })
18
+
19
+ const extractSymbolMessage = (info) => {
20
+ if (
21
+ (!info.message || info.message === '')
22
+ && info[SYMBOL_MESSAGE]
23
+ && typeof info[SYMBOL_MESSAGE] === 'string'
24
+ ) {
25
+ try {
26
+ const parsed = JSON.parse(info[SYMBOL_MESSAGE])
27
+ if (typeof parsed === 'string') {
28
+ info.message = parsed
19
29
  }
20
- } else if (info.message instanceof Error) {
21
- info.message = info.message.message || info.message.toString()
22
- } else if (typeof info.message !== 'string') {
23
- info.message = JSON.stringify(info.message)
24
- }
25
- if (logger?.debug && info.stack) {
26
- info.stacktrace = info.stack
30
+ } catch {
31
+ info.message = info[SYMBOL_MESSAGE]
27
32
  }
28
- return info
29
- })(),
30
- winston.format(
31
- stackDriver({ level: logger?.level, defaultLevel })
32
- )(),
33
- winston.format.json()
34
- )
33
+ }
34
+ }
35
35
 
36
- const simpleLoggerWithStack = winston.format.printf(({ level, message, stack }) => {
37
- const text = `${level.toUpperCase()}: ${message}`
38
- return stack ? `${text}\n${stack}` : text
39
- })
36
+ const attachEmbeddedError = (info) => {
37
+ const embedded
38
+ = (info.error instanceof Error && info.error)
39
+ || (info.exception instanceof Error && info.exception)
40
40
 
41
- const defaultFormatter = winston.format.combine(
42
- winston.format.errors({ stack: logger?.debug ?? false }),
43
- winston.format((info) => {
44
- if (!info.message && info instanceof Error) {
41
+ if (info instanceof Error) {
42
+ if (!info.message || info.message === '') {
45
43
  info.message = info.toString()
46
44
  }
47
- return info
48
- })(),
49
- simpleLoggerWithStack
45
+ return
46
+ }
47
+
48
+ if (info.message instanceof Error) {
49
+ info.message = info.message.message || info.message.toString()
50
+ return
51
+ }
52
+
53
+ if (embedded) {
54
+ info.message = embedded.message || embedded.toString()
55
+ if (!info.stack && embedded.stack) {
56
+ info.stack = embedded.stack
57
+ }
58
+ }
59
+ }
60
+
61
+ const stringifyNonStringMessage = (info) => {
62
+ if (info.message && typeof info.message !== 'string') {
63
+ try {
64
+ info.message = JSON.stringify(info.message)
65
+ } catch {
66
+ info.message = String(info.message)
67
+ }
68
+ }
69
+ }
70
+
71
+ const deriveMessageFromStack = (info) => {
72
+ if (info.stack && (!info.message || info.message === '')) {
73
+ info.message = stackHead(info.stack)
74
+ }
75
+ }
76
+
77
+ const finalizeEmptyMessage = (info) => {
78
+ if (!info.message || info.message === '') {
79
+ const clone = { ...info }
80
+ delete clone.level
81
+ delete clone.stack
82
+ delete clone.error
83
+ delete clone.exception
84
+ delete clone[SYMBOL_MESSAGE]
85
+ const keys = Object.keys(clone)
86
+ info.message = keys.length > 0 ? JSON.stringify(clone) : ''
87
+ }
88
+ }
89
+
90
+ const duplicateStackTraceIfDebug = (info) => {
91
+ if ((logger?.debug ?? false) && info.stack) {
92
+ info.stacktrace = info.stack
93
+ }
94
+ }
95
+
96
+ const normalizeMessage = winston.format((info) => {
97
+ extractSymbolMessage(info)
98
+ attachEmbeddedError(info)
99
+ stringifyNonStringMessage(info)
100
+ deriveMessageFromStack(info)
101
+ finalizeEmptyMessage(info)
102
+ duplicateStackTraceIfDebug(info)
103
+ return info
104
+ })
105
+
106
+ const jsonFormatter = winston.format.combine(
107
+ ensureErrorProps(),
108
+ winston.format.errors({ stack: stackTrace }),
109
+ normalizeMessage(),
110
+ winston.format(stackDriver({ level: logger?.level, defaultLevel }))(),
111
+ winston.format.json()
112
+ )
113
+
114
+ const simpleFormatter = winston.format.combine(
115
+ ensureErrorProps(),
116
+ winston.format.errors({ stack: stackTrace }),
117
+ normalizeMessage(),
118
+ winston.format.printf(({ level, message, stack }) => {
119
+ const base = `${level}: ${message || stackHead(stack)}`
120
+ return stack && (logger?.debug ?? false) ? `${base}\n${stack}` : base
121
+ })
50
122
  )
51
123
 
52
124
  return new winston.transports.Console({
53
125
  level: logger?.level || defaultLevel,
54
- format:
55
- logger?.format === 'json'
56
- ? jsonFormatter
57
- : defaultFormatter
126
+ handleExceptions: true,
127
+ handleRejections: true,
128
+ format: logger?.format === 'json' ? jsonFormatter : simpleFormatter
58
129
  })
59
130
  }