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
@@ -1,3 +1,6 @@
1
+ import {
2
+ RuntimeService
3
+ } from "./chunk-W6WR37HN.js";
1
4
  import {
2
5
  exemplarFromTraceContext,
3
6
  injectTraceContext,
@@ -6,19 +9,33 @@ import {
6
9
  makeRequestObservabilityContext,
7
10
  normalizeHttpRoute,
8
11
  sanitizeHttpTarget,
12
+ shouldSampleWith,
9
13
  spanEvent,
10
14
  validateHttpObservabilityOptions,
11
15
  withSpan
12
- } from "./chunk-CIZFIMK5.js";
16
+ } from "./chunk-4YQHPIWJ.js";
13
17
  import {
18
+ HttpClientService,
14
19
  getHttpRequestPolicy,
15
20
  httpErrorStatus,
16
- isRetryableHttpError
17
- } from "./chunk-3Y2RIUMM.js";
21
+ isRetryableHttpError,
22
+ makeDefaultHttpClient
23
+ } from "./chunk-H5GYX7RZ.js";
24
+ import {
25
+ registerHttpEffect
26
+ } from "./chunk-TRM4JUZQ.js";
18
27
  import {
28
+ layerEffect,
29
+ makeServiceTag
30
+ } from "./chunk-IBRHSH5H.js";
31
+ import {
32
+ Runtime,
33
+ fromPromiseAbortable,
19
34
  getCurrentFiber
20
- } from "./chunk-FH2X7BVP.js";
35
+ } from "./chunk-XSAHV5HQ.js";
21
36
  import {
37
+ Cause,
38
+ asyncEffect,
22
39
  asyncFail,
23
40
  asyncFlatMap,
24
41
  asyncFold,
@@ -28,6 +45,9 @@ import {
28
45
 
29
46
  // src/observability/http.ts
30
47
  var DEFAULT_DURATION_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4];
48
+ var MAX_HTTP_METRIC_CACHE_ENTRIES = 4096;
49
+ var EMPTY_SPAN_ATTRIBUTES = Object.freeze({});
50
+ var TRACE_SAMPLER_RATIO = /* @__PURE__ */ Symbol.for("brass-runtime.traceSampler.ratio");
31
51
  var POLICY_LABEL_NAMES = {
32
52
  preset: "policy",
33
53
  lane: "lane",
@@ -103,10 +123,19 @@ function withHttpObservability(options) {
103
123
  validateHttpObservabilityOptions(options ?? {});
104
124
  }
105
125
  const resolved = resolveHttpObservabilityOptions(options);
126
+ const metricCache = makeHttpMetricCache();
127
+ attachMetricCache(resolved.metrics, metricCache);
106
128
  return (next) => {
129
+ const adaptiveLimiter = adaptiveLimiterOf(next);
130
+ if (canUseLeanHttpMetricsPath(resolved, adaptiveLimiter)) {
131
+ return withLeanHttpMetricsObservability(next, resolved, metricCache);
132
+ }
133
+ if (canUseLeanHttpSampledSpanPath(resolved, adaptiveLimiter)) {
134
+ return withLeanHttpSampledSpanObservability(next, resolved, metricCache);
135
+ }
107
136
  return (req) => {
108
137
  const run = asyncFlatMap(
109
- beginHttpObservation(req, resolved),
138
+ beginHttpObservation(req, resolved, metricCache),
110
139
  (state) => asyncFlatMap(
111
140
  logHttpRequest(req, resolved),
112
141
  () => asyncFlatMap(
@@ -116,23 +145,25 @@ function withHttpObservability(options) {
116
145
  (error) => {
117
146
  const finish = state.finishWithError(error);
118
147
  const adaptiveLimiterAttrs = observeAdaptiveLimiter(wireReq, next, resolved);
148
+ const traceEvent = !shouldEmitHttpSpanEvents(resolved) ? asyncSucceed(void 0) : spanEvent("http.client.error", {
149
+ ...finish.spanAttributes,
150
+ ...adaptiveLimiterAttrs,
151
+ "error.type": error._tag
152
+ });
119
153
  return asyncFlatMap(
120
- spanEvent("http.client.error", {
121
- ...finish.spanAttributes,
122
- ...adaptiveLimiterAttrs,
123
- "error.type": error._tag
124
- }),
154
+ traceEvent,
125
155
  () => asyncFlatMap(logHttpError(req, error, finish, resolved), () => asyncFail(error))
126
156
  );
127
157
  },
128
158
  (res) => {
129
159
  const finish = state.finishWithResponse(res);
130
160
  const adaptiveLimiterAttrs = observeAdaptiveLimiter(wireReq, next, resolved);
161
+ const traceEvent = !shouldEmitHttpSpanEvents(resolved) ? asyncSucceed(void 0) : spanEvent("http.client.response", {
162
+ ...finish.spanAttributes,
163
+ ...adaptiveLimiterAttrs
164
+ });
131
165
  return asyncFlatMap(
132
- spanEvent("http.client.response", {
133
- ...finish.spanAttributes,
134
- ...adaptiveLimiterAttrs
135
- }),
166
+ traceEvent,
136
167
  () => asyncFlatMap(logHttpResponse(req, res, finish, resolved), () => asyncSucceed(res))
137
168
  );
138
169
  }
@@ -146,26 +177,410 @@ function withHttpObservability(options) {
146
177
  };
147
178
  }
148
179
  var makeHttpObservabilityMiddleware = withHttpObservability;
149
- function beginHttpObservation(req, options) {
180
+ var metricResetRegistry = /* @__PURE__ */ new WeakMap();
181
+ function canUseLeanHttpMetricsPath(options, adaptiveLimiter) {
182
+ return Boolean(
183
+ options.metrics && options.logs === false && options.spans === false && !options.injectTraceHeaders && (!options.adaptiveLimiter.enabled || !adaptiveLimiter)
184
+ );
185
+ }
186
+ function canUseLeanHttpSampledSpanPath(options, adaptiveLimiter) {
187
+ return Boolean(
188
+ options.spans !== false && options.spans.events === false && options.logs === false && !options.injectTraceHeaders && (!options.adaptiveLimiter.enabled || !adaptiveLimiter)
189
+ );
190
+ }
191
+ function withLeanHttpSampledSpanObservability(next, options, metricCache) {
192
+ return (req) => asyncEffect((env, cb) => {
193
+ const startedAt = options.clock();
194
+ const baseLabels = requestBaseLabels(req, options);
195
+ const handles = beginLeanHttpMetricsObservation(baseLabels, options, metricCache);
196
+ const span = startLeanHttpSpan(req, options);
197
+ let finished = false;
198
+ let cancelInner;
199
+ const finish = (exit) => {
200
+ if (finished) return;
201
+ finished = true;
202
+ cancelInner = void 0;
203
+ if (exit._tag === "Success") {
204
+ recordHttpMetricsFinish(
205
+ handles,
206
+ httpStatusOutcome(exit.value.status),
207
+ String(exit.value.status),
208
+ startedAt,
209
+ options,
210
+ metricCache
211
+ );
212
+ finishLeanHttpSpan(span, "success");
213
+ } else {
214
+ const error = httpErrorFromCause(exit.cause);
215
+ const status = httpErrorStatus(error);
216
+ recordHttpMetricsFinish(
217
+ handles,
218
+ httpErrorOutcome(error),
219
+ status !== void 0 ? String(status) : void 0,
220
+ startedAt,
221
+ options,
222
+ metricCache
223
+ );
224
+ finishLeanHttpSpan(span, Cause.containsInterrupt(exit.cause) ? "interrupted" : "failure", error);
225
+ }
226
+ cb(exit);
227
+ };
228
+ try {
229
+ cancelInner = registerHttpEffect(next(req), env, finish);
230
+ } catch (error) {
231
+ const defectExit = {
232
+ _tag: "Failure",
233
+ cause: Cause.die(error)
234
+ };
235
+ finish(defectExit);
236
+ }
237
+ return () => {
238
+ if (!finished) {
239
+ finished = true;
240
+ recordHttpMetricsFinish(
241
+ handles,
242
+ "abort",
243
+ void 0,
244
+ startedAt,
245
+ options,
246
+ metricCache
247
+ );
248
+ finishLeanHttpSpan(span, "interrupted");
249
+ }
250
+ const cancel = cancelInner;
251
+ cancelInner = void 0;
252
+ cancel?.();
253
+ };
254
+ });
255
+ }
256
+ function withLeanHttpMetricsObservability(next, options, metricCache) {
257
+ return (req) => asyncEffect((env, cb) => {
258
+ const startedAt = options.clock();
259
+ const baseLabels = requestBaseLabels(req, options);
260
+ const handles = beginLeanHttpMetricsObservation(baseLabels, options, metricCache);
261
+ let finished = false;
262
+ let cancelInner;
263
+ const finish = (exit) => {
264
+ if (finished) return;
265
+ finished = true;
266
+ cancelInner = void 0;
267
+ if (exit._tag === "Success") {
268
+ recordHttpMetricsFinish(
269
+ handles,
270
+ httpStatusOutcome(exit.value.status),
271
+ String(exit.value.status),
272
+ startedAt,
273
+ options,
274
+ metricCache
275
+ );
276
+ } else {
277
+ const error = httpErrorFromCause(exit.cause);
278
+ const status = httpErrorStatus(error);
279
+ recordHttpMetricsFinish(
280
+ handles,
281
+ httpErrorOutcome(error),
282
+ status !== void 0 ? String(status) : void 0,
283
+ startedAt,
284
+ options,
285
+ metricCache
286
+ );
287
+ }
288
+ cb(exit);
289
+ };
290
+ try {
291
+ cancelInner = registerHttpEffect(next(req), env, finish);
292
+ } catch (error) {
293
+ const defectExit = {
294
+ _tag: "Failure",
295
+ cause: Cause.die(error)
296
+ };
297
+ finish(defectExit);
298
+ }
299
+ return () => {
300
+ if (!finished) {
301
+ finished = true;
302
+ recordHttpMetricsFinish(
303
+ handles,
304
+ "abort",
305
+ void 0,
306
+ startedAt,
307
+ options,
308
+ metricCache
309
+ );
310
+ }
311
+ const cancel = cancelInner;
312
+ cancelInner = void 0;
313
+ cancel?.();
314
+ };
315
+ });
316
+ }
317
+ function beginLeanHttpMetricsObservation(baseLabels, options, metricCache) {
318
+ const cacheKey = metricCacheKey("brass_http_client", baseLabels);
319
+ const existing = metricCache.disabled ? void 0 : metricCache.leanEntries.get(cacheKey);
320
+ if (existing) {
321
+ existing.inFlight?.increment();
322
+ return existing;
323
+ }
324
+ const inFlight = options.metrics ? getCachedMetric(
325
+ metricCache,
326
+ metricCache.inFlightGauges,
327
+ metricCacheKey("brass_http_client_in_flight", baseLabels),
328
+ () => options.metrics.gauge("brass_http_client_in_flight", baseLabels)
329
+ ) : void 0;
330
+ inFlight?.increment();
331
+ const created = {
332
+ baseLabels,
333
+ inFlight,
334
+ finishMetrics: /* @__PURE__ */ new Map()
335
+ };
336
+ if (!metricCache.disabled && metricCache.leanEntries.size < MAX_HTTP_METRIC_CACHE_ENTRIES) {
337
+ metricCache.leanEntries.set(cacheKey, created);
338
+ }
339
+ return created;
340
+ }
341
+ function recordHttpMetricsFinish(handles, outcome, status, startedAt, options, metricCache) {
342
+ const durationMs = Math.max(0, options.clock() - startedAt);
343
+ handles.inFlight?.decrement();
344
+ if (!options.metrics) return;
345
+ const finishKey = `${outcome}|${status ?? "none"}`;
346
+ let finishMetrics = metricCache.disabled ? void 0 : handles.finishMetrics.get(finishKey);
347
+ if (!finishMetrics) {
348
+ const labels = requestFinishLabelsFromBase(handles.baseLabels, outcome, status);
349
+ finishMetrics = {
350
+ counter: options.metrics.counter("brass_http_client_requests_total", labels),
351
+ histogram: options.metrics.histogram(
352
+ "brass_http_client_duration_ms",
353
+ [...options.durationBuckets ?? DEFAULT_DURATION_BUCKETS],
354
+ labels
355
+ )
356
+ };
357
+ if (!metricCache.disabled) handles.finishMetrics.set(finishKey, finishMetrics);
358
+ }
359
+ finishMetrics.counter.increment();
360
+ finishMetrics.histogram.observe(durationMs);
361
+ }
362
+ function startLeanHttpSpan(req, options) {
363
+ const configuredSampleRate = options.spans === false ? void 0 : options.spans.sampleRate;
364
+ if (configuredSampleRate !== void 0) {
365
+ if (configuredSampleRate <= 0) return void 0;
366
+ if (configuredSampleRate < 1 && Math.random() >= configuredSampleRate) return void 0;
367
+ }
368
+ const fiber = getCurrentFiber();
369
+ const runtime = fiber?.runtime;
370
+ const sink = options.spanSink ?? runtime?.hooks;
371
+ if (!fiber?.fiberContext || !runtime || !sink) return void 0;
372
+ const previousTrace = fiber.fiberContext.trace ?? null;
373
+ const brass = runtime.env?.brass;
374
+ if (previousTrace?.sampled === false && brass?.respectRemoteSampled !== false) return void 0;
375
+ const name = spanName(req, options.spans);
376
+ const tracer = resolveRuntimeTracer(runtime);
377
+ let traceId = previousTrace?.traceId;
378
+ let sampled = previousTrace?.sampled;
379
+ let attributes;
380
+ if (!previousTrace && configuredSampleRate === void 0) {
381
+ const ratio = traceSamplerRatio(brass?.sampler);
382
+ if (ratio !== void 0) {
383
+ if (ratio <= 0) return void 0;
384
+ if (ratio < 1 && Math.random() >= ratio) return void 0;
385
+ sampled = true;
386
+ }
387
+ traceId = tracer.newTraceId();
388
+ } else if (!previousTrace) {
389
+ sampled = true;
390
+ traceId = tracer.newTraceId();
391
+ }
392
+ attributes = spanStartAttributes(req, options);
393
+ if (sampled === void 0) {
394
+ traceId = traceId ?? tracer.newTraceId();
395
+ sampled = shouldSampleWith(brass?.sampler, {
396
+ traceId,
397
+ spanName: name,
398
+ parentSampled: previousTrace?.sampled,
399
+ attributes
400
+ });
401
+ }
402
+ if (sampled === false) return void 0;
403
+ const trace = {
404
+ traceId: traceId ?? tracer.newTraceId(),
405
+ spanId: tracer.newSpanId(),
406
+ parentSpanId: previousTrace?.spanId,
407
+ sampled: true,
408
+ traceState: previousTrace?.traceState,
409
+ ...previousTrace?.baggage ? { baggage: previousTrace.baggage } : {}
410
+ };
411
+ const state = {
412
+ fiber,
413
+ sink,
414
+ previousTrace,
415
+ trace,
416
+ name,
417
+ ended: false
418
+ };
419
+ fiber.fiberContext = { ...fiber.fiberContext, trace };
420
+ fiber.addFinalizer?.((exit) => {
421
+ const status = exit?._tag === "Success" ? "success" : exit?.cause && Cause.containsInterrupt(exit.cause) ? "interrupted" : "failure";
422
+ finishLeanHttpSpan(state, status, exit?._tag === "Failure" ? exit.cause : void 0);
423
+ });
424
+ sink.emit(
425
+ { type: "span.start", name, attributes, links: [] },
426
+ leanSpanContext(fiber, trace)
427
+ );
428
+ return state;
429
+ }
430
+ function finishLeanHttpSpan(state, status, error) {
431
+ if (!state || state.ended) return;
432
+ state.ended = true;
433
+ state.sink.emit(
434
+ { type: "span.end", name: state.name, status, error },
435
+ leanSpanContext(state.fiber, state.trace)
436
+ );
437
+ if (state.fiber?.fiberContext) {
438
+ state.fiber.fiberContext = { ...state.fiber.fiberContext, trace: state.previousTrace };
439
+ }
440
+ }
441
+ function leanSpanContext(fiber, trace) {
442
+ return {
443
+ fiberId: fiber?.id,
444
+ scopeId: fiber?.scopeId,
445
+ traceId: trace.traceId,
446
+ spanId: trace.spanId,
447
+ parentSpanId: trace.parentSpanId,
448
+ traceState: trace.traceState,
449
+ baggage: trace.baggage,
450
+ sampled: trace.sampled
451
+ };
452
+ }
453
+ function resolveRuntimeTracer(runtime) {
454
+ const tracer = runtime?.env?.brass?.tracer;
455
+ if (tracer?.newTraceId && tracer?.newSpanId) return tracer;
456
+ return {
457
+ newTraceId: () => randomRuntimeId("trace"),
458
+ newSpanId: () => randomRuntimeId("span")
459
+ };
460
+ }
461
+ function traceSamplerRatio(sampler) {
462
+ if (!sampler || typeof sampler === "function") return void 0;
463
+ const ratio = sampler[TRACE_SAMPLER_RATIO];
464
+ return typeof ratio === "number" && Number.isFinite(ratio) ? ratio : void 0;
465
+ }
466
+ function randomRuntimeId(prefix) {
467
+ const cryptoLike = globalThis.crypto;
468
+ if (typeof cryptoLike?.randomUUID === "function") return cryptoLike.randomUUID();
469
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
470
+ }
471
+ function httpErrorFromCause(cause) {
472
+ const failure = Cause.firstFailure(cause);
473
+ if (failure._tag === "Some") return failure.value;
474
+ if (Cause.containsInterrupt(cause)) return { _tag: "Abort" };
475
+ const defect = Cause.firstDefect(cause);
476
+ const value = defect._tag === "Some" ? defect.value : Cause.toError(cause);
477
+ return {
478
+ _tag: "FetchError",
479
+ message: value instanceof Error ? value.message : String(value),
480
+ cause: value
481
+ };
482
+ }
483
+ function makeHttpMetricCache() {
484
+ return {
485
+ disabled: false,
486
+ leanEntries: /* @__PURE__ */ new Map(),
487
+ inFlightGauges: /* @__PURE__ */ new Map(),
488
+ requestCounters: /* @__PURE__ */ new Map(),
489
+ durationHistograms: /* @__PURE__ */ new Map()
490
+ };
491
+ }
492
+ function attachMetricCache(metrics, cache) {
493
+ if (!metrics) return;
494
+ const existing = metricResetRegistry.get(metrics);
495
+ if (existing) {
496
+ existing.caches.add(cache);
497
+ return;
498
+ }
499
+ const entry = {
500
+ caches: /* @__PURE__ */ new Set([cache]),
501
+ reset: metrics.reset.bind(metrics)
502
+ };
503
+ try {
504
+ Object.defineProperty(metrics, "reset", {
505
+ configurable: true,
506
+ value: () => {
507
+ for (const cached of entry.caches) clearHttpMetricCache(cached);
508
+ entry.reset();
509
+ }
510
+ });
511
+ metricResetRegistry.set(metrics, entry);
512
+ } catch {
513
+ cache.disabled = true;
514
+ }
515
+ }
516
+ function clearHttpMetricCache(cache) {
517
+ cache.leanEntries.clear();
518
+ cache.inFlightGauges.clear();
519
+ cache.requestCounters.clear();
520
+ cache.durationHistograms.clear();
521
+ }
522
+ function getCachedMetric(owner, cache, key, create) {
523
+ if (owner.disabled) return create();
524
+ const existing = cache.get(key);
525
+ if (existing) return existing;
526
+ const created = create();
527
+ if (cache.size < MAX_HTTP_METRIC_CACHE_ENTRIES) {
528
+ cache.set(key, created);
529
+ }
530
+ return created;
531
+ }
532
+ function metricCacheKey(name, labels) {
533
+ let key = name;
534
+ for (const [label, value] of Object.entries(labels)) {
535
+ key += `|${label.length}:${label}=${value.length}:${value}`;
536
+ }
537
+ return key;
538
+ }
539
+ function beginHttpObservation(req, options, metricCache) {
150
540
  return asyncSync(() => {
151
541
  const startedAt = options.clock();
152
542
  let finished = false;
153
- const inFlight = options.metrics?.gauge("brass_http_client_in_flight", requestBaseLabels(req, options));
543
+ const baseLabels = requestBaseLabels(req, options);
544
+ const inFlight = options.metrics ? getCachedMetric(
545
+ metricCache,
546
+ metricCache.inFlightGauges,
547
+ metricCacheKey("brass_http_client_in_flight", baseLabels),
548
+ () => options.metrics.gauge("brass_http_client_in_flight", baseLabels)
549
+ ) : void 0;
154
550
  inFlight?.increment();
155
551
  const finish = (outcome, status, extra = {}) => {
156
552
  const durationMs = Math.max(0, options.clock() - startedAt);
157
- const labels = requestFinishLabels(req, outcome, status, options);
553
+ const labels = requestFinishLabelsFromBase(baseLabels, outcome, status);
158
554
  if (!finished) {
159
555
  finished = true;
160
556
  if (inFlight && inFlight.value() > 0) inFlight.decrement();
161
- options.metrics?.counter("brass_http_client_requests_total", labels).increment();
162
- options.metrics?.histogram("brass_http_client_duration_ms", [...options.durationBuckets ?? DEFAULT_DURATION_BUCKETS], labels).observe(durationMs, currentTraceExemplar(durationMs, startedAt + durationMs));
557
+ if (options.metrics) {
558
+ getCachedMetric(
559
+ metricCache,
560
+ metricCache.requestCounters,
561
+ metricCacheKey("brass_http_client_requests_total", labels),
562
+ () => options.metrics.counter("brass_http_client_requests_total", labels)
563
+ ).increment();
564
+ getCachedMetric(
565
+ metricCache,
566
+ metricCache.durationHistograms,
567
+ metricCacheKey("brass_http_client_duration_ms", labels),
568
+ () => options.metrics.histogram(
569
+ "brass_http_client_duration_ms",
570
+ [...options.durationBuckets ?? DEFAULT_DURATION_BUCKETS],
571
+ labels
572
+ )
573
+ ).observe(
574
+ durationMs,
575
+ options.spans === false ? void 0 : currentTraceExemplar(durationMs, startedAt + durationMs)
576
+ );
577
+ }
163
578
  }
164
579
  return {
165
580
  durationMs,
166
581
  outcome,
167
582
  labels,
168
- spanAttributes: {
583
+ spanAttributes: options.spans === false ? EMPTY_SPAN_ATTRIBUTES : {
169
584
  "http.duration_ms": durationMs,
170
585
  "http.outcome": outcome,
171
586
  ...status ? { "http.status_code": Number(status) } : {},
@@ -234,6 +649,7 @@ function resolveHttpObservabilityOptions(options) {
234
649
  metrics: raw?.metrics === false ? void 0 : raw?.metrics,
235
650
  logs: raw?.logs ?? {},
236
651
  spans: raw?.spans ?? {},
652
+ spanSink: raw?.spanSink === false ? void 0 : raw?.spanSink,
237
653
  adaptiveLimiter: resolveAdaptiveLimiterObservabilityOptions(raw?.adaptiveLimiter),
238
654
  policy: resolveHttpPolicyObservabilityOptions(raw?.policy),
239
655
  injectTraceHeaders: raw?.injectTraceHeaders ?? true,
@@ -330,6 +746,9 @@ function spanName(req, options) {
330
746
  }
331
747
  return `HTTP ${req.method}`;
332
748
  }
749
+ function shouldEmitHttpSpanEvents(options) {
750
+ return options.spans !== false && options.spans.events !== false;
751
+ }
333
752
  function spanStartAttributes(req, options) {
334
753
  const spanOptions = options.spans === false ? {} : options.spans.attributes;
335
754
  const custom = typeof spanOptions === "function" ? spanOptions(req) : spanOptions ?? {};
@@ -361,9 +780,9 @@ function requestBaseLabels(req, options) {
361
780
  ...requestPolicyMetricLabels(req, options)
362
781
  });
363
782
  }
364
- function requestFinishLabels(req, outcome, status, options) {
783
+ function requestFinishLabelsFromBase(baseLabels, outcome, status) {
365
784
  return {
366
- ...requestBaseLabels(req, options),
785
+ ...baseLabels,
367
786
  outcome,
368
787
  status: status ?? "none"
369
788
  };
@@ -759,6 +1178,88 @@ function readProcessEnv() {
759
1178
  return typeof process !== "undefined" ? process.env : {};
760
1179
  }
761
1180
 
1181
+ // src/observability/layer.ts
1182
+ var ObservabilityService = makeServiceTag("Observability");
1183
+ function makeObservabilityLayer(config = {}, options = {}) {
1184
+ const tag = options.tag ?? ObservabilityService;
1185
+ return layerEffect(
1186
+ tag,
1187
+ (context) => asyncSync(() => makeObservability(resolveObservabilityLayerConfig(config, context))),
1188
+ shutdownObservability
1189
+ );
1190
+ }
1191
+ function makeObservedRuntimeLayer(options = {}) {
1192
+ const {
1193
+ observabilityTag = ObservabilityService,
1194
+ runtimeTag = RuntimeService,
1195
+ env = {},
1196
+ ...runtimeOptions
1197
+ } = options;
1198
+ return layerEffect(
1199
+ runtimeTag,
1200
+ (context) => asyncSync(() => {
1201
+ const observability = context.unsafeGet(observabilityTag);
1202
+ return new Runtime({
1203
+ ...runtimeOptions,
1204
+ env: mergeRuntimeEnv(resolveObservedRuntimeLayerEnv(env, context, observability), observability.env),
1205
+ hooks: observability.hooks
1206
+ });
1207
+ })
1208
+ );
1209
+ }
1210
+ function makeObservedHttpClientLayer(config = {}, options = {}) {
1211
+ const observabilityTag = options.observabilityTag ?? ObservabilityService;
1212
+ const httpTag = options.httpTag ?? HttpClientService;
1213
+ return layerEffect(
1214
+ httpTag,
1215
+ (context) => asyncSync(() => {
1216
+ const observability = context.unsafeGet(observabilityTag);
1217
+ const httpConfig = resolveObservedHttpClientLayerConfig(config, context, observability);
1218
+ const httpObservability = resolveObservedHttpObservabilityOptions(
1219
+ options.httpObservability,
1220
+ context,
1221
+ observability
1222
+ );
1223
+ return makeDefaultHttpClient({
1224
+ ...httpConfig,
1225
+ middleware: [
1226
+ ...httpConfig.middleware ?? [],
1227
+ withHttpObservability({
1228
+ metrics: observability.metrics,
1229
+ ...httpObservability
1230
+ })
1231
+ ]
1232
+ });
1233
+ }),
1234
+ shutdownDefaultHttpClient
1235
+ );
1236
+ }
1237
+ function resolveObservabilityLayerConfig(config, context) {
1238
+ return typeof config === "function" ? config(context) : config;
1239
+ }
1240
+ function resolveObservedHttpClientLayerConfig(config, context, observability) {
1241
+ return typeof config === "function" ? config(context, observability) : config;
1242
+ }
1243
+ function resolveObservedHttpObservabilityOptions(options, context, observability) {
1244
+ return typeof options === "function" ? options(context, observability) : options ?? {};
1245
+ }
1246
+ function resolveObservedRuntimeLayerEnv(env, context, observability) {
1247
+ return typeof env === "function" ? env(context, observability) : env;
1248
+ }
1249
+ function mergeRuntimeEnv(env, observabilityEnv) {
1250
+ return Object.assign({}, env, observabilityEnv);
1251
+ }
1252
+ function shutdownObservability(observability) {
1253
+ return fromPromiseAbortable(
1254
+ () => observability.shutdown().then(() => void 0),
1255
+ () => void 0,
1256
+ { label: "observability.shutdown" }
1257
+ );
1258
+ }
1259
+ function shutdownDefaultHttpClient(client) {
1260
+ return client.shutdown();
1261
+ }
1262
+
762
1263
  export {
763
1264
  HTTP_OBSERVABILITY_CONTRACT,
764
1265
  withHttpObservability,
@@ -773,5 +1274,9 @@ export {
773
1274
  makeObservabilityPreset,
774
1275
  makeObservabilityFromEnv,
775
1276
  observabilityOptionsForPreset,
776
- observabilityOptionsFromEnv
1277
+ observabilityOptionsFromEnv,
1278
+ ObservabilityService,
1279
+ makeObservabilityLayer,
1280
+ makeObservedRuntimeLayer,
1281
+ makeObservedHttpClientLayer
777
1282
  };