@squiz/optimization-utils 1.0.0
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/cloudflare/CloudflareKVHttpService.d.ts +11 -0
- package/dist/cloudflare/CloudflareKVHttpService.js +19 -0
- package/dist/cloudflare/CloudflareKVHttpService.js.map +1 -0
- package/dist/cloudflare/ImplCloudflareKVHttpService.d.ts +22 -0
- package/dist/cloudflare/ImplCloudflareKVHttpService.js +96 -0
- package/dist/cloudflare/ImplCloudflareKVHttpService.js.map +1 -0
- package/dist/config/ConfigurationLoader.d.ts +23 -0
- package/dist/config/ConfigurationLoader.js +99 -0
- package/dist/config/ConfigurationLoader.js.map +1 -0
- package/dist/date/DateManipulator.d.ts +6 -0
- package/dist/date/DateManipulator.js +21 -0
- package/dist/date/DateManipulator.js.map +1 -0
- package/dist/event/AggregateRoot.d.ts +4 -0
- package/dist/event/AggregateRoot.js +3 -0
- package/dist/event/AggregateRoot.js.map +1 -0
- package/dist/event/DomainEvent.d.ts +13 -0
- package/dist/event/DomainEvent.js +28 -0
- package/dist/event/DomainEvent.js.map +1 -0
- package/dist/event/DynamoDBEventMapper.d.ts +36 -0
- package/dist/event/DynamoDBEventMapper.js +55 -0
- package/dist/event/DynamoDBEventMapper.js.map +1 -0
- package/dist/event/EventHandler.d.ts +14 -0
- package/dist/event/EventHandler.js +43 -0
- package/dist/event/EventHandler.js.map +1 -0
- package/dist/exception/DomainException.d.ts +18 -0
- package/dist/exception/DomainException.js +41 -0
- package/dist/exception/DomainException.js.map +1 -0
- package/dist/httpClient/FetchHttpClient.d.ts +7 -0
- package/dist/httpClient/FetchHttpClient.js +86 -0
- package/dist/httpClient/FetchHttpClient.js.map +1 -0
- package/dist/httpClient/HttpClient.d.ts +25 -0
- package/dist/httpClient/HttpClient.js +45 -0
- package/dist/httpClient/HttpClient.js.map +1 -0
- package/dist/httpClient/HttpRequestBuilder.d.ts +22 -0
- package/dist/httpClient/HttpRequestBuilder.js +126 -0
- package/dist/httpClient/HttpRequestBuilder.js.map +1 -0
- package/dist/logger/Logger.d.ts +10 -0
- package/dist/logger/Logger.js +30 -0
- package/dist/logger/Logger.js.map +1 -0
- package/dist/logger/LoggerMessage.d.ts +27 -0
- package/dist/logger/LoggerMessage.js +94 -0
- package/dist/logger/LoggerMessage.js.map +1 -0
- package/dist/logger/RemoteLogger.d.ts +30 -0
- package/dist/logger/RemoteLogger.js +35 -0
- package/dist/logger/RemoteLogger.js.map +1 -0
- package/dist/logger/SquizRemoteLogger.d.ts +53 -0
- package/dist/logger/SquizRemoteLogger.js +128 -0
- package/dist/logger/SquizRemoteLogger.js.map +1 -0
- package/dist/package.d.ts +23 -0
- package/dist/package.js +40 -0
- package/dist/package.js.map +1 -0
- package/dist/scheduler/EventBridgeScheduler.d.ts +21 -0
- package/dist/scheduler/EventBridgeScheduler.js +131 -0
- package/dist/scheduler/EventBridgeScheduler.js.map +1 -0
- package/dist/scheduler/Scheduler.d.ts +21 -0
- package/dist/scheduler/Scheduler.js +17 -0
- package/dist/scheduler/Scheduler.js.map +1 -0
- package/dist/testing/mock.d.ts +12 -0
- package/dist/testing/mock.js +51 -0
- package/dist/testing/mock.js.map +1 -0
- package/dist/typesUtils/DynamoDB.d.ts +4 -0
- package/dist/typesUtils/DynamoDB.js +3 -0
- package/dist/typesUtils/DynamoDB.js.map +1 -0
- package/dist/typesUtils/utilities.d.ts +9 -0
- package/dist/typesUtils/utilities.js +3 -0
- package/dist/typesUtils/utilities.js.map +1 -0
- package/dist/validation/handleValidation.d.ts +2 -0
- package/dist/validation/handleValidation.js +11 -0
- package/dist/validation/handleValidation.js.map +1 -0
- package/dist/valueObject/TenantId.d.ts +10 -0
- package/dist/valueObject/TenantId.js +23 -0
- package/dist/valueObject/TenantId.js.map +1 -0
- package/package.json +26 -0
- package/src/cloudflare/CloudflareKVHttpService.ts +20 -0
- package/src/cloudflare/ImplCloudflareKVHttpService.ts +128 -0
- package/src/cloudflare/__tests__/ImplCloudflareKVHttpService.spec.ts +178 -0
- package/src/config/ConfigurationLoader.ts +72 -0
- package/src/config/__tests__/ConfigurationLoader.spec.ts +62 -0
- package/src/date/DateManipulator.ts +29 -0
- package/src/date/__tests__/DateManipulator.spec.ts +64 -0
- package/src/event/AggregateRoot.ts +5 -0
- package/src/event/DomainEvent.ts +52 -0
- package/src/event/DynamoDBEventMapper.ts +72 -0
- package/src/event/EventHandler.ts +57 -0
- package/src/event/__tests__/DynamoDBEventMapper.spec.ts +113 -0
- package/src/exception/DomainException.ts +34 -0
- package/src/httpClient/FetchHttpClient.ts +92 -0
- package/src/httpClient/HttpClient.ts +46 -0
- package/src/httpClient/HttpRequestBuilder.ts +120 -0
- package/src/httpClient/__tests__/FetchHttpClient.spec.ts +146 -0
- package/src/httpClient/__tests__/HttpClient.spec.ts +52 -0
- package/src/httpClient/__tests__/httpRequestBuilder.spec.ts +75 -0
- package/src/logger/Logger.ts +40 -0
- package/src/logger/LoggerMessage.ts +151 -0
- package/src/logger/RemoteLogger.ts +32 -0
- package/src/logger/SquizRemoteLogger.ts +157 -0
- package/src/logger/__tests__/LoggerMessage.spec.ts +133 -0
- package/src/logger/__tests__/SquizRemoteLogger.spec.ts +185 -0
- package/src/package.ts +23 -0
- package/src/scheduler/EventBridgeScheduler.ts +177 -0
- package/src/scheduler/Scheduler.ts +32 -0
- package/src/scheduler/__tests__/EventBridgeScheduler.spec.ts +310 -0
- package/src/testing/mock.ts +62 -0
- package/src/typesUtils/DynamoDB.ts +17 -0
- package/src/typesUtils/utilities.ts +11 -0
- package/src/validation/handleValidation.ts +13 -0
- package/src/valueObject/TenantId.ts +27 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Container } from 'inversify';
|
|
2
|
+
import { faker } from '@faker-js/faker';
|
|
3
|
+
import {
|
|
4
|
+
HttpClient,
|
|
5
|
+
HttpMethod,
|
|
6
|
+
HttpRequestOptions,
|
|
7
|
+
HttpResponse,
|
|
8
|
+
} from '../HttpClient';
|
|
9
|
+
import { HttpRequestBuilderFactory } from '../HttpRequestBuilder';
|
|
10
|
+
import { createHttpClientMock } from '../../testing/mock';
|
|
11
|
+
|
|
12
|
+
describe('HttpRequestBuilder', () => {
|
|
13
|
+
let httpClient: HttpClient;
|
|
14
|
+
let httpRequestBuilderFactory: HttpRequestBuilderFactory;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
const container = new Container();
|
|
18
|
+
|
|
19
|
+
container.bind(HttpClient).toConstantValue(createHttpClientMock());
|
|
20
|
+
container.bind(HttpRequestBuilderFactory).to(HttpRequestBuilderFactory);
|
|
21
|
+
|
|
22
|
+
httpClient = container.get(HttpClient);
|
|
23
|
+
httpRequestBuilderFactory = container.get(HttpRequestBuilderFactory);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should compose all params and call HttpClient from builder', async () => {
|
|
27
|
+
const expectedResponse = new HttpResponse({
|
|
28
|
+
statusCode: 200,
|
|
29
|
+
body: undefined,
|
|
30
|
+
});
|
|
31
|
+
const params = {
|
|
32
|
+
url: faker.internet.url(),
|
|
33
|
+
method: faker.helpers.arrayElement(Object.values(HttpMethod)),
|
|
34
|
+
headers: {
|
|
35
|
+
example: faker.word.words(),
|
|
36
|
+
},
|
|
37
|
+
body: {
|
|
38
|
+
exampleBody: faker.word.words(),
|
|
39
|
+
},
|
|
40
|
+
handleHttpErrorStatusCodes: [401, 403, 404],
|
|
41
|
+
};
|
|
42
|
+
const bearerToken = faker.word.words();
|
|
43
|
+
const apiKey = faker.word.words();
|
|
44
|
+
|
|
45
|
+
jest
|
|
46
|
+
.spyOn(httpClient, 'sendRequest')
|
|
47
|
+
.mockResolvedValueOnce(expectedResponse);
|
|
48
|
+
|
|
49
|
+
const response = await httpRequestBuilderFactory
|
|
50
|
+
.create()
|
|
51
|
+
.url(params.url)
|
|
52
|
+
.method(params.method)
|
|
53
|
+
.body(params.body)
|
|
54
|
+
.headers(params.headers)
|
|
55
|
+
.applicationJson()
|
|
56
|
+
.authorizationByBearer(bearerToken)
|
|
57
|
+
.authorizationByXApiKey(apiKey)
|
|
58
|
+
.handleErrorStatusCode(params.handleHttpErrorStatusCodes[0])
|
|
59
|
+
.handleErrorStatusCode(params.handleHttpErrorStatusCodes[1])
|
|
60
|
+
.handleNotFound()
|
|
61
|
+
.sendRequest();
|
|
62
|
+
|
|
63
|
+
expect(response).toEqual(expectedResponse);
|
|
64
|
+
expect(httpClient.sendRequest).toHaveBeenCalledWith({
|
|
65
|
+
...params,
|
|
66
|
+
headers: expect.objectContaining({
|
|
67
|
+
...params.headers,
|
|
68
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
69
|
+
'x-api-key': apiKey,
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
}),
|
|
72
|
+
handleHttpErrorStatusCodes: params.handleHttpErrorStatusCodes,
|
|
73
|
+
} as HttpRequestOptions);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger as createWinstonLogger,
|
|
3
|
+
format,
|
|
4
|
+
LeveledLogMethod,
|
|
5
|
+
Logform,
|
|
6
|
+
LogMethod,
|
|
7
|
+
transports,
|
|
8
|
+
} from 'winston';
|
|
9
|
+
|
|
10
|
+
export abstract class Logger {
|
|
11
|
+
abstract log: LogMethod;
|
|
12
|
+
abstract error: LeveledLogMethod;
|
|
13
|
+
abstract warn: LeveledLogMethod;
|
|
14
|
+
abstract info: LeveledLogMethod;
|
|
15
|
+
abstract debug: LeveledLogMethod;
|
|
16
|
+
abstract verbose: LeveledLogMethod;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lambdaFormat = (): Logform.Format => {
|
|
20
|
+
return format.printf(({ ...attrs }) => {
|
|
21
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
|
|
22
|
+
return JSON.stringify({
|
|
23
|
+
type: 'app',
|
|
24
|
+
env: process.env.ENV,
|
|
25
|
+
name: process.env.AWS_LAMBDA_FUNCTION_NAME,
|
|
26
|
+
version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
|
|
27
|
+
region: process.env.AWS_REGION,
|
|
28
|
+
trace: process.env._X_AMZN_TRACE_ID,
|
|
29
|
+
...attrs,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function createLogger(): Logger {
|
|
35
|
+
return createWinstonLogger({
|
|
36
|
+
level: process.env.LOGGING_LEVEL || 'debug',
|
|
37
|
+
format: format.combine(format.timestamp(), format.splat(), lambdaFormat()),
|
|
38
|
+
transports: [new transports.Console()],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
type ValueOf = {
|
|
4
|
+
valueOf(): string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
const normalizeCaller = <T extends abstract new (...args: any) => any>(
|
|
9
|
+
stringOrClassInstance: string | InstanceType<T>,
|
|
10
|
+
): string =>
|
|
11
|
+
typeof stringOrClassInstance === 'string'
|
|
12
|
+
? stringOrClassInstance
|
|
13
|
+
: stringOrClassInstance.constructor.name;
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
export function createLogMessage<T extends abstract new (...args: any) => any>(
|
|
17
|
+
...metadata: Array<string | InstanceType<T>>
|
|
18
|
+
): (message?: string) => string {
|
|
19
|
+
const context = randomUUID();
|
|
20
|
+
|
|
21
|
+
const formattedMetadata = [...metadata, `Context: ${context}`]
|
|
22
|
+
.filter((data) => !!data)
|
|
23
|
+
.map((data) => `[${normalizeCaller(data)}]`)
|
|
24
|
+
.join('');
|
|
25
|
+
|
|
26
|
+
return (message) => `${formattedMetadata} ${message ?? ''}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const TENANT_ID_METADATA = (
|
|
30
|
+
tenantId: string | ValueOf,
|
|
31
|
+
): {
|
|
32
|
+
tenantId: string;
|
|
33
|
+
} => ({
|
|
34
|
+
tenantId: typeof tenantId === 'string' ? tenantId : tenantId.valueOf(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const PROCESSABLE_LOG_METADATA = { processable: true } as const;
|
|
38
|
+
|
|
39
|
+
export type LogMessage = (
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
message: string | any,
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
) => [string | any, Record<string, unknown>];
|
|
44
|
+
export type CreateLog = {
|
|
45
|
+
attachMetadata: (metadata: Record<string, unknown>) => CreateLog;
|
|
46
|
+
attachEventName: (eventName: string) => CreateLog;
|
|
47
|
+
attachCorrelationId: (id: string) => CreateLog;
|
|
48
|
+
attachTenantId: (tenantId: string | ValueOf) => CreateLog;
|
|
49
|
+
attachExperimentId: (experimentId: string | ValueOf) => CreateLog;
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
attachCaller: <T extends abstract new (...args: any) => any>(
|
|
52
|
+
caller: InstanceType<T> | string,
|
|
53
|
+
) => CreateLog;
|
|
54
|
+
isProcessable: () => CreateLog;
|
|
55
|
+
getMetadata: () => Record<string, unknown>;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
create: <T extends abstract new (...args: any) => any>(
|
|
58
|
+
caller?: InstanceType<T> | string,
|
|
59
|
+
) => LogMessage;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function createLog(opts?: {
|
|
63
|
+
message?: string;
|
|
64
|
+
metadata?: Record<string, string | boolean | unknown>;
|
|
65
|
+
}): CreateLog {
|
|
66
|
+
opts = {
|
|
67
|
+
metadata: {
|
|
68
|
+
context: randomUUID(),
|
|
69
|
+
...opts?.metadata,
|
|
70
|
+
},
|
|
71
|
+
message: opts?.message,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const attachMetadata = (
|
|
75
|
+
metadata: Record<string, string | boolean | unknown>,
|
|
76
|
+
): CreateLog => {
|
|
77
|
+
return createLog({
|
|
78
|
+
...(opts && opts),
|
|
79
|
+
metadata: {
|
|
80
|
+
...opts?.metadata,
|
|
81
|
+
...metadata,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const attachTenantId = (tenantId: string | ValueOf): CreateLog => {
|
|
87
|
+
return attachMetadata(TENANT_ID_METADATA(tenantId));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const attachExperimentId = (experimentId: string | ValueOf): CreateLog => {
|
|
91
|
+
return attachMetadata({
|
|
92
|
+
experimentId:
|
|
93
|
+
typeof experimentId === 'string'
|
|
94
|
+
? experimentId
|
|
95
|
+
: experimentId.valueOf(),
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const attachEventName = (eventName: string): CreateLog => {
|
|
100
|
+
return attachMetadata({
|
|
101
|
+
eventName,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const attachCorrelationId = (id: string): CreateLog => {
|
|
106
|
+
return attachMetadata({
|
|
107
|
+
correlationId: id,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const isProcessable = (): CreateLog => {
|
|
112
|
+
return attachMetadata(PROCESSABLE_LOG_METADATA);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const attachCaller = <T extends abstract new (...args: any) => any>(
|
|
117
|
+
caller: T | string,
|
|
118
|
+
): CreateLog => {
|
|
119
|
+
return attachMetadata({
|
|
120
|
+
caller: normalizeCaller(caller),
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
const create = <T extends abstract new (...args: any) => any>(
|
|
126
|
+
caller?: T | string,
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
): ((message: string | any) => [string | any, Record<string, unknown>]) => {
|
|
129
|
+
const metadata = {
|
|
130
|
+
...opts?.metadata,
|
|
131
|
+
...(caller && {
|
|
132
|
+
caller: normalizeCaller(caller),
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
return (message: string | any) => [message, metadata];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
attachMetadata,
|
|
142
|
+
attachTenantId,
|
|
143
|
+
attachExperimentId,
|
|
144
|
+
attachEventName,
|
|
145
|
+
attachCorrelationId,
|
|
146
|
+
isProcessable,
|
|
147
|
+
attachCaller,
|
|
148
|
+
getMetadata: () => opts?.metadata ?? {},
|
|
149
|
+
create,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TenantId } from './../valueObject/TenantId';
|
|
2
|
+
import { injectable } from 'inversify';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export enum RemoteLogLevel {
|
|
6
|
+
INFO = 'info',
|
|
7
|
+
WARN = 'warn',
|
|
8
|
+
ERROR = 'error',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const REMOTE_LOG_SCHEMA = z.object({
|
|
12
|
+
tenantId: z.string().transform((v) => new TenantId(v)),
|
|
13
|
+
message: z.string(),
|
|
14
|
+
timestamp: z
|
|
15
|
+
.string()
|
|
16
|
+
.datetime()
|
|
17
|
+
.transform((v) => new Date(v))
|
|
18
|
+
.optional(),
|
|
19
|
+
level: z.nativeEnum(RemoteLogLevel),
|
|
20
|
+
});
|
|
21
|
+
export type RemoteLog = z.infer<typeof REMOTE_LOG_SCHEMA>;
|
|
22
|
+
|
|
23
|
+
@injectable()
|
|
24
|
+
export abstract class RemoteLogger {
|
|
25
|
+
abstract info(log: Omit<RemoteLog, 'level'>): Promise<void>;
|
|
26
|
+
|
|
27
|
+
abstract warn(log: Omit<RemoteLog, 'level'>): Promise<void>;
|
|
28
|
+
|
|
29
|
+
abstract error(log: Omit<RemoteLog, 'level'>): Promise<void>;
|
|
30
|
+
|
|
31
|
+
abstract log(logs: ReadonlyArray<RemoteLog>): Promise<void>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RemoteLog, RemoteLogLevel, RemoteLogger } from './RemoteLogger';
|
|
3
|
+
import { TenantId } from './../valueObject/TenantId';
|
|
4
|
+
import { injectable } from 'inversify';
|
|
5
|
+
import { Logger } from './Logger';
|
|
6
|
+
import { HttpRequestBuilderFactory } from './../httpClient/HttpRequestBuilder';
|
|
7
|
+
import { createLogMessage } from './LoggerMessage';
|
|
8
|
+
import { HttpMethod } from './../httpClient/HttpClient';
|
|
9
|
+
|
|
10
|
+
export type SquizRemoteLoggerServiceConfig = {
|
|
11
|
+
loggerServiceUrl: URL;
|
|
12
|
+
createLogsPath: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
serviceName: string;
|
|
15
|
+
};
|
|
16
|
+
export type SquizRemoteLoggerServiceConfigProvider =
|
|
17
|
+
() => Promise<SquizRemoteLoggerServiceConfig>;
|
|
18
|
+
|
|
19
|
+
const CREATE_LOGS_REQUEST_BODY_DTO_SCHEMA = z
|
|
20
|
+
.object({
|
|
21
|
+
level: z.enum(['INFO', 'WARNING', 'ERROR']),
|
|
22
|
+
service: z.string(),
|
|
23
|
+
timestamp: z.string().datetime(),
|
|
24
|
+
host: z.string().optional(),
|
|
25
|
+
userid: z.string().optional(),
|
|
26
|
+
message: z.string().optional(),
|
|
27
|
+
tags: z.string().optional(),
|
|
28
|
+
traceid: z.string().optional(),
|
|
29
|
+
})
|
|
30
|
+
.array();
|
|
31
|
+
|
|
32
|
+
export type CreateLogsRequestBodyDto = z.infer<
|
|
33
|
+
typeof CREATE_LOGS_REQUEST_BODY_DTO_SCHEMA
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
@injectable()
|
|
37
|
+
export class SquizRemoteLogger implements RemoteLogger {
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly config: SquizRemoteLoggerServiceConfigProvider,
|
|
40
|
+
private readonly logger: Logger,
|
|
41
|
+
private readonly httpRequestBuilderFactory: HttpRequestBuilderFactory,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
async info(log: Omit<RemoteLog, 'level'>): Promise<void> {
|
|
45
|
+
await this.callApi(log.tenantId, [
|
|
46
|
+
await this.mapRemoteToRequestBody({
|
|
47
|
+
...log,
|
|
48
|
+
level: RemoteLogLevel.INFO,
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async warn(log: Omit<RemoteLog, 'level'>): Promise<void> {
|
|
54
|
+
await this.callApi(log.tenantId, [
|
|
55
|
+
await this.mapRemoteToRequestBody({
|
|
56
|
+
...log,
|
|
57
|
+
level: RemoteLogLevel.WARN,
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async error(log: Omit<RemoteLog, 'level'>): Promise<void> {
|
|
63
|
+
await this.callApi(log.tenantId, [
|
|
64
|
+
await this.mapRemoteToRequestBody({
|
|
65
|
+
...log,
|
|
66
|
+
level: RemoteLogLevel.ERROR,
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async log(logs: ReadonlyArray<RemoteLog>): Promise<void> {
|
|
72
|
+
const logsGrouped = await logs.reduce(
|
|
73
|
+
async (acc, log) => {
|
|
74
|
+
const resolvedAcc = await acc;
|
|
75
|
+
const tenantLogs = resolvedAcc[log.tenantId.valueOf()];
|
|
76
|
+
const mappedLog = await this.mapRemoteToRequestBody(log);
|
|
77
|
+
const mappedLogs: CreateLogsRequestBodyDto = tenantLogs
|
|
78
|
+
? [...tenantLogs, mappedLog]
|
|
79
|
+
: [mappedLog];
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...resolvedAcc,
|
|
83
|
+
[log.tenantId.valueOf()]: mappedLogs,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
Promise.resolve({}) as Promise<Record<string, CreateLogsRequestBodyDto>>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const entries = Object.entries(logsGrouped);
|
|
90
|
+
const promises = entries.map(async ([tenantId, logs]) => {
|
|
91
|
+
await this.callApi(new TenantId(tenantId), logs);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await Promise.all(promises);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async mapRemoteToRequestBody({
|
|
98
|
+
tenantId,
|
|
99
|
+
...log
|
|
100
|
+
}: RemoteLog): Promise<CreateLogsRequestBodyDto[number]> {
|
|
101
|
+
const config = await this.config();
|
|
102
|
+
const logLevels = new Map<
|
|
103
|
+
RemoteLogLevel,
|
|
104
|
+
CreateLogsRequestBodyDto[number]['level']
|
|
105
|
+
>([
|
|
106
|
+
[RemoteLogLevel.INFO, 'INFO'],
|
|
107
|
+
[RemoteLogLevel.WARN, 'WARNING'],
|
|
108
|
+
[RemoteLogLevel.ERROR, 'ERROR'],
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...log,
|
|
113
|
+
timestamp: log.timestamp
|
|
114
|
+
? log.timestamp.toISOString()
|
|
115
|
+
: new Date().toISOString(),
|
|
116
|
+
level: logLevels.get(
|
|
117
|
+
log.level,
|
|
118
|
+
) as CreateLogsRequestBodyDto[number]['level'],
|
|
119
|
+
service: config.serviceName,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async callApi(
|
|
124
|
+
tenantId: TenantId,
|
|
125
|
+
logs: CreateLogsRequestBodyDto,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
if (!logs.length) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const logMessage = createLogMessage(
|
|
132
|
+
this,
|
|
133
|
+
`tenantId: ${tenantId.valueOf()}`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
this.logger.debug(logMessage(`started receiving config`));
|
|
137
|
+
const config = await this.config();
|
|
138
|
+
|
|
139
|
+
this.logger.debug(logMessage(`finished receiving config`));
|
|
140
|
+
|
|
141
|
+
const url = new URL(
|
|
142
|
+
config.createLogsPath + `/${tenantId.valueOf()}`,
|
|
143
|
+
config.loggerServiceUrl,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
this.logger.debug(logMessage(`started request to: ${url.toString()}`));
|
|
147
|
+
await this.httpRequestBuilderFactory
|
|
148
|
+
.create()
|
|
149
|
+
.url(url)
|
|
150
|
+
.method(HttpMethod.POST)
|
|
151
|
+
.applicationJson()
|
|
152
|
+
.authorizationByXApiKey(config.apiKey)
|
|
153
|
+
.body(logs)
|
|
154
|
+
.sendRequest();
|
|
155
|
+
this.logger.debug(logMessage(`finished request to: ${url.toString()}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { createLog, createLogMessage } from '../LoggerMessage';
|
|
3
|
+
import { TenantId } from '../../valueObject/TenantId';
|
|
4
|
+
|
|
5
|
+
jest.mock('crypto');
|
|
6
|
+
|
|
7
|
+
describe('createLogMessage', () => {
|
|
8
|
+
class ExampleClass {}
|
|
9
|
+
|
|
10
|
+
const mockUUID = (uuid: string): jest.Mock =>
|
|
11
|
+
(randomUUID as jest.Mock).mockReturnValueOnce(uuid);
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.resetAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should create a log message with the instance of class', () => {
|
|
18
|
+
mockUUID('some-uuid');
|
|
19
|
+
|
|
20
|
+
const logMessage = createLogMessage(new ExampleClass());
|
|
21
|
+
|
|
22
|
+
expect(logMessage('example message')).toBe(
|
|
23
|
+
'[ExampleClass][Context: some-uuid] example message',
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should create a log message with given metadata', () => {
|
|
28
|
+
mockUUID('some-uuid');
|
|
29
|
+
|
|
30
|
+
const logMessage = createLogMessage(
|
|
31
|
+
new ExampleClass(),
|
|
32
|
+
'secondMetadata',
|
|
33
|
+
'thirdMetadata',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(logMessage('example message')).toBe(
|
|
37
|
+
'[ExampleClass][secondMetadata][thirdMetadata][Context: some-uuid] example message',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should create a log message without additional metadata', () => {
|
|
42
|
+
mockUUID('some-uuid');
|
|
43
|
+
|
|
44
|
+
const logMessage = createLogMessage();
|
|
45
|
+
|
|
46
|
+
expect(logMessage('example message')).toBe(
|
|
47
|
+
'[Context: some-uuid] example message',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('createLog', () => {
|
|
53
|
+
const mockUUID: jest.Mock<
|
|
54
|
+
ReturnType<typeof randomUUID>,
|
|
55
|
+
Parameters<typeof randomUUID>
|
|
56
|
+
> = randomUUID as jest.Mock;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
mockUUID.mockReset();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should create a log message with attached tenant id as metadata', () => {
|
|
63
|
+
const logMessage = createLog().attachTenantId(new TenantId('1')).create();
|
|
64
|
+
|
|
65
|
+
const [, metadata] = logMessage('test');
|
|
66
|
+
|
|
67
|
+
expect(metadata).toEqual({ tenantId: '1' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should create a log message with attached processable = true as metadata', () => {
|
|
71
|
+
const logMessage = createLog().isProcessable().create();
|
|
72
|
+
|
|
73
|
+
const [, metadata] = logMessage('test');
|
|
74
|
+
|
|
75
|
+
expect(metadata).toEqual({ processable: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should create a log message with given message', () => {
|
|
79
|
+
const logMessage = createLog().isProcessable().create();
|
|
80
|
+
|
|
81
|
+
const [message] = logMessage('test');
|
|
82
|
+
|
|
83
|
+
expect(message).toContain('test');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should create a log message with message and caller', () => {
|
|
87
|
+
class ExampleClass {}
|
|
88
|
+
|
|
89
|
+
const logMessage = createLog().isProcessable().create(new ExampleClass());
|
|
90
|
+
|
|
91
|
+
const [message] = logMessage('test');
|
|
92
|
+
|
|
93
|
+
expect(message).toContain('test');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should combine metadata', () => {
|
|
97
|
+
mockUUID.mockReturnValue('70c1d16a-c15f-462b-844b-c2cd379f425f');
|
|
98
|
+
|
|
99
|
+
const logMessage = createLog()
|
|
100
|
+
.attachTenantId(new TenantId('1'))
|
|
101
|
+
.isProcessable()
|
|
102
|
+
.attachMetadata({ some: 'metadata' })
|
|
103
|
+
.create('SomeCaller');
|
|
104
|
+
|
|
105
|
+
const [, metadata] = logMessage('test');
|
|
106
|
+
|
|
107
|
+
expect(metadata).toEqual({
|
|
108
|
+
tenantId: '1',
|
|
109
|
+
processable: true,
|
|
110
|
+
some: 'metadata',
|
|
111
|
+
context: '70c1d16a-c15f-462b-844b-c2cd379f425f',
|
|
112
|
+
caller: 'SomeCaller',
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should take the context from the base createLog function', () => {
|
|
117
|
+
mockUUID
|
|
118
|
+
.mockReturnValueOnce('70c1d16a-c15f-462b-844b-c2cd379f425f')
|
|
119
|
+
.mockReturnValue('16ed5709-8e7f-4fcd-a4e4-73dd01a66914');
|
|
120
|
+
|
|
121
|
+
const baseLogger = createLog();
|
|
122
|
+
|
|
123
|
+
const logMessage = createLog({
|
|
124
|
+
metadata: baseLogger.getMetadata(),
|
|
125
|
+
}).create();
|
|
126
|
+
|
|
127
|
+
const [, metadata] = logMessage('test');
|
|
128
|
+
|
|
129
|
+
expect(metadata).toEqual({
|
|
130
|
+
context: '70c1d16a-c15f-462b-844b-c2cd379f425f',
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|