@nwire/observability 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Gefter / 200apps Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @nwire/observability
2
+
3
+ > Dispatch tracing — logger-backed by default, OTEL bridge when `@opentelemetry/api` is present.
4
+
5
+ ## What it does
6
+
7
+ Wraps every dispatch in a span. By default emits two structured log lines per dispatch (`dispatch.start` / `dispatch.end` with `durationMs` + ok/error) because `ctx.logger` already carries envelope ids. If consumers pass an OTEL `tracer` the middleware ALSO opens a span around `next()` — `@opentelemetry/api` stays an optional peer.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @nwire/observability
13
+ # optional:
14
+ pnpm add @opentelemetry/api
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { tracingPlugin } from "@nwire/observability";
21
+ import { defineApp } from "@nwire/forge";
22
+ import { trace } from "@opentelemetry/api"; // optional
23
+
24
+ defineApp("my-app", {
25
+ plugins: [tracingPlugin({ tracer: trace.getTracer("my-app") })],
26
+ });
27
+ ```
28
+
29
+ Or as middleware:
30
+
31
+ ```ts
32
+ import { tracingMiddleware } from "@nwire/observability";
33
+ app.runtime.use(tracingMiddleware());
34
+ ```
35
+
36
+ ## API surface
37
+
38
+ - `tracingPlugin({ tracer?, alwaysLog? })` — `PluginDefinition` form.
39
+ - `tracingMiddleware({ tracer?, alwaysLog? })` — `DispatchMiddleware` form for ad-hoc wiring.
40
+
41
+ ## When to use
42
+
43
+ Every production app. Fits L2 and up. Pair with `@nwire/telemetry-otel` for OTLP export to Tempo/Honeycomb/Datadog/GreptimeDB.
44
+
45
+ ## Standalone use
46
+
47
+ For developers using `@nwire/observability` **without the rest of Nwire** — pair it with any TypeScript project, any container, any HTTP framework.
48
+
49
+ ```ts
50
+ // See the package's main entry (src/) for the standalone surface.
51
+ // The exports below work without @nwire/app or @nwire/forge.
52
+ import {} from /* ...standalone exports... */ "@nwire/observability";
53
+ ```
54
+
55
+ ## Within nwire-app
56
+
57
+ For developers using this package as part of the Nwire stack — register it via `app.use(...)` or it auto-wires when you compose `createApp({ modules })`.
58
+
59
+ ```ts
60
+ import { createApp } from "@nwire/forge";
61
+
62
+ const app = createApp({
63
+ /* ...config... */
64
+ });
65
+ // Adapter/plugin wiring happens here when applicable.
66
+ ```
67
+
68
+ ## See also
69
+
70
+ - [Architecture sketch §05 — Adapters tier](../../architecture-sketch.html#packages)
71
+ - Sibling packages: [@nwire/logger-pino](../nwire-logger-pino), [@nwire/telemetry-otel](../nwire-telemetry-otel)
@@ -0,0 +1,7 @@
1
+ /**
2
+ * tracingMiddleware contract — logs start/end per dispatch, surfaces errors,
3
+ * runs once per dispatch even with retries, and bridges to an OTEL tracer
4
+ * when one is provided.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=observability.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/observability.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * tracingMiddleware contract — logs start/end per dispatch, surfaces errors,
3
+ * runs once per dispatch even with retries, and bridges to an OTEL tracer
4
+ * when one is provided.
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import { z } from "zod";
8
+ import { defineAction, defineHandler, Runtime } from "@nwire/forge";
9
+ import { tracingMiddleware } from "../observability.js";
10
+ class CapturingLogger {
11
+ entries = [];
12
+ debug(message, fields) {
13
+ this.entries.push({ level: "debug", message, fields });
14
+ }
15
+ info(message, fields) {
16
+ this.entries.push({ level: "info", message, fields });
17
+ }
18
+ warn(message, fields) {
19
+ this.entries.push({ level: "warn", message, fields });
20
+ }
21
+ error(message, fields) {
22
+ this.entries.push({ level: "error", message, fields });
23
+ }
24
+ child() {
25
+ return this;
26
+ }
27
+ }
28
+ describe("tracingMiddleware", () => {
29
+ it("logs dispatch.start and dispatch.end with durationMs on success", async () => {
30
+ const logger = new CapturingLogger();
31
+ const action = defineAction({ name: "obs.ok", schema: z.object({}) });
32
+ const handler = defineHandler(action, async () => undefined);
33
+ const runtime = new Runtime({ logger });
34
+ runtime.registerHandler(handler);
35
+ runtime.use(tracingMiddleware());
36
+ await runtime.dispatch(action, {});
37
+ const obs = logger.entries.filter((e) => e.message.startsWith("dispatch."));
38
+ expect(obs.map((e) => e.message)).toEqual(["dispatch.start", "dispatch.end"]);
39
+ expect(obs[1]?.fields?.ok).toBe(true);
40
+ expect(typeof obs[1]?.fields?.durationMs).toBe("number");
41
+ expect(obs[0]?.fields?.action).toBe("obs.ok");
42
+ });
43
+ it("logs dispatch.end with ok:false and rethrows on handler error", async () => {
44
+ const logger = new CapturingLogger();
45
+ const action = defineAction({ name: "obs.boom", schema: z.object({}) });
46
+ const handler = defineHandler(action, async () => {
47
+ throw new Error("kaboom");
48
+ });
49
+ const runtime = new Runtime({ logger });
50
+ runtime.registerHandler(handler);
51
+ runtime.use(tracingMiddleware());
52
+ await expect(runtime.dispatch(action, {})).rejects.toThrow(/kaboom/);
53
+ const end = logger.entries.find((e) => e.message === "dispatch.end");
54
+ expect(end?.level).toBe("error");
55
+ expect(end?.fields?.ok).toBe(false);
56
+ expect(end?.fields?.error).toBe("kaboom");
57
+ });
58
+ it("runs exactly once per dispatch despite retries", async () => {
59
+ const logger = new CapturingLogger();
60
+ const action = defineAction({
61
+ name: "obs.retry",
62
+ schema: z.object({}),
63
+ retry: { max: 2, backoff: "fixed", baseDelayMs: 1 },
64
+ });
65
+ let calls = 0;
66
+ const handler = defineHandler(action, async () => {
67
+ calls++;
68
+ if (calls < 3)
69
+ throw new Error("flake");
70
+ return undefined;
71
+ });
72
+ const runtime = new Runtime({ logger });
73
+ runtime.registerHandler(handler);
74
+ runtime.use(tracingMiddleware());
75
+ await runtime.dispatch(action, {});
76
+ expect(calls).toBe(3);
77
+ const starts = logger.entries.filter((e) => e.message === "dispatch.start");
78
+ const ends = logger.entries.filter((e) => e.message === "dispatch.end");
79
+ expect(starts).toHaveLength(1);
80
+ expect(ends).toHaveLength(1);
81
+ expect(ends[0]?.fields?.ok).toBe(true);
82
+ });
83
+ it("opens an OTEL span when a tracer is provided", async () => {
84
+ const logger = new CapturingLogger();
85
+ const action = defineAction({ name: "obs.otel", schema: z.object({}) });
86
+ const handler = defineHandler(action, async () => undefined);
87
+ const spanCalls = [];
88
+ const fakeTracer = {
89
+ startActiveSpan: async (name, fn) => {
90
+ spanCalls.push(`start:${name}`);
91
+ const span = {
92
+ setAttribute: (k, v) => spanCalls.push(`attr:${k}=${String(v)}`),
93
+ recordException: () => spanCalls.push("exception"),
94
+ setStatus: (s) => spanCalls.push(`status:${s.code}`),
95
+ end: () => spanCalls.push("end"),
96
+ };
97
+ return fn(span);
98
+ },
99
+ };
100
+ const runtime = new Runtime({ logger });
101
+ runtime.registerHandler(handler);
102
+ runtime.use(tracingMiddleware({ tracer: fakeTracer }));
103
+ await runtime.dispatch(action, {});
104
+ expect(spanCalls[0]).toBe("start:dispatch obs.otel");
105
+ expect(spanCalls).toContain("attr:nwire.action=obs.otel");
106
+ expect(spanCalls).toContain("status:1");
107
+ expect(spanCalls[spanCalls.length - 1]).toBe("end");
108
+ });
109
+ });
110
+ //# sourceMappingURL=observability.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.test.js","sourceRoot":"","sources":["../../src/__tests__/observability.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `@nwire/observability` — dispatch tracing.
3
+ *
4
+ * Wraps every dispatch in a logger-backed span (entry + exit + durationMs
5
+ * + ok/error). If consumers pass an OTEL `tracer` the middleware ALSO
6
+ * opens an OTEL span — `@opentelemetry/api` stays an optional peer.
7
+ * Plugin and middleware forms both ship.
8
+ *
9
+ * See: architecture-sketch.html §05 (Adapters tier).
10
+ */
11
+ import type { DispatchMiddleware, PluginDefinition } from "@nwire/forge";
12
+ export interface TracingOptions {
13
+ /**
14
+ * If true (default), logs `dispatch.start` and `dispatch.end` per
15
+ * dispatch. Set false to emit only on errors.
16
+ */
17
+ readonly logEachDispatch?: boolean;
18
+ /**
19
+ * Optional OTEL `Tracer` instance. When provided, an OTEL span wraps
20
+ * `next()`. We type it as `unknown` and probe at runtime so
21
+ * @opentelemetry/api stays optional.
22
+ */
23
+ readonly tracer?: unknown;
24
+ /** Span name builder. Default: `dispatch <action.name>`. */
25
+ readonly spanName?: (actionName: string) => string;
26
+ }
27
+ export declare function tracingMiddleware(options?: TracingOptions): DispatchMiddleware;
28
+ /**
29
+ * Plugin form — the recommended shape for new code.
30
+ * createApp({ plugins: [tracingPlugin({ tracer })] })
31
+ */
32
+ export declare function tracingPlugin(options?: TracingOptions): PluginDefinition;
33
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGzE,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;CACpD;AAYD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,cAAmB,GAAG,kBAAkB,CAwDlF;AASD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,gBAAgB,CAI5E"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `@nwire/observability` — dispatch tracing.
3
+ *
4
+ * Wraps every dispatch in a logger-backed span (entry + exit + durationMs
5
+ * + ok/error). If consumers pass an OTEL `tracer` the middleware ALSO
6
+ * opens an OTEL span — `@opentelemetry/api` stays an optional peer.
7
+ * Plugin and middleware forms both ship.
8
+ *
9
+ * See: architecture-sketch.html §05 (Adapters tier).
10
+ */
11
+ import { definePlugin } from "@nwire/forge";
12
+ export function tracingMiddleware(options = {}) {
13
+ const logEach = options.logEachDispatch ?? true;
14
+ const spanName = options.spanName ?? ((name) => `dispatch ${name}`);
15
+ const tracer = options.tracer;
16
+ return async (next, action, _input, ctx) => {
17
+ const start = nowMs();
18
+ if (logEach) {
19
+ ctx.logger.info("dispatch.start", { action: action.name });
20
+ }
21
+ const run = async () => {
22
+ try {
23
+ const result = await next();
24
+ if (logEach) {
25
+ ctx.logger.info("dispatch.end", {
26
+ action: action.name,
27
+ durationMs: nowMs() - start,
28
+ ok: true,
29
+ });
30
+ }
31
+ return result;
32
+ }
33
+ catch (err) {
34
+ ctx.logger.error("dispatch.end", {
35
+ action: action.name,
36
+ durationMs: nowMs() - start,
37
+ ok: false,
38
+ error: err?.message,
39
+ });
40
+ throw err;
41
+ }
42
+ };
43
+ if (!tracer || typeof tracer.startActiveSpan !== "function") {
44
+ return run();
45
+ }
46
+ return tracer.startActiveSpan(spanName(action.name), async (span) => {
47
+ try {
48
+ span.setAttribute("nwire.action", action.name);
49
+ if (ctx.envelope.tenant)
50
+ span.setAttribute("nwire.tenant", ctx.envelope.tenant);
51
+ if (ctx.envelope.userId)
52
+ span.setAttribute("nwire.user_id", ctx.envelope.userId);
53
+ span.setAttribute("nwire.message_id", ctx.envelope.messageId);
54
+ span.setAttribute("nwire.correlation_id", ctx.envelope.correlationId);
55
+ const result = await run();
56
+ span.setStatus({ code: 1 /* OK */ });
57
+ return result;
58
+ }
59
+ catch (err) {
60
+ span.recordException(err);
61
+ span.setStatus({ code: 2 /* ERROR */, message: err?.message });
62
+ throw err;
63
+ }
64
+ finally {
65
+ span.end();
66
+ }
67
+ });
68
+ };
69
+ }
70
+ function nowMs() {
71
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
72
+ return performance.now();
73
+ }
74
+ return Date.now();
75
+ }
76
+ /**
77
+ * Plugin form — the recommended shape for new code.
78
+ * createApp({ plugins: [tracingPlugin({ tracer })] })
79
+ */
80
+ export function tracingPlugin(options = {}) {
81
+ return definePlugin("observability", {
82
+ middleware: [tracingMiddleware(options)],
83
+ });
84
+ }
85
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@nwire/observability",
3
+ "version": "0.7.0",
4
+ "description": "Nwire — dispatch tracing middleware. Logger-backed by default, with optional bridge to @opentelemetry/api (declared by the consumer, not by us).",
5
+ "keywords": [
6
+ "middleware",
7
+ "nwire",
8
+ "observability",
9
+ "otel",
10
+ "tracing"
11
+ ],
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "type": "module",
17
+ "main": "./dist/observability.js",
18
+ "types": "./dist/observability.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/observability.js",
22
+ "types": "./dist/observability.d.ts"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@nwire/forge": "0.7.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.19.9",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "^4.0.18"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
38
+ "dev": "tsc --watch",
39
+ "typecheck": "tsc --noEmit"
40
+ }
41
+ }