brass-runtime 1.15.0 → 1.16.1
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/CHANGELOG.md +17 -0
- package/README.md +673 -136
- package/dist/agent/cli/main.cjs +40 -35
- package/dist/agent/cli/main.js +9 -4
- package/dist/agent/cli/main.mjs +9 -4
- package/dist/agent/index.cjs +8 -4
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +7 -3
- package/dist/agent/index.mjs +7 -3
- package/dist/chunk-2HQTDLHF.mjs +683 -0
- package/dist/chunk-36I3M4UC.mjs +370 -0
- package/dist/chunk-3AYM6WPJ.js +1629 -0
- package/dist/chunk-3LOYJFRR.cjs +300 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3Y2RIUMM.js +300 -0
- package/dist/{chunk-VEZNF5GZ.cjs → chunk-4ROBZFL6.cjs} +130 -126
- package/dist/{chunk-3QMOKAS5.js → chunk-52OB2ROS.js} +9 -5
- package/dist/chunk-52PPNNI4.cjs +416 -0
- package/dist/chunk-5EC274J5.cjs +2874 -0
- package/dist/chunk-5QC7LRZ3.js +229 -0
- package/dist/chunk-5VRJNBLZ.mjs +2874 -0
- package/dist/chunk-62AZW6UT.cjs +313 -0
- package/dist/chunk-6IXXWIUM.js +683 -0
- package/dist/chunk-74ZTY6CP.js +2871 -0
- package/dist/chunk-76YMRMH2.cjs +777 -0
- package/dist/chunk-7CMJS3QE.mjs +2871 -0
- package/dist/{chunk-4NHES7VK.mjs → chunk-7JIJOVCT.js} +27 -13
- package/dist/chunk-A2OM6NEH.mjs +194 -0
- package/dist/chunk-AGR5B2BC.cjs +683 -0
- package/dist/chunk-AVNQLJ5V.js +777 -0
- package/dist/chunk-B33ICAKP.js +313 -0
- package/dist/{chunk-ELOOF35R.mjs → chunk-B5JD23U7.mjs} +1 -1
- package/dist/chunk-BABBZK4Y.js +2024 -0
- package/dist/chunk-C3MDXTRZ.js +354 -0
- package/dist/chunk-CIZFIMK5.js +2193 -0
- package/dist/chunk-CZIVE6NT.cjs +354 -0
- package/dist/chunk-DNFJLJMW.mjs +354 -0
- package/dist/chunk-DNFO2EIZ.mjs +777 -0
- package/dist/chunk-EJ6BPYVR.mjs +416 -0
- package/dist/chunk-ENKODRU3.cjs +2193 -0
- package/dist/chunk-EOC4UHBS.mjs +229 -0
- package/dist/{chunk-BMH5AV44.js → chunk-FH2X7BVP.js} +756 -440
- package/dist/{chunk-PPUXIH5R.js → chunk-FHQGHPMO.mjs} +27 -13
- package/dist/{chunk-TGIFUAK4.cjs → chunk-GLE2WY7Z.cjs} +951 -635
- package/dist/{chunk-BDF4AMWX.mjs → chunk-GYM3LLGS.mjs} +756 -440
- package/dist/chunk-HLWLMW2F.mjs +2024 -0
- package/dist/chunk-JF5WGYJJ.cjs +194 -0
- package/dist/chunk-KH4SYAOS.mjs +1629 -0
- package/dist/chunk-KN32XNTH.mjs +313 -0
- package/dist/chunk-KQLYONSE.cjs +2871 -0
- package/dist/{chunk-STVLQ3XD.cjs → chunk-KZJQ723N.cjs} +92 -78
- package/dist/chunk-L2SYFEBS.js +194 -0
- package/dist/chunk-L6VB5N7Q.cjs +104 -0
- package/dist/{chunk-K6M7MDZ4.mjs → chunk-MBEJI5HF.mjs} +9 -5
- package/dist/chunk-MIIYDLGM.js +2874 -0
- package/dist/chunk-MOO4L7F4.mjs +104 -0
- package/dist/chunk-MT3OWDPC.mjs +2193 -0
- package/dist/chunk-MVGUEJ5Z.cjs +370 -0
- package/dist/chunk-OBGZSXTJ.cjs +10 -0
- package/dist/chunk-PD4EJTQC.cjs +229 -0
- package/dist/chunk-PWC3RBQE.mjs +300 -0
- package/dist/chunk-Q2I37RP3.cjs +1629 -0
- package/dist/chunk-RKGKFN2A.js +416 -0
- package/dist/{chunk-R3R2FVLG.cjs → chunk-SA6HUJVI.cjs} +5 -5
- package/dist/chunk-TRM4JUZQ.js +104 -0
- package/dist/chunk-UB4B6OFY.js +370 -0
- package/dist/{chunk-TO7IKXYT.js → chunk-UCUBNWM2.js} +1 -1
- package/dist/chunk-VN44DYYT.cjs +2024 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/client-CZHU674n.d.ts +820 -0
- package/dist/core/index.cjs +198 -4
- package/dist/core/index.d.ts +311 -212
- package/dist/core/index.js +237 -43
- package/dist/core/index.mjs +237 -43
- package/dist/{effect-CMOQKX8y.d.ts → effect-DIUHZ9IN.d.ts} +195 -1
- package/dist/effectRunner-CFLC32IK.cjs +8 -0
- package/dist/effectRunner-L4S7IPT3.js +8 -0
- package/dist/effectRunner-NNGG75QA.mjs +8 -0
- package/dist/http/index.cjs +1227 -2971
- package/dist/http/index.d.ts +826 -280
- package/dist/http/index.js +1089 -2833
- package/dist/http/index.mjs +1089 -2833
- package/dist/http/testing.cjs +161 -0
- package/dist/http/testing.d.ts +43 -0
- package/dist/http/testing.js +161 -0
- package/dist/http/testing.mjs +161 -0
- package/dist/index.cjs +486 -250
- package/dist/index.d.ts +87 -95
- package/dist/index.js +391 -155
- package/dist/index.mjs +391 -155
- package/dist/observability/index.cjs +162 -0
- package/dist/observability/index.d.ts +152 -0
- package/dist/observability/index.js +162 -0
- package/dist/observability/index.mjs +162 -0
- package/dist/perf/cli.cjs +401 -0
- package/dist/perf/cli.d.ts +1 -0
- package/dist/perf/cli.js +401 -0
- package/dist/perf/cli.mjs +401 -0
- package/dist/perf/index.cjs +141 -0
- package/dist/perf/index.d.ts +483 -0
- package/dist/perf/index.js +141 -0
- package/dist/perf/index.mjs +141 -0
- package/dist/schedule-CK3Ml_7p.d.ts +259 -0
- package/dist/schema/index.cjs +29 -0
- package/dist/schema/index.d.ts +179 -0
- package/dist/schema/index.js +29 -0
- package/dist/schema/index.mjs +29 -0
- package/dist/server-GJPg8ZSG.d.ts +675 -0
- package/dist/{stream-FQm9h4Mg.d.ts → stream-B4oK9JFP.d.ts} +1 -1
- package/dist/tracer-Hwt1cl7h.d.ts +189 -0
- package/dist/tracing-DqbTKGcf.d.ts +148 -0
- package/docs/ARCHITECTURE.md +292 -0
- package/docs/README.md +63 -0
- package/docs/adr/0001-ai-context-pack.md +32 -0
- package/docs/agent-apply-mode.md +104 -0
- package/docs/agent-approvals.md +110 -0
- package/docs/agent-batch.md +185 -0
- package/docs/agent-boundaries.md +112 -0
- package/docs/agent-chat-sessions.md +160 -0
- package/docs/agent-ci.md +17 -0
- package/docs/agent-cli.md +405 -0
- package/docs/agent-config.md +480 -0
- package/docs/agent-context-discovery.md +159 -0
- package/docs/agent-copilot-like-dx.md +126 -0
- package/docs/agent-declarative-optimized-planning.md +138 -0
- package/docs/agent-dx.md +224 -0
- package/docs/agent-env-files.md +126 -0
- package/docs/agent-follow-up-context.md +43 -0
- package/docs/agent-global-usage.md +180 -0
- package/docs/agent-init.md +109 -0
- package/docs/agent-install-and-configure.md +516 -0
- package/docs/agent-language-workspace-ux.md +99 -0
- package/docs/agent-llm-adapters.md +123 -0
- package/docs/agent-local-install.md +190 -0
- package/docs/agent-local-tests.md +51 -0
- package/docs/agent-observability.md +155 -0
- package/docs/agent-patch-quality-loop.md +162 -0
- package/docs/agent-presets.md +22 -0
- package/docs/agent-project-commands.md +237 -0
- package/docs/agent-project-intelligence.md +156 -0
- package/docs/agent-redaction.md +18 -0
- package/docs/agent-release-readiness.md +76 -0
- package/docs/agent-rollback-safety.md +162 -0
- package/docs/agent-rollback.md +23 -0
- package/docs/agent-run-artifacts.md +16 -0
- package/docs/agent-vscode-auto-discovery.md +137 -0
- package/docs/agent-vscode-batch-runner.md +100 -0
- package/docs/agent-vscode-chat-layout.md +90 -0
- package/docs/agent-vscode-clean-install.md +147 -0
- package/docs/agent-vscode-code-actions.md +70 -0
- package/docs/agent-vscode-diff-preview.md +45 -0
- package/docs/agent-vscode-inline-assist.md +56 -0
- package/docs/agent-vscode-install.md +186 -0
- package/docs/agent-vscode-model-setup.md +97 -0
- package/docs/agent-vscode-patch-preview.md +92 -0
- package/docs/agent-vscode-problems.md +79 -0
- package/docs/agent-vscode-project-dashboard.md +106 -0
- package/docs/agent-vscode-run-history.md +92 -0
- package/docs/agent-vscode-ux.md +73 -0
- package/docs/ai/INVARIANTS.md +84 -0
- package/docs/ai/PROJECT_MAP.md +338 -0
- package/docs/ai/PUBLIC_API.md +336 -0
- package/docs/ai/VALIDATION_MATRIX.md +67 -0
- package/docs/api-polish.md +37 -0
- package/docs/cancellation.md +162 -0
- package/docs/coverage.md +46 -0
- package/docs/getting-started.md +159 -0
- package/docs/guides/README.md +40 -0
- package/docs/guides/circuit-breaker.md +89 -0
- package/docs/guides/error-handling.md +91 -0
- package/docs/guides/getting-started.md +107 -0
- package/docs/guides/layers.md +189 -0
- package/docs/guides/metrics.md +101 -0
- package/docs/guides/resource-management.md +141 -0
- package/docs/guides/retry.md +215 -0
- package/docs/guides/semaphore.md +66 -0
- package/docs/guides/streams.md +117 -0
- package/docs/guides/supervisors.md +98 -0
- package/docs/guides/testing.md +162 -0
- package/docs/guides/tracing.md +71 -0
- package/docs/http-recipes.md +399 -0
- package/docs/http.md +749 -0
- package/docs/modules.md +285 -0
- package/docs/observability-collector-smoke.md +31 -0
- package/docs/observability-framework-examples.md +98 -0
- package/docs/observability.md +542 -0
- package/docs/otel-collector-smoke.yaml +27 -0
- package/docs/performance-profiler.md +199 -0
- package/docs/production-readiness.md +73 -0
- package/docs/recipes/README.md +12 -0
- package/docs/recipes/http-server.md +45 -0
- package/docs/recipes/layers.md +44 -0
- package/docs/recipes/performance.md +47 -0
- package/docs/recipes/runtime.md +41 -0
- package/docs/recipes/testing.md +41 -0
- package/docs/release.md +53 -0
- package/docs/wasm-bounded-queues.md +44 -0
- package/docs/wasm-engine-observability-benchmarks.md +85 -0
- package/docs/wasm-fiber-engine.md +117 -0
- package/docs/wasm-scheduler-state-machine.md +122 -0
- package/docs/wasm-stream-chunks.md +54 -0
- package/package.json +48 -2
- package/dist/chunk-AR22SXML.js +0 -1043
- package/dist/chunk-BDYEENHT.js +0 -224
- package/dist/chunk-JFPU5GQI.mjs +0 -1043
- package/dist/chunk-MS34J5LY.cjs +0 -224
- package/dist/chunk-UMAZLXAB.mjs +0 -224
- package/dist/chunk-XPZNXSVN.cjs +0 -1043
- package/dist/tracing-DNT9jEbr.d.ts +0 -106
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# 🔭 Observability: Hooks, Events, and Tracing
|
|
2
|
+
|
|
3
|
+
`brass-runtime` exposes **RuntimeHooks** to emit runtime events (fibers,
|
|
4
|
+
scopes, supervisors, logs) and connect sinks (console, in-memory, exporters).
|
|
5
|
+
|
|
6
|
+
Public observability helpers are exported from `brass-runtime/core` and, for
|
|
7
|
+
root compatibility, from `brass-runtime`.
|
|
8
|
+
|
|
9
|
+
Production exporters live under `brass-runtime/observability`.
|
|
10
|
+
|
|
11
|
+
This doc covers:
|
|
12
|
+
|
|
13
|
+
- which events exist
|
|
14
|
+
- what `RuntimeEmitContext` is
|
|
15
|
+
- how to fan-out sinks without blocking the runtime
|
|
16
|
+
- practical patterns for tracing (`traceId`, `spanId`) and structured logging
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Mental model: “emit is a controlled side-effect”
|
|
21
|
+
|
|
22
|
+
In a ZIO-style runtime the computation core aims to stay pure, but we still need:
|
|
23
|
+
|
|
24
|
+
- logs
|
|
25
|
+
- tracing
|
|
26
|
+
- latency / spans / scope lifecycle visibility
|
|
27
|
+
|
|
28
|
+
So we route side-effects through a small interface:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export interface RuntimeHooks {
|
|
32
|
+
emit(ev: RuntimeEvent, ctx: RuntimeEmitContext): void;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The runtime calls `hooks.emit(...)` at well-defined points: fiber start/end,
|
|
37
|
+
scope open/close, supervisor child start/end/restart/escalation, logs, and
|
|
38
|
+
spans.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## RuntimeEvent + RuntimeEmitContext
|
|
43
|
+
|
|
44
|
+
A good split is:
|
|
45
|
+
|
|
46
|
+
- `RuntimeEvent`: what happened (the “what”)
|
|
47
|
+
- `RuntimeEmitContext`: current contextual info (the “where/with what trace”)
|
|
48
|
+
|
|
49
|
+
Useful context fields:
|
|
50
|
+
|
|
51
|
+
- `fiberId`, `scopeId`
|
|
52
|
+
- `traceId`, `spanId`
|
|
53
|
+
- `parentSpanId`
|
|
54
|
+
- `traceState`
|
|
55
|
+
|
|
56
|
+
Most sinks want the **merged** view, so it’s convenient to define a record:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export type RuntimeEventRecord = RuntimeEvent & RuntimeEmitContext & {
|
|
60
|
+
seq: number;
|
|
61
|
+
wallTs: number;
|
|
62
|
+
ts: number;
|
|
63
|
+
contextFiberId?: number;
|
|
64
|
+
contextScopeId?: number;
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When an event has its own `fiberId` or `scopeId`, that event payload wins in
|
|
69
|
+
the merged record. `contextFiberId` and `contextScopeId` preserve the ambient
|
|
70
|
+
runtime context for sinks that need it.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## EventBus: fan-out without blocking
|
|
75
|
+
|
|
76
|
+
If you have multiple sinks (console, in-memory tracer, exporter), avoid calling each sink inline from the runtime—slow sinks can stall execution.
|
|
77
|
+
|
|
78
|
+
Recommended pattern:
|
|
79
|
+
|
|
80
|
+
1) `EventBus` implements `RuntimeHooks`
|
|
81
|
+
2) `emit()` enqueues events (ring buffer)
|
|
82
|
+
3) `flush()` drains with a budget (microtask) and calls subscribers
|
|
83
|
+
|
|
84
|
+
This decouples runtime execution from sink speed.
|
|
85
|
+
|
|
86
|
+
Runtime hook sinks can be attached directly:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { EventBus, InMemoryTracer, RuntimeRegistry, consoleJsonLogger } from "brass-runtime/core";
|
|
90
|
+
|
|
91
|
+
const bus = new EventBus();
|
|
92
|
+
bus.subscribeHooks(consoleJsonLogger());
|
|
93
|
+
bus.subscribeHooks(new InMemoryTracer());
|
|
94
|
+
bus.subscribeHooks(new RuntimeRegistry());
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Observability Export
|
|
100
|
+
|
|
101
|
+
Use `brass-runtime/observability` when runtime signals need to leave the
|
|
102
|
+
process for dashboards, collectors, or log pipelines.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Runtime } from "brass-runtime/core";
|
|
106
|
+
import { makeDefaultHttpClient } from "brass-runtime/http";
|
|
107
|
+
import { logEffect, makeObservability, makeRequestObservabilityContext, withHttpObservability, withLogContext, withSpan } from "brass-runtime/observability";
|
|
108
|
+
|
|
109
|
+
const obs = makeObservability({
|
|
110
|
+
serviceName: "api",
|
|
111
|
+
serviceVersion: "1.2.3",
|
|
112
|
+
logs: { minLevel: "info" },
|
|
113
|
+
otlp: {
|
|
114
|
+
metricsUrl: "http://collector:4318/v1/metrics",
|
|
115
|
+
tracesUrl: "http://collector:4318/v1/traces",
|
|
116
|
+
logsUrl: "http://collector:4318/v1/logs",
|
|
117
|
+
},
|
|
118
|
+
flushIntervalMs: 10_000,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const runtime = new Runtime({ env: obs.env, hooks: obs.hooks });
|
|
122
|
+
const http = makeDefaultHttpClient({
|
|
123
|
+
baseUrl: "https://api.example.com",
|
|
124
|
+
middleware: [withHttpObservability(obs)],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const program = withSpan(
|
|
128
|
+
"request",
|
|
129
|
+
withLogContext({ requestId: "req-1" }, logEffect("info", "accepted"))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await runtime.toPromise(program);
|
|
133
|
+
await runtime.toPromise(http.get("/users/1"));
|
|
134
|
+
await obs.flush();
|
|
135
|
+
await obs.shutdown();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For server/request adapters, derive the runtime environment from incoming W3C
|
|
139
|
+
trace headers and reuse the same observability hooks:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const ctx = makeRequestObservabilityContext(obs, {
|
|
143
|
+
method: "GET",
|
|
144
|
+
route: "/users/:id",
|
|
145
|
+
headers: {
|
|
146
|
+
traceparent: request.headers.get("traceparent") ?? undefined,
|
|
147
|
+
tracestate: request.headers.get("tracestate") ?? undefined,
|
|
148
|
+
baggage: request.headers.get("baggage") ?? undefined,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const runtime = ctx.makeRuntime();
|
|
153
|
+
await runtime.toPromise(
|
|
154
|
+
ctx.withRequestSpan(handleUserRequest)
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For lower-level wiring, compose the same pieces directly:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { EventBus, InMemoryTracer, Runtime, makeMetrics } from "brass-runtime/core";
|
|
162
|
+
import {
|
|
163
|
+
logEffect,
|
|
164
|
+
makeOtlpHttpMetricsExporter,
|
|
165
|
+
makeOtlpHttpSpanExporter,
|
|
166
|
+
makePrometheusMetricsExporter,
|
|
167
|
+
makeRuntimeMetricsSink,
|
|
168
|
+
makeStructuredLogSink,
|
|
169
|
+
spanEvent,
|
|
170
|
+
withLogContext,
|
|
171
|
+
withSpan,
|
|
172
|
+
} from "brass-runtime/observability";
|
|
173
|
+
|
|
174
|
+
const bus = new EventBus();
|
|
175
|
+
const metrics = makeMetrics();
|
|
176
|
+
const tracer = new InMemoryTracer();
|
|
177
|
+
|
|
178
|
+
bus.subscribeHooks(makeRuntimeMetricsSink(metrics));
|
|
179
|
+
bus.subscribeHooks(makeStructuredLogSink({ minLevel: "info" }));
|
|
180
|
+
bus.subscribeHooks(tracer);
|
|
181
|
+
|
|
182
|
+
const runtime = new Runtime({ env: {}, hooks: bus });
|
|
183
|
+
|
|
184
|
+
const program = withSpan(
|
|
185
|
+
"request",
|
|
186
|
+
withLogContext({ requestId: "req-1" }, logEffect("info", "accepted"))
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
await runtime.toPromise(program);
|
|
190
|
+
|
|
191
|
+
// Prometheus scrape body
|
|
192
|
+
const prometheusText = makePrometheusMetricsExporter(metrics).export();
|
|
193
|
+
|
|
194
|
+
// OTLP JSON/HTTP push
|
|
195
|
+
await makeOtlpHttpMetricsExporter(metrics, {
|
|
196
|
+
url: "http://collector:4318/v1/metrics",
|
|
197
|
+
resource: { "service.name": "api" },
|
|
198
|
+
}).export();
|
|
199
|
+
|
|
200
|
+
await makeOtlpHttpSpanExporter(tracer, {
|
|
201
|
+
url: "http://collector:4318/v1/traces",
|
|
202
|
+
resource: { "service.name": "api" },
|
|
203
|
+
}).export();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The exporters are dependency-free:
|
|
207
|
+
|
|
208
|
+
- Prometheus uses the text exposition format.
|
|
209
|
+
- OTLP exporters emit JSON and accept a custom `fetch` for tests or runtime
|
|
210
|
+
adapters.
|
|
211
|
+
- Runtime metrics are derived from `RuntimeEvent`s and keep high-cardinality
|
|
212
|
+
labels disabled by default.
|
|
213
|
+
- Supervisor events flow through the same sinks as fibers/scopes, so restart
|
|
214
|
+
rates and escalation counts are visible through runtime event counters.
|
|
215
|
+
- `withHttpObservability` adds client request spans, `traceparent` propagation,
|
|
216
|
+
request metrics, structured HTTP logs, and adaptive limiter gauges/span
|
|
217
|
+
attributes when the wrapped client owns a limiter.
|
|
218
|
+
- HTTP request `policy` is included in logs and span attributes automatically.
|
|
219
|
+
Metric labels for policy fields are opt-in with `policy.labelKeys` so lanes
|
|
220
|
+
or dedup keys do not accidentally create high-cardinality series.
|
|
221
|
+
- `parseTraceparent`, `extractTraceContext`, `formatTraceparent`, and
|
|
222
|
+
`injectTraceContext` provide backend-neutral W3C trace-context helpers.
|
|
223
|
+
- `baggage` is extracted, merged into the runtime trace seed, and propagated
|
|
224
|
+
on outbound HTTP when Brass owns the outgoing trace headers.
|
|
225
|
+
- `makeRequestObservabilityContext` and `obs.envForRequest()` seed runtime
|
|
226
|
+
tracing from incoming request headers.
|
|
227
|
+
- `withSpan` updates the current fiber trace context, so nested spans and child
|
|
228
|
+
fibers inherit the right `traceId`, `spanId`, and `parentSpanId`.
|
|
229
|
+
- `withSpan(name, effect, { links })`, `spanLink(trace)`, and
|
|
230
|
+
`currentSpanLink()` model fan-out/fan-in without inventing false parentage.
|
|
231
|
+
- Runtime and HTTP duration histograms attach exemplars when a sampled trace is
|
|
232
|
+
active, so a slow bucket can point back to a concrete `traceId`/`spanId`.
|
|
233
|
+
- `makeRuntimeHealth`, `readiness`, and `healthToHttpResponse` expose
|
|
234
|
+
runtime/fiber/scope/scheduler plus registered circuit breaker and adaptive
|
|
235
|
+
limiter health.
|
|
236
|
+
- `makeObservability` returns `hooks`, `env`, `metrics`, `tracer`, exporters,
|
|
237
|
+
plus `flush()`, `start()`, `stop()`, and `shutdown()`.
|
|
238
|
+
|
|
239
|
+
### HTTP policy observability
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const policies = defineHttpPolicyPresets({
|
|
243
|
+
readModel: {
|
|
244
|
+
lane: "read-model",
|
|
245
|
+
poolKey: "users-api",
|
|
246
|
+
retry: { maxRetries: 2, baseDelayMs: 50 },
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const http = makeDefaultHttpClient({
|
|
251
|
+
baseUrl: "https://api.example.com",
|
|
252
|
+
policyPresets: policies,
|
|
253
|
+
middleware: [
|
|
254
|
+
withHttpObservability({
|
|
255
|
+
metrics: obs.metrics,
|
|
256
|
+
route: "/users/:id",
|
|
257
|
+
policy: { labelKeys: ["preset", "lane", "poolKey"] },
|
|
258
|
+
}),
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await http.getJson("/users/1", {
|
|
263
|
+
policy: { preset: "readModel", dedupKey: "users:1" },
|
|
264
|
+
}).unsafeRunPromise();
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The request log and HTTP span carry the policy context. Prometheus metrics only
|
|
268
|
+
receive `policy`, `lane`, and `pool_key` because those labels were explicitly
|
|
269
|
+
allowed.
|
|
270
|
+
Fetch/transport errors with status metadata also flow into HTTP error metrics
|
|
271
|
+
and span events, and include a `http.retryable` signal for retry dashboards.
|
|
272
|
+
|
|
273
|
+
The stable dashboard contract is exported as `HTTP_OBSERVABILITY_CONTRACT`:
|
|
274
|
+
|
|
275
|
+
| Signal | Contract |
|
|
276
|
+
|--------|----------|
|
|
277
|
+
| Requests | `brass_http_client_requests_total` with `method`, `host`, `route`, `outcome`, `status` |
|
|
278
|
+
| Duration | `brass_http_client_duration_ms` histogram with the same labels |
|
|
279
|
+
| In-flight | `brass_http_client_in_flight` with request labels only |
|
|
280
|
+
| Policy labels | opt-in `policy`, `lane`, `pool_key`, `dedup_key`, `priority`, `retry` |
|
|
281
|
+
| Adaptive limiter | `brass_http_adaptive_limiter_*` gauges, optional `key` label |
|
|
282
|
+
| Span events | `http.client.response` / `http.client.error` |
|
|
283
|
+
| Error attrs | `http.status_code`, `error.type`, `http.retryable` |
|
|
284
|
+
|
|
285
|
+
### Production hardening
|
|
286
|
+
|
|
287
|
+
`makeObservability` includes production-oriented controls without adding vendor
|
|
288
|
+
dependencies:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const obs = makeObservability({
|
|
292
|
+
serviceName: "api",
|
|
293
|
+
logs: { minLevel: "info" },
|
|
294
|
+
sampling: {
|
|
295
|
+
ratio: 0.25,
|
|
296
|
+
respectRemoteSampled: true,
|
|
297
|
+
forceSampleOnError: true,
|
|
298
|
+
},
|
|
299
|
+
redaction: {},
|
|
300
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
301
|
+
otlp: {
|
|
302
|
+
metricsUrl: "http://collector:4318/v1/metrics",
|
|
303
|
+
tracesUrl: "http://collector:4318/v1/traces",
|
|
304
|
+
timeoutMs: 10_000,
|
|
305
|
+
retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
|
|
306
|
+
pipeline: {
|
|
307
|
+
maxQueueSize: 10_000,
|
|
308
|
+
batchSize: 512,
|
|
309
|
+
dropPolicy: "drop-oldest",
|
|
310
|
+
shutdownTimeoutMs: 10_000,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
traces: {
|
|
314
|
+
maxFinishedSpans: 10_000,
|
|
315
|
+
maxSpanAgeMs: 600_000,
|
|
316
|
+
},
|
|
317
|
+
flushIntervalMs: 10_000,
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The export pipeline is bounded and non-blocking from the runtime perspective:
|
|
322
|
+
it batches spans, retries failed exports with backoff, applies export timeouts,
|
|
323
|
+
drops according to policy when the queue is full, exposes exporter metrics, and
|
|
324
|
+
drains on `shutdown()` with a deadline. Flushes are single-flight, so a slow
|
|
325
|
+
collector does not create overlapping exports.
|
|
326
|
+
|
|
327
|
+
Finished spans are pruned after successful export and can also be bounded with
|
|
328
|
+
`traces.maxFinishedSpans` / `traces.maxSpanAgeMs`.
|
|
329
|
+
|
|
330
|
+
Sampling can be configured globally, by ratio, or with rules:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const obs = makeObservability({
|
|
334
|
+
sampling: {
|
|
335
|
+
ratio: 0.1,
|
|
336
|
+
rules: [
|
|
337
|
+
{ route: "/health", sampled: false },
|
|
338
|
+
{ name: /^checkout\./, ratio: 1 },
|
|
339
|
+
],
|
|
340
|
+
respectRemoteSampled: true,
|
|
341
|
+
forceSampleOnError: true,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Redaction is enabled by passing `redaction: {}`. Default sensitive keys include
|
|
347
|
+
authorization headers, cookies, passwords, secrets, tokens, and API keys. You
|
|
348
|
+
can override the key/header patterns and replacement text.
|
|
349
|
+
|
|
350
|
+
Metric label cardinality can be bounded with `cardinality.maxValuesPerLabel`.
|
|
351
|
+
New label values past the limit are mapped to `__overflow__`.
|
|
352
|
+
|
|
353
|
+
Environment-based setup is available for deployment entry points:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import { makeObservabilityFromEnv } from "brass-runtime/observability";
|
|
357
|
+
|
|
358
|
+
const obs = makeObservabilityFromEnv(process.env);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Recognized environment variables include `BRASS_OBSERVABILITY_PRESET`,
|
|
362
|
+
`BRASS_OBSERVABILITY=disabled`, `OTEL_SERVICE_NAME`,
|
|
363
|
+
`OTEL_SERVICE_VERSION`, `OTEL_EXPORTER_OTLP_ENDPOINT`,
|
|
364
|
+
`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
|
|
365
|
+
`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`,
|
|
366
|
+
`BRASS_TRACE_SAMPLE_RATIO`,
|
|
367
|
+
`BRASS_OBSERVABILITY_FLUSH_INTERVAL_MS`, and
|
|
368
|
+
`BRASS_OBSERVABILITY_EXPORT_TIMEOUT_MS`.
|
|
369
|
+
|
|
370
|
+
Inbound adapters are available for common request shapes:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import {
|
|
374
|
+
makeFetchRequestObservabilityContext,
|
|
375
|
+
makeNodeRequestObservabilityContext,
|
|
376
|
+
makeExpressRequestObservabilityContext,
|
|
377
|
+
makeFastifyRequestObservabilityContext,
|
|
378
|
+
} from "brass-runtime/observability";
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
For server-side request metrics around an effect:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { runObservedHttpServerEffect } from "brass-runtime/observability";
|
|
385
|
+
|
|
386
|
+
const result = await runObservedHttpServerEffect(
|
|
387
|
+
obs,
|
|
388
|
+
{ method: "GET", route: "/users/:id", headers: request.headers },
|
|
389
|
+
program,
|
|
390
|
+
{ statusCode: () => 200 }
|
|
391
|
+
);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
This emits `brass_http_server_requests_total`,
|
|
395
|
+
`brass_http_server_duration_ms`, and `brass_http_server_in_flight`, and creates
|
|
396
|
+
server spans with OpenTelemetry-friendly attributes such as `span.kind`,
|
|
397
|
+
`http.request.method`, `http.route`, and `url.path`.
|
|
398
|
+
|
|
399
|
+
Runtime health can be reported as an effect or converted to an HTTP response:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { healthToHttpResponse, makeRuntimeHealth } from "brass-runtime/observability";
|
|
403
|
+
import { makeRuntimeHealthRoute, makeRuntimeReadinessRoute, makeHttpRouter } from "brass-runtime/http";
|
|
404
|
+
|
|
405
|
+
const report = await runtime.toPromise(makeRuntimeHealth({
|
|
406
|
+
runtime,
|
|
407
|
+
registry: runtime.registry,
|
|
408
|
+
adaptiveLimiters: { api: limiter },
|
|
409
|
+
}));
|
|
410
|
+
|
|
411
|
+
const response = healthToHttpResponse(report);
|
|
412
|
+
|
|
413
|
+
const router = makeHttpRouter([
|
|
414
|
+
makeRuntimeHealthRoute({ runtime, registry: runtime.registry }),
|
|
415
|
+
makeRuntimeReadinessRoute({
|
|
416
|
+
runtime,
|
|
417
|
+
registry: runtime.registry,
|
|
418
|
+
adaptiveLimiters: { api: limiter },
|
|
419
|
+
readiness: { failOnDegraded: true },
|
|
420
|
+
}),
|
|
421
|
+
]);
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Runnable framework examples live in
|
|
425
|
+
[`docs/observability-framework-examples.md`](./observability-framework-examples.md).
|
|
426
|
+
|
|
427
|
+
Collector smoke and performance budget helpers:
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
npm run smoke:observability:collector
|
|
431
|
+
npm run benchmark:observability
|
|
432
|
+
npm run benchmark:observability:budget
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Should hooks be centralized or split?
|
|
438
|
+
|
|
439
|
+
✅ Centralizing is a good idea when you want:
|
|
440
|
+
|
|
441
|
+
- a single configuration point
|
|
442
|
+
- fan-out to multiple sinks
|
|
443
|
+
- backpressure / dropping policies
|
|
444
|
+
- global correlation (`seq`, etc.)
|
|
445
|
+
|
|
446
|
+
This doesn’t conflict with ZIO. In ZIO you “compose” logging/tracing via the environment; here `RuntimeHooks` is the equivalent boundary.
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Structured JSON log sink
|
|
451
|
+
|
|
452
|
+
Example sink printing JSON:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import type { RuntimeEvent, RuntimeEmitContext } from "brass-runtime/core";
|
|
456
|
+
|
|
457
|
+
export const consoleJsonSink = () => (ev: RuntimeEvent, ctx: RuntimeEmitContext) => {
|
|
458
|
+
if (ev.type !== "log") return;
|
|
459
|
+
const level = ev.level ?? "info";
|
|
460
|
+
const out = { level, message: ev.message, fields: ev.fields ?? {}, traceId: ctx.traceId, spanId: ctx.spanId };
|
|
461
|
+
if (level === "error") console.error(JSON.stringify(out));
|
|
462
|
+
else console.log(JSON.stringify(out));
|
|
463
|
+
};
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Recommendations:
|
|
467
|
+
- include `traceId/spanId` if available in context
|
|
468
|
+
- prefer structured data over free-form strings
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Tracing: propagating traceId/spanId
|
|
473
|
+
|
|
474
|
+
### Recommended fiber context model
|
|
475
|
+
|
|
476
|
+
- `traceId`: stable for a “request / operation”
|
|
477
|
+
- `spanId`: changes per sub-operation (e.g. fork child or scope span)
|
|
478
|
+
|
|
479
|
+
Simple policy:
|
|
480
|
+
|
|
481
|
+
- when forking, if parent has trace:
|
|
482
|
+
- `traceId` = same
|
|
483
|
+
- `spanId` = new
|
|
484
|
+
- `parentSpanId` = parent’s span
|
|
485
|
+
|
|
486
|
+
### Where to store it
|
|
487
|
+
|
|
488
|
+
- in a per-fiber `FiberContext`
|
|
489
|
+
- and when emitting events, copy into `RuntimeEmitContext`
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## InMemoryTracer (for tests)
|
|
494
|
+
|
|
495
|
+
Very useful for tests:
|
|
496
|
+
|
|
497
|
+
- store spans in memory
|
|
498
|
+
- verify they close
|
|
499
|
+
- export only finished spans
|
|
500
|
+
|
|
501
|
+
Recommendation: choose one mapping strategy:
|
|
502
|
+
- span per `scope.open/close`
|
|
503
|
+
- or span per `fiber.start/end`
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Practical recipes
|
|
508
|
+
|
|
509
|
+
### 1) Enabling observability in a Runtime
|
|
510
|
+
|
|
511
|
+
- create an `EventBus`
|
|
512
|
+
- subscribe sinks
|
|
513
|
+
- pass `hooks: eventBus` to the Runtime constructor
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
import { EventBus, Runtime, consoleJsonLogger } from "brass-runtime/core";
|
|
517
|
+
|
|
518
|
+
const bus = new EventBus();
|
|
519
|
+
bus.subscribeHooks(consoleJsonLogger());
|
|
520
|
+
|
|
521
|
+
const runtime = new Runtime({
|
|
522
|
+
env: {},
|
|
523
|
+
hooks: bus
|
|
524
|
+
});
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 2) Drop policy / budget
|
|
528
|
+
To avoid memory blowups:
|
|
529
|
+
|
|
530
|
+
- ring buffer per sink
|
|
531
|
+
- `flush()` budget
|
|
532
|
+
- emit a periodic “bus.dropped” warning
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Checklist
|
|
537
|
+
|
|
538
|
+
- [ ] `Runtime` accepts optional `hooks`
|
|
539
|
+
- [ ] `emit` is non-blocking (enqueue + microtask flush)
|
|
540
|
+
- [ ] at least one “official” log sink exists
|
|
541
|
+
- [ ] tracing propagates through fiber/scope context
|
|
542
|
+
- [ ] tests cover “spans close” and “no leaks”
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
receivers:
|
|
2
|
+
otlp:
|
|
3
|
+
protocols:
|
|
4
|
+
http:
|
|
5
|
+
endpoint: 0.0.0.0:4318
|
|
6
|
+
|
|
7
|
+
processors:
|
|
8
|
+
batch:
|
|
9
|
+
|
|
10
|
+
exporters:
|
|
11
|
+
debug:
|
|
12
|
+
verbosity: normal
|
|
13
|
+
|
|
14
|
+
service:
|
|
15
|
+
pipelines:
|
|
16
|
+
traces:
|
|
17
|
+
receivers: [otlp]
|
|
18
|
+
processors: [batch]
|
|
19
|
+
exporters: [debug]
|
|
20
|
+
metrics:
|
|
21
|
+
receivers: [otlp]
|
|
22
|
+
processors: [batch]
|
|
23
|
+
exporters: [debug]
|
|
24
|
+
logs:
|
|
25
|
+
receivers: [otlp]
|
|
26
|
+
processors: [batch]
|
|
27
|
+
exporters: [debug]
|