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.
Files changed (120) hide show
  1. package/README.md +36 -3
  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-7X3K5RMS.js → chunk-22HZQG5F.js} +9 -11
  10. package/dist/{chunk-GLE2WY7Z.cjs → chunk-2JHJ4YHS.cjs} +417 -124
  11. package/dist/{chunk-Q2I37RP3.cjs → chunk-2OW6IFY2.cjs} +44 -323
  12. package/dist/{chunk-7ZPEZ57L.cjs → chunk-5LC7V2OZ.cjs} +18 -20
  13. package/dist/{chunk-AGR5B2BC.cjs → chunk-5RZ7YITF.cjs} +564 -12
  14. package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
  15. package/dist/{chunk-EJ6BPYVR.mjs → chunk-6V2AWT4R.mjs} +1 -1
  16. package/dist/{chunk-3AYM6WPJ.js → chunk-7DU7IQHK.js} +20 -299
  17. package/dist/{chunk-SK7UZRNI.mjs → chunk-7GBJYOX7.mjs} +528 -23
  18. package/dist/chunk-7TKI527D.cjs +123 -0
  19. package/dist/{chunk-52OB2ROS.js → chunk-7VQLEN37.js} +2 -4
  20. package/dist/{chunk-KH4SYAOS.mjs → chunk-B5FKOLTB.mjs} +20 -299
  21. package/dist/{chunk-FHQGHPMO.mjs → chunk-BC6Q6BCO.mjs} +2 -4
  22. package/dist/{chunk-4P2HHGAX.mjs → chunk-COOW7BJX.mjs} +32 -11
  23. package/dist/{chunk-2HQTDLHF.mjs → chunk-EEN5OTCR.mjs} +555 -3
  24. package/dist/{chunk-KZJQ723N.cjs → chunk-EICAJDNX.cjs} +13 -15
  25. package/dist/chunk-ELIECDYN.cjs +33 -0
  26. package/dist/{chunk-GYM3LLGS.mjs → chunk-H626ZTDZ.mjs} +399 -106
  27. package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
  28. package/dist/{chunk-7JIJOVCT.js → chunk-IPSMXUWA.js} +2 -4
  29. package/dist/{chunk-4ROBZFL6.cjs → chunk-J6DUHITE.cjs} +6 -8
  30. package/dist/{chunk-6RY2FFN4.mjs → chunk-JWIEMBE6.mjs} +9 -11
  31. package/dist/{chunk-PD4EJTQC.cjs → chunk-KNTJ7FQB.cjs} +5 -5
  32. package/dist/chunk-KTGDLBLD.mjs +123 -0
  33. package/dist/chunk-LSYQ3C2M.js +33 -0
  34. package/dist/{chunk-RKGKFN2A.js → chunk-OW5VHAOE.js} +1 -1
  35. package/dist/{chunk-EOC4UHBS.mjs → chunk-RBHNOKH4.mjs} +2 -2
  36. package/dist/{chunk-6IXXWIUM.js → chunk-S4HXADU4.js} +555 -3
  37. package/dist/{chunk-FH2X7BVP.js → chunk-TTSPIU3U.js} +399 -106
  38. package/dist/{chunk-5QC7LRZ3.js → chunk-UAKAF32U.js} +2 -2
  39. package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
  40. package/dist/{chunk-MBEJI5HF.mjs → chunk-WCBNXPN6.mjs} +2 -4
  41. package/dist/{chunk-52PPNNI4.cjs → chunk-WGE2FEZE.cjs} +2 -2
  42. package/dist/{chunk-WBGRHGBP.cjs → chunk-WI7GZF3B.cjs} +114 -93
  43. package/dist/chunk-WUDHOZIH.js +6234 -0
  44. package/dist/{chunk-F6XWZQY4.cjs → chunk-WVSZOPGQ.cjs} +583 -78
  45. package/dist/chunk-XPIMJQYS.cjs +6234 -0
  46. package/dist/{chunk-VWIPB6I5.js → chunk-YGR2IN4R.js} +528 -23
  47. package/dist/chunk-YM3EDNYD.js +123 -0
  48. package/dist/chunk-YWLLH27R.mjs +33 -0
  49. package/dist/{chunk-BKK77SBA.js → chunk-YZ5LQ32F.js} +32 -11
  50. package/dist/chunk-Z3ZZMQUZ.mjs +6234 -0
  51. package/dist/core/index.cjs +37 -9
  52. package/dist/core/index.d.ts +19 -152
  53. package/dist/core/index.js +86 -58
  54. package/dist/core/index.mjs +86 -58
  55. package/dist/defaultClient-Cid0JoUR.d.ts +1648 -0
  56. package/dist/{effect-DIUHZ9IN.d.ts → effect-DnGUuhw6.d.ts} +22 -1
  57. package/dist/http/index.cjs +206 -59
  58. package/dist/http/index.d.ts +55 -819
  59. package/dist/http/index.js +220 -73
  60. package/dist/http/index.mjs +220 -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 +116 -88
  66. package/dist/index.d.ts +9 -8
  67. package/dist/index.js +87 -59
  68. package/dist/index.mjs +87 -59
  69. package/dist/{schedule-CK3Ml_7p.d.ts → layer-D2LFcBVx.d.ts} +176 -2
  70. package/dist/observability/index.cjs +20 -7
  71. package/dist/observability/index.d.ts +32 -8
  72. package/dist/observability/index.js +19 -6
  73. package/dist/observability/index.mjs +19 -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-D6JZ15_e.d.ts → server-Bf1zNYZk.d.ts} +5 -5
  84. package/dist/{stream-B4oK9JFP.d.ts → stream-I7bkvF7a.d.ts} +1 -1
  85. package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DF83nLn6.d.ts} +2 -2
  86. package/dist/{tracing-DqbTKGcf.d.ts → tracing-CWV4gT0u.d.ts} +1 -1
  87. package/docs/README.md +2 -0
  88. package/docs/ai/PUBLIC_API.md +28 -7
  89. package/docs/articles/brass-runtime-http-observability.md +467 -0
  90. package/docs/frameworks/angular.md +51 -0
  91. package/docs/frameworks/express.md +58 -0
  92. package/docs/frameworks/fastify.md +49 -0
  93. package/docs/frameworks/nestjs.md +53 -0
  94. package/docs/frameworks/nextjs.md +55 -0
  95. package/docs/frameworks/react.md +44 -0
  96. package/docs/frameworks/vanilla.md +56 -0
  97. package/docs/guides/layers.md +130 -0
  98. package/docs/http-recipes.md +31 -1
  99. package/docs/http.md +50 -1
  100. package/docs/observability.md +132 -0
  101. package/docs/performance-profiler.md +6 -2
  102. package/docs/recipes/layers.md +46 -2
  103. package/docs/recipes/testing.md +25 -0
  104. package/package.json +6 -2
  105. package/dist/chunk-3LOYJFRR.cjs +0 -300
  106. package/dist/chunk-3Y2RIUMM.js +0 -300
  107. package/dist/chunk-5EC274J5.cjs +0 -2874
  108. package/dist/chunk-5VRJNBLZ.mjs +0 -2874
  109. package/dist/chunk-62AZW6UT.cjs +0 -313
  110. package/dist/chunk-74ZTY6CP.js +0 -2871
  111. package/dist/chunk-7CMJS3QE.mjs +0 -2871
  112. package/dist/chunk-A2OM6NEH.mjs +0 -194
  113. package/dist/chunk-B33ICAKP.js +0 -313
  114. package/dist/chunk-JF5WGYJJ.cjs +0 -194
  115. package/dist/chunk-KN32XNTH.mjs +0 -313
  116. package/dist/chunk-KQLYONSE.cjs +0 -2871
  117. package/dist/chunk-L2SYFEBS.js +0 -194
  118. package/dist/chunk-MIIYDLGM.js +0 -2874
  119. package/dist/chunk-PWC3RBQE.mjs +0 -300
  120. package/dist/client-CZHU674n.d.ts +0 -820
@@ -49,8 +49,11 @@ Primary categories:
49
49
  `TestRuntime`, `TestScheduler`, `TestClock`, and testing helpers
50
50
  - Layer 2.0 dependency graph helpers: `Layer`, `LayerContext`,
51
51
  `ServiceTag`, `makeServiceTag`, `layerValue`, `layerEffect`,
52
- `defineService`, `getService`, `formatLayerError`, `buildLayer`,
53
- `makeLayerScope`, `provide`, and `provideLayerContext`
52
+ `defineService`, `getService`, `getServices`, `useService`,
53
+ `useServices`, `composeAll`, `mergeAll`, `makeConfigLayer`,
54
+ `makeRuntimeLayer`, `RuntimeService`, `makeTestLayer`, `makeTestLayers`,
55
+ `formatLayerError`, `buildLayer`, `makeLayerScope`, `provide`, and
56
+ `provideLayerContext`
54
57
  - worker pool, tracing, metrics, runtime observability hooks/events
55
58
  - typed errors
56
59
  - runtime engines and capabilities
@@ -81,7 +84,11 @@ Observability helpers include `RuntimeHooks`, `RuntimeEvent`,
81
84
  `Schedule.driver` / `makeScheduleDriver`, runtime-clock-aware schedule runners,
82
85
  supervisor APIs, `makeRuntime` / `runPromise` / `runExit`, and Layer 2.0
83
86
  primitives for typed service tags, immutable contexts, scoped memoized builds,
84
- missing-service formatting, and idempotent release.
87
+ multi-layer `Layer.all(...)` composition for independent layers,
88
+ `Layer.composeAll(...)` for ordered context graphs, typed `Layer.use(...)` /
89
+ `Layer.useAll(...)` accessors, schema-backed config layers, runtime service
90
+ layers, test-service replacement layers, missing-service formatting, and
91
+ idempotent release.
85
92
 
86
93
  ## HTTP export: `brass-runtime/http`
87
94
 
@@ -90,8 +97,11 @@ Source: `src/http/index.ts`
90
97
  Recommended API order:
91
98
 
92
99
  - `makeDefaultHttpClient` for the one-stop default client with JSON/text
93
- helpers, lifecycle presets (`production`, `default`, `balanced`, `minimal`),
100
+ helpers, lifecycle presets (`production`, `default`, `balanced`,
101
+ `highThroughputProxy`, `proxy`, `minimal`),
94
102
  compression, stats, cache controls, `cancelAll`, and middleware integration.
103
+ - `HttpClientService` and `makeDefaultHttpClientLayer` for optional Layer/DI
104
+ application graphs with owned default-client lifecycle.
95
105
  - `makeHttpRouter`, `route` / `httpRoute`, and `makeNodeHttpServerResource`
96
106
  for the first-party HTTP server MVP: Node adapter, simple router,
97
107
  effect-based middleware, schema validation, observability, runtime
@@ -114,6 +124,10 @@ Recommended API order:
114
124
  - `HttpTransport`, `HttpStreamTransport`, `makeFetchTransport`, and
115
125
  `makeFetchStreamTransport` for replacing the default fetch-backed transport
116
126
  with an effect-based backend such as Axios, undici, or test doubles.
127
+ - `makeNodeHttpProxyClient`, `makeNodeHttpTransport`, `NodeHttpTransport`, and
128
+ `NodeHttpTransportConfig` for Node-only BFF/proxy workloads that should use
129
+ `node:http` / `node:https` keep-alive agents instead of the default fetch
130
+ backend.
117
131
  - `makePromiseHttpTransport`, `promiseHttpTransport`, and
118
132
  `normalizeHttpHeaders` for adapting Promise-based clients without writing
119
133
  `Async.async` / `Cause.fail` plumbing in consuming projects; the fluent
@@ -144,7 +158,7 @@ Primary categories:
144
158
  - HTTP server router, Node adapter, response helpers, and server resources
145
159
  - HTTP runtime probe helpers: `makeRuntimeHealthRoute` and
146
160
  `makeRuntimeReadinessRoute`
147
- - production HTTP client presets (`minimal`, `balanced`, `default`)
161
+ - production HTTP client presets (`minimal`, `proxy`, `highThroughputProxy`, `balanced`, `default`, `production`)
148
162
  - dependency-free schema validation for JSON responses
149
163
  - builder API for default HTTP client configuration
150
164
  - adaptive limiter presets, diagnostics, and public config helper
@@ -164,7 +178,8 @@ Source: `src/http/testing.ts`
164
178
 
165
179
  Dependency-free helpers for users' test suites:
166
180
 
167
- - `makeMockHttpClient`, `makeSequenceHttpClient`
181
+ - `makeMockHttpClient`, `makeSequenceHttpClient`,
182
+ `makeMockDefaultHttpClient`, and `makeMockDefaultHttpClientLayer`
168
183
  - `makeHttpResponse`, `makeTextHttpResponse`, `makeJsonHttpResponse`
169
184
  - `runHttpEffect`
170
185
  - `installMockFetch`, `withMockFetch`
@@ -221,9 +236,15 @@ Primary categories:
221
236
  - structured log sink plus effect-level logging helpers
222
237
  - `withSpan` and `spanEvent` for trace spans across effect composition
223
238
  - `makeObservability` production preset with `flush()` and `shutdown()`
239
+ - `ObservabilityService`, `makeObservabilityLayer`,
240
+ `makeObservedRuntimeLayer`, and `makeObservedHttpClientLayer` for optional
241
+ Layer/DI wiring across observability, runtime, and HTTP without making any
242
+ factory mandatory.
224
243
  - `withHttpObservability` middleware for HTTP client metrics, logs, spans, and
225
244
  W3C `traceparent` injection, including request-policy context in logs/spans
226
- and opt-in policy metric labels
245
+ and opt-in policy metric labels. Hot proxy paths can use
246
+ `spans: { events: false, sampleRate }` plus `spanSink` to emit sampled HTTP
247
+ spans without enabling global runtime hooks.
227
248
  - `HTTP_OBSERVABILITY_CONTRACT` for stable dashboard metric names, label names,
228
249
  span attribute names, and structured log message names
229
250
  - adaptive limiter gauges and HTTP span attributes when the wrapped client
@@ -0,0 +1,467 @@
1
+ # Brass Runtime: efectos, HTTP y observabilidad sin casarte con un proveedor
2
+
3
+ En las últimas iteraciones de `brass-runtime` trabajamos sobre una idea simple:
4
+ si el runtime quiere ser útil en proyectos reales, no alcanza con tener un
5
+ modelo de efectos prolijo. Tiene que integrarse bien con HTTP, con la
6
+ observabilidad de producción, con los frameworks que los equipos ya usan, y con
7
+ las herramientas que cada organización decide traer a la mesa.
8
+
9
+ La dirección fue clara: **Brass debe conocer contratos, no proveedores**.
10
+
11
+ Eso aparece en varias decisiones de diseño:
12
+
13
+ - HTTP no está atado a `fetch`.
14
+ - Observability no está atada a Grafana, AppDynamics ni OpenTelemetry SDKs.
15
+ - Las políticas de ejecución viajan con el request sin obligar al usuario a
16
+ pensar en detalles internos.
17
+ - La integración con frameworks vive en recetas y ejemplos, no como
18
+ dependencias duras del runtime.
19
+
20
+ Este artículo resume ese trabajo.
21
+
22
+ ## El punto de partida
23
+
24
+ `brass-runtime` es un runtime de efectos para TypeScript, inspirado por ideas
25
+ tipo ZIO: efectos lazy, composición explícita, runtime controlado,
26
+ cancelación, recursos, capas y herramientas para modelar fallas.
27
+
28
+ Pero cuando un runtime sale del laboratorio y entra en una app real, aparecen
29
+ preguntas más prácticas:
30
+
31
+ - ¿Cómo llamo APIs HTTP sin perder retries, timeouts y cancelación?
32
+ - ¿Cómo observo lo que pasa sin meter un SDK gigante como dependencia dura?
33
+ - ¿Cómo conecto esto con Nest, Express, Next.js, React o Angular?
34
+ - ¿Cómo dejo que un proyecto use Axios, `fetch`, undici o un cliente propio?
35
+ - ¿Cómo evito que cada consumidor tenga que escribir plumbing de bajo nivel?
36
+
37
+ La respuesta fue fortalecer el módulo HTTP y la historia de observabilidad.
38
+
39
+ ## HTTP como una capa de efectos
40
+
41
+ El primer movimiento importante fue tratar HTTP no como una función suelta que
42
+ hace `fetch`, sino como una capa de ejecución alrededor de efectos.
43
+
44
+ El cliente HTTP recomendado hoy es `makeDefaultHttpClient`, que compone:
45
+
46
+ - timeout;
47
+ - retry;
48
+ - cache;
49
+ - deduplicación;
50
+ - priority scheduling;
51
+ - adaptive concurrency;
52
+ - compression;
53
+ - middleware;
54
+ - observability;
55
+ - políticas por request.
56
+
57
+ Un ejemplo mínimo:
58
+
59
+ ```ts
60
+ import { makeDefaultHttpClient } from "brass-runtime/http";
61
+ import { makeObservability, withHttpObservability } from "brass-runtime/observability";
62
+
63
+ const observability = makeObservability({
64
+ serviceName: "orders-api",
65
+ });
66
+
67
+ const http = makeDefaultHttpClient({
68
+ baseUrl: "https://users-api.internal",
69
+ preset: "production",
70
+ middleware: [withHttpObservability(observability)],
71
+ });
72
+
73
+ const user = await http.getJson<{ id: string; name: string }>("/users/42")
74
+ .unsafeRunPromise();
75
+ ```
76
+
77
+ La parte importante no es solo que el cliente llame una URL. Es que la llamada
78
+ viaja por una pipeline controlada por Brass.
79
+
80
+ ## El transporte no tiene que ser `fetch`
81
+
82
+ Una pregunta clave fue: si hoy el runtime se apalanca mucho en `fetch`, ¿por qué
83
+ no hacer que la capa de transporte sea intercambiable?
84
+
85
+ Eso llevó a formalizar `HttpTransport`.
86
+
87
+ La idea es que Brass conserve la semántica que le importa:
88
+
89
+ - request normalizado;
90
+ - URL resuelta;
91
+ - `AbortSignal`;
92
+ - respuesta wire;
93
+ - errores normalizados;
94
+ - métricas y timings.
95
+
96
+ Pero el mecanismo concreto puede ser `fetch`, Axios, undici, un mock, un SDK
97
+ interno o cualquier cliente Promise-based.
98
+
99
+ Para que eso no obligue al usuario a escribir `Async.async` y `Cause.fail`, se
100
+ sumó un helper fluido:
101
+
102
+ ```ts
103
+ import axios from "axios";
104
+ import {
105
+ makeDefaultHttpClient,
106
+ promiseHttpTransport,
107
+ } from "brass-runtime/http";
108
+
109
+ const axiosInstance = axios.create({
110
+ timeout: 10_000,
111
+ headers: { "x-client": "orders-api" },
112
+ });
113
+
114
+ const axiosTransport = promiseHttpTransport()
115
+ .requestConfig(({ request, url }) => ({
116
+ url: url.toString(),
117
+ method: request.method,
118
+ headers: request.headers,
119
+ data: request.body,
120
+ }))
121
+ .send((config) => axiosInstance.request(config))
122
+ .json(
123
+ (response) => response.data,
124
+ (response) => ({
125
+ status: response.status,
126
+ statusText: response.statusText,
127
+ headers: response.headers,
128
+ }),
129
+ );
130
+
131
+ const http = makeDefaultHttpClient({
132
+ baseUrl: "https://api.example.com",
133
+ preset: "production",
134
+ transport: axiosTransport,
135
+ });
136
+ ```
137
+
138
+ El `AbortSignal` sigue viajando, pero el usuario no tiene que declararlo en cada
139
+ request. Brass lo inyecta en el borde correcto.
140
+
141
+ ## DX: menos ceremonia, más intención
142
+
143
+ Durante el diseño apareció una mejora importante de DX: evitar APIs redundantes
144
+ como:
145
+
146
+ ```ts
147
+ fromJson().response();
148
+ ```
149
+
150
+ Terminamos con una forma más directa:
151
+
152
+ ```ts
153
+ .json()
154
+ ```
155
+
156
+ La intención es más clara: “este transporte devuelve JSON”. Si hace falta
157
+ customizar cómo se extrae el body o cómo se leen status/headers, se puede pasar
158
+ un mapper. Pero el camino común queda corto.
159
+
160
+ Ese fue un patrón de diseño que se repitió varias veces: **hacer fácil el caso
161
+ común sin cerrar la puerta al caso avanzado**.
162
+
163
+ ## Policies: la intención viaja con el request
164
+
165
+ Otro bloque importante fue la historia de policies.
166
+
167
+ En producción, no todos los requests son iguales. Un GET de lectura puede
168
+ aceptar retry y deduplicación. Un comando de escritura puede necesitar prioridad
169
+ alta y cero retry automático. Un request batch puede necesitar otro lane.
170
+
171
+ Para eso se agregó una forma estructurada de expresar intención:
172
+
173
+ ```ts
174
+ import {
175
+ defineHttpPolicyPresets,
176
+ makeDefaultHttpClient,
177
+ } from "brass-runtime/http";
178
+
179
+ const policies = defineHttpPolicyPresets({
180
+ readModel: {
181
+ lane: "read-model",
182
+ priority: 3,
183
+ retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
184
+ },
185
+ command: {
186
+ lane: "command",
187
+ priority: 1,
188
+ retry: false,
189
+ },
190
+ });
191
+
192
+ const http = makeDefaultHttpClient({
193
+ baseUrl: "https://users-api.internal",
194
+ preset: "production",
195
+ policyPresets: policies,
196
+ });
197
+
198
+ await http.getJson("/users/42", {
199
+ policy: "readModel",
200
+ }).unsafeRunPromise();
201
+
202
+ await http.postJson("/users", body, {
203
+ policy: "command",
204
+ }).unsafeRunPromise();
205
+ ```
206
+
207
+ La policy puede alimentar retry, dedup, priority, pool key, lanes y también
208
+ observability. El request no solo transporta datos; transporta intención
209
+ operativa.
210
+
211
+ ## Observability sin dependencia dura
212
+
213
+ El otro gran bloque fue observability.
214
+
215
+ La meta no era “integrarnos con Grafana” o “integrarnos con AppDynamics” como
216
+ dependencia de runtime. Eso volvería a Brass más pesado y más frágil.
217
+
218
+ La meta fue otra: **emitir señales usando contratos estándar y permitir que la
219
+ aplicación decida dónde enviarlas**.
220
+
221
+ Brass expone:
222
+
223
+ - métricas;
224
+ - spans;
225
+ - logs estructurados;
226
+ - propagación W3C `traceparent`;
227
+ - contexto por request;
228
+ - middleware HTTP observado;
229
+ - exportadores OTLP HTTP;
230
+ - Prometheus text exporter;
231
+ - redacción;
232
+ - sampling;
233
+ - control de cardinalidad;
234
+ - pipelines de export con batch, retry, timeout y shutdown.
235
+
236
+ Un ejemplo de producción:
237
+
238
+ ```ts
239
+ import {
240
+ makeObservability,
241
+ makeOtlpOptions,
242
+ withHttpObservability,
243
+ } from "brass-runtime/observability";
244
+ import { makeDefaultHttpClient } from "brass-runtime/http";
245
+
246
+ const observability = makeObservability({
247
+ serviceName: "orders-api",
248
+ serviceVersion: "1.2.3",
249
+ resource: {
250
+ "service.namespace": "commerce",
251
+ "deployment.environment": "production",
252
+ },
253
+ logs: { minLevel: "info" },
254
+ sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
255
+ redaction: {},
256
+ cardinality: { maxValuesPerLabel: 100 },
257
+ otlp: makeOtlpOptions({
258
+ endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
259
+ headers: process.env.GRAFANA_OTLP_AUTHORIZATION
260
+ ? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
261
+ : undefined,
262
+ timeoutMs: 10_000,
263
+ retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
264
+ pipeline: {
265
+ maxQueueSize: 10_000,
266
+ batchSize: 512,
267
+ dropPolicy: "drop-oldest",
268
+ shutdownTimeoutMs: 10_000,
269
+ },
270
+ }),
271
+ flushIntervalMs: 10_000,
272
+ autoStart: true,
273
+ });
274
+
275
+ const http = makeDefaultHttpClient({
276
+ baseUrl: "https://users-api.internal",
277
+ preset: "production",
278
+ middleware: [withHttpObservability(observability)],
279
+ });
280
+ ```
281
+
282
+ La aplicación puede mandar a Grafana Cloud, Grafana Alloy, AppDynamics
283
+ Collector, OpenTelemetry Collector o cualquier endpoint compatible con OTLP
284
+ HTTP. Brass solo necesita URLs, headers y tuning.
285
+
286
+ ## `makeOtlpOptions`: helper genérico, no vendor-specific
287
+
288
+ Para reducir repetición, agregamos `makeOtlpOptions`.
289
+
290
+ En lugar de escribir:
291
+
292
+ ```ts
293
+ otlp: {
294
+ metricsUrl: "http://collector:4318/v1/metrics",
295
+ tracesUrl: "http://collector:4318/v1/traces",
296
+ logsUrl: "http://collector:4318/v1/logs",
297
+ }
298
+ ```
299
+
300
+ Ahora se puede escribir:
301
+
302
+ ```ts
303
+ otlp: makeOtlpOptions({
304
+ endpoint: "http://collector:4318",
305
+ });
306
+ ```
307
+
308
+ Esto mantiene la frontera limpia: Brass conoce OTLP HTTP, no conoce Grafana ni
309
+ AppDynamics como implementaciones.
310
+
311
+ También hubo un detalle interesante: GitHub Advanced Security marcó una regex
312
+ en la normalización del endpoint. Aunque el riesgo práctico era bajo, lo
313
+ resolvimos reemplazando la regex por un loop simple. Es una buena muestra del
314
+ criterio de la librería: si algo puede ser más claro y más seguro sin costo, se
315
+ hace.
316
+
317
+ ## Observability en HTTP
318
+
319
+ El middleware `withHttpObservability` conecta la historia HTTP con la historia
320
+ de observability.
321
+
322
+ Registra:
323
+
324
+ - métricas de requests;
325
+ - duración;
326
+ - outcome;
327
+ - status;
328
+ - spans de cliente;
329
+ - logs de request/response/error;
330
+ - headers de trace;
331
+ - policy context;
332
+ - señales del adaptive limiter cuando el cliente lo posee.
333
+
334
+ Eso permite responder preguntas de producción:
335
+
336
+ - ¿qué endpoint está fallando?
337
+ - ¿qué lane está saturado?
338
+ - ¿los retries están aumentando?
339
+ - ¿qué policy genera más latencia?
340
+ - ¿el adaptive limiter está bajando concurrencia?
341
+ - ¿qué requests viajan con prioridad alta?
342
+
343
+ La observabilidad deja de ser solo “métricas por endpoint” y empieza a reflejar
344
+ decisiones operativas del runtime.
345
+
346
+ ## Frameworks: integración sin acoplamiento
347
+
348
+ Una librería de runtime no vive aislada. Los equipos usan frameworks.
349
+
350
+ Por eso sumamos documentación y ejemplos para:
351
+
352
+ - Vanilla browser/Node;
353
+ - React;
354
+ - Next.js;
355
+ - Angular;
356
+ - Express;
357
+ - Fastify;
358
+ - NestJS.
359
+
360
+ El patrón cambia por framework, pero la idea se mantiene:
361
+
362
+ - browser: no exponer tokens, usar proxy `/api/otel`;
363
+ - server: crear una instancia compartida de observability;
364
+ - HTTP client: usar `makeDefaultHttpClient` observado;
365
+ - inbound requests: crear contexto desde headers;
366
+ - shutdown: drenar HTTP y exporters.
367
+
368
+ En Nest, por ejemplo, el diseño natural es un módulo global con tokens:
369
+
370
+ ```ts
371
+ export const BRASS_OBSERVABILITY = Symbol("BRASS_OBSERVABILITY");
372
+ export const BRASS_RUNTIME = Symbol("BRASS_RUNTIME");
373
+ export const BRASS_HTTP = Symbol("BRASS_HTTP");
374
+ ```
375
+
376
+ En React, el diseño natural es un provider:
377
+
378
+ ```tsx
379
+ <BrassProvider>
380
+ <App />
381
+ </BrassProvider>
382
+ ```
383
+
384
+ En Express/Fastify, el diseño natural es crear Brass en startup y cerrar en
385
+ `SIGTERM`.
386
+
387
+ El runtime no necesita depender de ninguno de esos frameworks. Solo tiene que
388
+ dar buenas piezas para integrarse.
389
+
390
+ ## Tests, coverage y deuda técnica
391
+
392
+ Además del diseño de API, se trabajó en sostener la calidad:
393
+
394
+ - tests para transporte Promise;
395
+ - tests para normalización de errores;
396
+ - tests de policies;
397
+ - tests de observability HTTP;
398
+ - tests de `makeOtlpOptions`;
399
+ - coverage sobre paths de runtime y HTTP;
400
+ - documentación actualizada en README, docs de HTTP, observability y contexto
401
+ para agentes.
402
+
403
+ También apareció una señal interesante: perseguir branch coverage al 95% a
404
+ nivel global no es trivial cuando el runtime tiene muchos caminos internos de
405
+ fallas, engines, schedulers y puentes. Aun así, el trabajo mejoró la cobertura
406
+ de zonas críticas como HTTP transport y errors.
407
+
408
+ La conclusión ahí es práctica: la cobertura sirve cuando protege decisiones de
409
+ comportamiento, no cuando se vuelve un número decorativo.
410
+
411
+ ## Qué cambió en la forma de usar Brass
412
+
413
+ Antes, un consumidor avanzado podía terminar escribiendo demasiado plumbing:
414
+
415
+ - adaptar Axios a `Async`;
416
+ - normalizar errores;
417
+ - propagar aborts;
418
+ - mapear respuestas;
419
+ - conectar retry y dedup;
420
+ - emitir métricas;
421
+ - agregar spans;
422
+ - documentar cómo integrarlo en cada framework.
423
+
424
+ Después de este trabajo, el consumidor puede moverse en un nivel más cercano a
425
+ la intención:
426
+
427
+ ```ts
428
+ const http = makeDefaultHttpClient({
429
+ preset: "production",
430
+ transport: axiosTransport,
431
+ middleware: [withHttpObservability(observability)],
432
+ policyPresets,
433
+ });
434
+ ```
435
+
436
+ Y luego:
437
+
438
+ ```ts
439
+ await http.getJson("/users/42", {
440
+ policy: "readModel",
441
+ }).unsafeRunPromise();
442
+ ```
443
+
444
+ Menos ceremonia. Más semántica.
445
+
446
+ ## Cierre
447
+
448
+ Lo interesante de este recorrido es que Brass no creció agregando magia.
449
+ Creció aclarando fronteras.
450
+
451
+ El core sigue siendo un runtime de efectos.
452
+
453
+ HTTP es una capa de ejecución observable y configurable.
454
+
455
+ El transporte es intercambiable.
456
+
457
+ La observabilidad usa contratos estándar.
458
+
459
+ Los proveedores viven afuera.
460
+
461
+ Los frameworks se integran por recetas, no por dependencias obligatorias.
462
+
463
+ Ese equilibrio es lo que hace que una librería chica pueda escalar en uso sin
464
+ volverse pesada. Brass no intenta ser todo. Intenta ser una base sólida para que
465
+ cada proyecto exprese sus decisiones de ejecución, resiliencia y observabilidad
466
+ sin reescribir el mismo plumbing una y otra vez.
467
+
@@ -151,3 +151,54 @@ export function installBrassBrowserShutdown() {
151
151
  }
152
152
  ```
153
153
 
154
+ ## Layer Variant
155
+
156
+ Angular `InjectionToken`s can receive services from one Brass layer graph:
157
+
158
+ ```ts
159
+ import { InjectionToken, type Provider } from "@angular/core";
160
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
161
+ import { s } from "brass-runtime/schema";
162
+ import { HttpClientService } from "brass-runtime/http";
163
+ import {
164
+ makeObservabilityLayer,
165
+ makeObservedRuntimeLayer,
166
+ makeObservedHttpClientLayer,
167
+ makeOtlpOptions,
168
+ } from "brass-runtime/observability";
169
+
170
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
171
+ const AppLayer = Layer.composeAll(
172
+ makeConfigLayer(Config, s.object({
173
+ serviceName: s.nonEmptyString(),
174
+ apiBaseUrl: s.string(),
175
+ otlpEndpoint: s.string(),
176
+ }), { serviceName: "shop-angular", apiBaseUrl: "/api", otlpEndpoint: "/api/otel" }),
177
+ makeObservabilityLayer((ctx) => {
178
+ const config = ctx.unsafeGet(Config);
179
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
180
+ }),
181
+ makeObservedRuntimeLayer(),
182
+ makeObservedHttpClientLayer((ctx) => ({ baseUrl: ctx.unsafeGet(Config).apiBaseUrl, preset: "balanced" })),
183
+ );
184
+
185
+ export const BRASS_LAYER = new InjectionToken<Promise<Awaited<ReturnType<typeof buildBrassLayer>>>>("BRASS_LAYER");
186
+
187
+ async function buildBrassLayer() {
188
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
189
+ return {
190
+ runtime: built.service.unsafeGet(RuntimeService),
191
+ http: built.service.unsafeGet(HttpClientService),
192
+ shutdown: () => Runtime.make({}).toPromise(built.close()),
193
+ };
194
+ }
195
+
196
+ export function provideBrassLayer(): Provider[] {
197
+ return [{ provide: BRASS_LAYER, useFactory: () => buildBrassLayer() }];
198
+ }
199
+ ```
200
+
201
+ ## Runnable Example
202
+
203
+ A minimal runnable app lives at
204
+ [examples/angular](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/angular).
@@ -123,3 +123,61 @@ process.once("SIGTERM", async () => {
123
123
  ```
124
124
 
125
125
  Runnable repo example: `src/examples/observabilityExpress.ts`.
126
+
127
+ ## Layer Variant
128
+
129
+ For larger Express apps, use the Layer/DI helpers to keep startup wiring and
130
+ shutdown ownership together:
131
+
132
+ ```ts
133
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
134
+ import { s } from "brass-runtime/schema";
135
+ import { defineHttpPolicyPresets, HttpClientService } from "brass-runtime/http";
136
+ import {
137
+ ObservabilityService,
138
+ makeObservabilityLayer,
139
+ makeObservedRuntimeLayer,
140
+ makeObservedHttpClientLayer,
141
+ makeOtlpOptions,
142
+ } from "brass-runtime/observability";
143
+
144
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
145
+ const ConfigSchema = s.object({
146
+ serviceName: s.nonEmptyString(),
147
+ apiBaseUrl: s.url(),
148
+ otlpEndpoint: s.url(),
149
+ });
150
+
151
+ const ConfigLayer = makeConfigLayer(Config, ConfigSchema, {
152
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-express",
153
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
154
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
155
+ });
156
+
157
+ const AppLayer = Layer.composeAll(
158
+ ConfigLayer,
159
+ makeObservabilityLayer((ctx) => {
160
+ const config = ctx.unsafeGet(Config);
161
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
162
+ }),
163
+ makeObservedRuntimeLayer(),
164
+ makeObservedHttpClientLayer((ctx) => ({
165
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
166
+ preset: "production",
167
+ policyPresets,
168
+ })),
169
+ );
170
+
171
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
172
+ export const brass = {
173
+ observability: built.service.unsafeGet(ObservabilityService),
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/express](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/express).