@naturalcycles/backend-lib 9.76.0 → 9.77.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/index.d.ts CHANGED
@@ -18,4 +18,5 @@ export type * from './server/server.model.js';
18
18
  export * from './server/serverStatsMiddleware.js';
19
19
  export * from './server/serverStatusMiddleware.js';
20
20
  export * from './server/simpleRequestLoggerMiddleware.js';
21
+ export * from './server/trace.util.js';
21
22
  export * from './util.js';
package/dist/index.js CHANGED
@@ -17,4 +17,5 @@ export * from './server/safeJsonMiddleware.js';
17
17
  export * from './server/serverStatsMiddleware.js';
18
18
  export * from './server/serverStatusMiddleware.js';
19
19
  export * from './server/simpleRequestLoggerMiddleware.js';
20
+ export * from './server/trace.util.js';
20
21
  export * from './util.js';
@@ -5,6 +5,7 @@ import { _safeJsonStringify } from '@naturalcycles/js-lib/string/safeJsonStringi
5
5
  import { _objectAssign } from '@naturalcycles/js-lib/types';
6
6
  import { logEntryToString } from '@naturalcycles/nodejs-lib';
7
7
  import { dimGrey } from '@naturalcycles/nodejs-lib/colors';
8
+ import { getRequestTraceId } from './trace.util.js';
8
9
  const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, K_SERVICE, APP_ENV } = process.env;
9
10
  const isGAE = !!GAE_INSTANCE;
10
11
  const isCloudRun = !!K_SERVICE;
@@ -75,11 +76,11 @@ export function logMiddleware() {
75
76
  // gcloud run deploy my-service \
76
77
  // --update-env-vars=GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
77
78
  if (GOOGLE_CLOUD_PROJECT) {
78
- const traceHeader = req.header('x-cloud-trace-context');
79
- if (traceHeader) {
80
- const [trace] = traceHeader.split('/');
81
- meta['logging.googleapis.com/trace'] = `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`;
82
- req.requestId = trace;
79
+ const traceId = getRequestTraceId(req);
80
+ if (traceId) {
81
+ meta['logging.googleapis.com/trace'] =
82
+ `projects/${GOOGLE_CLOUD_PROJECT}/traces/${traceId}`;
83
+ req.requestId = traceId;
83
84
  }
84
85
  }
85
86
  if (isGAE) {
@@ -0,0 +1,16 @@
1
+ import type { BackendRequest } from './server.model.js';
2
+ /**
3
+ * The trace a request belongs to, as carried by the W3C `traceparent` header.
4
+ */
5
+ export interface TraceContext {
6
+ traceId: string;
7
+ spanId: string;
8
+ sampled: boolean;
9
+ }
10
+ /**
11
+ * The trace id of the request, from `traceparent`. Cached per request, so it is cheap to call on
12
+ * every log line.
13
+ */
14
+ export declare function getRequestTraceId(req: BackendRequest): string | undefined;
15
+ export declare function getRequestTraceContext(req: BackendRequest): TraceContext | undefined;
16
+ export declare function parseTraceparent(header: string | undefined): TraceContext | undefined;
@@ -0,0 +1,35 @@
1
+ // https://www.w3.org/TR/trace-context/#traceparent-header
2
+ // Version `ff` is invalid. Higher versions may append `-` fields, which are ignored (forward
3
+ // compatibility), while version `00` must not have any.
4
+ const TRACEPARENT_REGEX = /^(?!ff)([\da-f]{2})-([\da-f]{32})-([\da-f]{16})-([\da-f]{2})(-.*)?$/;
5
+ const INVALID_TRACE_ID = '0'.repeat(32);
6
+ const INVALID_SPAN_ID = '0'.repeat(16);
7
+ const traceIdByRequest = new WeakMap();
8
+ /**
9
+ * The trace id of the request, from `traceparent`. Cached per request, so it is cheap to call on
10
+ * every log line.
11
+ */
12
+ export function getRequestTraceId(req) {
13
+ let traceId = traceIdByRequest.get(req);
14
+ if (traceId === undefined) {
15
+ traceId = getRequestTraceContext(req)?.traceId ?? null;
16
+ traceIdByRequest.set(req, traceId);
17
+ }
18
+ return traceId ?? undefined;
19
+ }
20
+ export function getRequestTraceContext(req) {
21
+ // Reads from `req.headers` and not `req.header()`, so a bare request object (as in tests) works too
22
+ const header = req.headers?.['traceparent'];
23
+ return parseTraceparent(typeof header === 'string' ? header : undefined);
24
+ }
25
+ export function parseTraceparent(header) {
26
+ const match = header ? TRACEPARENT_REGEX.exec(header) : null;
27
+ if (!match)
28
+ return;
29
+ const [, version, traceId, spanId, flags, extension] = match;
30
+ if (version === '00' && extension !== undefined)
31
+ return;
32
+ if (traceId === INVALID_TRACE_ID || spanId === INVALID_SPAN_ID)
33
+ return;
34
+ return { traceId, spanId, sampled: Number.parseInt(flags, 16) % 2 === 1 };
35
+ }
@@ -75,7 +75,9 @@ async function parseMultipart(req, maxFileSize) {
75
75
  stream.on('close', () => {
76
76
  const data = Buffer.concat(chunks);
77
77
  files[name] = {
78
- name: info.filename,
78
+ // busboy reports `filename` as undefined for a bare `application/octet-stream` part with
79
+ // no filename; normalize to `""`, same as a blank file input (`filename=""`) yields.
80
+ name: info.filename ?? '',
79
81
  mimeType: info.mimeType,
80
82
  encoding: info.encoding,
81
83
  data,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.76.0",
4
+ "version": "9.77.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/db-lib": "^10",
7
7
  "@naturalcycles/js-lib": "^15",
package/src/index.ts CHANGED
@@ -18,4 +18,5 @@ export type * from './server/server.model.js'
18
18
  export * from './server/serverStatsMiddleware.js'
19
19
  export * from './server/serverStatusMiddleware.js'
20
20
  export * from './server/simpleRequestLoggerMiddleware.js'
21
+ export * from './server/trace.util.js'
21
22
  export * from './util.js'
@@ -8,6 +8,7 @@ import type { AnyObject } from '@naturalcycles/js-lib/types'
8
8
  import { logEntryToString } from '@naturalcycles/nodejs-lib'
9
9
  import { dimGrey } from '@naturalcycles/nodejs-lib/colors'
10
10
  import type { BackendRequestHandler } from './server.model.js'
11
+ import { getRequestTraceId } from './trace.util.js'
11
12
 
12
13
  const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, K_SERVICE, APP_ENV } = process.env
13
14
  const isGAE = !!GAE_INSTANCE
@@ -88,11 +89,11 @@ export function logMiddleware(): BackendRequestHandler {
88
89
  // gcloud run deploy my-service \
89
90
  // --update-env-vars=GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
90
91
  if (GOOGLE_CLOUD_PROJECT) {
91
- const traceHeader = req.header('x-cloud-trace-context')
92
- if (traceHeader) {
93
- const [trace] = traceHeader.split('/')
94
- meta['logging.googleapis.com/trace'] = `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`
95
- req.requestId = trace
92
+ const traceId = getRequestTraceId(req)
93
+ if (traceId) {
94
+ meta['logging.googleapis.com/trace'] =
95
+ `projects/${GOOGLE_CLOUD_PROJECT}/traces/${traceId}`
96
+ req.requestId = traceId
96
97
  }
97
98
  }
98
99
  if (isGAE) {
@@ -0,0 +1,54 @@
1
+ import type { BackendRequest } from './server.model.js'
2
+
3
+ /**
4
+ * The trace a request belongs to, as carried by the W3C `traceparent` header.
5
+ */
6
+ export interface TraceContext {
7
+ traceId: string
8
+ spanId: string
9
+ sampled: boolean
10
+ }
11
+
12
+ // https://www.w3.org/TR/trace-context/#traceparent-header
13
+ // Version `ff` is invalid. Higher versions may append `-` fields, which are ignored (forward
14
+ // compatibility), while version `00` must not have any.
15
+ const TRACEPARENT_REGEX = /^(?!ff)([\da-f]{2})-([\da-f]{32})-([\da-f]{16})-([\da-f]{2})(-.*)?$/
16
+ const INVALID_TRACE_ID = '0'.repeat(32)
17
+ const INVALID_SPAN_ID = '0'.repeat(16)
18
+
19
+ const traceIdByRequest = new WeakMap<BackendRequest, string | null>()
20
+
21
+ /**
22
+ * The trace id of the request, from `traceparent`. Cached per request, so it is cheap to call on
23
+ * every log line.
24
+ */
25
+ export function getRequestTraceId(req: BackendRequest): string | undefined {
26
+ let traceId = traceIdByRequest.get(req)
27
+ if (traceId === undefined) {
28
+ traceId = getRequestTraceContext(req)?.traceId ?? null
29
+ traceIdByRequest.set(req, traceId)
30
+ }
31
+ return traceId ?? undefined
32
+ }
33
+
34
+ export function getRequestTraceContext(req: BackendRequest): TraceContext | undefined {
35
+ // Reads from `req.headers` and not `req.header()`, so a bare request object (as in tests) works too
36
+ const header = req.headers?.['traceparent']
37
+ return parseTraceparent(typeof header === 'string' ? header : undefined)
38
+ }
39
+
40
+ export function parseTraceparent(header: string | undefined): TraceContext | undefined {
41
+ const match = header ? TRACEPARENT_REGEX.exec(header) : null
42
+ if (!match) return
43
+ const [, version, traceId, spanId, flags, extension] = match as unknown as [
44
+ string,
45
+ string,
46
+ string,
47
+ string,
48
+ string,
49
+ string?,
50
+ ]
51
+ if (version === '00' && extension !== undefined) return
52
+ if (traceId === INVALID_TRACE_ID || spanId === INVALID_SPAN_ID) return
53
+ return { traceId, spanId, sampled: Number.parseInt(flags, 16) % 2 === 1 }
54
+ }
@@ -102,7 +102,9 @@ async function parseMultipart(
102
102
  stream.on('close', () => {
103
103
  const data = Buffer.concat(chunks)
104
104
  files[name] = {
105
- name: info.filename,
105
+ // busboy reports `filename` as undefined for a bare `application/octet-stream` part with
106
+ // no filename; normalize to `""`, same as a blank file input (`filename=""`) yields.
107
+ name: info.filename ?? '',
106
108
  mimeType: info.mimeType,
107
109
  encoding: info.encoding,
108
110
  data,