@trojs/logger 2.0.7 → 2.1.0

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