@ogcio/fastify-logging-wrapper 4.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/README.md +55 -0
- package/__tests__/fastify-logging-wrapper.errors.test.ts +127 -0
- package/__tests__/fastify-logging-wrapper.test.ts +106 -0
- package/__tests__/helpers/build-fastify.ts +54 -0
- package/__tests__/helpers/build-logger.ts +28 -0
- package/__tests__/helpers/fastify-test-helpers.ts +258 -0
- package/__tests__/logging-wrapper.test.ts +168 -0
- package/dist/fastify-logging-wrapper.d.ts +5 -0
- package/dist/fastify-logging-wrapper.d.ts.map +1 -0
- package/dist/fastify-logging-wrapper.js +34 -0
- package/dist/fastify-logging-wrapper.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/logging-wrapper-entities.d.ts +55 -0
- package/dist/logging-wrapper-entities.d.ts.map +1 -0
- package/dist/logging-wrapper-entities.js +72 -0
- package/dist/logging-wrapper-entities.js.map +1 -0
- package/dist/logging-wrapper.d.ts +20 -0
- package/dist/logging-wrapper.d.ts.map +1 -0
- package/dist/logging-wrapper.js +66 -0
- package/dist/logging-wrapper.js.map +1 -0
- package/package.json +32 -0
- package/src/fastify-logging-wrapper.ts +59 -0
- package/src/index.ts +11 -0
- package/src/logging-wrapper-entities.ts +130 -0
- package/src/logging-wrapper.ts +106 -0
- package/tsconfig.json +12 -0
- package/tsconfig.prod.json +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Fastify Logging Wrapper
|
|
2
|
+
|
|
3
|
+
This logging wrapper goal is to standardize the records written by our Fastify services.
|
|
4
|
+
|
|
5
|
+
## How to
|
|
6
|
+
|
|
7
|
+
To use this package three steps are needed:
|
|
8
|
+
- install the package with
|
|
9
|
+
```
|
|
10
|
+
npm i @ogcio/fastify-logging-wrapper
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- use the `getLoggingConfiguration()` method to get the configuration for the `fastify` server
|
|
14
|
+
```
|
|
15
|
+
const server = fastify({
|
|
16
|
+
...getLoggingConfiguration()
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- after the server is initialized, invoke the `initializeLoggingHooks(server)` to setup the needed `fastify` hooks
|
|
21
|
+
```
|
|
22
|
+
initializeLoggingHooks(server);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's it! Just log as you usually do!
|
|
26
|
+
|
|
27
|
+
## Default records
|
|
28
|
+
|
|
29
|
+
We will have 3 mandatory log entries that will be written for each request the service manages.
|
|
30
|
+
|
|
31
|
+
Those 3 records are:
|
|
32
|
+
- **New Request**, written when a request is received
|
|
33
|
+
```
|
|
34
|
+
{"level":30,"level_name":"INFO","hostname":"hostname","request_id":"q9Y6NwwbRimle4TxcXRPkQ-0000000000","timestamp":1713868947766,"request":{"scheme":"http","method":"GET","path":"/ping","hostname":"localhost:80","query_params":{},"headers":{"user-agent":"lightMyRequest","host":"localhost:80"},"client_ip":"127.0.0.1","user_agent":"lightMyRequest"},"message":"NEW_REQUEST"}
|
|
35
|
+
```
|
|
36
|
+
- **Response**, containing most of the response data
|
|
37
|
+
```
|
|
38
|
+
{"level":30,"level_name":"INFO","hostname":"hostname","request_id":"q9Y6NwwbRimle4TxcXRPkQ-0000000000","timestamp":1713868947769,"request":{"scheme":"http","method":"GET","path":"/ping","hostname":"localhost:80","query_params":{}},"response":{"status_code":200,"headers":{"content-type":"application/json; charset=utf-8","content-length":"17"}},"message":"RESPONSE"}
|
|
39
|
+
```
|
|
40
|
+
- **API Track**, it contains data about the lifecycle of the request, including errors, if any
|
|
41
|
+
```
|
|
42
|
+
{"level":30,"level_name":"INFO","hostname":"hostname","request_id":"5c_RLAnSS4y9-Q5STsJyiQ-0000000008","timestamp":1713869128434,"request":{"scheme":"http","method":"GET","path":"/this-path-must-not-exist","hostname":"localhost:80","query_params":{"status_code":"404","error_message":"Not Found"}},"response":{"status_code":404,"headers":{"content-type":"application/json; charset=utf-8","content-length":"107"}},"error":{"class":"REQUEST_ERROR","message":"Not Found","code":"FST_ERR_NOT_FOUND"},"message":"API_TRACK"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Error record
|
|
46
|
+
|
|
47
|
+
If an error is thrown, a log entry is automatically written.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
{"level":50,"level_name":"ERROR","hostname":"hostname","request_id":"1kPptKhMSeyZ9OwcSwBxhg-0000000008","timestamp":1713869258238,"request":{"scheme":"http","method":"GET","path":"/this-path-must-not-exist","hostname":"localhost:80","query_params":{"status_code":"404","error_message":"Not Found"}},"error":{"class":"REQUEST_ERROR","message":"Not Found","trace":"FastifyError: Not Found..... The whole trace here","code":"FST_ERR_NOT_FOUND"},"message":"ERROR"}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Additional entries
|
|
54
|
+
|
|
55
|
+
Additional log entries can be added as needed, but they will include, thanks to this package, common info about the request context that will be useful for future troubleshooting.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { LogErrorClasses } from "../src/logging-wrapper-entities.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_METHOD,
|
|
6
|
+
checkExpectedApiTrackEntry,
|
|
7
|
+
checkExpectedErrorEntry,
|
|
8
|
+
checkExpectedRequestEntry,
|
|
9
|
+
checkExpectedResponseEntry,
|
|
10
|
+
initializeServer,
|
|
11
|
+
parseLogEntry,
|
|
12
|
+
runErrorTest,
|
|
13
|
+
} from "./helpers/fastify-test-helpers.js";
|
|
14
|
+
import { httpErrors } from '@fastify/sensible';
|
|
15
|
+
|
|
16
|
+
test("Error data are correctly set", async (t) => {
|
|
17
|
+
const { server, loggingDestination } = initializeServer();
|
|
18
|
+
t.after(() => server.close());
|
|
19
|
+
await runErrorTest({
|
|
20
|
+
server,
|
|
21
|
+
loggingDestination,
|
|
22
|
+
inputStatusCode: "500",
|
|
23
|
+
expectedStatusCode: 500,
|
|
24
|
+
errorMessage: "WHoooopS!",
|
|
25
|
+
expectedClass: LogErrorClasses.ServerError,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Unknown Error route logs expected values", async (t) => {
|
|
30
|
+
const { server, loggingDestination } = initializeServer();
|
|
31
|
+
t.after(() => server.close());
|
|
32
|
+
await runErrorTest({
|
|
33
|
+
server,
|
|
34
|
+
loggingDestination,
|
|
35
|
+
inputStatusCode: "399",
|
|
36
|
+
expectedStatusCode: 500,
|
|
37
|
+
errorMessage: "Unknown!",
|
|
38
|
+
expectedClass: LogErrorClasses.UnknownError,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("400 Error route logs expected values", async (t) => {
|
|
43
|
+
const { server, loggingDestination } = initializeServer();
|
|
44
|
+
t.after(() => server.close());
|
|
45
|
+
await runErrorTest({
|
|
46
|
+
server,
|
|
47
|
+
loggingDestination,
|
|
48
|
+
inputStatusCode: "400",
|
|
49
|
+
expectedStatusCode: 400,
|
|
50
|
+
errorMessage: "Bad request!",
|
|
51
|
+
expectedClass: LogErrorClasses.RequestError,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("422 Validation Error route logs expected values", async (t) => {
|
|
56
|
+
const { server, loggingDestination } = initializeServer();
|
|
57
|
+
t.after(() => server.close());
|
|
58
|
+
await runErrorTest({
|
|
59
|
+
server,
|
|
60
|
+
loggingDestination,
|
|
61
|
+
inputStatusCode: "422",
|
|
62
|
+
expectedStatusCode: 422,
|
|
63
|
+
errorMessage: "Bad request!",
|
|
64
|
+
expectedClass: LogErrorClasses.ValidationError,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("Error without status code logs expected values", async (t) => {
|
|
69
|
+
const { server, loggingDestination } = initializeServer();
|
|
70
|
+
t.after(() => server.close());
|
|
71
|
+
await runErrorTest({
|
|
72
|
+
server,
|
|
73
|
+
loggingDestination,
|
|
74
|
+
inputStatusCode: undefined,
|
|
75
|
+
expectedStatusCode: 500,
|
|
76
|
+
errorMessage: "Unknown!",
|
|
77
|
+
expectedClass: LogErrorClasses.UnknownError,
|
|
78
|
+
expectedFastifyCode: "UNHANDLED_EXCEPTION",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("Life events error logs expected values", async (t) => {
|
|
83
|
+
const { server, loggingDestination } = initializeServer();
|
|
84
|
+
t.after(() => server.close());
|
|
85
|
+
const response = await server.inject({
|
|
86
|
+
method: DEFAULT_METHOD,
|
|
87
|
+
url: "/life-events-error",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.ok(typeof response !== "undefined");
|
|
91
|
+
assert.equal(response.statusCode, 500);
|
|
92
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
93
|
+
assert.equal(loggedRecords.length, 4);
|
|
94
|
+
const mockErrorInstance = httpErrors.createError("mock");
|
|
95
|
+
checkExpectedRequestEntry({
|
|
96
|
+
requestLogEntry: loggedRecords[0],
|
|
97
|
+
inputPath: "/life-events-error",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
checkExpectedErrorEntry({
|
|
101
|
+
errorLogEntry: loggedRecords[1],
|
|
102
|
+
inputPath: "/life-events-error",
|
|
103
|
+
errorClass: LogErrorClasses.ServerError,
|
|
104
|
+
errorMessage: "mock",
|
|
105
|
+
errorCode: mockErrorInstance.name,
|
|
106
|
+
expectedLevelName: "ERROR",
|
|
107
|
+
});
|
|
108
|
+
const parsed = parseLogEntry(loggedRecords[1]);
|
|
109
|
+
assert.equal(parsed.error.process, "TESTING");
|
|
110
|
+
assert.equal(parsed.error.parent.message, "I am the parent");
|
|
111
|
+
assert.equal(parsed.error.parent.name, "Error");
|
|
112
|
+
assert.equal(typeof parsed.error.parent.stack, "string");
|
|
113
|
+
|
|
114
|
+
checkExpectedResponseEntry({
|
|
115
|
+
responseLogEntry: loggedRecords[2],
|
|
116
|
+
inputPath: "/life-events-error",
|
|
117
|
+
responseStatusCode: 500,
|
|
118
|
+
});
|
|
119
|
+
checkExpectedApiTrackEntry({
|
|
120
|
+
apiTrackLogEntry: loggedRecords[3],
|
|
121
|
+
inputPath: "/life-events-error",
|
|
122
|
+
responseStatusCode: 500,
|
|
123
|
+
errorClass: LogErrorClasses.ServerError,
|
|
124
|
+
errorMessage: "mock",
|
|
125
|
+
errorCode: mockErrorInstance.name,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { LogMessages } from "../src/logging-wrapper-entities.js";
|
|
2
|
+
import { initializeServer, DEFAULT_METHOD, DEFAULT_PATH, checkExpectedRequestEntry, checkExpectedResponseEntry, parseLogEntry, checkGenericEntryFields } from "./helpers/fastify-test-helpers.js";
|
|
3
|
+
import { REQUEST_ID_HEADER } from "@ogcio/shared-errors";
|
|
4
|
+
import assert from "node:assert";
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
|
|
7
|
+
test("Logging entries when all works fine are the expected ones", async (t) => {
|
|
8
|
+
const { server, loggingDestination } = initializeServer();
|
|
9
|
+
t.after(() => server.close());
|
|
10
|
+
|
|
11
|
+
const response = await server.inject({
|
|
12
|
+
method: DEFAULT_METHOD,
|
|
13
|
+
url: DEFAULT_PATH,
|
|
14
|
+
});
|
|
15
|
+
assert.ok(typeof response !== "undefined");
|
|
16
|
+
assert.equal(response?.statusCode, 200);
|
|
17
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
18
|
+
assert.equal(loggedRecords.length, 3);
|
|
19
|
+
checkExpectedRequestEntry({
|
|
20
|
+
requestLogEntry: loggedRecords[0],
|
|
21
|
+
});
|
|
22
|
+
checkExpectedResponseEntry({
|
|
23
|
+
responseLogEntry: loggedRecords[1],
|
|
24
|
+
responseStatusCode: 200,
|
|
25
|
+
});
|
|
26
|
+
checkExpectedResponseEntry({
|
|
27
|
+
responseLogEntry: loggedRecords[2],
|
|
28
|
+
responseStatusCode: 200,
|
|
29
|
+
expectedMessage: LogMessages.ApiTrack,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("Request id is overriden by header", async (t) => {
|
|
34
|
+
const { server, loggingDestination } = initializeServer();
|
|
35
|
+
t.after(() => server.close());
|
|
36
|
+
const customRequestId = "Another request id";
|
|
37
|
+
const response = await server.inject({
|
|
38
|
+
method: DEFAULT_METHOD,
|
|
39
|
+
url: DEFAULT_PATH,
|
|
40
|
+
headers: { [REQUEST_ID_HEADER]: customRequestId },
|
|
41
|
+
});
|
|
42
|
+
assert.ok(typeof response !== "undefined");
|
|
43
|
+
assert.equal(response?.statusCode, 200);
|
|
44
|
+
const logged = loggingDestination.getLoggedRecords();
|
|
45
|
+
checkExpectedRequestEntry({
|
|
46
|
+
requestLogEntry: logged[0],
|
|
47
|
+
inputHeaders: { [REQUEST_ID_HEADER]: customRequestId },
|
|
48
|
+
});
|
|
49
|
+
const parsedEntry = parseLogEntry(logged[0]);
|
|
50
|
+
assert.deepEqual(parsedEntry.request_id, customRequestId);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("Logging context is reset between requests", async (t) => {
|
|
54
|
+
const { server, loggingDestination } = initializeServer();
|
|
55
|
+
t.after(() => server.close());
|
|
56
|
+
|
|
57
|
+
let response = await server.inject({
|
|
58
|
+
method: DEFAULT_METHOD,
|
|
59
|
+
url: DEFAULT_PATH,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assert.ok(typeof response !== "undefined");
|
|
63
|
+
assert.equal(response?.statusCode, 200);
|
|
64
|
+
let loggedRecords = loggingDestination.getLoggedRecords();
|
|
65
|
+
assert.equal(loggedRecords.length, 3);
|
|
66
|
+
let parsedResponse = parseLogEntry(loggedRecords[1]);
|
|
67
|
+
assert.ok(typeof parsedResponse.response !== "undefined");
|
|
68
|
+
|
|
69
|
+
response = await server.inject({
|
|
70
|
+
method: DEFAULT_METHOD,
|
|
71
|
+
url: DEFAULT_PATH,
|
|
72
|
+
});
|
|
73
|
+
assert.ok(typeof response !== "undefined");
|
|
74
|
+
assert.equal(response?.statusCode, 200);
|
|
75
|
+
loggedRecords = loggingDestination.getLoggedRecords();
|
|
76
|
+
assert.equal(loggedRecords.length, 6);
|
|
77
|
+
// 3 is the New Request for 2nd call
|
|
78
|
+
parsedResponse = parseLogEntry(loggedRecords[3]);
|
|
79
|
+
// if undefined it means that the logging context
|
|
80
|
+
// has been reset between requests
|
|
81
|
+
assert.ok(typeof parsedResponse.response === "undefined");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Additional logs are correctly written", async (t) => {
|
|
85
|
+
const { server, loggingDestination } = initializeServer();
|
|
86
|
+
t.after(() => server.close());
|
|
87
|
+
const logMessage = "Testing additional logs";
|
|
88
|
+
|
|
89
|
+
const response = await server.inject({
|
|
90
|
+
method: "POST",
|
|
91
|
+
url: "/logs",
|
|
92
|
+
body: { log_entry: logMessage },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
assert.ok(typeof response !== "undefined");
|
|
96
|
+
assert.equal(response?.statusCode, 200);
|
|
97
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
98
|
+
assert.equal(loggedRecords.length, 4);
|
|
99
|
+
const parsedAdditional = parseLogEntry(loggedRecords[1]);
|
|
100
|
+
checkGenericEntryFields({
|
|
101
|
+
parsedEntry: parsedAdditional,
|
|
102
|
+
expectedLevelName: "INFO",
|
|
103
|
+
expectedMessage: logMessage,
|
|
104
|
+
});
|
|
105
|
+
assert.ok(typeof parsedAdditional.request !== "undefined");
|
|
106
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import {
|
|
3
|
+
getLoggingConfiguration,
|
|
4
|
+
initializeLoggingHooks,
|
|
5
|
+
} from "../../src/fastify-logging-wrapper.js";
|
|
6
|
+
import { DestinationStream } from "pino";
|
|
7
|
+
import { createError } from "@fastify/error";
|
|
8
|
+
import { httpErrors } from "@fastify/sensible";
|
|
9
|
+
|
|
10
|
+
export const buildFastify = (loggerDestination?: DestinationStream) => {
|
|
11
|
+
const server = Fastify({
|
|
12
|
+
...getLoggingConfiguration(loggerDestination),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
initializeLoggingHooks(server);
|
|
16
|
+
|
|
17
|
+
server.get("/ping", async (_request, _reply) => {
|
|
18
|
+
return { data: "pong\n" };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
server.get("/error", async (request, _reply) => {
|
|
22
|
+
const parsed = request.query as { [x: string]: unknown };
|
|
23
|
+
const requestedStatusCode = Number(parsed["status_code"] ?? "500");
|
|
24
|
+
const requestedMessage = String(parsed["error_message"] ?? "WHOOOPS");
|
|
25
|
+
|
|
26
|
+
if (!parsed["status_code"]) {
|
|
27
|
+
throw new Error(requestedMessage);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw createError(
|
|
31
|
+
"CUSTOM_CODE",
|
|
32
|
+
requestedMessage as string,
|
|
33
|
+
requestedStatusCode as number
|
|
34
|
+
)();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.get("/life-events-error", async (_request, _reply) => {
|
|
38
|
+
throw httpErrors.createError(500, "mock", {
|
|
39
|
+
parentError: new Error("I am the parent"),
|
|
40
|
+
errorProcess: "TESTING",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
server.post("/logs", async (request, _reply) => {
|
|
45
|
+
const body = request.body as { [x: string]: unknown };
|
|
46
|
+
const logMessage = body.log_entry ?? "Default additional message";
|
|
47
|
+
|
|
48
|
+
request.log.info(logMessage);
|
|
49
|
+
|
|
50
|
+
return { data: { message: logMessage } };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return server;
|
|
54
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PinoLoggerOptions } from "fastify/types/logger.js";
|
|
2
|
+
import { pino } from "pino";
|
|
3
|
+
|
|
4
|
+
export const buildLogger = (loggerConfiguration: PinoLoggerOptions) => {
|
|
5
|
+
const { loggerDestination, getLoggedRecords } = getTestingDestinationLogger();
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
logger: pino(loggerConfiguration, loggerDestination),
|
|
9
|
+
loggedRecordsMethod: getLoggedRecords,
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface TestingLoggerDestination {
|
|
14
|
+
loggerDestination: {
|
|
15
|
+
write: (_data: string) => number;
|
|
16
|
+
};
|
|
17
|
+
getLoggedRecords: () => string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getTestingDestinationLogger = (): TestingLoggerDestination => {
|
|
21
|
+
const testCaseRecords: string[] = [];
|
|
22
|
+
const getLoggedRecords = () => testCaseRecords;
|
|
23
|
+
const loggerDestination = {
|
|
24
|
+
write: (data: string) => testCaseRecords.push(data),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return { loggerDestination, getLoggedRecords };
|
|
28
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { FastifyInstance } from "fastify";
|
|
2
|
+
import { buildFastify } from "./build-fastify.js";
|
|
3
|
+
import {
|
|
4
|
+
TestingLoggerDestination,
|
|
5
|
+
getTestingDestinationLogger,
|
|
6
|
+
} from "./build-logger.js";
|
|
7
|
+
import {
|
|
8
|
+
LogErrorClasses,
|
|
9
|
+
LogMessages,
|
|
10
|
+
} from "../../src/logging-wrapper-entities.js";
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_HOSTNAME = "localhost:80";
|
|
14
|
+
export const DEFAULT_USER_AGENT = "lightMyRequest";
|
|
15
|
+
export const DEFAULT_REQUEST_HEADERS = {
|
|
16
|
+
"user-agent": "lightMyRequest",
|
|
17
|
+
host: "localhost:80",
|
|
18
|
+
};
|
|
19
|
+
export const DEFAULT_CLIENT_IP = "127.0.0.1";
|
|
20
|
+
export const DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
21
|
+
export const DEFAULT_METHOD = "GET";
|
|
22
|
+
export const DEFAULT_SCHEME = "http";
|
|
23
|
+
export const DEFAULT_PATH = "/ping";
|
|
24
|
+
|
|
25
|
+
export const initializeServer = (): {
|
|
26
|
+
server: FastifyInstance;
|
|
27
|
+
loggingDestination: TestingLoggerDestination;
|
|
28
|
+
} => {
|
|
29
|
+
const loggingDestination = getTestingDestinationLogger();
|
|
30
|
+
const server = buildFastify(loggingDestination.loggerDestination);
|
|
31
|
+
|
|
32
|
+
return { server, loggingDestination };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
export const parseLogEntry = (logEntry: string): { [x: string]: any } =>
|
|
37
|
+
JSON.parse(logEntry);
|
|
38
|
+
|
|
39
|
+
export const checkGenericEntryFields = (params: {
|
|
40
|
+
parsedEntry: { [x: string]: unknown };
|
|
41
|
+
expectedLevelName: string;
|
|
42
|
+
expectedMessage: string;
|
|
43
|
+
}): void => {
|
|
44
|
+
const { parsedEntry, expectedLevelName, expectedMessage } = params;
|
|
45
|
+
|
|
46
|
+
assert.equal(parsedEntry.level_name, expectedLevelName);
|
|
47
|
+
assert.ok(typeof parsedEntry.level !== "undefined");
|
|
48
|
+
assert.equal(typeof parsedEntry.level, "number");
|
|
49
|
+
assert.equal(parsedEntry.message, expectedMessage);
|
|
50
|
+
assert.ok(typeof parsedEntry.request_id !== "undefined");
|
|
51
|
+
assert.equal(typeof parsedEntry.request_id, "string");
|
|
52
|
+
assert.ok(typeof parsedEntry.timestamp !== "undefined");
|
|
53
|
+
assert.equal(typeof parsedEntry.timestamp, "number");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const checkExpectedRequestEntry = (params: {
|
|
57
|
+
requestLogEntry: string;
|
|
58
|
+
inputScheme?: string;
|
|
59
|
+
inputQueryParams?: { [x: string]: unknown };
|
|
60
|
+
inputMethod?: string;
|
|
61
|
+
inputPath?: string;
|
|
62
|
+
inputHeaders?: { [x: string]: unknown };
|
|
63
|
+
}): void => {
|
|
64
|
+
const parsed = parseLogEntry(params.requestLogEntry);
|
|
65
|
+
params.inputMethod = params.inputMethod ?? DEFAULT_METHOD;
|
|
66
|
+
params.inputScheme = params.inputScheme ?? DEFAULT_SCHEME;
|
|
67
|
+
params.inputPath = params.inputPath ?? DEFAULT_PATH;
|
|
68
|
+
checkGenericEntryFields({
|
|
69
|
+
parsedEntry: parsed,
|
|
70
|
+
expectedLevelName: "INFO",
|
|
71
|
+
expectedMessage: LogMessages.NewRequest,
|
|
72
|
+
});
|
|
73
|
+
assert.ok(typeof parsed.request !== "undefined");
|
|
74
|
+
assert.equal(parsed.request?.scheme, params.inputScheme);
|
|
75
|
+
assert.equal(parsed.request?.method, params.inputMethod);
|
|
76
|
+
assert.equal(parsed.request?.path, params.inputPath);
|
|
77
|
+
assert.equal(parsed.request?.hostname, DEFAULT_HOSTNAME);
|
|
78
|
+
assert.deepStrictEqual(parsed.request?.query_params, params.inputQueryParams ?? {});
|
|
79
|
+
assert.deepStrictEqual(parsed.request?.headers, {
|
|
80
|
+
...DEFAULT_REQUEST_HEADERS,
|
|
81
|
+
...(params.inputHeaders ?? {}),
|
|
82
|
+
});
|
|
83
|
+
assert.equal(parsed.request?.client_ip, DEFAULT_CLIENT_IP);
|
|
84
|
+
assert.equal(parsed.request?.user_agent, DEFAULT_USER_AGENT);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const checkExpectedResponseEntry = (params: {
|
|
88
|
+
responseLogEntry: string;
|
|
89
|
+
inputScheme?: string;
|
|
90
|
+
inputQueryParams?: { [x: string]: unknown };
|
|
91
|
+
inputMethod?: string;
|
|
92
|
+
inputPath?: string;
|
|
93
|
+
responseStatusCode: number;
|
|
94
|
+
expectedMessage?: string;
|
|
95
|
+
}): void => {
|
|
96
|
+
const parsed = parseLogEntry(params.responseLogEntry);
|
|
97
|
+
params.inputMethod = params.inputMethod ?? DEFAULT_METHOD;
|
|
98
|
+
params.inputScheme = params.inputScheme ?? DEFAULT_SCHEME;
|
|
99
|
+
params.inputPath = params.inputPath ?? DEFAULT_PATH;
|
|
100
|
+
|
|
101
|
+
checkGenericEntryFields({
|
|
102
|
+
parsedEntry: parsed,
|
|
103
|
+
expectedLevelName: "INFO",
|
|
104
|
+
expectedMessage: params.expectedMessage ?? LogMessages.Response,
|
|
105
|
+
});
|
|
106
|
+
assert.ok(typeof parsed.request !== "undefined");
|
|
107
|
+
assert.equal(parsed.request.scheme, params.inputScheme);
|
|
108
|
+
assert.equal(parsed.request.method, params.inputMethod);
|
|
109
|
+
assert.equal(parsed.request.path, params.inputPath);
|
|
110
|
+
assert.equal(parsed.request.hostname, DEFAULT_HOSTNAME);
|
|
111
|
+
assert.deepStrictEqual(parsed.request.query_params, params.inputQueryParams ?? {});
|
|
112
|
+
assert.ok(typeof parsed.response !== "undefined");
|
|
113
|
+
assert.equal(parsed.response.status_code, params.responseStatusCode);
|
|
114
|
+
assert.equal(parsed.response.headers["content-type"], DEFAULT_CONTENT_TYPE);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const checkExpectedApiTrackEntry = (params: {
|
|
118
|
+
apiTrackLogEntry: string;
|
|
119
|
+
inputScheme?: string;
|
|
120
|
+
inputQueryParams?: { [x: string]: unknown };
|
|
121
|
+
inputMethod?: string;
|
|
122
|
+
inputPath?: string;
|
|
123
|
+
errorClass?: string;
|
|
124
|
+
errorMessage?: string;
|
|
125
|
+
errorCode?: string;
|
|
126
|
+
responseStatusCode: number;
|
|
127
|
+
}) => {
|
|
128
|
+
params.inputMethod = params.inputMethod ?? DEFAULT_METHOD;
|
|
129
|
+
params.inputScheme = params.inputScheme ?? DEFAULT_SCHEME;
|
|
130
|
+
params.inputPath = params.inputPath ?? DEFAULT_PATH;
|
|
131
|
+
|
|
132
|
+
checkExpectedResponseEntry({
|
|
133
|
+
...params,
|
|
134
|
+
responseLogEntry: params.apiTrackLogEntry,
|
|
135
|
+
expectedMessage: LogMessages.ApiTrack,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!params.errorClass) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const parsed = parseLogEntry(params.apiTrackLogEntry);
|
|
143
|
+
|
|
144
|
+
assert.ok(typeof parsed.error !== "undefined");
|
|
145
|
+
assert.equal(parsed.error.class, params.errorClass);
|
|
146
|
+
assert.equal(parsed.error.message, params.errorMessage);
|
|
147
|
+
assert.equal(parsed.error.code, params.errorCode);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const checkExpectedErrorEntry = (params: {
|
|
151
|
+
errorLogEntry: string;
|
|
152
|
+
inputScheme?: string;
|
|
153
|
+
inputQueryParams?: { [x: string]: unknown };
|
|
154
|
+
inputMethod?: string;
|
|
155
|
+
inputPath?: string;
|
|
156
|
+
errorClass: string;
|
|
157
|
+
errorMessage: string;
|
|
158
|
+
errorCode: string;
|
|
159
|
+
expectedLevelName?: string;
|
|
160
|
+
}): void => {
|
|
161
|
+
const parsed = parseLogEntry(params.errorLogEntry);
|
|
162
|
+
params.inputMethod = params.inputMethod ?? DEFAULT_METHOD;
|
|
163
|
+
params.inputScheme = params.inputScheme ?? DEFAULT_SCHEME;
|
|
164
|
+
params.inputPath = params.inputPath ?? DEFAULT_PATH;
|
|
165
|
+
params.expectedLevelName = params.expectedLevelName ?? "ERROR";
|
|
166
|
+
checkGenericEntryFields({
|
|
167
|
+
parsedEntry: parsed,
|
|
168
|
+
expectedLevelName: params.expectedLevelName,
|
|
169
|
+
expectedMessage: "ERROR",
|
|
170
|
+
});
|
|
171
|
+
assert.ok(typeof parsed.request !== "undefined");
|
|
172
|
+
assert.equal(parsed.request?.scheme, params.inputScheme);
|
|
173
|
+
assert.equal(parsed.request?.method, params.inputMethod);
|
|
174
|
+
assert.equal(parsed.request?.path, params.inputPath);
|
|
175
|
+
assert.equal(parsed.request?.hostname, DEFAULT_HOSTNAME);
|
|
176
|
+
assert.deepStrictEqual(parsed.request?.query_params, params.inputQueryParams ?? {});
|
|
177
|
+
assert.ok(typeof parsed.error !== "undefined");
|
|
178
|
+
assert.equal(parsed.error.class, params.errorClass);
|
|
179
|
+
assert.equal(parsed.error.code, params.errorCode);
|
|
180
|
+
assert.equal(parsed.error.message, params.errorMessage);
|
|
181
|
+
assert.ok(typeof parsed.error.trace !== "undefined");
|
|
182
|
+
assert.equal(typeof parsed.error.trace, "string");
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const runErrorTest = async (params: {
|
|
186
|
+
server: FastifyInstance;
|
|
187
|
+
loggingDestination: TestingLoggerDestination;
|
|
188
|
+
inputStatusCode?: string;
|
|
189
|
+
errorMessage: string;
|
|
190
|
+
expectedClass: LogErrorClasses;
|
|
191
|
+
expectedStatusCode: number;
|
|
192
|
+
expectedErrorMessage?: string;
|
|
193
|
+
path?: string;
|
|
194
|
+
expectedFastifyCode?: string;
|
|
195
|
+
}) => {
|
|
196
|
+
const {
|
|
197
|
+
server,
|
|
198
|
+
loggingDestination,
|
|
199
|
+
inputStatusCode,
|
|
200
|
+
errorMessage,
|
|
201
|
+
expectedClass,
|
|
202
|
+
expectedStatusCode,
|
|
203
|
+
expectedErrorMessage,
|
|
204
|
+
} = params;
|
|
205
|
+
const path = params.path ?? "/error";
|
|
206
|
+
const expectedFastifyCode = params.expectedFastifyCode ?? "CUSTOM_CODE";
|
|
207
|
+
const inputHeaders = { accept: DEFAULT_CONTENT_TYPE };
|
|
208
|
+
const query: { error_message: string; status_code?: string } = {
|
|
209
|
+
error_message: errorMessage,
|
|
210
|
+
};
|
|
211
|
+
if (inputStatusCode) {
|
|
212
|
+
query.status_code = inputStatusCode;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const response = await server.inject({
|
|
216
|
+
method: DEFAULT_METHOD,
|
|
217
|
+
url: path,
|
|
218
|
+
query,
|
|
219
|
+
headers: inputHeaders,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
assert.ok(typeof response !== "undefined");
|
|
223
|
+
assert.equal(response.statusCode, expectedStatusCode);
|
|
224
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
225
|
+
assert.equal(loggedRecords.length, 4);
|
|
226
|
+
checkExpectedRequestEntry({
|
|
227
|
+
requestLogEntry: loggedRecords[0],
|
|
228
|
+
inputPath: path,
|
|
229
|
+
inputQueryParams: query,
|
|
230
|
+
inputHeaders,
|
|
231
|
+
});
|
|
232
|
+
checkExpectedErrorEntry({
|
|
233
|
+
errorLogEntry: loggedRecords[1],
|
|
234
|
+
inputPath: path,
|
|
235
|
+
errorClass: expectedClass,
|
|
236
|
+
errorMessage,
|
|
237
|
+
errorCode: expectedFastifyCode,
|
|
238
|
+
inputQueryParams: query,
|
|
239
|
+
expectedLevelName: "ERROR",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
checkExpectedResponseEntry({
|
|
243
|
+
responseLogEntry: loggedRecords[2],
|
|
244
|
+
inputPath: path,
|
|
245
|
+
inputQueryParams: query,
|
|
246
|
+
responseStatusCode: Number(expectedStatusCode),
|
|
247
|
+
expectedMessage: expectedErrorMessage,
|
|
248
|
+
});
|
|
249
|
+
checkExpectedApiTrackEntry({
|
|
250
|
+
apiTrackLogEntry: loggedRecords[3],
|
|
251
|
+
inputPath: path,
|
|
252
|
+
inputQueryParams: query,
|
|
253
|
+
responseStatusCode: Number(expectedStatusCode),
|
|
254
|
+
errorClass: expectedClass,
|
|
255
|
+
errorMessage,
|
|
256
|
+
errorCode: expectedFastifyCode,
|
|
257
|
+
});
|
|
258
|
+
};
|