@trojs/logger 0.5.16 → 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.16",
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,61 +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
- info.message = info.message || info.toString()
12
- } else {
13
- const clone = { ...info }
14
- delete clone.level
15
- const keys = Object.keys(clone)
16
- 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
17
29
  }
18
- } else if (info.message instanceof Error) {
19
- info.message = info.message.message || info.message.toString()
20
- } else if (typeof info.message !== 'string') {
21
- info.message = JSON.stringify(info.message)
30
+ } catch {
31
+ info.message = info[SYMBOL_MESSAGE]
22
32
  }
23
- if (logger?.debug && info.stack) {
24
- info.stacktrace = info.stack
33
+ }
34
+ }
35
+
36
+ const attachEmbeddedError = (info) => {
37
+ const embedded
38
+ = (info.error instanceof Error && info.error)
39
+ || (info.exception instanceof Error && info.exception)
40
+
41
+ if (info instanceof Error) {
42
+ if (!info.message || info.message === '') {
43
+ info.message = info.toString()
25
44
  }
26
- return info
27
- })(),
28
- winston.format(
29
- stackDriver({ level: logger?.level, defaultLevel })
30
- )(),
31
- winston.format.json()
32
- )
45
+ return
46
+ }
33
47
 
34
- const simpleLoggerWithStack = winston.format.printf(({ level, message, stack }) => {
35
- const text = `${level.toUpperCase()}: ${message}`
36
- return stack ? `${text}\n${stack}` : text
37
- })
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
+ }
38
60
 
39
- const defaultFormatter = winston.format.combine(
40
- winston.format.errors({ stack: logger?.debug ?? false }),
41
- winston.format((info) => {
42
- if (info instanceof Error) {
43
- info.message = info.message || info.toString()
44
- } else if (!info.message) {
45
- info.message = ''
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)
46
67
  }
47
- return info
48
- })(),
49
- simpleLoggerWithStack
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
126
  handleExceptions: true,
55
127
  handleRejections: true,
56
- format:
57
- logger?.format === 'json'
58
- ? jsonFormatter
59
- : defaultFormatter
128
+ format: logger?.format === 'json' ? jsonFormatter : simpleFormatter
60
129
  })
61
130
  }