@nifrajs/otel 2.0.0 → 2.2.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/effects.d.ts +24 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +129 -0
- package/dist/effects.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/effects.ts +146 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Token-only OpenTelemetry spans for capability admission, execution, compensation, and reconciliation. */
|
|
2
|
+
import type { EffectLifecycleObserver } from "@nifrajs/core/effect-lifecycle";
|
|
3
|
+
import type { IdentityPlugin } from "@nifrajs/core/server";
|
|
4
|
+
import type { ObservationAdapter } from "./span.js";
|
|
5
|
+
export interface EffectTracingOptions {
|
|
6
|
+
readonly exporter?: ObservationAdapter;
|
|
7
|
+
readonly adapters?: readonly ObservationAdapter[];
|
|
8
|
+
/** Maximum unmatched started events retained at once. Default 10,000. */
|
|
9
|
+
readonly maxActive?: number;
|
|
10
|
+
/** Opportunistically expire unmatched events older than this many milliseconds. Default 5 min. */
|
|
11
|
+
readonly maxActiveAgeMs?: number;
|
|
12
|
+
/** Injectable wall clock for deterministic retention tests. */
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
export interface EffectTracingPlugin extends IdentityPlugin {
|
|
16
|
+
/** Pass the same observer to `createSagaEngine({ observer })` and reconciliation helpers. */
|
|
17
|
+
readonly observer: EffectLifecycleObserver;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Installs child effect spans on subsequent routes. The observer consumes only the constrained
|
|
21
|
+
* `EffectLifecycleEvent` contract; request/business payloads and error text cannot enter an export.
|
|
22
|
+
*/
|
|
23
|
+
export declare function effectTracing(options?: EffectTracingOptions): EffectTracingPlugin;
|
|
24
|
+
//# sourceMappingURL=effects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effects.d.ts","sourceRoot":"","sources":["../src/effects.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAE5G,OAAO,KAAK,EAAwB,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AACnG,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErE,OAAO,KAAK,EAAkB,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAEnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACjD,yEAAyE;IACzE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAA;CAC3C;AAoBD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,mBAAmB,CAoGrF"}
|
package/dist/effects.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Token-only OpenTelemetry spans for capability admission, execution, compensation, and reconciliation. */
|
|
2
|
+
import { createObservationLifecycle } from "./lifecycle.js";
|
|
3
|
+
function attributesOf(event) {
|
|
4
|
+
const attributes = {
|
|
5
|
+
"nifra.effect.id": event.effectId,
|
|
6
|
+
"nifra.effect.capability": event.capability,
|
|
7
|
+
"nifra.effect.stage": event.stage,
|
|
8
|
+
"nifra.effect.phase": event.phase,
|
|
9
|
+
};
|
|
10
|
+
if (event.target !== undefined)
|
|
11
|
+
attributes["nifra.effect.target"] = event.target;
|
|
12
|
+
if (event.digest !== undefined)
|
|
13
|
+
attributes["nifra.effect.digest"] = event.digest;
|
|
14
|
+
if (event.attempt !== undefined)
|
|
15
|
+
attributes["nifra.effect.attempt"] = event.attempt;
|
|
16
|
+
if (event.errorCode !== undefined)
|
|
17
|
+
attributes["nifra.effect.error_code"] = event.errorCode;
|
|
18
|
+
if (event.durationMs !== undefined)
|
|
19
|
+
attributes["nifra.effect.duration_ms"] = event.durationMs;
|
|
20
|
+
for (const [axis, value] of Object.entries(event.cost ?? {})) {
|
|
21
|
+
attributes[`nifra.effect.cost.${axis}`] = value;
|
|
22
|
+
}
|
|
23
|
+
return attributes;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Installs child effect spans on subsequent routes. The observer consumes only the constrained
|
|
27
|
+
* `EffectLifecycleEvent` contract; request/business payloads and error text cannot enter an export.
|
|
28
|
+
*/
|
|
29
|
+
export function effectTracing(options = {}) {
|
|
30
|
+
const adapters = [
|
|
31
|
+
...(options.exporter === undefined ? [] : [options.exporter]),
|
|
32
|
+
...(options.adapters ?? []),
|
|
33
|
+
];
|
|
34
|
+
const maxActive = options.maxActive ?? 10_000;
|
|
35
|
+
const maxActiveAgeMs = options.maxActiveAgeMs ?? 5 * 60_000;
|
|
36
|
+
if (!Number.isSafeInteger(maxActive) || maxActive < 1) {
|
|
37
|
+
throw new RangeError("effect tracing maxActive must be a positive safe integer");
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isSafeInteger(maxActiveAgeMs) || maxActiveAgeMs < 1) {
|
|
40
|
+
throw new RangeError("effect tracing maxActiveAgeMs must be a positive safe integer");
|
|
41
|
+
}
|
|
42
|
+
const now = options.now ?? Date.now;
|
|
43
|
+
const lifecycle = createObservationLifecycle({ adapters });
|
|
44
|
+
const active = new Map();
|
|
45
|
+
let nextSweepAt = Number.POSITIVE_INFINITY;
|
|
46
|
+
const keyOf = (event) => `${event.effectId}\n${event.stage}`;
|
|
47
|
+
const evict = (key, errorCode) => {
|
|
48
|
+
const entry = active.get(key);
|
|
49
|
+
if (entry === undefined)
|
|
50
|
+
return;
|
|
51
|
+
active.delete(key);
|
|
52
|
+
entry.observation.setAttributes({
|
|
53
|
+
"nifra.effect.phase": "ambiguous",
|
|
54
|
+
"nifra.effect.error_code": errorCode,
|
|
55
|
+
});
|
|
56
|
+
entry.observation.end({ status: "error" });
|
|
57
|
+
};
|
|
58
|
+
const sweep = () => {
|
|
59
|
+
const at = now();
|
|
60
|
+
if (!Number.isFinite(at) || at < nextSweepAt)
|
|
61
|
+
return at;
|
|
62
|
+
// `active` iterates in insertion order and entries carry a monotonic `at`, so the front is the
|
|
63
|
+
// oldest / earliest-expiring. Evict the expired prefix and stop at the first live entry: its expiry
|
|
64
|
+
// is the earliest remaining, i.e. the next sweep deadline. A sweep is therefore O(evicted), not
|
|
65
|
+
// O(active.size). (A backwards clock only defers an eviction to the size cap - the hard bound; age
|
|
66
|
+
// eviction is opportunistic.)
|
|
67
|
+
nextSweepAt = Number.POSITIVE_INFINITY;
|
|
68
|
+
for (const [key, entry] of active) {
|
|
69
|
+
const expiresAt = entry.at + maxActiveAgeMs;
|
|
70
|
+
if (at >= expiresAt) {
|
|
71
|
+
evict(key, "observation_expired");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
nextSweepAt = expiresAt;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return at;
|
|
79
|
+
};
|
|
80
|
+
const observer = (event) => {
|
|
81
|
+
const at = sweep();
|
|
82
|
+
const key = keyOf(event);
|
|
83
|
+
if (event.phase === "started") {
|
|
84
|
+
if (active.has(key))
|
|
85
|
+
return;
|
|
86
|
+
while (active.size >= maxActive) {
|
|
87
|
+
const oldest = active.keys().next().value;
|
|
88
|
+
if (oldest === undefined)
|
|
89
|
+
break;
|
|
90
|
+
evict(oldest, "observation_evicted");
|
|
91
|
+
}
|
|
92
|
+
const startedAt = Number.isFinite(at) ? at : event.at;
|
|
93
|
+
active.set(key, Object.freeze({
|
|
94
|
+
observation: lifecycle.start({
|
|
95
|
+
name: `nifra.effect.${event.stage}`,
|
|
96
|
+
parent: event.trace ?? null,
|
|
97
|
+
attributes: attributesOf(event),
|
|
98
|
+
}),
|
|
99
|
+
at: startedAt,
|
|
100
|
+
}));
|
|
101
|
+
nextSweepAt = Math.min(nextSweepAt, startedAt + maxActiveAgeMs);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const entry = active.get(key);
|
|
105
|
+
let observation;
|
|
106
|
+
if (entry === undefined) {
|
|
107
|
+
observation = lifecycle.start({
|
|
108
|
+
name: `nifra.effect.${event.stage}`,
|
|
109
|
+
parent: event.trace ?? null,
|
|
110
|
+
attributes: attributesOf(event),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
active.delete(key);
|
|
115
|
+
observation = entry.observation;
|
|
116
|
+
observation.setAttributes(attributesOf(event));
|
|
117
|
+
}
|
|
118
|
+
observation.end({ status: event.phase === "succeeded" ? "ok" : "error" });
|
|
119
|
+
};
|
|
120
|
+
const apply = (app) => {
|
|
121
|
+
app.observeCapability(observer);
|
|
122
|
+
return app;
|
|
123
|
+
};
|
|
124
|
+
return Object.assign(apply, {
|
|
125
|
+
pluginName: "nifra:effect-tracing",
|
|
126
|
+
observer,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=effects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effects.js","sourceRoot":"","sources":["../src/effects.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
|
|
10
10
|
*/
|
|
11
11
|
export { causalitySpanLink } from "./causality.js";
|
|
12
|
+
export { type EffectTracingOptions, type EffectTracingPlugin, effectTracing, } from "./effects.js";
|
|
12
13
|
export { type ActiveObservation, createObservationLifecycle, type EndObservation, type ObservationClock, type ObservationContext, type ObservationLifecycle, type ObservationLifecycleOptions, type ObservationParent, type StartObservation, } from "./lifecycle.js";
|
|
13
14
|
export { type OtlpExporter, type OtlpExporterOptions, otlpExporter } from "./otlp.js";
|
|
14
15
|
export { type AttributeValue, combineObservationAdapters, consoleSpanExporter, type NifraSpan, type ObservationAdapter, type ObservationLink, type SpanStatus, } from "./span.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EACL,KAAK,iBAAiB,EACtB,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EACL,KAAK,cAAc,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,aAAa,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,iBAAiB,EACtB,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EACL,KAAK,cAAc,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
|
|
10
10
|
*/
|
|
11
11
|
export { causalitySpanLink } from "./causality.js";
|
|
12
|
+
export { effectTracing, } from "./effects.js";
|
|
12
13
|
export { createObservationLifecycle, } from "./lifecycle.js";
|
|
13
14
|
export { otlpExporter } from "./otlp.js";
|
|
14
15
|
export { combineObservationAdapters, consoleSpanExporter, } from "./span.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EAEL,0BAA0B,GAQ3B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAA+C,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EAEL,0BAA0B,EAC1B,mBAAmB,GAKpB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EAEf,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA0C,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EAGL,aAAa,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EAEL,0BAA0B,GAQ3B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAA+C,YAAY,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EAEL,0BAA0B,EAC1B,mBAAmB,GAKpB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EAEf,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA0C,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/otel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Distributed tracing for nifra — W3C traceparent/tracestate propagation + OpenTelemetry-semantic-convention spans via a pluggable exporter. Bridge to the OpenTelemetry SDK or log spans directly; no SDK bundled. Edge-safe.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"bun": "./src/metrics.ts",
|
|
30
30
|
"types": "./dist/metrics.d.ts",
|
|
31
31
|
"default": "./dist/metrics.js"
|
|
32
|
+
},
|
|
33
|
+
"./effects": {
|
|
34
|
+
"bun": "./src/effects.ts",
|
|
35
|
+
"types": "./dist/effects.d.ts",
|
|
36
|
+
"default": "./dist/effects.js"
|
|
32
37
|
}
|
|
33
38
|
},
|
|
34
39
|
"files": [
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
"access": "public"
|
|
45
50
|
},
|
|
46
51
|
"peerDependencies": {
|
|
47
|
-
"@nifrajs/core": "^2.
|
|
52
|
+
"@nifrajs/core": "^2.2.0"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@nifrajs/core": "workspace:*"
|
package/src/effects.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Token-only OpenTelemetry spans for capability admission, execution, compensation, and reconciliation. */
|
|
2
|
+
|
|
3
|
+
import type { EffectLifecycleEvent, EffectLifecycleObserver } from "@nifrajs/core/effect-lifecycle"
|
|
4
|
+
import type { AnyServer, IdentityPlugin } from "@nifrajs/core/server"
|
|
5
|
+
import { type ActiveObservation, createObservationLifecycle } from "./lifecycle.ts"
|
|
6
|
+
import type { AttributeValue, ObservationAdapter } from "./span.ts"
|
|
7
|
+
|
|
8
|
+
export interface EffectTracingOptions {
|
|
9
|
+
readonly exporter?: ObservationAdapter
|
|
10
|
+
readonly adapters?: readonly ObservationAdapter[]
|
|
11
|
+
/** Maximum unmatched started events retained at once. Default 10,000. */
|
|
12
|
+
readonly maxActive?: number
|
|
13
|
+
/** Opportunistically expire unmatched events older than this many milliseconds. Default 5 min. */
|
|
14
|
+
readonly maxActiveAgeMs?: number
|
|
15
|
+
/** Injectable wall clock for deterministic retention tests. */
|
|
16
|
+
readonly now?: () => number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EffectTracingPlugin extends IdentityPlugin {
|
|
20
|
+
/** Pass the same observer to `createSagaEngine({ observer })` and reconciliation helpers. */
|
|
21
|
+
readonly observer: EffectLifecycleObserver
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function attributesOf(event: EffectLifecycleEvent): Record<string, AttributeValue> {
|
|
25
|
+
const attributes: Record<string, AttributeValue> = {
|
|
26
|
+
"nifra.effect.id": event.effectId,
|
|
27
|
+
"nifra.effect.capability": event.capability,
|
|
28
|
+
"nifra.effect.stage": event.stage,
|
|
29
|
+
"nifra.effect.phase": event.phase,
|
|
30
|
+
}
|
|
31
|
+
if (event.target !== undefined) attributes["nifra.effect.target"] = event.target
|
|
32
|
+
if (event.digest !== undefined) attributes["nifra.effect.digest"] = event.digest
|
|
33
|
+
if (event.attempt !== undefined) attributes["nifra.effect.attempt"] = event.attempt
|
|
34
|
+
if (event.errorCode !== undefined) attributes["nifra.effect.error_code"] = event.errorCode
|
|
35
|
+
if (event.durationMs !== undefined) attributes["nifra.effect.duration_ms"] = event.durationMs
|
|
36
|
+
for (const [axis, value] of Object.entries(event.cost ?? {})) {
|
|
37
|
+
attributes[`nifra.effect.cost.${axis}`] = value
|
|
38
|
+
}
|
|
39
|
+
return attributes
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Installs child effect spans on subsequent routes. The observer consumes only the constrained
|
|
44
|
+
* `EffectLifecycleEvent` contract; request/business payloads and error text cannot enter an export.
|
|
45
|
+
*/
|
|
46
|
+
export function effectTracing(options: EffectTracingOptions = {}): EffectTracingPlugin {
|
|
47
|
+
const adapters = [
|
|
48
|
+
...(options.exporter === undefined ? [] : [options.exporter]),
|
|
49
|
+
...(options.adapters ?? []),
|
|
50
|
+
]
|
|
51
|
+
const maxActive = options.maxActive ?? 10_000
|
|
52
|
+
const maxActiveAgeMs = options.maxActiveAgeMs ?? 5 * 60_000
|
|
53
|
+
if (!Number.isSafeInteger(maxActive) || maxActive < 1) {
|
|
54
|
+
throw new RangeError("effect tracing maxActive must be a positive safe integer")
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isSafeInteger(maxActiveAgeMs) || maxActiveAgeMs < 1) {
|
|
57
|
+
throw new RangeError("effect tracing maxActiveAgeMs must be a positive safe integer")
|
|
58
|
+
}
|
|
59
|
+
const now = options.now ?? Date.now
|
|
60
|
+
const lifecycle = createObservationLifecycle({ adapters })
|
|
61
|
+
const active = new Map<string, { readonly observation: ActiveObservation; readonly at: number }>()
|
|
62
|
+
let nextSweepAt = Number.POSITIVE_INFINITY
|
|
63
|
+
const keyOf = (event: EffectLifecycleEvent): string => `${event.effectId}\n${event.stage}`
|
|
64
|
+
|
|
65
|
+
const evict = (key: string, errorCode: "observation_evicted" | "observation_expired"): void => {
|
|
66
|
+
const entry = active.get(key)
|
|
67
|
+
if (entry === undefined) return
|
|
68
|
+
active.delete(key)
|
|
69
|
+
entry.observation.setAttributes({
|
|
70
|
+
"nifra.effect.phase": "ambiguous",
|
|
71
|
+
"nifra.effect.error_code": errorCode,
|
|
72
|
+
})
|
|
73
|
+
entry.observation.end({ status: "error" })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sweep = (): number => {
|
|
77
|
+
const at = now()
|
|
78
|
+
if (!Number.isFinite(at) || at < nextSweepAt) return at
|
|
79
|
+
// `active` iterates in insertion order and entries carry a monotonic `at`, so the front is the
|
|
80
|
+
// oldest / earliest-expiring. Evict the expired prefix and stop at the first live entry: its expiry
|
|
81
|
+
// is the earliest remaining, i.e. the next sweep deadline. A sweep is therefore O(evicted), not
|
|
82
|
+
// O(active.size). (A backwards clock only defers an eviction to the size cap - the hard bound; age
|
|
83
|
+
// eviction is opportunistic.)
|
|
84
|
+
nextSweepAt = Number.POSITIVE_INFINITY
|
|
85
|
+
for (const [key, entry] of active) {
|
|
86
|
+
const expiresAt = entry.at + maxActiveAgeMs
|
|
87
|
+
if (at >= expiresAt) {
|
|
88
|
+
evict(key, "observation_expired")
|
|
89
|
+
} else {
|
|
90
|
+
nextSweepAt = expiresAt
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return at
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const observer: EffectLifecycleObserver = (event) => {
|
|
98
|
+
const at = sweep()
|
|
99
|
+
const key = keyOf(event)
|
|
100
|
+
if (event.phase === "started") {
|
|
101
|
+
if (active.has(key)) return
|
|
102
|
+
while (active.size >= maxActive) {
|
|
103
|
+
const oldest = active.keys().next().value as string | undefined
|
|
104
|
+
if (oldest === undefined) break
|
|
105
|
+
evict(oldest, "observation_evicted")
|
|
106
|
+
}
|
|
107
|
+
const startedAt = Number.isFinite(at) ? at : event.at
|
|
108
|
+
active.set(
|
|
109
|
+
key,
|
|
110
|
+
Object.freeze({
|
|
111
|
+
observation: lifecycle.start({
|
|
112
|
+
name: `nifra.effect.${event.stage}`,
|
|
113
|
+
parent: event.trace ?? null,
|
|
114
|
+
attributes: attributesOf(event),
|
|
115
|
+
}),
|
|
116
|
+
at: startedAt,
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
nextSweepAt = Math.min(nextSweepAt, startedAt + maxActiveAgeMs)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
const entry = active.get(key)
|
|
123
|
+
let observation: ActiveObservation
|
|
124
|
+
if (entry === undefined) {
|
|
125
|
+
observation = lifecycle.start({
|
|
126
|
+
name: `nifra.effect.${event.stage}`,
|
|
127
|
+
parent: event.trace ?? null,
|
|
128
|
+
attributes: attributesOf(event),
|
|
129
|
+
})
|
|
130
|
+
} else {
|
|
131
|
+
active.delete(key)
|
|
132
|
+
observation = entry.observation
|
|
133
|
+
observation.setAttributes(attributesOf(event))
|
|
134
|
+
}
|
|
135
|
+
observation.end({ status: event.phase === "succeeded" ? "ok" : "error" })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const apply = <S extends AnyServer>(app: S): S => {
|
|
139
|
+
app.observeCapability(observer)
|
|
140
|
+
return app
|
|
141
|
+
}
|
|
142
|
+
return Object.assign(apply, {
|
|
143
|
+
pluginName: "nifra:effect-tracing",
|
|
144
|
+
observer,
|
|
145
|
+
}) as EffectTracingPlugin
|
|
146
|
+
}
|
package/src/index.ts
CHANGED