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.
- package/README.md +40 -8
- package/dist/agent/cli/main.cjs +31 -32
- package/dist/agent/cli/main.js +3 -4
- package/dist/agent/cli/main.mjs +3 -4
- package/dist/agent/index.cjs +4 -5
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +3 -4
- package/dist/agent/index.mjs +3 -4
- package/dist/{chunk-GYM3LLGS.mjs → chunk-2QNREG6K.mjs} +188 -5
- package/dist/{chunk-4ROBZFL6.cjs → chunk-2SLT3X6G.cjs} +6 -8
- package/dist/{chunk-KZJQ723N.cjs → chunk-3PFZGP23.cjs} +13 -15
- package/dist/{chunk-AVNQLJ5V.js → chunk-3PHU7FWS.js} +528 -23
- package/dist/{chunk-CIZFIMK5.js → chunk-4YQHPIWJ.js} +60 -11
- package/dist/chunk-5XADBMSU.cjs +33 -0
- package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
- package/dist/chunk-7TKI527D.cjs +123 -0
- package/dist/{chunk-AGR5B2BC.cjs → chunk-7TXQJFZX.cjs} +564 -12
- package/dist/{chunk-RKGKFN2A.js → chunk-AADFFVYS.js} +1 -1
- package/dist/{chunk-52PPNNI4.cjs → chunk-AJMKZXRB.cjs} +2 -2
- package/dist/{chunk-3AYM6WPJ.js → chunk-BG5RNEA2.js} +20 -299
- package/dist/{chunk-2HQTDLHF.mjs → chunk-ELLF55ER.mjs} +555 -3
- package/dist/{chunk-EOC4UHBS.mjs → chunk-G5JTCFMI.mjs} +2 -2
- package/dist/chunk-H5GYX7RZ.js +6126 -0
- package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
- package/dist/{chunk-6IXXWIUM.js → chunk-IBRHSH5H.js} +555 -3
- package/dist/{chunk-Q2I37RP3.cjs → chunk-IFRBVMWJ.cjs} +44 -323
- package/dist/{chunk-52OB2ROS.js → chunk-ITG6I7ZS.js} +2 -4
- package/dist/chunk-ITZQ526U.mjs +33 -0
- package/dist/{chunk-7JIJOVCT.js → chunk-JH4GI3DW.js} +2 -4
- package/dist/{chunk-76YMRMH2.cjs → chunk-KHACHFBQ.cjs} +583 -78
- package/dist/{chunk-MT3OWDPC.mjs → chunk-KRYP6CAE.mjs} +60 -11
- package/dist/chunk-KTGDLBLD.mjs +123 -0
- package/dist/{chunk-ENKODRU3.cjs → chunk-LXBU5E77.cjs} +143 -94
- package/dist/{chunk-PD4EJTQC.cjs → chunk-N6QNSTWD.cjs} +5 -5
- package/dist/{chunk-HLWLMW2F.mjs → chunk-OI4ESUMC.mjs} +9 -11
- package/dist/{chunk-EJ6BPYVR.mjs → chunk-OT2TESZU.mjs} +1 -1
- package/dist/{chunk-BABBZK4Y.js → chunk-PSEU65ND.js} +9 -11
- package/dist/{chunk-DNFO2EIZ.mjs → chunk-QCOLAHU3.mjs} +528 -23
- package/dist/{chunk-KH4SYAOS.mjs → chunk-QZ6QFJNM.mjs} +20 -299
- package/dist/{chunk-MBEJI5HF.mjs → chunk-R6WDSZA6.mjs} +2 -4
- package/dist/{chunk-FHQGHPMO.mjs → chunk-RREBJX2S.mjs} +2 -4
- package/dist/{chunk-5QC7LRZ3.js → chunk-S4HHFUYP.js} +2 -2
- package/dist/{chunk-GLE2WY7Z.cjs → chunk-SSQJKDN3.cjs} +194 -11
- package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
- package/dist/chunk-VIFA4DPN.cjs +6126 -0
- package/dist/chunk-W6WR37HN.js +33 -0
- package/dist/{chunk-FH2X7BVP.js → chunk-XSAHV5HQ.js} +188 -5
- package/dist/chunk-YM3EDNYD.js +123 -0
- package/dist/{chunk-VN44DYYT.cjs → chunk-YTX2JYYP.cjs} +18 -20
- package/dist/chunk-Z3PSSXP3.mjs +6126 -0
- package/dist/core/index.cjs +31 -9
- package/dist/core/index.d.ts +19 -152
- package/dist/core/index.js +80 -58
- package/dist/core/index.mjs +80 -58
- package/dist/defaultClient-DhpCQW9m.d.ts +1623 -0
- package/dist/{effect-DIUHZ9IN.d.ts → effect-CtUDl5M5.d.ts} +1 -1
- package/dist/http/index.cjs +202 -59
- package/dist/http/index.d.ts +55 -819
- package/dist/http/index.js +216 -73
- package/dist/http/index.mjs +216 -73
- package/dist/http/testing.cjs +31 -10
- package/dist/http/testing.d.ts +16 -5
- package/dist/http/testing.js +29 -8
- package/dist/http/testing.mjs +29 -8
- package/dist/index.cjs +110 -88
- package/dist/index.d.ts +9 -8
- package/dist/index.js +81 -59
- package/dist/index.mjs +81 -59
- package/dist/{schedule-CK3Ml_7p.d.ts → layer-BalPI6cN.d.ts} +176 -2
- package/dist/observability/index.cjs +22 -7
- package/dist/observability/index.d.ts +32 -8
- package/dist/observability/index.js +21 -6
- package/dist/observability/index.mjs +21 -6
- package/dist/perf/cli.cjs +26 -28
- package/dist/perf/cli.js +11 -13
- package/dist/perf/cli.mjs +11 -13
- package/dist/perf/index.cjs +13 -15
- package/dist/perf/index.js +11 -13
- package/dist/perf/index.mjs +11 -13
- package/dist/schema/index.cjs +2 -2
- package/dist/schema/index.js +1 -1
- package/dist/schema/index.mjs +1 -1
- package/dist/{server-GJPg8ZSG.d.ts → server-C1zVmqE6.d.ts} +16 -5
- package/dist/{stream-B4oK9JFP.d.ts → stream-Bb4FTejt.d.ts} +1 -1
- package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DzfuE6um.d.ts} +2 -2
- package/dist/{tracing-DqbTKGcf.d.ts → tracing-BABA5arE.d.ts} +1 -1
- package/docs/README.md +4 -0
- package/docs/ai/PUBLIC_API.md +31 -7
- package/docs/articles/brass-runtime-http-observability.md +467 -0
- package/docs/framework-integrations.md +38 -0
- package/docs/frameworks/angular.md +204 -0
- package/docs/frameworks/express.md +183 -0
- package/docs/frameworks/fastify.md +173 -0
- package/docs/frameworks/nestjs.md +335 -0
- package/docs/frameworks/nextjs.md +202 -0
- package/docs/frameworks/react.md +183 -0
- package/docs/frameworks/vanilla.md +280 -0
- package/docs/guides/layers.md +130 -0
- package/docs/http-recipes.md +31 -1
- package/docs/http.md +50 -1
- package/docs/nestjs.md +6 -0
- package/docs/observability-framework-examples.md +12 -0
- package/docs/observability.md +239 -0
- package/docs/performance-profiler.md +6 -2
- package/docs/recipes/layers.md +46 -2
- package/docs/recipes/testing.md +25 -0
- package/package.json +4 -1
- package/dist/chunk-3LOYJFRR.cjs +0 -300
- package/dist/chunk-3Y2RIUMM.js +0 -300
- package/dist/chunk-5EC274J5.cjs +0 -2874
- package/dist/chunk-5VRJNBLZ.mjs +0 -2874
- package/dist/chunk-62AZW6UT.cjs +0 -313
- package/dist/chunk-74ZTY6CP.js +0 -2871
- package/dist/chunk-7CMJS3QE.mjs +0 -2871
- package/dist/chunk-A2OM6NEH.mjs +0 -194
- package/dist/chunk-B33ICAKP.js +0 -313
- package/dist/chunk-JF5WGYJJ.cjs +0 -194
- package/dist/chunk-KN32XNTH.mjs +0 -313
- package/dist/chunk-KQLYONSE.cjs +0 -2871
- package/dist/chunk-L2SYFEBS.js +0 -194
- package/dist/chunk-MIIYDLGM.js +0 -2874
- package/dist/chunk-PWC3RBQE.mjs +0 -300
- package/dist/client-CZHU674n.d.ts +0 -820
package/docs/guides/layers.md
CHANGED
|
@@ -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.
|
package/docs/http-recipes.md
CHANGED
|
@@ -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
|
|
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
|
@@ -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`
|
package/docs/observability.md
CHANGED
|
@@ -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,
|
|
145
|
-
|
|
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`
|
package/docs/recipes/layers.md
CHANGED
|
@@ -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(
|
|
36
|
-
|
|
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
|
+
```
|
package/docs/recipes/testing.md
CHANGED
|
@@ -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.
|
|
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",
|