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,183 @@
1
+ # Express integration
2
+
3
+ Express can wire Brass directly at process startup: one observability instance,
4
+ one runtime, one HTTP client, and a shutdown handler.
5
+
6
+ ## App Setup
7
+
8
+ ```ts
9
+ import express from "express";
10
+ import { asyncFlatMap } from "brass-runtime/core";
11
+ import {
12
+ defineHttpPolicyPresets,
13
+ makeDefaultHttpClient,
14
+ } from "brass-runtime/http";
15
+ import {
16
+ logEffect,
17
+ makeExpressRequestObservabilityContext,
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
+ const observability = makeObservability({
37
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-express",
38
+ serviceVersion: process.env.OTEL_SERVICE_VERSION,
39
+ resource: {
40
+ "deployment.environment": process.env.NODE_ENV ?? "development",
41
+ },
42
+ logs: { minLevel: "info" },
43
+ sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
44
+ redaction: {},
45
+ cardinality: { maxValuesPerLabel: 100 },
46
+ otlp: makeOtlpOptions({
47
+ endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
48
+ headers: process.env.GRAFANA_OTLP_AUTHORIZATION
49
+ ? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
50
+ : undefined,
51
+ timeoutMs: 10_000,
52
+ retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
53
+ pipeline: {
54
+ maxQueueSize: 10_000,
55
+ batchSize: 512,
56
+ dropPolicy: "drop-oldest",
57
+ shutdownTimeoutMs: 10_000,
58
+ },
59
+ }),
60
+ flushIntervalMs: 10_000,
61
+ autoStart: true,
62
+ });
63
+
64
+ const http = makeDefaultHttpClient({
65
+ baseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
66
+ preset: "production",
67
+ timeoutMs: 5_000,
68
+ policyPresets,
69
+ middleware: [withHttpObservability(observability)],
70
+ });
71
+
72
+ const app = express();
73
+ ```
74
+
75
+ ## Routes
76
+
77
+ ```ts
78
+ app.get("/users/:id", async (req, res, next) => {
79
+ const ctx = makeExpressRequestObservabilityContext(observability, req, {
80
+ route: "/users/:id",
81
+ });
82
+
83
+ try {
84
+ const response = await ctx.run(
85
+ ctx.withRequestSpan(
86
+ asyncFlatMap(
87
+ logEffect("info", "users.lookup", {
88
+ userId: req.params.id,
89
+ authorization: req.headers.authorization,
90
+ }),
91
+ () =>
92
+ http.getJson(`/users/${req.params.id}`, {
93
+ policy: "readModel",
94
+ timeoutMs: 2_000,
95
+ }),
96
+ ),
97
+ ),
98
+ );
99
+
100
+ res.json(response.body);
101
+ } catch (error) {
102
+ next(error);
103
+ }
104
+ });
105
+
106
+ app.get("/metrics", (_req, res) => {
107
+ res
108
+ .type(observability.prometheus.contentType)
109
+ .send(observability.prometheus.export());
110
+ });
111
+ ```
112
+
113
+ ## Shutdown
114
+
115
+ ```ts
116
+ const server = app.listen(process.env.PORT ?? 3000);
117
+
118
+ process.once("SIGTERM", async () => {
119
+ await new Promise<void>((resolve) => server.close(() => resolve()));
120
+ await http.shutdown();
121
+ await observability.shutdown();
122
+ });
123
+ ```
124
+
125
+ Runnable repo example: `src/examples/observabilityExpress.ts`.
126
+
127
+ ## Layer Variant
128
+
129
+ For larger Express apps, use the Layer/DI helpers to keep startup wiring and
130
+ shutdown ownership together:
131
+
132
+ ```ts
133
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
134
+ import { s } from "brass-runtime/schema";
135
+ import { defineHttpPolicyPresets, HttpClientService } from "brass-runtime/http";
136
+ import {
137
+ ObservabilityService,
138
+ makeObservabilityLayer,
139
+ makeObservedRuntimeLayer,
140
+ makeObservedHttpClientLayer,
141
+ makeOtlpOptions,
142
+ } from "brass-runtime/observability";
143
+
144
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
145
+ const ConfigSchema = s.object({
146
+ serviceName: s.nonEmptyString(),
147
+ apiBaseUrl: s.url(),
148
+ otlpEndpoint: s.url(),
149
+ });
150
+
151
+ const ConfigLayer = makeConfigLayer(Config, ConfigSchema, {
152
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-express",
153
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
154
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
155
+ });
156
+
157
+ const AppLayer = Layer.composeAll(
158
+ ConfigLayer,
159
+ makeObservabilityLayer((ctx) => {
160
+ const config = ctx.unsafeGet(Config);
161
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
162
+ }),
163
+ makeObservedRuntimeLayer(),
164
+ makeObservedHttpClientLayer((ctx) => ({
165
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
166
+ preset: "production",
167
+ policyPresets,
168
+ })),
169
+ );
170
+
171
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
172
+ export const brass = {
173
+ observability: built.service.unsafeGet(ObservabilityService),
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/express](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/express).
@@ -0,0 +1,173 @@
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
+
125
+ ## Layer Variant
126
+
127
+ Fastify can use the same app graph shape as Express. Build it once during
128
+ startup, read services from the produced `LayerContext`, and close the layer in
129
+ `onClose`/shutdown:
130
+
131
+ ```ts
132
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
133
+ import { s } from "brass-runtime/schema";
134
+ import { HttpClientService } from "brass-runtime/http";
135
+ import {
136
+ ObservabilityService,
137
+ makeObservabilityLayer,
138
+ makeObservedRuntimeLayer,
139
+ makeObservedHttpClientLayer,
140
+ makeOtlpOptions,
141
+ } from "brass-runtime/observability";
142
+
143
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
144
+
145
+ const AppLayer = Layer.composeAll(
146
+ makeConfigLayer(Config, s.object({
147
+ serviceName: s.nonEmptyString(),
148
+ apiBaseUrl: s.url(),
149
+ otlpEndpoint: s.url(),
150
+ }), {
151
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-fastify",
152
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
153
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
154
+ }),
155
+ makeObservabilityLayer((ctx) => {
156
+ const config = ctx.unsafeGet(Config);
157
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
158
+ }),
159
+ makeObservedRuntimeLayer(),
160
+ makeObservedHttpClientLayer((ctx) => ({
161
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
162
+ preset: "production",
163
+ })),
164
+ );
165
+
166
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
167
+ app.decorate("brass", {
168
+ observability: built.service.unsafeGet(ObservabilityService),
169
+ runtime: built.service.unsafeGet(RuntimeService),
170
+ http: built.service.unsafeGet(HttpClientService),
171
+ });
172
+ app.addHook("onClose", () => Runtime.make({}).toPromise(built.close()));
173
+ ```
@@ -0,0 +1,335 @@
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
+ ## Layer Variant
274
+
275
+ Nest providers can also be backed by one Brass layer graph. This keeps
276
+ validation, runtime hooks, HTTP observability, and shutdown in one place:
277
+
278
+ ```ts
279
+ import { Runtime, Layer, RuntimeService, makeConfigLayer } from "brass-runtime/core";
280
+ import { s } from "brass-runtime/schema";
281
+ import { HttpClientService } from "brass-runtime/http";
282
+ import {
283
+ ObservabilityService,
284
+ makeObservabilityLayer,
285
+ makeObservedRuntimeLayer,
286
+ makeObservedHttpClientLayer,
287
+ makeOtlpOptions,
288
+ } from "brass-runtime/observability";
289
+
290
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
291
+ const AppLayer = Layer.composeAll(
292
+ makeConfigLayer(Config, s.object({
293
+ serviceName: s.nonEmptyString(),
294
+ apiBaseUrl: s.url(),
295
+ otlpEndpoint: s.url(),
296
+ }), {
297
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "orders-api",
298
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
299
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
300
+ }),
301
+ makeObservabilityLayer((ctx) => {
302
+ const config = ctx.unsafeGet(Config);
303
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
304
+ }),
305
+ makeObservedRuntimeLayer(),
306
+ makeObservedHttpClientLayer((ctx) => ({
307
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
308
+ preset: "production",
309
+ })),
310
+ );
311
+
312
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
313
+
314
+ export const brassProviders = [
315
+ { provide: BRASS_OBSERVABILITY, useValue: built.service.unsafeGet(ObservabilityService) },
316
+ { provide: BRASS_RUNTIME, useValue: built.service.unsafeGet(RuntimeService) },
317
+ { provide: BRASS_HTTP, useValue: built.service.unsafeGet(HttpClientService) },
318
+ { provide: BRASS_LAYER_CLOSE, useValue: () => Runtime.make({}).toPromise(built.close()) },
319
+ ];
320
+ ```
321
+
322
+ It uses a fake OTLP `fetch` by default so it can run without a collector. To
323
+ send to Grafana/Alloy instead:
324
+
325
+ ```bash
326
+ BRASS_EXAMPLE_REAL_OTLP=true \
327
+ GRAFANA_OTLP_ENDPOINT=http://grafana-alloy:4318 \
328
+ GRAFANA_OTLP_AUTHORIZATION='Basic <token>' \
329
+ npm run example:observability:nest
330
+ ```
331
+
332
+ ## Runnable Example
333
+
334
+ A minimal runnable app lives at
335
+ [examples/nestjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nestjs).