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
package/docs/ai/PUBLIC_API.md
CHANGED
|
@@ -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`, `
|
|
53
|
-
`
|
|
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
|
-
|
|
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`,
|
|
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).
|