@kids-reporter/logger 0.0.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/README.md +64 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +129 -0
- package/dist/utils.js.map +1 -0
- package/eslint.config.mjs +22 -0
- package/package.json +26 -0
- package/src/index.ts +2 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +180 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @kids-reporter/logger
|
|
2
|
+
|
|
3
|
+
Shared structured logger and trace utilities for Kids Reporter services. Uses Google Cloud `X-Cloud-Trace-Context` to propagate a single `traceId` so that logs can be correlated in GCP Cloud Logging via `logging.googleapis.com/trace`.
|
|
4
|
+
|
|
5
|
+
## Types
|
|
6
|
+
|
|
7
|
+
- **`LogSeverity`** — `'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'ALERT' | 'CRITICAL'`
|
|
8
|
+
- **`StructuredLogPayload`** — `{ severity: LogSeverity; message?: string } & Record<string, unknown>`
|
|
9
|
+
- **`TraceHeaderInput`** — `Headers | Record<string, string | undefined | unknown> | undefined`
|
|
10
|
+
- **`NormalizedTraceContext`** — `{ traceId: string; traceHeaders: { 'X-Cloud-Trace-Context': string } }`
|
|
11
|
+
|
|
12
|
+
## API
|
|
13
|
+
|
|
14
|
+
### `normalizeTraceContext(headersInput?, options?)`
|
|
15
|
+
|
|
16
|
+
Parses trace context from the `X-Cloud-Trace-Context` header (GCP format). Optionally generates a new trace ID when the header is missing.
|
|
17
|
+
|
|
18
|
+
- **`headersInput`** — Request headers (e.g. `Headers` or plain object).
|
|
19
|
+
- **`options.generateIfMissing`** — If `true` (default), creates a new trace ID when the header is missing; if `false`, returns `undefined` in that case.
|
|
20
|
+
|
|
21
|
+
**Returns:** `NormalizedTraceContext` or `undefined`.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { normalizeTraceContext } from '@kids-reporter/logger'
|
|
25
|
+
|
|
26
|
+
const ctx = normalizeTraceContext(request.headers)
|
|
27
|
+
// ctx.traceId, ctx.traceHeaders['X-Cloud-Trace-Context']
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `getTraceLogFields(headersInput?, options?)`
|
|
31
|
+
|
|
32
|
+
Builds an object of trace-related fields for structured logging. Includes `logging.googleapis.com/trace` when `projectId` is set for GCP log correlation.
|
|
33
|
+
|
|
34
|
+
- **`headersInput`** — Same as `normalizeTraceContext`.
|
|
35
|
+
- **`options.projectId`** — GCP project ID for the trace field; falls back to `GOOGLE_CLOUD_PROJECT` or `GCP_PROJECT` env vars.
|
|
36
|
+
- **`options.generateIfMissing`** — Same as `normalizeTraceContext` (default `false` here).
|
|
37
|
+
|
|
38
|
+
**Returns:** Object with `traceId` and optionally `logging.googleapis.com/trace`.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { getTraceLogFields } from '@kids-reporter/logger'
|
|
42
|
+
|
|
43
|
+
const fields = getTraceLogFields(req.headers, { projectId: 'my-gcp-project' })
|
|
44
|
+
console.log(JSON.stringify({ ...fields, message: 'Request processed' }))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `getGcpTraceField({ projectId, traceId })`
|
|
48
|
+
|
|
49
|
+
Returns the GCP trace resource name: `projects/{projectId}/traces/{traceId}`. Returns `undefined` if `projectId` or `traceId` is missing.
|
|
50
|
+
|
|
51
|
+
### `emitStructured(payload)`
|
|
52
|
+
|
|
53
|
+
Writes a structured log entry to the console as JSON. Uses `console.error` for ERROR/ALERT/CRITICAL, `console.warn` for WARNING, and `console.log` for others.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { emitStructured } from '@kids-reporter/logger'
|
|
57
|
+
|
|
58
|
+
emitStructured({
|
|
59
|
+
severity: 'INFO',
|
|
60
|
+
message: 'User signed in',
|
|
61
|
+
userId: 'usr_123',
|
|
62
|
+
...getTraceLogFields(request.headers),
|
|
63
|
+
})
|
|
64
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type LogSeverity = 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'ALERT' | 'CRITICAL';
|
|
2
|
+
export type StructuredLogPayload = {
|
|
3
|
+
severity: LogSeverity;
|
|
4
|
+
message?: string;
|
|
5
|
+
} & Record<string, unknown>;
|
|
6
|
+
/** Supports Express req.headers where values can be string | string[]. */
|
|
7
|
+
export type TraceHeaderInput = Headers | Record<string, string | string[] | undefined | unknown> | undefined;
|
|
8
|
+
/** Trace context for GCP Cloud Logging correlation. Only traceId is propagated. */
|
|
9
|
+
export type NormalizedTraceContext = {
|
|
10
|
+
traceId: string;
|
|
11
|
+
traceHeaders: {
|
|
12
|
+
'X-Cloud-Trace-Context': string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,MAAM,GACN,QAAQ,GACR,SAAS,GACT,OAAO,GACP,OAAO,GACP,UAAU,CAAA;AAEd,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,WAAW,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE3B,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GACxB,OAAO,GACP,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAAC,GACvD,SAAS,CAAA;AAEb,mFAAmF;AACnF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE;QACZ,uBAAuB,EAAE,MAAM,CAAA;KAChC,CAAA;CACF,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NormalizedTraceContext, StructuredLogPayload, TraceHeaderInput } from './types.js';
|
|
2
|
+
export declare function normalizeTraceContext(headersInput: TraceHeaderInput | undefined, options: {
|
|
3
|
+
generateIfMissing: true;
|
|
4
|
+
}): NormalizedTraceContext;
|
|
5
|
+
export declare function normalizeTraceContext(headersInput?: TraceHeaderInput, options?: {
|
|
6
|
+
generateIfMissing?: boolean;
|
|
7
|
+
}): NormalizedTraceContext | undefined;
|
|
8
|
+
export declare function getGcpTraceField({ projectId, traceId, }: {
|
|
9
|
+
projectId?: string;
|
|
10
|
+
traceId: string;
|
|
11
|
+
}): string | undefined;
|
|
12
|
+
export declare function getTraceLogFields(headersInput?: TraceHeaderInput, options?: {
|
|
13
|
+
projectId?: string;
|
|
14
|
+
generateIfMissing?: boolean;
|
|
15
|
+
}): {};
|
|
16
|
+
export declare function emitStructured(payload: StructuredLogPayload): void;
|
|
17
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,YAAY,CAAA;AAuFnB,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,gBAAgB,GAAG,SAAS,EAC1C,OAAO,EAAE;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,GACnC,sBAAsB,CAAA;AACzB,wBAAgB,qBAAqB,CACnC,YAAY,CAAC,EAAE,gBAAgB,EAC/B,OAAO,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACxC,sBAAsB,GAAG,SAAS,CAAA;AAyBrC,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;CAChB,sBAKA;AAED,wBAAgB,iBAAiB,CAC/B,YAAY,CAAC,EAAE,gBAAgB,EAC/B,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAO,MAsBlE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,oBAAoB,QAiB3D"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const XCLOUD_TRACE_HEADER = 'x-cloud-trace-context';
|
|
2
|
+
const TRACE_ID_REGEX = /^[a-f0-9]{32}$/i;
|
|
3
|
+
const CONSOLE_BY_SEVERITY = {
|
|
4
|
+
ALERT: 'error',
|
|
5
|
+
CRITICAL: 'error',
|
|
6
|
+
ERROR: 'error',
|
|
7
|
+
WARNING: 'warn',
|
|
8
|
+
NOTICE: 'log',
|
|
9
|
+
INFO: 'log',
|
|
10
|
+
DEBUG: 'log',
|
|
11
|
+
};
|
|
12
|
+
function normalizeHeaderValue(value) {
|
|
13
|
+
if (value === undefined || value === null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value) &&
|
|
20
|
+
value.length > 0 &&
|
|
21
|
+
typeof value[0] === 'string') {
|
|
22
|
+
return value[0];
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function getHeaderValue(input, headerName) {
|
|
27
|
+
if (!input) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (typeof Headers !== 'undefined' && input instanceof Headers) {
|
|
31
|
+
return input.get(headerName) || undefined;
|
|
32
|
+
}
|
|
33
|
+
const lowered = headerName.toLowerCase();
|
|
34
|
+
for (const [key, value] of Object.entries(input)) {
|
|
35
|
+
if (key.toLowerCase() === lowered) {
|
|
36
|
+
return normalizeHeaderValue(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function getRandomHex(length) {
|
|
42
|
+
const bytes = new Uint8Array(Math.ceil(length / 2));
|
|
43
|
+
if (globalThis.crypto &&
|
|
44
|
+
typeof globalThis.crypto.getRandomValues === 'function') {
|
|
45
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
49
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Array.from(bytes, (value) => value.toString(16).padStart(2, '0'))
|
|
53
|
+
.join('')
|
|
54
|
+
.slice(0, length);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parses traceId from GCP X-Cloud-Trace-Context.
|
|
58
|
+
* Accepts "traceId;o=1" or "traceId/spanId;o=1" (spanId is ignored).
|
|
59
|
+
*/
|
|
60
|
+
function parseXCloudTraceContext(xCloudTraceContext) {
|
|
61
|
+
if (!xCloudTraceContext) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const [traceAndSpan] = xCloudTraceContext.trim().split(';');
|
|
65
|
+
const traceId = traceAndSpan.split('/')[0]?.trim();
|
|
66
|
+
if (!traceId || !TRACE_ID_REGEX.test(traceId)) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return traceId.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
export function normalizeTraceContext(headersInput, options = {}) {
|
|
72
|
+
/* eslint-enable no-redeclare */
|
|
73
|
+
const shouldGenerate = options.generateIfMissing !== false;
|
|
74
|
+
const xCloudTraceContext = getHeaderValue(headersInput, XCLOUD_TRACE_HEADER);
|
|
75
|
+
const traceId = parseXCloudTraceContext(xCloudTraceContext);
|
|
76
|
+
if (!traceId && !shouldGenerate) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const resolvedTraceId = traceId ?? getRandomHex(32);
|
|
80
|
+
const xCloudValue = `${resolvedTraceId};o=1`;
|
|
81
|
+
return {
|
|
82
|
+
traceId: resolvedTraceId,
|
|
83
|
+
traceHeaders: {
|
|
84
|
+
'X-Cloud-Trace-Context': xCloudValue,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function getGcpTraceField({ projectId, traceId, }) {
|
|
89
|
+
if (!projectId || !traceId) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return `projects/${projectId}/traces/${traceId}`;
|
|
93
|
+
}
|
|
94
|
+
export function getTraceLogFields(headersInput, options = {}) {
|
|
95
|
+
const traceContext = normalizeTraceContext(headersInput, {
|
|
96
|
+
generateIfMissing: options.generateIfMissing ?? false,
|
|
97
|
+
});
|
|
98
|
+
if (!traceContext) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
const projectId = options.projectId ||
|
|
102
|
+
process.env.GOOGLE_CLOUD_PROJECT ||
|
|
103
|
+
process.env.GCP_PROJECT;
|
|
104
|
+
const traceField = getGcpTraceField({
|
|
105
|
+
projectId,
|
|
106
|
+
traceId: traceContext.traceId,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
...(traceField ? { 'logging.googleapis.com/trace': traceField } : {}),
|
|
110
|
+
traceId: traceContext.traceId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function emitStructured(payload) {
|
|
114
|
+
const severity = typeof payload?.severity === 'string'
|
|
115
|
+
? payload.severity.toUpperCase()
|
|
116
|
+
: 'INFO';
|
|
117
|
+
const method = CONSOLE_BY_SEVERITY[severity] || 'log';
|
|
118
|
+
const message = JSON.stringify(payload);
|
|
119
|
+
if (method === 'error') {
|
|
120
|
+
console.error(message);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (method === 'warn') {
|
|
124
|
+
console.warn(message);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
console.log(message);
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA,MAAM,mBAAmB,GAAG,uBAAuB,CAAA;AACnD,MAAM,cAAc,GAAG,iBAAiB,CAAA;AAExC,MAAM,mBAAmB,GAA6C;IACpE,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,OAAO;IACjB,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,MAAM;IACf,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,KAAK;IACX,KAAK,EAAE,KAAK;CACb,CAAA;AAED,SAAS,oBAAoB,CAC3B,KAA8C;IAE9C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IACE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACpB,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAC5B,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,cAAc,CAAC,KAAuB,EAAE,UAAkB;IACjE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,KAAK,YAAY,OAAO,EAAE,CAAC;QAC/D,OAAO,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,SAAS,CAAA;IAC3C,CAAC;IACD,MAAM,OAAO,GAAG,UAAU,CAAC,WAAW,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,OAAO,EAAE,CAAC;YAClC,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAA;QACpC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,YAAY,CAAC,MAAc;IAClC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;IACnD,IACE,UAAU,CAAC,MAAM;QACjB,OAAO,UAAU,CAAC,MAAM,CAAC,eAAe,KAAK,UAAU,EACvD,CAAC;QACD,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC1C,CAAC;SAAM,CAAC;QACN,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACrE,IAAI,CAAC,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;AACrB,CAAC;AAED;;;GAGG;AACH,SAAS,uBAAuB,CAC9B,kBAA2B;IAE3B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,CAAC,YAAY,CAAC,GAAG,kBAAkB,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC3D,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;IAClD,IAAI,CAAC,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,OAAO,CAAC,WAAW,EAAE,CAAA;AAC9B,CAAC;AAYD,MAAM,UAAU,qBAAqB,CACnC,YAA+B,EAC/B,UAA2C,EAAE;IAE7C,gCAAgC;IAChC,MAAM,cAAc,GAAG,OAAO,CAAC,iBAAiB,KAAK,KAAK,CAAA;IAC1D,MAAM,kBAAkB,GAAG,cAAc,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,uBAAuB,CAAC,kBAAkB,CAAC,CAAA;IAE3D,IAAI,CAAC,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;QAChC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,eAAe,GAAG,OAAO,IAAI,YAAY,CAAC,EAAE,CAAC,CAAA;IACnD,MAAM,WAAW,GAAG,GAAG,eAAe,MAAM,CAAA;IAE5C,OAAO;QACL,OAAO,EAAE,eAAe;QACxB,YAAY,EAAE;YACZ,uBAAuB,EAAE,WAAW;SACrC;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAC/B,SAAS,EACT,OAAO,GAIR;IACC,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;QAC3B,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,YAAY,SAAS,WAAW,OAAO,EAAE,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,YAA+B,EAC/B,UAA+D,EAAE;IAEjE,MAAM,YAAY,GAAG,qBAAqB,CAAC,YAAY,EAAE;QACvD,iBAAiB,EAAE,OAAO,CAAC,iBAAiB,IAAI,KAAK;KACtD,CAAC,CAAA;IACF,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,SAAS,GACb,OAAO,CAAC,SAAS;QACjB,OAAO,CAAC,GAAG,CAAC,oBAAoB;QAChC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAA;IACzB,MAAM,UAAU,GAAG,gBAAgB,CAAC;QAClC,SAAS;QACT,OAAO,EAAE,YAAY,CAAC,OAAO;KAC9B,CAAC,CAAA;IAEF,OAAO;QACL,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,8BAA8B,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,OAAO,EAAE,YAAY,CAAC,OAAO;KAC9B,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAA6B;IAC1D,MAAM,QAAQ,GACZ,OAAO,OAAO,EAAE,QAAQ,KAAK,QAAQ;QACnC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE;QAChC,CAAC,CAAC,MAAM,CAAA;IACZ,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAA;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IAEvC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACtB,OAAM;IACR,CAAC;IACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACrB,OAAM;IACR,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;AACtB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import baseConfig, {
|
|
2
|
+
nodeConfig,
|
|
3
|
+
typescriptConfig,
|
|
4
|
+
} from '../../eslint.base.config.mjs'
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
...baseConfig,
|
|
8
|
+
{
|
|
9
|
+
...typescriptConfig,
|
|
10
|
+
files: ['src/**/*.ts'],
|
|
11
|
+
rules: {
|
|
12
|
+
...typescriptConfig.rules,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
...nodeConfig,
|
|
17
|
+
files: ['**/*.config.{js,mjs,cjs}'],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
ignores: ['node_modules/**', 'dist/**'],
|
|
21
|
+
},
|
|
22
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kids-reporter/logger",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Shared structured logger for Kids Reporter services",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"postinstall": "yarn build",
|
|
19
|
+
"lint:check": "eslint src --config ./eslint.config.mjs --ext .ts",
|
|
20
|
+
"type-check": "tsc --noEmit -p tsconfig.json"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"eslint": "^9.0.0",
|
|
24
|
+
"typescript": "^5.9.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type LogSeverity =
|
|
2
|
+
| 'DEBUG'
|
|
3
|
+
| 'INFO'
|
|
4
|
+
| 'NOTICE'
|
|
5
|
+
| 'WARNING'
|
|
6
|
+
| 'ERROR'
|
|
7
|
+
| 'ALERT'
|
|
8
|
+
| 'CRITICAL'
|
|
9
|
+
|
|
10
|
+
export type StructuredLogPayload = {
|
|
11
|
+
severity: LogSeverity
|
|
12
|
+
message?: string
|
|
13
|
+
} & Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
/** Supports Express req.headers where values can be string | string[]. */
|
|
16
|
+
export type TraceHeaderInput =
|
|
17
|
+
| Headers
|
|
18
|
+
| Record<string, string | string[] | undefined | unknown>
|
|
19
|
+
| undefined
|
|
20
|
+
|
|
21
|
+
/** Trace context for GCP Cloud Logging correlation. Only traceId is propagated. */
|
|
22
|
+
export type NormalizedTraceContext = {
|
|
23
|
+
traceId: string
|
|
24
|
+
traceHeaders: {
|
|
25
|
+
'X-Cloud-Trace-Context': string
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NormalizedTraceContext,
|
|
3
|
+
StructuredLogPayload,
|
|
4
|
+
TraceHeaderInput,
|
|
5
|
+
} from './types.js'
|
|
6
|
+
|
|
7
|
+
const XCLOUD_TRACE_HEADER = 'x-cloud-trace-context'
|
|
8
|
+
const TRACE_ID_REGEX = /^[a-f0-9]{32}$/i
|
|
9
|
+
|
|
10
|
+
const CONSOLE_BY_SEVERITY: Record<string, 'log' | 'warn' | 'error'> = {
|
|
11
|
+
ALERT: 'error',
|
|
12
|
+
CRITICAL: 'error',
|
|
13
|
+
ERROR: 'error',
|
|
14
|
+
WARNING: 'warn',
|
|
15
|
+
NOTICE: 'log',
|
|
16
|
+
INFO: 'log',
|
|
17
|
+
DEBUG: 'log',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeHeaderValue(
|
|
21
|
+
value: string | string[] | undefined | unknown
|
|
22
|
+
): string | undefined {
|
|
23
|
+
if (value === undefined || value === null) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
Array.isArray(value) &&
|
|
31
|
+
value.length > 0 &&
|
|
32
|
+
typeof value[0] === 'string'
|
|
33
|
+
) {
|
|
34
|
+
return value[0]
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getHeaderValue(input: TraceHeaderInput, headerName: string) {
|
|
40
|
+
if (!input) {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
if (typeof Headers !== 'undefined' && input instanceof Headers) {
|
|
44
|
+
return input.get(headerName) || undefined
|
|
45
|
+
}
|
|
46
|
+
const lowered = headerName.toLowerCase()
|
|
47
|
+
for (const [key, value] of Object.entries(input)) {
|
|
48
|
+
if (key.toLowerCase() === lowered) {
|
|
49
|
+
return normalizeHeaderValue(value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getRandomHex(length: number) {
|
|
56
|
+
const bytes = new Uint8Array(Math.ceil(length / 2))
|
|
57
|
+
if (
|
|
58
|
+
globalThis.crypto &&
|
|
59
|
+
typeof globalThis.crypto.getRandomValues === 'function'
|
|
60
|
+
) {
|
|
61
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
62
|
+
} else {
|
|
63
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
64
|
+
bytes[i] = Math.floor(Math.random() * 256)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return Array.from(bytes, (value) => value.toString(16).padStart(2, '0'))
|
|
68
|
+
.join('')
|
|
69
|
+
.slice(0, length)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses traceId from GCP X-Cloud-Trace-Context.
|
|
74
|
+
* Accepts "traceId;o=1" or "traceId/spanId;o=1" (spanId is ignored).
|
|
75
|
+
*/
|
|
76
|
+
function parseXCloudTraceContext(
|
|
77
|
+
xCloudTraceContext?: string
|
|
78
|
+
): string | undefined {
|
|
79
|
+
if (!xCloudTraceContext) {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
const [traceAndSpan] = xCloudTraceContext.trim().split(';')
|
|
83
|
+
const traceId = traceAndSpan.split('/')[0]?.trim()
|
|
84
|
+
if (!traceId || !TRACE_ID_REGEX.test(traceId)) {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
return traceId.toLowerCase()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Overloads: when generateIfMissing is true, return is always NormalizedTraceContext
|
|
91
|
+
/* eslint-disable no-redeclare -- overload signatures + implementation */
|
|
92
|
+
export function normalizeTraceContext(
|
|
93
|
+
headersInput: TraceHeaderInput | undefined,
|
|
94
|
+
options: { generateIfMissing: true }
|
|
95
|
+
): NormalizedTraceContext
|
|
96
|
+
export function normalizeTraceContext(
|
|
97
|
+
headersInput?: TraceHeaderInput,
|
|
98
|
+
options?: { generateIfMissing?: boolean }
|
|
99
|
+
): NormalizedTraceContext | undefined
|
|
100
|
+
export function normalizeTraceContext(
|
|
101
|
+
headersInput?: TraceHeaderInput,
|
|
102
|
+
options: { generateIfMissing?: boolean } = {}
|
|
103
|
+
) {
|
|
104
|
+
/* eslint-enable no-redeclare */
|
|
105
|
+
const shouldGenerate = options.generateIfMissing !== false
|
|
106
|
+
const xCloudTraceContext = getHeaderValue(headersInput, XCLOUD_TRACE_HEADER)
|
|
107
|
+
const traceId = parseXCloudTraceContext(xCloudTraceContext)
|
|
108
|
+
|
|
109
|
+
if (!traceId && !shouldGenerate) {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resolvedTraceId = traceId ?? getRandomHex(32)
|
|
114
|
+
const xCloudValue = `${resolvedTraceId};o=1`
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
traceId: resolvedTraceId,
|
|
118
|
+
traceHeaders: {
|
|
119
|
+
'X-Cloud-Trace-Context': xCloudValue,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getGcpTraceField({
|
|
125
|
+
projectId,
|
|
126
|
+
traceId,
|
|
127
|
+
}: {
|
|
128
|
+
projectId?: string
|
|
129
|
+
traceId: string
|
|
130
|
+
}) {
|
|
131
|
+
if (!projectId || !traceId) {
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
return `projects/${projectId}/traces/${traceId}`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getTraceLogFields(
|
|
138
|
+
headersInput?: TraceHeaderInput,
|
|
139
|
+
options: { projectId?: string; generateIfMissing?: boolean } = {}
|
|
140
|
+
) {
|
|
141
|
+
const traceContext = normalizeTraceContext(headersInput, {
|
|
142
|
+
generateIfMissing: options.generateIfMissing ?? false,
|
|
143
|
+
})
|
|
144
|
+
if (!traceContext) {
|
|
145
|
+
return {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const projectId =
|
|
149
|
+
options.projectId ||
|
|
150
|
+
process.env.GOOGLE_CLOUD_PROJECT ||
|
|
151
|
+
process.env.GCP_PROJECT
|
|
152
|
+
const traceField = getGcpTraceField({
|
|
153
|
+
projectId,
|
|
154
|
+
traceId: traceContext.traceId,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...(traceField ? { 'logging.googleapis.com/trace': traceField } : {}),
|
|
159
|
+
traceId: traceContext.traceId,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function emitStructured(payload: StructuredLogPayload) {
|
|
164
|
+
const severity =
|
|
165
|
+
typeof payload?.severity === 'string'
|
|
166
|
+
? payload.severity.toUpperCase()
|
|
167
|
+
: 'INFO'
|
|
168
|
+
const method = CONSOLE_BY_SEVERITY[severity] || 'log'
|
|
169
|
+
const message = JSON.stringify(payload)
|
|
170
|
+
|
|
171
|
+
if (method === 'error') {
|
|
172
|
+
console.error(message)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (method === 'warn') {
|
|
176
|
+
console.warn(message)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
console.log(message)
|
|
180
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"noEmit": false,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|