brass-runtime 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- 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-7X3K5RMS.js → chunk-22HZQG5F.js} +9 -11
- package/dist/{chunk-GLE2WY7Z.cjs → chunk-2JHJ4YHS.cjs} +417 -124
- package/dist/{chunk-Q2I37RP3.cjs → chunk-2OW6IFY2.cjs} +44 -323
- package/dist/{chunk-7ZPEZ57L.cjs → chunk-5LC7V2OZ.cjs} +18 -20
- package/dist/{chunk-AGR5B2BC.cjs → chunk-5RZ7YITF.cjs} +564 -12
- package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
- package/dist/{chunk-EJ6BPYVR.mjs → chunk-6V2AWT4R.mjs} +1 -1
- package/dist/{chunk-3AYM6WPJ.js → chunk-7DU7IQHK.js} +20 -299
- package/dist/{chunk-SK7UZRNI.mjs → chunk-7GBJYOX7.mjs} +528 -23
- package/dist/chunk-7TKI527D.cjs +123 -0
- package/dist/{chunk-52OB2ROS.js → chunk-7VQLEN37.js} +2 -4
- package/dist/{chunk-KH4SYAOS.mjs → chunk-B5FKOLTB.mjs} +20 -299
- package/dist/{chunk-FHQGHPMO.mjs → chunk-BC6Q6BCO.mjs} +2 -4
- package/dist/{chunk-4P2HHGAX.mjs → chunk-COOW7BJX.mjs} +32 -11
- package/dist/{chunk-2HQTDLHF.mjs → chunk-EEN5OTCR.mjs} +555 -3
- package/dist/{chunk-KZJQ723N.cjs → chunk-EICAJDNX.cjs} +13 -15
- package/dist/chunk-ELIECDYN.cjs +33 -0
- package/dist/{chunk-GYM3LLGS.mjs → chunk-H626ZTDZ.mjs} +399 -106
- package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
- package/dist/{chunk-7JIJOVCT.js → chunk-IPSMXUWA.js} +2 -4
- package/dist/{chunk-4ROBZFL6.cjs → chunk-J6DUHITE.cjs} +6 -8
- package/dist/{chunk-6RY2FFN4.mjs → chunk-JWIEMBE6.mjs} +9 -11
- package/dist/{chunk-PD4EJTQC.cjs → chunk-KNTJ7FQB.cjs} +5 -5
- package/dist/chunk-KTGDLBLD.mjs +123 -0
- package/dist/chunk-LSYQ3C2M.js +33 -0
- package/dist/{chunk-RKGKFN2A.js → chunk-OW5VHAOE.js} +1 -1
- package/dist/{chunk-EOC4UHBS.mjs → chunk-RBHNOKH4.mjs} +2 -2
- package/dist/{chunk-6IXXWIUM.js → chunk-S4HXADU4.js} +555 -3
- package/dist/{chunk-FH2X7BVP.js → chunk-TTSPIU3U.js} +399 -106
- package/dist/{chunk-5QC7LRZ3.js → chunk-UAKAF32U.js} +2 -2
- package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
- package/dist/{chunk-MBEJI5HF.mjs → chunk-WCBNXPN6.mjs} +2 -4
- package/dist/{chunk-52PPNNI4.cjs → chunk-WGE2FEZE.cjs} +2 -2
- package/dist/{chunk-WBGRHGBP.cjs → chunk-WI7GZF3B.cjs} +114 -93
- package/dist/chunk-WUDHOZIH.js +6234 -0
- package/dist/{chunk-F6XWZQY4.cjs → chunk-WVSZOPGQ.cjs} +583 -78
- package/dist/chunk-XPIMJQYS.cjs +6234 -0
- package/dist/{chunk-VWIPB6I5.js → chunk-YGR2IN4R.js} +528 -23
- package/dist/chunk-YM3EDNYD.js +123 -0
- package/dist/chunk-YWLLH27R.mjs +33 -0
- package/dist/{chunk-BKK77SBA.js → chunk-YZ5LQ32F.js} +32 -11
- package/dist/chunk-Z3ZZMQUZ.mjs +6234 -0
- package/dist/core/index.cjs +37 -9
- package/dist/core/index.d.ts +19 -152
- package/dist/core/index.js +86 -58
- package/dist/core/index.mjs +86 -58
- package/dist/defaultClient-Cid0JoUR.d.ts +1648 -0
- package/dist/{effect-DIUHZ9IN.d.ts → effect-DnGUuhw6.d.ts} +22 -1
- package/dist/http/index.cjs +206 -59
- package/dist/http/index.d.ts +55 -819
- package/dist/http/index.js +220 -73
- package/dist/http/index.mjs +220 -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 +116 -88
- package/dist/index.d.ts +9 -8
- package/dist/index.js +87 -59
- package/dist/index.mjs +87 -59
- package/dist/{schedule-CK3Ml_7p.d.ts → layer-D2LFcBVx.d.ts} +176 -2
- package/dist/observability/index.cjs +20 -7
- package/dist/observability/index.d.ts +32 -8
- package/dist/observability/index.js +19 -6
- package/dist/observability/index.mjs +19 -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-D6JZ15_e.d.ts → server-Bf1zNYZk.d.ts} +5 -5
- package/dist/{stream-B4oK9JFP.d.ts → stream-I7bkvF7a.d.ts} +1 -1
- package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DF83nLn6.d.ts} +2 -2
- package/dist/{tracing-DqbTKGcf.d.ts → tracing-CWV4gT0u.d.ts} +1 -1
- package/docs/README.md +2 -0
- package/docs/ai/PUBLIC_API.md +28 -7
- package/docs/articles/brass-runtime-http-observability.md +467 -0
- package/docs/frameworks/angular.md +51 -0
- package/docs/frameworks/express.md +58 -0
- package/docs/frameworks/fastify.md +49 -0
- package/docs/frameworks/nestjs.md +53 -0
- package/docs/frameworks/nextjs.md +55 -0
- package/docs/frameworks/react.md +44 -0
- package/docs/frameworks/vanilla.md +56 -0
- package/docs/guides/layers.md +130 -0
- package/docs/http-recipes.md +31 -1
- package/docs/http.md +50 -1
- package/docs/observability.md +132 -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 +6 -2
- 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
|
@@ -122,3 +122,52 @@ process.once("SIGTERM", async () => {
|
|
|
122
122
|
|
|
123
123
|
Runnable repo example: `src/examples/observabilityFastify.ts`.
|
|
124
124
|
|
|
125
|
+
## Layer Variant
|
|
126
|
+
|
|
127
|
+
Fastify can use the same app graph shape as Express. Build it once during
|
|
128
|
+
startup, read services from the produced `LayerContext`, and close the layer in
|
|
129
|
+
`onClose`/shutdown:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
133
|
+
import { s } from "brass-runtime/schema";
|
|
134
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
135
|
+
import {
|
|
136
|
+
ObservabilityService,
|
|
137
|
+
makeObservabilityLayer,
|
|
138
|
+
makeObservedRuntimeLayer,
|
|
139
|
+
makeObservedHttpClientLayer,
|
|
140
|
+
makeOtlpOptions,
|
|
141
|
+
} from "brass-runtime/observability";
|
|
142
|
+
|
|
143
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
144
|
+
|
|
145
|
+
const AppLayer = Layer.composeAll(
|
|
146
|
+
makeConfigLayer(Config, s.object({
|
|
147
|
+
serviceName: s.nonEmptyString(),
|
|
148
|
+
apiBaseUrl: s.url(),
|
|
149
|
+
otlpEndpoint: s.url(),
|
|
150
|
+
}), {
|
|
151
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-fastify",
|
|
152
|
+
apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
153
|
+
otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
154
|
+
}),
|
|
155
|
+
makeObservabilityLayer((ctx) => {
|
|
156
|
+
const config = ctx.unsafeGet(Config);
|
|
157
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
158
|
+
}),
|
|
159
|
+
makeObservedRuntimeLayer(),
|
|
160
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
161
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
162
|
+
preset: "production",
|
|
163
|
+
})),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
167
|
+
app.decorate("brass", {
|
|
168
|
+
observability: built.service.unsafeGet(ObservabilityService),
|
|
169
|
+
runtime: built.service.unsafeGet(RuntimeService),
|
|
170
|
+
http: built.service.unsafeGet(HttpClientService),
|
|
171
|
+
});
|
|
172
|
+
app.addHook("onClose", () => Runtime.make({}).toPromise(built.close()));
|
|
173
|
+
```
|
|
@@ -270,6 +270,55 @@ npm install --save-dev @nestjs/core @nestjs/common @nestjs/platform-express refl
|
|
|
270
270
|
npm run example:observability:nest
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
+
## Layer Variant
|
|
274
|
+
|
|
275
|
+
Nest providers can also be backed by one Brass layer graph. This keeps
|
|
276
|
+
validation, runtime hooks, HTTP observability, and shutdown in one place:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { Runtime, Layer, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
280
|
+
import { s } from "brass-runtime/schema";
|
|
281
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
282
|
+
import {
|
|
283
|
+
ObservabilityService,
|
|
284
|
+
makeObservabilityLayer,
|
|
285
|
+
makeObservedRuntimeLayer,
|
|
286
|
+
makeObservedHttpClientLayer,
|
|
287
|
+
makeOtlpOptions,
|
|
288
|
+
} from "brass-runtime/observability";
|
|
289
|
+
|
|
290
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
291
|
+
const AppLayer = Layer.composeAll(
|
|
292
|
+
makeConfigLayer(Config, s.object({
|
|
293
|
+
serviceName: s.nonEmptyString(),
|
|
294
|
+
apiBaseUrl: s.url(),
|
|
295
|
+
otlpEndpoint: s.url(),
|
|
296
|
+
}), {
|
|
297
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "orders-api",
|
|
298
|
+
apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
299
|
+
otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
300
|
+
}),
|
|
301
|
+
makeObservabilityLayer((ctx) => {
|
|
302
|
+
const config = ctx.unsafeGet(Config);
|
|
303
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
304
|
+
}),
|
|
305
|
+
makeObservedRuntimeLayer(),
|
|
306
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
307
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
308
|
+
preset: "production",
|
|
309
|
+
})),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
313
|
+
|
|
314
|
+
export const brassProviders = [
|
|
315
|
+
{ provide: BRASS_OBSERVABILITY, useValue: built.service.unsafeGet(ObservabilityService) },
|
|
316
|
+
{ provide: BRASS_RUNTIME, useValue: built.service.unsafeGet(RuntimeService) },
|
|
317
|
+
{ provide: BRASS_HTTP, useValue: built.service.unsafeGet(HttpClientService) },
|
|
318
|
+
{ provide: BRASS_LAYER_CLOSE, useValue: () => Runtime.make({}).toPromise(built.close()) },
|
|
319
|
+
];
|
|
320
|
+
```
|
|
321
|
+
|
|
273
322
|
It uses a fake OTLP `fetch` by default so it can run without a collector. To
|
|
274
323
|
send to Grafana/Alloy instead:
|
|
275
324
|
|
|
@@ -280,3 +329,7 @@ GRAFANA_OTLP_AUTHORIZATION='Basic <token>' \
|
|
|
280
329
|
npm run example:observability:nest
|
|
281
330
|
```
|
|
282
331
|
|
|
332
|
+
## Runnable Example
|
|
333
|
+
|
|
334
|
+
A minimal runnable app lives at
|
|
335
|
+
[examples/nestjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nestjs).
|
|
@@ -145,3 +145,58 @@ export async function POST(
|
|
|
145
145
|
For Client Components, reuse the React provider recipe and set
|
|
146
146
|
`otlpEndpoint: "/api/otel"`.
|
|
147
147
|
|
|
148
|
+
## Layer Variant
|
|
149
|
+
|
|
150
|
+
For Route Handlers, keep a server-only layer singleton and close it from your
|
|
151
|
+
process lifecycle when the platform exposes one:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// app/lib/brass.server.ts
|
|
155
|
+
import "server-only";
|
|
156
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
157
|
+
import { s } from "brass-runtime/schema";
|
|
158
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
159
|
+
import {
|
|
160
|
+
ObservabilityService,
|
|
161
|
+
makeObservabilityLayer,
|
|
162
|
+
makeObservedRuntimeLayer,
|
|
163
|
+
makeObservedHttpClientLayer,
|
|
164
|
+
makeOtlpOptions,
|
|
165
|
+
} from "brass-runtime/observability";
|
|
166
|
+
|
|
167
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
168
|
+
const AppLayer = Layer.composeAll(
|
|
169
|
+
makeConfigLayer(Config, s.object({
|
|
170
|
+
serviceName: s.nonEmptyString(),
|
|
171
|
+
apiBaseUrl: s.url(),
|
|
172
|
+
otlpEndpoint: s.url(),
|
|
173
|
+
}), {
|
|
174
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-next",
|
|
175
|
+
apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
176
|
+
otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
177
|
+
}),
|
|
178
|
+
makeObservabilityLayer((ctx) => {
|
|
179
|
+
const config = ctx.unsafeGet(Config);
|
|
180
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
181
|
+
}),
|
|
182
|
+
makeObservedRuntimeLayer(),
|
|
183
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
184
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
185
|
+
preset: "production",
|
|
186
|
+
})),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
190
|
+
|
|
191
|
+
export const brass = {
|
|
192
|
+
observability: built.service.unsafeGet(ObservabilityService),
|
|
193
|
+
runtime: built.service.unsafeGet(RuntimeService),
|
|
194
|
+
http: built.service.unsafeGet(HttpClientService),
|
|
195
|
+
shutdown: () => Runtime.make({}).toPromise(built.close()),
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Runnable Example
|
|
200
|
+
|
|
201
|
+
A minimal runnable app lives at
|
|
202
|
+
[examples/nextjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nextjs).
|
package/docs/frameworks/react.md
CHANGED
|
@@ -137,3 +137,47 @@ export function Profile() {
|
|
|
137
137
|
Point `/api/otel` to a server route that owns the collector credentials. The
|
|
138
138
|
Next.js recipe includes one proxy example; the same idea works in any BFF.
|
|
139
139
|
|
|
140
|
+
## Layer Variant
|
|
141
|
+
|
|
142
|
+
The provider can build a layer graph instead of manually constructing each
|
|
143
|
+
service. In browser apps, use a same-origin OTLP proxy and close the layer from
|
|
144
|
+
the React cleanup:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
148
|
+
import { s } from "brass-runtime/schema";
|
|
149
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
150
|
+
import {
|
|
151
|
+
makeObservabilityLayer,
|
|
152
|
+
makeObservedRuntimeLayer,
|
|
153
|
+
makeObservedHttpClientLayer,
|
|
154
|
+
makeOtlpOptions,
|
|
155
|
+
} from "brass-runtime/observability";
|
|
156
|
+
|
|
157
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
158
|
+
const AppLayer = Layer.composeAll(
|
|
159
|
+
makeConfigLayer(Config, s.object({
|
|
160
|
+
serviceName: s.nonEmptyString(),
|
|
161
|
+
apiBaseUrl: s.string(),
|
|
162
|
+
otlpEndpoint: s.string(),
|
|
163
|
+
}), { serviceName: "shop-react", apiBaseUrl: "/api", otlpEndpoint: "/api/otel" }),
|
|
164
|
+
makeObservabilityLayer((ctx) => {
|
|
165
|
+
const config = ctx.unsafeGet(Config);
|
|
166
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
167
|
+
}),
|
|
168
|
+
makeObservedRuntimeLayer(),
|
|
169
|
+
makeObservedHttpClientLayer((ctx) => ({ baseUrl: ctx.unsafeGet(Config).apiBaseUrl, preset: "balanced" })),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
173
|
+
const brass = {
|
|
174
|
+
runtime: built.service.unsafeGet(RuntimeService),
|
|
175
|
+
http: built.service.unsafeGet(HttpClientService),
|
|
176
|
+
shutdown: () => Runtime.make({}).toPromise(built.close()),
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Runnable Example
|
|
181
|
+
|
|
182
|
+
A minimal runnable app lives at
|
|
183
|
+
[examples/react](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/react).
|
|
@@ -222,3 +222,59 @@ process.once("SIGTERM", async () => {
|
|
|
222
222
|
server.listen(process.env.PORT ?? 3000);
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
## Layer Variant
|
|
226
|
+
|
|
227
|
+
For scripts, CLIs, workers, or small Node servers, a layer graph keeps startup
|
|
228
|
+
and teardown explicit without a framework:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
232
|
+
import { s } from "brass-runtime/schema";
|
|
233
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
234
|
+
import {
|
|
235
|
+
ObservabilityService,
|
|
236
|
+
makeObservabilityLayer,
|
|
237
|
+
makeObservedRuntimeLayer,
|
|
238
|
+
makeObservedHttpClientLayer,
|
|
239
|
+
makeOtlpOptions,
|
|
240
|
+
} from "brass-runtime/observability";
|
|
241
|
+
|
|
242
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
243
|
+
const AppLayer = Layer.composeAll(
|
|
244
|
+
makeConfigLayer(Config, s.object({
|
|
245
|
+
serviceName: s.nonEmptyString(),
|
|
246
|
+
apiBaseUrl: s.url(),
|
|
247
|
+
otlpEndpoint: s.url(),
|
|
248
|
+
}), {
|
|
249
|
+
serviceName: "worker",
|
|
250
|
+
apiBaseUrl: "https://users-api.internal",
|
|
251
|
+
otlpEndpoint: "http://grafana-alloy:4318",
|
|
252
|
+
}),
|
|
253
|
+
makeObservabilityLayer((ctx) => {
|
|
254
|
+
const config = ctx.unsafeGet(Config);
|
|
255
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
256
|
+
}),
|
|
257
|
+
makeObservedRuntimeLayer(),
|
|
258
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
259
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
260
|
+
preset: "production",
|
|
261
|
+
})),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
265
|
+
const runtime = built.service.unsafeGet(RuntimeService);
|
|
266
|
+
const http = built.service.unsafeGet(HttpClientService);
|
|
267
|
+
const observability = built.service.unsafeGet(ObservabilityService);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await runtime.toPromise(http.getJson("/users/42"));
|
|
271
|
+
console.log(observability.prometheus.export());
|
|
272
|
+
} finally {
|
|
273
|
+
await Runtime.make({}).toPromise(built.close());
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Runnable Example
|
|
278
|
+
|
|
279
|
+
A minimal runnable app lives at
|
|
280
|
+
[examples/vanilla](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/vanilla).
|
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/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
|
|
@@ -430,6 +515,53 @@ const http = makeDefaultHttpClient({
|
|
|
430
515
|
});
|
|
431
516
|
```
|
|
432
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
|
+
|
|
433
565
|
Sampling can be configured globally, by ratio, or with rules:
|
|
434
566
|
|
|
435
567
|
```ts
|