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,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).
|