@forge-ops/tracker 0.10.0 → 0.12.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 +129 -3
- package/package.json +1 -1
- package/src/changes.js +147 -0
- package/src/client.js +17 -0
- package/src/configuration.js +105 -0
- package/src/eventBuilder.js +20 -3
- package/src/httpTracing.js +53 -10
- package/src/index.js +169 -8
- package/src/integrations/express.js +16 -5
- package/src/integrations/fastify.js +18 -5
- package/src/integrations/tracing.js +73 -16
- package/src/reporter.js +3 -2
- package/src/requestState.js +117 -0
- package/src/spanBuffer.js +32 -9
- package/src/traceParent.js +88 -0
package/src/spanBuffer.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateSpanId, generateTraceId } from "./traceParent.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A short, unique-enough identifier for one span:
|
|
5
|
-
*
|
|
4
|
+
* A short, unique-enough identifier for one span: a W3C span id (16 lowercase hex characters,
|
|
5
|
+
* never all zeros; see traceParent.js), the same id a traceparent header's parent-id carries. The
|
|
6
6
|
* server only ever needs these to be unique within one trace's own array (see
|
|
7
7
|
* Api::V1::SpansController on the Rails side), never a real database id, so this is deliberately
|
|
8
8
|
* cheap rather than a full UUID.
|
|
9
9
|
* @returns {string}
|
|
10
10
|
*/
|
|
11
11
|
export function randomSpanId() {
|
|
12
|
-
return
|
|
12
|
+
return generateSpanId();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -33,17 +33,31 @@ export class SpanBuffer {
|
|
|
33
33
|
rootSpanId;
|
|
34
34
|
spans = [];
|
|
35
35
|
#rootDurationMs = null;
|
|
36
|
+
#remoteParentSpanId;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* traceId/parentSpanId come from the request's own state (see _runWithRequestState in index.js)
|
|
40
|
+
* when there is one, never generated separately here, so a trace this sends and an error event
|
|
41
|
+
* from the same request always agree on which trace they belong to. parentSpanId is the calling
|
|
42
|
+
* service's span (from an incoming traceparent header), recorded as the root span's parent: the
|
|
43
|
+
* server treats a parent that isn't in this trace's own batch as remote, and nests this
|
|
44
|
+
* service's root under the caller's outgoing HTTP span.
|
|
45
|
+
*
|
|
46
|
+
* @param {import("./configuration.js").Configuration} configuration
|
|
47
|
+
* @param {{ traceId?: string, parentSpanId?: string | null }} [ids]
|
|
48
|
+
*/
|
|
49
|
+
constructor(configuration, { traceId = generateTraceId(), parentSpanId = null } = {}) {
|
|
38
50
|
this.#configuration = configuration;
|
|
39
|
-
this.traceId =
|
|
51
|
+
this.traceId = traceId;
|
|
40
52
|
this.rootSpanId = randomSpanId();
|
|
53
|
+
this.#remoteParentSpanId = parentSpanId;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
/**
|
|
44
57
|
* root: true only for the one call that records the request's own root span: forces
|
|
45
|
-
* parent_span_id null
|
|
46
|
-
*
|
|
58
|
+
* parent_span_id to the remote parent (null unless the request arrived with a traceparent
|
|
59
|
+
* header) explicitly, the same reasoning span_buffer.rb's own #record documents for why that
|
|
60
|
+
* can't just be read off of "whatever's currently open" for the root specifically.
|
|
47
61
|
*
|
|
48
62
|
* @param {{
|
|
49
63
|
* spanId: string,
|
|
@@ -59,7 +73,7 @@ export class SpanBuffer {
|
|
|
59
73
|
record({ spanId, parentSpanId = null, name, kind, startedAt, durationMs, data = {}, root = false }) {
|
|
60
74
|
this.spans.push({
|
|
61
75
|
span_id: spanId,
|
|
62
|
-
parent_span_id: root ?
|
|
76
|
+
parent_span_id: root ? this.#remoteParentSpanId : parentSpanId,
|
|
63
77
|
name,
|
|
64
78
|
kind,
|
|
65
79
|
// Date#toISOString() already yields millisecond precision UTC ("...sssZ"), exactly the wire
|
|
@@ -76,6 +90,15 @@ export class SpanBuffer {
|
|
|
76
90
|
}
|
|
77
91
|
}
|
|
78
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Whether the request's own root span has been recorded: an errored request's trace is only sent
|
|
95
|
+
* once it has, since spans with no root to hang from would render as a waterfall with no top.
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
get rootRecorded() {
|
|
99
|
+
return this.#rootDurationMs !== null;
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
/**
|
|
80
103
|
* null (never sent) until the root span has actually been recorded: a request whose own
|
|
81
104
|
* tracing integration never got the chance to call back at all has no duration to compare
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads and writes the W3C Trace Context `traceparent` header
|
|
5
|
+
* (https://www.w3.org/TR/trace-context/), the vendor-neutral format for carrying one trace across
|
|
6
|
+
* service boundaries: `00-<32 hex trace-id>-<16 hex parent-id>-<2 hex flags>`. Used in both
|
|
7
|
+
* directions: the Express/Fastify integrations parse an incoming one so a request continues the
|
|
8
|
+
* caller's trace instead of starting its own, and httpTracing.js builds an outgoing one so the next
|
|
9
|
+
* service along continues this request's. Ported from
|
|
10
|
+
* gems/forge_ops_tracker/lib/forge_ops_tracker/trace_parent.rb.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately strict on the way in, the same posture the spec itself asks receivers to take: a
|
|
13
|
+
* malformed value, uppercase hex, the reserved version "ff", or an all-zero trace/parent id are all
|
|
14
|
+
* treated as "no usable header at all" (parseTraceparent returns null and the request starts a
|
|
15
|
+
* fresh trace), never half-trusted. A version this client doesn't know yet is still accepted as
|
|
16
|
+
* long as its first four fields have version 00's shape, which is exactly what the spec says a
|
|
17
|
+
* version-00 parser should do with a future version; version 00 itself must have exactly four
|
|
18
|
+
* fields.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const TRACEPARENT_HEADER = "traceparent";
|
|
22
|
+
|
|
23
|
+
const PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/s;
|
|
24
|
+
const INVALID_TRACE_ID = "0".repeat(32);
|
|
25
|
+
const INVALID_PARENT_ID = "0".repeat(16);
|
|
26
|
+
|
|
27
|
+
// Always "01" (sampled) on the way out: this client decides whether to actually send a trace only
|
|
28
|
+
// once the request is over (see _finishSpanTrace in index.js), long after this header has already
|
|
29
|
+
// gone out on an outbound call, so there's no honest earlier answer to give than "this may be
|
|
30
|
+
// recorded." A downstream service is free to make its own decision either way.
|
|
31
|
+
const SAMPLED_FLAGS = "01";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {unknown} value an incoming header's raw value
|
|
35
|
+
* @returns {{ traceId: string, parentSpanId: string } | null} null for anything that isn't a usable header
|
|
36
|
+
*/
|
|
37
|
+
export function parseTraceparent(value) {
|
|
38
|
+
if (typeof value !== "string") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const match = PATTERN.exec(value.trim());
|
|
42
|
+
if (match === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const [, version, traceId, parentSpanId, , rest] = match;
|
|
46
|
+
if (version === "ff" || (version === "00" && rest !== undefined)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (traceId === INVALID_TRACE_ID || parentSpanId === INVALID_PARENT_ID) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return { traceId, parentSpanId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} traceId
|
|
57
|
+
* @param {string} spanId
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function buildTraceparent(traceId, spanId) {
|
|
61
|
+
return `00-${traceId}-${spanId}-${SAMPLED_FLAGS}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 32 lowercase hex characters, the W3C trace-id format, never all zeros (the spec reserves that as
|
|
66
|
+
* invalid, and a receiver drops the whole header over it): vanishingly unlikely from 16 random
|
|
67
|
+
* bytes, but free to rule out.
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function generateTraceId() {
|
|
71
|
+
return randomId(16);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 16 lowercase hex characters, the W3C parent-id (span id) format, never all zeros.
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function generateSpanId() {
|
|
79
|
+
return randomId(8);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function randomId(bytes) {
|
|
83
|
+
let id = randomBytes(bytes).toString("hex");
|
|
84
|
+
while (/^0+$/.test(id)) {
|
|
85
|
+
id = randomBytes(bytes).toString("hex");
|
|
86
|
+
}
|
|
87
|
+
return id;
|
|
88
|
+
}
|