@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,150 @@
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 startTime = performance.now();
76
+ const endActiveRequest = recordHttpRequestStart({ method: request.method, route: url.pathname });
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
+ endActiveRequest();
93
+ const duration = performance.now() - startTime;
94
+ recordHttpRequestDuration(
95
+ { method: request.method, route: url.pathname, status },
96
+ duration
97
+ );
98
+ if (isAction) {
99
+ recordActionDuration({ name: actionName, status }, duration);
100
+ }
101
+ };
102
+ try {
103
+ const response = await next();
104
+ if (!response.body) {
105
+ finalize(response.status, 0);
106
+ return response;
107
+ }
108
+ const [measureStream, clientStream] = response.body.tee();
109
+ let responseSize = 0;
110
+ (async () => {
111
+ const reader = measureStream.getReader();
112
+ try {
113
+ while (true) {
114
+ const { done, value } = await reader.read();
115
+ if (done) break;
116
+ responseSize += value.length;
117
+ }
118
+ } finally {
119
+ finalize(response.status, responseSize);
120
+ }
121
+ })();
122
+ return new Response(clientStream, {
123
+ status: response.status,
124
+ headers: response.headers
125
+ });
126
+ } catch (e) {
127
+ span.setStatus({
128
+ code: SpanStatusCode.ERROR,
129
+ message: e instanceof Error ? e.message : "Unknown error"
130
+ });
131
+ span.end();
132
+ endActiveRequest();
133
+ const duration = performance.now() - startTime;
134
+ recordHttpRequestDuration(
135
+ { method: request.method, route: url.pathname, status: 500 },
136
+ duration
137
+ );
138
+ if (isAction) {
139
+ recordActionDuration({ name: actionName, status: 500 }, duration);
140
+ }
141
+ throw e;
142
+ }
143
+ }
144
+ );
145
+ };
146
+ }
147
+
148
+ export {
149
+ createOpenTelemetryMiddleware
150
+ };
@@ -0,0 +1,83 @@
1
+ import {
2
+ recordFetchRequestDuration
3
+ } from "./chunk-DPYEL3WF.js";
4
+
5
+ // src/fetch.ts
6
+ import {
7
+ SpanKind,
8
+ SpanStatusCode,
9
+ context,
10
+ propagation,
11
+ trace
12
+ } from "@opentelemetry/api";
13
+ var LIB_NAME = "@astroscope/opentelemetry";
14
+ function instrumentFetch() {
15
+ const originalFetch = globalThis.fetch;
16
+ async function instrumentedFetch(input, init) {
17
+ const tracer = trace.getTracer(LIB_NAME);
18
+ const activeContext = context.active();
19
+ const request = new Request(input, init);
20
+ const url = new URL(request.url);
21
+ const span = tracer.startSpan(
22
+ `FETCH ${request.method}`,
23
+ {
24
+ kind: SpanKind.CLIENT,
25
+ attributes: {
26
+ "http.request.method": request.method,
27
+ "url.full": request.url,
28
+ "url.path": url.pathname,
29
+ "url.scheme": url.protocol.replace(":", ""),
30
+ "server.address": url.hostname,
31
+ "server.port": url.port ? parseInt(url.port) : url.protocol === "https:" ? 443 : 80
32
+ }
33
+ },
34
+ activeContext
35
+ );
36
+ const headers = new Headers(request.headers);
37
+ const carrier = {};
38
+ propagation.inject(trace.setSpan(activeContext, span), carrier);
39
+ for (const [key, value] of Object.entries(carrier)) {
40
+ headers.set(key, value);
41
+ }
42
+ const startTime = performance.now();
43
+ try {
44
+ const response = await originalFetch(request.url, {
45
+ ...init,
46
+ method: request.method,
47
+ headers,
48
+ body: request.body
49
+ });
50
+ span.setAttribute("http.response.status_code", response.status);
51
+ if (response.status >= 400) {
52
+ span.setStatus({
53
+ code: SpanStatusCode.ERROR,
54
+ message: `HTTP ${response.status}`
55
+ });
56
+ } else {
57
+ span.setStatus({ code: SpanStatusCode.OK });
58
+ }
59
+ span.end();
60
+ recordFetchRequestDuration(
61
+ { method: request.method, host: url.hostname, status: response.status },
62
+ performance.now() - startTime
63
+ );
64
+ return response;
65
+ } catch (error) {
66
+ span.setStatus({
67
+ code: SpanStatusCode.ERROR,
68
+ message: error instanceof Error ? error.message : "Unknown error"
69
+ });
70
+ span.end();
71
+ recordFetchRequestDuration(
72
+ { method: request.method, host: url.hostname, status: 0 },
73
+ performance.now() - startTime
74
+ );
75
+ throw error;
76
+ }
77
+ }
78
+ globalThis.fetch = Object.assign(instrumentedFetch, originalFetch);
79
+ }
80
+
81
+ export {
82
+ instrumentFetch
83
+ };
@@ -0,0 +1,89 @@
1
+ import {
2
+ recordFetchRequestDuration
3
+ } from "./chunk-DPYEL3WF.js";
4
+
5
+ // src/fetch.ts
6
+ import {
7
+ SpanKind,
8
+ SpanStatusCode,
9
+ context,
10
+ propagation,
11
+ trace
12
+ } from "@opentelemetry/api";
13
+ var LIB_NAME = "@astroscope/opentelemetry";
14
+ var metricsEnabled = false;
15
+ function instrumentFetch(options) {
16
+ metricsEnabled = options?.metrics ?? false;
17
+ const originalFetch = globalThis.fetch;
18
+ async function instrumentedFetch(input, init) {
19
+ const tracer = trace.getTracer(LIB_NAME);
20
+ const activeContext = context.active();
21
+ const request = new Request(input, init);
22
+ const url = new URL(request.url);
23
+ const span = tracer.startSpan(
24
+ `FETCH ${request.method}`,
25
+ {
26
+ kind: SpanKind.CLIENT,
27
+ attributes: {
28
+ "http.request.method": request.method,
29
+ "url.full": request.url,
30
+ "url.path": url.pathname,
31
+ "url.scheme": url.protocol.replace(":", ""),
32
+ "server.address": url.hostname,
33
+ "server.port": url.port ? parseInt(url.port) : url.protocol === "https:" ? 443 : 80
34
+ }
35
+ },
36
+ activeContext
37
+ );
38
+ const headers = new Headers(request.headers);
39
+ const carrier = {};
40
+ propagation.inject(trace.setSpan(activeContext, span), carrier);
41
+ for (const [key, value] of Object.entries(carrier)) {
42
+ headers.set(key, value);
43
+ }
44
+ const startTime = metricsEnabled ? performance.now() : 0;
45
+ try {
46
+ const response = await originalFetch(request.url, {
47
+ ...init,
48
+ method: request.method,
49
+ headers,
50
+ body: request.body
51
+ });
52
+ span.setAttribute("http.response.status_code", response.status);
53
+ if (response.status >= 400) {
54
+ span.setStatus({
55
+ code: SpanStatusCode.ERROR,
56
+ message: `HTTP ${response.status}`
57
+ });
58
+ } else {
59
+ span.setStatus({ code: SpanStatusCode.OK });
60
+ }
61
+ span.end();
62
+ if (metricsEnabled) {
63
+ recordFetchRequestDuration(
64
+ { method: request.method, host: url.hostname, status: response.status },
65
+ performance.now() - startTime
66
+ );
67
+ }
68
+ return response;
69
+ } catch (error) {
70
+ span.setStatus({
71
+ code: SpanStatusCode.ERROR,
72
+ message: error instanceof Error ? error.message : "Unknown error"
73
+ });
74
+ span.end();
75
+ if (metricsEnabled) {
76
+ recordFetchRequestDuration(
77
+ { method: request.method, host: url.hostname, status: 0 },
78
+ performance.now() - startTime
79
+ );
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ globalThis.fetch = Object.assign(instrumentedFetch, originalFetch);
85
+ }
86
+
87
+ export {
88
+ instrumentFetch
89
+ };
@@ -0,0 +1,91 @@
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
+ var actionDuration = null;
8
+ function getHttpRequestDuration() {
9
+ if (!httpRequestDuration) {
10
+ const meter = metrics.getMeter(LIB_NAME);
11
+ httpRequestDuration = meter.createHistogram("http.server.request.duration", {
12
+ description: "Duration of HTTP server requests",
13
+ unit: "s",
14
+ valueType: ValueType.DOUBLE
15
+ });
16
+ }
17
+ return httpRequestDuration;
18
+ }
19
+ function getHttpActiveRequests() {
20
+ if (!httpActiveRequests) {
21
+ const meter = metrics.getMeter(LIB_NAME);
22
+ httpActiveRequests = meter.createUpDownCounter("http.server.active_requests", {
23
+ description: "Number of active HTTP server requests",
24
+ unit: "{request}",
25
+ valueType: ValueType.INT
26
+ });
27
+ }
28
+ return httpActiveRequests;
29
+ }
30
+ function getFetchRequestDuration() {
31
+ if (!fetchRequestDuration) {
32
+ const meter = metrics.getMeter(LIB_NAME);
33
+ fetchRequestDuration = meter.createHistogram("http.client.request.duration", {
34
+ description: "Duration of HTTP client requests (fetch)",
35
+ unit: "s",
36
+ valueType: ValueType.DOUBLE
37
+ });
38
+ }
39
+ return fetchRequestDuration;
40
+ }
41
+ function getActionDuration() {
42
+ if (!actionDuration) {
43
+ const meter = metrics.getMeter(LIB_NAME);
44
+ actionDuration = meter.createHistogram("astro.action.duration", {
45
+ description: "Duration of Astro action executions",
46
+ unit: "s",
47
+ valueType: ValueType.DOUBLE
48
+ });
49
+ }
50
+ return actionDuration;
51
+ }
52
+ function recordHttpRequestStart(attributes) {
53
+ const startTime = performance.now();
54
+ getHttpActiveRequests().add(1, {
55
+ "http.request.method": attributes.method,
56
+ "http.route": attributes.route
57
+ });
58
+ return () => {
59
+ getHttpActiveRequests().add(-1, {
60
+ "http.request.method": attributes.method,
61
+ "http.route": attributes.route
62
+ });
63
+ };
64
+ }
65
+ function recordHttpRequestDuration(attributes, durationMs) {
66
+ getHttpRequestDuration().record(durationMs / 1e3, {
67
+ "http.request.method": attributes.method,
68
+ "http.route": attributes.route,
69
+ "http.response.status_code": attributes.status
70
+ });
71
+ }
72
+ function recordFetchRequestDuration(attributes, durationMs) {
73
+ getFetchRequestDuration().record(durationMs / 1e3, {
74
+ "http.request.method": attributes.method,
75
+ "server.address": attributes.host,
76
+ "http.response.status_code": attributes.status
77
+ });
78
+ }
79
+ function recordActionDuration(attributes, durationMs) {
80
+ getActionDuration().record(durationMs / 1e3, {
81
+ "astro.action.name": attributes.name,
82
+ "http.response.status_code": attributes.status
83
+ });
84
+ }
85
+
86
+ export {
87
+ recordHttpRequestStart,
88
+ recordHttpRequestDuration,
89
+ recordFetchRequestDuration,
90
+ recordActionDuration
91
+ };
@@ -0,0 +1,70 @@
1
+ // src/fetch.ts
2
+ import {
3
+ SpanKind,
4
+ SpanStatusCode,
5
+ context,
6
+ propagation,
7
+ trace
8
+ } from "@opentelemetry/api";
9
+ var LIB_NAME = "@astroscope/opentelemetry";
10
+ function instrumentFetch() {
11
+ const originalFetch = globalThis.fetch;
12
+ async function instrumentedFetch(input, init) {
13
+ const tracer = trace.getTracer(LIB_NAME);
14
+ const activeContext = context.active();
15
+ const request = new Request(input, init);
16
+ const url = new URL(request.url);
17
+ const span = tracer.startSpan(
18
+ `FETCH ${request.method}`,
19
+ {
20
+ kind: SpanKind.CLIENT,
21
+ attributes: {
22
+ "http.request.method": request.method,
23
+ "url.full": request.url,
24
+ "url.path": url.pathname,
25
+ "url.scheme": url.protocol.replace(":", ""),
26
+ "server.address": url.hostname,
27
+ "server.port": url.port ? parseInt(url.port) : url.protocol === "https:" ? 443 : 80
28
+ }
29
+ },
30
+ activeContext
31
+ );
32
+ const headers = new Headers(request.headers);
33
+ const carrier = {};
34
+ propagation.inject(trace.setSpan(activeContext, span), carrier);
35
+ for (const [key, value] of Object.entries(carrier)) {
36
+ headers.set(key, value);
37
+ }
38
+ try {
39
+ const response = await originalFetch(request.url, {
40
+ ...init,
41
+ method: request.method,
42
+ headers,
43
+ body: request.body
44
+ });
45
+ span.setAttribute("http.response.status_code", response.status);
46
+ if (response.status >= 400) {
47
+ span.setStatus({
48
+ code: SpanStatusCode.ERROR,
49
+ message: `HTTP ${response.status}`
50
+ });
51
+ } else {
52
+ span.setStatus({ code: SpanStatusCode.OK });
53
+ }
54
+ span.end();
55
+ return response;
56
+ } catch (error) {
57
+ span.setStatus({
58
+ code: SpanStatusCode.ERROR,
59
+ message: error instanceof Error ? error.message : "Unknown error"
60
+ });
61
+ span.end();
62
+ throw error;
63
+ }
64
+ }
65
+ globalThis.fetch = Object.assign(instrumentedFetch, originalFetch);
66
+ }
67
+
68
+ export {
69
+ instrumentFetch
70
+ };
@@ -0,0 +1,123 @@
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 spanName = isAction ? `ACTION ${url.pathname.slice(ACTIONS_PREFIX.length)}` : `${request.method} ${url.pathname}`;
65
+ const span = tracer.startSpan(spanName, spanOptions, parentContext);
66
+ const spanContext = trace.setSpan(parentContext, span);
67
+ const rpcMetadata = { type: RPCType.HTTP, span };
68
+ return context.with(
69
+ setRPCMetadata(spanContext, rpcMetadata),
70
+ async () => {
71
+ const finalize = (status, responseSize) => {
72
+ span.setAttribute("http.response.status_code", status);
73
+ span.setAttribute("http.response.body.size", responseSize);
74
+ if (status >= 400) {
75
+ span.setStatus({
76
+ code: SpanStatusCode.ERROR,
77
+ message: `HTTP ${status}`
78
+ });
79
+ } else {
80
+ span.setStatus({ code: SpanStatusCode.OK });
81
+ }
82
+ span.end();
83
+ };
84
+ try {
85
+ const response = await next();
86
+ if (!response.body) {
87
+ finalize(response.status, 0);
88
+ return response;
89
+ }
90
+ const [measureStream, clientStream] = response.body.tee();
91
+ let responseSize = 0;
92
+ (async () => {
93
+ const reader = measureStream.getReader();
94
+ try {
95
+ while (true) {
96
+ const { done, value } = await reader.read();
97
+ if (done) break;
98
+ responseSize += value.length;
99
+ }
100
+ } finally {
101
+ finalize(response.status, responseSize);
102
+ }
103
+ })();
104
+ return new Response(clientStream, {
105
+ status: response.status,
106
+ headers: response.headers
107
+ });
108
+ } catch (e) {
109
+ span.setStatus({
110
+ code: SpanStatusCode.ERROR,
111
+ message: e instanceof Error ? e.message : "Unknown error"
112
+ });
113
+ span.end();
114
+ throw e;
115
+ }
116
+ }
117
+ );
118
+ };
119
+ }
120
+
121
+ export {
122
+ createOpenTelemetryMiddleware
123
+ };