@astroscope/opentelemetry 0.1.1 → 0.2.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,72 @@
1
+ // src/metrics.ts
2
+ import { metrics, ValueType } from "@opentelemetry/api";
3
+ var LIB_NAME = "@astroscope/opentelemetry";
4
+ var httpRequestDuration = null;
5
+ var httpActiveRequests = null;
6
+ var fetchRequestDuration = null;
7
+ function getHttpRequestDuration() {
8
+ if (!httpRequestDuration) {
9
+ const meter = metrics.getMeter(LIB_NAME);
10
+ httpRequestDuration = meter.createHistogram("http.server.request.duration", {
11
+ description: "Duration of HTTP server requests",
12
+ unit: "s",
13
+ valueType: ValueType.DOUBLE
14
+ });
15
+ }
16
+ return httpRequestDuration;
17
+ }
18
+ function getHttpActiveRequests() {
19
+ if (!httpActiveRequests) {
20
+ const meter = metrics.getMeter(LIB_NAME);
21
+ httpActiveRequests = meter.createUpDownCounter("http.server.active_requests", {
22
+ description: "Number of active HTTP server requests",
23
+ unit: "{request}",
24
+ valueType: ValueType.INT
25
+ });
26
+ }
27
+ return httpActiveRequests;
28
+ }
29
+ function getFetchRequestDuration() {
30
+ if (!fetchRequestDuration) {
31
+ const meter = metrics.getMeter(LIB_NAME);
32
+ fetchRequestDuration = meter.createHistogram("http.client.request.duration", {
33
+ description: "Duration of HTTP client requests (fetch)",
34
+ unit: "s",
35
+ valueType: ValueType.DOUBLE
36
+ });
37
+ }
38
+ return fetchRequestDuration;
39
+ }
40
+ function recordHttpRequestStart(attributes) {
41
+ const startTime = performance.now();
42
+ getHttpActiveRequests().add(1, {
43
+ "http.request.method": attributes.method,
44
+ "http.route": attributes.route
45
+ });
46
+ return () => {
47
+ getHttpActiveRequests().add(-1, {
48
+ "http.request.method": attributes.method,
49
+ "http.route": attributes.route
50
+ });
51
+ };
52
+ }
53
+ function recordHttpRequestDuration(attributes, durationMs) {
54
+ getHttpRequestDuration().record(durationMs / 1e3, {
55
+ "http.request.method": attributes.method,
56
+ "http.route": attributes.route,
57
+ "http.response.status_code": attributes.status
58
+ });
59
+ }
60
+ function recordFetchRequestDuration(attributes, durationMs) {
61
+ getFetchRequestDuration().record(durationMs / 1e3, {
62
+ "http.request.method": attributes.method,
63
+ "server.address": attributes.host,
64
+ "http.response.status_code": attributes.status
65
+ });
66
+ }
67
+
68
+ export {
69
+ recordHttpRequestStart,
70
+ recordHttpRequestDuration,
71
+ recordFetchRequestDuration
72
+ };
@@ -0,0 +1,124 @@
1
+ // src/middleware.ts
2
+ import {
3
+ SpanKind,
4
+ SpanStatusCode,
5
+ context,
6
+ propagation,
7
+ trace
8
+ } from "@opentelemetry/api";
9
+ import { RPCType, setRPCMetadata } from "@opentelemetry/core";
10
+ var LIB_NAME = "@astroscope/opentelemetry";
11
+ var ACTIONS_PREFIX = "/_actions/";
12
+ function matchesPattern(path, pattern) {
13
+ if ("pattern" in pattern) {
14
+ return pattern.pattern.test(path);
15
+ }
16
+ if ("prefix" in pattern) {
17
+ return path.startsWith(pattern.prefix);
18
+ }
19
+ return path === pattern.exact;
20
+ }
21
+ function shouldExclude(ctx, exclude) {
22
+ if (!exclude) return false;
23
+ if (typeof exclude === "function") {
24
+ return exclude(ctx);
25
+ }
26
+ const path = ctx.url.pathname;
27
+ return exclude.some((pattern) => matchesPattern(path, pattern));
28
+ }
29
+ function getClientIp(request) {
30
+ return request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? request.headers.get("x-real-ip") ?? request.headers.get("cf-connecting-ip") ?? // Cloudflare
31
+ void 0;
32
+ }
33
+ function createOpenTelemetryMiddleware(options = {}) {
34
+ const tracer = trace.getTracer(LIB_NAME);
35
+ return async (ctx, next) => {
36
+ if (shouldExclude(ctx, options.exclude)) {
37
+ return next();
38
+ }
39
+ const { request, url } = ctx;
40
+ const input = {
41
+ traceparent: request.headers.get("traceparent"),
42
+ tracestate: request.headers.get("tracestate")
43
+ };
44
+ const parentContext = propagation.extract(context.active(), input);
45
+ const clientIp = getClientIp(request);
46
+ const contentLength = request.headers.get("content-length");
47
+ const spanOptions = {
48
+ attributes: {
49
+ "http.request.method": request.method,
50
+ "url.full": request.url,
51
+ "url.path": url.pathname,
52
+ "url.query": url.search.slice(1),
53
+ // Remove leading "?"
54
+ "url.scheme": url.protocol.replace(":", ""),
55
+ "server.address": url.hostname,
56
+ "server.port": url.port ? parseInt(url.port) : url.protocol === "https:" ? 443 : 80,
57
+ "user_agent.original": request.headers.get("user-agent") ?? "",
58
+ ...contentLength && { "http.request.body.size": parseInt(contentLength) },
59
+ ...clientIp && { "client.address": clientIp }
60
+ },
61
+ kind: SpanKind.SERVER
62
+ };
63
+ const isAction = url.pathname.startsWith(ACTIONS_PREFIX);
64
+ const actionName = url.pathname.slice(ACTIONS_PREFIX.length).replace(/\/$/, "");
65
+ const spanName = isAction ? `ACTION ${actionName}` : `${request.method} ${url.pathname}`;
66
+ const span = tracer.startSpan(spanName, spanOptions, parentContext);
67
+ const spanContext = trace.setSpan(parentContext, span);
68
+ const rpcMetadata = { type: RPCType.HTTP, span };
69
+ return context.with(
70
+ setRPCMetadata(spanContext, rpcMetadata),
71
+ async () => {
72
+ const finalize = (status, responseSize) => {
73
+ span.setAttribute("http.response.status_code", status);
74
+ span.setAttribute("http.response.body.size", responseSize);
75
+ if (status >= 400) {
76
+ span.setStatus({
77
+ code: SpanStatusCode.ERROR,
78
+ message: `HTTP ${status}`
79
+ });
80
+ } else {
81
+ span.setStatus({ code: SpanStatusCode.OK });
82
+ }
83
+ span.end();
84
+ };
85
+ try {
86
+ const response = await next();
87
+ if (!response.body) {
88
+ finalize(response.status, 0);
89
+ return response;
90
+ }
91
+ const [measureStream, clientStream] = response.body.tee();
92
+ let responseSize = 0;
93
+ (async () => {
94
+ const reader = measureStream.getReader();
95
+ try {
96
+ while (true) {
97
+ const { done, value } = await reader.read();
98
+ if (done) break;
99
+ responseSize += value.length;
100
+ }
101
+ } finally {
102
+ finalize(response.status, responseSize);
103
+ }
104
+ })();
105
+ return new Response(clientStream, {
106
+ status: response.status,
107
+ headers: response.headers
108
+ });
109
+ } catch (e) {
110
+ span.setStatus({
111
+ code: SpanStatusCode.ERROR,
112
+ message: e instanceof Error ? e.message : "Unknown error"
113
+ });
114
+ span.end();
115
+ throw e;
116
+ }
117
+ }
118
+ );
119
+ };
120
+ }
121
+
122
+ export {
123
+ createOpenTelemetryMiddleware
124
+ };
@@ -0,0 +1,155 @@
1
+ import {
2
+ recordActionDuration,
3
+ recordHttpRequestDuration,
4
+ recordHttpRequestStart
5
+ } from "./chunk-DPYEL3WF.js";
6
+
7
+ // src/middleware.ts
8
+ import {
9
+ SpanKind,
10
+ SpanStatusCode,
11
+ context,
12
+ propagation,
13
+ trace
14
+ } from "@opentelemetry/api";
15
+ import { RPCType, setRPCMetadata } from "@opentelemetry/core";
16
+ var LIB_NAME = "@astroscope/opentelemetry";
17
+ var ACTIONS_PREFIX = "/_actions/";
18
+ function matchesPattern(path, pattern) {
19
+ if ("pattern" in pattern) {
20
+ return pattern.pattern.test(path);
21
+ }
22
+ if ("prefix" in pattern) {
23
+ return path.startsWith(pattern.prefix);
24
+ }
25
+ return path === pattern.exact;
26
+ }
27
+ function shouldExclude(ctx, exclude) {
28
+ if (!exclude) return false;
29
+ if (typeof exclude === "function") {
30
+ return exclude(ctx);
31
+ }
32
+ const path = ctx.url.pathname;
33
+ return exclude.some((pattern) => matchesPattern(path, pattern));
34
+ }
35
+ function getClientIp(request) {
36
+ return request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? request.headers.get("x-real-ip") ?? request.headers.get("cf-connecting-ip") ?? // Cloudflare
37
+ void 0;
38
+ }
39
+ function createOpenTelemetryMiddleware(options = {}) {
40
+ const tracer = trace.getTracer(LIB_NAME);
41
+ return async (ctx, next) => {
42
+ if (shouldExclude(ctx, options.exclude)) {
43
+ return next();
44
+ }
45
+ const { request, url } = ctx;
46
+ const input = {
47
+ traceparent: request.headers.get("traceparent"),
48
+ tracestate: request.headers.get("tracestate")
49
+ };
50
+ const parentContext = propagation.extract(context.active(), input);
51
+ const clientIp = getClientIp(request);
52
+ const contentLength = request.headers.get("content-length");
53
+ const spanOptions = {
54
+ attributes: {
55
+ "http.request.method": request.method,
56
+ "url.full": request.url,
57
+ "url.path": url.pathname,
58
+ "url.query": url.search.slice(1),
59
+ // Remove leading "?"
60
+ "url.scheme": url.protocol.replace(":", ""),
61
+ "server.address": url.hostname,
62
+ "server.port": url.port ? parseInt(url.port) : url.protocol === "https:" ? 443 : 80,
63
+ "user_agent.original": request.headers.get("user-agent") ?? "",
64
+ ...contentLength && { "http.request.body.size": parseInt(contentLength) },
65
+ ...clientIp && { "client.address": clientIp }
66
+ },
67
+ kind: SpanKind.SERVER
68
+ };
69
+ const isAction = url.pathname.startsWith(ACTIONS_PREFIX);
70
+ const actionName = url.pathname.slice(ACTIONS_PREFIX.length).replace(/\/$/, "");
71
+ const spanName = isAction ? `ACTION ${actionName}` : `${request.method} ${url.pathname}`;
72
+ const span = tracer.startSpan(spanName, spanOptions, parentContext);
73
+ const spanContext = trace.setSpan(parentContext, span);
74
+ const rpcMetadata = { type: RPCType.HTTP, span };
75
+ const metricsEnabled = options.metrics ?? false;
76
+ const startTime = metricsEnabled ? performance.now() : 0;
77
+ const endActiveRequest = metricsEnabled ? recordHttpRequestStart({ method: request.method, route: url.pathname }) : void 0;
78
+ return context.with(
79
+ setRPCMetadata(spanContext, rpcMetadata),
80
+ async () => {
81
+ const finalize = (status, responseSize) => {
82
+ span.setAttribute("http.response.status_code", status);
83
+ span.setAttribute("http.response.body.size", responseSize);
84
+ if (status >= 400) {
85
+ span.setStatus({
86
+ code: SpanStatusCode.ERROR,
87
+ message: `HTTP ${status}`
88
+ });
89
+ } else {
90
+ span.setStatus({ code: SpanStatusCode.OK });
91
+ }
92
+ span.end();
93
+ if (metricsEnabled) {
94
+ endActiveRequest?.();
95
+ const duration = performance.now() - startTime;
96
+ recordHttpRequestDuration(
97
+ { method: request.method, route: url.pathname, status },
98
+ duration
99
+ );
100
+ if (isAction) {
101
+ recordActionDuration({ name: actionName, status }, duration);
102
+ }
103
+ }
104
+ };
105
+ try {
106
+ const response = await next();
107
+ if (!response.body) {
108
+ finalize(response.status, 0);
109
+ return response;
110
+ }
111
+ const [measureStream, clientStream] = response.body.tee();
112
+ let responseSize = 0;
113
+ (async () => {
114
+ const reader = measureStream.getReader();
115
+ try {
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done) break;
119
+ responseSize += value.length;
120
+ }
121
+ } finally {
122
+ finalize(response.status, responseSize);
123
+ }
124
+ })();
125
+ return new Response(clientStream, {
126
+ status: response.status,
127
+ headers: response.headers
128
+ });
129
+ } catch (e) {
130
+ span.setStatus({
131
+ code: SpanStatusCode.ERROR,
132
+ message: e instanceof Error ? e.message : "Unknown error"
133
+ });
134
+ span.end();
135
+ if (metricsEnabled) {
136
+ endActiveRequest?.();
137
+ const duration = performance.now() - startTime;
138
+ recordHttpRequestDuration(
139
+ { method: request.method, route: url.pathname, status: 500 },
140
+ duration
141
+ );
142
+ if (isAction) {
143
+ recordActionDuration({ name: actionName, status: 500 }, duration);
144
+ }
145
+ }
146
+ throw e;
147
+ }
148
+ }
149
+ );
150
+ };
151
+ }
152
+
153
+ export {
154
+ createOpenTelemetryMiddleware
155
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ instrumentFetch
3
+ } from "./chunk-QIWOBUML.js";
4
+ import "./chunk-UPNRPRAW.js";
5
+ export {
6
+ instrumentFetch
7
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ instrumentFetch
3
+ } from "./chunk-QTSNLOSC.js";
4
+ export {
5
+ instrumentFetch
6
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ instrumentFetch
3
+ } from "./chunk-FEC4ETRL.js";
4
+ export {
5
+ instrumentFetch
6
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ instrumentFetch
3
+ } from "./chunk-BQFWPPEO.js";
4
+ import "./chunk-DPYEL3WF.js";
5
+ export {
6
+ instrumentFetch
7
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ instrumentFetch
3
+ } from "./chunk-CEPTXEJV.js";
4
+ import "./chunk-DPYEL3WF.js";
5
+ export {
6
+ instrumentFetch
7
+ };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,6 @@
1
1
  import { APIContext, MiddlewareHandler, AstroIntegration } from 'astro';
2
+ import { ExcludePattern } from '@astroscope/excludes';
2
3
 
3
- type ExcludePattern = {
4
- pattern: RegExp;
5
- } | {
6
- prefix: string;
7
- } | {
8
- exact: string;
9
- };
10
4
  interface OpenTelemetryMiddlewareOptions {
11
5
  /**
12
6
  * Paths to exclude from tracing.
@@ -86,7 +80,7 @@ interface OpenTelemetryIntegrationOptions {
86
80
  * ```ts
87
81
  * // astro.config.ts
88
82
  * import { defineConfig } from "astro/config";
89
- * import { opentelemetry } from "@astroscope/opentelemetry";
83
+ * import opentelemetry from "@astroscope/opentelemetry";
90
84
  *
91
85
  * export default defineConfig({
92
86
  * integrations: [opentelemetry()],
@@ -121,32 +115,4 @@ interface OpenTelemetryIntegrationOptions {
121
115
  */
122
116
  declare function opentelemetry(options?: OpenTelemetryIntegrationOptions): AstroIntegration;
123
117
 
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 };
118
+ export { type OpenTelemetryIntegrationOptions, type OpenTelemetryMiddlewareOptions, createOpenTelemetryMiddleware, opentelemetry as default, instrumentFetch };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createOpenTelemetryMiddleware
3
- } from "./chunk-PCCKGEEG.js";
3
+ } from "./chunk-SNVIK2H6.js";
4
4
  import {
5
5
  instrumentFetch
6
6
  } from "./chunk-45TY3U7U.js";
@@ -9,33 +9,9 @@ import "./chunk-JU6FJKLT.js";
9
9
  // src/integration.ts
10
10
  import fs from "fs";
11
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 = [{ prefix: "/_astro/" }, { prefix: "/_image" }];
22
- var STATIC_EXCLUDES = [
23
- { exact: "/favicon.ico" },
24
- { exact: "/robots.txt" },
25
- { exact: "/sitemap.xml" },
26
- { exact: "/browserconfig.xml" },
27
- { exact: "/manifest.json" },
28
- { exact: "/manifest.webmanifest" },
29
- { prefix: "/.well-known/" }
30
- ];
31
- var RECOMMENDED_EXCLUDES = [...DEV_EXCLUDES, ...ASTRO_STATIC_EXCLUDES, ...STATIC_EXCLUDES];
32
-
33
- // src/integration.ts
12
+ import { RECOMMENDED_EXCLUDES, serializeExcludePatterns } from "@astroscope/excludes";
34
13
  var VIRTUAL_MODULE_ID = "virtual:@astroscope/opentelemetry/config";
35
14
  var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
36
- function serializeExcludePatterns(patterns) {
37
- return `[${patterns.map((p) => "pattern" in p ? `{ pattern: ${p.pattern.toString()} }` : JSON.stringify(p)).join(", ")}]`;
38
- }
39
15
  function opentelemetry(options = {}) {
40
16
  const httpConfig = options.instrumentations?.http ?? {
41
17
  enabled: true,
@@ -115,11 +91,7 @@ ${content}`
115
91
  };
116
92
  }
117
93
  export {
118
- ASTRO_STATIC_EXCLUDES,
119
- DEV_EXCLUDES,
120
- RECOMMENDED_EXCLUDES,
121
- STATIC_EXCLUDES,
122
94
  createOpenTelemetryMiddleware,
123
- instrumentFetch,
124
- opentelemetry
95
+ opentelemetry as default,
96
+ instrumentFetch
125
97
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createOpenTelemetryMiddleware
3
- } from "./chunk-PCCKGEEG.js";
3
+ } from "./chunk-SNVIK2H6.js";
4
4
  import "./chunk-JU6FJKLT.js";
5
5
 
6
6
  // src/middleware-entrypoint.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/opentelemetry",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "OpenTelemetry tracing middleware for Astro SSR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -61,6 +61,7 @@
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "peerDependencies": {
64
+ "@astroscope/excludes": "workspace:*",
64
65
  "@opentelemetry/api": "^1.0.0",
65
66
  "@opentelemetry/core": "^2.0.0",
66
67
  "astro": "^5.0.0"