brass-runtime 1.16.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- 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-AVNQLJ5V.js → chunk-3PHU7FWS.js} +528 -23
- package/dist/{chunk-CIZFIMK5.js → chunk-4YQHPIWJ.js} +60 -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-76YMRMH2.cjs → chunk-KHACHFBQ.cjs} +583 -78
- package/dist/{chunk-MT3OWDPC.mjs → chunk-KRYP6CAE.mjs} +60 -11
- package/dist/chunk-KTGDLBLD.mjs +123 -0
- package/dist/{chunk-ENKODRU3.cjs → chunk-LXBU5E77.cjs} +143 -94
- package/dist/{chunk-PD4EJTQC.cjs → chunk-N6QNSTWD.cjs} +5 -5
- package/dist/{chunk-HLWLMW2F.mjs → chunk-OI4ESUMC.mjs} +9 -11
- package/dist/{chunk-EJ6BPYVR.mjs → chunk-OT2TESZU.mjs} +1 -1
- package/dist/{chunk-BABBZK4Y.js → chunk-PSEU65ND.js} +9 -11
- package/dist/{chunk-DNFO2EIZ.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-VN44DYYT.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 +22 -7
- package/dist/observability/index.d.ts +32 -8
- package/dist/observability/index.js +21 -6
- package/dist/observability/index.mjs +21 -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-GJPg8ZSG.d.ts → server-C1zVmqE6.d.ts} +16 -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 +4 -0
- package/docs/ai/PUBLIC_API.md +31 -7
- package/docs/articles/brass-runtime-http-observability.md +467 -0
- package/docs/framework-integrations.md +38 -0
- package/docs/frameworks/angular.md +204 -0
- package/docs/frameworks/express.md +183 -0
- package/docs/frameworks/fastify.md +173 -0
- package/docs/frameworks/nestjs.md +335 -0
- package/docs/frameworks/nextjs.md +202 -0
- package/docs/frameworks/react.md +183 -0
- package/docs/frameworks/vanilla.md +280 -0
- package/docs/guides/layers.md +130 -0
- package/docs/http-recipes.md +31 -1
- package/docs/http.md +50 -1
- package/docs/nestjs.md +6 -0
- package/docs/observability-framework-examples.md +12 -0
- package/docs/observability.md +239 -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
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Framework integrations
|
|
2
|
+
|
|
3
|
+
These recipes show how to wire Brass into common TypeScript application
|
|
4
|
+
frameworks without adding framework-specific dependencies to `brass-runtime`.
|
|
5
|
+
|
|
6
|
+
The common shape is:
|
|
7
|
+
|
|
8
|
+
- create one `Observability` instance near the application boundary;
|
|
9
|
+
- create one `Runtime` when framework handlers need to run effects;
|
|
10
|
+
- create one `makeDefaultHttpClient(...)` with `withHttpObservability(...)`;
|
|
11
|
+
- keep collector/vendor config in the application;
|
|
12
|
+
- shut down HTTP and observability queues from the host lifecycle when possible.
|
|
13
|
+
|
|
14
|
+
For browser apps, never expose Grafana Cloud tokens or collector secrets. Send
|
|
15
|
+
browser telemetry to a same-origin proxy such as `/api/otel`, then forward to
|
|
16
|
+
Grafana, Alloy, AppDynamics, or OpenTelemetry Collector from a trusted server.
|
|
17
|
+
|
|
18
|
+
## Recipes
|
|
19
|
+
|
|
20
|
+
| Framework | Recipe | Covers |
|
|
21
|
+
|-----------|--------|--------|
|
|
22
|
+
| Vanilla | [`docs/frameworks/vanilla.md`](./frameworks/vanilla.md) | Browser and Node setup without a framework |
|
|
23
|
+
| React | [`docs/frameworks/react.md`](./frameworks/react.md) | Context provider, hook, component usage |
|
|
24
|
+
| Next.js | [`docs/frameworks/nextjs.md`](./frameworks/nextjs.md) | App Router, server singleton, OTLP proxy |
|
|
25
|
+
| Angular | [`docs/frameworks/angular.md`](./frameworks/angular.md) | InjectionToken providers and services |
|
|
26
|
+
| Express | [`docs/frameworks/express.md`](./frameworks/express.md) | Request spans, `/metrics`, shutdown |
|
|
27
|
+
| Fastify | [`docs/frameworks/fastify.md`](./frameworks/fastify.md) | Request adapter, `/metrics`, shutdown |
|
|
28
|
+
| NestJS | [`docs/frameworks/nestjs.md`](./frameworks/nestjs.md) | Module providers, DI tokens, shutdown hooks |
|
|
29
|
+
|
|
30
|
+
Runnable dependency-optional examples live in:
|
|
31
|
+
|
|
32
|
+
- `src/examples/observabilityExpress.ts`
|
|
33
|
+
- `src/examples/observabilityFastify.ts`
|
|
34
|
+
- `src/examples/observabilityNest.ts`
|
|
35
|
+
|
|
36
|
+
See also [`docs/observability-framework-examples.md`](./observability-framework-examples.md)
|
|
37
|
+
for commands that run those examples locally.
|
|
38
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Angular integration
|
|
2
|
+
|
|
3
|
+
Angular apps should expose Brass through `InjectionToken`s. Browser telemetry
|
|
4
|
+
should go to a same-origin proxy such as `/api/otel`; collector credentials
|
|
5
|
+
belong on the server side.
|
|
6
|
+
|
|
7
|
+
## Providers
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// brass.providers.ts
|
|
11
|
+
import { inject, InjectionToken, type Provider } from "@angular/core";
|
|
12
|
+
import { Runtime } from "brass-runtime/core";
|
|
13
|
+
import {
|
|
14
|
+
defineHttpPolicyPresets,
|
|
15
|
+
makeDefaultHttpClient,
|
|
16
|
+
} from "brass-runtime/http";
|
|
17
|
+
import {
|
|
18
|
+
makeObservability,
|
|
19
|
+
makeOtlpOptions,
|
|
20
|
+
withHttpObservability,
|
|
21
|
+
} from "brass-runtime/observability";
|
|
22
|
+
|
|
23
|
+
const policyPresets = defineHttpPolicyPresets({
|
|
24
|
+
readModel: {
|
|
25
|
+
lane: "read-model",
|
|
26
|
+
priority: 3,
|
|
27
|
+
retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
|
|
28
|
+
},
|
|
29
|
+
command: {
|
|
30
|
+
lane: "command",
|
|
31
|
+
priority: 1,
|
|
32
|
+
retry: false,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function makeAngularBrass() {
|
|
37
|
+
const observability = makeObservability({
|
|
38
|
+
serviceName: "shop-angular",
|
|
39
|
+
resource: { "deployment.environment": "browser" },
|
|
40
|
+
logs: false,
|
|
41
|
+
sampling: { ratio: 0.1, respectRemoteSampled: true, forceSampleOnError: true },
|
|
42
|
+
redaction: {},
|
|
43
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
44
|
+
otlp: makeOtlpOptions({
|
|
45
|
+
endpoint: "/api/otel",
|
|
46
|
+
timeoutMs: 10_000,
|
|
47
|
+
retry: { attempts: 2, initialDelayMs: 100, maxDelayMs: 1_000 },
|
|
48
|
+
pipeline: { maxQueueSize: 2_000, batchSize: 128, dropPolicy: "drop-oldest" },
|
|
49
|
+
}),
|
|
50
|
+
flushIntervalMs: 15_000,
|
|
51
|
+
autoStart: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const runtime = new Runtime({
|
|
55
|
+
env: observability.env,
|
|
56
|
+
hooks: observability.hooks,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const http = makeDefaultHttpClient({
|
|
60
|
+
baseUrl: "/api",
|
|
61
|
+
preset: "balanced",
|
|
62
|
+
timeoutMs: 5_000,
|
|
63
|
+
policyPresets,
|
|
64
|
+
middleware: [withHttpObservability(observability)],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
observability,
|
|
69
|
+
runtime,
|
|
70
|
+
http,
|
|
71
|
+
shutdown: async () => {
|
|
72
|
+
await http.shutdown();
|
|
73
|
+
await observability.shutdown();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const BRASS = new InjectionToken<ReturnType<typeof makeAngularBrass>>("BRASS");
|
|
79
|
+
|
|
80
|
+
export function provideBrass(): Provider[] {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
provide: BRASS,
|
|
84
|
+
useFactory: () => makeAngularBrass(),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function injectBrass() {
|
|
90
|
+
return inject(BRASS);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Register the provider:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// app.config.ts
|
|
98
|
+
import { type ApplicationConfig } from "@angular/core";
|
|
99
|
+
import { provideBrass } from "./brass.providers";
|
|
100
|
+
|
|
101
|
+
export const appConfig: ApplicationConfig = {
|
|
102
|
+
providers: [
|
|
103
|
+
...provideBrass(),
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Service Usage
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// users.service.ts
|
|
112
|
+
import { Injectable } from "@angular/core";
|
|
113
|
+
import { injectBrass } from "./brass.providers";
|
|
114
|
+
|
|
115
|
+
type User = {
|
|
116
|
+
readonly id: string;
|
|
117
|
+
readonly name: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
@Injectable({ providedIn: "root" })
|
|
121
|
+
export class UsersService {
|
|
122
|
+
private readonly brass = injectBrass();
|
|
123
|
+
|
|
124
|
+
getUser(id: string): Promise<User> {
|
|
125
|
+
return this.brass.runtime
|
|
126
|
+
.toPromise(
|
|
127
|
+
this.brass.http.getJson<User>(`/users/${id}`, {
|
|
128
|
+
policy: "readModel",
|
|
129
|
+
timeoutMs: 2_000,
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
.then((response) => response.body);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Shutdown
|
|
138
|
+
|
|
139
|
+
Browser apps often do not have a reliable shutdown hook, but you can flush on
|
|
140
|
+
page lifecycle events:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { injectBrass } from "./brass.providers";
|
|
144
|
+
|
|
145
|
+
export function installBrassBrowserShutdown() {
|
|
146
|
+
const brass = injectBrass();
|
|
147
|
+
|
|
148
|
+
window.addEventListener("pagehide", () => {
|
|
149
|
+
void brass.shutdown();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
```
|
|
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).
|