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.
Files changed (209) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +673 -136
  3. package/dist/agent/cli/main.cjs +40 -35
  4. package/dist/agent/cli/main.js +9 -4
  5. package/dist/agent/cli/main.mjs +9 -4
  6. package/dist/agent/index.cjs +8 -4
  7. package/dist/agent/index.d.ts +1 -1
  8. package/dist/agent/index.js +7 -3
  9. package/dist/agent/index.mjs +7 -3
  10. package/dist/chunk-2HQTDLHF.mjs +683 -0
  11. package/dist/chunk-36I3M4UC.mjs +370 -0
  12. package/dist/chunk-3AYM6WPJ.js +1629 -0
  13. package/dist/chunk-3LOYJFRR.cjs +300 -0
  14. package/dist/chunk-3RG5ZIWI.js +10 -0
  15. package/dist/chunk-3Y2RIUMM.js +300 -0
  16. package/dist/{chunk-VEZNF5GZ.cjs → chunk-4ROBZFL6.cjs} +130 -126
  17. package/dist/{chunk-3QMOKAS5.js → chunk-52OB2ROS.js} +9 -5
  18. package/dist/chunk-52PPNNI4.cjs +416 -0
  19. package/dist/chunk-5EC274J5.cjs +2874 -0
  20. package/dist/chunk-5QC7LRZ3.js +229 -0
  21. package/dist/chunk-5VRJNBLZ.mjs +2874 -0
  22. package/dist/chunk-62AZW6UT.cjs +313 -0
  23. package/dist/chunk-6IXXWIUM.js +683 -0
  24. package/dist/chunk-74ZTY6CP.js +2871 -0
  25. package/dist/chunk-76YMRMH2.cjs +777 -0
  26. package/dist/chunk-7CMJS3QE.mjs +2871 -0
  27. package/dist/{chunk-4NHES7VK.mjs → chunk-7JIJOVCT.js} +27 -13
  28. package/dist/chunk-A2OM6NEH.mjs +194 -0
  29. package/dist/chunk-AGR5B2BC.cjs +683 -0
  30. package/dist/chunk-AVNQLJ5V.js +777 -0
  31. package/dist/chunk-B33ICAKP.js +313 -0
  32. package/dist/{chunk-ELOOF35R.mjs → chunk-B5JD23U7.mjs} +1 -1
  33. package/dist/chunk-BABBZK4Y.js +2024 -0
  34. package/dist/chunk-C3MDXTRZ.js +354 -0
  35. package/dist/chunk-CIZFIMK5.js +2193 -0
  36. package/dist/chunk-CZIVE6NT.cjs +354 -0
  37. package/dist/chunk-DNFJLJMW.mjs +354 -0
  38. package/dist/chunk-DNFO2EIZ.mjs +777 -0
  39. package/dist/chunk-EJ6BPYVR.mjs +416 -0
  40. package/dist/chunk-ENKODRU3.cjs +2193 -0
  41. package/dist/chunk-EOC4UHBS.mjs +229 -0
  42. package/dist/{chunk-BMH5AV44.js → chunk-FH2X7BVP.js} +756 -440
  43. package/dist/{chunk-PPUXIH5R.js → chunk-FHQGHPMO.mjs} +27 -13
  44. package/dist/{chunk-TGIFUAK4.cjs → chunk-GLE2WY7Z.cjs} +951 -635
  45. package/dist/{chunk-BDF4AMWX.mjs → chunk-GYM3LLGS.mjs} +756 -440
  46. package/dist/chunk-HLWLMW2F.mjs +2024 -0
  47. package/dist/chunk-JF5WGYJJ.cjs +194 -0
  48. package/dist/chunk-KH4SYAOS.mjs +1629 -0
  49. package/dist/chunk-KN32XNTH.mjs +313 -0
  50. package/dist/chunk-KQLYONSE.cjs +2871 -0
  51. package/dist/{chunk-STVLQ3XD.cjs → chunk-KZJQ723N.cjs} +92 -78
  52. package/dist/chunk-L2SYFEBS.js +194 -0
  53. package/dist/chunk-L6VB5N7Q.cjs +104 -0
  54. package/dist/{chunk-K6M7MDZ4.mjs → chunk-MBEJI5HF.mjs} +9 -5
  55. package/dist/chunk-MIIYDLGM.js +2874 -0
  56. package/dist/chunk-MOO4L7F4.mjs +104 -0
  57. package/dist/chunk-MT3OWDPC.mjs +2193 -0
  58. package/dist/chunk-MVGUEJ5Z.cjs +370 -0
  59. package/dist/chunk-OBGZSXTJ.cjs +10 -0
  60. package/dist/chunk-PD4EJTQC.cjs +229 -0
  61. package/dist/chunk-PWC3RBQE.mjs +300 -0
  62. package/dist/chunk-Q2I37RP3.cjs +1629 -0
  63. package/dist/chunk-RKGKFN2A.js +416 -0
  64. package/dist/{chunk-R3R2FVLG.cjs → chunk-SA6HUJVI.cjs} +5 -5
  65. package/dist/chunk-TRM4JUZQ.js +104 -0
  66. package/dist/chunk-UB4B6OFY.js +370 -0
  67. package/dist/{chunk-TO7IKXYT.js → chunk-UCUBNWM2.js} +1 -1
  68. package/dist/chunk-VN44DYYT.cjs +2024 -0
  69. package/dist/chunk-Y6FXYEAI.mjs +10 -0
  70. package/dist/client-CZHU674n.d.ts +820 -0
  71. package/dist/core/index.cjs +198 -4
  72. package/dist/core/index.d.ts +311 -212
  73. package/dist/core/index.js +237 -43
  74. package/dist/core/index.mjs +237 -43
  75. package/dist/{effect-CMOQKX8y.d.ts → effect-DIUHZ9IN.d.ts} +195 -1
  76. package/dist/effectRunner-CFLC32IK.cjs +8 -0
  77. package/dist/effectRunner-L4S7IPT3.js +8 -0
  78. package/dist/effectRunner-NNGG75QA.mjs +8 -0
  79. package/dist/http/index.cjs +1227 -2971
  80. package/dist/http/index.d.ts +826 -280
  81. package/dist/http/index.js +1089 -2833
  82. package/dist/http/index.mjs +1089 -2833
  83. package/dist/http/testing.cjs +161 -0
  84. package/dist/http/testing.d.ts +43 -0
  85. package/dist/http/testing.js +161 -0
  86. package/dist/http/testing.mjs +161 -0
  87. package/dist/index.cjs +486 -250
  88. package/dist/index.d.ts +87 -95
  89. package/dist/index.js +391 -155
  90. package/dist/index.mjs +391 -155
  91. package/dist/observability/index.cjs +162 -0
  92. package/dist/observability/index.d.ts +152 -0
  93. package/dist/observability/index.js +162 -0
  94. package/dist/observability/index.mjs +162 -0
  95. package/dist/perf/cli.cjs +401 -0
  96. package/dist/perf/cli.d.ts +1 -0
  97. package/dist/perf/cli.js +401 -0
  98. package/dist/perf/cli.mjs +401 -0
  99. package/dist/perf/index.cjs +141 -0
  100. package/dist/perf/index.d.ts +483 -0
  101. package/dist/perf/index.js +141 -0
  102. package/dist/perf/index.mjs +141 -0
  103. package/dist/schedule-CK3Ml_7p.d.ts +259 -0
  104. package/dist/schema/index.cjs +29 -0
  105. package/dist/schema/index.d.ts +179 -0
  106. package/dist/schema/index.js +29 -0
  107. package/dist/schema/index.mjs +29 -0
  108. package/dist/server-GJPg8ZSG.d.ts +675 -0
  109. package/dist/{stream-FQm9h4Mg.d.ts → stream-B4oK9JFP.d.ts} +1 -1
  110. package/dist/tracer-Hwt1cl7h.d.ts +189 -0
  111. package/dist/tracing-DqbTKGcf.d.ts +148 -0
  112. package/docs/ARCHITECTURE.md +292 -0
  113. package/docs/README.md +63 -0
  114. package/docs/adr/0001-ai-context-pack.md +32 -0
  115. package/docs/agent-apply-mode.md +104 -0
  116. package/docs/agent-approvals.md +110 -0
  117. package/docs/agent-batch.md +185 -0
  118. package/docs/agent-boundaries.md +112 -0
  119. package/docs/agent-chat-sessions.md +160 -0
  120. package/docs/agent-ci.md +17 -0
  121. package/docs/agent-cli.md +405 -0
  122. package/docs/agent-config.md +480 -0
  123. package/docs/agent-context-discovery.md +159 -0
  124. package/docs/agent-copilot-like-dx.md +126 -0
  125. package/docs/agent-declarative-optimized-planning.md +138 -0
  126. package/docs/agent-dx.md +224 -0
  127. package/docs/agent-env-files.md +126 -0
  128. package/docs/agent-follow-up-context.md +43 -0
  129. package/docs/agent-global-usage.md +180 -0
  130. package/docs/agent-init.md +109 -0
  131. package/docs/agent-install-and-configure.md +516 -0
  132. package/docs/agent-language-workspace-ux.md +99 -0
  133. package/docs/agent-llm-adapters.md +123 -0
  134. package/docs/agent-local-install.md +190 -0
  135. package/docs/agent-local-tests.md +51 -0
  136. package/docs/agent-observability.md +155 -0
  137. package/docs/agent-patch-quality-loop.md +162 -0
  138. package/docs/agent-presets.md +22 -0
  139. package/docs/agent-project-commands.md +237 -0
  140. package/docs/agent-project-intelligence.md +156 -0
  141. package/docs/agent-redaction.md +18 -0
  142. package/docs/agent-release-readiness.md +76 -0
  143. package/docs/agent-rollback-safety.md +162 -0
  144. package/docs/agent-rollback.md +23 -0
  145. package/docs/agent-run-artifacts.md +16 -0
  146. package/docs/agent-vscode-auto-discovery.md +137 -0
  147. package/docs/agent-vscode-batch-runner.md +100 -0
  148. package/docs/agent-vscode-chat-layout.md +90 -0
  149. package/docs/agent-vscode-clean-install.md +147 -0
  150. package/docs/agent-vscode-code-actions.md +70 -0
  151. package/docs/agent-vscode-diff-preview.md +45 -0
  152. package/docs/agent-vscode-inline-assist.md +56 -0
  153. package/docs/agent-vscode-install.md +186 -0
  154. package/docs/agent-vscode-model-setup.md +97 -0
  155. package/docs/agent-vscode-patch-preview.md +92 -0
  156. package/docs/agent-vscode-problems.md +79 -0
  157. package/docs/agent-vscode-project-dashboard.md +106 -0
  158. package/docs/agent-vscode-run-history.md +92 -0
  159. package/docs/agent-vscode-ux.md +73 -0
  160. package/docs/ai/INVARIANTS.md +84 -0
  161. package/docs/ai/PROJECT_MAP.md +338 -0
  162. package/docs/ai/PUBLIC_API.md +336 -0
  163. package/docs/ai/VALIDATION_MATRIX.md +67 -0
  164. package/docs/api-polish.md +37 -0
  165. package/docs/cancellation.md +162 -0
  166. package/docs/coverage.md +46 -0
  167. package/docs/getting-started.md +159 -0
  168. package/docs/guides/README.md +40 -0
  169. package/docs/guides/circuit-breaker.md +89 -0
  170. package/docs/guides/error-handling.md +91 -0
  171. package/docs/guides/getting-started.md +107 -0
  172. package/docs/guides/layers.md +189 -0
  173. package/docs/guides/metrics.md +101 -0
  174. package/docs/guides/resource-management.md +141 -0
  175. package/docs/guides/retry.md +215 -0
  176. package/docs/guides/semaphore.md +66 -0
  177. package/docs/guides/streams.md +117 -0
  178. package/docs/guides/supervisors.md +98 -0
  179. package/docs/guides/testing.md +162 -0
  180. package/docs/guides/tracing.md +71 -0
  181. package/docs/http-recipes.md +399 -0
  182. package/docs/http.md +749 -0
  183. package/docs/modules.md +285 -0
  184. package/docs/observability-collector-smoke.md +31 -0
  185. package/docs/observability-framework-examples.md +98 -0
  186. package/docs/observability.md +542 -0
  187. package/docs/otel-collector-smoke.yaml +27 -0
  188. package/docs/performance-profiler.md +199 -0
  189. package/docs/production-readiness.md +73 -0
  190. package/docs/recipes/README.md +12 -0
  191. package/docs/recipes/http-server.md +45 -0
  192. package/docs/recipes/layers.md +44 -0
  193. package/docs/recipes/performance.md +47 -0
  194. package/docs/recipes/runtime.md +41 -0
  195. package/docs/recipes/testing.md +41 -0
  196. package/docs/release.md +53 -0
  197. package/docs/wasm-bounded-queues.md +44 -0
  198. package/docs/wasm-engine-observability-benchmarks.md +85 -0
  199. package/docs/wasm-fiber-engine.md +117 -0
  200. package/docs/wasm-scheduler-state-machine.md +122 -0
  201. package/docs/wasm-stream-chunks.md +54 -0
  202. package/package.json +48 -2
  203. package/dist/chunk-AR22SXML.js +0 -1043
  204. package/dist/chunk-BDYEENHT.js +0 -224
  205. package/dist/chunk-JFPU5GQI.mjs +0 -1043
  206. package/dist/chunk-MS34J5LY.cjs +0 -224
  207. package/dist/chunk-UMAZLXAB.mjs +0 -224
  208. package/dist/chunk-XPZNXSVN.cjs +0 -1043
  209. 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]