ac-logger 2.2.3 → 2.3.2

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/.ncurc.js ADDED
@@ -0,0 +1,8 @@
1
+ // List packages for minor updates
2
+ const minorUpdatePackages = ['chai']
3
+
4
+ module.exports = {
5
+ target: packageName => {
6
+ return minorUpdatePackages.includes(packageName) ? 'minor' : 'latest'
7
+ }
8
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,43 @@
1
+ <a name="2.3.2"></a>
2
+
3
+ ## [2.3.2](https://github.com/admiralcloud/ac-logger/compare/v2.3.1..v2.3.2) (2024-05-08 15:32:14)
4
+
5
+
6
+ ### Bug Fix
7
+
8
+ * **App:** Added option to change logLevel during runtime | MP | [1e59bed4b7d56dff95584fdf528f4dd931e3e6a2](https://github.com/admiralcloud/ac-logger/commit/1e59bed4b7d56dff95584fdf528f4dd931e3e6a2)
9
+ It is now possible to change loglevel after init
10
+ Related issues: [undefined/undefined#master](undefined/browse/master)
11
+ ### Chores
12
+
13
+ * **App:** Updated packages | MP | [af832d3e7cb148e625b0bcd0756d69044bcb812d](https://github.com/admiralcloud/ac-logger/commit/af832d3e7cb148e625b0bcd0756d69044bcb812d)
14
+ Updated packages
15
+ Related issues: [undefined/undefined#master](undefined/browse/master)
16
+ <a name="2.3.1"></a>
17
+
18
+ ## [2.3.1](https://github.com/admiralcloud/ac-logger/compare/v2.3.0..v2.3.1) (2024-03-10 12:31:44)
19
+
20
+
21
+ ### Bug Fix
22
+
23
+ * **App:** Minor improvement for backwards compatibily | MP | [462a6c38269dafccde1f18ffd8a079ac19022269](https://github.com/admiralcloud/ac-logger/commit/462a6c38269dafccde1f18ffd8a079ac19022269)
24
+ Minor improvement for backwards compatibility
25
+ Related issues: [undefined/undefined#master](undefined/browse/master)
26
+ <a name="2.3.0"></a>
27
+
28
+ # [2.3.0](https://github.com/admiralcloud/ac-logger/compare/v2.2.3..v2.3.0) (2024-03-10 11:09:52)
29
+
30
+
31
+ ### Feature
32
+
33
+ * **App:** Improved application logs | MP | [5d505c148e44045daf40909092542d7e96949f74](https://github.com/admiralcloud/ac-logger/commit/5d505c148e44045daf40909092542d7e96949f74)
34
+ ac-logger now supports machine readable application logs (if enabled). This way analyzing logs in Cloudwatch (or other apps) will be much easier.
35
+ Related issues: [undefined/undefined#master](undefined/browse/master)
36
+ ### Chores
37
+
38
+ * **App:** Updated packages | MP | [d2fe6c8ade4f1058e0315c30a8f4c85686f03181](https://github.com/admiralcloud/ac-logger/commit/d2fe6c8ade4f1058e0315c30a8f4c85686f03181)
39
+ Updated packages
40
+ Related issues: [undefined/undefined#master](undefined/browse/master)
1
41
  <a name="2.2.3"></a>
2
42
 
3
43
  ## [2.2.3](https://github.com/admiralcloud/ac-logger/compare/v2.2.2..v2.2.3) (2024-01-01 13:10:51)
package/README.md CHANGED
@@ -14,12 +14,16 @@ const aclog = require('ac-logger')
14
14
  const logConfig = {
15
15
  prefixFields: [
16
16
  { field: 'jobId', short: 'J' },
17
- ]
17
+ ],
18
+ applicationLogs: {
19
+ enabled: true // if true, JSON formatted, machine readable application logs will be created into logs directory
20
+ }
18
21
  }
19
22
 
20
23
  app = {} //
21
24
  app.log = aclog({ prefixFields: logConfig.prefixFields }).acLogger
22
25
 
26
+ // DEPRECATED
23
27
  let logMeta = {
24
28
  fileName: 'UploadS3',
25
29
  functionName: 'upload',
@@ -28,6 +32,9 @@ let logMeta = {
28
32
  app.log.info('Message for %s', 'some string', { meta: logMeta })
29
33
  // -> INFO UploadS3 | upload | J 193 | Message for some string
30
34
 
35
+ // MODERN APPROACH (that is best suited for machine and human readability)
36
+ app.log.info('Message for some string', { functionName: 'upload', ... }) // all meta data like "functionName" will be available in application logs
37
+
31
38
  ```
32
39
 
33
40
  ## Error logging
package/index.js CHANGED
@@ -1,43 +1,128 @@
1
1
  const _ = require('lodash')
2
2
  const moment = require('moment')
3
3
  const path = require('path')
4
+ const util = require('util')
4
5
 
5
6
  const { createLogger, format, transports, addColors } = require('winston')
6
7
  require('winston-daily-rotate-file')
7
8
 
8
- module.exports = ({ prefixFields = [], timestampFormat = 'YYYY-MM-DD HH:mm:ss', level = (process.env.NODE_ENV === 'production' ? 'info' : 'verbose'), headLength = 80, padLength = 12, customLevels, transporters = [] } = {}) => {
9
+ module.exports = ({ prefixFields = [], timestampFormat = 'YYYY-MM-DD HH:mm:ss', level = (process.env.NODE_ENV === 'production' ? 'info' : 'verbose'), headLength = 80, padLength = 12, customLevels, transporters = [], applicationLogs = { enabled: false } } = {}) => {
9
10
  const precisionMap = [
10
11
  { precision: 1e6, name: 'ms' },
11
12
  { precision: 1e3, name: 'µs' },
12
13
  { precision: 1, name: 'ns' },
13
14
  ]
14
15
 
15
- const myFormat = format.printf(({ timestamp, level, message, meta, e }) => {
16
- const fileName = _.get(meta, 'fileName') ? _.get(meta, 'fileName') + ' | ' : ''
17
- const functionName = _.get(meta, 'functionName') ? _.get(meta, 'functionName') + ' | ' : ''
18
- const subName = _.get(meta, 'sub') ? _.get(meta, 'sub') + ' | ' : ''
19
-
20
- let prefix = []
21
- let data = []
22
- _.forEach(prefixFields, item => {
23
- if (_.get(meta, _.get(item, 'field'))) {
24
- prefix.push(_.get(item, 'short'))
25
- data.push(_.get(meta, _.get(item, 'field')))
26
- }
27
- })
28
- const prefixData = _.size(prefix) ? (_.join(prefix, '/') + ' ' + _.join(data, '/') + ' | ') : ''
29
- if (e instanceof Error) {
30
- // log the stack
31
- console.error(e)
32
- if (_.get(e, 'message')) message += ' | ' + _.get(e, 'message', '')
16
+ const splatFormatter = format((data) => {
17
+ // Check if splat is available and the message contains placeholder(s)
18
+ if (data[Symbol.for('splat')] && /%[sjdifoO%]/.test(data.message)) {
19
+ data.message = util.format(data.message, ...data[Symbol.for('splat')])
33
20
  }
34
- return `${timestamp} ${level} ${fileName}${functionName}${subName}${prefixData}${message}`
21
+ return data
35
22
  })
36
23
 
24
+ const myFormat = format.printf((data) => {
25
+ const level = data[Symbol.for('level')]
26
+ // http log format
27
+ if (level === 'http' && data?.controller) {
28
+ // display accessKey, link (if applicable)
29
+ let message = data?.message ? ` ${data.message}` : ''
30
+ let cuid = data?.customerId ? data?.customerId : ''
31
+ if (data?.userId) cuid += `/${data.userId} `
32
+ return `${data?.timestamp} ${data?.level} ${data?.ip} ${data?.iso2 || ''} ${data?.accessKey || ''} ${cuid}${data?.controller} ${data?.action}${message} | ${data?.statusCode} | ${data?.performance?.executionTime}ms`
33
+ }
34
+ else {
35
+ const meta = data?.meta
36
+ //
37
+ let fileName = _.get(meta, 'fileName') ? _.get(meta, 'fileName') + ' | ' : ''
38
+ let functionName = _.get(meta, 'functionName') ? _.get(meta, 'functionName') + ' | ' : _.get(data, 'functionName') ? _.get(data, 'functionName') + ' | ' : ''
39
+ let subName = _.get(meta, 'sub') ? _.get(meta, 'sub') + ' | ' : _.get(data, 'sub') ? _.get(data, 'sub') + ' | ' : ''
40
+
41
+ // modern approach (fileName, functionName, sub) // TBD with team
42
+ let functionIdentifier = _.get(data, 'functionIdentifier') ? _.get(data, 'functionIdentifier') + ' | ' : ''
43
+ if (!fileName && _.get(data, 'fileName')) fileName = _.get(data, 'fileName')
44
+ if (!functionName && _.get(data, 'functionName')) functionName = _.get(data, 'functionName')
45
+ if (!subName && _.get(data, 'sub')) subName = _.get(data, 'sub')
46
+
47
+
48
+ let message = data?.message
49
+
50
+
51
+ let prefix = []
52
+ let dataFromPrefix = []
53
+ _.forEach(prefixFields, item => {
54
+ if (_.get(meta, _.get(item, 'field'))) {
55
+ prefix.push(_.get(item, 'short'))
56
+ dataFromPrefix.push(_.get(meta, _.get(item, 'field')))
57
+ }
58
+ })
59
+ const prefixData = _.size(prefix) ? (_.join(prefix, '/') + ' ' + _.join(dataFromPrefix, '/') + ' | ') : ''
60
+ if (data?.e instanceof Error) {
61
+ // log the stack
62
+ console.error(data?.e)
63
+ if (data?.e?.message) message += ' | ' + ( data?.e?.message || '')
64
+ }
65
+ // TODO: for better readability, we shold use padding for fileName, functionname, etc
66
+ return `${data?.timestamp} ${data?.level} ${fileName}${functionName}${functionIdentifier}${subName}${prefixData}${message}`
67
+ }
68
+ })
69
+
70
+
37
71
  const logTransports = []
38
72
  if (!_.size(transporters)) {
39
73
  // default behaviour
40
- logTransports.push(new transports.Console())
74
+ // human-readable console output
75
+ logTransports.push(new transports.Console({
76
+ format: format.combine(
77
+ format.timestamp({
78
+ format: timestampFormat
79
+ }),
80
+ format.errors({ stack: true }),
81
+ format(info => {
82
+ info.level = _.padEnd(info.level.toUpperCase(), 8)
83
+ return info
84
+ })(),
85
+ format.colorize(),
86
+ splatFormatter(),
87
+ myFormat
88
+ ),
89
+ }))
90
+
91
+ // structured logging for analysis and querying within the CloudWatch service.
92
+ if (applicationLogs?.enabled) {
93
+ let filename = applicationLogs?.filename || 'application.log'
94
+ const symlinkName = filename
95
+ if (filename.endsWith('.log')) {
96
+ // If the filename ends with .log, insert -%DATE% before the extension
97
+ filename = filename.replace(/\.log$/, '-%DATE%.log');
98
+ }
99
+ else {
100
+ // If the filename doesn't end with .log, append -%DATE%.log
101
+ filename = `${filename}-%DATE%.log`;
102
+ }
103
+
104
+ const transport = new transports.DailyRotateFile({
105
+ filename,
106
+ datePattern: 'YYYY-MM-DD',
107
+ zippedArchive: true, // Enable gzip compression for rotated files
108
+ maxSize: applicationLogs?.maxSize || '100m', // Rotate the file when it reaches 20MB
109
+ maxFiles: applicationLogs?.maxFiles || '7d', // Keep rotated files for 7 days
110
+ dirname: applicationLogs?.firname || './logs', // Specify the directory for log files
111
+ format: format.combine(
112
+ format.timestamp(),
113
+ splatFormatter(),
114
+ format.json()
115
+ ),
116
+ createSymlink: true, // Enable symlink creation
117
+ symlinkName: symlinkName // Fixed name for the current log file symlink
118
+ })
119
+
120
+ transport.on('error', error => {
121
+ acLogger.error('ac-logger | ApplicationLogs | TransportError | %j', error?.message)
122
+ })
123
+ logTransports.push(transport)
124
+ }
125
+
41
126
  if (process.env.NODE_ENV === 'test') {
42
127
  logTransports.push(new transports.File({
43
128
  filename: 'test.log',
@@ -55,19 +140,6 @@ module.exports = ({ prefixFields = [], timestampFormat = 'YYYY-MM-DD HH:mm:ss',
55
140
 
56
141
  const logConfig = {
57
142
  level,
58
- format: format.combine(
59
- format.timestamp({
60
- format: timestampFormat
61
- }),
62
- format.errors({ stack: true }),
63
- format(info => {
64
- info.level = _.padEnd(info.level.toUpperCase(), 8)
65
- return info
66
- })(),
67
- format.colorize(),
68
- format.splat(),
69
- myFormat
70
- ),
71
143
  transports: logTransports
72
144
  }
73
145
  if (_.get(customLevels, 'levels')) _.set(logConfig, 'levels', _.get(customLevels, 'levels'))
@@ -75,6 +147,15 @@ module.exports = ({ prefixFields = [], timestampFormat = 'YYYY-MM-DD HH:mm:ss',
75
147
  const acLogger = createLogger(logConfig)
76
148
  if (_.get(customLevels, 'colors')) addColors(_.get(customLevels, 'colors'))
77
149
 
150
+ const changeLogLevel = (newLevel) => {
151
+ acLogger.transports.forEach((transport) => {
152
+ const currentLevel = transport.level || acLogger.level; // Fallback to the logger's default level
153
+ if (currentLevel !== newLevel) {
154
+ console.log(`Changing level of ${transport.name} from ${currentLevel} to ${newLevel}`);
155
+ transport.level = newLevel; // Confirm this line is executing
156
+ }
157
+ });
158
+ };
78
159
 
79
160
  const headline = (params) => {
80
161
  acLogger.info('')
@@ -158,6 +239,7 @@ module.exports = ({ prefixFields = [], timestampFormat = 'YYYY-MM-DD HH:mm:ss',
158
239
 
159
240
  return {
160
241
  acLogger,
242
+ changeLogLevel,
161
243
  headline,
162
244
  functionStartLine,
163
245
  listing,
package/package.json CHANGED
@@ -3,19 +3,19 @@
3
3
  "author": "Mark Poepping (https://www.admiralcloud.com)",
4
4
  "license": "MIT",
5
5
  "repository": "admiralcloud/ac-logger",
6
- "version": "2.2.3",
6
+ "version": "2.3.2",
7
7
  "dependencies": {
8
8
  "lodash": "^4.17.21",
9
9
  "moment": "^2.30.1",
10
- "winston": "^3.11.0",
11
- "winston-daily-rotate-file": "^4.7.1"
10
+ "winston": "^3.13.0",
11
+ "winston-daily-rotate-file": "^5.0.0"
12
12
  },
13
13
  "devDependencies": {
14
14
  "ac-semantic-release": "^0.4.2",
15
- "chai": "^4.3.10",
16
- "eslint": "^8.56.0",
15
+ "chai": "^4.4.1",
16
+ "eslint": "^8.57.0",
17
17
  "intercept-stdout": "^0.1.2",
18
- "mocha": "^10.2.0",
18
+ "mocha": "^10.4.0",
19
19
  "mocha-jenkins-reporter": "^0.4.8"
20
20
  },
21
21
  "scripts": {
package/test/test.js CHANGED
@@ -23,6 +23,30 @@ describe('Tests', () => {
23
23
  expect(lines[9]).to.contain('AppName')
24
24
  expect(lines[9]).to.contain('myApp')
25
25
  })
26
+
27
+
28
+ it('Log info', () => {
29
+ captured_text = ''
30
+ const log = aclog().acLogger
31
+ log.info('Hello Info')
32
+ log.debug('Hello Debug')
33
+ const lines = captured_text.split('\n')
34
+ expect(lines[0]).to.contain('INFO')
35
+ expect(lines[1]).not.to.contain('DEBUG')
36
+ })
37
+
38
+ it('Log debug', () => {
39
+ const loggerSetup = aclog();
40
+ const log = loggerSetup.acLogger
41
+ loggerSetup.changeLogLevel('debug')
42
+ captured_text = ''
43
+ log.info('Hello Info')
44
+ log.debug('Hello Debug')
45
+ const lines = captured_text.split('\n')
46
+ expect(lines[0]).to.contain('INFO')
47
+ expect(lines[1]).to.contain('DEBUG')
48
+ })
49
+
26
50
 
27
51
  it('Unhook', () => {
28
52
  unhook_intercept()