brass-runtime 1.16.1 → 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.
- package/README.md +4 -5
- package/dist/{chunk-MT3OWDPC.mjs → chunk-4P2HHGAX.mjs} +28 -0
- package/dist/{chunk-HLWLMW2F.mjs → chunk-6RY2FFN4.mjs} +2 -2
- package/dist/{chunk-BABBZK4Y.js → chunk-7X3K5RMS.js} +2 -2
- package/dist/{chunk-VN44DYYT.cjs → chunk-7ZPEZ57L.cjs} +4 -4
- package/dist/{chunk-CIZFIMK5.js → chunk-BKK77SBA.js} +28 -0
- package/dist/{chunk-76YMRMH2.cjs → chunk-F6XWZQY4.cjs} +25 -25
- package/dist/{chunk-DNFO2EIZ.mjs → chunk-SK7UZRNI.mjs} +1 -1
- package/dist/{chunk-AVNQLJ5V.js → chunk-VWIPB6I5.js} +1 -1
- package/dist/{chunk-ENKODRU3.cjs → chunk-WBGRHGBP.cjs} +29 -1
- package/dist/http/index.cjs +4 -4
- package/dist/http/index.d.ts +1 -1
- package/dist/http/index.js +1 -1
- package/dist/http/index.mjs +1 -1
- package/dist/observability/index.cjs +5 -3
- package/dist/observability/index.d.ts +2 -2
- package/dist/observability/index.js +4 -2
- package/dist/observability/index.mjs +4 -2
- package/dist/perf/cli.cjs +18 -18
- package/dist/perf/cli.js +3 -3
- package/dist/perf/cli.mjs +3 -3
- package/dist/perf/index.cjs +5 -5
- package/dist/perf/index.js +3 -3
- package/dist/perf/index.mjs +3 -3
- package/dist/{server-GJPg8ZSG.d.ts → server-D6JZ15_e.d.ts} +12 -1
- package/docs/README.md +2 -0
- package/docs/ai/PUBLIC_API.md +3 -0
- package/docs/framework-integrations.md +38 -0
- package/docs/frameworks/angular.md +153 -0
- package/docs/frameworks/express.md +125 -0
- package/docs/frameworks/fastify.md +124 -0
- package/docs/frameworks/nestjs.md +282 -0
- package/docs/frameworks/nextjs.md +147 -0
- package/docs/frameworks/react.md +139 -0
- package/docs/frameworks/vanilla.md +224 -0
- package/docs/nestjs.md +6 -0
- package/docs/observability-framework-examples.md +12 -0
- package/docs/observability.md +107 -0
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Fastify integration
|
|
2
|
+
|
|
3
|
+
Fastify has its own request shape, so use
|
|
4
|
+
`makeFastifyRequestObservabilityContext` for inbound spans and a shared Brass
|
|
5
|
+
HTTP client for downstream calls.
|
|
6
|
+
|
|
7
|
+
## App Setup
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import Fastify from "fastify";
|
|
11
|
+
import { asyncFlatMap } from "brass-runtime/core";
|
|
12
|
+
import {
|
|
13
|
+
defineHttpPolicyPresets,
|
|
14
|
+
makeDefaultHttpClient,
|
|
15
|
+
} from "brass-runtime/http";
|
|
16
|
+
import {
|
|
17
|
+
logEffect,
|
|
18
|
+
makeFastifyRequestObservabilityContext,
|
|
19
|
+
makeObservability,
|
|
20
|
+
makeOtlpOptions,
|
|
21
|
+
withHttpObservability,
|
|
22
|
+
} from "brass-runtime/observability";
|
|
23
|
+
|
|
24
|
+
const policyPresets = defineHttpPolicyPresets({
|
|
25
|
+
readModel: {
|
|
26
|
+
lane: "read-model",
|
|
27
|
+
priority: 3,
|
|
28
|
+
retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
|
|
29
|
+
},
|
|
30
|
+
command: {
|
|
31
|
+
lane: "command",
|
|
32
|
+
priority: 1,
|
|
33
|
+
retry: false,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const observability = makeObservability({
|
|
38
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-fastify",
|
|
39
|
+
serviceVersion: process.env.OTEL_SERVICE_VERSION,
|
|
40
|
+
resource: {
|
|
41
|
+
"deployment.environment": process.env.NODE_ENV ?? "development",
|
|
42
|
+
},
|
|
43
|
+
logs: { minLevel: "info" },
|
|
44
|
+
sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
|
|
45
|
+
redaction: {},
|
|
46
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
47
|
+
otlp: makeOtlpOptions({
|
|
48
|
+
endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
49
|
+
headers: process.env.GRAFANA_OTLP_AUTHORIZATION
|
|
50
|
+
? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
|
|
51
|
+
: undefined,
|
|
52
|
+
timeoutMs: 10_000,
|
|
53
|
+
retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
|
|
54
|
+
pipeline: {
|
|
55
|
+
maxQueueSize: 10_000,
|
|
56
|
+
batchSize: 512,
|
|
57
|
+
dropPolicy: "drop-oldest",
|
|
58
|
+
shutdownTimeoutMs: 10_000,
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
flushIntervalMs: 10_000,
|
|
62
|
+
autoStart: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const http = makeDefaultHttpClient({
|
|
66
|
+
baseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
67
|
+
preset: "production",
|
|
68
|
+
timeoutMs: 5_000,
|
|
69
|
+
policyPresets,
|
|
70
|
+
middleware: [withHttpObservability(observability)],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const app = Fastify({ logger: true });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Routes
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
app.get("/users/:id", async (request, reply) => {
|
|
80
|
+
const params = request.params as { id: string };
|
|
81
|
+
const ctx = makeFastifyRequestObservabilityContext(observability, request, {
|
|
82
|
+
route: "/users/:id",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const response = await ctx.run(
|
|
86
|
+
ctx.withRequestSpan(
|
|
87
|
+
asyncFlatMap(
|
|
88
|
+
logEffect("info", "users.lookup", {
|
|
89
|
+
userId: params.id,
|
|
90
|
+
authorization: request.headers.authorization,
|
|
91
|
+
}),
|
|
92
|
+
() =>
|
|
93
|
+
http.getJson(`/users/${params.id}`, {
|
|
94
|
+
policy: "readModel",
|
|
95
|
+
timeoutMs: 2_000,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return reply.send(response.body);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
app.get("/metrics", async (_request, reply) => {
|
|
105
|
+
return reply
|
|
106
|
+
.type(observability.prometheus.contentType)
|
|
107
|
+
.send(observability.prometheus.export());
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Shutdown
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
await app.listen({ port: Number(process.env.PORT ?? 3000), host: "0.0.0.0" });
|
|
115
|
+
|
|
116
|
+
process.once("SIGTERM", async () => {
|
|
117
|
+
await app.close();
|
|
118
|
+
await http.shutdown();
|
|
119
|
+
await observability.shutdown();
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Runnable repo example: `src/examples/observabilityFastify.ts`.
|
|
124
|
+
|
|
@@ -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
|
+
|