@unito/integration-sdk 1.0.26 → 1.0.27

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.
@@ -34,6 +34,35 @@ var LogLevel;
34
34
  LogLevel["LOG"] = "log";
35
35
  LogLevel["DEBUG"] = "debug";
36
36
  })(LogLevel || (LogLevel = {}));
37
+ /**
38
+ * See https://docs.datadoghq.com/logs/log_collection/?tab=host#custom-log-forwarding
39
+ * - Datadog Agent splits at 256kB (256000 bytes)...
40
+ * - ... but the same docs say that "for optimal performance, it is
41
+ * recommended that an individual log be no greater than 25kB"
42
+ * -> Truncating at 25kB - a bit of wiggle room for metadata = 20kB.
43
+ */
44
+ const MAX_LOG_MESSAGE_SIZE = parseInt(process.env.MAX_LOG_MESSAGE_SIZE ?? '20000', 10);
45
+ const LOG_LINE_TRUNCATED_SUFFIX = ' - LOG LINE TRUNCATED';
46
+ /**
47
+ * For *LogMeta* sanitization, we let in anything that was passed, except for clearly-problematic keys
48
+ */
49
+ const LOGMETA_BLACKLIST = [
50
+ // Security
51
+ 'access_token',
52
+ 'bot_auth_code',
53
+ 'client_secret',
54
+ 'jwt',
55
+ 'oauth_token',
56
+ 'password',
57
+ 'refresh_token',
58
+ 'shared_secret',
59
+ 'token',
60
+ // Privacy
61
+ 'billing_email',
62
+ 'email',
63
+ 'first_name',
64
+ 'last_name',
65
+ ];
37
66
  /**
38
67
  * Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
39
68
  */
@@ -113,20 +142,27 @@ class Logger {
113
142
  this.metadata = {};
114
143
  }
115
144
  send(logLevel, message, metadata) {
116
- const processedMessage = this.snakifyKeys({
117
- ...this.metadata,
118
- ...metadata,
145
+ // We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
146
+ const date = Date.now();
147
+ if (message.length > MAX_LOG_MESSAGE_SIZE) {
148
+ message = `${message.substring(0, MAX_LOG_MESSAGE_SIZE)}${LOG_LINE_TRUNCATED_SUFFIX}`;
149
+ }
150
+ let processedMetadata = Logger.snakifyKeys({ ...this.metadata, ...metadata, logMessageSize: message.length });
151
+ processedMetadata = Logger.pruneSensitiveMetadata(processedMetadata);
152
+ const processedLogs = {
153
+ ...processedMetadata,
119
154
  message,
155
+ date,
120
156
  status: logLevel,
121
- });
157
+ };
122
158
  if (process.env.NODE_ENV === 'development') {
123
- console[logLevel](JSON.stringify(processedMessage, null, 2));
159
+ console[logLevel](JSON.stringify(processedLogs, null, 2));
124
160
  }
125
161
  else {
126
- console[logLevel](JSON.stringify(processedMessage));
162
+ console[logLevel](JSON.stringify(processedLogs));
127
163
  }
128
164
  }
129
- snakifyKeys(value) {
165
+ static snakifyKeys(value) {
130
166
  const result = {};
131
167
  for (const key in value) {
132
168
  const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
@@ -135,6 +171,22 @@ class Logger {
135
171
  }
136
172
  return result;
137
173
  }
174
+ static pruneSensitiveMetadata(metadata, topLevelMeta) {
175
+ const prunedMetadata = {};
176
+ for (const key in metadata) {
177
+ if (LOGMETA_BLACKLIST.includes(key)) {
178
+ prunedMetadata[key] = '[REDACTED]';
179
+ (topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
180
+ }
181
+ else if (typeof metadata[key] === 'object') {
182
+ prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
183
+ }
184
+ else {
185
+ prunedMetadata[key] = metadata[key];
186
+ }
187
+ }
188
+ return prunedMetadata;
189
+ }
138
190
  }
139
191
 
140
192
  /**
@@ -65,6 +65,7 @@ export default class Logger {
65
65
  */
66
66
  clearMetadata(): void;
67
67
  private send;
68
- private snakifyKeys;
68
+ private static snakifyKeys;
69
+ private static pruneSensitiveMetadata;
69
70
  }
70
71
  export {};
@@ -6,6 +6,35 @@ var LogLevel;
6
6
  LogLevel["LOG"] = "log";
7
7
  LogLevel["DEBUG"] = "debug";
8
8
  })(LogLevel || (LogLevel = {}));
9
+ /**
10
+ * See https://docs.datadoghq.com/logs/log_collection/?tab=host#custom-log-forwarding
11
+ * - Datadog Agent splits at 256kB (256000 bytes)...
12
+ * - ... but the same docs say that "for optimal performance, it is
13
+ * recommended that an individual log be no greater than 25kB"
14
+ * -> Truncating at 25kB - a bit of wiggle room for metadata = 20kB.
15
+ */
16
+ const MAX_LOG_MESSAGE_SIZE = parseInt(process.env.MAX_LOG_MESSAGE_SIZE ?? '20000', 10);
17
+ const LOG_LINE_TRUNCATED_SUFFIX = ' - LOG LINE TRUNCATED';
18
+ /**
19
+ * For *LogMeta* sanitization, we let in anything that was passed, except for clearly-problematic keys
20
+ */
21
+ const LOGMETA_BLACKLIST = [
22
+ // Security
23
+ 'access_token',
24
+ 'bot_auth_code',
25
+ 'client_secret',
26
+ 'jwt',
27
+ 'oauth_token',
28
+ 'password',
29
+ 'refresh_token',
30
+ 'shared_secret',
31
+ 'token',
32
+ // Privacy
33
+ 'billing_email',
34
+ 'email',
35
+ 'first_name',
36
+ 'last_name',
37
+ ];
9
38
  /**
10
39
  * Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
11
40
  */
@@ -85,20 +114,27 @@ export default class Logger {
85
114
  this.metadata = {};
86
115
  }
87
116
  send(logLevel, message, metadata) {
88
- const processedMessage = this.snakifyKeys({
89
- ...this.metadata,
90
- ...metadata,
117
+ // We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
118
+ const date = Date.now();
119
+ if (message.length > MAX_LOG_MESSAGE_SIZE) {
120
+ message = `${message.substring(0, MAX_LOG_MESSAGE_SIZE)}${LOG_LINE_TRUNCATED_SUFFIX}`;
121
+ }
122
+ let processedMetadata = Logger.snakifyKeys({ ...this.metadata, ...metadata, logMessageSize: message.length });
123
+ processedMetadata = Logger.pruneSensitiveMetadata(processedMetadata);
124
+ const processedLogs = {
125
+ ...processedMetadata,
91
126
  message,
127
+ date,
92
128
  status: logLevel,
93
- });
129
+ };
94
130
  if (process.env.NODE_ENV === 'development') {
95
- console[logLevel](JSON.stringify(processedMessage, null, 2));
131
+ console[logLevel](JSON.stringify(processedLogs, null, 2));
96
132
  }
97
133
  else {
98
- console[logLevel](JSON.stringify(processedMessage));
134
+ console[logLevel](JSON.stringify(processedLogs));
99
135
  }
100
136
  }
101
- snakifyKeys(value) {
137
+ static snakifyKeys(value) {
102
138
  const result = {};
103
139
  for (const key in value) {
104
140
  const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
@@ -107,4 +143,20 @@ export default class Logger {
107
143
  }
108
144
  return result;
109
145
  }
146
+ static pruneSensitiveMetadata(metadata, topLevelMeta) {
147
+ const prunedMetadata = {};
148
+ for (const key in metadata) {
149
+ if (LOGMETA_BLACKLIST.includes(key)) {
150
+ prunedMetadata[key] = '[REDACTED]';
151
+ (topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
152
+ }
153
+ else if (typeof metadata[key] === 'object') {
154
+ prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
155
+ }
156
+ else {
157
+ prunedMetadata[key] = metadata[key];
158
+ }
159
+ }
160
+ return prunedMetadata;
161
+ }
110
162
  }
@@ -32,86 +32,112 @@ describe('Logger', () => {
32
32
  assert.strictEqual(logSpy.mock.calls.length, 0);
33
33
  logger.log('test');
34
34
  assert.strictEqual(logSpy.mock.calls.length, 1);
35
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
36
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'log' }),
37
- ]);
35
+ let actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
36
+ assert.ok(actual['date']);
37
+ assert.equal(actual['correlation_id'], '123456789');
38
+ assert.equal(actual['message'], 'test');
39
+ assert.equal(actual['status'], 'log');
38
40
  const errorSpy = testContext.mock.method(global.console, 'error', () => { });
39
41
  assert.strictEqual(errorSpy.mock.calls.length, 0);
40
42
  logger.error('test');
41
43
  assert.strictEqual(errorSpy.mock.calls.length, 1);
42
- assert.deepEqual(errorSpy.mock.calls[0]?.arguments, [
43
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'error' }),
44
- ]);
44
+ actual = JSON.parse(errorSpy.mock.calls[0]?.arguments[0]);
45
+ assert.ok(actual['date']);
46
+ assert.equal(actual['correlation_id'], '123456789');
47
+ assert.equal(actual['message'], 'test');
48
+ assert.equal(actual['status'], 'error');
45
49
  const warnSpy = testContext.mock.method(global.console, 'warn', () => { });
46
50
  assert.strictEqual(warnSpy.mock.calls.length, 0);
47
51
  logger.warn('test');
48
52
  assert.strictEqual(warnSpy.mock.calls.length, 1);
49
- assert.deepEqual(warnSpy.mock.calls[0]?.arguments, [
50
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'warn' }),
51
- ]);
53
+ actual = JSON.parse(warnSpy.mock.calls[0]?.arguments[0]);
54
+ assert.ok(actual['date']);
55
+ assert.equal(actual['correlation_id'], '123456789');
56
+ assert.equal(actual['message'], 'test');
57
+ assert.equal(actual['status'], 'warn');
52
58
  const infoSpy = testContext.mock.method(global.console, 'info', () => { });
53
59
  assert.strictEqual(infoSpy.mock.calls.length, 0);
54
60
  logger.info('test');
55
61
  assert.strictEqual(infoSpy.mock.calls.length, 1);
56
- assert.deepEqual(infoSpy.mock.calls[0]?.arguments, [
57
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'info' }),
58
- ]);
62
+ actual = JSON.parse(infoSpy.mock.calls[0]?.arguments[0]);
63
+ assert.ok(actual['date']);
64
+ assert.equal(actual['correlation_id'], '123456789');
65
+ assert.equal(actual['message'], 'test');
66
+ assert.equal(actual['status'], 'info');
59
67
  const debugSpy = testContext.mock.method(global.console, 'debug', () => { });
60
68
  assert.strictEqual(debugSpy.mock.calls.length, 0);
61
69
  logger.debug('test');
62
70
  assert.strictEqual(debugSpy.mock.calls.length, 1);
63
- assert.deepEqual(debugSpy.mock.calls[0]?.arguments, [
64
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'debug' }),
65
- ]);
71
+ actual = JSON.parse(debugSpy.mock.calls[0]?.arguments[0]);
72
+ assert.ok(actual['date']);
73
+ assert.equal(actual['correlation_id'], '123456789');
74
+ assert.equal(actual['message'], 'test');
75
+ assert.equal(actual['status'], 'debug');
66
76
  });
67
77
  it('merges message payload with metadata', testContext => {
68
- const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
69
- const logger = new Logger(metadata);
70
78
  const logSpy = testContext.mock.method(global.console, 'log', () => { });
71
79
  assert.strictEqual(logSpy.mock.calls.length, 0);
80
+ const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
81
+ const logger = new Logger(metadata);
72
82
  logger.log('test', { error: { code: '200', message: 'Page Not Found' } });
73
83
  assert.strictEqual(logSpy.mock.calls.length, 1);
74
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
75
- JSON.stringify({
76
- correlation_id: '123456789',
77
- http: { method: 'GET' },
78
- error: { code: '200', message: 'Page Not Found' },
79
- message: 'test',
80
- status: 'log',
81
- }),
82
- ]);
84
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
85
+ assert.ok(actual['date']);
86
+ assert.equal(actual['correlation_id'], '123456789');
87
+ assert.equal(actual['message'], 'test');
88
+ assert.equal(actual['status'], 'log');
89
+ assert.deepEqual(actual['http'], { method: 'GET' });
90
+ assert.deepEqual(actual['error'], { code: '200', message: 'Page Not Found' });
83
91
  });
84
92
  it('overwrites conflicting metadata keys (1st level) with message payload', testContext => {
85
- const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
86
- const logger = new Logger(metadata);
87
93
  const logSpy = testContext.mock.method(global.console, 'log', () => { });
88
94
  assert.strictEqual(logSpy.mock.calls.length, 0);
95
+ const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
96
+ const logger = new Logger(metadata);
89
97
  logger.log('test', { http: { status_code: 200 } });
90
98
  assert.strictEqual(logSpy.mock.calls.length, 1);
91
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
92
- JSON.stringify({
93
- correlation_id: '123456789',
94
- http: { status_code: 200 },
95
- message: 'test',
96
- status: 'log',
97
- }),
98
- ]);
99
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
100
+ assert.ok(actual['date']);
101
+ assert.equal(actual['correlation_id'], '123456789');
102
+ assert.equal(actual['message'], 'test');
103
+ assert.equal(actual['status'], 'log');
104
+ assert.deepEqual(actual['http'], { status_code: 200 });
99
105
  });
100
106
  it('snakify keys of Message and Metadata', testContext => {
107
+ const logSpy = testContext.mock.method(global.console, 'log', () => { });
108
+ assert.strictEqual(logSpy.mock.calls.length, 0);
101
109
  const metadata = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 } };
102
110
  const logger = new Logger(metadata);
111
+ logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
112
+ assert.strictEqual(logSpy.mock.calls.length, 1);
113
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
114
+ assert.ok(actual['date']);
115
+ assert.equal(actual['correlation_id'], '123456789');
116
+ assert.equal(actual['message'], 'test');
117
+ assert.equal(actual['status'], 'log');
118
+ assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 });
119
+ assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' });
120
+ });
121
+ it('prunes sensitive Metadata', testContext => {
103
122
  const logSpy = testContext.mock.method(global.console, 'log', () => { });
104
123
  assert.strictEqual(logSpy.mock.calls.length, 0);
124
+ const metadata = {
125
+ correlationId: '123456789',
126
+ http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' },
127
+ user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } },
128
+ access_token: 'secret',
129
+ };
130
+ const logger = new Logger(metadata);
105
131
  logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
106
132
  assert.strictEqual(logSpy.mock.calls.length, 1);
107
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
108
- JSON.stringify({
109
- correlation_id: '123456789',
110
- http: { method: 'GET', status_code: 200 },
111
- error_context: { error_code: 200, error_message: 'Page Not Found' },
112
- message: 'test',
113
- status: 'log',
114
- }),
115
- ]);
133
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
134
+ assert.ok(actual['date']);
135
+ assert.equal(actual['correlation_id'], '123456789');
136
+ assert.equal(actual['message'], 'test');
137
+ assert.equal(actual['status'], 'log');
138
+ assert.equal(actual['has_sensitive_attribute'], true);
139
+ assert.equal(actual['access_token'], '[REDACTED]');
140
+ assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
141
+ assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
116
142
  });
117
143
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -14,6 +14,37 @@ type Value = {
14
14
  export type Metadata = Value & { message?: never };
15
15
  type ForbidenMetadataKey = 'message';
16
16
 
17
+ /**
18
+ * See https://docs.datadoghq.com/logs/log_collection/?tab=host#custom-log-forwarding
19
+ * - Datadog Agent splits at 256kB (256000 bytes)...
20
+ * - ... but the same docs say that "for optimal performance, it is
21
+ * recommended that an individual log be no greater than 25kB"
22
+ * -> Truncating at 25kB - a bit of wiggle room for metadata = 20kB.
23
+ */
24
+ const MAX_LOG_MESSAGE_SIZE = parseInt(process.env.MAX_LOG_MESSAGE_SIZE ?? '20000', 10);
25
+ const LOG_LINE_TRUNCATED_SUFFIX = ' - LOG LINE TRUNCATED';
26
+
27
+ /**
28
+ * For *LogMeta* sanitization, we let in anything that was passed, except for clearly-problematic keys
29
+ */
30
+ const LOGMETA_BLACKLIST = [
31
+ // Security
32
+ 'access_token',
33
+ 'bot_auth_code',
34
+ 'client_secret',
35
+ 'jwt',
36
+ 'oauth_token',
37
+ 'password',
38
+ 'refresh_token',
39
+ 'shared_secret',
40
+ 'token',
41
+ // Privacy
42
+ 'billing_email',
43
+ 'email',
44
+ 'first_name',
45
+ 'last_name',
46
+ ];
47
+
17
48
  /**
18
49
  * Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
19
50
  */
@@ -107,21 +138,31 @@ export default class Logger {
107
138
  }
108
139
 
109
140
  private send(logLevel: LogLevel, message: string, metadata?: Metadata): void {
110
- const processedMessage = this.snakifyKeys({
111
- ...this.metadata,
112
- ...metadata,
141
+ // We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
142
+ const date = Date.now();
143
+
144
+ if (message.length > MAX_LOG_MESSAGE_SIZE) {
145
+ message = `${message.substring(0, MAX_LOG_MESSAGE_SIZE)}${LOG_LINE_TRUNCATED_SUFFIX}`;
146
+ }
147
+
148
+ let processedMetadata = Logger.snakifyKeys({ ...this.metadata, ...metadata, logMessageSize: message.length });
149
+ processedMetadata = Logger.pruneSensitiveMetadata(processedMetadata);
150
+
151
+ const processedLogs = {
152
+ ...processedMetadata,
113
153
  message,
154
+ date,
114
155
  status: logLevel,
115
- });
156
+ };
116
157
 
117
158
  if (process.env.NODE_ENV === 'development') {
118
- console[logLevel](JSON.stringify(processedMessage, null, 2));
159
+ console[logLevel](JSON.stringify(processedLogs, null, 2));
119
160
  } else {
120
- console[logLevel](JSON.stringify(processedMessage));
161
+ console[logLevel](JSON.stringify(processedLogs));
121
162
  }
122
163
  }
123
164
 
124
- private snakifyKeys<T extends Value>(value: T): T {
165
+ private static snakifyKeys(value: Value): Value {
125
166
  const result: Value = {};
126
167
 
127
168
  for (const key in value) {
@@ -130,6 +171,23 @@ export default class Logger {
130
171
  result[snakifiedKey] = deepValue;
131
172
  }
132
173
 
133
- return result as T;
174
+ return result;
175
+ }
176
+
177
+ private static pruneSensitiveMetadata(metadata: Value, topLevelMeta?: Value): Value {
178
+ const prunedMetadata: Value = {};
179
+
180
+ for (const key in metadata) {
181
+ if (LOGMETA_BLACKLIST.includes(key)) {
182
+ prunedMetadata[key] = '[REDACTED]';
183
+ (topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
184
+ } else if (typeof metadata[key] === 'object') {
185
+ prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key] as Value, topLevelMeta ?? prunedMetadata);
186
+ } else {
187
+ prunedMetadata[key] = metadata[key];
188
+ }
189
+ }
190
+
191
+ return prunedMetadata;
134
192
  }
135
193
  }
@@ -41,96 +41,131 @@ describe('Logger', () => {
41
41
  assert.strictEqual(logSpy.mock.calls.length, 0);
42
42
  logger.log('test');
43
43
  assert.strictEqual(logSpy.mock.calls.length, 1);
44
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
45
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'log' }),
46
- ]);
44
+ let actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
45
+ assert.ok(actual['date']);
46
+ assert.equal(actual['correlation_id'], '123456789');
47
+ assert.equal(actual['message'], 'test');
48
+ assert.equal(actual['status'], 'log');
47
49
 
48
50
  const errorSpy = testContext.mock.method(global.console, 'error', () => {});
49
51
  assert.strictEqual(errorSpy.mock.calls.length, 0);
50
52
  logger.error('test');
51
53
  assert.strictEqual(errorSpy.mock.calls.length, 1);
52
- assert.deepEqual(errorSpy.mock.calls[0]?.arguments, [
53
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'error' }),
54
- ]);
54
+ actual = JSON.parse(errorSpy.mock.calls[0]?.arguments[0]);
55
+ assert.ok(actual['date']);
56
+ assert.equal(actual['correlation_id'], '123456789');
57
+ assert.equal(actual['message'], 'test');
58
+ assert.equal(actual['status'], 'error');
55
59
 
56
60
  const warnSpy = testContext.mock.method(global.console, 'warn', () => {});
57
61
  assert.strictEqual(warnSpy.mock.calls.length, 0);
58
62
  logger.warn('test');
59
63
  assert.strictEqual(warnSpy.mock.calls.length, 1);
60
- assert.deepEqual(warnSpy.mock.calls[0]?.arguments, [
61
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'warn' }),
62
- ]);
64
+ actual = JSON.parse(warnSpy.mock.calls[0]?.arguments[0]);
65
+ assert.ok(actual['date']);
66
+ assert.equal(actual['correlation_id'], '123456789');
67
+ assert.equal(actual['message'], 'test');
68
+ assert.equal(actual['status'], 'warn');
63
69
 
64
70
  const infoSpy = testContext.mock.method(global.console, 'info', () => {});
65
71
  assert.strictEqual(infoSpy.mock.calls.length, 0);
66
72
  logger.info('test');
67
73
  assert.strictEqual(infoSpy.mock.calls.length, 1);
68
- assert.deepEqual(infoSpy.mock.calls[0]?.arguments, [
69
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'info' }),
70
- ]);
74
+ actual = JSON.parse(infoSpy.mock.calls[0]?.arguments[0]);
75
+ assert.ok(actual['date']);
76
+ assert.equal(actual['correlation_id'], '123456789');
77
+ assert.equal(actual['message'], 'test');
78
+ assert.equal(actual['status'], 'info');
71
79
 
72
80
  const debugSpy = testContext.mock.method(global.console, 'debug', () => {});
73
81
  assert.strictEqual(debugSpy.mock.calls.length, 0);
74
82
  logger.debug('test');
75
83
  assert.strictEqual(debugSpy.mock.calls.length, 1);
76
- assert.deepEqual(debugSpy.mock.calls[0]?.arguments, [
77
- JSON.stringify({ correlation_id: '123456789', message: 'test', status: 'debug' }),
78
- ]);
84
+ actual = JSON.parse(debugSpy.mock.calls[0]?.arguments[0]);
85
+ assert.ok(actual['date']);
86
+ assert.equal(actual['correlation_id'], '123456789');
87
+ assert.equal(actual['message'], 'test');
88
+ assert.equal(actual['status'], 'debug');
79
89
  });
80
90
 
81
91
  it('merges message payload with metadata', testContext => {
82
- const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
83
- const logger = new Logger(metadata);
84
-
85
92
  const logSpy = testContext.mock.method(global.console, 'log', () => {});
86
93
  assert.strictEqual(logSpy.mock.calls.length, 0);
94
+
95
+ const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
96
+ const logger = new Logger(metadata);
87
97
  logger.log('test', { error: { code: '200', message: 'Page Not Found' } });
88
98
  assert.strictEqual(logSpy.mock.calls.length, 1);
89
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
90
- JSON.stringify({
91
- correlation_id: '123456789',
92
- http: { method: 'GET' },
93
- error: { code: '200', message: 'Page Not Found' },
94
- message: 'test',
95
- status: 'log',
96
- }),
97
- ]);
99
+
100
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
101
+
102
+ assert.ok(actual['date']);
103
+ assert.equal(actual['correlation_id'], '123456789');
104
+ assert.equal(actual['message'], 'test');
105
+ assert.equal(actual['status'], 'log');
106
+ assert.deepEqual(actual['http'], { method: 'GET' });
107
+ assert.deepEqual(actual['error'], { code: '200', message: 'Page Not Found' });
98
108
  });
99
109
 
100
110
  it('overwrites conflicting metadata keys (1st level) with message payload', testContext => {
101
- const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
102
- const logger = new Logger(metadata);
103
-
104
111
  const logSpy = testContext.mock.method(global.console, 'log', () => {});
105
112
  assert.strictEqual(logSpy.mock.calls.length, 0);
113
+
114
+ const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
115
+ const logger = new Logger(metadata);
106
116
  logger.log('test', { http: { status_code: 200 } });
107
117
  assert.strictEqual(logSpy.mock.calls.length, 1);
108
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
109
- JSON.stringify({
110
- correlation_id: '123456789',
111
- http: { status_code: 200 },
112
- message: 'test',
113
- status: 'log',
114
- }),
115
- ]);
118
+
119
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
120
+
121
+ assert.ok(actual['date']);
122
+ assert.equal(actual['correlation_id'], '123456789');
123
+ assert.equal(actual['message'], 'test');
124
+ assert.equal(actual['status'], 'log');
125
+ assert.deepEqual(actual['http'], { status_code: 200 });
116
126
  });
117
127
 
118
128
  it('snakify keys of Message and Metadata', testContext => {
129
+ const logSpy = testContext.mock.method(global.console, 'log', () => {});
130
+ assert.strictEqual(logSpy.mock.calls.length, 0);
131
+
119
132
  const metadata = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 } };
120
133
  const logger = new Logger(metadata);
134
+ logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
135
+ assert.strictEqual(logSpy.mock.calls.length, 1);
121
136
 
137
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
138
+
139
+ assert.ok(actual['date']);
140
+ assert.equal(actual['correlation_id'], '123456789');
141
+ assert.equal(actual['message'], 'test');
142
+ assert.equal(actual['status'], 'log');
143
+ assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 });
144
+ assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' });
145
+ });
146
+
147
+ it('prunes sensitive Metadata', testContext => {
122
148
  const logSpy = testContext.mock.method(global.console, 'log', () => {});
123
149
  assert.strictEqual(logSpy.mock.calls.length, 0);
150
+
151
+ const metadata = {
152
+ correlationId: '123456789',
153
+ http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' },
154
+ user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } },
155
+ access_token: 'secret',
156
+ };
157
+ const logger = new Logger(metadata);
124
158
  logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
125
159
  assert.strictEqual(logSpy.mock.calls.length, 1);
126
- assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
127
- JSON.stringify({
128
- correlation_id: '123456789',
129
- http: { method: 'GET', status_code: 200 },
130
- error_context: { error_code: 200, error_message: 'Page Not Found' },
131
- message: 'test',
132
- status: 'log',
133
- }),
134
- ]);
160
+
161
+ const actual = JSON.parse(logSpy.mock.calls[0]?.arguments[0]);
162
+ assert.ok(actual['date']);
163
+ assert.equal(actual['correlation_id'], '123456789');
164
+ assert.equal(actual['message'], 'test');
165
+ assert.equal(actual['status'], 'log');
166
+ assert.equal(actual['has_sensitive_attribute'], true);
167
+ assert.equal(actual['access_token'], '[REDACTED]');
168
+ assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
169
+ assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
135
170
  });
136
171
  });