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.
Files changed (123) hide show
  1. package/README.md +40 -8
  2. package/dist/agent/cli/main.cjs +31 -32
  3. package/dist/agent/cli/main.js +3 -4
  4. package/dist/agent/cli/main.mjs +3 -4
  5. package/dist/agent/index.cjs +4 -5
  6. package/dist/agent/index.d.ts +1 -1
  7. package/dist/agent/index.js +3 -4
  8. package/dist/agent/index.mjs +3 -4
  9. package/dist/{chunk-GYM3LLGS.mjs → chunk-2QNREG6K.mjs} +188 -5
  10. package/dist/{chunk-4ROBZFL6.cjs → chunk-2SLT3X6G.cjs} +6 -8
  11. package/dist/{chunk-KZJQ723N.cjs → chunk-3PFZGP23.cjs} +13 -15
  12. package/dist/{chunk-AVNQLJ5V.js → chunk-3PHU7FWS.js} +528 -23
  13. package/dist/{chunk-CIZFIMK5.js → chunk-4YQHPIWJ.js} +60 -11
  14. package/dist/chunk-5XADBMSU.cjs +33 -0
  15. package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
  16. package/dist/chunk-7TKI527D.cjs +123 -0
  17. package/dist/{chunk-AGR5B2BC.cjs → chunk-7TXQJFZX.cjs} +564 -12
  18. package/dist/{chunk-RKGKFN2A.js → chunk-AADFFVYS.js} +1 -1
  19. package/dist/{chunk-52PPNNI4.cjs → chunk-AJMKZXRB.cjs} +2 -2
  20. package/dist/{chunk-3AYM6WPJ.js → chunk-BG5RNEA2.js} +20 -299
  21. package/dist/{chunk-2HQTDLHF.mjs → chunk-ELLF55ER.mjs} +555 -3
  22. package/dist/{chunk-EOC4UHBS.mjs → chunk-G5JTCFMI.mjs} +2 -2
  23. package/dist/chunk-H5GYX7RZ.js +6126 -0
  24. package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
  25. package/dist/{chunk-6IXXWIUM.js → chunk-IBRHSH5H.js} +555 -3
  26. package/dist/{chunk-Q2I37RP3.cjs → chunk-IFRBVMWJ.cjs} +44 -323
  27. package/dist/{chunk-52OB2ROS.js → chunk-ITG6I7ZS.js} +2 -4
  28. package/dist/chunk-ITZQ526U.mjs +33 -0
  29. package/dist/{chunk-7JIJOVCT.js → chunk-JH4GI3DW.js} +2 -4
  30. package/dist/{chunk-76YMRMH2.cjs → chunk-KHACHFBQ.cjs} +583 -78
  31. package/dist/{chunk-MT3OWDPC.mjs → chunk-KRYP6CAE.mjs} +60 -11
  32. package/dist/chunk-KTGDLBLD.mjs +123 -0
  33. package/dist/{chunk-ENKODRU3.cjs → chunk-LXBU5E77.cjs} +143 -94
  34. package/dist/{chunk-PD4EJTQC.cjs → chunk-N6QNSTWD.cjs} +5 -5
  35. package/dist/{chunk-HLWLMW2F.mjs → chunk-OI4ESUMC.mjs} +9 -11
  36. package/dist/{chunk-EJ6BPYVR.mjs → chunk-OT2TESZU.mjs} +1 -1
  37. package/dist/{chunk-BABBZK4Y.js → chunk-PSEU65ND.js} +9 -11
  38. package/dist/{chunk-DNFO2EIZ.mjs → chunk-QCOLAHU3.mjs} +528 -23
  39. package/dist/{chunk-KH4SYAOS.mjs → chunk-QZ6QFJNM.mjs} +20 -299
  40. package/dist/{chunk-MBEJI5HF.mjs → chunk-R6WDSZA6.mjs} +2 -4
  41. package/dist/{chunk-FHQGHPMO.mjs → chunk-RREBJX2S.mjs} +2 -4
  42. package/dist/{chunk-5QC7LRZ3.js → chunk-S4HHFUYP.js} +2 -2
  43. package/dist/{chunk-GLE2WY7Z.cjs → chunk-SSQJKDN3.cjs} +194 -11
  44. package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
  45. package/dist/chunk-VIFA4DPN.cjs +6126 -0
  46. package/dist/chunk-W6WR37HN.js +33 -0
  47. package/dist/{chunk-FH2X7BVP.js → chunk-XSAHV5HQ.js} +188 -5
  48. package/dist/chunk-YM3EDNYD.js +123 -0
  49. package/dist/{chunk-VN44DYYT.cjs → chunk-YTX2JYYP.cjs} +18 -20
  50. package/dist/chunk-Z3PSSXP3.mjs +6126 -0
  51. package/dist/core/index.cjs +31 -9
  52. package/dist/core/index.d.ts +19 -152
  53. package/dist/core/index.js +80 -58
  54. package/dist/core/index.mjs +80 -58
  55. package/dist/defaultClient-DhpCQW9m.d.ts +1623 -0
  56. package/dist/{effect-DIUHZ9IN.d.ts → effect-CtUDl5M5.d.ts} +1 -1
  57. package/dist/http/index.cjs +202 -59
  58. package/dist/http/index.d.ts +55 -819
  59. package/dist/http/index.js +216 -73
  60. package/dist/http/index.mjs +216 -73
  61. package/dist/http/testing.cjs +31 -10
  62. package/dist/http/testing.d.ts +16 -5
  63. package/dist/http/testing.js +29 -8
  64. package/dist/http/testing.mjs +29 -8
  65. package/dist/index.cjs +110 -88
  66. package/dist/index.d.ts +9 -8
  67. package/dist/index.js +81 -59
  68. package/dist/index.mjs +81 -59
  69. package/dist/{schedule-CK3Ml_7p.d.ts → layer-BalPI6cN.d.ts} +176 -2
  70. package/dist/observability/index.cjs +22 -7
  71. package/dist/observability/index.d.ts +32 -8
  72. package/dist/observability/index.js +21 -6
  73. package/dist/observability/index.mjs +21 -6
  74. package/dist/perf/cli.cjs +26 -28
  75. package/dist/perf/cli.js +11 -13
  76. package/dist/perf/cli.mjs +11 -13
  77. package/dist/perf/index.cjs +13 -15
  78. package/dist/perf/index.js +11 -13
  79. package/dist/perf/index.mjs +11 -13
  80. package/dist/schema/index.cjs +2 -2
  81. package/dist/schema/index.js +1 -1
  82. package/dist/schema/index.mjs +1 -1
  83. package/dist/{server-GJPg8ZSG.d.ts → server-C1zVmqE6.d.ts} +16 -5
  84. package/dist/{stream-B4oK9JFP.d.ts → stream-Bb4FTejt.d.ts} +1 -1
  85. package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DzfuE6um.d.ts} +2 -2
  86. package/dist/{tracing-DqbTKGcf.d.ts → tracing-BABA5arE.d.ts} +1 -1
  87. package/docs/README.md +4 -0
  88. package/docs/ai/PUBLIC_API.md +31 -7
  89. package/docs/articles/brass-runtime-http-observability.md +467 -0
  90. package/docs/framework-integrations.md +38 -0
  91. package/docs/frameworks/angular.md +204 -0
  92. package/docs/frameworks/express.md +183 -0
  93. package/docs/frameworks/fastify.md +173 -0
  94. package/docs/frameworks/nestjs.md +335 -0
  95. package/docs/frameworks/nextjs.md +202 -0
  96. package/docs/frameworks/react.md +183 -0
  97. package/docs/frameworks/vanilla.md +280 -0
  98. package/docs/guides/layers.md +130 -0
  99. package/docs/http-recipes.md +31 -1
  100. package/docs/http.md +50 -1
  101. package/docs/nestjs.md +6 -0
  102. package/docs/observability-framework-examples.md +12 -0
  103. package/docs/observability.md +239 -0
  104. package/docs/performance-profiler.md +6 -2
  105. package/docs/recipes/layers.md +46 -2
  106. package/docs/recipes/testing.md +25 -0
  107. package/package.json +4 -1
  108. package/dist/chunk-3LOYJFRR.cjs +0 -300
  109. package/dist/chunk-3Y2RIUMM.js +0 -300
  110. package/dist/chunk-5EC274J5.cjs +0 -2874
  111. package/dist/chunk-5VRJNBLZ.mjs +0 -2874
  112. package/dist/chunk-62AZW6UT.cjs +0 -313
  113. package/dist/chunk-74ZTY6CP.js +0 -2871
  114. package/dist/chunk-7CMJS3QE.mjs +0 -2871
  115. package/dist/chunk-A2OM6NEH.mjs +0 -194
  116. package/dist/chunk-B33ICAKP.js +0 -313
  117. package/dist/chunk-JF5WGYJJ.cjs +0 -194
  118. package/dist/chunk-KN32XNTH.mjs +0 -313
  119. package/dist/chunk-KQLYONSE.cjs +0 -2871
  120. package/dist/chunk-L2SYFEBS.js +0 -194
  121. package/dist/chunk-MIIYDLGM.js +0 -2874
  122. package/dist/chunk-PWC3RBQE.mjs +0 -300
  123. 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).