@geekmidas/telescope 10.0.0-alpha.81 → 10.0.0-alpha.83
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/dist/instrumentation/hono.cjs +25 -2
- package/dist/instrumentation/hono.cjs.map +1 -1
- package/dist/instrumentation/hono.d.cts +19 -0
- package/dist/instrumentation/hono.d.cts.map +1 -1
- package/dist/instrumentation/hono.d.mts +19 -0
- package/dist/instrumentation/hono.d.mts.map +1 -1
- package/dist/instrumentation/hono.mjs +26 -3
- package/dist/instrumentation/hono.mjs.map +1 -1
- package/dist/instrumentation/http.cjs +27 -2
- package/dist/instrumentation/http.cjs.map +1 -1
- package/dist/instrumentation/http.d.cts +18 -2
- package/dist/instrumentation/http.d.cts.map +1 -1
- package/dist/instrumentation/http.d.mts +18 -2
- package/dist/instrumentation/http.d.mts.map +1 -1
- package/dist/instrumentation/http.mjs +28 -4
- package/dist/instrumentation/http.mjs.map +1 -1
- package/dist/instrumentation/index.cjs +8 -0
- package/dist/instrumentation/index.d.cts +4 -2
- package/dist/instrumentation/index.d.mts +4 -2
- package/dist/instrumentation/index.mjs +4 -2
- package/dist/instrumentation/middleware.cjs +4 -2
- package/dist/instrumentation/middleware.cjs.map +1 -1
- package/dist/instrumentation/middleware.d.cts +13 -0
- package/dist/instrumentation/middleware.d.cts.map +1 -1
- package/dist/instrumentation/middleware.d.mts +13 -0
- package/dist/instrumentation/middleware.d.mts.map +1 -1
- package/dist/instrumentation/middleware.mjs +5 -3
- package/dist/instrumentation/middleware.mjs.map +1 -1
- package/dist/instrumentation/sampler.cjs +54 -0
- package/dist/instrumentation/sampler.cjs.map +1 -0
- package/dist/instrumentation/sampler.d.cts +38 -0
- package/dist/instrumentation/sampler.d.cts.map +1 -0
- package/dist/instrumentation/sampler.d.mts +38 -0
- package/dist/instrumentation/sampler.d.mts.map +1 -0
- package/dist/instrumentation/sampler.mjs +52 -0
- package/dist/instrumentation/sampler.mjs.map +1 -0
- package/dist/instrumentation/setup.cjs +6 -4
- package/dist/instrumentation/setup.cjs.map +1 -1
- package/dist/instrumentation/setup.d.cts +5 -3
- package/dist/instrumentation/setup.d.cts.map +1 -1
- package/dist/instrumentation/setup.d.mts +5 -3
- package/dist/instrumentation/setup.d.mts.map +1 -1
- package/dist/instrumentation/setup.mjs +7 -5
- package/dist/instrumentation/setup.mjs.map +1 -1
- package/dist/instrumentation/trust.cjs +101 -0
- package/dist/instrumentation/trust.cjs.map +1 -0
- package/dist/instrumentation/trust.d.cts +57 -0
- package/dist/instrumentation/trust.d.cts.map +1 -0
- package/dist/instrumentation/trust.d.mts +57 -0
- package/dist/instrumentation/trust.d.mts.map +1 -0
- package/dist/instrumentation/trust.mjs +98 -0
- package/dist/instrumentation/trust.mjs.map +1 -0
- package/package.json +4 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_instrumentation_http = require("./http.cjs");
|
|
3
|
+
const require_instrumentation_trust = require("./trust.cjs");
|
|
3
4
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
4
5
|
let hono_route = require("hono/route");
|
|
5
6
|
//#region src/instrumentation/hono.ts
|
|
@@ -36,9 +37,9 @@ function honoTelemetryMiddleware(options = {}) {
|
|
|
36
37
|
c.req.raw.headers.forEach((value, key) => {
|
|
37
38
|
headers[key] = value;
|
|
38
39
|
});
|
|
39
|
-
const parentContext = require_instrumentation_http.
|
|
40
|
+
const { parent: parentContext, links } = require_instrumentation_http.incomingTraceContext(headers, isTrustedCaller(c, options));
|
|
40
41
|
const attrs = buildHonoSpanAttributes(c, options);
|
|
41
|
-
const span = require_instrumentation_http.createHttpServerSpan(attrs, parentContext);
|
|
42
|
+
const span = require_instrumentation_http.createHttpServerSpan(attrs, parentContext, links);
|
|
42
43
|
const spanContext = _opentelemetry_api.trace.setSpan(parentContext, span);
|
|
43
44
|
c.set(SPAN_KEY, span);
|
|
44
45
|
c.set(CONTEXT_KEY, spanContext);
|
|
@@ -65,6 +66,28 @@ function honoTelemetryMiddleware(options = {}) {
|
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
69
|
+
* Whether this request's `traceparent` is continued: from one of the API's
|
|
70
|
+
* own origins, or from an internal caller.
|
|
71
|
+
*/
|
|
72
|
+
function isTrustedCaller(c, options) {
|
|
73
|
+
const origin = c.req.header("origin");
|
|
74
|
+
if (origin) {
|
|
75
|
+
const trusted = typeof options.trustedOrigins === "function" ? options.trustedOrigins() : options.trustedOrigins;
|
|
76
|
+
return require_instrumentation_trust.isTrustedOrigin(origin, trusted);
|
|
77
|
+
}
|
|
78
|
+
const internal = options.internalCallers ?? true;
|
|
79
|
+
if (typeof internal === "function") return internal(c);
|
|
80
|
+
if (!internal) return false;
|
|
81
|
+
return require_instrumentation_trust.isInternalCaller({
|
|
82
|
+
headers: c.req.raw.headers,
|
|
83
|
+
remoteAddress: remoteAddressOf(c)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** The TCP peer's address under `@hono/node-server`, or undefined. */
|
|
87
|
+
function remoteAddressOf(c) {
|
|
88
|
+
return c.env?.incoming?.socket?.remoteAddress;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
68
91
|
* The route the request matched, once the handler has run — `/users/:id`,
|
|
69
92
|
* never `/users/42` — or undefined when only middleware ran (a 404).
|
|
70
93
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.cjs","names":["extractTraceContext","createHttpServerSpan","trace","context","routePath"],"sources":["../../src/instrumentation/hono.ts"],"sourcesContent":["import { context, type Span, trace } from '@opentelemetry/api';\n/**\n * OpenTelemetry instrumentation middleware for Hono\n *\n * This middleware automatically creates spans for HTTP requests,\n * extracts trace context from incoming headers, and records\n * request/response metadata as span attributes.\n */\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { routePath } from 'hono/route';\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\textractTraceContext,\n\ttype HttpSpanAttributes,\n} from './http';\n\n/**\n * Options for the Hono telemetry middleware\n */\nexport interface HonoTelemetryMiddlewareOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract user ID from context\n\t */\n\tgetUserId?: (c: Context) => string | undefined;\n\n\t/**\n\t * Paths to skip tracing (supports wildcards)\n\t */\n\tignorePaths?: string[];\n\n\t/**\n\t * Whether to skip tracing for this request\n\t */\n\tshouldSkip?: (c: Context) => boolean;\n}\n\n// Key for storing span on context\nconst SPAN_KEY = 'telemetry.span';\nconst CONTEXT_KEY = 'telemetry.context';\n\n/**\n * Get the current span from Hono context\n */\nexport function getSpanFromContext(c: Context): Span | undefined {\n\treturn c.get(SPAN_KEY);\n}\n\n/**\n * Get the current trace context from Hono context\n */\nexport function getTraceContextFromHono(c: Context): any {\n\treturn c.get(CONTEXT_KEY);\n}\n\n/**\n * Create a telemetry middleware for Hono applications\n *\n * @example\n * ```typescript\n * import { honoTelemetryMiddleware } from '@geekmidas/telescope/instrumentation';\n *\n * const app = new Hono();\n * app.use('*', honoTelemetryMiddleware());\n * ```\n */\nexport function honoTelemetryMiddleware(\n\toptions: HonoTelemetryMiddlewareOptions = {},\n): MiddlewareHandler {\n\treturn async (c, next) => {\n\t\t// Check if path should be ignored\n\t\tif (\n\t\t\toptions.ignorePaths &&\n\t\t\tshouldIgnorePath(c.req.path, options.ignorePaths)\n\t\t) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Skip if custom skip function returns true\n\t\tif (options.shouldSkip?.(c)) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Extract trace context from headers\n\t\tconst headers: Record<string, string | string[] | undefined> = {};\n\t\tc.req.raw.headers.forEach((value, key) => {\n\t\t\theaders[key] = value;\n\t\t});\n\t\tconst parentContext = extractTraceContext(headers);\n\n\t\t// Build span attributes from request\n\t\tconst attrs = buildHonoSpanAttributes(c, options);\n\n\t\t// Create the span\n\t\tconst span = createHttpServerSpan(attrs, parentContext);\n\t\tconst spanContext = trace.setSpan(parentContext, span);\n\n\t\t// Store span and context on Hono context\n\t\tc.set(SPAN_KEY, span);\n\t\tc.set(CONTEXT_KEY, spanContext);\n\n\t\t// Record body if requested\n\t\tif (options.recordBody) {\n\t\t\ttry {\n\t\t\t\tconst body = await c.req.text();\n\t\t\t\tif (body) {\n\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t'http.request.body',\n\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Body may have already been consumed\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\t// Execute the rest of the middleware chain within span context, so\n\t\t\t// the handler's logs and outbound spans nest under this one.\n\t\t\tawait context.with(spanContext, async () => {\n\t\t\t\tawait next();\n\t\t\t});\n\n\t\t\tnameAfterRoute(c, span);\n\n\t\t\t// Record response\n\t\t\tconst statusCode = c.res.status;\n\n\t\t\tif (options.recordResponseBody) {\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await c.res.clone().text();\n\t\t\t\t\tif (body) {\n\t\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t\t'http.response.body',\n\t\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Response body may not be available\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Hono hands a thrown error to `onError` and sets `c.error`\n\t\t\t// rather than rethrowing, so that is where it is found.\n\t\t\tendHttpSpan(span, { statusCode }, c.error);\n\t\t} catch (error) {\n\t\t\tnameAfterRoute(c, span);\n\t\t\tendHttpSpan(\n\t\t\t\tspan,\n\t\t\t\t{ statusCode: 500 },\n\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t);\n\t\t\tthrow error;\n\t\t}\n\t};\n}\n\n/**\n * The route the request matched, once the handler has run — `/users/:id`,\n * never `/users/42` — or undefined when only middleware ran (a 404).\n *\n * Read after `next()`: before it, the route is this middleware's own `*`.\n */\nfunction matchedRoute(c: Context): string | undefined {\n\tconst path = routePath(c);\n\treturn path && !path.includes('*') ? path : undefined;\n}\n\n/** `GET /users/:id`, or the bare method when no route matched. */\nfunction nameAfterRoute(c: Context, span: Span): void {\n\tconst route = matchedRoute(c);\n\tif (route) {\n\t\tspan.setAttribute('http.route', route);\n\t\tspan.updateName(`${c.req.method} ${route}`);\n\t} else {\n\t\tspan.updateName(c.req.method);\n\t}\n}\n\n/**\n * Build span attributes from Hono context\n */\nfunction buildHonoSpanAttributes(\n\tc: Context,\n\toptions: HonoTelemetryMiddlewareOptions,\n): HttpSpanAttributes {\n\tconst req = c.req;\n\tconst url = new URL(req.url);\n\n\t// No `url.full`: a query string can carry a token, and the span would\n\t// keep it. The route is named once the handler has run.\n\tconst attrs: HttpSpanAttributes = {\n\t\tmethod: req.method,\n\t\tpath: url.pathname,\n\t\thost: url.host,\n\t\tscheme: url.protocol.replace(':', '') as 'http' | 'https',\n\t\tuserAgent: req.header('user-agent'),\n\t\tclientIp: getClientIp(c),\n\t\trequestId: req.header('x-request-id') || req.header('x-amzn-requestid'),\n\t};\n\n\t// Add user metadata if extractor provided\n\tif (options.getUserId) {\n\t\tconst userId = options.getUserId(c);\n\t\tif (userId) {\n\t\t\tattrs.user = { userId };\n\t\t}\n\t}\n\n\treturn attrs;\n}\n\n/**\n * Extract client IP from Hono context\n */\nfunction getClientIp(c: Context): string | undefined {\n\t// Try various headers in order of preference\n\tconst xForwardedFor = c.req.header('x-forwarded-for');\n\tif (xForwardedFor) {\n\t\tconst first = xForwardedFor.split(',')[0];\n\t\treturn first?.trim();\n\t}\n\n\tconst xRealIp = c.req.header('x-real-ip');\n\tif (xRealIp) {\n\t\treturn xRealIp;\n\t}\n\n\t// Try CF-Connecting-IP (Cloudflare)\n\tconst cfConnectingIp = c.req.header('cf-connecting-ip');\n\tif (cfConnectingIp) {\n\t\treturn cfConnectingIp;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Check if path should be ignored\n */\nfunction shouldIgnorePath(path: string, ignorePaths: string[]): boolean {\n\tfor (const pattern of ignorePaths) {\n\t\tif (pattern.endsWith('*')) {\n\t\t\t// Wildcard match\n\t\t\tconst prefix = pattern.slice(0, -1);\n\t\t\tif (path.startsWith(prefix)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (path === pattern) {\n\t\t\t// Exact match\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Run a function within the span context from Hono context\n *\n * @example\n * ```typescript\n * app.get('/api/users', async (c) => {\n * return withHonoSpanContext(c, async () => {\n * // Any spans created here will be children of the request span\n * const users = await fetchUsers();\n * return c.json(users);\n * });\n * });\n * ```\n */\nexport async function withHonoSpanContext<T>(\n\tc: Context,\n\tfn: () => Promise<T>,\n): Promise<T> {\n\tconst ctx = getTraceContextFromHono(c);\n\tif (!ctx) {\n\t\treturn fn();\n\t}\n\treturn context.with(ctx, fn);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hono.cjs","names":["incomingTraceContext","createHttpServerSpan","trace","context","isTrustedOrigin","isInternalCaller","routePath"],"sources":["../../src/instrumentation/hono.ts"],"sourcesContent":["import { context, type Span, trace } from '@opentelemetry/api';\n/**\n * OpenTelemetry instrumentation middleware for Hono\n *\n * This middleware automatically creates spans for HTTP requests,\n * extracts trace context from incoming headers, and records\n * request/response metadata as span attributes.\n */\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { routePath } from 'hono/route';\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\ttype HttpSpanAttributes,\n\tincomingTraceContext,\n} from './http';\nimport { isInternalCaller, isTrustedOrigin } from './trust';\n\n/**\n * Options for the Hono telemetry middleware\n */\nexport interface HonoTelemetryMiddlewareOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract user ID from context\n\t */\n\tgetUserId?: (c: Context) => string | undefined;\n\n\t/**\n\t * Paths to skip tracing (supports wildcards)\n\t */\n\tignorePaths?: string[];\n\n\t/**\n\t * Whether to skip tracing for this request\n\t */\n\tshouldSkip?: (c: Context) => boolean;\n\n\t/**\n\t * The origins whose `traceparent` is continued: the API's own sites, the\n\t * same list its CORS allows. A request from any other origin starts a new\n\t * trace linked to the one it claimed.\n\t *\n\t * A function is read on each request, for a server that learns its origins\n\t * only once the app has loaded its configuration.\n\t */\n\ttrustedOrigins?: readonly string[] | (() => readonly string[] | undefined);\n\n\t/**\n\t * Whether a request with no `Origin` from an internal caller continues its\n\t * `traceparent` — another service on the private network (see `./trust`):\n\t * no `Origin`, no proxy's forwarding header, and a loopback or private\n\t * peer address, read from `@hono/node-server`'s socket.\n\t *\n\t * `false` trusts no caller without an origin; a function decides itself.\n\t * @default true\n\t */\n\tinternalCallers?: boolean | ((c: Context) => boolean);\n}\n\n// Key for storing span on context\nconst SPAN_KEY = 'telemetry.span';\nconst CONTEXT_KEY = 'telemetry.context';\n\n/**\n * Get the current span from Hono context\n */\nexport function getSpanFromContext(c: Context): Span | undefined {\n\treturn c.get(SPAN_KEY);\n}\n\n/**\n * Get the current trace context from Hono context\n */\nexport function getTraceContextFromHono(c: Context): any {\n\treturn c.get(CONTEXT_KEY);\n}\n\n/**\n * Create a telemetry middleware for Hono applications\n *\n * @example\n * ```typescript\n * import { honoTelemetryMiddleware } from '@geekmidas/telescope/instrumentation';\n *\n * const app = new Hono();\n * app.use('*', honoTelemetryMiddleware());\n * ```\n */\nexport function honoTelemetryMiddleware(\n\toptions: HonoTelemetryMiddlewareOptions = {},\n): MiddlewareHandler {\n\treturn async (c, next) => {\n\t\t// Check if path should be ignored\n\t\tif (\n\t\t\toptions.ignorePaths &&\n\t\t\tshouldIgnorePath(c.req.path, options.ignorePaths)\n\t\t) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Skip if custom skip function returns true\n\t\tif (options.shouldSkip?.(c)) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Extract trace context from headers\n\t\tconst headers: Record<string, string | string[] | undefined> = {};\n\t\tc.req.raw.headers.forEach((value, key) => {\n\t\t\theaders[key] = value;\n\t\t});\n\t\t// Continued only from a trusted caller; anyone else gets a new trace\n\t\t// that links to the one they claimed.\n\t\tconst { parent: parentContext, links } = incomingTraceContext(\n\t\t\theaders,\n\t\t\tisTrustedCaller(c, options),\n\t\t);\n\n\t\t// Build span attributes from request\n\t\tconst attrs = buildHonoSpanAttributes(c, options);\n\n\t\t// Create the span\n\t\tconst span = createHttpServerSpan(attrs, parentContext, links);\n\t\tconst spanContext = trace.setSpan(parentContext, span);\n\n\t\t// Store span and context on Hono context\n\t\tc.set(SPAN_KEY, span);\n\t\tc.set(CONTEXT_KEY, spanContext);\n\n\t\t// Record body if requested\n\t\tif (options.recordBody) {\n\t\t\ttry {\n\t\t\t\tconst body = await c.req.text();\n\t\t\t\tif (body) {\n\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t'http.request.body',\n\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Body may have already been consumed\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\t// Execute the rest of the middleware chain within span context, so\n\t\t\t// the handler's logs and outbound spans nest under this one.\n\t\t\tawait context.with(spanContext, async () => {\n\t\t\t\tawait next();\n\t\t\t});\n\n\t\t\tnameAfterRoute(c, span);\n\n\t\t\t// Record response\n\t\t\tconst statusCode = c.res.status;\n\n\t\t\tif (options.recordResponseBody) {\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await c.res.clone().text();\n\t\t\t\t\tif (body) {\n\t\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t\t'http.response.body',\n\t\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Response body may not be available\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Hono hands a thrown error to `onError` and sets `c.error`\n\t\t\t// rather than rethrowing, so that is where it is found.\n\t\t\tendHttpSpan(span, { statusCode }, c.error);\n\t\t} catch (error) {\n\t\t\tnameAfterRoute(c, span);\n\t\t\tendHttpSpan(\n\t\t\t\tspan,\n\t\t\t\t{ statusCode: 500 },\n\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t);\n\t\t\tthrow error;\n\t\t}\n\t};\n}\n\n/**\n * Whether this request's `traceparent` is continued: from one of the API's\n * own origins, or from an internal caller.\n */\nfunction isTrustedCaller(\n\tc: Context,\n\toptions: HonoTelemetryMiddlewareOptions,\n): boolean {\n\tconst origin = c.req.header('origin');\n\tif (origin) {\n\t\tconst trusted =\n\t\t\ttypeof options.trustedOrigins === 'function'\n\t\t\t\t? options.trustedOrigins()\n\t\t\t\t: options.trustedOrigins;\n\t\treturn isTrustedOrigin(origin, trusted);\n\t}\n\n\tconst internal = options.internalCallers ?? true;\n\tif (typeof internal === 'function') return internal(c);\n\tif (!internal) return false;\n\treturn isInternalCaller({\n\t\theaders: c.req.raw.headers,\n\t\tremoteAddress: remoteAddressOf(c),\n\t});\n}\n\n/** The TCP peer's address under `@hono/node-server`, or undefined. */\nfunction remoteAddressOf(c: Context): string | undefined {\n\tconst env = c.env as\n\t\t| { incoming?: { socket?: { remoteAddress?: string } } }\n\t\t| undefined;\n\treturn env?.incoming?.socket?.remoteAddress;\n}\n\n/**\n * The route the request matched, once the handler has run — `/users/:id`,\n * never `/users/42` — or undefined when only middleware ran (a 404).\n *\n * Read after `next()`: before it, the route is this middleware's own `*`.\n */\nfunction matchedRoute(c: Context): string | undefined {\n\tconst path = routePath(c);\n\treturn path && !path.includes('*') ? path : undefined;\n}\n\n/** `GET /users/:id`, or the bare method when no route matched. */\nfunction nameAfterRoute(c: Context, span: Span): void {\n\tconst route = matchedRoute(c);\n\tif (route) {\n\t\tspan.setAttribute('http.route', route);\n\t\tspan.updateName(`${c.req.method} ${route}`);\n\t} else {\n\t\tspan.updateName(c.req.method);\n\t}\n}\n\n/**\n * Build span attributes from Hono context\n */\nfunction buildHonoSpanAttributes(\n\tc: Context,\n\toptions: HonoTelemetryMiddlewareOptions,\n): HttpSpanAttributes {\n\tconst req = c.req;\n\tconst url = new URL(req.url);\n\n\t// No `url.full`: a query string can carry a token, and the span would\n\t// keep it. The route is named once the handler has run.\n\tconst attrs: HttpSpanAttributes = {\n\t\tmethod: req.method,\n\t\tpath: url.pathname,\n\t\thost: url.host,\n\t\tscheme: url.protocol.replace(':', '') as 'http' | 'https',\n\t\tuserAgent: req.header('user-agent'),\n\t\tclientIp: getClientIp(c),\n\t\trequestId: req.header('x-request-id') || req.header('x-amzn-requestid'),\n\t};\n\n\t// Add user metadata if extractor provided\n\tif (options.getUserId) {\n\t\tconst userId = options.getUserId(c);\n\t\tif (userId) {\n\t\t\tattrs.user = { userId };\n\t\t}\n\t}\n\n\treturn attrs;\n}\n\n/**\n * Extract client IP from Hono context\n */\nfunction getClientIp(c: Context): string | undefined {\n\t// Try various headers in order of preference\n\tconst xForwardedFor = c.req.header('x-forwarded-for');\n\tif (xForwardedFor) {\n\t\tconst first = xForwardedFor.split(',')[0];\n\t\treturn first?.trim();\n\t}\n\n\tconst xRealIp = c.req.header('x-real-ip');\n\tif (xRealIp) {\n\t\treturn xRealIp;\n\t}\n\n\t// Try CF-Connecting-IP (Cloudflare)\n\tconst cfConnectingIp = c.req.header('cf-connecting-ip');\n\tif (cfConnectingIp) {\n\t\treturn cfConnectingIp;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Check if path should be ignored\n */\nfunction shouldIgnorePath(path: string, ignorePaths: string[]): boolean {\n\tfor (const pattern of ignorePaths) {\n\t\tif (pattern.endsWith('*')) {\n\t\t\t// Wildcard match\n\t\t\tconst prefix = pattern.slice(0, -1);\n\t\t\tif (path.startsWith(prefix)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (path === pattern) {\n\t\t\t// Exact match\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Run a function within the span context from Hono context\n *\n * @example\n * ```typescript\n * app.get('/api/users', async (c) => {\n * return withHonoSpanContext(c, async () => {\n * // Any spans created here will be children of the request span\n * const users = await fetchUsers();\n * return c.json(users);\n * });\n * });\n * ```\n */\nexport async function withHonoSpanContext<T>(\n\tc: Context,\n\tfn: () => Promise<T>,\n): Promise<T> {\n\tconst ctx = getTraceContextFromHono(c);\n\tif (!ctx) {\n\t\treturn fn();\n\t}\n\treturn context.with(ctx, fn);\n}\n"],"mappings":"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"}
|
|
@@ -27,6 +27,25 @@ export interface HonoTelemetryMiddlewareOptions {
|
|
|
27
27
|
* Whether to skip tracing for this request
|
|
28
28
|
*/
|
|
29
29
|
shouldSkip?: (c: Context) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The origins whose `traceparent` is continued: the API's own sites, the
|
|
32
|
+
* same list its CORS allows. A request from any other origin starts a new
|
|
33
|
+
* trace linked to the one it claimed.
|
|
34
|
+
*
|
|
35
|
+
* A function is read on each request, for a server that learns its origins
|
|
36
|
+
* only once the app has loaded its configuration.
|
|
37
|
+
*/
|
|
38
|
+
trustedOrigins?: readonly string[] | (() => readonly string[] | undefined);
|
|
39
|
+
/**
|
|
40
|
+
* Whether a request with no `Origin` from an internal caller continues its
|
|
41
|
+
* `traceparent` — another service on the private network (see `./trust`):
|
|
42
|
+
* no `Origin`, no proxy's forwarding header, and a loopback or private
|
|
43
|
+
* peer address, read from `@hono/node-server`'s socket.
|
|
44
|
+
*
|
|
45
|
+
* `false` trusts no caller without an origin; a function decides itself.
|
|
46
|
+
* @default true
|
|
47
|
+
*/
|
|
48
|
+
internalCallers?: boolean | ((c: Context) => boolean);
|
|
30
49
|
}
|
|
31
50
|
/**
|
|
32
51
|
* Get the current span from Hono context
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.cts","names":[],"sources":["../../src/instrumentation/hono.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"hono.d.cts","names":[],"sources":["../../src/instrumentation/hono.ts"],"mappings":";;;;;;iBAqBiB;;;;;EAKhB;;;;;EAMA;;;;EAKA,aAAa,GAAG;;;;EAKhB;;;;EAKA,cAAc,GAAG;;;;;;;;;EAUjB;;;;;;;;;;EAWA,8BAA8B,GAAG;;;;;wBAUlB,mBAAmB,GAAG,UAAU;;;;wBAOhC,wBAAwB,GAAG;;;;;;;;;;;;wBAe3B,wBACf,UAAS,iCACP;;;;;;;;;;;;;;;wBAiPmB,oBAAoB,GACzC,GAAG,SACH,UAAU,QAAQ,KAChB,QAAQ"}
|
|
@@ -27,6 +27,25 @@ export interface HonoTelemetryMiddlewareOptions {
|
|
|
27
27
|
* Whether to skip tracing for this request
|
|
28
28
|
*/
|
|
29
29
|
shouldSkip?: (c: Context$1) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The origins whose `traceparent` is continued: the API's own sites, the
|
|
32
|
+
* same list its CORS allows. A request from any other origin starts a new
|
|
33
|
+
* trace linked to the one it claimed.
|
|
34
|
+
*
|
|
35
|
+
* A function is read on each request, for a server that learns its origins
|
|
36
|
+
* only once the app has loaded its configuration.
|
|
37
|
+
*/
|
|
38
|
+
trustedOrigins?: readonly string[] | (() => readonly string[] | undefined);
|
|
39
|
+
/**
|
|
40
|
+
* Whether a request with no `Origin` from an internal caller continues its
|
|
41
|
+
* `traceparent` — another service on the private network (see `./trust`):
|
|
42
|
+
* no `Origin`, no proxy's forwarding header, and a loopback or private
|
|
43
|
+
* peer address, read from `@hono/node-server`'s socket.
|
|
44
|
+
*
|
|
45
|
+
* `false` trusts no caller without an origin; a function decides itself.
|
|
46
|
+
* @default true
|
|
47
|
+
*/
|
|
48
|
+
internalCallers?: boolean | ((c: Context$1) => boolean);
|
|
30
49
|
}
|
|
31
50
|
/**
|
|
32
51
|
* Get the current span from Hono context
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.mts","names":["Context"],"sources":["../../src/instrumentation/hono.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"hono.d.mts","names":["Context"],"sources":["../../src/instrumentation/hono.ts"],"mappings":";;;;;;iBAqBiB;;;;;EAKhB;;;;;EAMA;;;;EAKA,aAAa,GAAGA;;;;EAKhB;;;;EAKA,cAAc,GAAGA;;;;;;;;;EAUjB;;;;;;;;;;EAWA,8BAA8B,GAAGA;;;;;wBAUlB,mBAAmB,GAAGA,YAAU;;;;wBAOhC,wBAAwB,GAAGA;;;;;;;;;;;;wBAe3B,wBACf,UAAS,iCACP;;;;;;;;;;;;;;;wBAiPmB,oBAAoB,GACzC,GAAGA,WACH,UAAU,QAAQ,KAChB,QAAQ"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createHttpServerSpan, endHttpSpan,
|
|
1
|
+
import { createHttpServerSpan, endHttpSpan, incomingTraceContext } from "./http.mjs";
|
|
2
|
+
import { isInternalCaller, isTrustedOrigin } from "./trust.mjs";
|
|
2
3
|
import { context, trace } from "@opentelemetry/api";
|
|
3
4
|
import { routePath } from "hono/route";
|
|
4
5
|
//#region src/instrumentation/hono.ts
|
|
@@ -35,9 +36,9 @@ function honoTelemetryMiddleware(options = {}) {
|
|
|
35
36
|
c.req.raw.headers.forEach((value, key) => {
|
|
36
37
|
headers[key] = value;
|
|
37
38
|
});
|
|
38
|
-
const parentContext =
|
|
39
|
+
const { parent: parentContext, links } = incomingTraceContext(headers, isTrustedCaller(c, options));
|
|
39
40
|
const attrs = buildHonoSpanAttributes(c, options);
|
|
40
|
-
const span = createHttpServerSpan(attrs, parentContext);
|
|
41
|
+
const span = createHttpServerSpan(attrs, parentContext, links);
|
|
41
42
|
const spanContext = trace.setSpan(parentContext, span);
|
|
42
43
|
c.set(SPAN_KEY, span);
|
|
43
44
|
c.set(CONTEXT_KEY, spanContext);
|
|
@@ -64,6 +65,28 @@ function honoTelemetryMiddleware(options = {}) {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
68
|
+
* Whether this request's `traceparent` is continued: from one of the API's
|
|
69
|
+
* own origins, or from an internal caller.
|
|
70
|
+
*/
|
|
71
|
+
function isTrustedCaller(c, options) {
|
|
72
|
+
const origin = c.req.header("origin");
|
|
73
|
+
if (origin) {
|
|
74
|
+
const trusted = typeof options.trustedOrigins === "function" ? options.trustedOrigins() : options.trustedOrigins;
|
|
75
|
+
return isTrustedOrigin(origin, trusted);
|
|
76
|
+
}
|
|
77
|
+
const internal = options.internalCallers ?? true;
|
|
78
|
+
if (typeof internal === "function") return internal(c);
|
|
79
|
+
if (!internal) return false;
|
|
80
|
+
return isInternalCaller({
|
|
81
|
+
headers: c.req.raw.headers,
|
|
82
|
+
remoteAddress: remoteAddressOf(c)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** The TCP peer's address under `@hono/node-server`, or undefined. */
|
|
86
|
+
function remoteAddressOf(c) {
|
|
87
|
+
return c.env?.incoming?.socket?.remoteAddress;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
67
90
|
* The route the request matched, once the handler has run — `/users/:id`,
|
|
68
91
|
* never `/users/42` — or undefined when only middleware ran (a 404).
|
|
69
92
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.mjs","names":[],"sources":["../../src/instrumentation/hono.ts"],"sourcesContent":["import { context, type Span, trace } from '@opentelemetry/api';\n/**\n * OpenTelemetry instrumentation middleware for Hono\n *\n * This middleware automatically creates spans for HTTP requests,\n * extracts trace context from incoming headers, and records\n * request/response metadata as span attributes.\n */\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { routePath } from 'hono/route';\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\textractTraceContext,\n\ttype HttpSpanAttributes,\n} from './http';\n\n/**\n * Options for the Hono telemetry middleware\n */\nexport interface HonoTelemetryMiddlewareOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract user ID from context\n\t */\n\tgetUserId?: (c: Context) => string | undefined;\n\n\t/**\n\t * Paths to skip tracing (supports wildcards)\n\t */\n\tignorePaths?: string[];\n\n\t/**\n\t * Whether to skip tracing for this request\n\t */\n\tshouldSkip?: (c: Context) => boolean;\n}\n\n// Key for storing span on context\nconst SPAN_KEY = 'telemetry.span';\nconst CONTEXT_KEY = 'telemetry.context';\n\n/**\n * Get the current span from Hono context\n */\nexport function getSpanFromContext(c: Context): Span | undefined {\n\treturn c.get(SPAN_KEY);\n}\n\n/**\n * Get the current trace context from Hono context\n */\nexport function getTraceContextFromHono(c: Context): any {\n\treturn c.get(CONTEXT_KEY);\n}\n\n/**\n * Create a telemetry middleware for Hono applications\n *\n * @example\n * ```typescript\n * import { honoTelemetryMiddleware } from '@geekmidas/telescope/instrumentation';\n *\n * const app = new Hono();\n * app.use('*', honoTelemetryMiddleware());\n * ```\n */\nexport function honoTelemetryMiddleware(\n\toptions: HonoTelemetryMiddlewareOptions = {},\n): MiddlewareHandler {\n\treturn async (c, next) => {\n\t\t// Check if path should be ignored\n\t\tif (\n\t\t\toptions.ignorePaths &&\n\t\t\tshouldIgnorePath(c.req.path, options.ignorePaths)\n\t\t) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Skip if custom skip function returns true\n\t\tif (options.shouldSkip?.(c)) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Extract trace context from headers\n\t\tconst headers: Record<string, string | string[] | undefined> = {};\n\t\tc.req.raw.headers.forEach((value, key) => {\n\t\t\theaders[key] = value;\n\t\t});\n\t\tconst parentContext = extractTraceContext(headers);\n\n\t\t// Build span attributes from request\n\t\tconst attrs = buildHonoSpanAttributes(c, options);\n\n\t\t// Create the span\n\t\tconst span = createHttpServerSpan(attrs, parentContext);\n\t\tconst spanContext = trace.setSpan(parentContext, span);\n\n\t\t// Store span and context on Hono context\n\t\tc.set(SPAN_KEY, span);\n\t\tc.set(CONTEXT_KEY, spanContext);\n\n\t\t// Record body if requested\n\t\tif (options.recordBody) {\n\t\t\ttry {\n\t\t\t\tconst body = await c.req.text();\n\t\t\t\tif (body) {\n\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t'http.request.body',\n\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Body may have already been consumed\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\t// Execute the rest of the middleware chain within span context, so\n\t\t\t// the handler's logs and outbound spans nest under this one.\n\t\t\tawait context.with(spanContext, async () => {\n\t\t\t\tawait next();\n\t\t\t});\n\n\t\t\tnameAfterRoute(c, span);\n\n\t\t\t// Record response\n\t\t\tconst statusCode = c.res.status;\n\n\t\t\tif (options.recordResponseBody) {\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await c.res.clone().text();\n\t\t\t\t\tif (body) {\n\t\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t\t'http.response.body',\n\t\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Response body may not be available\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Hono hands a thrown error to `onError` and sets `c.error`\n\t\t\t// rather than rethrowing, so that is where it is found.\n\t\t\tendHttpSpan(span, { statusCode }, c.error);\n\t\t} catch (error) {\n\t\t\tnameAfterRoute(c, span);\n\t\t\tendHttpSpan(\n\t\t\t\tspan,\n\t\t\t\t{ statusCode: 500 },\n\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t);\n\t\t\tthrow error;\n\t\t}\n\t};\n}\n\n/**\n * The route the request matched, once the handler has run — `/users/:id`,\n * never `/users/42` — or undefined when only middleware ran (a 404).\n *\n * Read after `next()`: before it, the route is this middleware's own `*`.\n */\nfunction matchedRoute(c: Context): string | undefined {\n\tconst path = routePath(c);\n\treturn path && !path.includes('*') ? path : undefined;\n}\n\n/** `GET /users/:id`, or the bare method when no route matched. */\nfunction nameAfterRoute(c: Context, span: Span): void {\n\tconst route = matchedRoute(c);\n\tif (route) {\n\t\tspan.setAttribute('http.route', route);\n\t\tspan.updateName(`${c.req.method} ${route}`);\n\t} else {\n\t\tspan.updateName(c.req.method);\n\t}\n}\n\n/**\n * Build span attributes from Hono context\n */\nfunction buildHonoSpanAttributes(\n\tc: Context,\n\toptions: HonoTelemetryMiddlewareOptions,\n): HttpSpanAttributes {\n\tconst req = c.req;\n\tconst url = new URL(req.url);\n\n\t// No `url.full`: a query string can carry a token, and the span would\n\t// keep it. The route is named once the handler has run.\n\tconst attrs: HttpSpanAttributes = {\n\t\tmethod: req.method,\n\t\tpath: url.pathname,\n\t\thost: url.host,\n\t\tscheme: url.protocol.replace(':', '') as 'http' | 'https',\n\t\tuserAgent: req.header('user-agent'),\n\t\tclientIp: getClientIp(c),\n\t\trequestId: req.header('x-request-id') || req.header('x-amzn-requestid'),\n\t};\n\n\t// Add user metadata if extractor provided\n\tif (options.getUserId) {\n\t\tconst userId = options.getUserId(c);\n\t\tif (userId) {\n\t\t\tattrs.user = { userId };\n\t\t}\n\t}\n\n\treturn attrs;\n}\n\n/**\n * Extract client IP from Hono context\n */\nfunction getClientIp(c: Context): string | undefined {\n\t// Try various headers in order of preference\n\tconst xForwardedFor = c.req.header('x-forwarded-for');\n\tif (xForwardedFor) {\n\t\tconst first = xForwardedFor.split(',')[0];\n\t\treturn first?.trim();\n\t}\n\n\tconst xRealIp = c.req.header('x-real-ip');\n\tif (xRealIp) {\n\t\treturn xRealIp;\n\t}\n\n\t// Try CF-Connecting-IP (Cloudflare)\n\tconst cfConnectingIp = c.req.header('cf-connecting-ip');\n\tif (cfConnectingIp) {\n\t\treturn cfConnectingIp;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Check if path should be ignored\n */\nfunction shouldIgnorePath(path: string, ignorePaths: string[]): boolean {\n\tfor (const pattern of ignorePaths) {\n\t\tif (pattern.endsWith('*')) {\n\t\t\t// Wildcard match\n\t\t\tconst prefix = pattern.slice(0, -1);\n\t\t\tif (path.startsWith(prefix)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (path === pattern) {\n\t\t\t// Exact match\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Run a function within the span context from Hono context\n *\n * @example\n * ```typescript\n * app.get('/api/users', async (c) => {\n * return withHonoSpanContext(c, async () => {\n * // Any spans created here will be children of the request span\n * const users = await fetchUsers();\n * return c.json(users);\n * });\n * });\n * ```\n */\nexport async function withHonoSpanContext<T>(\n\tc: Context,\n\tfn: () => Promise<T>,\n): Promise<T> {\n\tconst ctx = getTraceContextFromHono(c);\n\tif (!ctx) {\n\t\treturn fn();\n\t}\n\treturn context.with(ctx, fn);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hono.mjs","names":[],"sources":["../../src/instrumentation/hono.ts"],"sourcesContent":["import { context, type Span, trace } from '@opentelemetry/api';\n/**\n * OpenTelemetry instrumentation middleware for Hono\n *\n * This middleware automatically creates spans for HTTP requests,\n * extracts trace context from incoming headers, and records\n * request/response metadata as span attributes.\n */\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { routePath } from 'hono/route';\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\ttype HttpSpanAttributes,\n\tincomingTraceContext,\n} from './http';\nimport { isInternalCaller, isTrustedOrigin } from './trust';\n\n/**\n * Options for the Hono telemetry middleware\n */\nexport interface HonoTelemetryMiddlewareOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract user ID from context\n\t */\n\tgetUserId?: (c: Context) => string | undefined;\n\n\t/**\n\t * Paths to skip tracing (supports wildcards)\n\t */\n\tignorePaths?: string[];\n\n\t/**\n\t * Whether to skip tracing for this request\n\t */\n\tshouldSkip?: (c: Context) => boolean;\n\n\t/**\n\t * The origins whose `traceparent` is continued: the API's own sites, the\n\t * same list its CORS allows. A request from any other origin starts a new\n\t * trace linked to the one it claimed.\n\t *\n\t * A function is read on each request, for a server that learns its origins\n\t * only once the app has loaded its configuration.\n\t */\n\ttrustedOrigins?: readonly string[] | (() => readonly string[] | undefined);\n\n\t/**\n\t * Whether a request with no `Origin` from an internal caller continues its\n\t * `traceparent` — another service on the private network (see `./trust`):\n\t * no `Origin`, no proxy's forwarding header, and a loopback or private\n\t * peer address, read from `@hono/node-server`'s socket.\n\t *\n\t * `false` trusts no caller without an origin; a function decides itself.\n\t * @default true\n\t */\n\tinternalCallers?: boolean | ((c: Context) => boolean);\n}\n\n// Key for storing span on context\nconst SPAN_KEY = 'telemetry.span';\nconst CONTEXT_KEY = 'telemetry.context';\n\n/**\n * Get the current span from Hono context\n */\nexport function getSpanFromContext(c: Context): Span | undefined {\n\treturn c.get(SPAN_KEY);\n}\n\n/**\n * Get the current trace context from Hono context\n */\nexport function getTraceContextFromHono(c: Context): any {\n\treturn c.get(CONTEXT_KEY);\n}\n\n/**\n * Create a telemetry middleware for Hono applications\n *\n * @example\n * ```typescript\n * import { honoTelemetryMiddleware } from '@geekmidas/telescope/instrumentation';\n *\n * const app = new Hono();\n * app.use('*', honoTelemetryMiddleware());\n * ```\n */\nexport function honoTelemetryMiddleware(\n\toptions: HonoTelemetryMiddlewareOptions = {},\n): MiddlewareHandler {\n\treturn async (c, next) => {\n\t\t// Check if path should be ignored\n\t\tif (\n\t\t\toptions.ignorePaths &&\n\t\t\tshouldIgnorePath(c.req.path, options.ignorePaths)\n\t\t) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Skip if custom skip function returns true\n\t\tif (options.shouldSkip?.(c)) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Extract trace context from headers\n\t\tconst headers: Record<string, string | string[] | undefined> = {};\n\t\tc.req.raw.headers.forEach((value, key) => {\n\t\t\theaders[key] = value;\n\t\t});\n\t\t// Continued only from a trusted caller; anyone else gets a new trace\n\t\t// that links to the one they claimed.\n\t\tconst { parent: parentContext, links } = incomingTraceContext(\n\t\t\theaders,\n\t\t\tisTrustedCaller(c, options),\n\t\t);\n\n\t\t// Build span attributes from request\n\t\tconst attrs = buildHonoSpanAttributes(c, options);\n\n\t\t// Create the span\n\t\tconst span = createHttpServerSpan(attrs, parentContext, links);\n\t\tconst spanContext = trace.setSpan(parentContext, span);\n\n\t\t// Store span and context on Hono context\n\t\tc.set(SPAN_KEY, span);\n\t\tc.set(CONTEXT_KEY, spanContext);\n\n\t\t// Record body if requested\n\t\tif (options.recordBody) {\n\t\t\ttry {\n\t\t\t\tconst body = await c.req.text();\n\t\t\t\tif (body) {\n\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t'http.request.body',\n\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Body may have already been consumed\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\t// Execute the rest of the middleware chain within span context, so\n\t\t\t// the handler's logs and outbound spans nest under this one.\n\t\t\tawait context.with(spanContext, async () => {\n\t\t\t\tawait next();\n\t\t\t});\n\n\t\t\tnameAfterRoute(c, span);\n\n\t\t\t// Record response\n\t\t\tconst statusCode = c.res.status;\n\n\t\t\tif (options.recordResponseBody) {\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await c.res.clone().text();\n\t\t\t\t\tif (body) {\n\t\t\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\t\t'http.response.body',\n\t\t\t\t\t\t\tbody.length > 4096 ? `${body.slice(0, 4096)}...` : body,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Response body may not be available\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Hono hands a thrown error to `onError` and sets `c.error`\n\t\t\t// rather than rethrowing, so that is where it is found.\n\t\t\tendHttpSpan(span, { statusCode }, c.error);\n\t\t} catch (error) {\n\t\t\tnameAfterRoute(c, span);\n\t\t\tendHttpSpan(\n\t\t\t\tspan,\n\t\t\t\t{ statusCode: 500 },\n\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t);\n\t\t\tthrow error;\n\t\t}\n\t};\n}\n\n/**\n * Whether this request's `traceparent` is continued: from one of the API's\n * own origins, or from an internal caller.\n */\nfunction isTrustedCaller(\n\tc: Context,\n\toptions: HonoTelemetryMiddlewareOptions,\n): boolean {\n\tconst origin = c.req.header('origin');\n\tif (origin) {\n\t\tconst trusted =\n\t\t\ttypeof options.trustedOrigins === 'function'\n\t\t\t\t? options.trustedOrigins()\n\t\t\t\t: options.trustedOrigins;\n\t\treturn isTrustedOrigin(origin, trusted);\n\t}\n\n\tconst internal = options.internalCallers ?? true;\n\tif (typeof internal === 'function') return internal(c);\n\tif (!internal) return false;\n\treturn isInternalCaller({\n\t\theaders: c.req.raw.headers,\n\t\tremoteAddress: remoteAddressOf(c),\n\t});\n}\n\n/** The TCP peer's address under `@hono/node-server`, or undefined. */\nfunction remoteAddressOf(c: Context): string | undefined {\n\tconst env = c.env as\n\t\t| { incoming?: { socket?: { remoteAddress?: string } } }\n\t\t| undefined;\n\treturn env?.incoming?.socket?.remoteAddress;\n}\n\n/**\n * The route the request matched, once the handler has run — `/users/:id`,\n * never `/users/42` — or undefined when only middleware ran (a 404).\n *\n * Read after `next()`: before it, the route is this middleware's own `*`.\n */\nfunction matchedRoute(c: Context): string | undefined {\n\tconst path = routePath(c);\n\treturn path && !path.includes('*') ? path : undefined;\n}\n\n/** `GET /users/:id`, or the bare method when no route matched. */\nfunction nameAfterRoute(c: Context, span: Span): void {\n\tconst route = matchedRoute(c);\n\tif (route) {\n\t\tspan.setAttribute('http.route', route);\n\t\tspan.updateName(`${c.req.method} ${route}`);\n\t} else {\n\t\tspan.updateName(c.req.method);\n\t}\n}\n\n/**\n * Build span attributes from Hono context\n */\nfunction buildHonoSpanAttributes(\n\tc: Context,\n\toptions: HonoTelemetryMiddlewareOptions,\n): HttpSpanAttributes {\n\tconst req = c.req;\n\tconst url = new URL(req.url);\n\n\t// No `url.full`: a query string can carry a token, and the span would\n\t// keep it. The route is named once the handler has run.\n\tconst attrs: HttpSpanAttributes = {\n\t\tmethod: req.method,\n\t\tpath: url.pathname,\n\t\thost: url.host,\n\t\tscheme: url.protocol.replace(':', '') as 'http' | 'https',\n\t\tuserAgent: req.header('user-agent'),\n\t\tclientIp: getClientIp(c),\n\t\trequestId: req.header('x-request-id') || req.header('x-amzn-requestid'),\n\t};\n\n\t// Add user metadata if extractor provided\n\tif (options.getUserId) {\n\t\tconst userId = options.getUserId(c);\n\t\tif (userId) {\n\t\t\tattrs.user = { userId };\n\t\t}\n\t}\n\n\treturn attrs;\n}\n\n/**\n * Extract client IP from Hono context\n */\nfunction getClientIp(c: Context): string | undefined {\n\t// Try various headers in order of preference\n\tconst xForwardedFor = c.req.header('x-forwarded-for');\n\tif (xForwardedFor) {\n\t\tconst first = xForwardedFor.split(',')[0];\n\t\treturn first?.trim();\n\t}\n\n\tconst xRealIp = c.req.header('x-real-ip');\n\tif (xRealIp) {\n\t\treturn xRealIp;\n\t}\n\n\t// Try CF-Connecting-IP (Cloudflare)\n\tconst cfConnectingIp = c.req.header('cf-connecting-ip');\n\tif (cfConnectingIp) {\n\t\treturn cfConnectingIp;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Check if path should be ignored\n */\nfunction shouldIgnorePath(path: string, ignorePaths: string[]): boolean {\n\tfor (const pattern of ignorePaths) {\n\t\tif (pattern.endsWith('*')) {\n\t\t\t// Wildcard match\n\t\t\tconst prefix = pattern.slice(0, -1);\n\t\t\tif (path.startsWith(prefix)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (path === pattern) {\n\t\t\t// Exact match\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Run a function within the span context from Hono context\n *\n * @example\n * ```typescript\n * app.get('/api/users', async (c) => {\n * return withHonoSpanContext(c, async () => {\n * // Any spans created here will be children of the request span\n * const users = await fetchUsers();\n * return c.json(users);\n * });\n * });\n * ```\n */\nexport async function withHonoSpanContext<T>(\n\tc: Context,\n\tfn: () => Promise<T>,\n): Promise<T> {\n\tconst ctx = getTraceContextFromHono(c);\n\tif (!ctx) {\n\t\treturn fn();\n\t}\n\treturn context.with(ctx, fn);\n}\n"],"mappings":"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"}
|
|
@@ -45,6 +45,29 @@ function extractTraceContext(headers) {
|
|
|
45
45
|
return _opentelemetry_api.propagation.extract(_opentelemetry_api.context.active(), normalizedHeaders);
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
+
* The trace a request's span belongs to.
|
|
49
|
+
*
|
|
50
|
+
* From a trusted caller (see `./trust`) its `traceparent` is continued, and
|
|
51
|
+
* the span is its child. From anyone else the span starts a new trace — no
|
|
52
|
+
* parent, no baggage — with a link to the context the caller claimed, so the
|
|
53
|
+
* hop can still be followed without the caller choosing this trace.
|
|
54
|
+
*/
|
|
55
|
+
function incomingTraceContext(headers, trusted) {
|
|
56
|
+
const extracted = extractTraceContext(headers);
|
|
57
|
+
if (trusted) return {
|
|
58
|
+
parent: extracted,
|
|
59
|
+
links: []
|
|
60
|
+
};
|
|
61
|
+
const claimed = _opentelemetry_api.trace.getSpanContext(extracted);
|
|
62
|
+
return {
|
|
63
|
+
parent: _opentelemetry_api.trace.deleteSpan(_opentelemetry_api.context.active()),
|
|
64
|
+
links: claimed && (0, _opentelemetry_api.isSpanContextValid)(claimed) ? [{
|
|
65
|
+
context: claimed,
|
|
66
|
+
attributes: { "gkm.trace.untrusted_parent": true }
|
|
67
|
+
}] : []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
48
71
|
* Inject trace context into HTTP headers
|
|
49
72
|
*/
|
|
50
73
|
function injectTraceContext(headers, ctx) {
|
|
@@ -55,13 +78,14 @@ function injectTraceContext(headers, ctx) {
|
|
|
55
78
|
/**
|
|
56
79
|
* Create an HTTP server span for an incoming request
|
|
57
80
|
*/
|
|
58
|
-
function createHttpServerSpan(attrs, parentContext) {
|
|
81
|
+
function createHttpServerSpan(attrs, parentContext, links) {
|
|
59
82
|
const tracer = getConstructsTracer();
|
|
60
83
|
const spanName = attrs.route ? `${attrs.method} ${attrs.route}` : `${attrs.method} ${attrs.path || "/"}`;
|
|
61
84
|
const ctx = parentContext ?? _opentelemetry_api.context.active();
|
|
62
85
|
return tracer.startSpan(spanName, {
|
|
63
86
|
kind: _opentelemetry_api.SpanKind.SERVER,
|
|
64
|
-
attributes: toOtelAttributes(attrs)
|
|
87
|
+
attributes: toOtelAttributes(attrs),
|
|
88
|
+
...links?.length ? { links } : {}
|
|
65
89
|
}, ctx);
|
|
66
90
|
}
|
|
67
91
|
/**
|
|
@@ -156,6 +180,7 @@ exports.createHttpServerSpan = createHttpServerSpan;
|
|
|
156
180
|
exports.endHttpSpan = endHttpSpan;
|
|
157
181
|
exports.extractTraceContext = extractTraceContext;
|
|
158
182
|
exports.getConstructsTracer = getConstructsTracer;
|
|
183
|
+
exports.incomingTraceContext = incomingTraceContext;
|
|
159
184
|
exports.injectTraceContext = injectTraceContext;
|
|
160
185
|
exports.isTracingEnabled = isTracingEnabled;
|
|
161
186
|
exports.toOtelAttributes = toOtelAttributes;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.cjs","names":["trace","propagation","context","SpanKind","SpanStatusCode"],"sources":["../../src/instrumentation/http.ts"],"sourcesContent":["import {\n\ttype Attributes,\n\ttype Context,\n\tcontext,\n\tpropagation,\n\ttype Span,\n\tSpanKind,\n\tSpanStatusCode,\n\ttrace,\n} from '@opentelemetry/api';\n\n/**\n * HTTP request attributes following OpenTelemetry semantic conventions\n */\nexport interface HttpRequestAttributes {\n\tmethod: string;\n\turl?: string;\n\tpath?: string;\n\troute?: string;\n\thost?: string;\n\tscheme?: 'http' | 'https';\n\tuserAgent?: string;\n\tclientIp?: string;\n\trequestId?: string;\n}\n\n/**\n * HTTP response attributes\n */\nexport interface HttpResponseAttributes {\n\tstatusCode: number;\n\tresponseSize?: number;\n}\n\n/**\n * Endpoint-specific attributes\n */\nexport interface EndpointAttributes {\n\tname?: string;\n\toperationId?: string;\n\ttags?: string[];\n}\n\n/**\n * User/session attributes\n */\nexport interface UserAttributes {\n\tuserId?: string;\n\tsessionId?: string;\n\troles?: string[];\n}\n\n/**\n * Full span attributes for HTTP requests\n */\nexport interface HttpSpanAttributes extends HttpRequestAttributes {\n\tresponse?: HttpResponseAttributes;\n\tendpoint?: EndpointAttributes;\n\tuser?: UserAttributes;\n\tcustom?: Attributes;\n}\n\nconst TRACER_NAME = '@geekmidas/constructs';\n\n/**\n * Get or create a tracer for constructs instrumentation\n */\nexport function getConstructsTracer() {\n\treturn trace.getTracer(TRACER_NAME);\n}\n\n/**\n * Convert HTTP attributes to OpenTelemetry semantic conventions\n */\nexport function toOtelAttributes(attrs: HttpSpanAttributes): Attributes {\n\tconst otelAttrs: Attributes = {};\n\n\t// HTTP request attributes (OpenTelemetry semantic conventions)\n\tif (attrs.method) otelAttrs['http.request.method'] = attrs.method;\n\tif (attrs.url) otelAttrs['url.full'] = attrs.url;\n\tif (attrs.path) otelAttrs['url.path'] = attrs.path;\n\tif (attrs.route) otelAttrs['http.route'] = attrs.route;\n\tif (attrs.host) otelAttrs['server.address'] = attrs.host;\n\tif (attrs.scheme) otelAttrs['url.scheme'] = attrs.scheme;\n\tif (attrs.userAgent) otelAttrs['user_agent.original'] = attrs.userAgent;\n\tif (attrs.clientIp) otelAttrs['client.address'] = attrs.clientIp;\n\tif (attrs.requestId) otelAttrs['http.request.id'] = attrs.requestId;\n\n\t// HTTP response attributes\n\tif (attrs.response?.statusCode) {\n\t\totelAttrs['http.response.status_code'] = attrs.response.statusCode;\n\t}\n\tif (attrs.response?.responseSize) {\n\t\totelAttrs['http.response.body.size'] = attrs.response.responseSize;\n\t}\n\n\t// Endpoint attributes\n\tif (attrs.endpoint?.name) otelAttrs['endpoint.name'] = attrs.endpoint.name;\n\tif (attrs.endpoint?.operationId) {\n\t\totelAttrs['endpoint.operation_id'] = attrs.endpoint.operationId;\n\t}\n\tif (attrs.endpoint?.tags?.length) {\n\t\totelAttrs['endpoint.tags'] = attrs.endpoint.tags;\n\t}\n\n\t// User attributes\n\tif (attrs.user?.userId) otelAttrs['enduser.id'] = attrs.user.userId;\n\tif (attrs.user?.sessionId) otelAttrs['session.id'] = attrs.user.sessionId;\n\tif (attrs.user?.roles?.length)\n\t\totelAttrs['enduser.role'] = attrs.user.roles.join(',');\n\n\t// Custom attributes\n\tif (attrs.custom) {\n\t\tObject.assign(otelAttrs, attrs.custom);\n\t}\n\n\treturn otelAttrs;\n}\n\n/**\n * Extract trace context from HTTP headers\n */\nexport function extractTraceContext(\n\theaders: Record<string, string | string[] | undefined>,\n): Context {\n\t// Normalize headers to Record<string, string>\n\tconst normalizedHeaders: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (value) {\n\t\t\tconst stringValue = Array.isArray(value) ? value[0] : value;\n\t\t\tif (stringValue) {\n\t\t\t\tnormalizedHeaders[key.toLowerCase()] = stringValue;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn propagation.extract(context.active(), normalizedHeaders);\n}\n\n/**\n * Inject trace context into HTTP headers\n */\nexport function injectTraceContext(\n\theaders: Record<string, string>,\n\tctx?: Context,\n): Record<string, string> {\n\tconst result = { ...headers };\n\tpropagation.inject(ctx ?? context.active(), result);\n\treturn result;\n}\n\n/**\n * Create an HTTP server span for an incoming request\n */\nexport function createHttpServerSpan(\n\tattrs: HttpSpanAttributes,\n\tparentContext?: Context,\n): Span {\n\tconst tracer = getConstructsTracer();\n\tconst spanName = attrs.route\n\t\t? `${attrs.method} ${attrs.route}`\n\t\t: `${attrs.method} ${attrs.path || '/'}`;\n\n\tconst ctx = parentContext ?? context.active();\n\n\treturn tracer.startSpan(\n\t\tspanName,\n\t\t{\n\t\t\tkind: SpanKind.SERVER,\n\t\t\tattributes: toOtelAttributes(attrs),\n\t\t},\n\t\tctx,\n\t);\n}\n\n/**\n * End an HTTP span with response data\n */\nexport function endHttpSpan(\n\tspan: Span,\n\tresponse: HttpResponseAttributes,\n\terror?: Error,\n): void {\n\tspan.setAttribute('http.response.status_code', response.statusCode);\n\n\tif (response.responseSize) {\n\t\tspan.setAttribute('http.response.body.size', response.responseSize);\n\t}\n\n\tif (error) {\n\t\tspan.recordException(error);\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error.message,\n\t\t});\n\t} else if (response.statusCode >= 500) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: `HTTP ${response.statusCode}`,\n\t\t});\n\t} else if (response.statusCode < 400) {\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t}\n\t// A 4xx leaves the status unset: on a server span it is the client's\n\t// mistake, not the server's failure (HTTP semantic conventions).\n\n\tspan.end();\n}\n\n/**\n * Execute a function within an HTTP server span context\n *\n * @example\n * ```typescript\n * const result = await withHttpSpan(\n * { method: 'GET', path: '/users', route: '/users' },\n * async (span) => {\n * const users = await getUsers();\n * return { statusCode: 200, body: users };\n * }\n * );\n * ```\n */\nexport async function withHttpSpan<T>(\n\tattrs: HttpSpanAttributes,\n\tfn: (span: Span) => Promise<T & { statusCode?: number }>,\n\tparentContext?: Context,\n): Promise<T> {\n\tconst span = createHttpServerSpan(attrs, parentContext);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(parentContext ?? context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\n\t\tconst statusCode = (result as { statusCode?: number }).statusCode ?? 200;\n\t\tendHttpSpan(span, { statusCode });\n\n\t\treturn result;\n\t} catch (error) {\n\t\tendHttpSpan(\n\t\t\tspan,\n\t\t\t{ statusCode: 500 },\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t}\n}\n\n/**\n * Create a child span for internal operations (middleware, handlers, etc.)\n */\nexport function createChildSpan(name: string, attributes?: Attributes): Span {\n\tconst tracer = getConstructsTracer();\n\treturn tracer.startSpan(name, {\n\t\tkind: SpanKind.INTERNAL,\n\t\tattributes,\n\t});\n}\n\n/**\n * Execute a function within a child span\n */\nexport async function withChildSpan<T>(\n\tname: string,\n\tfn: (span: Span) => Promise<T>,\n\tattributes?: Attributes,\n): Promise<T> {\n\tconst span = createChildSpan(name, attributes);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t\treturn result;\n\t} catch (error) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error instanceof Error ? error.message : 'Unknown error',\n\t\t});\n\t\tspan.recordException(\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t} finally {\n\t\tspan.end();\n\t}\n}\n\n/**\n * Check if tracing is enabled (OpenTelemetry API is available and configured)\n */\nexport function isTracingEnabled(): boolean {\n\ttry {\n\t\tconst tracer = trace.getTracer(TRACER_NAME);\n\t\t// Check if we have a real tracer (not a no-op)\n\t\tconst span = tracer.startSpan('test');\n\t\tconst isReal =\n\t\t\tspan.spanContext().traceId !== '00000000000000000000000000000000';\n\t\tspan.end();\n\t\treturn isReal;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.cjs","names":["trace","propagation","context","isSpanContextValid","SpanKind","SpanStatusCode"],"sources":["../../src/instrumentation/http.ts"],"sourcesContent":["import {\n\ttype Attributes,\n\ttype Context,\n\tcontext,\n\tisSpanContextValid,\n\ttype Link,\n\tpropagation,\n\ttype Span,\n\tSpanKind,\n\tSpanStatusCode,\n\ttrace,\n} from '@opentelemetry/api';\n\n/**\n * HTTP request attributes following OpenTelemetry semantic conventions\n */\nexport interface HttpRequestAttributes {\n\tmethod: string;\n\turl?: string;\n\tpath?: string;\n\troute?: string;\n\thost?: string;\n\tscheme?: 'http' | 'https';\n\tuserAgent?: string;\n\tclientIp?: string;\n\trequestId?: string;\n}\n\n/**\n * HTTP response attributes\n */\nexport interface HttpResponseAttributes {\n\tstatusCode: number;\n\tresponseSize?: number;\n}\n\n/**\n * Endpoint-specific attributes\n */\nexport interface EndpointAttributes {\n\tname?: string;\n\toperationId?: string;\n\ttags?: string[];\n}\n\n/**\n * User/session attributes\n */\nexport interface UserAttributes {\n\tuserId?: string;\n\tsessionId?: string;\n\troles?: string[];\n}\n\n/**\n * Full span attributes for HTTP requests\n */\nexport interface HttpSpanAttributes extends HttpRequestAttributes {\n\tresponse?: HttpResponseAttributes;\n\tendpoint?: EndpointAttributes;\n\tuser?: UserAttributes;\n\tcustom?: Attributes;\n}\n\nconst TRACER_NAME = '@geekmidas/constructs';\n\n/**\n * Get or create a tracer for constructs instrumentation\n */\nexport function getConstructsTracer() {\n\treturn trace.getTracer(TRACER_NAME);\n}\n\n/**\n * Convert HTTP attributes to OpenTelemetry semantic conventions\n */\nexport function toOtelAttributes(attrs: HttpSpanAttributes): Attributes {\n\tconst otelAttrs: Attributes = {};\n\n\t// HTTP request attributes (OpenTelemetry semantic conventions)\n\tif (attrs.method) otelAttrs['http.request.method'] = attrs.method;\n\tif (attrs.url) otelAttrs['url.full'] = attrs.url;\n\tif (attrs.path) otelAttrs['url.path'] = attrs.path;\n\tif (attrs.route) otelAttrs['http.route'] = attrs.route;\n\tif (attrs.host) otelAttrs['server.address'] = attrs.host;\n\tif (attrs.scheme) otelAttrs['url.scheme'] = attrs.scheme;\n\tif (attrs.userAgent) otelAttrs['user_agent.original'] = attrs.userAgent;\n\tif (attrs.clientIp) otelAttrs['client.address'] = attrs.clientIp;\n\tif (attrs.requestId) otelAttrs['http.request.id'] = attrs.requestId;\n\n\t// HTTP response attributes\n\tif (attrs.response?.statusCode) {\n\t\totelAttrs['http.response.status_code'] = attrs.response.statusCode;\n\t}\n\tif (attrs.response?.responseSize) {\n\t\totelAttrs['http.response.body.size'] = attrs.response.responseSize;\n\t}\n\n\t// Endpoint attributes\n\tif (attrs.endpoint?.name) otelAttrs['endpoint.name'] = attrs.endpoint.name;\n\tif (attrs.endpoint?.operationId) {\n\t\totelAttrs['endpoint.operation_id'] = attrs.endpoint.operationId;\n\t}\n\tif (attrs.endpoint?.tags?.length) {\n\t\totelAttrs['endpoint.tags'] = attrs.endpoint.tags;\n\t}\n\n\t// User attributes\n\tif (attrs.user?.userId) otelAttrs['enduser.id'] = attrs.user.userId;\n\tif (attrs.user?.sessionId) otelAttrs['session.id'] = attrs.user.sessionId;\n\tif (attrs.user?.roles?.length)\n\t\totelAttrs['enduser.role'] = attrs.user.roles.join(',');\n\n\t// Custom attributes\n\tif (attrs.custom) {\n\t\tObject.assign(otelAttrs, attrs.custom);\n\t}\n\n\treturn otelAttrs;\n}\n\n/**\n * Extract trace context from HTTP headers\n */\nexport function extractTraceContext(\n\theaders: Record<string, string | string[] | undefined>,\n): Context {\n\t// Normalize headers to Record<string, string>\n\tconst normalizedHeaders: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (value) {\n\t\t\tconst stringValue = Array.isArray(value) ? value[0] : value;\n\t\t\tif (stringValue) {\n\t\t\t\tnormalizedHeaders[key.toLowerCase()] = stringValue;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn propagation.extract(context.active(), normalizedHeaders);\n}\n\n/** Where a request's span goes: under a parent, or a new root with links. */\nexport interface IncomingTraceContext {\n\t/** The context the request span starts in. */\n\tparent: Context;\n\t/** The untrusted caller's context, linked rather than continued. */\n\tlinks: Link[];\n}\n\n/**\n * The trace a request's span belongs to.\n *\n * From a trusted caller (see `./trust`) its `traceparent` is continued, and\n * the span is its child. From anyone else the span starts a new trace — no\n * parent, no baggage — with a link to the context the caller claimed, so the\n * hop can still be followed without the caller choosing this trace.\n */\nexport function incomingTraceContext(\n\theaders: Record<string, string | string[] | undefined>,\n\ttrusted: boolean,\n): IncomingTraceContext {\n\tconst extracted = extractTraceContext(headers);\n\tif (trusted) return { parent: extracted, links: [] };\n\n\tconst claimed = trace.getSpanContext(extracted);\n\treturn {\n\t\tparent: trace.deleteSpan(context.active()),\n\t\tlinks:\n\t\t\tclaimed && isSpanContextValid(claimed)\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontext: claimed,\n\t\t\t\t\t\t\tattributes: { 'gkm.trace.untrusted_parent': true },\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [],\n\t};\n}\n\n/**\n * Inject trace context into HTTP headers\n */\nexport function injectTraceContext(\n\theaders: Record<string, string>,\n\tctx?: Context,\n): Record<string, string> {\n\tconst result = { ...headers };\n\tpropagation.inject(ctx ?? context.active(), result);\n\treturn result;\n}\n\n/**\n * Create an HTTP server span for an incoming request\n */\nexport function createHttpServerSpan(\n\tattrs: HttpSpanAttributes,\n\tparentContext?: Context,\n\tlinks?: Link[],\n): Span {\n\tconst tracer = getConstructsTracer();\n\tconst spanName = attrs.route\n\t\t? `${attrs.method} ${attrs.route}`\n\t\t: `${attrs.method} ${attrs.path || '/'}`;\n\n\tconst ctx = parentContext ?? context.active();\n\n\treturn tracer.startSpan(\n\t\tspanName,\n\t\t{\n\t\t\tkind: SpanKind.SERVER,\n\t\t\tattributes: toOtelAttributes(attrs),\n\t\t\t...(links?.length ? { links } : {}),\n\t\t},\n\t\tctx,\n\t);\n}\n\n/**\n * End an HTTP span with response data\n */\nexport function endHttpSpan(\n\tspan: Span,\n\tresponse: HttpResponseAttributes,\n\terror?: Error,\n): void {\n\tspan.setAttribute('http.response.status_code', response.statusCode);\n\n\tif (response.responseSize) {\n\t\tspan.setAttribute('http.response.body.size', response.responseSize);\n\t}\n\n\tif (error) {\n\t\tspan.recordException(error);\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error.message,\n\t\t});\n\t} else if (response.statusCode >= 500) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: `HTTP ${response.statusCode}`,\n\t\t});\n\t} else if (response.statusCode < 400) {\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t}\n\t// A 4xx leaves the status unset: on a server span it is the client's\n\t// mistake, not the server's failure (HTTP semantic conventions).\n\n\tspan.end();\n}\n\n/**\n * Execute a function within an HTTP server span context\n *\n * @example\n * ```typescript\n * const result = await withHttpSpan(\n * { method: 'GET', path: '/users', route: '/users' },\n * async (span) => {\n * const users = await getUsers();\n * return { statusCode: 200, body: users };\n * }\n * );\n * ```\n */\nexport async function withHttpSpan<T>(\n\tattrs: HttpSpanAttributes,\n\tfn: (span: Span) => Promise<T & { statusCode?: number }>,\n\tparentContext?: Context,\n): Promise<T> {\n\tconst span = createHttpServerSpan(attrs, parentContext);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(parentContext ?? context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\n\t\tconst statusCode = (result as { statusCode?: number }).statusCode ?? 200;\n\t\tendHttpSpan(span, { statusCode });\n\n\t\treturn result;\n\t} catch (error) {\n\t\tendHttpSpan(\n\t\t\tspan,\n\t\t\t{ statusCode: 500 },\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t}\n}\n\n/**\n * Create a child span for internal operations (middleware, handlers, etc.)\n */\nexport function createChildSpan(name: string, attributes?: Attributes): Span {\n\tconst tracer = getConstructsTracer();\n\treturn tracer.startSpan(name, {\n\t\tkind: SpanKind.INTERNAL,\n\t\tattributes,\n\t});\n}\n\n/**\n * Execute a function within a child span\n */\nexport async function withChildSpan<T>(\n\tname: string,\n\tfn: (span: Span) => Promise<T>,\n\tattributes?: Attributes,\n): Promise<T> {\n\tconst span = createChildSpan(name, attributes);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t\treturn result;\n\t} catch (error) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error instanceof Error ? error.message : 'Unknown error',\n\t\t});\n\t\tspan.recordException(\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t} finally {\n\t\tspan.end();\n\t}\n}\n\n/**\n * Check if tracing is enabled (OpenTelemetry API is available and configured)\n */\nexport function isTracingEnabled(): boolean {\n\ttry {\n\t\tconst tracer = trace.getTracer(TRACER_NAME);\n\t\t// Check if we have a real tracer (not a no-op)\n\t\tconst span = tracer.startSpan('test');\n\t\tconst isReal =\n\t\t\tspan.spanContext().traceId !== '00000000000000000000000000000000';\n\t\tspan.end();\n\t\treturn isReal;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"mappings":";;;AAgEA,MAAM,cAAc;;;;AAKpB,SAAgB,sBAAsB;CACrC,OAAOA,mBAAAA,MAAM,UAAU,WAAW;AACnC;;;;AAKA,SAAgB,iBAAiB,OAAuC;CACvE,MAAM,YAAwB,CAAC;CAG/B,IAAI,MAAM,QAAQ,UAAU,yBAAyB,MAAM;CAC3D,IAAI,MAAM,KAAK,UAAU,cAAc,MAAM;CAC7C,IAAI,MAAM,MAAM,UAAU,cAAc,MAAM;CAC9C,IAAI,MAAM,OAAO,UAAU,gBAAgB,MAAM;CACjD,IAAI,MAAM,MAAM,UAAU,oBAAoB,MAAM;CACpD,IAAI,MAAM,QAAQ,UAAU,gBAAgB,MAAM;CAClD,IAAI,MAAM,WAAW,UAAU,yBAAyB,MAAM;CAC9D,IAAI,MAAM,UAAU,UAAU,oBAAoB,MAAM;CACxD,IAAI,MAAM,WAAW,UAAU,qBAAqB,MAAM;CAG1D,IAAI,MAAM,UAAU,YACnB,UAAU,+BAA+B,MAAM,SAAS;CAEzD,IAAI,MAAM,UAAU,cACnB,UAAU,6BAA6B,MAAM,SAAS;CAIvD,IAAI,MAAM,UAAU,MAAM,UAAU,mBAAmB,MAAM,SAAS;CACtE,IAAI,MAAM,UAAU,aACnB,UAAU,2BAA2B,MAAM,SAAS;CAErD,IAAI,MAAM,UAAU,MAAM,QACzB,UAAU,mBAAmB,MAAM,SAAS;CAI7C,IAAI,MAAM,MAAM,QAAQ,UAAU,gBAAgB,MAAM,KAAK;CAC7D,IAAI,MAAM,MAAM,WAAW,UAAU,gBAAgB,MAAM,KAAK;CAChE,IAAI,MAAM,MAAM,OAAO,QACtB,UAAU,kBAAkB,MAAM,KAAK,MAAM,KAAK,GAAG;CAGtD,IAAI,MAAM,QACT,OAAO,OAAO,WAAW,MAAM,MAAM;CAGtC,OAAO;AACR;;;;AAKA,SAAgB,oBACf,SACU;CAEV,MAAM,oBAA4C,CAAC;CACnD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,OAAO;EACV,MAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;EACtD,IAAI,aACH,kBAAkB,IAAI,YAAY,KAAK;CAEzC;CAGD,OAAOC,mBAAAA,YAAY,QAAQC,mBAAAA,QAAQ,OAAO,GAAG,iBAAiB;AAC/D;;;;;;;;;AAkBA,SAAgB,qBACf,SACA,SACuB;CACvB,MAAM,YAAY,oBAAoB,OAAO;CAC7C,IAAI,SAAS,OAAO;EAAE,QAAQ;EAAW,OAAO,CAAC;CAAE;CAEnD,MAAM,UAAUF,mBAAAA,MAAM,eAAe,SAAS;CAC9C,OAAO;EACN,QAAQA,mBAAAA,MAAM,WAAWE,mBAAAA,QAAQ,OAAO,CAAC;EACzC,OACC,YAAA,GAAWC,mBAAAA,mBAAAA,CAAmB,OAAO,IAClC,CACA;GACC,SAAS;GACT,YAAY,EAAE,8BAA8B,KAAK;EAClD,CACD,IACC,CAAC;CACN;AACD;;;;AAKA,SAAgB,mBACf,SACA,KACyB;CACzB,MAAM,SAAS,EAAE,GAAG,QAAQ;CAC5B,mBAAA,YAAY,OAAO,OAAOD,mBAAAA,QAAQ,OAAO,GAAG,MAAM;CAClD,OAAO;AACR;;;;AAKA,SAAgB,qBACf,OACA,eACA,OACO;CACP,MAAM,SAAS,oBAAoB;CACnC,MAAM,WAAW,MAAM,QACpB,GAAG,MAAM,OAAO,GAAG,MAAM,UACzB,GAAG,MAAM,OAAO,GAAG,MAAM,QAAQ;CAEpC,MAAM,MAAM,iBAAiBA,mBAAAA,QAAQ,OAAO;CAE5C,OAAO,OAAO,UACb,UACA;EACC,MAAME,mBAAAA,SAAS;EACf,YAAY,iBAAiB,KAAK;EAClC,GAAI,OAAO,SAAS,EAAE,MAAM,IAAI,CAAC;CAClC,GACA,GACD;AACD;;;;AAKA,SAAgB,YACf,MACA,UACA,OACO;CACP,KAAK,aAAa,6BAA6B,SAAS,UAAU;CAElE,IAAI,SAAS,cACZ,KAAK,aAAa,2BAA2B,SAAS,YAAY;CAGnE,IAAI,OAAO;EACV,KAAK,gBAAgB,KAAK;EAC1B,KAAK,UAAU;GACd,MAAMC,mBAAAA,eAAe;GACrB,SAAS,MAAM;EAChB,CAAC;CACF,OAAO,IAAI,SAAS,cAAc,KACjC,KAAK,UAAU;EACd,MAAMA,mBAAAA,eAAe;EACrB,SAAS,QAAQ,SAAS;CAC3B,CAAC;MACK,IAAI,SAAS,aAAa,KAChC,KAAK,UAAU,EAAE,MAAMA,mBAAAA,eAAe,GAAG,CAAC;CAK3C,KAAK,IAAI;AACV;;;;;;;;;;;;;;;AAgBA,eAAsB,aACrB,OACA,IACA,eACa;CACb,MAAM,OAAO,qBAAqB,OAAO,aAAa;CAEtD,IAAI;EACH,MAAM,SAAS,MAAMH,mBAAAA,QAAQ,KAC5BF,mBAAAA,MAAM,QAAQ,iBAAiBE,mBAAAA,QAAQ,OAAO,GAAG,IAAI,SAC/C,GAAG,IAAI,CACd;EAGA,YAAY,MAAM,EAAE,YADA,OAAmC,cAAc,IACtC,CAAC;EAEhC,OAAO;CACR,SAAS,OAAO;EACf,YACC,MACA,EAAE,YAAY,IAAI,GAClB,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CACzD;EACA,MAAM;CACP;AACD;;;;AAKA,SAAgB,gBAAgB,MAAc,YAA+B;CAE5E,OADe,oBACH,CAAC,CAAC,UAAU,MAAM;EAC7B,MAAME,mBAAAA,SAAS;EACf;CACD,CAAC;AACF;;;;AAKA,eAAsB,cACrB,MACA,IACA,YACa;CACb,MAAM,OAAO,gBAAgB,MAAM,UAAU;CAE7C,IAAI;EACH,MAAM,SAAS,MAAMF,mBAAAA,QAAQ,KAC5BF,mBAAAA,MAAM,QAAQE,mBAAAA,QAAQ,OAAO,GAAG,IAAI,SAC9B,GAAG,IAAI,CACd;EACA,KAAK,UAAU,EAAE,MAAMG,mBAAAA,eAAe,GAAG,CAAC;EAC1C,OAAO;CACR,SAAS,OAAO;EACf,KAAK,UAAU;GACd,MAAMA,mBAAAA,eAAe;GACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU;EACnD,CAAC;EACD,KAAK,gBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CACzD;EACA,MAAM;CACP,UAAU;EACT,KAAK,IAAI;CACV;AACD;;;;AAKA,SAAgB,mBAA4B;CAC3C,IAAI;EAGH,MAAM,OAFSL,mBAAAA,MAAM,UAAU,WAEb,CAAC,CAAC,UAAU,MAAM;EACpC,MAAM,SACL,KAAK,YAAY,CAAC,CAAC,YAAY;EAChC,KAAK,IAAI;EACT,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Attributes, Context, Span } from "@opentelemetry/api";
|
|
1
|
+
import { Attributes, Context, Link, Span } from "@opentelemetry/api";
|
|
2
2
|
//#region src/instrumentation/http.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* HTTP request attributes following OpenTelemetry semantic conventions
|
|
@@ -58,6 +58,22 @@ export declare function toOtelAttributes(attrs: HttpSpanAttributes): Attributes;
|
|
|
58
58
|
* Extract trace context from HTTP headers
|
|
59
59
|
*/
|
|
60
60
|
export declare function extractTraceContext(headers: Record<string, string | string[] | undefined>): Context;
|
|
61
|
+
/** Where a request's span goes: under a parent, or a new root with links. */
|
|
62
|
+
export interface IncomingTraceContext {
|
|
63
|
+
/** The context the request span starts in. */
|
|
64
|
+
parent: Context;
|
|
65
|
+
/** The untrusted caller's context, linked rather than continued. */
|
|
66
|
+
links: Link[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The trace a request's span belongs to.
|
|
70
|
+
*
|
|
71
|
+
* From a trusted caller (see `./trust`) its `traceparent` is continued, and
|
|
72
|
+
* the span is its child. From anyone else the span starts a new trace — no
|
|
73
|
+
* parent, no baggage — with a link to the context the caller claimed, so the
|
|
74
|
+
* hop can still be followed without the caller choosing this trace.
|
|
75
|
+
*/
|
|
76
|
+
export declare function incomingTraceContext(headers: Record<string, string | string[] | undefined>, trusted: boolean): IncomingTraceContext;
|
|
61
77
|
/**
|
|
62
78
|
* Inject trace context into HTTP headers
|
|
63
79
|
*/
|
|
@@ -65,7 +81,7 @@ export declare function injectTraceContext(headers: Record<string, string>, ctx?
|
|
|
65
81
|
/**
|
|
66
82
|
* Create an HTTP server span for an incoming request
|
|
67
83
|
*/
|
|
68
|
-
export declare function createHttpServerSpan(attrs: HttpSpanAttributes, parentContext?: Context): Span;
|
|
84
|
+
export declare function createHttpServerSpan(attrs: HttpSpanAttributes, parentContext?: Context, links?: Link[]): Span;
|
|
69
85
|
/**
|
|
70
86
|
* End an HTTP span with response data
|
|
71
87
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.cts","names":[],"sources":["../../src/instrumentation/http.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"http.d.cts","names":[],"sources":["../../src/instrumentation/http.ts"],"mappings":";;;;;iBAgBiB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;iBAMgB;EAChB;EACA;;;;;iBAMgB;EAChB;EACA;EACA;;;;;iBAMgB;EAChB;EACA;EACA;;;;;iBAMgB,2BAA2B;EAC3C,WAAW;EACX,WAAW;EACX,OAAO;EACP,SAAS;;;;;wBAQM,oDAAmB;;;;wBAOnB,iBAAiB,OAAO,qBAAqB;;;;wBAgD7C,oBACf,SAAS,gDACP;;iBAgBc;;EAEhB,QAAQ;;EAER,OAAO;;;;;;;;;;wBAWQ,qBACf,SAAS,+CACT,mBACE;;;;wBAsBa,mBACf,SAAS,wBACT,MAAM,UACJ;;;;wBASa,qBACf,OAAO,oBACP,gBAAgB,SAChB,QAAQ,SACN;;;;wBAsBa,YACf,MAAM,MACN,UAAU,wBACV,QAAQ;;;;;;;;;;;;;;;wBA0Ca,aAAa,GAClC,OAAO,oBACP,KAAK,MAAM,SAAS,QAAQ;EAAM;IAClC,gBAAgB,UACd,QAAQ;;;;wBA0BK,gBAAgB,cAAc,aAAa,aAAa;;;;wBAWlD,cAAc,GACnC,cACA,KAAK,MAAM,SAAS,QAAQ,IAC5B,aAAa,aACX,QAAQ;;;;wBA2BK"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Attributes, Context, Span } from "@opentelemetry/api";
|
|
1
|
+
import { Attributes, Context, Link, Span } from "@opentelemetry/api";
|
|
2
2
|
//#region src/instrumentation/http.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* HTTP request attributes following OpenTelemetry semantic conventions
|
|
@@ -58,6 +58,22 @@ export declare function toOtelAttributes(attrs: HttpSpanAttributes): Attributes;
|
|
|
58
58
|
* Extract trace context from HTTP headers
|
|
59
59
|
*/
|
|
60
60
|
export declare function extractTraceContext(headers: Record<string, string | string[] | undefined>): Context;
|
|
61
|
+
/** Where a request's span goes: under a parent, or a new root with links. */
|
|
62
|
+
export interface IncomingTraceContext {
|
|
63
|
+
/** The context the request span starts in. */
|
|
64
|
+
parent: Context;
|
|
65
|
+
/** The untrusted caller's context, linked rather than continued. */
|
|
66
|
+
links: Link[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The trace a request's span belongs to.
|
|
70
|
+
*
|
|
71
|
+
* From a trusted caller (see `./trust`) its `traceparent` is continued, and
|
|
72
|
+
* the span is its child. From anyone else the span starts a new trace — no
|
|
73
|
+
* parent, no baggage — with a link to the context the caller claimed, so the
|
|
74
|
+
* hop can still be followed without the caller choosing this trace.
|
|
75
|
+
*/
|
|
76
|
+
export declare function incomingTraceContext(headers: Record<string, string | string[] | undefined>, trusted: boolean): IncomingTraceContext;
|
|
61
77
|
/**
|
|
62
78
|
* Inject trace context into HTTP headers
|
|
63
79
|
*/
|
|
@@ -65,7 +81,7 @@ export declare function injectTraceContext(headers: Record<string, string>, ctx?
|
|
|
65
81
|
/**
|
|
66
82
|
* Create an HTTP server span for an incoming request
|
|
67
83
|
*/
|
|
68
|
-
export declare function createHttpServerSpan(attrs: HttpSpanAttributes, parentContext?: Context): Span;
|
|
84
|
+
export declare function createHttpServerSpan(attrs: HttpSpanAttributes, parentContext?: Context, links?: Link[]): Span;
|
|
69
85
|
/**
|
|
70
86
|
* End an HTTP span with response data
|
|
71
87
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.mts","names":[],"sources":["../../src/instrumentation/http.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"http.d.mts","names":[],"sources":["../../src/instrumentation/http.ts"],"mappings":";;;;;iBAgBiB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;iBAMgB;EAChB;EACA;;;;;iBAMgB;EAChB;EACA;EACA;;;;;iBAMgB;EAChB;EACA;EACA;;;;;iBAMgB,2BAA2B;EAC3C,WAAW;EACX,WAAW;EACX,OAAO;EACP,SAAS;;;;;wBAQM,oDAAmB;;;;wBAOnB,iBAAiB,OAAO,qBAAqB;;;;wBAgD7C,oBACf,SAAS,gDACP;;iBAgBc;;EAEhB,QAAQ;;EAER,OAAO;;;;;;;;;;wBAWQ,qBACf,SAAS,+CACT,mBACE;;;;wBAsBa,mBACf,SAAS,wBACT,MAAM,UACJ;;;;wBASa,qBACf,OAAO,oBACP,gBAAgB,SAChB,QAAQ,SACN;;;;wBAsBa,YACf,MAAM,MACN,UAAU,wBACV,QAAQ;;;;;;;;;;;;;;;wBA0Ca,aAAa,GAClC,OAAO,oBACP,KAAK,MAAM,SAAS,QAAQ;EAAM;IAClC,gBAAgB,UACd,QAAQ;;;;wBA0BK,gBAAgB,cAAc,aAAa,aAAa;;;;wBAWlD,cAAc,GACnC,cACA,KAAK,MAAM,SAAS,QAAQ,IAC5B,aAAa,aACX,QAAQ;;;;wBA2BK"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
1
|
+
import { SpanKind, SpanStatusCode, context, isSpanContextValid, propagation, trace } from "@opentelemetry/api";
|
|
2
2
|
//#region src/instrumentation/http.ts
|
|
3
3
|
const TRACER_NAME = "@geekmidas/constructs";
|
|
4
4
|
/**
|
|
@@ -44,6 +44,29 @@ function extractTraceContext(headers) {
|
|
|
44
44
|
return propagation.extract(context.active(), normalizedHeaders);
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
+
* The trace a request's span belongs to.
|
|
48
|
+
*
|
|
49
|
+
* From a trusted caller (see `./trust`) its `traceparent` is continued, and
|
|
50
|
+
* the span is its child. From anyone else the span starts a new trace — no
|
|
51
|
+
* parent, no baggage — with a link to the context the caller claimed, so the
|
|
52
|
+
* hop can still be followed without the caller choosing this trace.
|
|
53
|
+
*/
|
|
54
|
+
function incomingTraceContext(headers, trusted) {
|
|
55
|
+
const extracted = extractTraceContext(headers);
|
|
56
|
+
if (trusted) return {
|
|
57
|
+
parent: extracted,
|
|
58
|
+
links: []
|
|
59
|
+
};
|
|
60
|
+
const claimed = trace.getSpanContext(extracted);
|
|
61
|
+
return {
|
|
62
|
+
parent: trace.deleteSpan(context.active()),
|
|
63
|
+
links: claimed && isSpanContextValid(claimed) ? [{
|
|
64
|
+
context: claimed,
|
|
65
|
+
attributes: { "gkm.trace.untrusted_parent": true }
|
|
66
|
+
}] : []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
47
70
|
* Inject trace context into HTTP headers
|
|
48
71
|
*/
|
|
49
72
|
function injectTraceContext(headers, ctx) {
|
|
@@ -54,13 +77,14 @@ function injectTraceContext(headers, ctx) {
|
|
|
54
77
|
/**
|
|
55
78
|
* Create an HTTP server span for an incoming request
|
|
56
79
|
*/
|
|
57
|
-
function createHttpServerSpan(attrs, parentContext) {
|
|
80
|
+
function createHttpServerSpan(attrs, parentContext, links) {
|
|
58
81
|
const tracer = getConstructsTracer();
|
|
59
82
|
const spanName = attrs.route ? `${attrs.method} ${attrs.route}` : `${attrs.method} ${attrs.path || "/"}`;
|
|
60
83
|
const ctx = parentContext ?? context.active();
|
|
61
84
|
return tracer.startSpan(spanName, {
|
|
62
85
|
kind: SpanKind.SERVER,
|
|
63
|
-
attributes: toOtelAttributes(attrs)
|
|
86
|
+
attributes: toOtelAttributes(attrs),
|
|
87
|
+
...links?.length ? { links } : {}
|
|
64
88
|
}, ctx);
|
|
65
89
|
}
|
|
66
90
|
/**
|
|
@@ -150,6 +174,6 @@ function isTracingEnabled() {
|
|
|
150
174
|
}
|
|
151
175
|
}
|
|
152
176
|
//#endregion
|
|
153
|
-
export { createChildSpan, createHttpServerSpan, endHttpSpan, extractTraceContext, getConstructsTracer, injectTraceContext, isTracingEnabled, toOtelAttributes, withChildSpan, withHttpSpan };
|
|
177
|
+
export { createChildSpan, createHttpServerSpan, endHttpSpan, extractTraceContext, getConstructsTracer, incomingTraceContext, injectTraceContext, isTracingEnabled, toOtelAttributes, withChildSpan, withHttpSpan };
|
|
154
178
|
|
|
155
179
|
//# sourceMappingURL=http.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.mjs","names":[],"sources":["../../src/instrumentation/http.ts"],"sourcesContent":["import {\n\ttype Attributes,\n\ttype Context,\n\tcontext,\n\tpropagation,\n\ttype Span,\n\tSpanKind,\n\tSpanStatusCode,\n\ttrace,\n} from '@opentelemetry/api';\n\n/**\n * HTTP request attributes following OpenTelemetry semantic conventions\n */\nexport interface HttpRequestAttributes {\n\tmethod: string;\n\turl?: string;\n\tpath?: string;\n\troute?: string;\n\thost?: string;\n\tscheme?: 'http' | 'https';\n\tuserAgent?: string;\n\tclientIp?: string;\n\trequestId?: string;\n}\n\n/**\n * HTTP response attributes\n */\nexport interface HttpResponseAttributes {\n\tstatusCode: number;\n\tresponseSize?: number;\n}\n\n/**\n * Endpoint-specific attributes\n */\nexport interface EndpointAttributes {\n\tname?: string;\n\toperationId?: string;\n\ttags?: string[];\n}\n\n/**\n * User/session attributes\n */\nexport interface UserAttributes {\n\tuserId?: string;\n\tsessionId?: string;\n\troles?: string[];\n}\n\n/**\n * Full span attributes for HTTP requests\n */\nexport interface HttpSpanAttributes extends HttpRequestAttributes {\n\tresponse?: HttpResponseAttributes;\n\tendpoint?: EndpointAttributes;\n\tuser?: UserAttributes;\n\tcustom?: Attributes;\n}\n\nconst TRACER_NAME = '@geekmidas/constructs';\n\n/**\n * Get or create a tracer for constructs instrumentation\n */\nexport function getConstructsTracer() {\n\treturn trace.getTracer(TRACER_NAME);\n}\n\n/**\n * Convert HTTP attributes to OpenTelemetry semantic conventions\n */\nexport function toOtelAttributes(attrs: HttpSpanAttributes): Attributes {\n\tconst otelAttrs: Attributes = {};\n\n\t// HTTP request attributes (OpenTelemetry semantic conventions)\n\tif (attrs.method) otelAttrs['http.request.method'] = attrs.method;\n\tif (attrs.url) otelAttrs['url.full'] = attrs.url;\n\tif (attrs.path) otelAttrs['url.path'] = attrs.path;\n\tif (attrs.route) otelAttrs['http.route'] = attrs.route;\n\tif (attrs.host) otelAttrs['server.address'] = attrs.host;\n\tif (attrs.scheme) otelAttrs['url.scheme'] = attrs.scheme;\n\tif (attrs.userAgent) otelAttrs['user_agent.original'] = attrs.userAgent;\n\tif (attrs.clientIp) otelAttrs['client.address'] = attrs.clientIp;\n\tif (attrs.requestId) otelAttrs['http.request.id'] = attrs.requestId;\n\n\t// HTTP response attributes\n\tif (attrs.response?.statusCode) {\n\t\totelAttrs['http.response.status_code'] = attrs.response.statusCode;\n\t}\n\tif (attrs.response?.responseSize) {\n\t\totelAttrs['http.response.body.size'] = attrs.response.responseSize;\n\t}\n\n\t// Endpoint attributes\n\tif (attrs.endpoint?.name) otelAttrs['endpoint.name'] = attrs.endpoint.name;\n\tif (attrs.endpoint?.operationId) {\n\t\totelAttrs['endpoint.operation_id'] = attrs.endpoint.operationId;\n\t}\n\tif (attrs.endpoint?.tags?.length) {\n\t\totelAttrs['endpoint.tags'] = attrs.endpoint.tags;\n\t}\n\n\t// User attributes\n\tif (attrs.user?.userId) otelAttrs['enduser.id'] = attrs.user.userId;\n\tif (attrs.user?.sessionId) otelAttrs['session.id'] = attrs.user.sessionId;\n\tif (attrs.user?.roles?.length)\n\t\totelAttrs['enduser.role'] = attrs.user.roles.join(',');\n\n\t// Custom attributes\n\tif (attrs.custom) {\n\t\tObject.assign(otelAttrs, attrs.custom);\n\t}\n\n\treturn otelAttrs;\n}\n\n/**\n * Extract trace context from HTTP headers\n */\nexport function extractTraceContext(\n\theaders: Record<string, string | string[] | undefined>,\n): Context {\n\t// Normalize headers to Record<string, string>\n\tconst normalizedHeaders: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (value) {\n\t\t\tconst stringValue = Array.isArray(value) ? value[0] : value;\n\t\t\tif (stringValue) {\n\t\t\t\tnormalizedHeaders[key.toLowerCase()] = stringValue;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn propagation.extract(context.active(), normalizedHeaders);\n}\n\n/**\n * Inject trace context into HTTP headers\n */\nexport function injectTraceContext(\n\theaders: Record<string, string>,\n\tctx?: Context,\n): Record<string, string> {\n\tconst result = { ...headers };\n\tpropagation.inject(ctx ?? context.active(), result);\n\treturn result;\n}\n\n/**\n * Create an HTTP server span for an incoming request\n */\nexport function createHttpServerSpan(\n\tattrs: HttpSpanAttributes,\n\tparentContext?: Context,\n): Span {\n\tconst tracer = getConstructsTracer();\n\tconst spanName = attrs.route\n\t\t? `${attrs.method} ${attrs.route}`\n\t\t: `${attrs.method} ${attrs.path || '/'}`;\n\n\tconst ctx = parentContext ?? context.active();\n\n\treturn tracer.startSpan(\n\t\tspanName,\n\t\t{\n\t\t\tkind: SpanKind.SERVER,\n\t\t\tattributes: toOtelAttributes(attrs),\n\t\t},\n\t\tctx,\n\t);\n}\n\n/**\n * End an HTTP span with response data\n */\nexport function endHttpSpan(\n\tspan: Span,\n\tresponse: HttpResponseAttributes,\n\terror?: Error,\n): void {\n\tspan.setAttribute('http.response.status_code', response.statusCode);\n\n\tif (response.responseSize) {\n\t\tspan.setAttribute('http.response.body.size', response.responseSize);\n\t}\n\n\tif (error) {\n\t\tspan.recordException(error);\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error.message,\n\t\t});\n\t} else if (response.statusCode >= 500) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: `HTTP ${response.statusCode}`,\n\t\t});\n\t} else if (response.statusCode < 400) {\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t}\n\t// A 4xx leaves the status unset: on a server span it is the client's\n\t// mistake, not the server's failure (HTTP semantic conventions).\n\n\tspan.end();\n}\n\n/**\n * Execute a function within an HTTP server span context\n *\n * @example\n * ```typescript\n * const result = await withHttpSpan(\n * { method: 'GET', path: '/users', route: '/users' },\n * async (span) => {\n * const users = await getUsers();\n * return { statusCode: 200, body: users };\n * }\n * );\n * ```\n */\nexport async function withHttpSpan<T>(\n\tattrs: HttpSpanAttributes,\n\tfn: (span: Span) => Promise<T & { statusCode?: number }>,\n\tparentContext?: Context,\n): Promise<T> {\n\tconst span = createHttpServerSpan(attrs, parentContext);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(parentContext ?? context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\n\t\tconst statusCode = (result as { statusCode?: number }).statusCode ?? 200;\n\t\tendHttpSpan(span, { statusCode });\n\n\t\treturn result;\n\t} catch (error) {\n\t\tendHttpSpan(\n\t\t\tspan,\n\t\t\t{ statusCode: 500 },\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t}\n}\n\n/**\n * Create a child span for internal operations (middleware, handlers, etc.)\n */\nexport function createChildSpan(name: string, attributes?: Attributes): Span {\n\tconst tracer = getConstructsTracer();\n\treturn tracer.startSpan(name, {\n\t\tkind: SpanKind.INTERNAL,\n\t\tattributes,\n\t});\n}\n\n/**\n * Execute a function within a child span\n */\nexport async function withChildSpan<T>(\n\tname: string,\n\tfn: (span: Span) => Promise<T>,\n\tattributes?: Attributes,\n): Promise<T> {\n\tconst span = createChildSpan(name, attributes);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t\treturn result;\n\t} catch (error) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error instanceof Error ? error.message : 'Unknown error',\n\t\t});\n\t\tspan.recordException(\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t} finally {\n\t\tspan.end();\n\t}\n}\n\n/**\n * Check if tracing is enabled (OpenTelemetry API is available and configured)\n */\nexport function isTracingEnabled(): boolean {\n\ttry {\n\t\tconst tracer = trace.getTracer(TRACER_NAME);\n\t\t// Check if we have a real tracer (not a no-op)\n\t\tconst span = tracer.startSpan('test');\n\t\tconst isReal =\n\t\t\tspan.spanContext().traceId !== '00000000000000000000000000000000';\n\t\tspan.end();\n\t\treturn isReal;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.mjs","names":[],"sources":["../../src/instrumentation/http.ts"],"sourcesContent":["import {\n\ttype Attributes,\n\ttype Context,\n\tcontext,\n\tisSpanContextValid,\n\ttype Link,\n\tpropagation,\n\ttype Span,\n\tSpanKind,\n\tSpanStatusCode,\n\ttrace,\n} from '@opentelemetry/api';\n\n/**\n * HTTP request attributes following OpenTelemetry semantic conventions\n */\nexport interface HttpRequestAttributes {\n\tmethod: string;\n\turl?: string;\n\tpath?: string;\n\troute?: string;\n\thost?: string;\n\tscheme?: 'http' | 'https';\n\tuserAgent?: string;\n\tclientIp?: string;\n\trequestId?: string;\n}\n\n/**\n * HTTP response attributes\n */\nexport interface HttpResponseAttributes {\n\tstatusCode: number;\n\tresponseSize?: number;\n}\n\n/**\n * Endpoint-specific attributes\n */\nexport interface EndpointAttributes {\n\tname?: string;\n\toperationId?: string;\n\ttags?: string[];\n}\n\n/**\n * User/session attributes\n */\nexport interface UserAttributes {\n\tuserId?: string;\n\tsessionId?: string;\n\troles?: string[];\n}\n\n/**\n * Full span attributes for HTTP requests\n */\nexport interface HttpSpanAttributes extends HttpRequestAttributes {\n\tresponse?: HttpResponseAttributes;\n\tendpoint?: EndpointAttributes;\n\tuser?: UserAttributes;\n\tcustom?: Attributes;\n}\n\nconst TRACER_NAME = '@geekmidas/constructs';\n\n/**\n * Get or create a tracer for constructs instrumentation\n */\nexport function getConstructsTracer() {\n\treturn trace.getTracer(TRACER_NAME);\n}\n\n/**\n * Convert HTTP attributes to OpenTelemetry semantic conventions\n */\nexport function toOtelAttributes(attrs: HttpSpanAttributes): Attributes {\n\tconst otelAttrs: Attributes = {};\n\n\t// HTTP request attributes (OpenTelemetry semantic conventions)\n\tif (attrs.method) otelAttrs['http.request.method'] = attrs.method;\n\tif (attrs.url) otelAttrs['url.full'] = attrs.url;\n\tif (attrs.path) otelAttrs['url.path'] = attrs.path;\n\tif (attrs.route) otelAttrs['http.route'] = attrs.route;\n\tif (attrs.host) otelAttrs['server.address'] = attrs.host;\n\tif (attrs.scheme) otelAttrs['url.scheme'] = attrs.scheme;\n\tif (attrs.userAgent) otelAttrs['user_agent.original'] = attrs.userAgent;\n\tif (attrs.clientIp) otelAttrs['client.address'] = attrs.clientIp;\n\tif (attrs.requestId) otelAttrs['http.request.id'] = attrs.requestId;\n\n\t// HTTP response attributes\n\tif (attrs.response?.statusCode) {\n\t\totelAttrs['http.response.status_code'] = attrs.response.statusCode;\n\t}\n\tif (attrs.response?.responseSize) {\n\t\totelAttrs['http.response.body.size'] = attrs.response.responseSize;\n\t}\n\n\t// Endpoint attributes\n\tif (attrs.endpoint?.name) otelAttrs['endpoint.name'] = attrs.endpoint.name;\n\tif (attrs.endpoint?.operationId) {\n\t\totelAttrs['endpoint.operation_id'] = attrs.endpoint.operationId;\n\t}\n\tif (attrs.endpoint?.tags?.length) {\n\t\totelAttrs['endpoint.tags'] = attrs.endpoint.tags;\n\t}\n\n\t// User attributes\n\tif (attrs.user?.userId) otelAttrs['enduser.id'] = attrs.user.userId;\n\tif (attrs.user?.sessionId) otelAttrs['session.id'] = attrs.user.sessionId;\n\tif (attrs.user?.roles?.length)\n\t\totelAttrs['enduser.role'] = attrs.user.roles.join(',');\n\n\t// Custom attributes\n\tif (attrs.custom) {\n\t\tObject.assign(otelAttrs, attrs.custom);\n\t}\n\n\treturn otelAttrs;\n}\n\n/**\n * Extract trace context from HTTP headers\n */\nexport function extractTraceContext(\n\theaders: Record<string, string | string[] | undefined>,\n): Context {\n\t// Normalize headers to Record<string, string>\n\tconst normalizedHeaders: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (value) {\n\t\t\tconst stringValue = Array.isArray(value) ? value[0] : value;\n\t\t\tif (stringValue) {\n\t\t\t\tnormalizedHeaders[key.toLowerCase()] = stringValue;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn propagation.extract(context.active(), normalizedHeaders);\n}\n\n/** Where a request's span goes: under a parent, or a new root with links. */\nexport interface IncomingTraceContext {\n\t/** The context the request span starts in. */\n\tparent: Context;\n\t/** The untrusted caller's context, linked rather than continued. */\n\tlinks: Link[];\n}\n\n/**\n * The trace a request's span belongs to.\n *\n * From a trusted caller (see `./trust`) its `traceparent` is continued, and\n * the span is its child. From anyone else the span starts a new trace — no\n * parent, no baggage — with a link to the context the caller claimed, so the\n * hop can still be followed without the caller choosing this trace.\n */\nexport function incomingTraceContext(\n\theaders: Record<string, string | string[] | undefined>,\n\ttrusted: boolean,\n): IncomingTraceContext {\n\tconst extracted = extractTraceContext(headers);\n\tif (trusted) return { parent: extracted, links: [] };\n\n\tconst claimed = trace.getSpanContext(extracted);\n\treturn {\n\t\tparent: trace.deleteSpan(context.active()),\n\t\tlinks:\n\t\t\tclaimed && isSpanContextValid(claimed)\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontext: claimed,\n\t\t\t\t\t\t\tattributes: { 'gkm.trace.untrusted_parent': true },\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [],\n\t};\n}\n\n/**\n * Inject trace context into HTTP headers\n */\nexport function injectTraceContext(\n\theaders: Record<string, string>,\n\tctx?: Context,\n): Record<string, string> {\n\tconst result = { ...headers };\n\tpropagation.inject(ctx ?? context.active(), result);\n\treturn result;\n}\n\n/**\n * Create an HTTP server span for an incoming request\n */\nexport function createHttpServerSpan(\n\tattrs: HttpSpanAttributes,\n\tparentContext?: Context,\n\tlinks?: Link[],\n): Span {\n\tconst tracer = getConstructsTracer();\n\tconst spanName = attrs.route\n\t\t? `${attrs.method} ${attrs.route}`\n\t\t: `${attrs.method} ${attrs.path || '/'}`;\n\n\tconst ctx = parentContext ?? context.active();\n\n\treturn tracer.startSpan(\n\t\tspanName,\n\t\t{\n\t\t\tkind: SpanKind.SERVER,\n\t\t\tattributes: toOtelAttributes(attrs),\n\t\t\t...(links?.length ? { links } : {}),\n\t\t},\n\t\tctx,\n\t);\n}\n\n/**\n * End an HTTP span with response data\n */\nexport function endHttpSpan(\n\tspan: Span,\n\tresponse: HttpResponseAttributes,\n\terror?: Error,\n): void {\n\tspan.setAttribute('http.response.status_code', response.statusCode);\n\n\tif (response.responseSize) {\n\t\tspan.setAttribute('http.response.body.size', response.responseSize);\n\t}\n\n\tif (error) {\n\t\tspan.recordException(error);\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error.message,\n\t\t});\n\t} else if (response.statusCode >= 500) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: `HTTP ${response.statusCode}`,\n\t\t});\n\t} else if (response.statusCode < 400) {\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t}\n\t// A 4xx leaves the status unset: on a server span it is the client's\n\t// mistake, not the server's failure (HTTP semantic conventions).\n\n\tspan.end();\n}\n\n/**\n * Execute a function within an HTTP server span context\n *\n * @example\n * ```typescript\n * const result = await withHttpSpan(\n * { method: 'GET', path: '/users', route: '/users' },\n * async (span) => {\n * const users = await getUsers();\n * return { statusCode: 200, body: users };\n * }\n * );\n * ```\n */\nexport async function withHttpSpan<T>(\n\tattrs: HttpSpanAttributes,\n\tfn: (span: Span) => Promise<T & { statusCode?: number }>,\n\tparentContext?: Context,\n): Promise<T> {\n\tconst span = createHttpServerSpan(attrs, parentContext);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(parentContext ?? context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\n\t\tconst statusCode = (result as { statusCode?: number }).statusCode ?? 200;\n\t\tendHttpSpan(span, { statusCode });\n\n\t\treturn result;\n\t} catch (error) {\n\t\tendHttpSpan(\n\t\t\tspan,\n\t\t\t{ statusCode: 500 },\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t}\n}\n\n/**\n * Create a child span for internal operations (middleware, handlers, etc.)\n */\nexport function createChildSpan(name: string, attributes?: Attributes): Span {\n\tconst tracer = getConstructsTracer();\n\treturn tracer.startSpan(name, {\n\t\tkind: SpanKind.INTERNAL,\n\t\tattributes,\n\t});\n}\n\n/**\n * Execute a function within a child span\n */\nexport async function withChildSpan<T>(\n\tname: string,\n\tfn: (span: Span) => Promise<T>,\n\tattributes?: Attributes,\n): Promise<T> {\n\tconst span = createChildSpan(name, attributes);\n\n\ttry {\n\t\tconst result = await context.with(\n\t\t\ttrace.setSpan(context.active(), span),\n\t\t\t() => fn(span),\n\t\t);\n\t\tspan.setStatus({ code: SpanStatusCode.OK });\n\t\treturn result;\n\t} catch (error) {\n\t\tspan.setStatus({\n\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\tmessage: error instanceof Error ? error.message : 'Unknown error',\n\t\t});\n\t\tspan.recordException(\n\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t);\n\t\tthrow error;\n\t} finally {\n\t\tspan.end();\n\t}\n}\n\n/**\n * Check if tracing is enabled (OpenTelemetry API is available and configured)\n */\nexport function isTracingEnabled(): boolean {\n\ttry {\n\t\tconst tracer = trace.getTracer(TRACER_NAME);\n\t\t// Check if we have a real tracer (not a no-op)\n\t\tconst span = tracer.startSpan('test');\n\t\tconst isReal =\n\t\t\tspan.spanContext().traceId !== '00000000000000000000000000000000';\n\t\tspan.end();\n\t\treturn isReal;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}
|