nestjs-otel 8.0.4-alpha.7 → 8.1.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 +30 -4
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/wide-events/wide-event.interceptor.d.ts +7 -0
- package/lib/wide-events/wide-event.interceptor.js +19 -8
- package/lib/wide-events/wide-event.interceptor.spec.js +21 -0
- package/lib/wide-events/wide-event.span-processor.d.ts +36 -0
- package/lib/wide-events/wide-event.span-processor.js +59 -0
- package/lib/wide-events/wide-event.span-processor.spec.d.ts +1 -0
- package/lib/wide-events/wide-event.span-processor.spec.js +46 -0
- package/package.json +1 -1
- package/.claude/settings.json +0 -24
- package/.claude/settings.local.json +0 -20
- package/.tmp_build.out +0 -3
- package/graphify-out/.graphify_labels.json +0 -12
- package/graphify-out/.graphify_python +0 -1
- package/graphify-out/.graphify_root +0 -1
- package/graphify-out/GRAPH_REPORT.md +0 -89
- package/graphify-out/cost.json +0 -12
- package/graphify-out/graph.html +0 -6110
- package/graphify-out/graph.json +0 -4603
- package/graphify-out/manifest.json +0 -167
package/README.md
CHANGED
|
@@ -310,7 +310,9 @@ export class BookService {
|
|
|
310
310
|
|
|
311
311
|
Wide events (also known as canonical log lines) emit one context-rich event per request, with attributes accumulated across the whole request lifecycle. See [A Practitioner's Guide to Wide Events](https://jeremymorrell.dev/blog/a-practitioners-guide-to-wide-events/) for the pattern.
|
|
312
312
|
|
|
313
|
-
This library implements the pattern on top of OpenTelemetry: the `WideEventInterceptor` opens an attribute bag per request and, when the request finishes, flushes everything onto
|
|
313
|
+
This library implements the pattern on top of OpenTelemetry: the `WideEventInterceptor` opens an attribute bag per request and, when the request finishes, flushes everything onto a single span — marking it with `nestjs_otel.wide_event = true` so you can filter wide-event spans in your backend. The `WideEventService` lets any provider enrich that bag from anywhere in the request's async call chain — no request-scoped injection needed.
|
|
314
|
+
|
|
315
|
+
By default the attributes land on the best-available recording span at flush time (the HTTP root on Express / plain Fastify, or the active handler span when an instrumentation such as `@opentelemetry/instrumentation-nestjs-core` or `@fastify/otel` nests a span per request phase). To guarantee they land on the **trace root span** regardless of instrumentation nesting, register the [`WideEventSpanProcessor`](#targeting-the-root-span-wideeventspanprocessor).
|
|
314
316
|
|
|
315
317
|
1. Register the interceptor globally:
|
|
316
318
|
|
|
@@ -368,10 +370,11 @@ export class CheckoutService {
|
|
|
368
370
|
}
|
|
369
371
|
```
|
|
370
372
|
|
|
371
|
-
All accumulated attributes land on
|
|
373
|
+
All accumulated attributes land on a single span as one wide event:
|
|
372
374
|
|
|
373
375
|
```
|
|
374
|
-
|
|
376
|
+
GET /checkout
|
|
377
|
+
├── nestjs_otel.wide_event: true
|
|
375
378
|
├── code.function.name: CheckoutController.checkout
|
|
376
379
|
├── user.id: u-123
|
|
377
380
|
├── cart.items: 3
|
|
@@ -384,7 +387,8 @@ http_request
|
|
|
384
387
|
|
|
385
388
|
Notes:
|
|
386
389
|
|
|
387
|
-
- The
|
|
390
|
+
- The flushed span is marked `nestjs_otel.wide_event = true`. This marker is set last, so a handler-provided field can never overwrite it.
|
|
391
|
+
- The interceptor seeds `code.function.name` (`<Controller>.<handler>`) automatically and records `error.type`/`error.message` (plus `error.stack` when present) when the handler throws.
|
|
388
392
|
- `WideEventService` methods are safe no-ops outside a request handled by the interceptor.
|
|
389
393
|
- An async-context-aware context manager is required (the default with `NodeSDK` / `AsyncLocalStorageContextManager`), same as for tracing in general.
|
|
390
394
|
|
|
@@ -425,6 +429,28 @@ export class BooksService {
|
|
|
425
429
|
}
|
|
426
430
|
```
|
|
427
431
|
|
|
432
|
+
### Targeting the root span (`WideEventSpanProcessor`)
|
|
433
|
+
|
|
434
|
+
Instrumentations like `@opentelemetry/instrumentation-nestjs-core` and `@fastify/otel` wrap each request phase (middleware, guard, handler) in its own span. As a result, the span active when the interceptor runs is a nested child, not the trace root — so by default the wide event lands on that child span (e.g. `AppController.handler`) rather than on the root `GET /route`.
|
|
435
|
+
|
|
436
|
+
Register the `WideEventSpanProcessor` on your `NodeSDK` to make the interceptor flush onto the **local-root span** (the span with no in-process parent — normally the HTTP server span) instead:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
440
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
441
|
+
import { WideEventSpanProcessor } from 'nestjs-otel';
|
|
442
|
+
|
|
443
|
+
export const otelSDK = new NodeSDK({
|
|
444
|
+
spanProcessors: [
|
|
445
|
+
new WideEventSpanProcessor(),
|
|
446
|
+
new BatchSpanProcessor(traceExporter),
|
|
447
|
+
],
|
|
448
|
+
// ...instrumentations, contextManager, etc.
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
The processor only tracks which span is the local root per trace; it never exports spans, so keep your existing exporting processor in the list. It is optional — without it, wide events still flush onto the best-available recording span (see above), just not necessarily the trace root.
|
|
453
|
+
|
|
428
454
|
## Metric Service
|
|
429
455
|
|
|
430
456
|
[OpenTelemetry Metrics](https://www.npmjs.com/package/@opentelemetry/api) allow a user to collect data and export it to metrics backend like Prometheus.
|
package/lib/index.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export * from "./wide-events/wide-event.context";
|
|
|
13
13
|
export * from "./wide-events/wide-event.interceptor";
|
|
14
14
|
export * from "./wide-events/wide-event.middleware";
|
|
15
15
|
export * from "./wide-events/wide-event.service";
|
|
16
|
+
export * from "./wide-events/wide-event.span-processor";
|
|
16
17
|
export * from "./wide-events/wide-event-field.decorator";
|
package/lib/index.js
CHANGED
|
@@ -29,4 +29,5 @@ __exportStar(require("./wide-events/wide-event.context"), exports);
|
|
|
29
29
|
__exportStar(require("./wide-events/wide-event.interceptor"), exports);
|
|
30
30
|
__exportStar(require("./wide-events/wide-event.middleware"), exports);
|
|
31
31
|
__exportStar(require("./wide-events/wide-event.service"), exports);
|
|
32
|
+
__exportStar(require("./wide-events/wide-event.span-processor"), exports);
|
|
32
33
|
__exportStar(require("./wide-events/wide-event-field.decorator"), exports);
|
|
@@ -16,6 +16,13 @@ export declare class WideEventInterceptor implements NestInterceptor {
|
|
|
16
16
|
constructor(options?: OpenTelemetryModuleOptions | undefined);
|
|
17
17
|
intercept(executionContext: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
18
18
|
private rootSpanFromRequest;
|
|
19
|
+
/**
|
|
20
|
+
* Picks the span the wide event should be written to, preferring the
|
|
21
|
+
* local-root span (when the WideEventSpanProcessor is registered), then the
|
|
22
|
+
* middleware-captured root span, then the interceptor-time active span —
|
|
23
|
+
* skipping any that have already ended.
|
|
24
|
+
*/
|
|
25
|
+
private targetSpan;
|
|
19
26
|
private pickSpan;
|
|
20
27
|
private seed;
|
|
21
28
|
private flush;
|
|
@@ -19,6 +19,7 @@ const rxjs_1 = require("rxjs");
|
|
|
19
19
|
const operators_1 = require("rxjs/operators");
|
|
20
20
|
const opentelemetry_constants_1 = require("../opentelemetry.constants");
|
|
21
21
|
const wide_event_context_1 = require("./wide-event.context");
|
|
22
|
+
const wide_event_span_processor_1 = require("./wide-event.span-processor");
|
|
22
23
|
/**
|
|
23
24
|
* Opens a wide event for each request and flushes the accumulated
|
|
24
25
|
* attributes onto the span that was active when the request entered the
|
|
@@ -64,7 +65,7 @@ let WideEventInterceptor = class WideEventInterceptor {
|
|
|
64
65
|
bag.set("error.stack", error.stack);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
this.
|
|
68
|
+
this.targetSpan(rootSpan, activeSpan)?.setStatus({
|
|
68
69
|
code: api_1.SpanStatusCode.ERROR,
|
|
69
70
|
});
|
|
70
71
|
},
|
|
@@ -91,12 +92,22 @@ let WideEventInterceptor = class WideEventInterceptor {
|
|
|
91
92
|
return (request?.[wide_event_context_1.WIDE_EVENT_ROOT_SPAN] ??
|
|
92
93
|
request?.raw?.[wide_event_context_1.WIDE_EVENT_ROOT_SPAN]);
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Picks the span the wide event should be written to, preferring the
|
|
97
|
+
* local-root span (when the WideEventSpanProcessor is registered), then the
|
|
98
|
+
* middleware-captured root span, then the interceptor-time active span —
|
|
99
|
+
* skipping any that have already ended.
|
|
100
|
+
*/
|
|
101
|
+
targetSpan(rootSpan, activeSpan) {
|
|
102
|
+
const traceId = (activeSpan ?? rootSpan)?.spanContext().traceId;
|
|
103
|
+
const localRoot = traceId ? (0, wide_event_span_processor_1.getLocalRootSpan)(traceId) : undefined;
|
|
104
|
+
return this.pickSpan(localRoot, rootSpan, activeSpan);
|
|
105
|
+
}
|
|
106
|
+
pickSpan(...candidates) {
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (candidate?.isRecording()) {
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
100
111
|
}
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
@@ -121,7 +132,7 @@ let WideEventInterceptor = class WideEventInterceptor {
|
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
flush(bag, rootSpan, activeSpan) {
|
|
124
|
-
const span = this.
|
|
135
|
+
const span = this.targetSpan(rootSpan, activeSpan);
|
|
125
136
|
if (!span) {
|
|
126
137
|
return;
|
|
127
138
|
}
|
|
@@ -6,6 +6,7 @@ const rxjs_1 = require("rxjs");
|
|
|
6
6
|
const wide_event_context_1 = require("./wide-event.context");
|
|
7
7
|
const wide_event_interceptor_1 = require("./wide-event.interceptor");
|
|
8
8
|
const wide_event_service_1 = require("./wide-event.service");
|
|
9
|
+
const wide_event_span_processor_1 = require("./wide-event.span-processor");
|
|
9
10
|
class CatsController {
|
|
10
11
|
findAll() { }
|
|
11
12
|
}
|
|
@@ -238,6 +239,26 @@ describe("WideEventInterceptor", () => {
|
|
|
238
239
|
expect(active?.attributes["code.function.name"]).toBe("CatsController.findAll");
|
|
239
240
|
expect(ended?.attributes["nestjs_otel.wide_event"]).toBeUndefined();
|
|
240
241
|
});
|
|
242
|
+
it("should prefer the processor's local-root span over the active span", async () => {
|
|
243
|
+
// #given: a registered local-root span plus a nested active span in the
|
|
244
|
+
// same trace (as instrumentation-nestjs-core would create).
|
|
245
|
+
const processor = new wide_event_span_processor_1.WideEventSpanProcessor();
|
|
246
|
+
const tracer = api_1.trace.getTracer("test");
|
|
247
|
+
const rootSpan = tracer.startSpan("GET /actors");
|
|
248
|
+
processor.onStart(rootSpan, {});
|
|
249
|
+
const childContext = api_1.trace.setSpan(api_1.context.active(), rootSpan);
|
|
250
|
+
const nestedSpan = tracer.startSpan("ActorController.findAll", undefined, childContext);
|
|
251
|
+
await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), nestedSpan), async () => (0, rxjs_1.lastValueFrom)(interceptor.intercept(executionContext, callHandler(() => (0, rxjs_1.of)("ok")))));
|
|
252
|
+
nestedSpan.end();
|
|
253
|
+
rootSpan.end();
|
|
254
|
+
// #then: marker lands on the local-root span, not the nested span
|
|
255
|
+
const finished = traceExporter.getFinishedSpans();
|
|
256
|
+
const root = finished.find((s) => s.name === "GET /actors");
|
|
257
|
+
const nested = finished.find((s) => s.name === "ActorController.findAll");
|
|
258
|
+
expect(root?.attributes["nestjs_otel.wide_event"]).toBe(true);
|
|
259
|
+
expect(root?.attributes["code.function.name"]).toBe("CatsController.findAll");
|
|
260
|
+
expect(nested?.attributes["nestjs_otel.wide_event"]).toBeUndefined();
|
|
261
|
+
});
|
|
241
262
|
it("should propagate the handler result untouched when no span is active", async () => {
|
|
242
263
|
const result = await (0, rxjs_1.lastValueFrom)(interceptor.intercept(executionContext, callHandler(() => (0, rxjs_1.of)("ok"))));
|
|
243
264
|
expect(result).toBe("ok");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Context } from "@opentelemetry/api";
|
|
2
|
+
import type { ReadableSpan, Span, SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
/**
|
|
4
|
+
* The local-root span for the given trace (the span with no in-process
|
|
5
|
+
* parent — usually the HTTP server span created by instrumentation), or
|
|
6
|
+
* undefined when the WideEventSpanProcessor is not registered or the span has
|
|
7
|
+
* already ended.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function getLocalRootSpan(traceId: string): Span | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Tracks the local-root span of every trace so the WideEventInterceptor can
|
|
14
|
+
* flush accumulated attributes onto it instead of a nested instrumentation
|
|
15
|
+
* span (e.g. the per-request span created by instrumentation-nestjs-core or
|
|
16
|
+
* the per-phase spans created by @fastify/otel).
|
|
17
|
+
*
|
|
18
|
+
* Register it on your NodeSDK alongside the exporting processor:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* new NodeSDK({
|
|
22
|
+
* spanProcessors: [
|
|
23
|
+
* new WideEventSpanProcessor(),
|
|
24
|
+
* new BatchSpanProcessor(traceExporter),
|
|
25
|
+
* ],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @publicApi
|
|
30
|
+
*/
|
|
31
|
+
export declare class WideEventSpanProcessor implements SpanProcessor {
|
|
32
|
+
onStart(span: Span, _parentContext: Context): void;
|
|
33
|
+
onEnd(span: ReadableSpan): void;
|
|
34
|
+
forceFlush(): Promise<void>;
|
|
35
|
+
shutdown(): Promise<void>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WideEventSpanProcessor = void 0;
|
|
4
|
+
exports.getLocalRootSpan = getLocalRootSpan;
|
|
5
|
+
const localRootSpans = new Map();
|
|
6
|
+
const isLocalRoot = (span) => {
|
|
7
|
+
const parent = span.parentSpanContext;
|
|
8
|
+
return !parent || parent.isRemote === true;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The local-root span for the given trace (the span with no in-process
|
|
12
|
+
* parent — usually the HTTP server span created by instrumentation), or
|
|
13
|
+
* undefined when the WideEventSpanProcessor is not registered or the span has
|
|
14
|
+
* already ended.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
function getLocalRootSpan(traceId) {
|
|
19
|
+
return localRootSpans.get(traceId);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Tracks the local-root span of every trace so the WideEventInterceptor can
|
|
23
|
+
* flush accumulated attributes onto it instead of a nested instrumentation
|
|
24
|
+
* span (e.g. the per-request span created by instrumentation-nestjs-core or
|
|
25
|
+
* the per-phase spans created by @fastify/otel).
|
|
26
|
+
*
|
|
27
|
+
* Register it on your NodeSDK alongside the exporting processor:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* new NodeSDK({
|
|
31
|
+
* spanProcessors: [
|
|
32
|
+
* new WideEventSpanProcessor(),
|
|
33
|
+
* new BatchSpanProcessor(traceExporter),
|
|
34
|
+
* ],
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @publicApi
|
|
39
|
+
*/
|
|
40
|
+
class WideEventSpanProcessor {
|
|
41
|
+
onStart(span, _parentContext) {
|
|
42
|
+
if (isLocalRoot(span)) {
|
|
43
|
+
localRootSpans.set(span.spanContext().traceId, span);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
onEnd(span) {
|
|
47
|
+
if (isLocalRoot(span)) {
|
|
48
|
+
localRootSpans.delete(span.spanContext().traceId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
forceFlush() {
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
}
|
|
54
|
+
shutdown() {
|
|
55
|
+
localRootSpans.clear();
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.WideEventSpanProcessor = WideEventSpanProcessor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("@opentelemetry/api");
|
|
4
|
+
const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
|
|
5
|
+
const wide_event_span_processor_1 = require("./wide-event.span-processor");
|
|
6
|
+
describe("WideEventSpanProcessor", () => {
|
|
7
|
+
let processor;
|
|
8
|
+
let provider;
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
provider = new sdk_trace_node_1.NodeTracerProvider();
|
|
11
|
+
provider.register();
|
|
12
|
+
});
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
processor = new wide_event_span_processor_1.WideEventSpanProcessor();
|
|
15
|
+
});
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await provider.shutdown();
|
|
18
|
+
});
|
|
19
|
+
const startSpan = (name) => api_1.trace.getTracer("test").startSpan(name);
|
|
20
|
+
it("should register a parentless span as the local root for its trace", () => {
|
|
21
|
+
const root = startSpan("GET /test");
|
|
22
|
+
processor.onStart(root, api_1.context.active());
|
|
23
|
+
expect((0, wide_event_span_processor_1.getLocalRootSpan)(root.spanContext().traceId)).toBe(root);
|
|
24
|
+
root.end();
|
|
25
|
+
});
|
|
26
|
+
it("should not register a span whose parent is local", () => {
|
|
27
|
+
const root = startSpan("GET /test");
|
|
28
|
+
processor.onStart(root, api_1.context.active());
|
|
29
|
+
const child = api_1.trace
|
|
30
|
+
.getTracer("test")
|
|
31
|
+
.startSpan("child", undefined, api_1.trace.setSpan(api_1.context.active(), root));
|
|
32
|
+
processor.onStart(child, api_1.context.active());
|
|
33
|
+
// local root for the trace is still the root, not the child
|
|
34
|
+
expect((0, wide_event_span_processor_1.getLocalRootSpan)(root.spanContext().traceId)).toBe(root);
|
|
35
|
+
child.end();
|
|
36
|
+
root.end();
|
|
37
|
+
});
|
|
38
|
+
it("should drop the local root from the registry when it ends", () => {
|
|
39
|
+
const root = startSpan("GET /test");
|
|
40
|
+
const traceId = root.spanContext().traceId;
|
|
41
|
+
processor.onStart(root, api_1.context.active());
|
|
42
|
+
processor.onEnd(root);
|
|
43
|
+
expect((0, wide_event_span_processor_1.getLocalRootSpan)(traceId)).toBeUndefined();
|
|
44
|
+
root.end();
|
|
45
|
+
});
|
|
46
|
+
});
|
package/package.json
CHANGED
package/.claude/settings.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"PreToolUse": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "Bash",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"MANDATORY: graphify-out/graph.json exists. You MUST run `graphify query \\\"<question>\\\"` before grepping raw files. Only grep after graphify has oriented you, or to modify/debug specific lines.\"}}' || true ;; esac"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"matcher": "Read|Glob",
|
|
15
|
-
"hooks": [
|
|
16
|
-
{
|
|
17
|
-
"type": "command",
|
|
18
|
-
"command": "HIT=$(python3 -c \"import json,sys;d=json.load(sys.stdin);t=d.get('tool_input',d);s=(str(t.get('file_path') or '')+' '+str(t.get('pattern') or '')+' '+str(t.get('path') or '')).lower().replace(chr(92),'/');exts=('.py','.js','.ts','.tsx','.jsx','.go','.rs','.java','.rb','.c','.h','.cpp','.hpp','.cc','.cs','.kt','.swift','.php','.scala','.lua','.sh','.md','.rst','.txt','.mdx');sys.stdout.write('1' if 'graphify-out/' not in s and any(e in s for e in exts) else '')\" 2>/dev/null || true); if [ \"$HIT\" = 1 ] && [ -f graphify-out/graph.json ]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"MANDATORY: graphify-out/graph.json exists. You MUST run graphify before reading source files. Use: `graphify query \\\"<question>\\\"` (scoped subgraph), `graphify explain \\\"<concept>\\\"`, or `graphify path \\\"<A>\\\" \\\"<B>\\\"`. Only read raw files after graphify has oriented you, or to modify/debug specific lines. This rule applies to subagents too \u2014 include it in every subagent prompt involving code exploration.\"}}'; fi || true"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(cp tests/e2e/wide-events/wide-event.spec.ts /tmp/we.bak)",
|
|
5
|
-
"Bash(cp /tmp/we.bak tests/e2e/wide-events/wide-event.spec.ts)",
|
|
6
|
-
"Bash(graphify query *)",
|
|
7
|
-
"Bash(rtk npm *)",
|
|
8
|
-
"Bash(rtk jest *)",
|
|
9
|
-
"WebFetch(domain:jeremymorrell.dev)",
|
|
10
|
-
"WebFetch(domain:boristane.com)",
|
|
11
|
-
"WebFetch(domain:isburmistrov.substack.com)",
|
|
12
|
-
"Bash(rtk npx *)",
|
|
13
|
-
"Bash(rtk ls *)",
|
|
14
|
-
"Bash(rtk grep *)",
|
|
15
|
-
"Bash(rtk read *)",
|
|
16
|
-
"Bash(cd /Users/pragmaticivan/Code/pragmaticivan/nestjs-otel-prom-grafana-tempo)",
|
|
17
|
-
"Bash(rtk find *)"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
}
|
package/.tmp_build.out
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"0": "Metric Decorators & Data",
|
|
3
|
-
"1": "Span Decorator & Tests",
|
|
4
|
-
"2": "Module Bootstrap & Tracing",
|
|
5
|
-
"3": "Wide Events",
|
|
6
|
-
"4": "Traceable Decorator",
|
|
7
|
-
"5": "Counter Decorators & Metadata Utils",
|
|
8
|
-
"6": "Test Service Fixtures",
|
|
9
|
-
"7": "Metric Injection Tokens",
|
|
10
|
-
"8": "Baggage Decorator",
|
|
11
|
-
"9": "CurrentSpan Decorator"
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/Users/pragmaticivan/.local/share/mise/installs/python/3.14.6/bin/python3.14
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/Users/pragmaticivan/Code/pragmaticivan/nestjs-otel
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Graph Report - . (2026-06-15)
|
|
2
|
-
|
|
3
|
-
## Corpus Check
|
|
4
|
-
- Corpus is ~6,192 words - fits in a single context window. You may not need a graph.
|
|
5
|
-
|
|
6
|
-
## Summary
|
|
7
|
-
- 157 nodes · 286 edges · 10 communities (6 shown, 4 thin omitted)
|
|
8
|
-
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
|
|
9
|
-
- Token cost: 0 input · 0 output
|
|
10
|
-
|
|
11
|
-
## Community Hubs (Navigation)
|
|
12
|
-
- [[_COMMUNITY_Metric Decorators & Data|Metric Decorators & Data]]
|
|
13
|
-
- [[_COMMUNITY_Span Decorator & Tests|Span Decorator & Tests]]
|
|
14
|
-
- [[_COMMUNITY_Module Bootstrap & Tracing|Module Bootstrap & Tracing]]
|
|
15
|
-
- [[_COMMUNITY_Wide Events|Wide Events]]
|
|
16
|
-
- [[_COMMUNITY_Traceable Decorator|Traceable Decorator]]
|
|
17
|
-
- [[_COMMUNITY_Counter Decorators & Metadata Utils|Counter Decorators & Metadata Utils]]
|
|
18
|
-
- [[_COMMUNITY_Test Service Fixtures|Test Service Fixtures]]
|
|
19
|
-
- [[_COMMUNITY_Metric Injection Tokens|Metric Injection Tokens]]
|
|
20
|
-
- [[_COMMUNITY_Baggage Decorator|Baggage Decorator]]
|
|
21
|
-
- [[_COMMUNITY_CurrentSpan Decorator|CurrentSpan Decorator]]
|
|
22
|
-
|
|
23
|
-
## God Nodes (most connected - your core abstractions)
|
|
24
|
-
1. `TestSpan` - 17 edges
|
|
25
|
-
2. `OtelMetricOptions` - 12 edges
|
|
26
|
-
3. `OpenTelemetryModuleOptions` - 9 edges
|
|
27
|
-
4. `MetricService` - 9 edges
|
|
28
|
-
5. `getOrCreate()` - 8 edges
|
|
29
|
-
6. `OpenTelemetryCoreModule` - 8 edges
|
|
30
|
-
7. `WideEventService` - 8 edges
|
|
31
|
-
8. `OpenTelemetryModuleAsyncOptions` - 7 edges
|
|
32
|
-
9. `TestService` - 7 edges
|
|
33
|
-
10. `getWideEventBag()` - 7 edges
|
|
34
|
-
|
|
35
|
-
## Surprising Connections (you probably didn't know these)
|
|
36
|
-
- `InjectMetric()` --calls--> `getToken()` [EXTRACTED]
|
|
37
|
-
src/metrics/injector.ts → src/metrics/utils.ts
|
|
38
|
-
- `record()` --calls--> `getWideEventBag()` [EXTRACTED]
|
|
39
|
-
src/wide-events/wide-event-field.decorator.ts → src/wide-events/wide-event.context.ts
|
|
40
|
-
|
|
41
|
-
## Import Cycles
|
|
42
|
-
- None detected.
|
|
43
|
-
|
|
44
|
-
## Communities (10 total, 4 thin omitted)
|
|
45
|
-
|
|
46
|
-
### Community 0 - "Metric Decorators & Data"
|
|
47
|
-
Cohesion: 0.14
|
|
48
|
-
Nodes (21): MetricParamDecorator, OtelCounter, OtelGauge, OtelHistogram, OtelObservableCounter, OtelObservableGauge, OtelObservableUpDownCounter, OtelUpDownCounter (+13 more)
|
|
49
|
-
|
|
50
|
-
### Community 1 - "Span Decorator & Tests"
|
|
51
|
-
Cohesion: 0.08
|
|
52
|
-
Nodes (10): ExtendedSpanOptions, handleOnResult(), recordException(), Span(), SpanDecoratorOptions, LazyThenable, makeFailingLazyThenable(), makeLazyThenable() (+2 more)
|
|
53
|
-
|
|
54
|
-
### Community 2 - "Module Bootstrap & Tracing"
|
|
55
|
-
Cohesion: 0.15
|
|
56
|
-
Nodes (8): OpenTelemetryMetrics, OpenTelemetryModuleAsyncOptions, OpenTelemetryModuleOptions, OpenTelemetryOptionsFactory, OpenTelemetryWideEvents, OpenTelemetryCoreModule, OpenTelemetryModule, TraceService
|
|
57
|
-
|
|
58
|
-
### Community 3 - "Wide Events"
|
|
59
|
-
Cohesion: 0.13
|
|
60
|
-
Nodes (10): getWideEventBag(), WIDE_EVENT_CONTEXT_KEY, WideEventBag, isAttributeValue(), record(), WideEventField(), CatsController, executionContext (+2 more)
|
|
61
|
-
|
|
62
|
-
### Community 4 - "Traceable Decorator"
|
|
63
|
-
Cohesion: 0.22
|
|
64
|
-
Nodes (3): TestTraceable, TestTraceableWithOptions, Traceable()
|
|
65
|
-
|
|
66
|
-
### Community 5 - "Counter Decorators & Metadata Utils"
|
|
67
|
-
Cohesion: 0.27
|
|
68
|
-
Nodes (4): OtelInstanceCounter(), OtelMethodCounter(), TestClass, copyMetadataFromFunctionToFunction()
|
|
69
|
-
|
|
70
|
-
## Knowledge Gaps
|
|
71
|
-
- **20 isolated node(s):** `OpenTelemetryMetrics`, `OpenTelemetryWideEvents`, `MetricParamDecorator`, `OtelCounter`, `OtelUpDownCounter` (+15 more)
|
|
72
|
-
These have ≤1 connection - possible missing edges or undocumented components.
|
|
73
|
-
- **4 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
|
74
|
-
|
|
75
|
-
## Suggested Questions
|
|
76
|
-
_Questions this graph is uniquely positioned to answer:_
|
|
77
|
-
|
|
78
|
-
- **Why does `TestService` connect `Test Service Fixtures` to `Wide Events`?**
|
|
79
|
-
_High betweenness centrality (0.076) - this node is a cross-community bridge._
|
|
80
|
-
- **What connects `OpenTelemetryMetrics`, `OpenTelemetryWideEvents`, `MetricParamDecorator` to the rest of the system?**
|
|
81
|
-
_20 weakly-connected nodes found - possible documentation gaps or missing edges._
|
|
82
|
-
- **Should `Metric Decorators & Data` be split into smaller, more focused modules?**
|
|
83
|
-
_Cohesion score 0.13636363636363635 - nodes in this community are weakly interconnected._
|
|
84
|
-
- **Should `Span Decorator & Tests` be split into smaller, more focused modules?**
|
|
85
|
-
_Cohesion score 0.08374384236453201 - nodes in this community are weakly interconnected._
|
|
86
|
-
- **Should `Module Bootstrap & Tracing` be split into smaller, more focused modules?**
|
|
87
|
-
_Cohesion score 0.14814814814814814 - nodes in this community are weakly interconnected._
|
|
88
|
-
- **Should `Wide Events` be split into smaller, more focused modules?**
|
|
89
|
-
_Cohesion score 0.1282051282051282 - nodes in this community are weakly interconnected._
|