brass-runtime 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-GYM3LLGS.mjs → chunk-2QNREG6K.mjs} +188 -5
- package/dist/{chunk-4ROBZFL6.cjs → chunk-2SLT3X6G.cjs} +6 -8
- package/dist/{chunk-KZJQ723N.cjs → chunk-3PFZGP23.cjs} +13 -15
- package/dist/{chunk-VWIPB6I5.js → chunk-3PHU7FWS.js} +528 -23
- package/dist/{chunk-BKK77SBA.js → chunk-4YQHPIWJ.js} +32 -11
- package/dist/chunk-5XADBMSU.cjs +33 -0
- package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
- package/dist/chunk-7TKI527D.cjs +123 -0
- package/dist/{chunk-AGR5B2BC.cjs → chunk-7TXQJFZX.cjs} +564 -12
- package/dist/{chunk-RKGKFN2A.js → chunk-AADFFVYS.js} +1 -1
- package/dist/{chunk-52PPNNI4.cjs → chunk-AJMKZXRB.cjs} +2 -2
- package/dist/{chunk-3AYM6WPJ.js → chunk-BG5RNEA2.js} +20 -299
- package/dist/{chunk-2HQTDLHF.mjs → chunk-ELLF55ER.mjs} +555 -3
- package/dist/{chunk-EOC4UHBS.mjs → chunk-G5JTCFMI.mjs} +2 -2
- package/dist/chunk-H5GYX7RZ.js +6126 -0
- package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
- package/dist/{chunk-6IXXWIUM.js → chunk-IBRHSH5H.js} +555 -3
- package/dist/{chunk-Q2I37RP3.cjs → chunk-IFRBVMWJ.cjs} +44 -323
- package/dist/{chunk-52OB2ROS.js → chunk-ITG6I7ZS.js} +2 -4
- package/dist/chunk-ITZQ526U.mjs +33 -0
- package/dist/{chunk-7JIJOVCT.js → chunk-JH4GI3DW.js} +2 -4
- package/dist/{chunk-F6XWZQY4.cjs → chunk-KHACHFBQ.cjs} +583 -78
- package/dist/{chunk-4P2HHGAX.mjs → chunk-KRYP6CAE.mjs} +32 -11
- package/dist/chunk-KTGDLBLD.mjs +123 -0
- package/dist/{chunk-WBGRHGBP.cjs → chunk-LXBU5E77.cjs} +114 -93
- package/dist/{chunk-PD4EJTQC.cjs → chunk-N6QNSTWD.cjs} +5 -5
- package/dist/{chunk-6RY2FFN4.mjs → chunk-OI4ESUMC.mjs} +9 -11
- package/dist/{chunk-EJ6BPYVR.mjs → chunk-OT2TESZU.mjs} +1 -1
- package/dist/{chunk-7X3K5RMS.js → chunk-PSEU65ND.js} +9 -11
- package/dist/{chunk-SK7UZRNI.mjs → chunk-QCOLAHU3.mjs} +528 -23
- package/dist/{chunk-KH4SYAOS.mjs → chunk-QZ6QFJNM.mjs} +20 -299
- package/dist/{chunk-MBEJI5HF.mjs → chunk-R6WDSZA6.mjs} +2 -4
- package/dist/{chunk-FHQGHPMO.mjs → chunk-RREBJX2S.mjs} +2 -4
- package/dist/{chunk-5QC7LRZ3.js → chunk-S4HHFUYP.js} +2 -2
- package/dist/{chunk-GLE2WY7Z.cjs → chunk-SSQJKDN3.cjs} +194 -11
- package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
- package/dist/chunk-VIFA4DPN.cjs +6126 -0
- package/dist/chunk-W6WR37HN.js +33 -0
- package/dist/{chunk-FH2X7BVP.js → chunk-XSAHV5HQ.js} +188 -5
- package/dist/chunk-YM3EDNYD.js +123 -0
- package/dist/{chunk-7ZPEZ57L.cjs → chunk-YTX2JYYP.cjs} +18 -20
- package/dist/chunk-Z3PSSXP3.mjs +6126 -0
- package/dist/core/index.cjs +31 -9
- package/dist/core/index.d.ts +19 -152
- package/dist/core/index.js +80 -58
- package/dist/core/index.mjs +80 -58
- package/dist/defaultClient-DhpCQW9m.d.ts +1623 -0
- package/dist/{effect-DIUHZ9IN.d.ts → effect-CtUDl5M5.d.ts} +1 -1
- package/dist/http/index.cjs +202 -59
- package/dist/http/index.d.ts +55 -819
- package/dist/http/index.js +216 -73
- package/dist/http/index.mjs +216 -73
- package/dist/http/testing.cjs +31 -10
- package/dist/http/testing.d.ts +16 -5
- package/dist/http/testing.js +29 -8
- package/dist/http/testing.mjs +29 -8
- package/dist/index.cjs +110 -88
- package/dist/index.d.ts +9 -8
- package/dist/index.js +81 -59
- package/dist/index.mjs +81 -59
- package/dist/{schedule-CK3Ml_7p.d.ts → layer-BalPI6cN.d.ts} +176 -2
- package/dist/observability/index.cjs +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-C1zVmqE6.d.ts} +5 -5
- package/dist/{stream-B4oK9JFP.d.ts → stream-Bb4FTejt.d.ts} +1 -1
- package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DzfuE6um.d.ts} +2 -2
- package/dist/{tracing-DqbTKGcf.d.ts → tracing-BABA5arE.d.ts} +1 -1
- package/docs/README.md +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 +4 -1
- package/dist/chunk-3LOYJFRR.cjs +0 -300
- package/dist/chunk-3Y2RIUMM.js +0 -300
- package/dist/chunk-5EC274J5.cjs +0 -2874
- package/dist/chunk-5VRJNBLZ.mjs +0 -2874
- package/dist/chunk-62AZW6UT.cjs +0 -313
- package/dist/chunk-74ZTY6CP.js +0 -2871
- package/dist/chunk-7CMJS3QE.mjs +0 -2871
- package/dist/chunk-A2OM6NEH.mjs +0 -194
- package/dist/chunk-B33ICAKP.js +0 -313
- package/dist/chunk-JF5WGYJJ.cjs +0 -194
- package/dist/chunk-KN32XNTH.mjs +0 -313
- package/dist/chunk-KQLYONSE.cjs +0 -2871
- package/dist/chunk-L2SYFEBS.js +0 -194
- package/dist/chunk-MIIYDLGM.js +0 -2874
- package/dist/chunk-PWC3RBQE.mjs +0 -300
- package/dist/client-CZHU674n.d.ts +0 -820
|
@@ -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).
|
|
@@ -122,3 +122,52 @@ process.once("SIGTERM", async () => {
|
|
|
122
122
|
|
|
123
123
|
Runnable repo example: `src/examples/observabilityFastify.ts`.
|
|
124
124
|
|
|
125
|
+
## Layer Variant
|
|
126
|
+
|
|
127
|
+
Fastify can use the same app graph shape as Express. Build it once during
|
|
128
|
+
startup, read services from the produced `LayerContext`, and close the layer in
|
|
129
|
+
`onClose`/shutdown:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
133
|
+
import { s } from "brass-runtime/schema";
|
|
134
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
135
|
+
import {
|
|
136
|
+
ObservabilityService,
|
|
137
|
+
makeObservabilityLayer,
|
|
138
|
+
makeObservedRuntimeLayer,
|
|
139
|
+
makeObservedHttpClientLayer,
|
|
140
|
+
makeOtlpOptions,
|
|
141
|
+
} from "brass-runtime/observability";
|
|
142
|
+
|
|
143
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
144
|
+
|
|
145
|
+
const AppLayer = Layer.composeAll(
|
|
146
|
+
makeConfigLayer(Config, s.object({
|
|
147
|
+
serviceName: s.nonEmptyString(),
|
|
148
|
+
apiBaseUrl: s.url(),
|
|
149
|
+
otlpEndpoint: s.url(),
|
|
150
|
+
}), {
|
|
151
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-fastify",
|
|
152
|
+
apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
153
|
+
otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
154
|
+
}),
|
|
155
|
+
makeObservabilityLayer((ctx) => {
|
|
156
|
+
const config = ctx.unsafeGet(Config);
|
|
157
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
158
|
+
}),
|
|
159
|
+
makeObservedRuntimeLayer(),
|
|
160
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
161
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
162
|
+
preset: "production",
|
|
163
|
+
})),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
167
|
+
app.decorate("brass", {
|
|
168
|
+
observability: built.service.unsafeGet(ObservabilityService),
|
|
169
|
+
runtime: built.service.unsafeGet(RuntimeService),
|
|
170
|
+
http: built.service.unsafeGet(HttpClientService),
|
|
171
|
+
});
|
|
172
|
+
app.addHook("onClose", () => Runtime.make({}).toPromise(built.close()));
|
|
173
|
+
```
|
|
@@ -270,6 +270,55 @@ npm install --save-dev @nestjs/core @nestjs/common @nestjs/platform-express refl
|
|
|
270
270
|
npm run example:observability:nest
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
+
## Layer Variant
|
|
274
|
+
|
|
275
|
+
Nest providers can also be backed by one Brass layer graph. This keeps
|
|
276
|
+
validation, runtime hooks, HTTP observability, and shutdown in one place:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { Runtime, Layer, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
280
|
+
import { s } from "brass-runtime/schema";
|
|
281
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
282
|
+
import {
|
|
283
|
+
ObservabilityService,
|
|
284
|
+
makeObservabilityLayer,
|
|
285
|
+
makeObservedRuntimeLayer,
|
|
286
|
+
makeObservedHttpClientLayer,
|
|
287
|
+
makeOtlpOptions,
|
|
288
|
+
} from "brass-runtime/observability";
|
|
289
|
+
|
|
290
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
291
|
+
const AppLayer = Layer.composeAll(
|
|
292
|
+
makeConfigLayer(Config, s.object({
|
|
293
|
+
serviceName: s.nonEmptyString(),
|
|
294
|
+
apiBaseUrl: s.url(),
|
|
295
|
+
otlpEndpoint: s.url(),
|
|
296
|
+
}), {
|
|
297
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "orders-api",
|
|
298
|
+
apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
299
|
+
otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
300
|
+
}),
|
|
301
|
+
makeObservabilityLayer((ctx) => {
|
|
302
|
+
const config = ctx.unsafeGet(Config);
|
|
303
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
304
|
+
}),
|
|
305
|
+
makeObservedRuntimeLayer(),
|
|
306
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
307
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
308
|
+
preset: "production",
|
|
309
|
+
})),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
313
|
+
|
|
314
|
+
export const brassProviders = [
|
|
315
|
+
{ provide: BRASS_OBSERVABILITY, useValue: built.service.unsafeGet(ObservabilityService) },
|
|
316
|
+
{ provide: BRASS_RUNTIME, useValue: built.service.unsafeGet(RuntimeService) },
|
|
317
|
+
{ provide: BRASS_HTTP, useValue: built.service.unsafeGet(HttpClientService) },
|
|
318
|
+
{ provide: BRASS_LAYER_CLOSE, useValue: () => Runtime.make({}).toPromise(built.close()) },
|
|
319
|
+
];
|
|
320
|
+
```
|
|
321
|
+
|
|
273
322
|
It uses a fake OTLP `fetch` by default so it can run without a collector. To
|
|
274
323
|
send to Grafana/Alloy instead:
|
|
275
324
|
|
|
@@ -280,3 +329,7 @@ GRAFANA_OTLP_AUTHORIZATION='Basic <token>' \
|
|
|
280
329
|
npm run example:observability:nest
|
|
281
330
|
```
|
|
282
331
|
|
|
332
|
+
## Runnable Example
|
|
333
|
+
|
|
334
|
+
A minimal runnable app lives at
|
|
335
|
+
[examples/nestjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nestjs).
|