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,202 @@
|
|
|
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
|
+
|
|
148
|
+
## Layer Variant
|
|
149
|
+
|
|
150
|
+
For Route Handlers, keep a server-only layer singleton and close it from your
|
|
151
|
+
process lifecycle when the platform exposes one:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// app/lib/brass.server.ts
|
|
155
|
+
import "server-only";
|
|
156
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
157
|
+
import { s } from "brass-runtime/schema";
|
|
158
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
159
|
+
import {
|
|
160
|
+
ObservabilityService,
|
|
161
|
+
makeObservabilityLayer,
|
|
162
|
+
makeObservedRuntimeLayer,
|
|
163
|
+
makeObservedHttpClientLayer,
|
|
164
|
+
makeOtlpOptions,
|
|
165
|
+
} from "brass-runtime/observability";
|
|
166
|
+
|
|
167
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
168
|
+
const AppLayer = Layer.composeAll(
|
|
169
|
+
makeConfigLayer(Config, s.object({
|
|
170
|
+
serviceName: s.nonEmptyString(),
|
|
171
|
+
apiBaseUrl: s.url(),
|
|
172
|
+
otlpEndpoint: s.url(),
|
|
173
|
+
}), {
|
|
174
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-next",
|
|
175
|
+
apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
|
|
176
|
+
otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
177
|
+
}),
|
|
178
|
+
makeObservabilityLayer((ctx) => {
|
|
179
|
+
const config = ctx.unsafeGet(Config);
|
|
180
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
181
|
+
}),
|
|
182
|
+
makeObservedRuntimeLayer(),
|
|
183
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
184
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
185
|
+
preset: "production",
|
|
186
|
+
})),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
190
|
+
|
|
191
|
+
export const brass = {
|
|
192
|
+
observability: built.service.unsafeGet(ObservabilityService),
|
|
193
|
+
runtime: built.service.unsafeGet(RuntimeService),
|
|
194
|
+
http: built.service.unsafeGet(HttpClientService),
|
|
195
|
+
shutdown: () => Runtime.make({}).toPromise(built.close()),
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Runnable Example
|
|
200
|
+
|
|
201
|
+
A minimal runnable app lives at
|
|
202
|
+
[examples/nextjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nextjs).
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
|
|
140
|
+
## Layer Variant
|
|
141
|
+
|
|
142
|
+
The provider can build a layer graph instead of manually constructing each
|
|
143
|
+
service. In browser apps, use a same-origin OTLP proxy and close the layer from
|
|
144
|
+
the React cleanup:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
148
|
+
import { s } from "brass-runtime/schema";
|
|
149
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
150
|
+
import {
|
|
151
|
+
makeObservabilityLayer,
|
|
152
|
+
makeObservedRuntimeLayer,
|
|
153
|
+
makeObservedHttpClientLayer,
|
|
154
|
+
makeOtlpOptions,
|
|
155
|
+
} from "brass-runtime/observability";
|
|
156
|
+
|
|
157
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
158
|
+
const AppLayer = Layer.composeAll(
|
|
159
|
+
makeConfigLayer(Config, s.object({
|
|
160
|
+
serviceName: s.nonEmptyString(),
|
|
161
|
+
apiBaseUrl: s.string(),
|
|
162
|
+
otlpEndpoint: s.string(),
|
|
163
|
+
}), { serviceName: "shop-react", apiBaseUrl: "/api", otlpEndpoint: "/api/otel" }),
|
|
164
|
+
makeObservabilityLayer((ctx) => {
|
|
165
|
+
const config = ctx.unsafeGet(Config);
|
|
166
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
167
|
+
}),
|
|
168
|
+
makeObservedRuntimeLayer(),
|
|
169
|
+
makeObservedHttpClientLayer((ctx) => ({ baseUrl: ctx.unsafeGet(Config).apiBaseUrl, preset: "balanced" })),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
173
|
+
const brass = {
|
|
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/react](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/react).
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Vanilla integration
|
|
2
|
+
|
|
3
|
+
Use this when an app does not have a framework-level DI or lifecycle system.
|
|
4
|
+
The same shape works for plain browser TypeScript, Node HTTP handlers, CLIs,
|
|
5
|
+
and small services.
|
|
6
|
+
|
|
7
|
+
## Browser
|
|
8
|
+
|
|
9
|
+
Browser apps should send telemetry to a same-origin proxy. Do not ship Grafana
|
|
10
|
+
Cloud or collector credentials to the client.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// brass.browser.ts
|
|
14
|
+
import { Runtime } from "brass-runtime/core";
|
|
15
|
+
import {
|
|
16
|
+
defineHttpPolicyPresets,
|
|
17
|
+
makeDefaultHttpClient,
|
|
18
|
+
} from "brass-runtime/http";
|
|
19
|
+
import {
|
|
20
|
+
makeObservability,
|
|
21
|
+
makeOtlpOptions,
|
|
22
|
+
withHttpObservability,
|
|
23
|
+
} from "brass-runtime/observability";
|
|
24
|
+
|
|
25
|
+
const policyPresets = defineHttpPolicyPresets({
|
|
26
|
+
readModel: {
|
|
27
|
+
lane: "read-model",
|
|
28
|
+
priority: 3,
|
|
29
|
+
retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
|
|
30
|
+
},
|
|
31
|
+
command: {
|
|
32
|
+
lane: "command",
|
|
33
|
+
priority: 1,
|
|
34
|
+
retry: false,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const brass = (() => {
|
|
39
|
+
const observability = makeObservability({
|
|
40
|
+
serviceName: "shop-web",
|
|
41
|
+
resource: { "deployment.environment": "browser" },
|
|
42
|
+
logs: false,
|
|
43
|
+
sampling: { ratio: 0.1, respectRemoteSampled: true, forceSampleOnError: true },
|
|
44
|
+
redaction: {},
|
|
45
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
46
|
+
otlp: makeOtlpOptions({
|
|
47
|
+
endpoint: "/api/otel",
|
|
48
|
+
timeoutMs: 10_000,
|
|
49
|
+
retry: { attempts: 2, initialDelayMs: 100, maxDelayMs: 1_000 },
|
|
50
|
+
pipeline: { maxQueueSize: 2_000, batchSize: 128, dropPolicy: "drop-oldest" },
|
|
51
|
+
}),
|
|
52
|
+
flushIntervalMs: 15_000,
|
|
53
|
+
autoStart: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const runtime = new Runtime({
|
|
57
|
+
env: observability.env,
|
|
58
|
+
hooks: observability.hooks,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const http = makeDefaultHttpClient({
|
|
62
|
+
baseUrl: "/api",
|
|
63
|
+
preset: "balanced",
|
|
64
|
+
timeoutMs: 5_000,
|
|
65
|
+
policyPresets,
|
|
66
|
+
middleware: [withHttpObservability(observability)],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
observability,
|
|
71
|
+
runtime,
|
|
72
|
+
http,
|
|
73
|
+
shutdown: async () => {
|
|
74
|
+
await http.shutdown();
|
|
75
|
+
await observability.shutdown();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
})();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use it from ordinary browser code:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
type User = {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
readonly name: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export async function loadCurrentUser(): Promise<User> {
|
|
90
|
+
const response = await brass.runtime.toPromise(
|
|
91
|
+
brass.http.getJson<User>("/users/me", {
|
|
92
|
+
policy: "readModel",
|
|
93
|
+
timeoutMs: 2_000,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return response.body;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.addEventListener("pagehide", () => {
|
|
101
|
+
void brass.shutdown();
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Node
|
|
106
|
+
|
|
107
|
+
On the server, Brass can send directly to a collector because credentials stay
|
|
108
|
+
inside the trusted process.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// brass.node.ts
|
|
112
|
+
import { Runtime } from "brass-runtime/core";
|
|
113
|
+
import {
|
|
114
|
+
defineHttpPolicyPresets,
|
|
115
|
+
makeDefaultHttpClient,
|
|
116
|
+
} from "brass-runtime/http";
|
|
117
|
+
import {
|
|
118
|
+
makeObservability,
|
|
119
|
+
makeOtlpOptions,
|
|
120
|
+
withHttpObservability,
|
|
121
|
+
} from "brass-runtime/observability";
|
|
122
|
+
|
|
123
|
+
const policyPresets = defineHttpPolicyPresets({
|
|
124
|
+
readModel: {
|
|
125
|
+
lane: "read-model",
|
|
126
|
+
priority: 3,
|
|
127
|
+
retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
|
|
128
|
+
},
|
|
129
|
+
command: {
|
|
130
|
+
lane: "command",
|
|
131
|
+
priority: 1,
|
|
132
|
+
retry: false,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const brass = (() => {
|
|
137
|
+
const observability = makeObservability({
|
|
138
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "vanilla-node",
|
|
139
|
+
serviceVersion: process.env.OTEL_SERVICE_VERSION,
|
|
140
|
+
resource: {
|
|
141
|
+
"deployment.environment": process.env.NODE_ENV ?? "development",
|
|
142
|
+
},
|
|
143
|
+
logs: { minLevel: "info" },
|
|
144
|
+
sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
|
|
145
|
+
redaction: {},
|
|
146
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
147
|
+
otlp: makeOtlpOptions({
|
|
148
|
+
endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
149
|
+
headers: process.env.GRAFANA_OTLP_AUTHORIZATION
|
|
150
|
+
? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
|
|
151
|
+
: undefined,
|
|
152
|
+
timeoutMs: 10_000,
|
|
153
|
+
retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
|
|
154
|
+
pipeline: {
|
|
155
|
+
maxQueueSize: 10_000,
|
|
156
|
+
batchSize: 512,
|
|
157
|
+
dropPolicy: "drop-oldest",
|
|
158
|
+
shutdownTimeoutMs: 10_000,
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
flushIntervalMs: 10_000,
|
|
162
|
+
autoStart: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const runtime = new Runtime({
|
|
166
|
+
env: observability.env,
|
|
167
|
+
hooks: observability.hooks,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const http = makeDefaultHttpClient({
|
|
171
|
+
baseUrl: process.env.API_BASE_URL ?? "https://api.internal",
|
|
172
|
+
preset: "production",
|
|
173
|
+
timeoutMs: 5_000,
|
|
174
|
+
policyPresets,
|
|
175
|
+
middleware: [withHttpObservability(observability)],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
observability,
|
|
180
|
+
runtime,
|
|
181
|
+
http,
|
|
182
|
+
shutdown: async () => {
|
|
183
|
+
await http.shutdown();
|
|
184
|
+
await observability.shutdown();
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
})();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use the Node request adapter when you receive `IncomingMessage`-like requests:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { createServer } from "node:http";
|
|
194
|
+
import {
|
|
195
|
+
makeNodeRequestObservabilityContext,
|
|
196
|
+
} from "brass-runtime/observability";
|
|
197
|
+
import { brass } from "./brass.node";
|
|
198
|
+
|
|
199
|
+
const server = createServer(async (req, res) => {
|
|
200
|
+
const ctx = makeNodeRequestObservabilityContext(brass.observability, req, {
|
|
201
|
+
route: req.url?.startsWith("/users/") ? "/users/:id" : req.url,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const response = await ctx.run(
|
|
205
|
+
ctx.withRequestSpan(
|
|
206
|
+
brass.http.getJson("/users/42", {
|
|
207
|
+
policy: "readModel",
|
|
208
|
+
timeoutMs: 2_000,
|
|
209
|
+
}),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
res.setHeader("content-type", "application/json");
|
|
214
|
+
res.end(JSON.stringify(response.body));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
process.once("SIGTERM", async () => {
|
|
218
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
219
|
+
await brass.shutdown();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
server.listen(process.env.PORT ?? 3000);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Layer Variant
|
|
226
|
+
|
|
227
|
+
For scripts, CLIs, workers, or small Node servers, a layer graph keeps startup
|
|
228
|
+
and teardown explicit without a framework:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
|
|
232
|
+
import { s } from "brass-runtime/schema";
|
|
233
|
+
import { HttpClientService } from "brass-runtime/http";
|
|
234
|
+
import {
|
|
235
|
+
ObservabilityService,
|
|
236
|
+
makeObservabilityLayer,
|
|
237
|
+
makeObservedRuntimeLayer,
|
|
238
|
+
makeObservedHttpClientLayer,
|
|
239
|
+
makeOtlpOptions,
|
|
240
|
+
} from "brass-runtime/observability";
|
|
241
|
+
|
|
242
|
+
const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
|
|
243
|
+
const AppLayer = Layer.composeAll(
|
|
244
|
+
makeConfigLayer(Config, s.object({
|
|
245
|
+
serviceName: s.nonEmptyString(),
|
|
246
|
+
apiBaseUrl: s.url(),
|
|
247
|
+
otlpEndpoint: s.url(),
|
|
248
|
+
}), {
|
|
249
|
+
serviceName: "worker",
|
|
250
|
+
apiBaseUrl: "https://users-api.internal",
|
|
251
|
+
otlpEndpoint: "http://grafana-alloy:4318",
|
|
252
|
+
}),
|
|
253
|
+
makeObservabilityLayer((ctx) => {
|
|
254
|
+
const config = ctx.unsafeGet(Config);
|
|
255
|
+
return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
|
|
256
|
+
}),
|
|
257
|
+
makeObservedRuntimeLayer(),
|
|
258
|
+
makeObservedHttpClientLayer((ctx) => ({
|
|
259
|
+
baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
|
|
260
|
+
preset: "production",
|
|
261
|
+
})),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
|
|
265
|
+
const runtime = built.service.unsafeGet(RuntimeService);
|
|
266
|
+
const http = built.service.unsafeGet(HttpClientService);
|
|
267
|
+
const observability = built.service.unsafeGet(ObservabilityService);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await runtime.toPromise(http.getJson("/users/42"));
|
|
271
|
+
console.log(observability.prometheus.export());
|
|
272
|
+
} finally {
|
|
273
|
+
await Runtime.make({}).toPromise(built.close());
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Runnable Example
|
|
278
|
+
|
|
279
|
+
A minimal runnable app lives at
|
|
280
|
+
[examples/vanilla](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/vanilla).
|