@unito/integration-sdk 2.3.2 → 2.3.4
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/dist/src/index.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var integrationApi = require('@unito/integration-api');
|
|
4
4
|
var cachette = require('cachette');
|
|
5
5
|
var crypto = require('crypto');
|
|
6
|
+
var util = require('util');
|
|
6
7
|
var express = require('express');
|
|
7
8
|
var busboy = require('busboy');
|
|
8
9
|
var https = require('https');
|
|
@@ -161,7 +162,12 @@ class Logger {
|
|
|
161
162
|
status: logLevel,
|
|
162
163
|
};
|
|
163
164
|
if (process.env.NODE_ENV === 'development') {
|
|
164
|
-
|
|
165
|
+
const coloredMessage = Logger.colorize(message, processedLogs, logLevel);
|
|
166
|
+
const metadata = {
|
|
167
|
+
date: new Date(processedLogs.date).toISOString(),
|
|
168
|
+
...processedMetadata,
|
|
169
|
+
};
|
|
170
|
+
console[logLevel](`${coloredMessage} ${JSON.stringify(metadata, null, 2)}`);
|
|
165
171
|
}
|
|
166
172
|
else {
|
|
167
173
|
console[logLevel](JSON.stringify(processedLogs));
|
|
@@ -170,7 +176,16 @@ class Logger {
|
|
|
170
176
|
static snakifyKeys(value) {
|
|
171
177
|
const result = {};
|
|
172
178
|
for (const key in value) {
|
|
173
|
-
|
|
179
|
+
let deepValue;
|
|
180
|
+
if (Array.isArray(value[key])) {
|
|
181
|
+
deepValue = value[key].map(item => this.snakifyKeys(item));
|
|
182
|
+
}
|
|
183
|
+
else if (typeof value[key] === 'object' && value[key] !== null) {
|
|
184
|
+
deepValue = this.snakifyKeys(value[key]);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
deepValue = value[key];
|
|
188
|
+
}
|
|
174
189
|
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
175
190
|
result[snakifiedKey] = deepValue;
|
|
176
191
|
}
|
|
@@ -183,7 +198,10 @@ class Logger {
|
|
|
183
198
|
prunedMetadata[key] = '[REDACTED]';
|
|
184
199
|
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
185
200
|
}
|
|
186
|
-
else if (
|
|
201
|
+
else if (Array.isArray(metadata[key])) {
|
|
202
|
+
prunedMetadata[key] = metadata[key].map(value => Logger.pruneSensitiveMetadata(value, topLevelMeta ?? prunedMetadata));
|
|
203
|
+
}
|
|
204
|
+
else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
|
|
187
205
|
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
|
|
188
206
|
}
|
|
189
207
|
else {
|
|
@@ -192,6 +210,56 @@ class Logger {
|
|
|
192
210
|
}
|
|
193
211
|
return prunedMetadata;
|
|
194
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Colorizes the log message based on the log level and status codes.
|
|
215
|
+
* @param message The message to colorize.
|
|
216
|
+
* @param metadata The metadata associated with the log.
|
|
217
|
+
* @param logLevel The log level of the message.
|
|
218
|
+
* @returns The colorized output string.
|
|
219
|
+
*/
|
|
220
|
+
static colorize(message, metadata, logLevel) {
|
|
221
|
+
if (!process.stdout.isTTY) {
|
|
222
|
+
return message;
|
|
223
|
+
}
|
|
224
|
+
const logOutput = `${logLevel}: ${message}`;
|
|
225
|
+
// Extract status code from logs
|
|
226
|
+
let statusCode;
|
|
227
|
+
if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
|
|
228
|
+
const statusCodeValue = metadata.http.status_code;
|
|
229
|
+
if (typeof statusCodeValue === 'number') {
|
|
230
|
+
statusCode = statusCodeValue;
|
|
231
|
+
}
|
|
232
|
+
else if (typeof statusCodeValue === 'string') {
|
|
233
|
+
statusCode = parseInt(statusCodeValue, 10);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Color based on status code first
|
|
237
|
+
if (statusCode) {
|
|
238
|
+
if (statusCode >= 400) {
|
|
239
|
+
return util.styleText('red', logOutput);
|
|
240
|
+
}
|
|
241
|
+
else if (statusCode >= 300) {
|
|
242
|
+
return util.styleText('yellow', logOutput);
|
|
243
|
+
}
|
|
244
|
+
else if (statusCode >= 200) {
|
|
245
|
+
return util.styleText('green', logOutput);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Fall back to log level if no status code found
|
|
249
|
+
switch (logLevel) {
|
|
250
|
+
case LogLevel.ERROR:
|
|
251
|
+
return util.styleText('red', logOutput);
|
|
252
|
+
case LogLevel.WARN:
|
|
253
|
+
return util.styleText('yellow', logOutput);
|
|
254
|
+
case LogLevel.INFO:
|
|
255
|
+
case LogLevel.LOG:
|
|
256
|
+
return util.styleText('green', logOutput);
|
|
257
|
+
case LogLevel.DEBUG:
|
|
258
|
+
return util.styleText('cyan', logOutput);
|
|
259
|
+
default:
|
|
260
|
+
return logOutput;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
195
263
|
}
|
|
196
264
|
const NULL_LOGGER = new Logger({}, true);
|
|
197
265
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
|
|
2
2
|
type Value = {
|
|
3
|
-
[key: string]: PrimitiveValue | Value;
|
|
3
|
+
[key: string]: PrimitiveValue | Value | PrimitiveValue[] | Value[];
|
|
4
4
|
};
|
|
5
5
|
export type Metadata = Value & {
|
|
6
6
|
message?: never;
|
|
@@ -68,6 +68,14 @@ export default class Logger {
|
|
|
68
68
|
private send;
|
|
69
69
|
private static snakifyKeys;
|
|
70
70
|
private static pruneSensitiveMetadata;
|
|
71
|
+
/**
|
|
72
|
+
* Colorizes the log message based on the log level and status codes.
|
|
73
|
+
* @param message The message to colorize.
|
|
74
|
+
* @param metadata The metadata associated with the log.
|
|
75
|
+
* @param logLevel The log level of the message.
|
|
76
|
+
* @returns The colorized output string.
|
|
77
|
+
*/
|
|
78
|
+
private static colorize;
|
|
71
79
|
}
|
|
72
80
|
export declare const NULL_LOGGER: Logger;
|
|
73
81
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { styleText } from 'util';
|
|
1
2
|
var LogLevel;
|
|
2
3
|
(function (LogLevel) {
|
|
3
4
|
LogLevel["ERROR"] = "error";
|
|
@@ -133,7 +134,12 @@ export default class Logger {
|
|
|
133
134
|
status: logLevel,
|
|
134
135
|
};
|
|
135
136
|
if (process.env.NODE_ENV === 'development') {
|
|
136
|
-
|
|
137
|
+
const coloredMessage = Logger.colorize(message, processedLogs, logLevel);
|
|
138
|
+
const metadata = {
|
|
139
|
+
date: new Date(processedLogs.date).toISOString(),
|
|
140
|
+
...processedMetadata,
|
|
141
|
+
};
|
|
142
|
+
console[logLevel](`${coloredMessage} ${JSON.stringify(metadata, null, 2)}`);
|
|
137
143
|
}
|
|
138
144
|
else {
|
|
139
145
|
console[logLevel](JSON.stringify(processedLogs));
|
|
@@ -142,7 +148,16 @@ export default class Logger {
|
|
|
142
148
|
static snakifyKeys(value) {
|
|
143
149
|
const result = {};
|
|
144
150
|
for (const key in value) {
|
|
145
|
-
|
|
151
|
+
let deepValue;
|
|
152
|
+
if (Array.isArray(value[key])) {
|
|
153
|
+
deepValue = value[key].map(item => this.snakifyKeys(item));
|
|
154
|
+
}
|
|
155
|
+
else if (typeof value[key] === 'object' && value[key] !== null) {
|
|
156
|
+
deepValue = this.snakifyKeys(value[key]);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
deepValue = value[key];
|
|
160
|
+
}
|
|
146
161
|
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
147
162
|
result[snakifiedKey] = deepValue;
|
|
148
163
|
}
|
|
@@ -155,7 +170,10 @@ export default class Logger {
|
|
|
155
170
|
prunedMetadata[key] = '[REDACTED]';
|
|
156
171
|
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
157
172
|
}
|
|
158
|
-
else if (
|
|
173
|
+
else if (Array.isArray(metadata[key])) {
|
|
174
|
+
prunedMetadata[key] = metadata[key].map(value => Logger.pruneSensitiveMetadata(value, topLevelMeta ?? prunedMetadata));
|
|
175
|
+
}
|
|
176
|
+
else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
|
|
159
177
|
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
|
|
160
178
|
}
|
|
161
179
|
else {
|
|
@@ -164,5 +182,55 @@ export default class Logger {
|
|
|
164
182
|
}
|
|
165
183
|
return prunedMetadata;
|
|
166
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Colorizes the log message based on the log level and status codes.
|
|
187
|
+
* @param message The message to colorize.
|
|
188
|
+
* @param metadata The metadata associated with the log.
|
|
189
|
+
* @param logLevel The log level of the message.
|
|
190
|
+
* @returns The colorized output string.
|
|
191
|
+
*/
|
|
192
|
+
static colorize(message, metadata, logLevel) {
|
|
193
|
+
if (!process.stdout.isTTY) {
|
|
194
|
+
return message;
|
|
195
|
+
}
|
|
196
|
+
const logOutput = `${logLevel}: ${message}`;
|
|
197
|
+
// Extract status code from logs
|
|
198
|
+
let statusCode;
|
|
199
|
+
if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
|
|
200
|
+
const statusCodeValue = metadata.http.status_code;
|
|
201
|
+
if (typeof statusCodeValue === 'number') {
|
|
202
|
+
statusCode = statusCodeValue;
|
|
203
|
+
}
|
|
204
|
+
else if (typeof statusCodeValue === 'string') {
|
|
205
|
+
statusCode = parseInt(statusCodeValue, 10);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Color based on status code first
|
|
209
|
+
if (statusCode) {
|
|
210
|
+
if (statusCode >= 400) {
|
|
211
|
+
return styleText('red', logOutput);
|
|
212
|
+
}
|
|
213
|
+
else if (statusCode >= 300) {
|
|
214
|
+
return styleText('yellow', logOutput);
|
|
215
|
+
}
|
|
216
|
+
else if (statusCode >= 200) {
|
|
217
|
+
return styleText('green', logOutput);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Fall back to log level if no status code found
|
|
221
|
+
switch (logLevel) {
|
|
222
|
+
case LogLevel.ERROR:
|
|
223
|
+
return styleText('red', logOutput);
|
|
224
|
+
case LogLevel.WARN:
|
|
225
|
+
return styleText('yellow', logOutput);
|
|
226
|
+
case LogLevel.INFO:
|
|
227
|
+
case LogLevel.LOG:
|
|
228
|
+
return styleText('green', logOutput);
|
|
229
|
+
case LogLevel.DEBUG:
|
|
230
|
+
return styleText('cyan', logOutput);
|
|
231
|
+
default:
|
|
232
|
+
return logOutput;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
167
235
|
}
|
|
168
236
|
export const NULL_LOGGER = new Logger({}, true);
|
|
@@ -106,7 +106,14 @@ describe('Logger', () => {
|
|
|
106
106
|
it('snakify keys of Message and Metadata', testContext => {
|
|
107
107
|
const logSpy = testContext.mock.method(global.console, 'log', () => { });
|
|
108
108
|
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
109
|
-
const metadata = {
|
|
109
|
+
const metadata = {
|
|
110
|
+
correlationId: '123456789',
|
|
111
|
+
http: { method: 'GET', statusCode: 200 },
|
|
112
|
+
links: [
|
|
113
|
+
{ id: 1, organizationId: 'a' },
|
|
114
|
+
{ id: 2, organizationId: 'b' },
|
|
115
|
+
],
|
|
116
|
+
};
|
|
110
117
|
const logger = new Logger(metadata);
|
|
111
118
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
112
119
|
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
@@ -117,6 +124,10 @@ describe('Logger', () => {
|
|
|
117
124
|
assert.equal(actual['status'], 'log');
|
|
118
125
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 });
|
|
119
126
|
assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' });
|
|
127
|
+
assert.deepEqual(actual['links'], [
|
|
128
|
+
{ id: 1, organization_id: 'a' },
|
|
129
|
+
{ id: 2, organization_id: 'b' },
|
|
130
|
+
]);
|
|
120
131
|
});
|
|
121
132
|
it('prunes sensitive Metadata', testContext => {
|
|
122
133
|
const logSpy = testContext.mock.method(global.console, 'log', () => { });
|
|
@@ -126,6 +137,10 @@ describe('Logger', () => {
|
|
|
126
137
|
http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' },
|
|
127
138
|
user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } },
|
|
128
139
|
access_token: 'secret',
|
|
140
|
+
items: [
|
|
141
|
+
{ id: 1, organizationId: 'a' },
|
|
142
|
+
{ id: 2, organizationId: 'b' },
|
|
143
|
+
],
|
|
129
144
|
};
|
|
130
145
|
const logger = new Logger(metadata);
|
|
131
146
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
@@ -139,6 +154,10 @@ describe('Logger', () => {
|
|
|
139
154
|
assert.equal(actual['access_token'], '[REDACTED]');
|
|
140
155
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
|
|
141
156
|
assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
|
|
157
|
+
assert.deepEqual(actual['items'], [
|
|
158
|
+
{ id: 1, organization_id: 'a' },
|
|
159
|
+
{ id: 2, organization_id: 'b' },
|
|
160
|
+
]);
|
|
142
161
|
});
|
|
143
162
|
it(`NULL_LOGGER should not log`, testContext => {
|
|
144
163
|
const logger = NULL_LOGGER;
|
|
@@ -158,4 +177,77 @@ describe('Logger', () => {
|
|
|
158
177
|
assert.strictEqual(infoSpy.mock.calls.length, 0);
|
|
159
178
|
assert.strictEqual(debugSpy.mock.calls.length, 0);
|
|
160
179
|
});
|
|
180
|
+
it('colorizes logs by status codes over log levels', () => {
|
|
181
|
+
const originalEnv = process.env.NODE_ENV;
|
|
182
|
+
const originalIsTTY = process.stdout.isTTY;
|
|
183
|
+
try {
|
|
184
|
+
process.env.NODE_ENV = 'development';
|
|
185
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
186
|
+
value: true,
|
|
187
|
+
configurable: true,
|
|
188
|
+
});
|
|
189
|
+
// 4xx status with warn level, should be red
|
|
190
|
+
const metadataWith400 = {
|
|
191
|
+
http: { status_code: 400 },
|
|
192
|
+
};
|
|
193
|
+
const redResult = Logger['colorize']('Some error', metadataWith400, 'warn');
|
|
194
|
+
// warn level without status code, should be yellow
|
|
195
|
+
const metadataNoStatus = {};
|
|
196
|
+
const yellowResult = Logger['colorize']('Some warning', metadataNoStatus, 'warn');
|
|
197
|
+
// Results should be different colors
|
|
198
|
+
assert.notEqual(redResult, yellowResult);
|
|
199
|
+
assert.ok(redResult.includes('\x1b[31m')); // Red color code
|
|
200
|
+
assert.ok(yellowResult.includes('\x1b[33m')); // Yellow color code
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
// Restore original values
|
|
204
|
+
process.env.NODE_ENV = originalEnv;
|
|
205
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
206
|
+
value: originalIsTTY,
|
|
207
|
+
configurable: true,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
it('colorizes logs with different status code ranges', () => {
|
|
212
|
+
const originalEnv = process.env.NODE_ENV;
|
|
213
|
+
const originalIsTTY = process.stdout.isTTY;
|
|
214
|
+
try {
|
|
215
|
+
process.env.NODE_ENV = 'development';
|
|
216
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
217
|
+
value: true,
|
|
218
|
+
configurable: true,
|
|
219
|
+
});
|
|
220
|
+
// 2xx status codes should be green
|
|
221
|
+
const successMetadata = {
|
|
222
|
+
http: { status_code: 200 },
|
|
223
|
+
};
|
|
224
|
+
const greenResult = Logger['colorize']('Success', successMetadata, 'info');
|
|
225
|
+
assert.ok(greenResult.includes('\x1b[32m')); // Green color code
|
|
226
|
+
// 3xx status codes should be yellow
|
|
227
|
+
const redirectMetadata = {
|
|
228
|
+
http: { status_code: 301 },
|
|
229
|
+
};
|
|
230
|
+
const yellowResult = Logger['colorize']('Redirect', redirectMetadata, 'info');
|
|
231
|
+
assert.ok(yellowResult.includes('\x1b[33m')); // Yellow color code
|
|
232
|
+
// 4xx status codes should be red
|
|
233
|
+
const clientErrorMetadata = {
|
|
234
|
+
http: { status_code: 404 },
|
|
235
|
+
};
|
|
236
|
+
const redResult = Logger['colorize']('Not Found', clientErrorMetadata, 'warn');
|
|
237
|
+
assert.ok(redResult.includes('\x1b[31m')); // Red color code
|
|
238
|
+
// 5xx status codes should be red
|
|
239
|
+
const serverErrorMetadata = {
|
|
240
|
+
http: { status_code: 500 },
|
|
241
|
+
};
|
|
242
|
+
const redResult2 = Logger['colorize']('Server Error', serverErrorMetadata, 'error');
|
|
243
|
+
assert.ok(redResult2.includes('\x1b[31m')); // Red color code
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
process.env.NODE_ENV = originalEnv;
|
|
247
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
248
|
+
value: originalIsTTY,
|
|
249
|
+
configurable: true,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
161
253
|
});
|
package/package.json
CHANGED
package/src/resources/logger.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { styleText } from 'util';
|
|
2
|
+
|
|
1
3
|
const enum LogLevel {
|
|
2
4
|
ERROR = 'error',
|
|
3
5
|
WARN = 'warn',
|
|
@@ -8,7 +10,7 @@ const enum LogLevel {
|
|
|
8
10
|
|
|
9
11
|
type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
|
|
10
12
|
type Value = {
|
|
11
|
-
[key: string]: PrimitiveValue | Value;
|
|
13
|
+
[key: string]: PrimitiveValue | Value | PrimitiveValue[] | Value[];
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export type Metadata = Value & { message?: never };
|
|
@@ -162,7 +164,14 @@ export default class Logger {
|
|
|
162
164
|
};
|
|
163
165
|
|
|
164
166
|
if (process.env.NODE_ENV === 'development') {
|
|
165
|
-
|
|
167
|
+
const coloredMessage = Logger.colorize(message, processedLogs, logLevel);
|
|
168
|
+
|
|
169
|
+
const metadata = {
|
|
170
|
+
date: new Date(processedLogs.date).toISOString(),
|
|
171
|
+
...processedMetadata,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
console[logLevel](`${coloredMessage} ${JSON.stringify(metadata, null, 2)}`);
|
|
166
175
|
} else {
|
|
167
176
|
console[logLevel](JSON.stringify(processedLogs));
|
|
168
177
|
}
|
|
@@ -172,7 +181,16 @@ export default class Logger {
|
|
|
172
181
|
const result: Value = {};
|
|
173
182
|
|
|
174
183
|
for (const key in value) {
|
|
175
|
-
|
|
184
|
+
let deepValue;
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(value[key])) {
|
|
187
|
+
deepValue = value[key].map(item => this.snakifyKeys(item as Value));
|
|
188
|
+
} else if (typeof value[key] === 'object' && value[key] !== null) {
|
|
189
|
+
deepValue = this.snakifyKeys(value[key] as Value);
|
|
190
|
+
} else {
|
|
191
|
+
deepValue = value[key];
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
177
195
|
result[snakifiedKey] = deepValue;
|
|
178
196
|
}
|
|
@@ -187,7 +205,11 @@ export default class Logger {
|
|
|
187
205
|
if (LOGMETA_BLACKLIST.includes(key)) {
|
|
188
206
|
prunedMetadata[key] = '[REDACTED]';
|
|
189
207
|
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
190
|
-
} else if (
|
|
208
|
+
} else if (Array.isArray(metadata[key])) {
|
|
209
|
+
prunedMetadata[key] = metadata[key].map(value =>
|
|
210
|
+
Logger.pruneSensitiveMetadata(value as Value, topLevelMeta ?? prunedMetadata),
|
|
211
|
+
);
|
|
212
|
+
} else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
|
|
191
213
|
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key] as Value, topLevelMeta ?? prunedMetadata);
|
|
192
214
|
} else {
|
|
193
215
|
prunedMetadata[key] = metadata[key];
|
|
@@ -196,6 +218,59 @@ export default class Logger {
|
|
|
196
218
|
|
|
197
219
|
return prunedMetadata;
|
|
198
220
|
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Colorizes the log message based on the log level and status codes.
|
|
224
|
+
* @param message The message to colorize.
|
|
225
|
+
* @param metadata The metadata associated with the log.
|
|
226
|
+
* @param logLevel The log level of the message.
|
|
227
|
+
* @returns The colorized output string.
|
|
228
|
+
*/
|
|
229
|
+
private static colorize(message: string, metadata: Value, logLevel: LogLevel): string {
|
|
230
|
+
if (!process.stdout.isTTY) {
|
|
231
|
+
return message;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const logOutput = `${logLevel}: ${message}`;
|
|
235
|
+
|
|
236
|
+
// Extract status code from logs
|
|
237
|
+
let statusCode: number | undefined;
|
|
238
|
+
if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
|
|
239
|
+
const statusCodeValue = metadata.http.status_code;
|
|
240
|
+
|
|
241
|
+
if (typeof statusCodeValue === 'number') {
|
|
242
|
+
statusCode = statusCodeValue;
|
|
243
|
+
} else if (typeof statusCodeValue === 'string') {
|
|
244
|
+
statusCode = parseInt(statusCodeValue, 10);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Color based on status code first
|
|
249
|
+
if (statusCode) {
|
|
250
|
+
if (statusCode >= 400) {
|
|
251
|
+
return styleText('red', logOutput);
|
|
252
|
+
} else if (statusCode >= 300) {
|
|
253
|
+
return styleText('yellow', logOutput);
|
|
254
|
+
} else if (statusCode >= 200) {
|
|
255
|
+
return styleText('green', logOutput);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Fall back to log level if no status code found
|
|
260
|
+
switch (logLevel) {
|
|
261
|
+
case LogLevel.ERROR:
|
|
262
|
+
return styleText('red', logOutput);
|
|
263
|
+
case LogLevel.WARN:
|
|
264
|
+
return styleText('yellow', logOutput);
|
|
265
|
+
case LogLevel.INFO:
|
|
266
|
+
case LogLevel.LOG:
|
|
267
|
+
return styleText('green', logOutput);
|
|
268
|
+
case LogLevel.DEBUG:
|
|
269
|
+
return styleText('cyan', logOutput);
|
|
270
|
+
default:
|
|
271
|
+
return logOutput;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
199
274
|
}
|
|
200
275
|
|
|
201
276
|
export const NULL_LOGGER = new Logger({}, true);
|
|
@@ -129,7 +129,14 @@ describe('Logger', () => {
|
|
|
129
129
|
const logSpy = testContext.mock.method(global.console, 'log', () => {});
|
|
130
130
|
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
131
131
|
|
|
132
|
-
const metadata = {
|
|
132
|
+
const metadata = {
|
|
133
|
+
correlationId: '123456789',
|
|
134
|
+
http: { method: 'GET', statusCode: 200 },
|
|
135
|
+
links: [
|
|
136
|
+
{ id: 1, organizationId: 'a' },
|
|
137
|
+
{ id: 2, organizationId: 'b' },
|
|
138
|
+
],
|
|
139
|
+
};
|
|
133
140
|
const logger = new Logger(metadata);
|
|
134
141
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
135
142
|
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
@@ -142,6 +149,10 @@ describe('Logger', () => {
|
|
|
142
149
|
assert.equal(actual['status'], 'log');
|
|
143
150
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 });
|
|
144
151
|
assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' });
|
|
152
|
+
assert.deepEqual(actual['links'], [
|
|
153
|
+
{ id: 1, organization_id: 'a' },
|
|
154
|
+
{ id: 2, organization_id: 'b' },
|
|
155
|
+
]);
|
|
145
156
|
});
|
|
146
157
|
|
|
147
158
|
it('prunes sensitive Metadata', testContext => {
|
|
@@ -153,6 +164,10 @@ describe('Logger', () => {
|
|
|
153
164
|
http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' },
|
|
154
165
|
user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } },
|
|
155
166
|
access_token: 'secret',
|
|
167
|
+
items: [
|
|
168
|
+
{ id: 1, organizationId: 'a' },
|
|
169
|
+
{ id: 2, organizationId: 'b' },
|
|
170
|
+
],
|
|
156
171
|
};
|
|
157
172
|
const logger = new Logger(metadata);
|
|
158
173
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
@@ -167,6 +182,10 @@ describe('Logger', () => {
|
|
|
167
182
|
assert.equal(actual['access_token'], '[REDACTED]');
|
|
168
183
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
|
|
169
184
|
assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
|
|
185
|
+
assert.deepEqual(actual['items'], [
|
|
186
|
+
{ id: 1, organization_id: 'a' },
|
|
187
|
+
{ id: 2, organization_id: 'b' },
|
|
188
|
+
]);
|
|
170
189
|
});
|
|
171
190
|
|
|
172
191
|
it(`NULL_LOGGER should not log`, testContext => {
|
|
@@ -190,4 +209,86 @@ describe('Logger', () => {
|
|
|
190
209
|
assert.strictEqual(infoSpy.mock.calls.length, 0);
|
|
191
210
|
assert.strictEqual(debugSpy.mock.calls.length, 0);
|
|
192
211
|
});
|
|
212
|
+
|
|
213
|
+
it('colorizes logs by status codes over log levels', () => {
|
|
214
|
+
const originalEnv = process.env.NODE_ENV;
|
|
215
|
+
const originalIsTTY = process.stdout.isTTY;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
process.env.NODE_ENV = 'development';
|
|
219
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
220
|
+
value: true,
|
|
221
|
+
configurable: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// 4xx status with warn level, should be red
|
|
225
|
+
const metadataWith400 = {
|
|
226
|
+
http: { status_code: 400 },
|
|
227
|
+
};
|
|
228
|
+
const redResult = Logger['colorize']('Some error', metadataWith400, 'warn' as any);
|
|
229
|
+
|
|
230
|
+
// warn level without status code, should be yellow
|
|
231
|
+
const metadataNoStatus = {};
|
|
232
|
+
const yellowResult = Logger['colorize']('Some warning', metadataNoStatus, 'warn' as any);
|
|
233
|
+
|
|
234
|
+
// Results should be different colors
|
|
235
|
+
assert.notEqual(redResult, yellowResult);
|
|
236
|
+
assert.ok(redResult.includes('\x1b[31m')); // Red color code
|
|
237
|
+
assert.ok(yellowResult.includes('\x1b[33m')); // Yellow color code
|
|
238
|
+
} finally {
|
|
239
|
+
// Restore original values
|
|
240
|
+
process.env.NODE_ENV = originalEnv;
|
|
241
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
242
|
+
value: originalIsTTY,
|
|
243
|
+
configurable: true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('colorizes logs with different status code ranges', () => {
|
|
249
|
+
const originalEnv = process.env.NODE_ENV;
|
|
250
|
+
const originalIsTTY = process.stdout.isTTY;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
process.env.NODE_ENV = 'development';
|
|
254
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
255
|
+
value: true,
|
|
256
|
+
configurable: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 2xx status codes should be green
|
|
260
|
+
const successMetadata = {
|
|
261
|
+
http: { status_code: 200 },
|
|
262
|
+
};
|
|
263
|
+
const greenResult = Logger['colorize']('Success', successMetadata, 'info' as any);
|
|
264
|
+
assert.ok(greenResult.includes('\x1b[32m')); // Green color code
|
|
265
|
+
|
|
266
|
+
// 3xx status codes should be yellow
|
|
267
|
+
const redirectMetadata = {
|
|
268
|
+
http: { status_code: 301 },
|
|
269
|
+
};
|
|
270
|
+
const yellowResult = Logger['colorize']('Redirect', redirectMetadata, 'info' as any);
|
|
271
|
+
assert.ok(yellowResult.includes('\x1b[33m')); // Yellow color code
|
|
272
|
+
|
|
273
|
+
// 4xx status codes should be red
|
|
274
|
+
const clientErrorMetadata = {
|
|
275
|
+
http: { status_code: 404 },
|
|
276
|
+
};
|
|
277
|
+
const redResult = Logger['colorize']('Not Found', clientErrorMetadata, 'warn' as any);
|
|
278
|
+
assert.ok(redResult.includes('\x1b[31m')); // Red color code
|
|
279
|
+
|
|
280
|
+
// 5xx status codes should be red
|
|
281
|
+
const serverErrorMetadata = {
|
|
282
|
+
http: { status_code: 500 },
|
|
283
|
+
};
|
|
284
|
+
const redResult2 = Logger['colorize']('Server Error', serverErrorMetadata, 'error' as any);
|
|
285
|
+
assert.ok(redResult2.includes('\x1b[31m')); // Red color code
|
|
286
|
+
} finally {
|
|
287
|
+
process.env.NODE_ENV = originalEnv;
|
|
288
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
289
|
+
value: originalIsTTY,
|
|
290
|
+
configurable: true,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
193
294
|
});
|