@ogcio/fastify-logging-wrapper 5.3.1 → 5.4.1
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/CHANGELOG.md +29 -0
- package/README.md +32 -0
- package/__tests__/fastify-logging-wrapper.errors.test.ts +2 -2
- package/__tests__/fastify-logging-wrapper.test.ts +85 -3
- package/__tests__/field-filter.test.ts +57 -0
- package/__tests__/helpers/build-fastify.ts +20 -2
- package/__tests__/helpers/fastify-test-helpers.ts +7 -5
- package/__tests__/logging-filtering.test.ts +99 -0
- package/__tests__/logging-wrapper.test.ts +8 -7
- package/coverage/cobertura-coverage.xml +347 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/fastify-logging-wrapper.ts.html +706 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/field-filter.ts.html +223 -0
- package/coverage/lcov-report/index.html +176 -0
- package/coverage/lcov-report/index.ts.html +130 -0
- package/coverage/lcov-report/logging-wrapper-entities.ts.html +484 -0
- package/coverage/lcov-report/logging-wrapper.ts.html +481 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +372 -0
- package/dist/fastify-logging-wrapper.d.ts +2 -1
- package/dist/fastify-logging-wrapper.d.ts.map +1 -1
- package/dist/fastify-logging-wrapper.js +58 -5
- package/dist/fastify-logging-wrapper.js.map +1 -1
- package/dist/field-filter.d.ts +17 -0
- package/dist/field-filter.d.ts.map +1 -0
- package/dist/field-filter.js +22 -0
- package/dist/field-filter.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/logging-wrapper-entities.d.ts +9 -4
- package/dist/logging-wrapper-entities.d.ts.map +1 -1
- package/dist/logging-wrapper-entities.js.map +1 -1
- package/dist/logging-wrapper.d.ts +4 -1
- package/dist/logging-wrapper.d.ts.map +1 -1
- package/dist/logging-wrapper.js +25 -8
- package/dist/logging-wrapper.js.map +1 -1
- package/package.json +38 -34
- package/src/fastify-logging-wrapper.ts +91 -8
- package/src/field-filter.ts +46 -0
- package/src/index.ts +7 -6
- package/src/logging-wrapper-entities.ts +11 -5
- package/src/logging-wrapper.ts +34 -9
- package/tsconfig.json +9 -11
- package/tsconfig.prod.tsbuildinfo +1 -0
- package/vitest.config.mts +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [5.4.0](https://github.com/ogcio/shared-node-utils/compare/@ogcio/fastify-logging-wrapper@v5.3.0...@ogcio/fastify-logging-wrapper@v5.4.0) (2026-07-14)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* AB[#36812](https://github.com/ogcio/shared-node-utils/issues/36812) pii-hash fastify logging wrapper ([#188](https://github.com/ogcio/shared-node-utils/issues/188)) ([e58f050](https://github.com/ogcio/shared-node-utils/commit/e58f050cda8931859eef9a2f738727f56534dc8f))
|
|
12
|
+
* AB[#36812](https://github.com/ogcio/shared-node-utils/issues/36812) pii-utils - fastify error handler ([#199](https://github.com/ogcio/shared-node-utils/issues/199)) ([ab0a2ad](https://github.com/ogcio/shared-node-utils/commit/ab0a2ad6e7c3470f4d7fd4e7e98928e56b71e4fb))
|
|
13
|
+
* **fastify-logging-wrapper:** add customConfig for initialize new pino instance ([#8](https://github.com/ogcio/shared-node-utils/issues/8)) ([847f075](https://github.com/ogcio/shared-node-utils/commit/847f075c3c3b873a844e413085ca97c0f6e1f1a7))
|
|
14
|
+
* **fastify-logging-wrapper:** log properties filter AB[#40055](https://github.com/ogcio/shared-node-utils/issues/40055) ([#286](https://github.com/ogcio/shared-node-utils/issues/286)) ([fe2c1dd](https://github.com/ogcio/shared-node-utils/commit/fe2c1dd992ee7fff47497cde7d59b8aba3111ee0))
|
|
15
|
+
* **root:** startup repo ([7422a6c](https://github.com/ogcio/shared-node-utils/commit/7422a6c8a7d51722299e6cd61eebacefe2b80d6d))
|
|
16
|
+
* updated deps across packages ([#129](https://github.com/ogcio/shared-node-utils/issues/129)) ([86d83ca](https://github.com/ogcio/shared-node-utils/commit/86d83ca57c614fd1a2612862b7e946a0a0d23576))
|
|
17
|
+
* **v5:** add api auth package ([#19](https://github.com/ogcio/shared-node-utils/issues/19)) ([cbc4b83](https://github.com/ogcio/shared-node-utils/commit/cbc4b831ceb29e926f0a7dd869039f0a7575fc69))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* log request in pre parsing ([#118](https://github.com/ogcio/shared-node-utils/issues/118)) ([c7f6d42](https://github.com/ogcio/shared-node-utils/commit/c7f6d42fe141b7f9bad4de6ae96d171dbd34284c))
|
|
23
|
+
* prepublish with pnpm ([#255](https://github.com/ogcio/shared-node-utils/issues/255)) ([d0746d4](https://github.com/ogcio/shared-node-utils/commit/d0746d43d7c359e06dee6d33a3898e27d75e896e))
|
|
24
|
+
* publish shared errors for version 5 ([#4](https://github.com/ogcio/shared-node-utils/issues/4)) ([7159dcd](https://github.com/ogcio/shared-node-utils/commit/7159dcdd2564a730bacbf3ff300aa129debbfe3a))
|
|
25
|
+
* skip internal objects serialize if empty AB[#26429](https://github.com/ogcio/shared-node-utils/issues/26429) ([#75](https://github.com/ogcio/shared-node-utils/issues/75)) ([b667b9d](https://github.com/ogcio/shared-node-utils/commit/b667b9ddd5c4f4b0f937ad8e11e171f81af8bba4))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Miscellaneous Chores
|
|
29
|
+
|
|
30
|
+
* bump patch release for fastify loggin wrapper ([#78](https://github.com/ogcio/shared-node-utils/issues/78)) ([8fae0da](https://github.com/ogcio/shared-node-utils/commit/8fae0dab725a9e05581b84945249668580a667e9))
|
|
31
|
+
* dependabot v5 ([#17](https://github.com/ogcio/shared-node-utils/issues/17)) ([89f9630](https://github.com/ogcio/shared-node-utils/commit/89f96309bed5637de99de5f8beeed275e74f2ef7))
|
|
32
|
+
* setup pnpm workspace ([#242](https://github.com/ogcio/shared-node-utils/issues/242)) ([a75f672](https://github.com/ogcio/shared-node-utils/commit/a75f6728bbf2f44a00f033b0ada12058756f06dd))
|
|
33
|
+
* update deps to 5.1.0 ([#14](https://github.com/ogcio/shared-node-utils/issues/14)) ([6b7821b](https://github.com/ogcio/shared-node-utils/commit/6b7821ba037ae00db96c2303b5911563deef029c))
|
|
34
|
+
|
|
6
35
|
## 5.0.2 (2024-10-03)
|
|
7
36
|
|
|
8
37
|
|
package/README.md
CHANGED
|
@@ -60,6 +60,38 @@ If an error is thrown, a log entry is automatically written.
|
|
|
60
60
|
{"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"}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Filtering logged fields
|
|
64
|
+
|
|
65
|
+
By default the wrapper logs the full request and response, so headers, query
|
|
66
|
+
params, cookies and similar fields end up in the logs. A built-in list always
|
|
67
|
+
masks the most sensitive headers (`authorization`, `cookie`, `set-cookie`,
|
|
68
|
+
`x-api-key`, `x-amz-security-token`, `proxy-authorization`).
|
|
69
|
+
|
|
70
|
+
To avoid logging additional sensitive or PII fields, pass an optional
|
|
71
|
+
configuration to `initializeLoggingHooks`. Rules are **additive**: they run on
|
|
72
|
+
top of the built-in redaction, they never disable it. Omitting the config keeps
|
|
73
|
+
the current behaviour unchanged.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
initializeLoggingHooks(server, {
|
|
77
|
+
request: {
|
|
78
|
+
headers: { omit: ["x-internal-trace"], mask: ["x-custom-token"] },
|
|
79
|
+
queryParams: { omit: ["email"], mask: ["ppsn"] },
|
|
80
|
+
clientIp: "omit",
|
|
81
|
+
userAgent: "omit",
|
|
82
|
+
},
|
|
83
|
+
response: {
|
|
84
|
+
headers: { omit: ["x-debug-info"] },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `omit` removes the field entirely from the log entry.
|
|
90
|
+
- `mask` keeps the field but replaces its value with `[redacted]`.
|
|
91
|
+
- Header names are matched **case-insensitively**; query-param names are matched
|
|
92
|
+
**exactly**.
|
|
93
|
+
- `clientIp` and `userAgent` are single values, so they only accept `"omit"`.
|
|
94
|
+
|
|
63
95
|
## Additional entries
|
|
64
96
|
|
|
65
97
|
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.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { httpErrors } from "@fastify/sensible";
|
|
2
|
-
import {
|
|
2
|
+
import { afterEach, assert, describe, it } from "vitest";
|
|
3
3
|
import { LogErrorClasses } from "../src/logging-wrapper-entities.js";
|
|
4
4
|
import {
|
|
5
|
-
DEFAULT_METHOD,
|
|
6
5
|
checkExpectedApiTrackEntry,
|
|
7
6
|
checkExpectedErrorEntry,
|
|
8
7
|
checkExpectedRequestEntry,
|
|
9
8
|
checkExpectedResponseEntry,
|
|
9
|
+
DEFAULT_METHOD,
|
|
10
10
|
initializeServer,
|
|
11
11
|
parseLogEntry,
|
|
12
12
|
runErrorTest,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { REQUEST_ID_HEADER } from "@ogcio/shared-errors";
|
|
2
|
-
import {
|
|
2
|
+
import { afterEach, assert, describe, it } from "vitest";
|
|
3
3
|
import { LogMessages } from "../src/logging-wrapper-entities.js";
|
|
4
4
|
import {
|
|
5
|
-
DEFAULT_METHOD,
|
|
6
|
-
DEFAULT_PATH,
|
|
7
5
|
checkExpectedRequestEntry,
|
|
8
6
|
checkExpectedResponseEntry,
|
|
9
7
|
checkGenericEntryFields,
|
|
8
|
+
DEFAULT_METHOD,
|
|
9
|
+
DEFAULT_PATH,
|
|
10
10
|
initializeServer,
|
|
11
11
|
parseLogEntry,
|
|
12
12
|
} from "./helpers/fastify-test-helpers.js";
|
|
@@ -121,3 +121,85 @@ describe("Additional logs are correctly written", () => {
|
|
|
121
121
|
assert.ok(typeof parsedAdditional.request !== "undefined");
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
describe("PseudoUser is included in logging context", () => {
|
|
126
|
+
it("should include pseudoUser in response and apiTrack entries", async () => {
|
|
127
|
+
const { server, loggingDestination } = initializeServer();
|
|
128
|
+
afterEach(() => server.close());
|
|
129
|
+
|
|
130
|
+
const response = await server.inject({
|
|
131
|
+
method: DEFAULT_METHOD,
|
|
132
|
+
url: "/with-pseudo-user",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.ok(typeof response !== "undefined");
|
|
136
|
+
assert.equal(response?.statusCode, 200);
|
|
137
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
138
|
+
assert.equal(loggedRecords.length, 3);
|
|
139
|
+
|
|
140
|
+
// Request entry should NOT have pseudoUser (not yet set)
|
|
141
|
+
const requestEntry = parseLogEntry(loggedRecords[0]);
|
|
142
|
+
assert.ok(typeof requestEntry.pseudoUser === "undefined");
|
|
143
|
+
|
|
144
|
+
// Response entry should have pseudoUser
|
|
145
|
+
const responseEntry = parseLogEntry(loggedRecords[1]);
|
|
146
|
+
assert.ok(typeof responseEntry.pseudoUser !== "undefined");
|
|
147
|
+
assert.equal(responseEntry.pseudoUser.id, "pseudo-user-id");
|
|
148
|
+
assert.equal(responseEntry.pseudoUser.version, "v1");
|
|
149
|
+
|
|
150
|
+
// ApiTrack entry should have pseudoUser
|
|
151
|
+
const apiTrackEntry = parseLogEntry(loggedRecords[2]);
|
|
152
|
+
assert.ok(typeof apiTrackEntry.pseudoUser !== "undefined");
|
|
153
|
+
assert.equal(apiTrackEntry.pseudoUser.id, "pseudo-user-id");
|
|
154
|
+
assert.equal(apiTrackEntry.pseudoUser.version, "v1");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should not include pseudoUser when userData has invalid shape", async () => {
|
|
158
|
+
const { server, loggingDestination } = initializeServer();
|
|
159
|
+
afterEach(() => server.close());
|
|
160
|
+
|
|
161
|
+
const response = await server.inject({
|
|
162
|
+
method: DEFAULT_METHOD,
|
|
163
|
+
url: "/with-invalid-pseudo-user",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assert.ok(typeof response !== "undefined");
|
|
167
|
+
assert.equal(response?.statusCode, 200);
|
|
168
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
169
|
+
assert.equal(loggedRecords.length, 3);
|
|
170
|
+
|
|
171
|
+
// pseudoUser should not be set when shape is invalid
|
|
172
|
+
const responseEntry = parseLogEntry(loggedRecords[1]);
|
|
173
|
+
assert.ok(typeof responseEntry.pseudoUser === "undefined");
|
|
174
|
+
|
|
175
|
+
const apiTrackEntry = parseLogEntry(loggedRecords[2]);
|
|
176
|
+
assert.ok(typeof apiTrackEntry.pseudoUser === "undefined");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should reset pseudoUser between requests", async () => {
|
|
180
|
+
const { server, loggingDestination } = initializeServer();
|
|
181
|
+
afterEach(() => server.close());
|
|
182
|
+
|
|
183
|
+
// First request with pseudoUser
|
|
184
|
+
await server.inject({
|
|
185
|
+
method: DEFAULT_METHOD,
|
|
186
|
+
url: "/with-pseudo-user",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Second request without pseudoUser
|
|
190
|
+
await server.inject({
|
|
191
|
+
method: DEFAULT_METHOD,
|
|
192
|
+
url: DEFAULT_PATH,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const loggedRecords = loggingDestination.getLoggedRecords();
|
|
196
|
+
assert.equal(loggedRecords.length, 6);
|
|
197
|
+
|
|
198
|
+
// Second request's response should NOT have pseudoUser
|
|
199
|
+
const secondResponseEntry = parseLogEntry(loggedRecords[4]);
|
|
200
|
+
assert.ok(typeof secondResponseEntry.pseudoUser === "undefined");
|
|
201
|
+
|
|
202
|
+
const secondApiTrackEntry = parseLogEntry(loggedRecords[5]);
|
|
203
|
+
assert.ok(typeof secondApiTrackEntry.pseudoUser === "undefined");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { assert, describe, it } from "vitest";
|
|
2
|
+
import { applyFieldRules } from "../src/field-filter.js";
|
|
3
|
+
import { REDACTED_VALUE } from "../src/logging-wrapper-entities.js";
|
|
4
|
+
|
|
5
|
+
describe("applyFieldRules", () => {
|
|
6
|
+
it("returns the value untouched when no rules are given", () => {
|
|
7
|
+
const value = { a: 1, b: 2 };
|
|
8
|
+
assert.strictEqual(applyFieldRules(value, undefined), value);
|
|
9
|
+
assert.strictEqual(applyFieldRules(value, {}), value);
|
|
10
|
+
assert.strictEqual(applyFieldRules(value, { omit: [], mask: [] }), value);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("omits the requested keys", () => {
|
|
14
|
+
const result = applyFieldRules({ a: 1, b: 2, c: 3 }, { omit: ["b"] });
|
|
15
|
+
assert.deepStrictEqual(result, { a: 1, c: 3 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("masks the requested keys", () => {
|
|
19
|
+
const result = applyFieldRules({ a: 1, b: 2 }, { mask: ["b"] });
|
|
20
|
+
assert.deepStrictEqual(result, { a: 1, b: REDACTED_VALUE });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("omit takes precedence over mask for the same key", () => {
|
|
24
|
+
const result = applyFieldRules(
|
|
25
|
+
{ a: 1, b: 2 },
|
|
26
|
+
{
|
|
27
|
+
omit: ["b"],
|
|
28
|
+
mask: ["b"],
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
assert.deepStrictEqual(result, { a: 1 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("matches keys case-insensitively when requested", () => {
|
|
35
|
+
const result = applyFieldRules(
|
|
36
|
+
{ "x-custom-token": "secret", "content-type": "json" },
|
|
37
|
+
{ mask: ["X-Custom-Token"] },
|
|
38
|
+
true,
|
|
39
|
+
);
|
|
40
|
+
assert.deepStrictEqual(result, {
|
|
41
|
+
"x-custom-token": REDACTED_VALUE,
|
|
42
|
+
"content-type": "json",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("matches keys case-sensitively by default", () => {
|
|
47
|
+
const result = applyFieldRules({ Email: "a@b.com" }, { omit: ["email"] });
|
|
48
|
+
assert.deepStrictEqual(result, { Email: "a@b.com" });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("passes through non-object values", () => {
|
|
52
|
+
assert.strictEqual(applyFieldRules("plain", { omit: ["a"] }), "plain");
|
|
53
|
+
assert.strictEqual(applyFieldRules(null, { omit: ["a"] }), null);
|
|
54
|
+
const arr = [1, 2];
|
|
55
|
+
assert.strictEqual(applyFieldRules(arr, { omit: ["a"] }), arr);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -6,15 +6,19 @@ import {
|
|
|
6
6
|
getLoggingConfiguration,
|
|
7
7
|
initializeLoggingHooks,
|
|
8
8
|
} from "../../src/fastify-logging-wrapper.js";
|
|
9
|
+
import type { LoggingOptions } from "../../src/field-filter.js";
|
|
9
10
|
|
|
10
|
-
export const buildFastify = (
|
|
11
|
+
export const buildFastify = (
|
|
12
|
+
loggerDestination?: DestinationStream,
|
|
13
|
+
options?: LoggingOptions,
|
|
14
|
+
) => {
|
|
11
15
|
const server = Fastify({
|
|
12
16
|
...getLoggingConfiguration({
|
|
13
17
|
loggerDestination: loggerDestination,
|
|
14
18
|
}),
|
|
15
19
|
});
|
|
16
20
|
|
|
17
|
-
initializeLoggingHooks(server);
|
|
21
|
+
initializeLoggingHooks(server, options);
|
|
18
22
|
|
|
19
23
|
server.get("/ping", async (_request, _reply) => {
|
|
20
24
|
return { data: "pong\n" };
|
|
@@ -52,5 +56,19 @@ export const buildFastify = (loggerDestination?: DestinationStream) => {
|
|
|
52
56
|
return { data: { message: logMessage } };
|
|
53
57
|
});
|
|
54
58
|
|
|
59
|
+
server.get("/with-pseudo-user", async (request, _reply) => {
|
|
60
|
+
(request as unknown as { userData: unknown }).userData = {
|
|
61
|
+
pseudoUser: { id: "pseudo-user-id", version: "v1" },
|
|
62
|
+
};
|
|
63
|
+
return { data: "ok" };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.get("/with-invalid-pseudo-user", async (request, _reply) => {
|
|
67
|
+
(request as unknown as { userData: unknown }).userData = {
|
|
68
|
+
pseudoUser: { id: 123 },
|
|
69
|
+
};
|
|
70
|
+
return { data: "ok" };
|
|
71
|
+
});
|
|
72
|
+
|
|
55
73
|
return server;
|
|
56
74
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import { assert } from "vitest";
|
|
3
|
+
import type { LoggingOptions } from "../../src/field-filter.js";
|
|
3
4
|
import {
|
|
4
5
|
type LogErrorClasses,
|
|
5
6
|
LogMessages,
|
|
6
7
|
} from "../../src/logging-wrapper-entities.js";
|
|
7
8
|
import { buildFastify } from "./build-fastify.js";
|
|
8
9
|
import {
|
|
9
|
-
type TestingLoggerDestination,
|
|
10
10
|
getTestingDestinationLogger,
|
|
11
|
+
type TestingLoggerDestination,
|
|
11
12
|
} from "./build-logger.js";
|
|
12
13
|
|
|
13
14
|
export const DEFAULT_HOSTNAME = "localhost";
|
|
@@ -23,18 +24,19 @@ export const DEFAULT_METHOD = "GET";
|
|
|
23
24
|
export const DEFAULT_SCHEME = "http";
|
|
24
25
|
export const DEFAULT_PATH = "/ping";
|
|
25
26
|
|
|
26
|
-
export const initializeServer = (
|
|
27
|
+
export const initializeServer = (
|
|
28
|
+
options?: LoggingOptions,
|
|
29
|
+
): {
|
|
27
30
|
server: FastifyInstance;
|
|
28
31
|
loggingDestination: TestingLoggerDestination;
|
|
29
32
|
} => {
|
|
30
33
|
const loggingDestination = getTestingDestinationLogger();
|
|
31
|
-
const server = buildFastify(loggingDestination.loggerDestination);
|
|
34
|
+
const server = buildFastify(loggingDestination.loggerDestination, options);
|
|
32
35
|
|
|
33
36
|
return { server, loggingDestination };
|
|
34
37
|
};
|
|
35
38
|
|
|
36
|
-
// biome-ignore lint/suspicious/noExplicitAny:
|
|
37
|
-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
39
|
+
// biome-ignore lint/suspicious/noExplicitAny: Needed for testing purposes
|
|
38
40
|
export const parseLogEntry = (logEntry: string): { [x: string]: any } =>
|
|
39
41
|
JSON.parse(logEntry);
|
|
40
42
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, assert, describe, it } from "vitest";
|
|
2
|
+
import { REDACTED_VALUE } from "../src/logging-wrapper-entities.js";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_METHOD,
|
|
5
|
+
initializeServer,
|
|
6
|
+
parseLogEntry,
|
|
7
|
+
} from "./helpers/fastify-test-helpers.js";
|
|
8
|
+
|
|
9
|
+
describe("Logging options filter request and response fields", () => {
|
|
10
|
+
it("omits and masks request headers (case-insensitive)", async () => {
|
|
11
|
+
const { server, loggingDestination } = initializeServer({
|
|
12
|
+
request: {
|
|
13
|
+
headers: { omit: ["x-drop-me"], mask: ["X-Secret"] },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => server.close());
|
|
17
|
+
|
|
18
|
+
await server.inject({
|
|
19
|
+
method: DEFAULT_METHOD,
|
|
20
|
+
url: "/ping",
|
|
21
|
+
headers: { "x-drop-me": "gone", "x-secret": "sensitive", "x-keep": "ok" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const request = parseLogEntry(
|
|
25
|
+
loggingDestination.getLoggedRecords()[0],
|
|
26
|
+
).request;
|
|
27
|
+
assert.notProperty(request.headers, "x-drop-me");
|
|
28
|
+
assert.equal(request.headers["x-secret"], REDACTED_VALUE);
|
|
29
|
+
assert.equal(request.headers["x-keep"], "ok");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("omits and masks query params (case-sensitive)", async () => {
|
|
33
|
+
const { server, loggingDestination } = initializeServer({
|
|
34
|
+
request: {
|
|
35
|
+
queryParams: { omit: ["email"], mask: ["ppsn"] },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => server.close());
|
|
39
|
+
|
|
40
|
+
await server.inject({
|
|
41
|
+
method: DEFAULT_METHOD,
|
|
42
|
+
url: "/ping?email=a@b.com&ppsn=123&keep=1",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const request = parseLogEntry(
|
|
46
|
+
loggingDestination.getLoggedRecords()[0],
|
|
47
|
+
).request;
|
|
48
|
+
assert.notProperty(request.query_params, "email");
|
|
49
|
+
assert.equal(request.query_params.ppsn, REDACTED_VALUE);
|
|
50
|
+
assert.equal(request.query_params.keep, "1");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("omits derived client_ip and user_agent fields", async () => {
|
|
54
|
+
const { server, loggingDestination } = initializeServer({
|
|
55
|
+
request: { clientIp: "omit", userAgent: "omit" },
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => server.close());
|
|
58
|
+
|
|
59
|
+
await server.inject({ method: DEFAULT_METHOD, url: "/ping" });
|
|
60
|
+
|
|
61
|
+
const request = parseLogEntry(
|
|
62
|
+
loggingDestination.getLoggedRecords()[0],
|
|
63
|
+
).request;
|
|
64
|
+
assert.notProperty(request, "client_ip");
|
|
65
|
+
assert.notProperty(request, "user_agent");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("masks response headers", async () => {
|
|
69
|
+
const { server, loggingDestination } = initializeServer({
|
|
70
|
+
response: { headers: { mask: ["content-type"] } },
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => server.close());
|
|
73
|
+
|
|
74
|
+
await server.inject({ method: DEFAULT_METHOD, url: "/ping" });
|
|
75
|
+
|
|
76
|
+
const response = parseLogEntry(
|
|
77
|
+
loggingDestination.getLoggedRecords()[1],
|
|
78
|
+
).response;
|
|
79
|
+
assert.equal(response.headers["content-type"], REDACTED_VALUE);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("keeps the default behaviour when no options are given", async () => {
|
|
83
|
+
const { server, loggingDestination } = initializeServer();
|
|
84
|
+
afterEach(() => server.close());
|
|
85
|
+
|
|
86
|
+
await server.inject({
|
|
87
|
+
method: DEFAULT_METHOD,
|
|
88
|
+
url: "/ping",
|
|
89
|
+
headers: { "x-anything": "kept" },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const request = parseLogEntry(
|
|
93
|
+
loggingDestination.getLoggedRecords()[0],
|
|
94
|
+
).request;
|
|
95
|
+
assert.equal(request.headers["x-anything"], "kept");
|
|
96
|
+
assert.property(request, "client_ip");
|
|
97
|
+
assert.property(request, "user_agent");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { LogLevel } from "fastify";
|
|
1
2
|
import { hostname } from "os";
|
|
2
3
|
import { assert, describe, it } from "vitest";
|
|
3
|
-
import { REDACTED_VALUE } from "../src/logging-wrapper-entities.js";
|
|
4
4
|
import { getLoggerConfiguration } from "../src/logging-wrapper.js";
|
|
5
|
+
import { REDACTED_VALUE } from "../src/logging-wrapper-entities.js";
|
|
5
6
|
import { buildLogger } from "./helpers/build-logger.js";
|
|
6
7
|
|
|
7
8
|
const getRandomFieldValue = () => Math.random().toString(36).slice(2);
|
|
@@ -59,7 +60,10 @@ const toRedactFields = {
|
|
|
59
60
|
},
|
|
60
61
|
};
|
|
61
62
|
|
|
62
|
-
const methodsDataProvider
|
|
63
|
+
const methodsDataProvider: {
|
|
64
|
+
method: LogLevel;
|
|
65
|
+
expected: { level: number; level_name: string };
|
|
66
|
+
}[] = [
|
|
63
67
|
{
|
|
64
68
|
method: "trace",
|
|
65
69
|
expected: {
|
|
@@ -121,7 +125,7 @@ describe("Basic format is the expected one", () => {
|
|
|
121
125
|
parsed.timestamp > Date.now() - 2000,
|
|
122
126
|
"the timestamp must be newer than 2 seconds ago",
|
|
123
127
|
);
|
|
124
|
-
|
|
128
|
+
|
|
125
129
|
delete parsed.timestamp;
|
|
126
130
|
assert.deepStrictEqual(parsed, {
|
|
127
131
|
level: 20,
|
|
@@ -141,13 +145,10 @@ describe("Fields are redacted as expected", () => {
|
|
|
141
145
|
|
|
142
146
|
const loggedRecords = loggedRecordsMethod();
|
|
143
147
|
const parsed = JSON.parse(loggedRecords[0]);
|
|
144
|
-
|
|
148
|
+
|
|
145
149
|
delete parsed.hostname;
|
|
146
|
-
// biome-ignore lint/performance/noDelete: Would change behaviour of the test
|
|
147
150
|
delete parsed.level;
|
|
148
|
-
// biome-ignore lint/performance/noDelete: Would change behaviour of the test
|
|
149
151
|
delete parsed.level_name;
|
|
150
|
-
// biome-ignore lint/performance/noDelete: Would change behaviour of the test
|
|
151
152
|
delete parsed.timestamp;
|
|
152
153
|
|
|
153
154
|
assert.deepStrictEqual(parsed, toRedactFields.expected_output);
|