brass-runtime 1.16.1 → 1.18.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 (123) hide show
  1. package/README.md +40 -8
  2. package/dist/agent/cli/main.cjs +31 -32
  3. package/dist/agent/cli/main.js +3 -4
  4. package/dist/agent/cli/main.mjs +3 -4
  5. package/dist/agent/index.cjs +4 -5
  6. package/dist/agent/index.d.ts +1 -1
  7. package/dist/agent/index.js +3 -4
  8. package/dist/agent/index.mjs +3 -4
  9. package/dist/{chunk-GYM3LLGS.mjs → chunk-2QNREG6K.mjs} +188 -5
  10. package/dist/{chunk-4ROBZFL6.cjs → chunk-2SLT3X6G.cjs} +6 -8
  11. package/dist/{chunk-KZJQ723N.cjs → chunk-3PFZGP23.cjs} +13 -15
  12. package/dist/{chunk-AVNQLJ5V.js → chunk-3PHU7FWS.js} +528 -23
  13. package/dist/{chunk-CIZFIMK5.js → chunk-4YQHPIWJ.js} +60 -11
  14. package/dist/chunk-5XADBMSU.cjs +33 -0
  15. package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
  16. package/dist/chunk-7TKI527D.cjs +123 -0
  17. package/dist/{chunk-AGR5B2BC.cjs → chunk-7TXQJFZX.cjs} +564 -12
  18. package/dist/{chunk-RKGKFN2A.js → chunk-AADFFVYS.js} +1 -1
  19. package/dist/{chunk-52PPNNI4.cjs → chunk-AJMKZXRB.cjs} +2 -2
  20. package/dist/{chunk-3AYM6WPJ.js → chunk-BG5RNEA2.js} +20 -299
  21. package/dist/{chunk-2HQTDLHF.mjs → chunk-ELLF55ER.mjs} +555 -3
  22. package/dist/{chunk-EOC4UHBS.mjs → chunk-G5JTCFMI.mjs} +2 -2
  23. package/dist/chunk-H5GYX7RZ.js +6126 -0
  24. package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
  25. package/dist/{chunk-6IXXWIUM.js → chunk-IBRHSH5H.js} +555 -3
  26. package/dist/{chunk-Q2I37RP3.cjs → chunk-IFRBVMWJ.cjs} +44 -323
  27. package/dist/{chunk-52OB2ROS.js → chunk-ITG6I7ZS.js} +2 -4
  28. package/dist/chunk-ITZQ526U.mjs +33 -0
  29. package/dist/{chunk-7JIJOVCT.js → chunk-JH4GI3DW.js} +2 -4
  30. package/dist/{chunk-76YMRMH2.cjs → chunk-KHACHFBQ.cjs} +583 -78
  31. package/dist/{chunk-MT3OWDPC.mjs → chunk-KRYP6CAE.mjs} +60 -11
  32. package/dist/chunk-KTGDLBLD.mjs +123 -0
  33. package/dist/{chunk-ENKODRU3.cjs → chunk-LXBU5E77.cjs} +143 -94
  34. package/dist/{chunk-PD4EJTQC.cjs → chunk-N6QNSTWD.cjs} +5 -5
  35. package/dist/{chunk-HLWLMW2F.mjs → chunk-OI4ESUMC.mjs} +9 -11
  36. package/dist/{chunk-EJ6BPYVR.mjs → chunk-OT2TESZU.mjs} +1 -1
  37. package/dist/{chunk-BABBZK4Y.js → chunk-PSEU65ND.js} +9 -11
  38. package/dist/{chunk-DNFO2EIZ.mjs → chunk-QCOLAHU3.mjs} +528 -23
  39. package/dist/{chunk-KH4SYAOS.mjs → chunk-QZ6QFJNM.mjs} +20 -299
  40. package/dist/{chunk-MBEJI5HF.mjs → chunk-R6WDSZA6.mjs} +2 -4
  41. package/dist/{chunk-FHQGHPMO.mjs → chunk-RREBJX2S.mjs} +2 -4
  42. package/dist/{chunk-5QC7LRZ3.js → chunk-S4HHFUYP.js} +2 -2
  43. package/dist/{chunk-GLE2WY7Z.cjs → chunk-SSQJKDN3.cjs} +194 -11
  44. package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
  45. package/dist/chunk-VIFA4DPN.cjs +6126 -0
  46. package/dist/chunk-W6WR37HN.js +33 -0
  47. package/dist/{chunk-FH2X7BVP.js → chunk-XSAHV5HQ.js} +188 -5
  48. package/dist/chunk-YM3EDNYD.js +123 -0
  49. package/dist/{chunk-VN44DYYT.cjs → chunk-YTX2JYYP.cjs} +18 -20
  50. package/dist/chunk-Z3PSSXP3.mjs +6126 -0
  51. package/dist/core/index.cjs +31 -9
  52. package/dist/core/index.d.ts +19 -152
  53. package/dist/core/index.js +80 -58
  54. package/dist/core/index.mjs +80 -58
  55. package/dist/defaultClient-DhpCQW9m.d.ts +1623 -0
  56. package/dist/{effect-DIUHZ9IN.d.ts → effect-CtUDl5M5.d.ts} +1 -1
  57. package/dist/http/index.cjs +202 -59
  58. package/dist/http/index.d.ts +55 -819
  59. package/dist/http/index.js +216 -73
  60. package/dist/http/index.mjs +216 -73
  61. package/dist/http/testing.cjs +31 -10
  62. package/dist/http/testing.d.ts +16 -5
  63. package/dist/http/testing.js +29 -8
  64. package/dist/http/testing.mjs +29 -8
  65. package/dist/index.cjs +110 -88
  66. package/dist/index.d.ts +9 -8
  67. package/dist/index.js +81 -59
  68. package/dist/index.mjs +81 -59
  69. package/dist/{schedule-CK3Ml_7p.d.ts → layer-BalPI6cN.d.ts} +176 -2
  70. package/dist/observability/index.cjs +22 -7
  71. package/dist/observability/index.d.ts +32 -8
  72. package/dist/observability/index.js +21 -6
  73. package/dist/observability/index.mjs +21 -6
  74. package/dist/perf/cli.cjs +26 -28
  75. package/dist/perf/cli.js +11 -13
  76. package/dist/perf/cli.mjs +11 -13
  77. package/dist/perf/index.cjs +13 -15
  78. package/dist/perf/index.js +11 -13
  79. package/dist/perf/index.mjs +11 -13
  80. package/dist/schema/index.cjs +2 -2
  81. package/dist/schema/index.js +1 -1
  82. package/dist/schema/index.mjs +1 -1
  83. package/dist/{server-GJPg8ZSG.d.ts → server-C1zVmqE6.d.ts} +16 -5
  84. package/dist/{stream-B4oK9JFP.d.ts → stream-Bb4FTejt.d.ts} +1 -1
  85. package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DzfuE6um.d.ts} +2 -2
  86. package/dist/{tracing-DqbTKGcf.d.ts → tracing-BABA5arE.d.ts} +1 -1
  87. package/docs/README.md +4 -0
  88. package/docs/ai/PUBLIC_API.md +31 -7
  89. package/docs/articles/brass-runtime-http-observability.md +467 -0
  90. package/docs/framework-integrations.md +38 -0
  91. package/docs/frameworks/angular.md +204 -0
  92. package/docs/frameworks/express.md +183 -0
  93. package/docs/frameworks/fastify.md +173 -0
  94. package/docs/frameworks/nestjs.md +335 -0
  95. package/docs/frameworks/nextjs.md +202 -0
  96. package/docs/frameworks/react.md +183 -0
  97. package/docs/frameworks/vanilla.md +280 -0
  98. package/docs/guides/layers.md +130 -0
  99. package/docs/http-recipes.md +31 -1
  100. package/docs/http.md +50 -1
  101. package/docs/nestjs.md +6 -0
  102. package/docs/observability-framework-examples.md +12 -0
  103. package/docs/observability.md +239 -0
  104. package/docs/performance-profiler.md +6 -2
  105. package/docs/recipes/layers.md +46 -2
  106. package/docs/recipes/testing.md +25 -0
  107. package/package.json +4 -1
  108. package/dist/chunk-3LOYJFRR.cjs +0 -300
  109. package/dist/chunk-3Y2RIUMM.js +0 -300
  110. package/dist/chunk-5EC274J5.cjs +0 -2874
  111. package/dist/chunk-5VRJNBLZ.mjs +0 -2874
  112. package/dist/chunk-62AZW6UT.cjs +0 -313
  113. package/dist/chunk-74ZTY6CP.js +0 -2871
  114. package/dist/chunk-7CMJS3QE.mjs +0 -2871
  115. package/dist/chunk-A2OM6NEH.mjs +0 -194
  116. package/dist/chunk-B33ICAKP.js +0 -313
  117. package/dist/chunk-JF5WGYJJ.cjs +0 -194
  118. package/dist/chunk-KN32XNTH.mjs +0 -313
  119. package/dist/chunk-KQLYONSE.cjs +0 -2871
  120. package/dist/chunk-L2SYFEBS.js +0 -194
  121. package/dist/chunk-MIIYDLGM.js +0 -2874
  122. package/dist/chunk-PWC3RBQE.mjs +0 -300
  123. package/dist/client-CZHU674n.d.ts +0 -820
@@ -111,6 +111,136 @@ await runtime.toPromise(
111
111
  );
112
112
  ```
113
113
 
114
+ For wider independent graphs, prefer `Layer.all(...)` / `mergeAll(...)` over
115
+ deeply nested `merge(...)` calls:
116
+
117
+ ```ts
118
+ const AppLayer = Layer.all(ConfigLayer, DbLayer, CacheLayer);
119
+ ```
120
+
121
+ For ordered graphs where each context layer reads services produced by previous
122
+ layers, use `Layer.composeAll(...)`:
123
+
124
+ ```ts
125
+ const AppLayer = Layer.composeAll(ConfigLayer, DbLayer, RepoLayer);
126
+ ```
127
+
128
+ ## Accessing Services
129
+
130
+ Use `Layer.use(...)` or `Layer.useAll(...)` when a program should consume
131
+ services without manually calling `ctx.unsafeGet(...)`.
132
+
133
+ ```ts
134
+ import { Layer, asyncSucceed } from "brass-runtime";
135
+
136
+ const Config = Layer.tag<{ readonly baseUrl: string }>("Config");
137
+ const Http = Layer.tag<{ readonly get: (path: string) => string }>("Http");
138
+
139
+ const ConfigLayer = Layer.value(Config, { baseUrl: "https://api.example.com" });
140
+ const HttpLayer = Layer.effect(Http, (ctx) => {
141
+ const config = ctx.unsafeGet(Config);
142
+ return asyncSucceed({
143
+ get: (path) => `${config.baseUrl}${path}`,
144
+ });
145
+ });
146
+
147
+ const AppLayer = Layer.composeAll(ConfigLayer, HttpLayer);
148
+
149
+ await runtime.toPromise(
150
+ Layer.provideContext(
151
+ AppLayer,
152
+ Layer.useAll({ config: Config, http: Http }, ({ config, http }) =>
153
+ asyncSucceed(http.get(`/users?origin=${config.baseUrl}`)),
154
+ ),
155
+ ),
156
+ );
157
+ ```
158
+
159
+ `getService(tag)` and `getServices({ ...tags })` remain available when a program
160
+ is directly evaluated with a `LayerContext` as its environment. `Layer.use(...)`
161
+ and `Layer.useAll(...)` are better fits for `Layer.provideContext(...)`.
162
+
163
+ ## Application Graphs
164
+
165
+ Application modules can model config, observability, and HTTP clients as
166
+ services. That keeps framework code focused on wiring and lets tests swap any
167
+ piece with a smaller layer.
168
+
169
+ ```ts
170
+ import { Runtime, Layer, makeConfigLayer } from "brass-runtime/core";
171
+ import { s } from "brass-runtime/schema";
172
+ import { defineHttpPolicyPresets, HttpClientService } from "brass-runtime/http";
173
+ import {
174
+ makeObservabilityLayer,
175
+ makeObservedRuntimeLayer,
176
+ makeObservedHttpClientLayer,
177
+ makeOtlpOptions,
178
+ } from "brass-runtime/observability";
179
+
180
+ type AppConfig = {
181
+ readonly serviceName: string;
182
+ readonly apiBaseUrl: string;
183
+ readonly otlpEndpoint: string;
184
+ };
185
+
186
+ const AppConfig = Layer.tag<AppConfig>("AppConfig");
187
+ const AppConfigSchema = s.object({
188
+ serviceName: s.nonEmptyString(),
189
+ apiBaseUrl: s.url(),
190
+ otlpEndpoint: s.url(),
191
+ });
192
+
193
+ const policyPresets = defineHttpPolicyPresets({
194
+ readModel: { lane: "read-model", priority: 3, retry: { maxRetries: 2 } },
195
+ command: { lane: "command", priority: 1, retry: false },
196
+ });
197
+
198
+ const ConfigLayer = makeConfigLayer(AppConfig, AppConfigSchema, {
199
+ serviceName: "orders-api",
200
+ apiBaseUrl: "https://users-api.internal",
201
+ otlpEndpoint: "http://grafana-alloy:4318",
202
+ });
203
+
204
+ const ObservabilityLayer = makeObservabilityLayer((ctx) => {
205
+ const config = ctx.unsafeGet(AppConfig);
206
+ return {
207
+ serviceName: config.serviceName,
208
+ otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }),
209
+ flushIntervalMs: 10_000,
210
+ autoStart: true,
211
+ };
212
+ });
213
+
214
+ const HttpLayer = makeObservedHttpClientLayer((ctx) => {
215
+ const config = ctx.unsafeGet(AppConfig);
216
+ return {
217
+ baseUrl: config.apiBaseUrl,
218
+ preset: "production",
219
+ policyPresets,
220
+ };
221
+ }, {
222
+ httpObservability: {
223
+ policy: { labelKeys: ["preset", "lane"] },
224
+ },
225
+ });
226
+
227
+ const AppLayer = Layer.composeAll(
228
+ ConfigLayer,
229
+ ObservabilityLayer,
230
+ makeObservedRuntimeLayer(),
231
+ HttpLayer,
232
+ );
233
+
234
+ const program = Layer.use(HttpClientService, (http) =>
235
+ http.getJson("/users/42", { policy: "readModel" }),
236
+ );
237
+
238
+ const runtime = Runtime.make({});
239
+ const response = await runtime.toPromise(
240
+ Layer.provideContext(AppLayer, program),
241
+ );
242
+ ```
243
+
114
244
  ## Scoped Builds
115
245
 
116
246
  Use `buildLayer` or `Layer.build` when a caller wants manual lifecycle control.
@@ -72,6 +72,35 @@ configs automatically. `.json()` infers Axios/Fetch-shaped responses. Use
72
72
  `.json((res) => res.payload, (res) => ({ status: res.code }))` when the
73
73
  external client uses a different response shape.
74
74
 
75
+ ## Node Proxy Transport
76
+
77
+ ```ts
78
+ import { toPromise } from "brass-runtime";
79
+ import { makeNodeHttpProxyClient } from "brass-runtime/http";
80
+
81
+ const http = makeNodeHttpProxyClient({
82
+ baseUrl: "https://api.example.com",
83
+ nodeTransport: {
84
+ maxSockets: 512,
85
+ maxFreeSockets: 512,
86
+ },
87
+ pool: {
88
+ key: "origin",
89
+ concurrency: 512,
90
+ maxQueue: 512,
91
+ },
92
+ });
93
+
94
+ await http.getJson("/users/1").unsafeRunPromise();
95
+ await toPromise(http.shutdown(), {});
96
+ ```
97
+
98
+ Use this in Node BFF/proxy services when benchmark evidence shows the default
99
+ `fetch` backend is the bottleneck. The factory uses
100
+ `preset: "highThroughputProxy"` and performs the final I/O through
101
+ `node:http` / `node:https` keep-alive agents while keeping Brass cancellation,
102
+ pooling, stats, policy, and observability.
103
+
75
104
  ## Named Policy Presets
76
105
 
77
106
  ```ts
@@ -363,7 +392,8 @@ try {
363
392
  `preset: "production"` is the explicit name for the full default stack:
364
393
  timeout, priority, retry, dedup, adaptive limiter, safe-method response cache,
365
394
  response compression, stats, and shutdown. `preset: "default"` is the same
366
- stack kept for compatibility.
395
+ stack kept for compatibility. `preset: "highThroughputProxy"` is the explicit
396
+ hot BFF/proxy preset, and `preset: "proxy"` is its shorter compatibility alias.
367
397
 
368
398
  Construction-time validation catches invalid setup before traffic starts:
369
399
 
package/docs/http.md CHANGED
@@ -205,6 +205,28 @@ That keeps timeout, pool/adaptive limiter, stats, retry, cache, deduplication
205
205
  and cancellation in Brass while letting the final I/O backend be `fetch`,
206
206
  Axios, undici, a test double, or an internal client.
207
207
 
208
+ For Node BFF/proxy workloads where `fetch`/Undici is the limiting cost, Brass
209
+ also ships a first-party `node:http` / `node:https` transport and a Node-only
210
+ factory for the recommended high-throughput proxy shape:
211
+
212
+ ```ts
213
+ import { toPromise } from "brass-runtime";
214
+ import { makeNodeHttpProxyClient } from "brass-runtime/http";
215
+
216
+ const http = makeNodeHttpProxyClient({
217
+ baseUrl: "https://api.example.com",
218
+ nodeTransport: {
219
+ maxSockets: 512,
220
+ maxFreeSockets: 512,
221
+ },
222
+ });
223
+
224
+ await toPromise(http.shutdown(), {}); // closes owned Node agents
225
+ ```
226
+
227
+ Use this path only in Node services. Browser and edge runtimes should keep the
228
+ default `fetch` transport or inject their platform client.
229
+
208
230
  ```ts
209
231
  import {
210
232
  makeDefaultHttpClient,
@@ -358,7 +380,10 @@ Named presets are available as `conservative`, `balanced`, and `aggressive`.
358
380
  The default HTTP client uses `balanced` for `preset: "balanced"` and
359
381
  `aggressive` for `preset: "default"` / `preset: "production"`.
360
382
  `production` is the explicit name for the full production-ready default stack;
361
- `default` remains as the compatibility name. Use `adaptiveLimiterPresets` or
383
+ `default` remains as the compatibility name. Use
384
+ `preset: "highThroughputProxy"` for high-throughput BFF/proxy paths where
385
+ Brass should not add priority/adaptive queues or timeout timers by default;
386
+ `preset: "proxy"` is the shorter compatibility alias. Use `adaptiveLimiterPresets` or
362
387
  `makeAdaptiveLimiterConfig(preset, overrides)` when you want a documented
363
388
  adaptive limiter baseline with a few local overrides.
364
389
 
@@ -371,6 +396,25 @@ attributes receive `preset`, `lane`, `poolKey`, `dedupKey`, `priority`, and retr
371
396
  overrides automatically, while metric labels stay opt-in through
372
397
  `policy.labelKeys` to avoid accidental high-cardinality metrics.
373
398
 
399
+ For application graphs, the HTTP subpath also exposes a DI layer helper. The
400
+ layer owns the default client lifecycle and calls `shutdown()` when released:
401
+
402
+ ```ts
403
+ import { Layer } from "brass-runtime/core";
404
+ import { HttpClientService, makeDefaultHttpClientLayer } from "brass-runtime/http";
405
+
406
+ const Config = Layer.tag<{ readonly apiBaseUrl: string }>("Config");
407
+
408
+ const HttpLayer = makeDefaultHttpClientLayer((ctx) => ({
409
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
410
+ preset: "production",
411
+ }));
412
+
413
+ const program = Layer.use(HttpClientService, (http) =>
414
+ http.getJson("/users/42"),
415
+ );
416
+ ```
417
+
374
418
  See [`http-recipes.md`](http-recipes.md) for typed API client, testing,
375
419
  observability, retry, adaptive limiter, and config validation recipes.
376
420
 
@@ -490,6 +534,7 @@ adopters' tests:
490
534
  ```ts
491
535
  import {
492
536
  makeJsonHttpResponse,
537
+ makeMockDefaultHttpClientLayer,
493
538
  makeMockHttpClient,
494
539
  runHttpEffect,
495
540
  withMockFetch,
@@ -497,6 +542,10 @@ import {
497
542
 
498
543
  const mock = makeMockHttpClient((req) => makeJsonHttpResponse({ url: req.url }));
499
544
  const wire = await runHttpEffect(mock({ method: "GET", url: "/users/1" }));
545
+
546
+ const MockHttpLayer = makeMockDefaultHttpClientLayer((req) =>
547
+ makeJsonHttpResponse({ url: req.url }),
548
+ );
500
549
  ```
501
550
 
502
551
  ---
package/docs/nestjs.md ADDED
@@ -0,0 +1,6 @@
1
+ # NestJS integration
2
+
3
+ The NestJS recipe now lives at [`docs/frameworks/nestjs.md`](./frameworks/nestjs.md).
4
+
5
+ This file is kept as a compatibility pointer for older links.
6
+
@@ -7,6 +7,10 @@ flush OTLP metrics/traces.
7
7
  They use optional framework dependencies so the runtime package does not force
8
8
  Express, Fastify, or Nest into normal installs.
9
9
 
10
+ For production-style integration recipes across React, Next.js, Angular,
11
+ Express, Fastify, and Nest, see
12
+ [`docs/framework-integrations.md`](./framework-integrations.md).
13
+
10
14
  ## Express
11
15
 
12
16
  ```bash
@@ -61,9 +65,17 @@ curl http://localhost:3002/metrics
61
65
 
62
66
  Source: `src/examples/observabilityNest.ts`.
63
67
 
68
+ For a production-style Nest module with DI tokens, Grafana/OTLP configuration,
69
+ an observed Brass HTTP client, and shutdown wiring, see
70
+ [`docs/frameworks/nestjs.md`](./frameworks/nestjs.md).
71
+
64
72
  ## What the examples demonstrate
65
73
 
66
74
  - `makeObservabilityFromEnv(process.env)` for deployment-style setup.
75
+ - `makeOtlpOptions(...)` for collector endpoint configuration without adding
76
+ vendor-specific code to Brass.
77
+ - `withHttpObservability(...)` around the Brass HTTP client for outbound
78
+ metrics, spans, logs, policy context, and trace propagation.
67
79
  - Framework-specific request adapters:
68
80
  - `makeExpressRequestObservabilityContext`
69
81
  - `makeFastifyRequestObservabilityContext`
@@ -236,6 +236,91 @@ The exporters are dependency-free:
236
236
  - `makeObservability` returns `hooks`, `env`, `metrics`, `tracer`, exporters,
237
237
  plus `flush()`, `start()`, `stop()`, and `shutdown()`.
238
238
 
239
+ ### High-TPS HTTP proxy path
240
+
241
+ For BFF/proxy paths where p99 matters more than full per-request tracing, keep
242
+ HTTP client metrics separate from global runtime hooks. Runtime hooks are useful
243
+ for fiber/span/log visibility, but on a hot proxy path they add work to every
244
+ effect execution.
245
+
246
+ ```ts
247
+ import { Runtime } from "brass-runtime/core";
248
+ import { makeDefaultHttpClient } from "brass-runtime/http";
249
+ import { makeObservability, withHttpObservability } from "brass-runtime/observability";
250
+
251
+ const observability = makeObservability({
252
+ metrics: false,
253
+ logs: false,
254
+ traces: false,
255
+ autoStart: false,
256
+ });
257
+
258
+ // Deliberately no observability hooks on this runtime.
259
+ const runtime = Runtime.make({});
260
+
261
+ const http = makeDefaultHttpClient({
262
+ baseUrl: "https://api.example.com",
263
+ preset: "highThroughputProxy",
264
+ middleware: [
265
+ withHttpObservability({
266
+ metrics: observability.metrics,
267
+ logs: false,
268
+ spans: false,
269
+ adaptiveLimiter: false,
270
+ injectTraceHeaders: false,
271
+ includeHostLabel: false,
272
+ route: "/downstream/:id",
273
+ }),
274
+ ],
275
+ });
276
+
277
+ await runtime.toPromise(http.getJson("/downstream/123"));
278
+ ```
279
+
280
+ Use stable `route` values instead of raw URLs, and enable `includeHostLabel`
281
+ only when the client really talks to multiple downstream hosts. If you need
282
+ traces on the same path, prefer HTTP-only sampled spans through `spanSink`
283
+ instead of global runtime hooks. Use a separate fully observed runtime/client
284
+ for diagnostic flows that need every fiber event.
285
+
286
+ For a sampled span path with lower per-request overhead, keep runtime metrics
287
+ off, avoid runtime hooks, avoid per-request HTTP span events, and only inject
288
+ `traceparent` when a downstream needs propagation:
289
+
290
+ ```ts
291
+ const traced = makeObservability({
292
+ metrics: false,
293
+ logs: false,
294
+ sampling: 0.001,
295
+ autoStart: false,
296
+ });
297
+
298
+ const tracedRuntime = new Runtime({
299
+ env: traced.env,
300
+ });
301
+
302
+ const tracedHttp = makeDefaultHttpClient({
303
+ baseUrl: "https://api.example.com",
304
+ preset: "highThroughputProxy",
305
+ middleware: [
306
+ withHttpObservability({
307
+ metrics: traced.metrics,
308
+ logs: false,
309
+ spans: { events: false, sampleRate: 0.001 },
310
+ spanSink: traced.tracer,
311
+ adaptiveLimiter: false,
312
+ injectTraceHeaders: false,
313
+ includeHostLabel: false,
314
+ route: "/downstream/:id",
315
+ }),
316
+ ],
317
+ });
318
+ ```
319
+
320
+ `sampleRate` lets the HTTP middleware skip tracing before touching the current
321
+ fiber/runtime on unsampled requests. This keeps p99 low on hot proxy paths while
322
+ still retaining a sampled trace stream.
323
+
239
324
  ### HTTP policy observability
240
325
 
241
326
  ```ts
@@ -327,6 +412,156 @@ collector does not create overlapping exports.
327
412
  Finished spans are pruned after successful export and can also be bounded with
328
413
  `traces.maxFinishedSpans` / `traces.maxSpanAgeMs`.
329
414
 
415
+ ### Vendor-neutral collector recipes
416
+
417
+ Brass intentionally does not know about Grafana Cloud, AppDynamics, or any
418
+ OpenTelemetry SDK implementation. The runtime only needs OTLP HTTP endpoint
419
+ URLs, headers, and optional export tuning. Keep vendor naming in application
420
+ code by writing small helpers that call the backend-neutral `makeOtlpOptions`.
421
+
422
+ ```ts
423
+ import {
424
+ makeObservability,
425
+ makeOtlpOptions,
426
+ type ObservabilityOtlpOptions,
427
+ } from "brass-runtime/observability";
428
+
429
+ function productionOtlp(input: {
430
+ readonly endpoint: string;
431
+ readonly headers?: Record<string, string>;
432
+ }): ObservabilityOtlpOptions {
433
+ return makeOtlpOptions({
434
+ endpoint: input.endpoint,
435
+ headers: input.headers,
436
+ timeoutMs: 10_000,
437
+ retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
438
+ pipeline: {
439
+ maxQueueSize: 10_000,
440
+ batchSize: 512,
441
+ dropPolicy: "drop-oldest",
442
+ shutdownTimeoutMs: 10_000,
443
+ },
444
+ });
445
+ }
446
+ ```
447
+
448
+ Grafana Cloud can be configured as a direct OTLP endpoint or through
449
+ Grafana Alloy/OpenTelemetry Collector. The helper stays in your app and only
450
+ returns Brass OTLP config:
451
+
452
+ ```ts
453
+ function grafanaCloudCollector(input: {
454
+ readonly endpoint: string;
455
+ readonly authorization?: string;
456
+ }): ObservabilityOtlpOptions {
457
+ return productionOtlp({
458
+ endpoint: input.endpoint,
459
+ headers: input.authorization
460
+ ? { Authorization: input.authorization }
461
+ : undefined,
462
+ });
463
+ }
464
+
465
+ const observability = makeObservability({
466
+ serviceName: "shopping-ms",
467
+ serviceVersion: "1.2.3",
468
+ resource: {
469
+ "service.namespace": "shopping",
470
+ "deployment.environment": "production",
471
+ },
472
+ otlp: grafanaCloudCollector({
473
+ endpoint: process.env.GRAFANA_OTLP_ENDPOINT!,
474
+ authorization: process.env.GRAFANA_OTLP_AUTHORIZATION,
475
+ }),
476
+ });
477
+ ```
478
+
479
+ For AppDynamics, prefer sending Brass telemetry to the AppDynamics/OpenTelemetry
480
+ Collector deployed next to the service. Authentication and vendor-specific
481
+ exporters stay in the collector config:
482
+
483
+ ```ts
484
+ function appDynamicsCollector(input: {
485
+ readonly endpoint: string;
486
+ }): ObservabilityOtlpOptions {
487
+ return productionOtlp({
488
+ endpoint: input.endpoint,
489
+ });
490
+ }
491
+
492
+ const observability = makeObservability({
493
+ serviceName: "car-rental-ms",
494
+ serviceVersion: "1.2.3",
495
+ resource: {
496
+ "service.namespace": "car-rental",
497
+ "deployment.environment": "production",
498
+ },
499
+ otlp: appDynamicsCollector({
500
+ endpoint: process.env.APPD_OTEL_COLLECTOR_ENDPOINT ?? "http://appd-otel-collector:4318",
501
+ }),
502
+ });
503
+ ```
504
+
505
+ Then attach the same observability instance to HTTP without changing the
506
+ collector helpers:
507
+
508
+ ```ts
509
+ import { makeDefaultHttpClient } from "brass-runtime/http";
510
+ import { withHttpObservability } from "brass-runtime/observability";
511
+
512
+ const http = makeDefaultHttpClient({
513
+ baseUrl: "https://api.example.com",
514
+ middleware: [withHttpObservability(observability)],
515
+ });
516
+ ```
517
+
518
+ For applications that use Brass layers, observability can own both the
519
+ observability lifecycle and an observed HTTP client:
520
+
521
+ ```ts
522
+ import { Layer } from "brass-runtime/core";
523
+ import { HttpClientService } from "brass-runtime/http";
524
+ import {
525
+ makeObservabilityLayer,
526
+ makeObservedRuntimeLayer,
527
+ makeObservedHttpClientLayer,
528
+ makeOtlpOptions,
529
+ } from "brass-runtime/observability";
530
+
531
+ const Config = Layer.tag<{
532
+ readonly serviceName: string;
533
+ readonly apiBaseUrl: string;
534
+ readonly otlpEndpoint: string;
535
+ }>("Config");
536
+
537
+ const ConfigLayer = Layer.value(Config, {
538
+ serviceName: "orders-api",
539
+ apiBaseUrl: "https://users-api.internal",
540
+ otlpEndpoint: "http://grafana-alloy:4318",
541
+ });
542
+
543
+ const ObservabilityLayer = makeObservabilityLayer((ctx) => {
544
+ const config = ctx.unsafeGet(Config);
545
+ return {
546
+ serviceName: config.serviceName,
547
+ otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }),
548
+ flushIntervalMs: 10_000,
549
+ autoStart: true,
550
+ };
551
+ });
552
+
553
+ const HttpLayer = makeObservedHttpClientLayer((ctx) => ({
554
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
555
+ preset: "production",
556
+ }));
557
+
558
+ const AppLayer = Layer.composeAll(ConfigLayer, ObservabilityLayer, makeObservedRuntimeLayer(), HttpLayer);
559
+
560
+ const program = Layer.use(HttpClientService, (http) =>
561
+ http.getJson("/users/42"),
562
+ );
563
+ ```
564
+
330
565
  Sampling can be configured globally, by ratio, or with rules:
331
566
 
332
567
  ```ts
@@ -423,6 +658,10 @@ const router = makeHttpRouter([
423
658
 
424
659
  Runnable framework examples live in
425
660
  [`docs/observability-framework-examples.md`](./observability-framework-examples.md).
661
+ Production-style framework integration recipes live in
662
+ [`docs/framework-integrations.md`](./framework-integrations.md).
663
+ For a NestJS module recipe with Grafana/OTLP, DI tokens, HTTP client
664
+ observability, and shutdown wiring, see [`docs/frameworks/nestjs.md`](./frameworks/nestjs.md).
426
665
 
427
666
  Collector smoke and performance budget helpers:
428
667
 
@@ -128,6 +128,9 @@ HTTP layer profile:
128
128
  - `node-http-text`
129
129
  - `wire-raw`
130
130
  - `default-minimal-json`
131
+ - `default-proxy-json`
132
+ - `default-proxy-node-json`
133
+ - `high-throughput-proxy-node-json`
131
134
  - `default-balanced-no-adaptive-json`
132
135
  - `default-balanced-json`
133
136
  - `default-json`
@@ -141,8 +144,9 @@ HTTP long-run memory lab:
141
144
 
142
145
  - defaults to `forceGc: true`, so use `node --expose-gc` for the strongest
143
146
  retained-memory signal
144
- - compares node transport, wire raw, minimal, balanced without adaptive,
145
- balanced, default, and default+observability variants
147
+ - compares node transport, wire raw, minimal, proxy, proxy+node transport,
148
+ high-throughput proxy+node transport, balanced without adaptive, balanced,
149
+ default, and default+observability variants
146
150
  - reports heap/rss totals, max p99, mean throughput, errors, and
147
151
  `heapDeltaPer10kRequestsMb`
148
152
  - highlights whether memory is `ok`, `watch`, `critical`, or `unknown-gc`
@@ -32,8 +32,9 @@ const RepoLayer = Layer.effect(Repo, (ctx: LayerContext) => {
32
32
  const AppLayer = Layer.compose(ConfigLayer, RepoLayer);
33
33
 
34
34
  const userUrl = await runPromise(
35
- provideContext(AppLayer, (ctx) =>
36
- asyncSucceed(ctx.unsafeGet(Repo).findUser("u1")),
35
+ provideContext(
36
+ AppLayer,
37
+ Layer.use(Repo, (repo) => asyncSucceed(repo.findUser("u1"))),
37
38
  ),
38
39
  );
39
40
 
@@ -42,3 +43,46 @@ console.log(userUrl);
42
43
 
43
44
  Missing services throw `MissingLayerServiceError`; use `formatLayerError` when
44
45
  surfacing the message.
46
+
47
+ For independent layers, `Layer.all(...)` keeps composition readable. For
48
+ ordered context graphs where later layers read earlier services, use
49
+ `Layer.composeAll(...)`. `Layer.useAll(...)` reads multiple services without
50
+ manual context access:
51
+
52
+ ```ts
53
+ const Logger = defineService<{ readonly info: (message: string) => void }>("Logger");
54
+ const LoggerLayer = Layer.value(Logger, console);
55
+
56
+ const AppLayer2 = Layer.composeAll(ConfigLayer, RepoLayer, LoggerLayer);
57
+
58
+ await runPromise(
59
+ provideContext(
60
+ AppLayer2,
61
+ Layer.useAll({ repo: Repo, logger: Logger }, ({ repo, logger }) => {
62
+ logger.info("loading user");
63
+ return asyncSucceed(repo.findUser("u1"));
64
+ }),
65
+ ),
66
+ );
67
+ ```
68
+
69
+ For app wiring, prefer the focused helpers:
70
+
71
+ ```ts
72
+ import { RuntimeService, makeConfigLayer, makeRuntimeLayer, makeTestLayer } from "brass-runtime";
73
+ import { s } from "brass-runtime/schema";
74
+
75
+ const ConfigSchema = s.object({ baseUrl: s.url() });
76
+
77
+ const ConfigLayer2 = makeConfigLayer(Config, ConfigSchema, {
78
+ baseUrl: "https://api.example.com",
79
+ });
80
+
81
+ const RuntimeLayer = makeRuntimeLayer((ctx) => ({
82
+ config: ctx.unsafeGet(Config),
83
+ }));
84
+
85
+ const TestConfigLayer = makeTestLayer(Config, {
86
+ baseUrl: "https://test.example.com",
87
+ });
88
+ ```
@@ -33,6 +33,31 @@ const response = await runHttpEffect(client({ method: "GET", url: "/health" }));
33
33
  console.log(response.status);
34
34
  ```
35
35
 
36
+ Layer-based applications can swap services with `makeTestLayer`,
37
+ `makeTestLayers`, or a mock default HTTP client layer:
38
+
39
+ ```ts
40
+ import { Layer, makeTestLayer } from "brass-runtime/core";
41
+ import { HttpClientService } from "brass-runtime/http";
42
+ import {
43
+ makeJsonHttpResponse,
44
+ makeMockDefaultHttpClientLayer,
45
+ } from "brass-runtime/http/testing";
46
+
47
+ const Config = Layer.tag<{ readonly baseUrl: string }>("Config");
48
+
49
+ const TestLayer = Layer.composeAll(
50
+ makeTestLayer(Config, { baseUrl: "test://users" }),
51
+ makeMockDefaultHttpClientLayer((req) =>
52
+ makeJsonHttpResponse({ url: req.url }),
53
+ ),
54
+ );
55
+
56
+ const program = Layer.use(HttpClientService, (http) =>
57
+ http.getJson("/users/1"),
58
+ );
59
+ ```
60
+
36
61
  For typed failures, assert on `Exit` instead of catching thrown values.
37
62
 
38
63
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brass-runtime",
3
- "version": "1.16.1",
3
+ "version": "1.18.0",
4
4
  "description": "Effect runtime utilities for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": "Augusto Vivaldelli",
@@ -116,6 +116,9 @@
116
116
  "benchmark:runtime": "tsx src/benchmarks/runner.ts runtime-performance-track",
117
117
  "benchmark:runtime:budget": "node scripts/check-runtime-benchmark-budget.mjs",
118
118
  "benchmark:http": "tsx src/benchmarks/runner.ts http-concurrent",
119
+ "benchmark:http:proxy": "BRASS_HTTP_BENCH_VARIANTS=node-http-text,default-minimal-json,default-proxy-json,default-proxy-node-json,default-json tsx src/benchmarks/runner.ts http-concurrent",
120
+ "benchmark:http:proxy:300tps": "BRASS_HTTP_RAMP_CLIENT=proxy BRASS_HTTP_RAMP_MAX_TPS=300 BRASS_HTTP_RAMP_STEP_TPS=300 BRASS_HTTP_RAMP_STEP_SECONDS=180 BRASS_HTTP_RAMP_CONCURRENCY=600 tsx src/benchmarks/runner.ts http-ramp-tps",
121
+ "benchmark:http:overhead": "tsx src/benchmarks/runner.ts http-local-overhead",
119
122
  "benchmark:http:soak": "BRASS_HTTP_BENCH_MODE=soak tsx src/benchmarks/runner.ts http-concurrent",
120
123
  "benchmark:http:budget": "node scripts/check-http-benchmark-budget.mjs",
121
124
  "benchmark:http:ramp": "tsx src/benchmarks/runner.ts http-ramp-tps",