@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.
@@ -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
- console[logLevel](JSON.stringify(processedLogs, null, 2));
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
- const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
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 (typeof metadata[key] === 'object') {
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
- console[logLevel](JSON.stringify(processedLogs, null, 2));
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
- const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
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 (typeof metadata[key] === 'object') {
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 = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 } };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -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
- console[logLevel](JSON.stringify(processedLogs, null, 2));
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
- const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key] as Value) : value[key];
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 (typeof metadata[key] === 'object') {
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 = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 } };
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
  });