@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.
@@ -0,0 +1,152 @@
1
+ import { APIContext, MiddlewareHandler, AstroIntegration } from 'astro';
2
+
3
+ type ExcludePattern = {
4
+ pattern: RegExp;
5
+ } | {
6
+ prefix: string;
7
+ } | {
8
+ exact: string;
9
+ };
10
+ interface OpenTelemetryMiddlewareOptions {
11
+ /**
12
+ * Paths to exclude from tracing.
13
+ * Can be an array of patterns or a function that returns true to exclude.
14
+ */
15
+ exclude?: ExcludePattern[] | ((context: APIContext) => boolean);
16
+ }
17
+
18
+ /**
19
+ * Creates an OpenTelemetry tracing middleware.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // src/middleware.ts
24
+ * import { sequence } from 'astro:middleware';
25
+ * import { createOpenTelemetryMiddleware, RECOMMENDED_EXCLUDES } from '@astroscope/opentelemetry';
26
+ *
27
+ * export const onRequest = sequence(
28
+ * createOpenTelemetryMiddleware({
29
+ * exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
30
+ * }),
31
+ * );
32
+ * ```
33
+ */
34
+ declare function createOpenTelemetryMiddleware(options?: OpenTelemetryMiddlewareOptions): MiddlewareHandler;
35
+
36
+ /**
37
+ * Instruments the global fetch to create OpenTelemetry spans for outgoing HTTP requests.
38
+ * Call this once at application startup (e.g., in your boot.ts onStartup hook).
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * // src/boot.ts
43
+ * import { NodeSDK } from "@opentelemetry/sdk-node";
44
+ * import { instrumentFetch } from "@astroscope/opentelemetry";
45
+ *
46
+ * const sdk = new NodeSDK({ ... });
47
+ *
48
+ * export function onStartup() {
49
+ * sdk.start();
50
+ * instrumentFetch();
51
+ * }
52
+ * ```
53
+ */
54
+ declare function instrumentFetch(): void;
55
+
56
+ interface OpenTelemetryIntegrationOptions {
57
+ /**
58
+ * Configure instrumentations.
59
+ */
60
+ instrumentations?: {
61
+ /**
62
+ * HTTP incoming request instrumentation (middleware).
63
+ * @default { enabled: true, exclude: RECOMMENDED_EXCLUDES }
64
+ */
65
+ http?: {
66
+ enabled: boolean;
67
+ exclude?: ExcludePattern[];
68
+ };
69
+ /**
70
+ * Fetch outgoing request instrumentation.
71
+ * @default { enabled: true }
72
+ */
73
+ fetch?: {
74
+ enabled: boolean;
75
+ };
76
+ };
77
+ }
78
+ /**
79
+ * Astro integration for OpenTelemetry instrumentation.
80
+ *
81
+ * This integration automatically:
82
+ * - Instruments incoming HTTP requests via middleware
83
+ * - Instruments outgoing fetch requests
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * // astro.config.ts
88
+ * import { defineConfig } from "astro/config";
89
+ * import { opentelemetry } from "@astroscope/opentelemetry";
90
+ *
91
+ * export default defineConfig({
92
+ * integrations: [opentelemetry()],
93
+ * });
94
+ * ```
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * // Disable fetch instrumentation
99
+ * opentelemetry({
100
+ * instrumentations: {
101
+ * fetch: { enabled: false }
102
+ * }
103
+ * })
104
+ * ```
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // Custom HTTP excludes
109
+ * opentelemetry({
110
+ * instrumentations: {
111
+ * http: {
112
+ * enabled: true,
113
+ * exclude: [
114
+ * ...RECOMMENDED_EXCLUDES,
115
+ * { exact: "/health" }
116
+ * ]
117
+ * }
118
+ * }
119
+ * })
120
+ * ```
121
+ */
122
+ declare function opentelemetry(options?: OpenTelemetryIntegrationOptions): AstroIntegration;
123
+
124
+ /**
125
+ * Vite/Astro dev server paths - only relevant in development.
126
+ */
127
+ declare const DEV_EXCLUDES: ExcludePattern[];
128
+ /**
129
+ * Astro internal paths for static assets and image optimization.
130
+ */
131
+ declare const ASTRO_STATIC_EXCLUDES: ExcludePattern[];
132
+ /**
133
+ * Common static asset paths.
134
+ */
135
+ declare const STATIC_EXCLUDES: ExcludePattern[];
136
+ /**
137
+ * Recommended excludes for OpenTelemetry middleware.
138
+ * Includes dev paths, Astro internals, and common static assets.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * createOpenTelemetryMiddleware({
143
+ * exclude: [
144
+ * ...RECOMMENDED_EXCLUDES,
145
+ * { exact: "/health" }, // your health endpoint
146
+ * ],
147
+ * })
148
+ * ```
149
+ */
150
+ declare const RECOMMENDED_EXCLUDES: ExcludePattern[];
151
+
152
+ export { ASTRO_STATIC_EXCLUDES, DEV_EXCLUDES, type ExcludePattern, type OpenTelemetryIntegrationOptions, type OpenTelemetryMiddlewareOptions, RECOMMENDED_EXCLUDES, STATIC_EXCLUDES, createOpenTelemetryMiddleware, instrumentFetch, opentelemetry };
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ import {
2
+ createOpenTelemetryMiddleware
3
+ } from "./chunk-7RBJ7XB3.js";
4
+ import {
5
+ instrumentFetch
6
+ } from "./chunk-BQFWPPEO.js";
7
+ import "./chunk-DPYEL3WF.js";
8
+
9
+ // src/integration.ts
10
+ import fs from "fs";
11
+ import path from "path";
12
+
13
+ // src/excludes.ts
14
+ var DEV_EXCLUDES = [
15
+ { prefix: "/@id/" },
16
+ { prefix: "/@fs/" },
17
+ { prefix: "/@vite/" },
18
+ { prefix: "/src/" },
19
+ { prefix: "/node_modules/" }
20
+ ];
21
+ var ASTRO_STATIC_EXCLUDES = [
22
+ { prefix: "/_astro/" },
23
+ { prefix: "/_image" }
24
+ ];
25
+ var STATIC_EXCLUDES = [
26
+ { exact: "/favicon.ico" },
27
+ { exact: "/robots.txt" },
28
+ { exact: "/sitemap.xml" },
29
+ { exact: "/browserconfig.xml" },
30
+ { exact: "/manifest.json" },
31
+ { exact: "/manifest.webmanifest" },
32
+ { prefix: "/.well-known/" }
33
+ ];
34
+ var RECOMMENDED_EXCLUDES = [
35
+ ...DEV_EXCLUDES,
36
+ ...ASTRO_STATIC_EXCLUDES,
37
+ ...STATIC_EXCLUDES
38
+ ];
39
+
40
+ // src/integration.ts
41
+ var VIRTUAL_MODULE_ID = "virtual:@astroscope/opentelemetry/config";
42
+ var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
43
+ function serializeExcludePatterns(patterns) {
44
+ const items = patterns.map((p) => {
45
+ if ("pattern" in p) {
46
+ return `{ pattern: ${p.pattern.toString()} }`;
47
+ } else if ("prefix" in p) {
48
+ return `{ prefix: ${JSON.stringify(p.prefix)} }`;
49
+ } else {
50
+ return `{ exact: ${JSON.stringify(p.exact)} }`;
51
+ }
52
+ });
53
+ return `[${items.join(", ")}]`;
54
+ }
55
+ function opentelemetry(options = {}) {
56
+ const httpConfig = options.instrumentations?.http ?? {
57
+ enabled: true,
58
+ exclude: RECOMMENDED_EXCLUDES
59
+ };
60
+ const fetchConfig = options.instrumentations?.fetch ?? { enabled: true };
61
+ const httpExclude = httpConfig.exclude ?? (httpConfig.enabled ? RECOMMENDED_EXCLUDES : []);
62
+ let isBuild = false;
63
+ let isSSR = false;
64
+ return {
65
+ name: "@astroscope/opentelemetry",
66
+ hooks: {
67
+ "astro:config:setup": ({
68
+ command,
69
+ updateConfig,
70
+ logger,
71
+ addMiddleware
72
+ }) => {
73
+ isBuild = command === "build";
74
+ if (httpConfig.enabled) {
75
+ addMiddleware({
76
+ entrypoint: "@astroscope/opentelemetry/middleware",
77
+ order: "pre"
78
+ });
79
+ }
80
+ updateConfig({
81
+ vite: {
82
+ plugins: [
83
+ {
84
+ name: "@astroscope/opentelemetry/virtual",
85
+ resolveId(id) {
86
+ if (id === VIRTUAL_MODULE_ID) {
87
+ return RESOLVED_VIRTUAL_MODULE_ID;
88
+ }
89
+ },
90
+ load(id) {
91
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) {
92
+ return `export const excludePatterns = ${serializeExcludePatterns(httpExclude)};`;
93
+ }
94
+ }
95
+ },
96
+ // Only add fetch instrumentation plugin if enabled
97
+ ...fetchConfig.enabled ? [
98
+ {
99
+ name: "@astroscope/opentelemetry/fetch",
100
+ configureServer(server) {
101
+ if (isBuild) return;
102
+ server.httpServer?.once("listening", async () => {
103
+ try {
104
+ const { instrumentFetch: instrumentFetch2 } = await import("./fetch-RERXGIJA.js");
105
+ instrumentFetch2();
106
+ logger.info("fetch instrumentation enabled");
107
+ } catch (error) {
108
+ logger.error(`Error instrumenting fetch: ${error}`);
109
+ }
110
+ });
111
+ },
112
+ configResolved(config) {
113
+ isSSR = !!config.build?.ssr;
114
+ },
115
+ writeBundle(outputOptions) {
116
+ if (!isSSR) return;
117
+ const outDir = outputOptions.dir;
118
+ if (!outDir) return;
119
+ const entryPath = path.join(outDir, "entry.mjs");
120
+ if (!fs.existsSync(entryPath)) return;
121
+ let content = fs.readFileSync(entryPath, "utf-8");
122
+ const instrumentCode = `import { instrumentFetch } from '@astroscope/opentelemetry';
123
+ instrumentFetch();
124
+ `;
125
+ content = instrumentCode + content;
126
+ fs.writeFileSync(entryPath, content);
127
+ logger.info(
128
+ "injected fetch instrumentation into entry.mjs"
129
+ );
130
+ }
131
+ }
132
+ ] : []
133
+ ]
134
+ }
135
+ });
136
+ }
137
+ }
138
+ };
139
+ }
140
+ export {
141
+ ASTRO_STATIC_EXCLUDES,
142
+ DEV_EXCLUDES,
143
+ RECOMMENDED_EXCLUDES,
144
+ STATIC_EXCLUDES,
145
+ createOpenTelemetryMiddleware,
146
+ instrumentFetch,
147
+ opentelemetry
148
+ };
@@ -0,0 +1,9 @@
1
+ import * as astro from 'astro';
2
+
3
+ /**
4
+ * Pre-configured middleware for use with the opentelemetry() integration.
5
+ * Exclude patterns are configured via the integration options.
6
+ */
7
+ declare const onRequest: astro.MiddlewareHandler;
8
+
9
+ export { onRequest };
@@ -0,0 +1,13 @@
1
+ import {
2
+ createOpenTelemetryMiddleware
3
+ } from "./chunk-7RBJ7XB3.js";
4
+ import "./chunk-DPYEL3WF.js";
5
+
6
+ // src/middleware-entrypoint.ts
7
+ import { excludePatterns } from "virtual:@astroscope/opentelemetry/config";
8
+ var onRequest = createOpenTelemetryMiddleware({
9
+ exclude: excludePatterns
10
+ });
11
+ export {
12
+ onRequest
13
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@astroscope/opentelemetry",
3
+ "version": "0.1.0",
4
+ "description": "OpenTelemetry tracing middleware for Astro SSR",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./middleware": {
14
+ "types": "./dist/middleware-entrypoint.d.ts",
15
+ "import": "./dist/middleware-entrypoint.js"
16
+ },
17
+ "./components": {
18
+ "import": "./src/components/index.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src/components"
24
+ ],
25
+ "sideEffects": false,
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/smnbbrv/astroscope.git",
32
+ "directory": "packages/opentelemetry"
33
+ },
34
+ "keywords": [
35
+ "astro",
36
+ "astro-integration",
37
+ "opentelemetry",
38
+ "tracing",
39
+ "observability",
40
+ "middleware",
41
+ "analytics",
42
+ "performance"
43
+ ],
44
+ "author": "smnbbrv",
45
+ "license": "MIT",
46
+ "bugs": {
47
+ "url": "https://github.com/smnbbrv/astroscope/issues"
48
+ },
49
+ "homepage": "https://github.com/smnbbrv/astroscope/tree/main/packages/opentelemetry#readme",
50
+ "scripts": {
51
+ "build": "tsup src/index.ts src/middleware-entrypoint.ts --format esm --dts --external virtual:@astroscope/opentelemetry/config",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "eslint 'src/**/*.{ts,tsx,astro}'",
54
+ "lint:fix": "eslint 'src/**/*.{ts,tsx,astro}' --fix"
55
+ },
56
+ "devDependencies": {
57
+ "@opentelemetry/api": "^1.9.0",
58
+ "@opentelemetry/core": "^1.30.0",
59
+ "astro": "^5.1.0",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.9.3"
62
+ },
63
+ "peerDependencies": {
64
+ "@opentelemetry/api": "^1.0.0",
65
+ "@opentelemetry/core": "^1.0.0",
66
+ "astro": "^5.0.0"
67
+ }
68
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { type AttributeValue, SpanStatusCode, context, trace } from '@opentelemetry/api';
3
+
4
+ interface Props {
5
+ /**
6
+ * Whether tracing is enabled for this component.
7
+ * When false, children render normally without any tracing overhead.
8
+ * @default true
9
+ */
10
+ enabled?: boolean;
11
+
12
+ /**
13
+ * The name of the span to create. This will appear in your tracing dashboard
14
+ * prefixed with "RENDER " (when withTimings is true) or ">> " (streaming mode).
15
+ */
16
+ name: string;
17
+
18
+ /**
19
+ * Additional attributes to attach to the span.
20
+ * Useful for adding context like component props, IDs, or other metadata.
21
+ */
22
+ params?: Record<string, AttributeValue>;
23
+
24
+ /**
25
+ * When true, buffers slot content to measure accurate render duration.
26
+ * This disables streaming for children - use only for single components.
27
+ * @default false
28
+ */
29
+ withTimings?: boolean;
30
+ }
31
+
32
+ const { name, params = {}, enabled = true, withTimings = false } = Astro.props;
33
+
34
+ const LIB_NAME = '@astroscope/opentelemetry';
35
+
36
+ let html: string | undefined;
37
+
38
+ if (enabled) {
39
+ const tracer = trace.getTracer(LIB_NAME);
40
+ const parentContext = context.active();
41
+
42
+ if (withTimings) {
43
+ const span = tracer.startSpan(`RENDER ${name}`, { attributes: params }, parentContext);
44
+
45
+ try {
46
+ html = await context.with(trace.setSpan(parentContext, span), () => Astro.slots.render('default'));
47
+
48
+ span.setStatus({ code: SpanStatusCode.OK });
49
+ } catch (e) {
50
+ span.setStatus({
51
+ code: SpanStatusCode.ERROR,
52
+ message: e instanceof Error ? e.message : 'Unknown error',
53
+ });
54
+
55
+ throw e;
56
+ } finally {
57
+ span.end();
58
+ }
59
+ } else {
60
+ const span = tracer.startSpan(`>> ${name}`, { attributes: params }, parentContext);
61
+
62
+ span.setStatus({ code: SpanStatusCode.OK });
63
+ span.end();
64
+ }
65
+ }
66
+ ---
67
+
68
+ {enabled && withTimings ? <Fragment set:html={html} /> : <slot />}
@@ -0,0 +1,2 @@
1
+ // @ts-expect-error: Astro types
2
+ export { default as Trace } from './Trace.astro';