brass-runtime 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +287 -23
  3. package/dist/agent/cli/main.cjs +38 -38
  4. package/dist/agent/cli/main.js +6 -6
  5. package/dist/agent/cli/main.mjs +6 -6
  6. package/dist/agent/index.cjs +7 -7
  7. package/dist/agent/index.d.ts +1 -1
  8. package/dist/agent/index.js +6 -6
  9. package/dist/agent/index.mjs +6 -6
  10. package/dist/chunk-2HQTDLHF.mjs +683 -0
  11. package/dist/chunk-36I3M4UC.mjs +370 -0
  12. package/dist/{chunk-QY5FKYEQ.js → chunk-3AYM6WPJ.js} +570 -51
  13. package/dist/chunk-3LOYJFRR.cjs +300 -0
  14. package/dist/chunk-3Y2RIUMM.js +300 -0
  15. package/dist/{chunk-7XOPAB5Q.js → chunk-4P2HHGAX.mjs} +83 -5
  16. package/dist/{chunk-N6VHMOWB.cjs → chunk-4ROBZFL6.cjs} +128 -128
  17. package/dist/{chunk-NC5SDRYE.js → chunk-52OB2ROS.js} +4 -4
  18. package/dist/{chunk-JX3LZQJH.cjs → chunk-52PPNNI4.cjs} +82 -20
  19. package/dist/{chunk-5YOQOXEQ.cjs → chunk-5EC274J5.cjs} +676 -293
  20. package/dist/chunk-5QC7LRZ3.js +229 -0
  21. package/dist/{chunk-7TL2LHQJ.js → chunk-5VRJNBLZ.mjs} +524 -141
  22. package/dist/chunk-62AZW6UT.cjs +313 -0
  23. package/dist/chunk-6IXXWIUM.js +683 -0
  24. package/dist/chunk-6RY2FFN4.mjs +2024 -0
  25. package/dist/chunk-74ZTY6CP.js +2871 -0
  26. package/dist/chunk-7CMJS3QE.mjs +2871 -0
  27. package/dist/{chunk-2WC63LJK.mjs → chunk-7JIJOVCT.js} +20 -10
  28. package/dist/chunk-7X3K5RMS.js +2024 -0
  29. package/dist/chunk-7ZPEZ57L.cjs +2024 -0
  30. package/dist/{chunk-FM4W4QPL.js → chunk-A2OM6NEH.mjs} +5 -4
  31. package/dist/chunk-AGR5B2BC.cjs +683 -0
  32. package/dist/chunk-B33ICAKP.js +313 -0
  33. package/dist/{chunk-J3H54ZRV.mjs → chunk-B5JD23U7.mjs} +1 -1
  34. package/dist/{chunk-F5EUMJL7.mjs → chunk-BKK77SBA.js} +83 -5
  35. package/dist/{chunk-U5KWK3PX.mjs → chunk-C3MDXTRZ.js} +11 -0
  36. package/dist/{chunk-SPUEME2B.cjs → chunk-CZIVE6NT.cjs} +12 -1
  37. package/dist/{chunk-TDVMADDN.js → chunk-DNFJLJMW.mjs} +11 -0
  38. package/dist/{chunk-XDZOO4L5.js → chunk-EJ6BPYVR.mjs} +79 -17
  39. package/dist/chunk-EOC4UHBS.mjs +229 -0
  40. package/dist/chunk-F6XWZQY4.cjs +777 -0
  41. package/dist/{chunk-7LVI2GIN.js → chunk-FH2X7BVP.js} +507 -72
  42. package/dist/{chunk-OOGJ73B6.js → chunk-FHQGHPMO.mjs} +20 -10
  43. package/dist/{chunk-WQ5QNU5R.cjs → chunk-GLE2WY7Z.cjs} +652 -217
  44. package/dist/{chunk-G6IQOE4P.mjs → chunk-GYM3LLGS.mjs} +507 -72
  45. package/dist/{chunk-TVN5I4U6.cjs → chunk-JF5WGYJJ.cjs} +25 -24
  46. package/dist/{chunk-CY33PGEX.mjs → chunk-KH4SYAOS.mjs} +570 -51
  47. package/dist/chunk-KN32XNTH.mjs +313 -0
  48. package/dist/chunk-KQLYONSE.cjs +2871 -0
  49. package/dist/{chunk-7HUOJA4W.cjs → chunk-KZJQ723N.cjs} +90 -80
  50. package/dist/{chunk-CCKHV5BT.mjs → chunk-L2SYFEBS.js} +5 -4
  51. package/dist/{chunk-IJT6RRQ5.cjs → chunk-L6VB5N7Q.cjs} +20 -9
  52. package/dist/{chunk-ZGLD4TVZ.mjs → chunk-MBEJI5HF.mjs} +4 -4
  53. package/dist/{chunk-PRWCB3QL.mjs → chunk-MIIYDLGM.js} +524 -141
  54. package/dist/{chunk-H55LI6WY.js → chunk-MOO4L7F4.mjs} +15 -4
  55. package/dist/chunk-MVGUEJ5Z.cjs +370 -0
  56. package/dist/chunk-PD4EJTQC.cjs +229 -0
  57. package/dist/chunk-PWC3RBQE.mjs +300 -0
  58. package/dist/{chunk-MWXMNYJS.cjs → chunk-Q2I37RP3.cjs} +643 -124
  59. package/dist/{chunk-VFIUZG7J.mjs → chunk-RKGKFN2A.js} +79 -17
  60. package/dist/{chunk-NYL4D7SK.cjs → chunk-SA6HUJVI.cjs} +5 -5
  61. package/dist/chunk-SK7UZRNI.mjs +777 -0
  62. package/dist/{chunk-K2T3DV26.mjs → chunk-TRM4JUZQ.js} +15 -4
  63. package/dist/chunk-UB4B6OFY.js +370 -0
  64. package/dist/{chunk-G3XGCZDQ.js → chunk-UCUBNWM2.js} +1 -1
  65. package/dist/chunk-VWIPB6I5.js +777 -0
  66. package/dist/{chunk-JNFRRJYH.cjs → chunk-WBGRHGBP.cjs} +270 -192
  67. package/dist/{client-CtFmoDvM.d.ts → client-CZHU674n.d.ts} +211 -36
  68. package/dist/core/index.cjs +135 -9
  69. package/dist/core/index.d.ts +238 -33
  70. package/dist/core/index.js +155 -29
  71. package/dist/core/index.mjs +155 -29
  72. package/dist/{effect-CGNl5Rqp.d.ts → effect-DIUHZ9IN.d.ts} +89 -1
  73. package/dist/effectRunner-CFLC32IK.cjs +8 -0
  74. package/dist/{effectRunner-A4CHJXJI.js → effectRunner-L4S7IPT3.js} +2 -2
  75. package/dist/{effectRunner-OPUF6QRN.mjs → effectRunner-NNGG75QA.mjs} +2 -2
  76. package/dist/http/index.cjs +324 -2986
  77. package/dist/http/index.d.ts +54 -68
  78. package/dist/http/index.js +238 -2900
  79. package/dist/http/index.mjs +238 -2900
  80. package/dist/http/testing.cjs +14 -12
  81. package/dist/http/testing.d.ts +5 -4
  82. package/dist/http/testing.js +10 -8
  83. package/dist/http/testing.mjs +10 -8
  84. package/dist/index.cjs +423 -255
  85. package/dist/index.d.ts +87 -69
  86. package/dist/index.js +301 -133
  87. package/dist/index.mjs +301 -133
  88. package/dist/observability/index.cjs +18 -531
  89. package/dist/observability/index.d.ts +81 -8
  90. package/dist/observability/index.js +25 -538
  91. package/dist/observability/index.mjs +25 -538
  92. package/dist/perf/cli.cjs +401 -0
  93. package/dist/perf/cli.d.ts +1 -0
  94. package/dist/perf/cli.js +401 -0
  95. package/dist/perf/cli.mjs +401 -0
  96. package/dist/perf/index.cjs +141 -0
  97. package/dist/perf/index.d.ts +483 -0
  98. package/dist/perf/index.js +141 -0
  99. package/dist/perf/index.mjs +141 -0
  100. package/dist/schedule-CK3Ml_7p.d.ts +259 -0
  101. package/dist/schema/index.cjs +6 -2
  102. package/dist/schema/index.d.ts +3 -1
  103. package/dist/schema/index.js +5 -1
  104. package/dist/schema/index.mjs +5 -1
  105. package/dist/{server-C8hDXA74.d.ts → server-D6JZ15_e.d.ts} +16 -4
  106. package/dist/{stream-dvSs0QS5.d.ts → stream-B4oK9JFP.d.ts} +1 -1
  107. package/dist/{tracer-B5tRH9H7.d.ts → tracer-Hwt1cl7h.d.ts} +13 -54
  108. package/dist/{tracing-Dt9S_6V8.d.ts → tracing-DqbTKGcf.d.ts} +1 -1
  109. package/docs/ARCHITECTURE.md +292 -0
  110. package/docs/README.md +65 -0
  111. package/docs/adr/0001-ai-context-pack.md +32 -0
  112. package/docs/agent-apply-mode.md +104 -0
  113. package/docs/agent-approvals.md +110 -0
  114. package/docs/agent-batch.md +185 -0
  115. package/docs/agent-boundaries.md +112 -0
  116. package/docs/agent-chat-sessions.md +160 -0
  117. package/docs/agent-ci.md +17 -0
  118. package/docs/agent-cli.md +405 -0
  119. package/docs/agent-config.md +480 -0
  120. package/docs/agent-context-discovery.md +159 -0
  121. package/docs/agent-copilot-like-dx.md +126 -0
  122. package/docs/agent-declarative-optimized-planning.md +138 -0
  123. package/docs/agent-dx.md +224 -0
  124. package/docs/agent-env-files.md +126 -0
  125. package/docs/agent-follow-up-context.md +43 -0
  126. package/docs/agent-global-usage.md +180 -0
  127. package/docs/agent-init.md +109 -0
  128. package/docs/agent-install-and-configure.md +516 -0
  129. package/docs/agent-language-workspace-ux.md +99 -0
  130. package/docs/agent-llm-adapters.md +123 -0
  131. package/docs/agent-local-install.md +190 -0
  132. package/docs/agent-local-tests.md +51 -0
  133. package/docs/agent-observability.md +155 -0
  134. package/docs/agent-patch-quality-loop.md +162 -0
  135. package/docs/agent-presets.md +22 -0
  136. package/docs/agent-project-commands.md +237 -0
  137. package/docs/agent-project-intelligence.md +156 -0
  138. package/docs/agent-redaction.md +18 -0
  139. package/docs/agent-release-readiness.md +76 -0
  140. package/docs/agent-rollback-safety.md +162 -0
  141. package/docs/agent-rollback.md +23 -0
  142. package/docs/agent-run-artifacts.md +16 -0
  143. package/docs/agent-vscode-auto-discovery.md +137 -0
  144. package/docs/agent-vscode-batch-runner.md +100 -0
  145. package/docs/agent-vscode-chat-layout.md +90 -0
  146. package/docs/agent-vscode-clean-install.md +147 -0
  147. package/docs/agent-vscode-code-actions.md +70 -0
  148. package/docs/agent-vscode-diff-preview.md +45 -0
  149. package/docs/agent-vscode-inline-assist.md +56 -0
  150. package/docs/agent-vscode-install.md +186 -0
  151. package/docs/agent-vscode-model-setup.md +97 -0
  152. package/docs/agent-vscode-patch-preview.md +92 -0
  153. package/docs/agent-vscode-problems.md +79 -0
  154. package/docs/agent-vscode-project-dashboard.md +106 -0
  155. package/docs/agent-vscode-run-history.md +92 -0
  156. package/docs/agent-vscode-ux.md +73 -0
  157. package/docs/ai/INVARIANTS.md +84 -0
  158. package/docs/ai/PROJECT_MAP.md +338 -0
  159. package/docs/ai/PUBLIC_API.md +339 -0
  160. package/docs/ai/VALIDATION_MATRIX.md +67 -0
  161. package/docs/api-polish.md +37 -0
  162. package/docs/cancellation.md +162 -0
  163. package/docs/coverage.md +46 -0
  164. package/docs/framework-integrations.md +38 -0
  165. package/docs/frameworks/angular.md +153 -0
  166. package/docs/frameworks/express.md +125 -0
  167. package/docs/frameworks/fastify.md +124 -0
  168. package/docs/frameworks/nestjs.md +282 -0
  169. package/docs/frameworks/nextjs.md +147 -0
  170. package/docs/frameworks/react.md +139 -0
  171. package/docs/frameworks/vanilla.md +224 -0
  172. package/docs/getting-started.md +159 -0
  173. package/docs/guides/README.md +40 -0
  174. package/docs/guides/circuit-breaker.md +89 -0
  175. package/docs/guides/error-handling.md +91 -0
  176. package/docs/guides/getting-started.md +107 -0
  177. package/docs/guides/layers.md +189 -0
  178. package/docs/guides/metrics.md +101 -0
  179. package/docs/guides/resource-management.md +141 -0
  180. package/docs/guides/retry.md +215 -0
  181. package/docs/guides/semaphore.md +66 -0
  182. package/docs/guides/streams.md +117 -0
  183. package/docs/guides/supervisors.md +98 -0
  184. package/docs/guides/testing.md +162 -0
  185. package/docs/guides/tracing.md +71 -0
  186. package/docs/http-recipes.md +399 -0
  187. package/docs/http.md +749 -0
  188. package/docs/modules.md +285 -0
  189. package/docs/nestjs.md +6 -0
  190. package/docs/observability-collector-smoke.md +31 -0
  191. package/docs/observability-framework-examples.md +110 -0
  192. package/docs/observability.md +649 -0
  193. package/docs/otel-collector-smoke.yaml +27 -0
  194. package/docs/performance-profiler.md +199 -0
  195. package/docs/production-readiness.md +73 -0
  196. package/docs/recipes/README.md +12 -0
  197. package/docs/recipes/http-server.md +45 -0
  198. package/docs/recipes/layers.md +44 -0
  199. package/docs/recipes/performance.md +47 -0
  200. package/docs/recipes/runtime.md +41 -0
  201. package/docs/recipes/testing.md +41 -0
  202. package/docs/release.md +53 -0
  203. package/docs/wasm-bounded-queues.md +44 -0
  204. package/docs/wasm-engine-observability-benchmarks.md +85 -0
  205. package/docs/wasm-fiber-engine.md +117 -0
  206. package/docs/wasm-scheduler-state-machine.md +122 -0
  207. package/docs/wasm-stream-chunks.md +54 -0
  208. package/package.json +22 -2
  209. package/dist/chunk-45F7OKGT.cjs +0 -104
  210. package/dist/chunk-7V4KY4RL.mjs +0 -104
  211. package/dist/chunk-DJQ7OMMB.cjs +0 -144
  212. package/dist/chunk-GOV47PPB.mjs +0 -552
  213. package/dist/chunk-JF4XXPZ5.cjs +0 -552
  214. package/dist/chunk-KCPT2D6G.js +0 -552
  215. package/dist/chunk-NOYZIMUJ.mjs +0 -144
  216. package/dist/chunk-PNVFW245.js +0 -144
  217. package/dist/chunk-ROJC3NBJ.js +0 -104
  218. package/dist/effectRunner-3ZHAD3LE.cjs +0 -8
  219. package/dist/schedule-Fque9Abz.d.ts +0 -70
@@ -0,0 +1,282 @@
1
+ # NestJS integration
2
+
3
+ This recipe shows one way to wire Brass into a Nest application with:
4
+
5
+ - one shared `Observability` instance;
6
+ - OTLP HTTP export to Grafana Cloud, Grafana Alloy, or any collector;
7
+ - one shared production HTTP client with Brass retry, cache, policy,
8
+ adaptive limiter, and HTTP observability;
9
+ - graceful shutdown for the HTTP client and exporter queues.
10
+
11
+ Brass stays vendor-neutral. The only Grafana-specific value is the endpoint and
12
+ authorization header supplied by the application.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install brass-runtime
18
+ npm install @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs
19
+ npm install --save-dev @types/express
20
+ ```
21
+
22
+ Example environment:
23
+
24
+ ```bash
25
+ OTEL_SERVICE_NAME=orders-api
26
+ OTEL_SERVICE_VERSION=1.2.3
27
+ GRAFANA_OTLP_ENDPOINT=http://grafana-alloy:4318
28
+ GRAFANA_OTLP_AUTHORIZATION='Basic <grafana-cloud-otlp-token>'
29
+ USERS_API_BASE_URL=https://users-api.internal
30
+ ```
31
+
32
+ `GRAFANA_OTLP_ENDPOINT` can point to Grafana Alloy/OpenTelemetry Collector next
33
+ to the service, or to a direct Grafana Cloud OTLP HTTP endpoint. When you need
34
+ different URLs per signal, pass `otlp: { metricsUrl, tracesUrl, logsUrl }`
35
+ directly instead of `makeOtlpOptions`.
36
+
37
+ ## Brass Module
38
+
39
+ Create one global module and inject Brass through Nest providers:
40
+
41
+ ```ts
42
+ // brass.module.ts
43
+ import { Global, Inject, Injectable, Module, type OnApplicationShutdown } from "@nestjs/common";
44
+ import { Runtime } from "brass-runtime/core";
45
+ import {
46
+ defineHttpPolicyPresets,
47
+ makeDefaultHttpClient,
48
+ type DefaultHttpClient,
49
+ } from "brass-runtime/http";
50
+ import {
51
+ makeObservability,
52
+ makeOtlpOptions,
53
+ withHttpObservability,
54
+ type Observability,
55
+ } from "brass-runtime/observability";
56
+
57
+ export const BRASS_OBSERVABILITY = Symbol("BRASS_OBSERVABILITY");
58
+ export const BRASS_RUNTIME = Symbol("BRASS_RUNTIME");
59
+ export const BRASS_HTTP = Symbol("BRASS_HTTP");
60
+
61
+ const policyPresets = defineHttpPolicyPresets({
62
+ readModel: {
63
+ lane: "read-model",
64
+ priority: 3,
65
+ retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
66
+ },
67
+ command: {
68
+ lane: "command",
69
+ priority: 1,
70
+ retry: false,
71
+ },
72
+ });
73
+
74
+ function grafanaOtlp() {
75
+ const authorization = process.env.GRAFANA_OTLP_AUTHORIZATION;
76
+
77
+ return makeOtlpOptions({
78
+ endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
79
+ headers: authorization ? { Authorization: authorization } : undefined,
80
+ timeoutMs: 10_000,
81
+ retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
82
+ pipeline: {
83
+ maxQueueSize: 10_000,
84
+ batchSize: 512,
85
+ dropPolicy: "drop-oldest",
86
+ shutdownTimeoutMs: 10_000,
87
+ },
88
+ });
89
+ }
90
+
91
+ @Injectable()
92
+ class BrassShutdown implements OnApplicationShutdown {
93
+ constructor(
94
+ @Inject(BRASS_OBSERVABILITY) private readonly observability: Observability,
95
+ @Inject(BRASS_HTTP) private readonly http: DefaultHttpClient,
96
+ ) {}
97
+
98
+ async onApplicationShutdown() {
99
+ await this.http.shutdown();
100
+ await this.observability.shutdown();
101
+ }
102
+ }
103
+
104
+ @Global()
105
+ @Module({
106
+ providers: [
107
+ {
108
+ provide: BRASS_OBSERVABILITY,
109
+ useFactory: () =>
110
+ makeObservability({
111
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "orders-api",
112
+ serviceVersion: process.env.OTEL_SERVICE_VERSION,
113
+ resource: {
114
+ "service.namespace": "commerce",
115
+ "deployment.environment": process.env.NODE_ENV ?? "development",
116
+ },
117
+ logs: { minLevel: "info" },
118
+ sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
119
+ redaction: {},
120
+ cardinality: { maxValuesPerLabel: 100 },
121
+ otlp: grafanaOtlp(),
122
+ flushIntervalMs: 10_000,
123
+ autoStart: true,
124
+ }),
125
+ },
126
+ {
127
+ provide: BRASS_RUNTIME,
128
+ useFactory: (observability: Observability) =>
129
+ new Runtime({ env: observability.env, hooks: observability.hooks }),
130
+ inject: [BRASS_OBSERVABILITY],
131
+ },
132
+ {
133
+ provide: BRASS_HTTP,
134
+ useFactory: (observability: Observability) =>
135
+ makeDefaultHttpClient({
136
+ baseUrl: process.env.USERS_API_BASE_URL,
137
+ preset: "production",
138
+ timeoutMs: 5_000,
139
+ policyPresets,
140
+ middleware: [withHttpObservability(observability)],
141
+ }),
142
+ inject: [BRASS_OBSERVABILITY],
143
+ },
144
+ BrassShutdown,
145
+ ],
146
+ exports: [BRASS_OBSERVABILITY, BRASS_RUNTIME, BRASS_HTTP],
147
+ })
148
+ export class BrassModule {}
149
+ ```
150
+
151
+ Enable shutdown hooks once in `main.ts` so Nest calls `BrassShutdown`:
152
+
153
+ ```ts
154
+ const app = await NestFactory.create(AppModule);
155
+ app.enableShutdownHooks();
156
+ await app.listen(process.env.PORT ?? 3000);
157
+ ```
158
+
159
+ ## Use The HTTP Client
160
+
161
+ Inject the Brass runtime and HTTP client where you call downstream services.
162
+ `withHttpObservability` records outbound metrics, logs, spans, policy context,
163
+ and W3C trace headers.
164
+
165
+ ```ts
166
+ // users.client.ts
167
+ import { Inject, Injectable } from "@nestjs/common";
168
+ import { Runtime } from "brass-runtime/core";
169
+ import type { DefaultHttpClient } from "brass-runtime/http";
170
+ import { BRASS_HTTP, BRASS_RUNTIME } from "./brass.module";
171
+
172
+ type UserDto = {
173
+ readonly id: string;
174
+ readonly name: string;
175
+ };
176
+
177
+ @Injectable()
178
+ export class UsersClient {
179
+ constructor(
180
+ @Inject(BRASS_RUNTIME) private readonly runtime: Runtime<any>,
181
+ @Inject(BRASS_HTTP) private readonly http: DefaultHttpClient,
182
+ ) {}
183
+
184
+ async getUser(id: string): Promise<UserDto> {
185
+ const response = await this.runtime.toPromise(
186
+ this.http.getJson<UserDto>(`/users/${id}`, {
187
+ policy: "readModel",
188
+ headers: { "x-client": "orders-api" },
189
+ timeoutMs: 2_000,
190
+ }),
191
+ );
192
+
193
+ return response.body;
194
+ }
195
+ }
196
+ ```
197
+
198
+ For command-style calls, choose a different policy:
199
+
200
+ ```ts
201
+ await this.runtime.toPromise(
202
+ this.http.postJson("/users", body, {
203
+ policy: "command",
204
+ timeoutMs: 3_000,
205
+ }),
206
+ );
207
+ ```
208
+
209
+ ## Inbound Request Spans
210
+
211
+ For Express-backed Nest apps, reuse the Express request adapter. The request
212
+ context seeds the Brass runtime from inbound `traceparent` / `baggage` headers,
213
+ then wraps your effect in a server span.
214
+
215
+ ```ts
216
+ import { Controller, Get, Inject, Param, Req, Res } from "@nestjs/common";
217
+ import type { Request, Response } from "express";
218
+ import { asyncFlatMap } from "brass-runtime/core";
219
+ import {
220
+ logEffect,
221
+ makeExpressRequestObservabilityContext,
222
+ type Observability,
223
+ } from "brass-runtime/observability";
224
+ import type { DefaultHttpClient } from "brass-runtime/http";
225
+ import { BRASS_HTTP, BRASS_OBSERVABILITY } from "./brass.module";
226
+
227
+ @Controller()
228
+ export class UsersController {
229
+ constructor(
230
+ @Inject(BRASS_OBSERVABILITY) private readonly observability: Observability,
231
+ @Inject(BRASS_HTTP) private readonly http: DefaultHttpClient,
232
+ ) {}
233
+
234
+ @Get("/users/:id")
235
+ getUser(@Req() req: Request, @Param("id") id: string) {
236
+ const ctx = makeExpressRequestObservabilityContext(this.observability, req, {
237
+ route: "/users/:id",
238
+ });
239
+
240
+ return ctx.run(
241
+ ctx.withRequestSpan(
242
+ asyncFlatMap(
243
+ logEffect("info", "users.lookup", {
244
+ userId: id,
245
+ authorization: req.headers.authorization,
246
+ }),
247
+ () => this.http.getJson(`/users/${id}`, { policy: "readModel" }),
248
+ ),
249
+ ),
250
+ );
251
+ }
252
+
253
+ @Get("/metrics")
254
+ metrics(@Res() res: Response) {
255
+ return res
256
+ .type(this.observability.prometheus.contentType)
257
+ .send(this.observability.prometheus.export());
258
+ }
259
+ }
260
+ ```
261
+
262
+ The `authorization` field is redacted by the default observability redactor.
263
+
264
+ ## Runnable Repo Example
265
+
266
+ The repo also includes a dependency-optional Nest example:
267
+
268
+ ```bash
269
+ npm install --save-dev @nestjs/core @nestjs/common @nestjs/platform-express reflect-metadata rxjs
270
+ npm run example:observability:nest
271
+ ```
272
+
273
+ It uses a fake OTLP `fetch` by default so it can run without a collector. To
274
+ send to Grafana/Alloy instead:
275
+
276
+ ```bash
277
+ BRASS_EXAMPLE_REAL_OTLP=true \
278
+ GRAFANA_OTLP_ENDPOINT=http://grafana-alloy:4318 \
279
+ GRAFANA_OTLP_AUTHORIZATION='Basic <token>' \
280
+ npm run example:observability:nest
281
+ ```
282
+
@@ -0,0 +1,147 @@
1
+ # Next.js integration
2
+
3
+ Use Brass in Next.js with a server-only singleton for Route Handlers and a
4
+ same-origin OTLP proxy for browser telemetry.
5
+
6
+ ## Server Singleton
7
+
8
+ ```ts
9
+ // app/lib/brass.server.ts
10
+ import "server-only";
11
+ import { Runtime } from "brass-runtime/core";
12
+ import {
13
+ defineHttpPolicyPresets,
14
+ makeDefaultHttpClient,
15
+ } from "brass-runtime/http";
16
+ import {
17
+ makeObservability,
18
+ makeOtlpOptions,
19
+ withHttpObservability,
20
+ } from "brass-runtime/observability";
21
+
22
+ const policyPresets = defineHttpPolicyPresets({
23
+ readModel: {
24
+ lane: "read-model",
25
+ priority: 3,
26
+ retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
27
+ },
28
+ command: {
29
+ lane: "command",
30
+ priority: 1,
31
+ retry: false,
32
+ },
33
+ });
34
+
35
+ const observability = makeObservability({
36
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-next",
37
+ serviceVersion: process.env.OTEL_SERVICE_VERSION,
38
+ resource: {
39
+ "deployment.environment": process.env.NODE_ENV ?? "development",
40
+ },
41
+ logs: { minLevel: "info" },
42
+ sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
43
+ redaction: {},
44
+ cardinality: { maxValuesPerLabel: 100 },
45
+ otlp: makeOtlpOptions({
46
+ endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
47
+ headers: process.env.GRAFANA_OTLP_AUTHORIZATION
48
+ ? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
49
+ : undefined,
50
+ timeoutMs: 10_000,
51
+ retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
52
+ pipeline: {
53
+ maxQueueSize: 10_000,
54
+ batchSize: 512,
55
+ dropPolicy: "drop-oldest",
56
+ shutdownTimeoutMs: 10_000,
57
+ },
58
+ }),
59
+ flushIntervalMs: 10_000,
60
+ autoStart: true,
61
+ });
62
+
63
+ export const brass = {
64
+ observability,
65
+ runtime: new Runtime({ env: observability.env, hooks: observability.hooks }),
66
+ http: makeDefaultHttpClient({
67
+ baseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
68
+ preset: "production",
69
+ timeoutMs: 5_000,
70
+ policyPresets,
71
+ middleware: [withHttpObservability(observability)],
72
+ }),
73
+ };
74
+ ```
75
+
76
+ ## Route Handler
77
+
78
+ Use `makeFetchRequestObservabilityContext` for App Router route handlers.
79
+
80
+ ```ts
81
+ // app/api/users/[id]/route.ts
82
+ import { makeFetchRequestObservabilityContext } from "brass-runtime/observability";
83
+ import { brass } from "@/app/lib/brass.server";
84
+
85
+ export async function GET(
86
+ request: Request,
87
+ { params }: { params: { id: string } | Promise<{ id: string }> },
88
+ ) {
89
+ const { id } = await params;
90
+ const ctx = makeFetchRequestObservabilityContext(brass.observability, request, {
91
+ route: "/api/users/[id]",
92
+ });
93
+
94
+ const response = await ctx.run(
95
+ ctx.withRequestSpan(
96
+ brass.http.getJson(`/users/${id}`, {
97
+ policy: "readModel",
98
+ timeoutMs: 2_000,
99
+ }),
100
+ ),
101
+ );
102
+
103
+ return Response.json(response.body);
104
+ }
105
+ ```
106
+
107
+ ## Browser OTLP Proxy
108
+
109
+ Client Components should never hold collector credentials. Use a route handler
110
+ as a narrow same-origin proxy:
111
+
112
+ ```ts
113
+ // app/api/otel/[...path]/route.ts
114
+ const allowedSignals = new Set(["v1/metrics", "v1/traces", "v1/logs"]);
115
+
116
+ export async function POST(
117
+ request: Request,
118
+ { params }: { params: { path: string[] } | Promise<{ path: string[] }> },
119
+ ) {
120
+ const { path } = await params;
121
+ const signalPath = path.join("/");
122
+
123
+ if (!allowedSignals.has(signalPath)) {
124
+ return new Response("unknown OTLP signal", { status: 404 });
125
+ }
126
+
127
+ const upstream = `${process.env.GRAFANA_OTLP_ENDPOINT}/${signalPath}`;
128
+ const body = await request.text();
129
+
130
+ const response = await fetch(upstream, {
131
+ method: "POST",
132
+ headers: {
133
+ "content-type": "application/json",
134
+ ...(process.env.GRAFANA_OTLP_AUTHORIZATION
135
+ ? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
136
+ : {}),
137
+ },
138
+ body,
139
+ });
140
+
141
+ return new Response(await response.text(), { status: response.status });
142
+ }
143
+ ```
144
+
145
+ For Client Components, reuse the React provider recipe and set
146
+ `otlpEndpoint: "/api/otel"`.
147
+
@@ -0,0 +1,139 @@
1
+ # React integration
2
+
3
+ React apps should create Brass once, expose it through context, and send
4
+ browser telemetry to a same-origin backend/proxy. Do not put Grafana Cloud or
5
+ collector credentials in client bundles.
6
+
7
+ ## Provider
8
+
9
+ ```tsx
10
+ // BrassProvider.tsx
11
+ import React, { createContext, useContext, useEffect, useMemo } from "react";
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 makeReactBrass() {
37
+ const observability = makeObservability({
38
+ serviceName: "shop-react",
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
+ type ReactBrass = ReturnType<typeof makeReactBrass>;
79
+
80
+ const BrassContext = createContext<ReactBrass | undefined>(undefined);
81
+
82
+ export function BrassProvider({ children }: { children: React.ReactNode }) {
83
+ const brass = useMemo(() => makeReactBrass(), []);
84
+
85
+ useEffect(() => {
86
+ return () => {
87
+ void brass.shutdown();
88
+ };
89
+ }, [brass]);
90
+
91
+ return <BrassContext.Provider value={brass}>{children}</BrassContext.Provider>;
92
+ }
93
+
94
+ export function useBrass() {
95
+ const brass = useContext(BrassContext);
96
+ if (!brass) throw new Error("BrassProvider is missing");
97
+ return brass;
98
+ }
99
+ ```
100
+
101
+ ## Component Usage
102
+
103
+ ```tsx
104
+ // Profile.tsx
105
+ import { useEffect, useState } from "react";
106
+ import { useBrass } from "./BrassProvider";
107
+
108
+ type User = {
109
+ readonly id: string;
110
+ readonly name: string;
111
+ };
112
+
113
+ export function Profile() {
114
+ const { runtime, http } = useBrass();
115
+ const [user, setUser] = useState<User | undefined>();
116
+
117
+ useEffect(() => {
118
+ let alive = true;
119
+
120
+ void runtime
121
+ .toPromise(http.getJson<User>("/users/me", { policy: "readModel" }))
122
+ .then((response) => {
123
+ if (alive) setUser(response.body);
124
+ });
125
+
126
+ return () => {
127
+ alive = false;
128
+ };
129
+ }, [runtime, http]);
130
+
131
+ return <span>{user?.name ?? "Loading..."}</span>;
132
+ }
133
+ ```
134
+
135
+ ## Collector Proxy
136
+
137
+ Point `/api/otel` to a server route that owns the collector credentials. The
138
+ Next.js recipe includes one proxy example; the same idea works in any BFF.
139
+