@unito/integration-sdk 0.1.7 → 0.1.8
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/.eslintrc.cjs +2 -0
- package/dist/src/errors.d.ts +0 -6
- package/dist/src/errors.js +3 -6
- package/dist/src/httpErrors.d.ts +3 -3
- package/dist/src/httpErrors.js +5 -5
- package/dist/src/index.cjs +829 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/integration.d.ts +0 -1
- package/dist/src/integration.js +2 -7
- package/dist/src/middlewares/errors.d.ts +9 -1
- package/dist/src/middlewares/errors.js +28 -13
- package/dist/src/middlewares/finish.d.ts +3 -1
- package/dist/src/middlewares/finish.js +25 -2
- package/dist/src/resources/cache.d.ts +18 -4
- package/dist/src/resources/cache.js +52 -21
- package/dist/src/resources/logger.d.ts +49 -10
- package/dist/src/resources/logger.js +64 -18
- package/dist/test/errors.test.js +1 -0
- package/dist/test/middlewares/errors.test.js +1 -1
- package/dist/test/resources/cache.test.js +7 -15
- package/dist/test/resources/logger.test.js +53 -6
- package/package.json +11 -4
- package/src/errors.ts +2 -6
- package/src/httpErrors.ts +6 -6
- package/src/index.ts +1 -0
- package/src/integration.ts +2 -9
- package/src/middlewares/errors.ts +39 -14
- package/src/middlewares/finish.ts +28 -3
- package/src/resources/cache.ts +66 -23
- package/src/resources/logger.ts +84 -24
- package/test/errors.test.ts +1 -0
- package/test/middlewares/errors.test.ts +1 -1
- package/test/resources/cache.test.ts +7 -17
- package/test/resources/logger.test.ts +60 -7
package/src/httpErrors.ts
CHANGED
|
@@ -31,20 +31,20 @@ export class TimeoutError extends HttpError {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export class
|
|
34
|
+
export class ResourceGoneError extends HttpError {
|
|
35
35
|
constructor(message?: string) {
|
|
36
|
-
super(message || '
|
|
36
|
+
super(message || 'Resource gone or unavailable', 410);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export class
|
|
40
|
+
export class UnprocessableEntityError extends HttpError {
|
|
41
41
|
constructor(message?: string) {
|
|
42
|
-
super(message || '
|
|
42
|
+
super(message || 'Unprocessable Entity', 422);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export class
|
|
46
|
+
export class RateLimitExceededError extends HttpError {
|
|
47
47
|
constructor(message?: string) {
|
|
48
|
-
super(message || '
|
|
48
|
+
super(message || 'Rate Limit Exceeded', 429);
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/index.ts
CHANGED
package/src/integration.ts
CHANGED
|
@@ -9,7 +9,7 @@ import selectsMiddleware from './middlewares/selects.js';
|
|
|
9
9
|
import errorsMiddleware from './middlewares/errors.js';
|
|
10
10
|
import finishMiddleware from './middlewares/finish.js';
|
|
11
11
|
import notFoundMiddleware from './middlewares/notFound.js';
|
|
12
|
-
import {
|
|
12
|
+
import { shutdownCaches } from './resources/cache.js';
|
|
13
13
|
import { HandlersInput, Handler } from './handler.js';
|
|
14
14
|
|
|
15
15
|
function printErrorMessage(message: string) {
|
|
@@ -27,8 +27,6 @@ export default class Integration {
|
|
|
27
27
|
|
|
28
28
|
private instance: Server<typeof IncomingMessage, typeof ServerResponse> | undefined = undefined;
|
|
29
29
|
|
|
30
|
-
private cache: Cache | undefined = undefined;
|
|
31
|
-
|
|
32
30
|
private port: number;
|
|
33
31
|
|
|
34
32
|
constructor(options: Options = {}) {
|
|
@@ -69,9 +67,6 @@ export default class Integration {
|
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
public start() {
|
|
72
|
-
// Initialize the cache.
|
|
73
|
-
this.cache = initializeCache();
|
|
74
|
-
|
|
75
70
|
// Express Server initialization
|
|
76
71
|
const app: express.Application = express();
|
|
77
72
|
|
|
@@ -121,9 +116,7 @@ export default class Integration {
|
|
|
121
116
|
this.instance.close();
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
|
|
125
|
-
await this.cache.quit();
|
|
126
|
-
}
|
|
119
|
+
await shutdownCaches();
|
|
127
120
|
} catch (e) {
|
|
128
121
|
console.error('Failed to gracefully exit', e);
|
|
129
122
|
}
|
|
@@ -1,30 +1,55 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { Error as
|
|
2
|
+
import { Error as ApiError } from '@unito/integration-api';
|
|
3
|
+
|
|
3
4
|
import { HttpError } from '../httpErrors.js';
|
|
4
5
|
|
|
6
|
+
declare global {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
8
|
+
namespace Express {
|
|
9
|
+
interface Locals {
|
|
10
|
+
error?: ApiError;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
const middleware = (err: Error, _req: Request, res: Response, next: NextFunction) => {
|
|
6
16
|
if (res.headersSent) {
|
|
7
17
|
return next(err);
|
|
8
18
|
}
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
res.locals.logger.error(err);
|
|
12
|
-
}
|
|
20
|
+
let error: ApiError;
|
|
13
21
|
|
|
14
22
|
if (err instanceof HttpError) {
|
|
15
|
-
|
|
23
|
+
error = {
|
|
24
|
+
code: err.status.toString(),
|
|
25
|
+
message: err.message,
|
|
26
|
+
details: {
|
|
27
|
+
stack: err.stack,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
} else {
|
|
31
|
+
error = {
|
|
32
|
+
code: '500',
|
|
33
|
+
message: 'Oops! Something went wrong',
|
|
34
|
+
originalError: {
|
|
35
|
+
code: err.name,
|
|
36
|
+
message: err.message,
|
|
37
|
+
details: {
|
|
38
|
+
stack: err.stack,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
16
42
|
}
|
|
17
43
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
res.locals.error = structuredClone(error);
|
|
45
|
+
|
|
46
|
+
// Keep the stack details in development for the Debugger
|
|
47
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
48
|
+
delete error.details;
|
|
49
|
+
delete error.originalError?.details;
|
|
50
|
+
}
|
|
22
51
|
|
|
23
|
-
res.status(
|
|
24
|
-
code: '500',
|
|
25
|
-
message: 'Oops! Something went wrong',
|
|
26
|
-
originalError,
|
|
27
|
-
} as APIError);
|
|
52
|
+
res.status(Number(error.code)).json(error);
|
|
28
53
|
};
|
|
29
54
|
|
|
30
55
|
export default middleware;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import
|
|
2
|
+
import { Error as ApiError } from '@unito/integration-api';
|
|
3
|
+
import { default as Logger, Metadata } from '../resources/logger.js';
|
|
3
4
|
|
|
4
5
|
declare global {
|
|
5
6
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
7
|
namespace Express {
|
|
7
8
|
interface Locals {
|
|
8
9
|
logger: Logger;
|
|
10
|
+
error?: ApiError;
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
}
|
|
@@ -13,8 +15,31 @@ declare global {
|
|
|
13
15
|
const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
14
16
|
if (req.originalUrl !== '/health') {
|
|
15
17
|
res.on('finish', function () {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
+
const error = res.locals.error;
|
|
19
|
+
|
|
20
|
+
const message = `${req.method} ${req.originalUrl} ${res.statusCode}`;
|
|
21
|
+
const metadata: Metadata = {
|
|
22
|
+
// Use reserved and standard attributes of Datadog
|
|
23
|
+
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
24
|
+
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
25
|
+
...(error
|
|
26
|
+
? {
|
|
27
|
+
error: {
|
|
28
|
+
kind: error.message,
|
|
29
|
+
stack: (error.originalError?.details?.stack ?? error.details?.stack) as string,
|
|
30
|
+
message: error.originalError?.message ?? error.message,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if ([404, 429].includes(res.statusCode)) {
|
|
37
|
+
res.locals.logger.warn(message, metadata);
|
|
38
|
+
} else if (res.statusCode >= 400) {
|
|
39
|
+
res.locals.logger.error(message, metadata);
|
|
40
|
+
} else {
|
|
41
|
+
res.locals.logger.info(message, metadata);
|
|
42
|
+
}
|
|
18
43
|
});
|
|
19
44
|
}
|
|
20
45
|
|
package/src/resources/cache.ts
CHANGED
|
@@ -1,34 +1,77 @@
|
|
|
1
|
-
import { WriteThroughCache, LocalCache, CacheInstance } from 'cachette';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { WriteThroughCache, LocalCache, CacheInstance, FetchingFunction, CachableValue } from 'cachette';
|
|
3
2
|
import * as uuid from 'uuid';
|
|
4
3
|
import Logger from './logger.js';
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Array of created caches kept to allow for graceful shutdown on exit signals.
|
|
7
|
+
*/
|
|
8
|
+
const caches: CacheInstance[] = [];
|
|
7
9
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
+
export const shutdownCaches = async () => {
|
|
11
|
+
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
12
|
+
};
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
export class Cache {
|
|
15
|
+
private cacheInstance: CacheInstance;
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
private constructor(cacheInstance: CacheInstance) {
|
|
18
|
+
this.cacheInstance = cacheInstance;
|
|
19
|
+
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
});
|
|
21
|
+
public getOrFetchValue<F extends FetchingFunction = FetchingFunction>(
|
|
22
|
+
key: string,
|
|
23
|
+
ttl: number,
|
|
24
|
+
fetcher: F,
|
|
25
|
+
lockTtl?: number,
|
|
26
|
+
shouldCacheError?: (err: Error) => boolean,
|
|
27
|
+
): Promise<ReturnType<F>> {
|
|
28
|
+
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
29
|
+
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
public getValue(key: string): Promise<CachableValue> {
|
|
32
|
+
return this.cacheInstance.getValue(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean> {
|
|
36
|
+
return this.cacheInstance.setValue(key, value, ttl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public delValue(key: string): Promise<void> {
|
|
40
|
+
return this.cacheInstance.delValue(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public getTtl(key: string): Promise<number | undefined> {
|
|
44
|
+
return this.cacheInstance.getTtl(key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initializes a WriteThroughCache instance with the provided redis url if present, or a LocalCache otherwise.
|
|
49
|
+
*
|
|
50
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
51
|
+
* @returns A cache instance.
|
|
52
|
+
*/
|
|
53
|
+
public static create(redisUrl?: string): Cache {
|
|
54
|
+
const cacheInstance: CacheInstance = redisUrl ? new WriteThroughCache(redisUrl) : new LocalCache();
|
|
55
|
+
|
|
56
|
+
// Push to the array of caches for graceful shutdown on exit signals.
|
|
57
|
+
caches.push(cacheInstance);
|
|
58
|
+
|
|
59
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
60
|
+
const correlationId = uuid.v4();
|
|
61
|
+
|
|
62
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
29
63
|
|
|
30
|
-
|
|
64
|
+
cacheInstance
|
|
65
|
+
.on('info', message => {
|
|
66
|
+
logger.info(message);
|
|
67
|
+
})
|
|
68
|
+
.on('warn', message => {
|
|
69
|
+
logger.warn(message);
|
|
70
|
+
})
|
|
71
|
+
.on('error', message => {
|
|
72
|
+
logger.error(message);
|
|
73
|
+
});
|
|
31
74
|
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
return new Cache(cacheInstance);
|
|
76
|
+
}
|
|
34
77
|
}
|
package/src/resources/logger.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
enum LogLevel {
|
|
1
|
+
const enum LogLevel {
|
|
2
2
|
ERROR = 'error',
|
|
3
3
|
WARN = 'warn',
|
|
4
4
|
INFO = 'info',
|
|
@@ -6,52 +6,112 @@ enum LogLevel {
|
|
|
6
6
|
DEBUG = 'debug',
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
|
|
10
|
+
type Value = {
|
|
11
|
+
[key: string]: PrimitiveValue | Value;
|
|
12
|
+
};
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.metadata = structuredClone(metadata);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
14
|
+
export type Metadata = Value & { message?: never };
|
|
15
|
+
type ForbidenMetadataKey = 'message';
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export default class Logger {
|
|
18
|
+
private metadata: Metadata;
|
|
19
|
+
|
|
20
|
+
constructor(metadata: Metadata = {}) {
|
|
21
|
+
this.metadata = structuredClone(metadata);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Logs a message with the 'log' log level.
|
|
26
|
+
* @param message The message to be logged.
|
|
27
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
28
|
+
*/
|
|
29
|
+
public log(message: string, metadata?: Metadata): void {
|
|
30
|
+
this.send(LogLevel.LOG, message, metadata);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Logs an error message with the 'error' log level.
|
|
35
|
+
* @param message The error message to be logged.
|
|
36
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
37
|
+
*/
|
|
38
|
+
public error(message: string, metadata?: Metadata): void {
|
|
39
|
+
this.send(LogLevel.ERROR, message, metadata);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Logs a warning message with the 'warn' log level.
|
|
44
|
+
* @param message The warning message to be logged.
|
|
45
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
46
|
+
*/
|
|
47
|
+
public warn(message: string, metadata?: Metadata): void {
|
|
48
|
+
this.send(LogLevel.WARN, message, metadata);
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Logs an informational message with the 'info' log level.
|
|
53
|
+
* @param message The informational message to be logged.
|
|
54
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
55
|
+
*/
|
|
56
|
+
public info(message: string, metadata?: Metadata): void {
|
|
57
|
+
this.send(LogLevel.INFO, message, metadata);
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Logs a debug message with the 'debug' log level.
|
|
62
|
+
* @param message The debug message to be logged.
|
|
63
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
64
|
+
*/
|
|
65
|
+
public debug(message: string, metadata?: Metadata): void {
|
|
66
|
+
this.send(LogLevel.DEBUG, message, metadata);
|
|
40
67
|
}
|
|
41
68
|
|
|
42
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Decorates the logger with additional metadata.
|
|
71
|
+
* @param metadata Additional metadata to be added to the logger.
|
|
72
|
+
*/
|
|
73
|
+
public decorate(metadata: Metadata): void {
|
|
43
74
|
this.metadata = { ...this.metadata, ...metadata };
|
|
44
75
|
}
|
|
45
76
|
|
|
46
|
-
public getMetadata() {
|
|
77
|
+
public getMetadata(): Metadata {
|
|
47
78
|
return structuredClone(this.metadata);
|
|
48
79
|
}
|
|
49
80
|
|
|
50
|
-
public setMetadata
|
|
81
|
+
public setMetadata<Key extends string>(
|
|
82
|
+
key: Key extends ForbidenMetadataKey ? never : Key,
|
|
83
|
+
value: PrimitiveValue | Value,
|
|
84
|
+
): void {
|
|
51
85
|
this.metadata[key] = value;
|
|
52
86
|
}
|
|
53
87
|
|
|
54
|
-
public clearMetadata() {
|
|
88
|
+
public clearMetadata(): void {
|
|
55
89
|
this.metadata = {};
|
|
56
90
|
}
|
|
91
|
+
|
|
92
|
+
private send(logLevel: LogLevel, message: string, metadata?: Metadata): void {
|
|
93
|
+
const processedMessage = this.snakifyKeys({
|
|
94
|
+
...this.metadata,
|
|
95
|
+
...metadata,
|
|
96
|
+
message,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (process.env.NODE_ENV === 'development') {
|
|
100
|
+
console[logLevel](JSON.stringify(processedMessage, null, 2));
|
|
101
|
+
} else {
|
|
102
|
+
console[logLevel](JSON.stringify(processedMessage));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private snakifyKeys<T extends Value>(value: T): T {
|
|
107
|
+
const result: Value = {};
|
|
108
|
+
|
|
109
|
+
for (const key in value) {
|
|
110
|
+
const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key] as Value) : value[key];
|
|
111
|
+
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
112
|
+
result[snakifiedKey] = deepValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result as T;
|
|
116
|
+
}
|
|
57
117
|
}
|
package/test/errors.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ describe('handleErrorResponse', () => {
|
|
|
9
9
|
assert.ok(errors.buildHttpError(403, 'forbidden') instanceof httpErrors.UnauthorizedError);
|
|
10
10
|
assert.ok(errors.buildHttpError(404, 'not found') instanceof httpErrors.NotFoundError);
|
|
11
11
|
assert.ok(errors.buildHttpError(408, 'timeout') instanceof httpErrors.TimeoutError);
|
|
12
|
+
assert.ok(errors.buildHttpError(410, 'resource gone') instanceof httpErrors.ResourceGoneError);
|
|
12
13
|
assert.ok(errors.buildHttpError(422, 'unprocessable entity') instanceof httpErrors.UnprocessableEntityError);
|
|
13
14
|
assert.ok(errors.buildHttpError(429, 'rate limit exceeded') instanceof httpErrors.RateLimitExceededError);
|
|
14
15
|
assert.ok(errors.buildHttpError(500, 'internal server error') instanceof httpErrors.HttpError);
|
|
@@ -49,7 +49,7 @@ describe('errors middleware', () => {
|
|
|
49
49
|
middleware(new HttpError('httpError', 429), {} as express.Request, response, () => {});
|
|
50
50
|
|
|
51
51
|
assert.strictEqual(actualStatus, 429);
|
|
52
|
-
assert.
|
|
52
|
+
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it('handles other error', () => {
|
|
@@ -1,31 +1,21 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { LocalCache } from 'cachette';
|
|
4
|
-
import {
|
|
4
|
+
import { Cache, shutdownCaches } from '../../src/resources/cache.js';
|
|
5
5
|
|
|
6
6
|
describe('Cache', () => {
|
|
7
7
|
describe('initializeCache', () => {
|
|
8
|
-
it('no redis url returns LocalCache', () => {
|
|
9
|
-
const cache =
|
|
8
|
+
it('no redis url returns Cache with a inner LocalCache', async () => {
|
|
9
|
+
const cache = Cache.create();
|
|
10
10
|
|
|
11
|
-
assert.
|
|
11
|
+
assert.ok(cache instanceof Cache);
|
|
12
|
+
assert.ok(cache['cacheInstance'] instanceof LocalCache);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
await shutdownCaches();
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
it('redis url returns (tries) WriteThroughCache', () => {
|
|
17
|
-
|
|
18
|
-
assert.throws(() => initializeCache(), Error, 'Invalid redis url fakereis.');
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('generateCacheKey', () => {
|
|
23
|
-
it('hashes string and returns hex value', () => {
|
|
24
|
-
const value = 'test';
|
|
25
|
-
const hash = generateCacheKey(value);
|
|
26
|
-
|
|
27
|
-
assert.equal(typeof hash, 'string');
|
|
28
|
-
assert.equal(hash.length, 64);
|
|
18
|
+
assert.throws(() => Cache.create('fakeredis'), Error, 'Invalid redis url fakereis.');
|
|
29
19
|
});
|
|
30
20
|
});
|
|
31
21
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
|
-
import Logger from '../../src/resources/logger.js';
|
|
3
|
+
import { default as Logger, Metadata } from '../../src/resources/logger.js';
|
|
4
4
|
|
|
5
5
|
describe('Logger', () => {
|
|
6
6
|
it('metadata', () => {
|
|
7
|
-
let metadata:
|
|
7
|
+
let metadata: Metadata = { correlation_id: 'test' };
|
|
8
8
|
const logger = new Logger(metadata);
|
|
9
9
|
|
|
10
10
|
// Changing initial object should not affect the logger
|
|
@@ -42,7 +42,7 @@ describe('Logger', () => {
|
|
|
42
42
|
logger.log('test');
|
|
43
43
|
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
44
44
|
assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
|
|
45
|
-
JSON.stringify({
|
|
45
|
+
JSON.stringify({ correlation_id: '123456789', message: 'test' }),
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
48
|
const errorSpy = testContext.mock.method(global.console, 'error', () => {});
|
|
@@ -50,7 +50,7 @@ describe('Logger', () => {
|
|
|
50
50
|
logger.error('test');
|
|
51
51
|
assert.strictEqual(errorSpy.mock.calls.length, 1);
|
|
52
52
|
assert.deepEqual(errorSpy.mock.calls[0]?.arguments, [
|
|
53
|
-
JSON.stringify({
|
|
53
|
+
JSON.stringify({ correlation_id: '123456789', message: 'test' }),
|
|
54
54
|
]);
|
|
55
55
|
|
|
56
56
|
const warnSpy = testContext.mock.method(global.console, 'warn', () => {});
|
|
@@ -58,7 +58,7 @@ describe('Logger', () => {
|
|
|
58
58
|
logger.warn('test');
|
|
59
59
|
assert.strictEqual(warnSpy.mock.calls.length, 1);
|
|
60
60
|
assert.deepEqual(warnSpy.mock.calls[0]?.arguments, [
|
|
61
|
-
JSON.stringify({
|
|
61
|
+
JSON.stringify({ correlation_id: '123456789', message: 'test' }),
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
64
|
const infoSpy = testContext.mock.method(global.console, 'info', () => {});
|
|
@@ -66,7 +66,7 @@ describe('Logger', () => {
|
|
|
66
66
|
logger.info('test');
|
|
67
67
|
assert.strictEqual(infoSpy.mock.calls.length, 1);
|
|
68
68
|
assert.deepEqual(infoSpy.mock.calls[0]?.arguments, [
|
|
69
|
-
JSON.stringify({
|
|
69
|
+
JSON.stringify({ correlation_id: '123456789', message: 'test' }),
|
|
70
70
|
]);
|
|
71
71
|
|
|
72
72
|
const debugSpy = testContext.mock.method(global.console, 'debug', () => {});
|
|
@@ -74,7 +74,60 @@ describe('Logger', () => {
|
|
|
74
74
|
logger.debug('test');
|
|
75
75
|
assert.strictEqual(debugSpy.mock.calls.length, 1);
|
|
76
76
|
assert.deepEqual(debugSpy.mock.calls[0]?.arguments, [
|
|
77
|
-
JSON.stringify({
|
|
77
|
+
JSON.stringify({ correlation_id: '123456789', message: 'test' }),
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('merges message payload with metadata', testContext => {
|
|
82
|
+
const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
|
|
83
|
+
const logger = new Logger(metadata);
|
|
84
|
+
|
|
85
|
+
const logSpy = testContext.mock.method(global.console, 'log', () => {});
|
|
86
|
+
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
87
|
+
logger.log('test', { error: { code: '200', message: 'Page Not Found' } });
|
|
88
|
+
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
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('overwrites conflicting metadata keys (1st level) with message payload', testContext => {
|
|
100
|
+
const metadata = { correlation_id: '123456789', http: { method: 'GET' } };
|
|
101
|
+
const logger = new Logger(metadata);
|
|
102
|
+
|
|
103
|
+
const logSpy = testContext.mock.method(global.console, 'log', () => {});
|
|
104
|
+
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
105
|
+
logger.log('test', { http: { status_code: 200 } });
|
|
106
|
+
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
107
|
+
assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
correlation_id: '123456789',
|
|
110
|
+
http: { status_code: 200 },
|
|
111
|
+
message: 'test',
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('snakify keys of Message and Metadata', testContext => {
|
|
117
|
+
const metadata = { correlationId: '123456789', http: { method: 'GET', statusCode: 200 } };
|
|
118
|
+
const logger = new Logger(metadata);
|
|
119
|
+
|
|
120
|
+
const logSpy = testContext.mock.method(global.console, 'log', () => {});
|
|
121
|
+
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
122
|
+
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
123
|
+
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
124
|
+
assert.deepEqual(logSpy.mock.calls[0]?.arguments, [
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
correlation_id: '123456789',
|
|
127
|
+
http: { method: 'GET', status_code: 200 },
|
|
128
|
+
error_context: { error_code: 200, error_message: 'Page Not Found' },
|
|
129
|
+
message: 'test',
|
|
130
|
+
}),
|
|
78
131
|
]);
|
|
79
132
|
});
|
|
80
133
|
});
|