brass-runtime 1.16.0 → 1.17.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.
Files changed (219) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +287 -23
  3. package/dist/agent/cli/main.cjs +38 -38
  4. package/dist/agent/cli/main.js +6 -6
  5. package/dist/agent/cli/main.mjs +6 -6
  6. package/dist/agent/index.cjs +7 -7
  7. package/dist/agent/index.d.ts +1 -1
  8. package/dist/agent/index.js +6 -6
  9. package/dist/agent/index.mjs +6 -6
  10. package/dist/chunk-2HQTDLHF.mjs +683 -0
  11. package/dist/chunk-36I3M4UC.mjs +370 -0
  12. package/dist/{chunk-QY5FKYEQ.js → chunk-3AYM6WPJ.js} +570 -51
  13. package/dist/chunk-3LOYJFRR.cjs +300 -0
  14. package/dist/chunk-3Y2RIUMM.js +300 -0
  15. package/dist/{chunk-7XOPAB5Q.js → chunk-4P2HHGAX.mjs} +83 -5
  16. package/dist/{chunk-N6VHMOWB.cjs → chunk-4ROBZFL6.cjs} +128 -128
  17. package/dist/{chunk-NC5SDRYE.js → chunk-52OB2ROS.js} +4 -4
  18. package/dist/{chunk-JX3LZQJH.cjs → chunk-52PPNNI4.cjs} +82 -20
  19. package/dist/{chunk-5YOQOXEQ.cjs → chunk-5EC274J5.cjs} +676 -293
  20. package/dist/chunk-5QC7LRZ3.js +229 -0
  21. package/dist/{chunk-7TL2LHQJ.js → chunk-5VRJNBLZ.mjs} +524 -141
  22. package/dist/chunk-62AZW6UT.cjs +313 -0
  23. package/dist/chunk-6IXXWIUM.js +683 -0
  24. package/dist/chunk-6RY2FFN4.mjs +2024 -0
  25. package/dist/chunk-74ZTY6CP.js +2871 -0
  26. package/dist/chunk-7CMJS3QE.mjs +2871 -0
  27. package/dist/{chunk-2WC63LJK.mjs → chunk-7JIJOVCT.js} +20 -10
  28. package/dist/chunk-7X3K5RMS.js +2024 -0
  29. package/dist/chunk-7ZPEZ57L.cjs +2024 -0
  30. package/dist/{chunk-FM4W4QPL.js → chunk-A2OM6NEH.mjs} +5 -4
  31. package/dist/chunk-AGR5B2BC.cjs +683 -0
  32. package/dist/chunk-B33ICAKP.js +313 -0
  33. package/dist/{chunk-J3H54ZRV.mjs → chunk-B5JD23U7.mjs} +1 -1
  34. package/dist/{chunk-F5EUMJL7.mjs → chunk-BKK77SBA.js} +83 -5
  35. package/dist/{chunk-U5KWK3PX.mjs → chunk-C3MDXTRZ.js} +11 -0
  36. package/dist/{chunk-SPUEME2B.cjs → chunk-CZIVE6NT.cjs} +12 -1
  37. package/dist/{chunk-TDVMADDN.js → chunk-DNFJLJMW.mjs} +11 -0
  38. package/dist/{chunk-XDZOO4L5.js → chunk-EJ6BPYVR.mjs} +79 -17
  39. package/dist/chunk-EOC4UHBS.mjs +229 -0
  40. package/dist/chunk-F6XWZQY4.cjs +777 -0
  41. package/dist/{chunk-7LVI2GIN.js → chunk-FH2X7BVP.js} +507 -72
  42. package/dist/{chunk-OOGJ73B6.js → chunk-FHQGHPMO.mjs} +20 -10
  43. package/dist/{chunk-WQ5QNU5R.cjs → chunk-GLE2WY7Z.cjs} +652 -217
  44. package/dist/{chunk-G6IQOE4P.mjs → chunk-GYM3LLGS.mjs} +507 -72
  45. package/dist/{chunk-TVN5I4U6.cjs → chunk-JF5WGYJJ.cjs} +25 -24
  46. package/dist/{chunk-CY33PGEX.mjs → chunk-KH4SYAOS.mjs} +570 -51
  47. package/dist/chunk-KN32XNTH.mjs +313 -0
  48. package/dist/chunk-KQLYONSE.cjs +2871 -0
  49. package/dist/{chunk-7HUOJA4W.cjs → chunk-KZJQ723N.cjs} +90 -80
  50. package/dist/{chunk-CCKHV5BT.mjs → chunk-L2SYFEBS.js} +5 -4
  51. package/dist/{chunk-IJT6RRQ5.cjs → chunk-L6VB5N7Q.cjs} +20 -9
  52. package/dist/{chunk-ZGLD4TVZ.mjs → chunk-MBEJI5HF.mjs} +4 -4
  53. package/dist/{chunk-PRWCB3QL.mjs → chunk-MIIYDLGM.js} +524 -141
  54. package/dist/{chunk-H55LI6WY.js → chunk-MOO4L7F4.mjs} +15 -4
  55. package/dist/chunk-MVGUEJ5Z.cjs +370 -0
  56. package/dist/chunk-PD4EJTQC.cjs +229 -0
  57. package/dist/chunk-PWC3RBQE.mjs +300 -0
  58. package/dist/{chunk-MWXMNYJS.cjs → chunk-Q2I37RP3.cjs} +643 -124
  59. package/dist/{chunk-VFIUZG7J.mjs → chunk-RKGKFN2A.js} +79 -17
  60. package/dist/{chunk-NYL4D7SK.cjs → chunk-SA6HUJVI.cjs} +5 -5
  61. package/dist/chunk-SK7UZRNI.mjs +777 -0
  62. package/dist/{chunk-K2T3DV26.mjs → chunk-TRM4JUZQ.js} +15 -4
  63. package/dist/chunk-UB4B6OFY.js +370 -0
  64. package/dist/{chunk-G3XGCZDQ.js → chunk-UCUBNWM2.js} +1 -1
  65. package/dist/chunk-VWIPB6I5.js +777 -0
  66. package/dist/{chunk-JNFRRJYH.cjs → chunk-WBGRHGBP.cjs} +270 -192
  67. package/dist/{client-CtFmoDvM.d.ts → client-CZHU674n.d.ts} +211 -36
  68. package/dist/core/index.cjs +135 -9
  69. package/dist/core/index.d.ts +238 -33
  70. package/dist/core/index.js +155 -29
  71. package/dist/core/index.mjs +155 -29
  72. package/dist/{effect-CGNl5Rqp.d.ts → effect-DIUHZ9IN.d.ts} +89 -1
  73. package/dist/effectRunner-CFLC32IK.cjs +8 -0
  74. package/dist/{effectRunner-A4CHJXJI.js → effectRunner-L4S7IPT3.js} +2 -2
  75. package/dist/{effectRunner-OPUF6QRN.mjs → effectRunner-NNGG75QA.mjs} +2 -2
  76. package/dist/http/index.cjs +324 -2986
  77. package/dist/http/index.d.ts +54 -68
  78. package/dist/http/index.js +238 -2900
  79. package/dist/http/index.mjs +238 -2900
  80. package/dist/http/testing.cjs +14 -12
  81. package/dist/http/testing.d.ts +5 -4
  82. package/dist/http/testing.js +10 -8
  83. package/dist/http/testing.mjs +10 -8
  84. package/dist/index.cjs +423 -255
  85. package/dist/index.d.ts +87 -69
  86. package/dist/index.js +301 -133
  87. package/dist/index.mjs +301 -133
  88. package/dist/observability/index.cjs +18 -531
  89. package/dist/observability/index.d.ts +81 -8
  90. package/dist/observability/index.js +25 -538
  91. package/dist/observability/index.mjs +25 -538
  92. package/dist/perf/cli.cjs +401 -0
  93. package/dist/perf/cli.d.ts +1 -0
  94. package/dist/perf/cli.js +401 -0
  95. package/dist/perf/cli.mjs +401 -0
  96. package/dist/perf/index.cjs +141 -0
  97. package/dist/perf/index.d.ts +483 -0
  98. package/dist/perf/index.js +141 -0
  99. package/dist/perf/index.mjs +141 -0
  100. package/dist/schedule-CK3Ml_7p.d.ts +259 -0
  101. package/dist/schema/index.cjs +6 -2
  102. package/dist/schema/index.d.ts +3 -1
  103. package/dist/schema/index.js +5 -1
  104. package/dist/schema/index.mjs +5 -1
  105. package/dist/{server-C8hDXA74.d.ts → server-D6JZ15_e.d.ts} +16 -4
  106. package/dist/{stream-dvSs0QS5.d.ts → stream-B4oK9JFP.d.ts} +1 -1
  107. package/dist/{tracer-B5tRH9H7.d.ts → tracer-Hwt1cl7h.d.ts} +13 -54
  108. package/dist/{tracing-Dt9S_6V8.d.ts → tracing-DqbTKGcf.d.ts} +1 -1
  109. package/docs/ARCHITECTURE.md +292 -0
  110. package/docs/README.md +65 -0
  111. package/docs/adr/0001-ai-context-pack.md +32 -0
  112. package/docs/agent-apply-mode.md +104 -0
  113. package/docs/agent-approvals.md +110 -0
  114. package/docs/agent-batch.md +185 -0
  115. package/docs/agent-boundaries.md +112 -0
  116. package/docs/agent-chat-sessions.md +160 -0
  117. package/docs/agent-ci.md +17 -0
  118. package/docs/agent-cli.md +405 -0
  119. package/docs/agent-config.md +480 -0
  120. package/docs/agent-context-discovery.md +159 -0
  121. package/docs/agent-copilot-like-dx.md +126 -0
  122. package/docs/agent-declarative-optimized-planning.md +138 -0
  123. package/docs/agent-dx.md +224 -0
  124. package/docs/agent-env-files.md +126 -0
  125. package/docs/agent-follow-up-context.md +43 -0
  126. package/docs/agent-global-usage.md +180 -0
  127. package/docs/agent-init.md +109 -0
  128. package/docs/agent-install-and-configure.md +516 -0
  129. package/docs/agent-language-workspace-ux.md +99 -0
  130. package/docs/agent-llm-adapters.md +123 -0
  131. package/docs/agent-local-install.md +190 -0
  132. package/docs/agent-local-tests.md +51 -0
  133. package/docs/agent-observability.md +155 -0
  134. package/docs/agent-patch-quality-loop.md +162 -0
  135. package/docs/agent-presets.md +22 -0
  136. package/docs/agent-project-commands.md +237 -0
  137. package/docs/agent-project-intelligence.md +156 -0
  138. package/docs/agent-redaction.md +18 -0
  139. package/docs/agent-release-readiness.md +76 -0
  140. package/docs/agent-rollback-safety.md +162 -0
  141. package/docs/agent-rollback.md +23 -0
  142. package/docs/agent-run-artifacts.md +16 -0
  143. package/docs/agent-vscode-auto-discovery.md +137 -0
  144. package/docs/agent-vscode-batch-runner.md +100 -0
  145. package/docs/agent-vscode-chat-layout.md +90 -0
  146. package/docs/agent-vscode-clean-install.md +147 -0
  147. package/docs/agent-vscode-code-actions.md +70 -0
  148. package/docs/agent-vscode-diff-preview.md +45 -0
  149. package/docs/agent-vscode-inline-assist.md +56 -0
  150. package/docs/agent-vscode-install.md +186 -0
  151. package/docs/agent-vscode-model-setup.md +97 -0
  152. package/docs/agent-vscode-patch-preview.md +92 -0
  153. package/docs/agent-vscode-problems.md +79 -0
  154. package/docs/agent-vscode-project-dashboard.md +106 -0
  155. package/docs/agent-vscode-run-history.md +92 -0
  156. package/docs/agent-vscode-ux.md +73 -0
  157. package/docs/ai/INVARIANTS.md +84 -0
  158. package/docs/ai/PROJECT_MAP.md +338 -0
  159. package/docs/ai/PUBLIC_API.md +339 -0
  160. package/docs/ai/VALIDATION_MATRIX.md +67 -0
  161. package/docs/api-polish.md +37 -0
  162. package/docs/cancellation.md +162 -0
  163. package/docs/coverage.md +46 -0
  164. package/docs/framework-integrations.md +38 -0
  165. package/docs/frameworks/angular.md +153 -0
  166. package/docs/frameworks/express.md +125 -0
  167. package/docs/frameworks/fastify.md +124 -0
  168. package/docs/frameworks/nestjs.md +282 -0
  169. package/docs/frameworks/nextjs.md +147 -0
  170. package/docs/frameworks/react.md +139 -0
  171. package/docs/frameworks/vanilla.md +224 -0
  172. package/docs/getting-started.md +159 -0
  173. package/docs/guides/README.md +40 -0
  174. package/docs/guides/circuit-breaker.md +89 -0
  175. package/docs/guides/error-handling.md +91 -0
  176. package/docs/guides/getting-started.md +107 -0
  177. package/docs/guides/layers.md +189 -0
  178. package/docs/guides/metrics.md +101 -0
  179. package/docs/guides/resource-management.md +141 -0
  180. package/docs/guides/retry.md +215 -0
  181. package/docs/guides/semaphore.md +66 -0
  182. package/docs/guides/streams.md +117 -0
  183. package/docs/guides/supervisors.md +98 -0
  184. package/docs/guides/testing.md +162 -0
  185. package/docs/guides/tracing.md +71 -0
  186. package/docs/http-recipes.md +399 -0
  187. package/docs/http.md +749 -0
  188. package/docs/modules.md +285 -0
  189. package/docs/nestjs.md +6 -0
  190. package/docs/observability-collector-smoke.md +31 -0
  191. package/docs/observability-framework-examples.md +110 -0
  192. package/docs/observability.md +649 -0
  193. package/docs/otel-collector-smoke.yaml +27 -0
  194. package/docs/performance-profiler.md +199 -0
  195. package/docs/production-readiness.md +73 -0
  196. package/docs/recipes/README.md +12 -0
  197. package/docs/recipes/http-server.md +45 -0
  198. package/docs/recipes/layers.md +44 -0
  199. package/docs/recipes/performance.md +47 -0
  200. package/docs/recipes/runtime.md +41 -0
  201. package/docs/recipes/testing.md +41 -0
  202. package/docs/release.md +53 -0
  203. package/docs/wasm-bounded-queues.md +44 -0
  204. package/docs/wasm-engine-observability-benchmarks.md +85 -0
  205. package/docs/wasm-fiber-engine.md +117 -0
  206. package/docs/wasm-scheduler-state-machine.md +122 -0
  207. package/docs/wasm-stream-chunks.md +54 -0
  208. package/package.json +22 -2
  209. package/dist/chunk-45F7OKGT.cjs +0 -104
  210. package/dist/chunk-7V4KY4RL.mjs +0 -104
  211. package/dist/chunk-DJQ7OMMB.cjs +0 -144
  212. package/dist/chunk-GOV47PPB.mjs +0 -552
  213. package/dist/chunk-JF4XXPZ5.cjs +0 -552
  214. package/dist/chunk-KCPT2D6G.js +0 -552
  215. package/dist/chunk-NOYZIMUJ.mjs +0 -144
  216. package/dist/chunk-PNVFW245.js +0 -144
  217. package/dist/chunk-ROJC3NBJ.js +0 -104
  218. package/dist/effectRunner-3ZHAD3LE.cjs +0 -8
  219. package/dist/schedule-Fque9Abz.d.ts +0 -70
@@ -0,0 +1,649 @@
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
+ ### Vendor-neutral collector recipes
331
+
332
+ Brass intentionally does not know about Grafana Cloud, AppDynamics, or any
333
+ OpenTelemetry SDK implementation. The runtime only needs OTLP HTTP endpoint
334
+ URLs, headers, and optional export tuning. Keep vendor naming in application
335
+ code by writing small helpers that call the backend-neutral `makeOtlpOptions`.
336
+
337
+ ```ts
338
+ import {
339
+ makeObservability,
340
+ makeOtlpOptions,
341
+ type ObservabilityOtlpOptions,
342
+ } from "brass-runtime/observability";
343
+
344
+ function productionOtlp(input: {
345
+ readonly endpoint: string;
346
+ readonly headers?: Record<string, string>;
347
+ }): ObservabilityOtlpOptions {
348
+ return makeOtlpOptions({
349
+ endpoint: input.endpoint,
350
+ headers: input.headers,
351
+ timeoutMs: 10_000,
352
+ retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
353
+ pipeline: {
354
+ maxQueueSize: 10_000,
355
+ batchSize: 512,
356
+ dropPolicy: "drop-oldest",
357
+ shutdownTimeoutMs: 10_000,
358
+ },
359
+ });
360
+ }
361
+ ```
362
+
363
+ Grafana Cloud can be configured as a direct OTLP endpoint or through
364
+ Grafana Alloy/OpenTelemetry Collector. The helper stays in your app and only
365
+ returns Brass OTLP config:
366
+
367
+ ```ts
368
+ function grafanaCloudCollector(input: {
369
+ readonly endpoint: string;
370
+ readonly authorization?: string;
371
+ }): ObservabilityOtlpOptions {
372
+ return productionOtlp({
373
+ endpoint: input.endpoint,
374
+ headers: input.authorization
375
+ ? { Authorization: input.authorization }
376
+ : undefined,
377
+ });
378
+ }
379
+
380
+ const observability = makeObservability({
381
+ serviceName: "shopping-ms",
382
+ serviceVersion: "1.2.3",
383
+ resource: {
384
+ "service.namespace": "shopping",
385
+ "deployment.environment": "production",
386
+ },
387
+ otlp: grafanaCloudCollector({
388
+ endpoint: process.env.GRAFANA_OTLP_ENDPOINT!,
389
+ authorization: process.env.GRAFANA_OTLP_AUTHORIZATION,
390
+ }),
391
+ });
392
+ ```
393
+
394
+ For AppDynamics, prefer sending Brass telemetry to the AppDynamics/OpenTelemetry
395
+ Collector deployed next to the service. Authentication and vendor-specific
396
+ exporters stay in the collector config:
397
+
398
+ ```ts
399
+ function appDynamicsCollector(input: {
400
+ readonly endpoint: string;
401
+ }): ObservabilityOtlpOptions {
402
+ return productionOtlp({
403
+ endpoint: input.endpoint,
404
+ });
405
+ }
406
+
407
+ const observability = makeObservability({
408
+ serviceName: "car-rental-ms",
409
+ serviceVersion: "1.2.3",
410
+ resource: {
411
+ "service.namespace": "car-rental",
412
+ "deployment.environment": "production",
413
+ },
414
+ otlp: appDynamicsCollector({
415
+ endpoint: process.env.APPD_OTEL_COLLECTOR_ENDPOINT ?? "http://appd-otel-collector:4318",
416
+ }),
417
+ });
418
+ ```
419
+
420
+ Then attach the same observability instance to HTTP without changing the
421
+ collector helpers:
422
+
423
+ ```ts
424
+ import { makeDefaultHttpClient } from "brass-runtime/http";
425
+ import { withHttpObservability } from "brass-runtime/observability";
426
+
427
+ const http = makeDefaultHttpClient({
428
+ baseUrl: "https://api.example.com",
429
+ middleware: [withHttpObservability(observability)],
430
+ });
431
+ ```
432
+
433
+ Sampling can be configured globally, by ratio, or with rules:
434
+
435
+ ```ts
436
+ const obs = makeObservability({
437
+ sampling: {
438
+ ratio: 0.1,
439
+ rules: [
440
+ { route: "/health", sampled: false },
441
+ { name: /^checkout\./, ratio: 1 },
442
+ ],
443
+ respectRemoteSampled: true,
444
+ forceSampleOnError: true,
445
+ },
446
+ });
447
+ ```
448
+
449
+ Redaction is enabled by passing `redaction: {}`. Default sensitive keys include
450
+ authorization headers, cookies, passwords, secrets, tokens, and API keys. You
451
+ can override the key/header patterns and replacement text.
452
+
453
+ Metric label cardinality can be bounded with `cardinality.maxValuesPerLabel`.
454
+ New label values past the limit are mapped to `__overflow__`.
455
+
456
+ Environment-based setup is available for deployment entry points:
457
+
458
+ ```ts
459
+ import { makeObservabilityFromEnv } from "brass-runtime/observability";
460
+
461
+ const obs = makeObservabilityFromEnv(process.env);
462
+ ```
463
+
464
+ Recognized environment variables include `BRASS_OBSERVABILITY_PRESET`,
465
+ `BRASS_OBSERVABILITY=disabled`, `OTEL_SERVICE_NAME`,
466
+ `OTEL_SERVICE_VERSION`, `OTEL_EXPORTER_OTLP_ENDPOINT`,
467
+ `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
468
+ `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`,
469
+ `BRASS_TRACE_SAMPLE_RATIO`,
470
+ `BRASS_OBSERVABILITY_FLUSH_INTERVAL_MS`, and
471
+ `BRASS_OBSERVABILITY_EXPORT_TIMEOUT_MS`.
472
+
473
+ Inbound adapters are available for common request shapes:
474
+
475
+ ```ts
476
+ import {
477
+ makeFetchRequestObservabilityContext,
478
+ makeNodeRequestObservabilityContext,
479
+ makeExpressRequestObservabilityContext,
480
+ makeFastifyRequestObservabilityContext,
481
+ } from "brass-runtime/observability";
482
+ ```
483
+
484
+ For server-side request metrics around an effect:
485
+
486
+ ```ts
487
+ import { runObservedHttpServerEffect } from "brass-runtime/observability";
488
+
489
+ const result = await runObservedHttpServerEffect(
490
+ obs,
491
+ { method: "GET", route: "/users/:id", headers: request.headers },
492
+ program,
493
+ { statusCode: () => 200 }
494
+ );
495
+ ```
496
+
497
+ This emits `brass_http_server_requests_total`,
498
+ `brass_http_server_duration_ms`, and `brass_http_server_in_flight`, and creates
499
+ server spans with OpenTelemetry-friendly attributes such as `span.kind`,
500
+ `http.request.method`, `http.route`, and `url.path`.
501
+
502
+ Runtime health can be reported as an effect or converted to an HTTP response:
503
+
504
+ ```ts
505
+ import { healthToHttpResponse, makeRuntimeHealth } from "brass-runtime/observability";
506
+ import { makeRuntimeHealthRoute, makeRuntimeReadinessRoute, makeHttpRouter } from "brass-runtime/http";
507
+
508
+ const report = await runtime.toPromise(makeRuntimeHealth({
509
+ runtime,
510
+ registry: runtime.registry,
511
+ adaptiveLimiters: { api: limiter },
512
+ }));
513
+
514
+ const response = healthToHttpResponse(report);
515
+
516
+ const router = makeHttpRouter([
517
+ makeRuntimeHealthRoute({ runtime, registry: runtime.registry }),
518
+ makeRuntimeReadinessRoute({
519
+ runtime,
520
+ registry: runtime.registry,
521
+ adaptiveLimiters: { api: limiter },
522
+ readiness: { failOnDegraded: true },
523
+ }),
524
+ ]);
525
+ ```
526
+
527
+ Runnable framework examples live in
528
+ [`docs/observability-framework-examples.md`](./observability-framework-examples.md).
529
+ Production-style framework integration recipes live in
530
+ [`docs/framework-integrations.md`](./framework-integrations.md).
531
+ For a NestJS module recipe with Grafana/OTLP, DI tokens, HTTP client
532
+ observability, and shutdown wiring, see [`docs/frameworks/nestjs.md`](./frameworks/nestjs.md).
533
+
534
+ Collector smoke and performance budget helpers:
535
+
536
+ ```bash
537
+ npm run smoke:observability:collector
538
+ npm run benchmark:observability
539
+ npm run benchmark:observability:budget
540
+ ```
541
+
542
+ ---
543
+
544
+ ## Should hooks be centralized or split?
545
+
546
+ ✅ Centralizing is a good idea when you want:
547
+
548
+ - a single configuration point
549
+ - fan-out to multiple sinks
550
+ - backpressure / dropping policies
551
+ - global correlation (`seq`, etc.)
552
+
553
+ This doesn’t conflict with ZIO. In ZIO you “compose” logging/tracing via the environment; here `RuntimeHooks` is the equivalent boundary.
554
+
555
+ ---
556
+
557
+ ## Structured JSON log sink
558
+
559
+ Example sink printing JSON:
560
+
561
+ ```ts
562
+ import type { RuntimeEvent, RuntimeEmitContext } from "brass-runtime/core";
563
+
564
+ export const consoleJsonSink = () => (ev: RuntimeEvent, ctx: RuntimeEmitContext) => {
565
+ if (ev.type !== "log") return;
566
+ const level = ev.level ?? "info";
567
+ const out = { level, message: ev.message, fields: ev.fields ?? {}, traceId: ctx.traceId, spanId: ctx.spanId };
568
+ if (level === "error") console.error(JSON.stringify(out));
569
+ else console.log(JSON.stringify(out));
570
+ };
571
+ ```
572
+
573
+ Recommendations:
574
+ - include `traceId/spanId` if available in context
575
+ - prefer structured data over free-form strings
576
+
577
+ ---
578
+
579
+ ## Tracing: propagating traceId/spanId
580
+
581
+ ### Recommended fiber context model
582
+
583
+ - `traceId`: stable for a “request / operation”
584
+ - `spanId`: changes per sub-operation (e.g. fork child or scope span)
585
+
586
+ Simple policy:
587
+
588
+ - when forking, if parent has trace:
589
+ - `traceId` = same
590
+ - `spanId` = new
591
+ - `parentSpanId` = parent’s span
592
+
593
+ ### Where to store it
594
+
595
+ - in a per-fiber `FiberContext`
596
+ - and when emitting events, copy into `RuntimeEmitContext`
597
+
598
+ ---
599
+
600
+ ## InMemoryTracer (for tests)
601
+
602
+ Very useful for tests:
603
+
604
+ - store spans in memory
605
+ - verify they close
606
+ - export only finished spans
607
+
608
+ Recommendation: choose one mapping strategy:
609
+ - span per `scope.open/close`
610
+ - or span per `fiber.start/end`
611
+
612
+ ---
613
+
614
+ ## Practical recipes
615
+
616
+ ### 1) Enabling observability in a Runtime
617
+
618
+ - create an `EventBus`
619
+ - subscribe sinks
620
+ - pass `hooks: eventBus` to the Runtime constructor
621
+
622
+ ```ts
623
+ import { EventBus, Runtime, consoleJsonLogger } from "brass-runtime/core";
624
+
625
+ const bus = new EventBus();
626
+ bus.subscribeHooks(consoleJsonLogger());
627
+
628
+ const runtime = new Runtime({
629
+ env: {},
630
+ hooks: bus
631
+ });
632
+ ```
633
+
634
+ ### 2) Drop policy / budget
635
+ To avoid memory blowups:
636
+
637
+ - ring buffer per sink
638
+ - `flush()` budget
639
+ - emit a periodic “bus.dropped” warning
640
+
641
+ ---
642
+
643
+ ## Checklist
644
+
645
+ - [ ] `Runtime` accepts optional `hooks`
646
+ - [ ] `emit` is non-blocking (enqueue + microtask flush)
647
+ - [ ] at least one “official” log sink exists
648
+ - [ ] tracing propagates through fiber/scope context
649
+ - [ ] 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]