@astroscope/opentelemetry 0.1.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 +449 -0
- package/dist/chunk-63MOS4WA.js +146 -0
- package/dist/chunk-7RBJ7XB3.js +150 -0
- package/dist/chunk-BQFWPPEO.js +83 -0
- package/dist/chunk-CEPTXEJV.js +89 -0
- package/dist/chunk-DPYEL3WF.js +91 -0
- package/dist/chunk-FEC4ETRL.js +70 -0
- package/dist/chunk-KLGWLEAU.js +123 -0
- package/dist/chunk-OFMO3ZKX.js +124 -0
- package/dist/chunk-QIWOBUML.js +89 -0
- package/dist/chunk-QTSNLOSC.js +70 -0
- package/dist/chunk-UPNRPRAW.js +72 -0
- package/dist/chunk-VHXZF2FP.js +124 -0
- package/dist/chunk-WTUM3JRA.js +155 -0
- package/dist/fetch-55MNQVLN.js +7 -0
- package/dist/fetch-FUKHXS77.js +6 -0
- package/dist/fetch-JGQFPQSL.js +6 -0
- package/dist/fetch-RERXGIJA.js +7 -0
- package/dist/fetch-SVGRNFLY.js +7 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +148 -0
- package/dist/middleware-entrypoint.d.ts +9 -0
- package/dist/middleware-entrypoint.js +13 -0
- package/package.json +68 -0
- package/src/components/Trace.astro +68 -0
- package/src/components/index.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# @astroscope/opentelemetry
|
|
2
|
+
|
|
3
|
+
OpenTelemetry support for Astro SSR.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
- [demo/opentelemetry](../../demo/opentelemetry) - Integration-based tracing (works in dev and production)
|
|
8
|
+
- [demo/opentelemetry-native](../../demo/opentelemetry-native) - Native ESM auto-instrumentation (production only)
|
|
9
|
+
|
|
10
|
+
## Why?
|
|
11
|
+
|
|
12
|
+
OpenTelemetry's auto-instrumentation relies on monkey-patching Node.js modules, which has two challenges:
|
|
13
|
+
|
|
14
|
+
1. **ESM support is still experimental** - Node.js ESM modules can't be monkey-patched like CommonJS. The OpenTelemetry team has been working on this for years ([#1946](https://github.com/open-telemetry/opentelemetry-js/issues/1946), [#4553](https://github.com/open-telemetry/opentelemetry-js/issues/4553)), and while there's progress, it requires experimental loader hooks that aren't yet stable.
|
|
15
|
+
|
|
16
|
+
2. **Vite dev mode loads modules before instrumentation** - Auto-instrumentation must run before any instrumented modules (like `http`) are imported. In Vite's dev mode, modules are loaded dynamically, making it impossible to instrument them in time.
|
|
17
|
+
|
|
18
|
+
This package handles both issues by creating spans directly in Astro's request lifecycle - no monkey-patching required. It works in both dev mode and production.
|
|
19
|
+
|
|
20
|
+
### Comparison
|
|
21
|
+
|
|
22
|
+
| Feature | @astroscope/opentelemetry | Native auto-instrumentation |
|
|
23
|
+
|---------|---------------------------|----------------------------|
|
|
24
|
+
| Works in dev mode | ✅ | ❌ |
|
|
25
|
+
| Works in production | ✅ | ✅ |
|
|
26
|
+
| Incoming HTTP requests | ✅ | ✅ |
|
|
27
|
+
| Outgoing fetch requests | ✅ | ❌ (ESM not supported) |
|
|
28
|
+
| Astro actions | ✅ (named spans) | ❌ |
|
|
29
|
+
| Component tracing | ✅ (`<Trace>` component) | ❌ |
|
|
30
|
+
| Metrics (Prometheus-compatible) | ✅ | ✅ |
|
|
31
|
+
| Other libraries | ❌ | ✅ (varies by library) |
|
|
32
|
+
| Setup complexity | Simple | Requires `--import` flag |
|
|
33
|
+
| Bundle size | Minimal | Heavy (30+ packages) |
|
|
34
|
+
| Cold start impact | Negligible | Significant |
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @astroscope/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/sdk-node
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Setup
|
|
44
|
+
|
|
45
|
+
### 1. Initialize the SDK
|
|
46
|
+
|
|
47
|
+
The OpenTelemetry SDK must be initialized before traces can be collected. Use [`@astroscope/boot`](../boot) for proper lifecycle management:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/boot.ts
|
|
51
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
52
|
+
|
|
53
|
+
const sdk = new NodeSDK({
|
|
54
|
+
// configuration at https://opentelemetry.io/docs/languages/js/getting-started/nodejs/
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function onStartup() {
|
|
58
|
+
sdk.start();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function onShutdown() {
|
|
62
|
+
await sdk.shutdown();
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Note, since this integration creates spans directly, you don't need to
|
|
67
|
+
|
|
68
|
+
- add any instrumentations to the SDK configuration
|
|
69
|
+
- use specific import order for auto-instrumentation (since none is used)
|
|
70
|
+
|
|
71
|
+
### 2. Add the integration
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// astro.config.ts
|
|
75
|
+
import { defineConfig } from "astro/config";
|
|
76
|
+
import boot from "@astroscope/boot";
|
|
77
|
+
import { opentelemetry } from "@astroscope/opentelemetry";
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
integrations: [opentelemetry(), boot()], // opentelemetry() should come as early as possible in the list
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This automatically:
|
|
85
|
+
- Adds middleware to trace incoming HTTP requests
|
|
86
|
+
- Instruments `fetch()` to trace outgoing requests
|
|
87
|
+
- Uses `RECOMMENDED_EXCLUDES` to skip static assets
|
|
88
|
+
- Provides `<Trace>` component for tracing specific sections or components
|
|
89
|
+
|
|
90
|
+
## Integration Options
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
opentelemetry({
|
|
94
|
+
instrumentations: {
|
|
95
|
+
http: {
|
|
96
|
+
enabled: true, // default: true
|
|
97
|
+
exclude: [...RECOMMENDED_EXCLUDES, { exact: "/health" }],
|
|
98
|
+
},
|
|
99
|
+
fetch: {
|
|
100
|
+
enabled: true, // default: true
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `instrumentations.http`
|
|
107
|
+
|
|
108
|
+
Controls incoming HTTP request tracing via middleware.
|
|
109
|
+
|
|
110
|
+
| Option | Type | Default | Description |
|
|
111
|
+
|--------|------|---------|-------------|
|
|
112
|
+
| `enabled` | `boolean` | `true` | Enable/disable HTTP tracing |
|
|
113
|
+
| `exclude` | `ExcludePattern[]` | `RECOMMENDED_EXCLUDES` | Paths to exclude from tracing |
|
|
114
|
+
|
|
115
|
+
### `instrumentations.fetch`
|
|
116
|
+
|
|
117
|
+
Controls outgoing fetch request tracing.
|
|
118
|
+
|
|
119
|
+
| Option | Type | Default | Description |
|
|
120
|
+
|--------|------|---------|-------------|
|
|
121
|
+
| `enabled` | `boolean` | `true` | Enable/disable fetch tracing |
|
|
122
|
+
|
|
123
|
+
## Metrics
|
|
124
|
+
|
|
125
|
+
To export metrics, configure a metrics reader in your SDK (e.g., Prometheus exporter):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// src/boot.ts
|
|
129
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
130
|
+
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
|
131
|
+
|
|
132
|
+
const prometheusExporter = new PrometheusExporter({ port: 9464 });
|
|
133
|
+
|
|
134
|
+
const sdk = new NodeSDK({
|
|
135
|
+
serviceName: "my-astro-app",
|
|
136
|
+
metricReader: prometheusExporter,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export function onStartup() {
|
|
140
|
+
sdk.start();
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
or use OTLP metrics exporter (for Grafana, Datadog, etc.):
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
148
|
+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
149
|
+
|
|
150
|
+
const metricReader = new PeriodicExportingMetricReader({
|
|
151
|
+
exporter: new OTLPMetricExporter(),
|
|
152
|
+
exportIntervalMillis: 60000,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const sdk = new NodeSDK({ metricReader });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Collected Metrics
|
|
159
|
+
|
|
160
|
+
| Metric | Type | Unit | Description |
|
|
161
|
+
|--------|------|------|-------------|
|
|
162
|
+
| `http.server.request.duration` | Histogram | seconds | Duration of incoming HTTP requests |
|
|
163
|
+
| `http.server.active_requests` | UpDownCounter | requests | Number of active HTTP requests |
|
|
164
|
+
| `http.client.request.duration` | Histogram | seconds | Duration of outgoing fetch requests |
|
|
165
|
+
| `astro.action.duration` | Histogram | seconds | Duration of Astro action executions |
|
|
166
|
+
|
|
167
|
+
### Metric Attributes
|
|
168
|
+
|
|
169
|
+
**HTTP Server metrics:**
|
|
170
|
+
- `http.request.method` - HTTP method (GET, POST, etc.)
|
|
171
|
+
- `http.route` - Request path
|
|
172
|
+
- `http.response.status_code` - Response status code
|
|
173
|
+
|
|
174
|
+
**HTTP Client (fetch) metrics:**
|
|
175
|
+
- `http.request.method` - HTTP method
|
|
176
|
+
- `server.address` - Target hostname
|
|
177
|
+
- `http.response.status_code` - Response status code
|
|
178
|
+
|
|
179
|
+
**Astro Action metrics:**
|
|
180
|
+
- `astro.action.name` - Action name (e.g., `newsletter.subscribe`)
|
|
181
|
+
- `http.response.status_code` - Response status code
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Host & Runtime Metrics
|
|
185
|
+
|
|
186
|
+
For system-level and Node.js runtime metrics (CPU, memory, event loop, GC), add these packages:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm install @opentelemetry/host-metrics @opentelemetry/instrumentation-runtime-node
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// src/boot.ts
|
|
194
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
195
|
+
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
|
196
|
+
import { HostMetrics } from "@opentelemetry/host-metrics";
|
|
197
|
+
import { RuntimeNodeInstrumentation } from "@opentelemetry/instrumentation-runtime-node";
|
|
198
|
+
|
|
199
|
+
const sdk = new NodeSDK({
|
|
200
|
+
serviceName: "my-astro-app",
|
|
201
|
+
metricReader: new PrometheusExporter({ port: 9464 }),
|
|
202
|
+
instrumentations: [new RuntimeNodeInstrumentation()],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
let hostMetrics: HostMetrics;
|
|
206
|
+
|
|
207
|
+
export function onStartup() {
|
|
208
|
+
sdk.start();
|
|
209
|
+
|
|
210
|
+
// the host metrics should be called after sdk.start()
|
|
211
|
+
hostMetrics = new HostMetrics({ name: "my-astro-app" });
|
|
212
|
+
hostMetrics.start();
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
This adds:
|
|
217
|
+
- **Host metrics**: `process.cpu.*`, `system.cpu.*`, `system.memory.*`, `system.network.*`
|
|
218
|
+
- **Runtime metrics**: `nodejs.eventloop.delay.*`, `nodejs.gc.duration`, `nodejs.eventloop.utilization`
|
|
219
|
+
|
|
220
|
+
## Component Tracing
|
|
221
|
+
|
|
222
|
+
Trace specific sections or components using the `<Trace>` component:
|
|
223
|
+
|
|
224
|
+
```astro
|
|
225
|
+
---
|
|
226
|
+
import { Trace } from "@astroscope/opentelemetry/components";
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
<Trace name="hero">
|
|
230
|
+
<HeroSection />
|
|
231
|
+
</Trace>
|
|
232
|
+
|
|
233
|
+
<Trace name="sidebar">
|
|
234
|
+
<Sidebar />
|
|
235
|
+
</Trace>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Props
|
|
239
|
+
|
|
240
|
+
| Prop | Type | Default | Description |
|
|
241
|
+
|------|------|---------|-------------|
|
|
242
|
+
| `name` | `string` | required | Span name |
|
|
243
|
+
| `params` | `Record<string, AttributeValue>` | `{}` | Custom span attributes |
|
|
244
|
+
| `enabled` | `boolean` | `true` | Enable/disable tracing (useful for conditional tracing) |
|
|
245
|
+
| `withTimings` | `boolean` | `false` | Enable accurate duration measurement |
|
|
246
|
+
|
|
247
|
+
### Streaming vs Timing Mode
|
|
248
|
+
|
|
249
|
+
By default, `<Trace>` preserves Astro's streaming behavior by creating an instant marker span (`>> name`). This is safe to use anywhere without affecting performance.
|
|
250
|
+
|
|
251
|
+
```astro
|
|
252
|
+
<!-- Default: streaming preserved, instant span -->
|
|
253
|
+
<Trace name="section">
|
|
254
|
+
<SlowComponent />
|
|
255
|
+
<FastComponent />
|
|
256
|
+
</Trace>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
For accurate render duration measurement, use `withTimings`:
|
|
260
|
+
|
|
261
|
+
```astro
|
|
262
|
+
<!-- With timing: accurate duration, but buffers content -->
|
|
263
|
+
<Trace name="data-table" withTimings>
|
|
264
|
+
<DataTable data={data} />
|
|
265
|
+
</Trace>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Important:** `withTimings` buffers all children before streaming. Use it only when:
|
|
269
|
+
- Wrapping a **single component** where you need timing
|
|
270
|
+
- You understand it will block streaming for that section
|
|
271
|
+
|
|
272
|
+
### Span Names
|
|
273
|
+
|
|
274
|
+
| Mode | Span Name | Description |
|
|
275
|
+
|------|-----------|-------------|
|
|
276
|
+
| `withTimings={false}` | `>> section-name` | Instant marker, no duration |
|
|
277
|
+
| `withTimings={true}` | `RENDER section-name` | Full render duration |
|
|
278
|
+
|
|
279
|
+
### Conditional Tracing
|
|
280
|
+
|
|
281
|
+
Use `enabled` to conditionally trace (e.g., in recursive components):
|
|
282
|
+
|
|
283
|
+
```astro
|
|
284
|
+
---
|
|
285
|
+
const { depth = 0 } = Astro.props;
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
<Trace name="tree-node" enabled={depth < 3} params={{ depth }}>
|
|
289
|
+
<TreeNode>
|
|
290
|
+
{children.map(child => <Astro.self depth={depth + 1} {...child} />)}
|
|
291
|
+
</TreeNode>
|
|
292
|
+
</Trace>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Context Propagation
|
|
296
|
+
|
|
297
|
+
Nested `<Trace>` components with `withTimings` create proper parent-child relationships:
|
|
298
|
+
|
|
299
|
+
```astro
|
|
300
|
+
<Trace name="page" withTimings>
|
|
301
|
+
<Trace name="header" withTimings>
|
|
302
|
+
<Header />
|
|
303
|
+
</Trace>
|
|
304
|
+
<Trace name="content" withTimings>
|
|
305
|
+
<Content />
|
|
306
|
+
</Trace>
|
|
307
|
+
</Trace>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Results in:
|
|
311
|
+
```
|
|
312
|
+
RENDER page
|
|
313
|
+
├── RENDER header
|
|
314
|
+
└── RENDER content
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Exclude Patterns
|
|
318
|
+
|
|
319
|
+
**Pattern types:**
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
exclude: [
|
|
323
|
+
{ prefix: "/_astro" }, // path.startsWith("/_astro")
|
|
324
|
+
{ exact: "/health" }, // path === "/health"
|
|
325
|
+
{ pattern: /^\/api\/internal/ } // regex.test(path)
|
|
326
|
+
]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Pre-built exclude lists:**
|
|
330
|
+
|
|
331
|
+
| Export | Description |
|
|
332
|
+
|--------|-------------|
|
|
333
|
+
| `RECOMMENDED_EXCLUDES` | All excludes below combined |
|
|
334
|
+
| `DEV_EXCLUDES` | Vite/Astro dev server (`/@vite/`, `/@fs/`, etc.) |
|
|
335
|
+
| `ASTRO_STATIC_EXCLUDES` | Astro static assets (`/_astro/`, `/_image`) |
|
|
336
|
+
| `STATIC_EXCLUDES` | Common static files (`/assets/`, `/favicon.ico`, etc.) |
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import {
|
|
340
|
+
opentelemetry,
|
|
341
|
+
RECOMMENDED_EXCLUDES,
|
|
342
|
+
} from "@astroscope/opentelemetry";
|
|
343
|
+
|
|
344
|
+
opentelemetry({
|
|
345
|
+
instrumentations: {
|
|
346
|
+
http: {
|
|
347
|
+
enabled: true,
|
|
348
|
+
exclude: [...RECOMMENDED_EXCLUDES, { exact: "/health" }],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Trace Context Propagation
|
|
355
|
+
|
|
356
|
+
The middleware automatically extracts `traceparent` and `tracestate` headers from incoming requests, allowing traces to span across services.
|
|
357
|
+
|
|
358
|
+
## Manual Setup
|
|
359
|
+
|
|
360
|
+
If you prefer manual control instead of using the integration:
|
|
361
|
+
|
|
362
|
+
### Manual middleware
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// src/middleware.ts
|
|
366
|
+
import { sequence } from "astro:middleware";
|
|
367
|
+
import {
|
|
368
|
+
createOpenTelemetryMiddleware,
|
|
369
|
+
RECOMMENDED_EXCLUDES,
|
|
370
|
+
} from "@astroscope/opentelemetry";
|
|
371
|
+
|
|
372
|
+
export const onRequest = sequence(
|
|
373
|
+
createOpenTelemetryMiddleware({
|
|
374
|
+
exclude: [...RECOMMENDED_EXCLUDES, { exact: "/health" }],
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Manual fetch instrumentation
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// src/boot.ts
|
|
383
|
+
import { instrumentFetch } from "@astroscope/opentelemetry";
|
|
384
|
+
|
|
385
|
+
export function onStartup() {
|
|
386
|
+
instrumentFetch();
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Alternative: Native ESM Auto-Instrumentation
|
|
391
|
+
|
|
392
|
+
If you only need tracing in production builds, you can use OpenTelemetry's native ESM loader hooks instead of this middleware. This approach uses Node.js module hooks to auto-instrument libraries like `http`, `express`, `pg`, etc.
|
|
393
|
+
|
|
394
|
+
**Advantages:**
|
|
395
|
+
|
|
396
|
+
- Full auto-instrumentation (HTTP client requests, database queries, etc.)
|
|
397
|
+
- No middleware code required
|
|
398
|
+
|
|
399
|
+
**Disadvantages:**
|
|
400
|
+
|
|
401
|
+
- Only works in production builds (not in Vite dev mode)
|
|
402
|
+
- Not all instrumentations support ESM yet ([tracking issue](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1942))
|
|
403
|
+
|
|
404
|
+
**Recommendation:** Use this package for Astro-specific tracing (HTTP, fetch, actions). For database and other library instrumentation, add only the specific instrumentations you need (e.g., `@opentelemetry/instrumentation-pg`) rather than `@opentelemetry/auto-instrumentations-node`, which pulls dozens of packages - most of which won't work in ESM anyway.
|
|
405
|
+
|
|
406
|
+
**Note:** When combining with native auto-instrumentation, you can disable the HTTP middleware (to avoid duplicate incoming request spans) while keeping fetch instrumentation (enabled by default):
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
opentelemetry({
|
|
410
|
+
instrumentations: {
|
|
411
|
+
http: { enabled: false }, // let native instrumentation work for incoming requests
|
|
412
|
+
// fetch remains enabled by default - native doesn't support it in ESM yet
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Setup
|
|
418
|
+
|
|
419
|
+
1. Create a `register.mjs` file:
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
// register.mjs
|
|
423
|
+
import { register } from "node:module";
|
|
424
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
425
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
426
|
+
|
|
427
|
+
register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);
|
|
428
|
+
|
|
429
|
+
const sdk = new NodeSDK({
|
|
430
|
+
serviceName: "my-astro-app",
|
|
431
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
sdk.start();
|
|
435
|
+
|
|
436
|
+
process.on("SIGTERM", () => {
|
|
437
|
+
sdk.shutdown().finally(() => process.exit(0));
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
2. Start your production server with the `--import` flag:
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
node --import=./register.mjs ./dist/server/entry.mjs
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## License
|
|
448
|
+
|
|
449
|
+
MIT
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordHttpRequestDuration,
|
|
3
|
+
recordHttpRequestStart
|
|
4
|
+
} from "./chunk-UPNRPRAW.js";
|
|
5
|
+
|
|
6
|
+
// src/middleware.ts
|
|
7
|
+
import {
|
|
8
|
+
SpanKind,
|
|
9
|
+
SpanStatusCode,
|
|
10
|
+
context,
|
|
11
|
+
propagation,
|
|
12
|
+
trace
|
|
13
|
+
} from "@opentelemetry/api";
|
|
14
|
+
import { RPCType, setRPCMetadata } from "@opentelemetry/core";
|
|
15
|
+
var LIB_NAME = "@astroscope/opentelemetry";
|
|
16
|
+
var ACTIONS_PREFIX = "/_actions/";
|
|
17
|
+
function matchesPattern(path, pattern) {
|
|
18
|
+
if ("pattern" in pattern) {
|
|
19
|
+
return pattern.pattern.test(path);
|
|
20
|
+
}
|
|
21
|
+
if ("prefix" in pattern) {
|
|
22
|
+
return path.startsWith(pattern.prefix);
|
|
23
|
+
}
|
|
24
|
+
return path === pattern.exact;
|
|
25
|
+
}
|
|
26
|
+
function shouldExclude(ctx, exclude) {
|
|
27
|
+
if (!exclude) return false;
|
|
28
|
+
if (typeof exclude === "function") {
|
|
29
|
+
return exclude(ctx);
|
|
30
|
+
}
|
|
31
|
+
const path = ctx.url.pathname;
|
|
32
|
+
return exclude.some((pattern) => matchesPattern(path, pattern));
|
|
33
|
+
}
|
|
34
|
+
function getClientIp(request) {
|
|
35
|
+
return request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? request.headers.get("x-real-ip") ?? request.headers.get("cf-connecting-ip") ?? // Cloudflare
|
|
36
|
+
void 0;
|
|
37
|
+
}
|
|
38
|
+
function createOpenTelemetryMiddleware(options = {}) {
|
|
39
|
+
const tracer = trace.getTracer(LIB_NAME);
|
|
40
|
+
return async (ctx, next) => {
|
|
41
|
+
if (shouldExclude(ctx, options.exclude)) {
|
|
42
|
+
return next();
|
|
43
|
+
}
|
|
44
|
+
const { request, url } = ctx;
|
|
45
|
+
const input = {
|
|
46
|
+
traceparent: request.headers.get("traceparent"),
|
|
47
|
+
tracestate: request.headers.get("tracestate")
|
|
48
|
+
};
|
|
49
|
+
const parentContext = propagation.extract(context.active(), input);
|
|
50
|
+
const clientIp = getClientIp(request);
|
|
51
|
+
const contentLength = request.headers.get("content-length");
|
|
52
|
+
const spanOptions = {
|
|
53
|
+
attributes: {
|
|
54
|
+
"http.request.method": request.method,
|
|
55
|
+
"url.full": request.url,
|
|
56
|
+
"url.path": url.pathname,
|
|
57
|
+
"url.query": url.search.slice(1),
|
|
58
|
+
// Remove leading "?"
|
|
59
|
+
"url.scheme": url.protocol.replace(":", ""),
|
|
60
|
+
"server.address": url.hostname,
|
|
61
|
+
"server.port": url.port ? parseInt(url.port) : url.protocol === "https:" ? 443 : 80,
|
|
62
|
+
"user_agent.original": request.headers.get("user-agent") ?? "",
|
|
63
|
+
...contentLength && { "http.request.body.size": parseInt(contentLength) },
|
|
64
|
+
...clientIp && { "client.address": clientIp }
|
|
65
|
+
},
|
|
66
|
+
kind: SpanKind.SERVER
|
|
67
|
+
};
|
|
68
|
+
const isAction = url.pathname.startsWith(ACTIONS_PREFIX);
|
|
69
|
+
const actionName = url.pathname.slice(ACTIONS_PREFIX.length).replace(/\/$/, "");
|
|
70
|
+
const spanName = isAction ? `ACTION ${actionName}` : `${request.method} ${url.pathname}`;
|
|
71
|
+
const span = tracer.startSpan(spanName, spanOptions, parentContext);
|
|
72
|
+
const spanContext = trace.setSpan(parentContext, span);
|
|
73
|
+
const rpcMetadata = { type: RPCType.HTTP, span };
|
|
74
|
+
const metricsEnabled = options.metrics ?? false;
|
|
75
|
+
const startTime = metricsEnabled ? performance.now() : 0;
|
|
76
|
+
const endActiveRequest = metricsEnabled ? recordHttpRequestStart({ method: request.method, route: url.pathname }) : void 0;
|
|
77
|
+
return context.with(
|
|
78
|
+
setRPCMetadata(spanContext, rpcMetadata),
|
|
79
|
+
async () => {
|
|
80
|
+
const finalize = (status, responseSize) => {
|
|
81
|
+
span.setAttribute("http.response.status_code", status);
|
|
82
|
+
span.setAttribute("http.response.body.size", responseSize);
|
|
83
|
+
if (status >= 400) {
|
|
84
|
+
span.setStatus({
|
|
85
|
+
code: SpanStatusCode.ERROR,
|
|
86
|
+
message: `HTTP ${status}`
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
90
|
+
}
|
|
91
|
+
span.end();
|
|
92
|
+
if (metricsEnabled) {
|
|
93
|
+
endActiveRequest?.();
|
|
94
|
+
recordHttpRequestDuration(
|
|
95
|
+
{ method: request.method, route: url.pathname, status },
|
|
96
|
+
performance.now() - startTime
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
const response = await next();
|
|
102
|
+
if (!response.body) {
|
|
103
|
+
finalize(response.status, 0);
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
const [measureStream, clientStream] = response.body.tee();
|
|
107
|
+
let responseSize = 0;
|
|
108
|
+
(async () => {
|
|
109
|
+
const reader = measureStream.getReader();
|
|
110
|
+
try {
|
|
111
|
+
while (true) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
responseSize += value.length;
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
finalize(response.status, responseSize);
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
return new Response(clientStream, {
|
|
121
|
+
status: response.status,
|
|
122
|
+
headers: response.headers
|
|
123
|
+
});
|
|
124
|
+
} catch (e) {
|
|
125
|
+
span.setStatus({
|
|
126
|
+
code: SpanStatusCode.ERROR,
|
|
127
|
+
message: e instanceof Error ? e.message : "Unknown error"
|
|
128
|
+
});
|
|
129
|
+
span.end();
|
|
130
|
+
if (metricsEnabled) {
|
|
131
|
+
endActiveRequest?.();
|
|
132
|
+
recordHttpRequestDuration(
|
|
133
|
+
{ method: request.method, route: url.pathname, status: 500 },
|
|
134
|
+
performance.now() - startTime
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
createOpenTelemetryMiddleware
|
|
146
|
+
};
|