@unito/integration-sdk 1.0.26 → 1.0.28
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/helpers.d.ts +2 -2
- package/dist/src/helpers.js +2 -2
- package/dist/src/index.cjs +267 -215
- package/dist/src/integration.js +8 -8
- package/dist/src/middlewares/correlationId.d.ts +2 -2
- package/dist/src/middlewares/correlationId.js +3 -3
- package/dist/src/middlewares/credentials.d.ts +2 -2
- package/dist/src/middlewares/credentials.js +3 -3
- package/dist/src/middlewares/errors.d.ts +2 -2
- package/dist/src/middlewares/errors.js +3 -3
- package/dist/src/middlewares/filters.d.ts +2 -2
- package/dist/src/middlewares/filters.js +4 -4
- package/dist/src/middlewares/finish.d.ts +2 -2
- package/dist/src/middlewares/finish.js +3 -3
- package/dist/src/middlewares/logger.d.ts +2 -2
- package/dist/src/middlewares/logger.js +3 -3
- package/dist/src/middlewares/notFound.d.ts +2 -2
- package/dist/src/middlewares/notFound.js +3 -3
- package/dist/src/middlewares/secrets.d.ts +2 -2
- package/dist/src/middlewares/secrets.js +3 -3
- package/dist/src/middlewares/selects.d.ts +2 -2
- package/dist/src/middlewares/selects.js +3 -3
- package/dist/src/middlewares/signal.d.ts +2 -2
- package/dist/src/middlewares/signal.js +3 -3
- package/dist/src/middlewares/{requestStartTime.d.ts → start.d.ts} +2 -2
- package/dist/src/middlewares/{requestStartTime.js → start.js} +3 -3
- package/dist/src/resources/logger.d.ts +2 -1
- package/dist/src/resources/logger.js +59 -7
- package/dist/test/middlewares/correlationId.test.js +3 -3
- package/dist/test/middlewares/credentials.test.js +4 -4
- package/dist/test/middlewares/errors.test.js +4 -4
- package/dist/test/middlewares/filters.test.js +20 -12
- package/dist/test/middlewares/finish.test.js +4 -4
- package/dist/test/middlewares/logger.test.js +5 -5
- package/dist/test/middlewares/notFound.test.js +2 -2
- package/dist/test/middlewares/secrets.test.js +3 -3
- package/dist/test/middlewares/selects.test.js +3 -3
- package/dist/test/middlewares/signal.test.js +3 -3
- package/dist/test/middlewares/start.test.d.ts +1 -0
- package/dist/test/middlewares/start.test.js +11 -0
- package/dist/test/resources/logger.test.js +71 -45
- package/package.json +1 -1
- package/src/helpers.ts +2 -2
- package/src/integration.ts +8 -8
- package/src/middlewares/correlationId.ts +3 -3
- package/src/middlewares/credentials.ts +3 -3
- package/src/middlewares/errors.ts +3 -3
- package/src/middlewares/filters.ts +4 -4
- package/src/middlewares/finish.ts +3 -3
- package/src/middlewares/logger.ts +3 -3
- package/src/middlewares/notFound.ts +3 -3
- package/src/middlewares/secrets.ts +3 -3
- package/src/middlewares/selects.ts +3 -3
- package/src/middlewares/signal.ts +3 -3
- package/src/middlewares/{requestStartTime.ts → start.ts} +3 -3
- package/src/resources/logger.ts +66 -8
- package/test/middlewares/correlationId.test.ts +3 -3
- package/test/middlewares/credentials.test.ts +4 -4
- package/test/middlewares/errors.test.ts +4 -4
- package/test/middlewares/filters.test.ts +31 -12
- package/test/middlewares/finish.test.ts +4 -4
- package/test/middlewares/logger.test.ts +5 -5
- package/test/middlewares/notFound.test.ts +2 -2
- package/test/middlewares/secrets.test.ts +3 -3
- package/test/middlewares/selects.test.ts +3 -3
- package/test/middlewares/signal.test.ts +3 -3
- package/test/middlewares/start.test.ts +14 -0
- package/test/resources/logger.test.ts +82 -47
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
package/src/helpers.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { Filter } from './index.js';
|
|
|
12
12
|
* @param fields The schema of the item against which the filters are applied
|
|
13
13
|
* @returns The validated filters
|
|
14
14
|
*/
|
|
15
|
-
export
|
|
15
|
+
export function getApplicableFilters(context: { filters: Filter[] }, fields: FieldSchema[]): Filter[] {
|
|
16
16
|
const applicableFilters: Filter[] = [];
|
|
17
17
|
|
|
18
18
|
for (const filter of context.filters) {
|
|
@@ -34,4 +34,4 @@ export const getApplicableFilters = (context: { filters: Filter[] }, fields: Fie
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return applicableFilters;
|
|
37
|
-
}
|
|
37
|
+
}
|
package/src/integration.ts
CHANGED
|
@@ -2,18 +2,18 @@ import { Server, IncomingMessage, ServerResponse } from 'http';
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
|
|
4
4
|
import { InvalidHandler } from './errors.js';
|
|
5
|
+
import { HandlersInput, Handler } from './handler.js';
|
|
5
6
|
import correlationIdMiddleware from './middlewares/correlationId.js';
|
|
6
|
-
import loggerMiddleware from './middlewares/logger.js';
|
|
7
7
|
import credentialsMiddleware from './middlewares/credentials.js';
|
|
8
|
-
import signalMiddleware from './middlewares/signal.js';
|
|
9
|
-
import secretsMiddleware from './middlewares/secrets.js';
|
|
10
|
-
import filtersMiddleware from './middlewares/filters.js';
|
|
11
|
-
import selectsMiddleware from './middlewares/selects.js';
|
|
12
8
|
import errorsMiddleware from './middlewares/errors.js';
|
|
9
|
+
import filtersMiddleware from './middlewares/filters.js';
|
|
13
10
|
import finishMiddleware from './middlewares/finish.js';
|
|
14
11
|
import notFoundMiddleware from './middlewares/notFound.js';
|
|
15
|
-
import
|
|
16
|
-
import
|
|
12
|
+
import loggerMiddleware from './middlewares/logger.js';
|
|
13
|
+
import startMiddleware from './middlewares/start.js';
|
|
14
|
+
import secretsMiddleware from './middlewares/secrets.js';
|
|
15
|
+
import selectsMiddleware from './middlewares/selects.js';
|
|
16
|
+
import signalMiddleware from './middlewares/signal.js';
|
|
17
17
|
|
|
18
18
|
function printErrorMessage(message: string) {
|
|
19
19
|
console.error();
|
|
@@ -131,7 +131,7 @@ export default class Integration {
|
|
|
131
131
|
app.use(finishMiddleware);
|
|
132
132
|
|
|
133
133
|
// Instantiate internal middlewares.
|
|
134
|
-
app.use(
|
|
134
|
+
app.use(startMiddleware);
|
|
135
135
|
app.use(correlationIdMiddleware);
|
|
136
136
|
app.use(loggerMiddleware);
|
|
137
137
|
|
|
@@ -10,10 +10,10 @@ declare global {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function extractCorrelationId(req: Request, res: Response, next: NextFunction) {
|
|
14
14
|
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? crypto.randomUUID();
|
|
15
15
|
|
|
16
16
|
next();
|
|
17
|
-
}
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
export default
|
|
19
|
+
export default extractCorrelationId;
|
|
@@ -14,7 +14,7 @@ export type Credentials = { accessToken?: string; [keys: string]: unknown };
|
|
|
14
14
|
|
|
15
15
|
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function extractCredentials(req: Request, res: Response, next: NextFunction) {
|
|
18
18
|
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
19
19
|
|
|
20
20
|
if (credentialsHeader) {
|
|
@@ -30,6 +30,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
next();
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
34
|
|
|
35
|
-
export default
|
|
35
|
+
export default extractCredentials;
|
|
@@ -12,7 +12,7 @@ declare global {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function onError(err: Error, _req: Request, res: Response, next: NextFunction) {
|
|
16
16
|
if (res.headersSent) {
|
|
17
17
|
return next(err);
|
|
18
18
|
}
|
|
@@ -50,6 +50,6 @@ const middleware = (err: Error, _req: Request, res: Response, next: NextFunction
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
res.status(Number(error.code)).json(error);
|
|
53
|
-
}
|
|
53
|
+
}
|
|
54
54
|
|
|
55
|
-
export default
|
|
55
|
+
export default onError;
|
|
@@ -32,9 +32,9 @@ export type Filter = {
|
|
|
32
32
|
// a subset of the symbol of another operator.
|
|
33
33
|
//
|
|
34
34
|
// For example, the symbol "=" (EQUAL) is a subset of the symbol "!=" (NOT_EQUAL).
|
|
35
|
-
const ORDERED_OPERATORS = Object.values(OperatorType).sort((o1, o2) =>
|
|
35
|
+
const ORDERED_OPERATORS = Object.values(OperatorType).sort((o1, o2) => o2.length - o1.length);
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function extractFilters(req: Request, res: Response, next: NextFunction) {
|
|
38
38
|
const rawFilters = req.query.filter;
|
|
39
39
|
|
|
40
40
|
res.locals.filters = [];
|
|
@@ -55,6 +55,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
next();
|
|
58
|
-
}
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
export default
|
|
60
|
+
export default extractFilters;
|
|
@@ -13,7 +13,7 @@ declare global {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function onFinish(req: Request, res: Response, next: NextFunction) {
|
|
17
17
|
if (req.originalUrl !== '/health') {
|
|
18
18
|
res.on('finish', function () {
|
|
19
19
|
const error = res.locals.error;
|
|
@@ -49,6 +49,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
next();
|
|
52
|
-
}
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
export default
|
|
54
|
+
export default onFinish;
|
|
@@ -13,7 +13,7 @@ declare global {
|
|
|
13
13
|
|
|
14
14
|
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function injectLogger(req: Request, res: Response, next: NextFunction) {
|
|
17
17
|
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
18
18
|
|
|
19
19
|
res.locals.logger = logger;
|
|
@@ -31,6 +31,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
next();
|
|
34
|
-
}
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
-
export default
|
|
36
|
+
export default injectLogger;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { Error as APIError } from '@unito/integration-api';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
function notFound(req: Request, res: Response, _next: NextFunction) {
|
|
5
5
|
const error: APIError = {
|
|
6
6
|
code: '404',
|
|
7
7
|
message: `Path ${req.path} not found.`,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
res.status(404).json(error);
|
|
11
|
-
}
|
|
11
|
+
}
|
|
12
12
|
|
|
13
|
-
export default
|
|
13
|
+
export default notFound;
|
|
@@ -19,7 +19,7 @@ export type Secrets = { [keys: string]: unknown };
|
|
|
19
19
|
|
|
20
20
|
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function extractSecrets(req: Request, res: Response, next: NextFunction) {
|
|
23
23
|
const secretsHeader = req.header(SECRETS_HEADER);
|
|
24
24
|
|
|
25
25
|
if (secretsHeader) {
|
|
@@ -35,6 +35,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
next();
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
export default
|
|
40
|
+
export default extractSecrets;
|
|
@@ -16,7 +16,7 @@ declare global {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
function extractSelects(req: Request, res: Response, next: NextFunction) {
|
|
20
20
|
const rawSelect = req.query.select;
|
|
21
21
|
|
|
22
22
|
if (typeof rawSelect === 'string') {
|
|
@@ -26,6 +26,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
next();
|
|
29
|
-
}
|
|
29
|
+
}
|
|
30
30
|
|
|
31
|
-
export default
|
|
31
|
+
export default extractSelects;
|
|
@@ -17,7 +17,7 @@ declare global {
|
|
|
17
17
|
|
|
18
18
|
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
function extractOperationDeadline(req: Request, res: Response, next: NextFunction) {
|
|
21
21
|
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
22
22
|
|
|
23
23
|
if (operationDeadlineHeader) {
|
|
@@ -36,6 +36,6 @@ const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
next();
|
|
39
|
-
}
|
|
39
|
+
}
|
|
40
40
|
|
|
41
|
-
export default
|
|
41
|
+
export default extractOperationDeadline;
|
|
@@ -9,10 +9,10 @@ declare global {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function start(_: Request, res: Response, next: NextFunction) {
|
|
13
13
|
res.locals.requestStartTime = process.hrtime.bigint();
|
|
14
14
|
|
|
15
15
|
next();
|
|
16
|
-
}
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
export default
|
|
18
|
+
export default start;
|
package/src/resources/logger.ts
CHANGED
|
@@ -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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
159
|
+
console[logLevel](JSON.stringify(processedLogs, null, 2));
|
|
119
160
|
} else {
|
|
120
|
-
console[logLevel](JSON.stringify(
|
|
161
|
+
console[logLevel](JSON.stringify(processedLogs));
|
|
121
162
|
}
|
|
122
163
|
}
|
|
123
164
|
|
|
124
|
-
private snakifyKeys
|
|
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
|
|
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
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
|
-
import
|
|
4
|
+
import extractCorrelationId from '../../src/middlewares/correlationId.js';
|
|
5
5
|
|
|
6
6
|
describe('correlationId middleware', () => {
|
|
7
7
|
it('uses header', () => {
|
|
8
8
|
const request = { header: (_key: string) => '123' } as express.Request;
|
|
9
9
|
const response = { locals: {} } as express.Response;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
extractCorrelationId(request, response, () => {});
|
|
12
12
|
|
|
13
13
|
assert.deepEqual(response.locals, {
|
|
14
14
|
correlationId: '123',
|
|
@@ -19,7 +19,7 @@ describe('correlationId middleware', () => {
|
|
|
19
19
|
const request = { header: (_key: string) => undefined } as express.Request;
|
|
20
20
|
const response = { locals: {} } as express.Response;
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
extractCorrelationId(request, response, () => {});
|
|
23
23
|
|
|
24
24
|
assert(response.locals.correlationId);
|
|
25
25
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
|
-
import
|
|
4
|
+
import extractCredentials from '../../src/middlewares/credentials.js';
|
|
5
5
|
import { BadRequestError } from '../../src/httpErrors.js';
|
|
6
6
|
|
|
7
7
|
describe('credentials middleware', () => {
|
|
@@ -15,7 +15,7 @@ describe('credentials middleware', () => {
|
|
|
15
15
|
const request = { header: (_key: string) => credentials } as express.Request;
|
|
16
16
|
const response = { locals: {} } as express.Response;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
extractCredentials(request, response, () => {});
|
|
19
19
|
|
|
20
20
|
assert.deepEqual(response.locals, {
|
|
21
21
|
credentials: {
|
|
@@ -28,7 +28,7 @@ describe('credentials middleware', () => {
|
|
|
28
28
|
const request = { header: (_key: string) => 'nope' } as express.Request;
|
|
29
29
|
const response = { locals: {} } as express.Response;
|
|
30
30
|
|
|
31
|
-
assert.throws(() =>
|
|
31
|
+
assert.throws(() => extractCredentials(request, response, () => {}), BadRequestError);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('variables', async () => {
|
|
@@ -41,7 +41,7 @@ describe('credentials middleware', () => {
|
|
|
41
41
|
const request = { header: (_key: string) => credentials } as express.Request;
|
|
42
42
|
const response = { locals: {} } as express.Response;
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
extractCredentials(request, response, () => {});
|
|
45
45
|
|
|
46
46
|
assert.deepEqual(response.locals, {
|
|
47
47
|
credentials: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
|
-
import
|
|
4
|
+
import onError from '../../src/middlewares/errors.js';
|
|
5
5
|
import { HttpError } from '../../src/httpErrors.js';
|
|
6
6
|
|
|
7
7
|
describe('errors middleware', () => {
|
|
@@ -23,7 +23,7 @@ describe('errors middleware', () => {
|
|
|
23
23
|
locals: { logger: { error: () => undefined } },
|
|
24
24
|
} as unknown as express.Response;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
onError(new Error(), {} as express.Request, response, () => {});
|
|
27
27
|
|
|
28
28
|
assert.strictEqual(actualStatus, undefined);
|
|
29
29
|
assert.strictEqual(actualJson, undefined);
|
|
@@ -46,7 +46,7 @@ describe('errors middleware', () => {
|
|
|
46
46
|
},
|
|
47
47
|
locals: { logger: { error: () => undefined } },
|
|
48
48
|
} as unknown as express.Response;
|
|
49
|
-
|
|
49
|
+
onError(new HttpError('httpError', 429), {} as express.Request, response, () => {});
|
|
50
50
|
|
|
51
51
|
assert.strictEqual(actualStatus, 429);
|
|
52
52
|
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
@@ -69,7 +69,7 @@ describe('errors middleware', () => {
|
|
|
69
69
|
},
|
|
70
70
|
locals: { logger: { error: () => undefined } },
|
|
71
71
|
} as unknown as express.Response;
|
|
72
|
-
|
|
72
|
+
onError(new Error('error'), {} as express.Request, response, () => {});
|
|
73
73
|
|
|
74
74
|
assert.strictEqual(actualStatus, 500);
|
|
75
75
|
assert.strictEqual(actualJson?.code, '500');
|