UNPKG

autotel

Version:
1 lines 17.2 kB
{"version":3,"file":"http.cjs","names":["getConfig","SpanStatusCode","getActiveContextWithBaggage","propagation","context"],"sources":["../src/http.ts"],"sourcesContent":["/**\n * HTTP Instrumentation Helpers\n *\n * Optional import: Not included in main bundle\n * Import from: 'autotel/http'\n *\n * Provides decorators and utilities for HTTP client instrumentation.\n * Works with fetch, axios, and other HTTP clients.\n *\n * @example\n * ```typescript\n * import { HttpInstrumented } from 'autotel/http'\n *\n * @HttpInstrumented()\n * class ApiClient {\n * async getUser(id: string) {\n * return fetch(`/api/users/${id}`)\n * }\n * }\n * ```\n */\n\nimport { SpanStatusCode, context, propagation } from '@opentelemetry/api';\nimport { getConfig } from './config';\nimport { getActiveContextWithBaggage } from './trace-context';\n\nexport interface HttpInstrumentedOptions {\n /** Service name for HTTP calls (default: 'http-client') */\n serviceName?: string;\n /** Extract URL from method arguments (default: first arg) */\n urlExtractor?: (args: unknown[]) => string | undefined;\n /** Extract HTTP method from method name or args */\n methodExtractor?: (methodName: string, args: unknown[]) => string;\n /** Add custom attributes to spans */\n attributesFromArgs?: (args: unknown[]) => Record<string, string | number>;\n /** Slow request threshold in milliseconds (adds warning attribute) - default: 3000ms */\n slowRequestThresholdMs?: number;\n}\n\n/**\n * Decorator for auto-instrumenting HTTP client methods\n *\n * @example Basic usage\n * ```typescript\n * @HttpInstrumented()\n * class ApiClient {\n * async fetchUser(userId: string) {\n * const res = await fetch(`https://api.example.com/users/${userId}`)\n * return res.json()\n * }\n *\n * async createOrder(order: Order) {\n * const res = await fetch('https://api.example.com/orders', {\n * method: 'POST',\n * body: JSON.stringify(order)\n * })\n * return res.json()\n * }\n * }\n * ```\n *\n * @example Advanced usage with custom extractors\n * ```typescript\n * @HttpInstrumented({\n * serviceName: 'payment-gateway',\n * urlExtractor: (args) => {\n * const config = args[0] as RequestConfig\n * return config.url\n * },\n * attributesFromArgs: (args) => ({\n * 'http.request_id': args[0]?.requestId,\n * 'http.retry_count': args[0]?.retryCount || 0\n * })\n * })\n * class PaymentClient {\n * async charge(config: RequestConfig) {\n * return axios(config)\n * }\n * }\n * ```\n */\nexport function HttpInstrumented(options: HttpInstrumentedOptions = {}) {\n const serviceName = options.serviceName || 'http-client';\n const slowRequestThresholdMs = options.slowRequestThresholdMs ?? 3000;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type\n return function <T extends { new (...args: any[]): {} }>(\n target: T,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n _context: ClassDecoratorContext,\n ) {\n return class extends target {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n constructor(...args: any[]) {\n super(...args);\n\n const proto = target.prototype;\n const methodNames = Object.getOwnPropertyNames(proto).filter(\n (name) =>\n name !== 'constructor' &&\n typeof proto[name] === 'function' &&\n !name.startsWith('_'),\n );\n\n for (const methodName of methodNames) {\n const originalMethod = proto[methodName];\n\n if (\n originalMethod.constructor.name === 'AsyncFunction' ||\n originalMethod.toString().startsWith('async ')\n ) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const wrappedMethod = async (...args: any[]) => {\n const config = getConfig();\n const tracer = config.tracer;\n\n const url = options.urlExtractor\n ? options.urlExtractor(args)\n : (args[0] as string | undefined);\n\n const method = options.methodExtractor\n ? options.methodExtractor(methodName, args)\n : inferHttpMethod(methodName);\n\n const spanName = url\n ? `HTTP ${method} ${extractPath(url)}`\n : `HTTP ${method}`;\n\n return tracer.startActiveSpan(spanName, async (span) => {\n const startTime = performance.now();\n\n try {\n span.setAttributes({\n 'http.method': method,\n 'http.url': url || 'unknown',\n 'service.name': serviceName,\n 'operation.name': `${serviceName}.${methodName}`,\n });\n\n if (url) {\n const urlObj = parseUrl(url);\n span.setAttributes({\n 'http.scheme': urlObj.protocol,\n 'http.host': urlObj.host,\n 'http.target': urlObj.path,\n });\n }\n\n if (options.attributesFromArgs) {\n span.setAttributes(options.attributesFromArgs(args));\n }\n\n const result = await originalMethod.apply(this, args);\n\n const duration = performance.now() - startTime;\n\n // Extract status code from response\n const statusCode = extractStatusCode(result);\n if (statusCode) {\n span.setAttribute('http.status_code', statusCode);\n\n if (statusCode >= 400) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${statusCode}`,\n });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.setAttributes({\n 'http.duration_ms': duration,\n });\n\n // Mark slow requests for investigation\n if (duration > slowRequestThresholdMs) {\n span.setAttribute('http.slow_request', true);\n span.setAttribute(\n 'http.slow_request_threshold_ms',\n slowRequestThresholdMs,\n );\n }\n\n return result;\n } catch (error) {\n const duration = performance.now() - startTime;\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message:\n error instanceof Error ? error.message : 'Unknown error',\n });\n\n span.setAttributes({\n 'http.duration_ms': duration,\n 'error.type':\n error instanceof Error\n ? error.constructor.name\n : 'Unknown',\n 'error.message':\n error instanceof Error ? error.message : 'Unknown error',\n });\n\n throw error;\n } finally {\n span.end();\n }\n });\n };\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any)[methodName] = wrappedMethod;\n }\n }\n }\n };\n };\n}\n\n/**\n * Helper: Trace a single HTTP request\n *\n * @example\n * ```typescript\n * import { traceHttpRequest } from 'autotel/http'\n *\n * const data = await traceHttpRequest(\n * 'GET /api/users',\n * () => fetch('https://api.example.com/users')\n * )\n * ```\n */\nexport async function traceHttpRequest<T>(\n spanName: string,\n fn: () => Promise<T>,\n attributes?: Record<string, string | number>,\n): Promise<T> {\n const config = getConfig();\n const tracer = config.tracer;\n\n return tracer.startActiveSpan(spanName, async (span) => {\n try {\n if (attributes) {\n span.setAttributes(attributes);\n }\n\n const result = await fn();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n throw error;\n } finally {\n span.end();\n }\n });\n}\n\n// Helper functions\n\nfunction inferHttpMethod(methodName: string): string {\n const lower = methodName.toLowerCase();\n if (\n lower.includes('get') ||\n lower.includes('fetch') ||\n lower.includes('list')\n )\n return 'GET';\n if (lower.includes('post') || lower.includes('create')) return 'POST';\n if (lower.includes('put') || lower.includes('update')) return 'PUT';\n if (lower.includes('delete') || lower.includes('remove')) return 'DELETE';\n if (lower.includes('patch')) return 'PATCH';\n return 'GET'; // Default\n}\n\nfunction extractPath(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.pathname;\n } catch {\n // Relative URL or invalid\n return url.split('?')[0] || url;\n }\n}\n\nfunction parseUrl(url: string): {\n protocol: string;\n host: string;\n path: string;\n} {\n try {\n const urlObj = new URL(url);\n return {\n protocol: urlObj.protocol.replace(':', ''),\n host: urlObj.host,\n path: urlObj.pathname + urlObj.search,\n };\n } catch {\n return {\n protocol: 'http',\n host: 'unknown',\n path: url,\n };\n }\n}\n\nfunction extractStatusCode(result: unknown): number | undefined {\n if (result && typeof result === 'object') {\n // Check for Response.status (fetch API)\n if ('status' in result && typeof result.status === 'number') {\n return result.status;\n }\n // Check for statusCode (axios, node http)\n if ('statusCode' in result && typeof result.statusCode === 'number') {\n return result.statusCode;\n }\n }\n return undefined;\n}\n\n/**\n * Inject trace context into HTTP headers (for distributed tracing)\n *\n * This includes W3C Trace Context (traceparent, tracestate) and W3C Baggage headers.\n * Uses OpenTelemetry's propagation system for full compatibility.\n *\n * @example\n * ```typescript\n * import { injectTraceContext } from 'autotel/http'\n *\n * const headers = injectTraceContext({\n * 'Content-Type': 'application/json'\n * })\n *\n * fetch('/api/users', { headers })\n * ```\n *\n * @example With baggage\n * ```typescript\n * import { trace, withBaggage, injectTraceContext } from 'autotel'\n *\n * export const createOrder = trace((ctx) => async (order: Order) => {\n * return await withBaggage({\n * baggage: { 'tenant.id': order.tenantId },\n * fn: async () => {\n * const headers = injectTraceContext();\n * // Headers now include 'baggage' header with tenant.id\n * await fetch('/api/charge', { headers });\n * },\n * });\n * });\n * ```\n */\nexport function injectTraceContext(\n headers: Record<string, string> = {},\n): Record<string, string> {\n // Use getActiveContextWithBaggage to check stored context (from baggage setters)\n // This ensures ctx.setBaggage() changes are included in injected headers\n const currentContext = getActiveContextWithBaggage();\n\n // Use OpenTelemetry's propagation.inject for full W3C support\n // This includes traceparent, tracestate, and baggage headers\n propagation.inject(currentContext, headers);\n\n return headers;\n}\n\n/**\n * Extract trace context from HTTP headers (for distributed tracing)\n *\n * This extracts W3C Trace Context (traceparent, tracestate) and W3C Baggage headers.\n * Uses OpenTelemetry's propagation system for full compatibility.\n *\n * Returns a context that can be used with context.with() to run code\n * with the extracted trace context and baggage.\n *\n * @example\n * ```typescript\n * import { extractTraceContext, trace } from 'autotel'\n * import { context } from 'autotel'\n *\n * // In Express middleware\n * app.use((req, res, next) => {\n * const extractedContext = extractTraceContext(req.headers);\n * context.with(extractedContext, () => {\n * next();\n * });\n * });\n * ```\n *\n * @example In a traced function\n * ```typescript\n * export const handleWebhook = trace((ctx) => async (req: Request) => {\n * const extractedContext = extractTraceContext(req.headers);\n * return await context.with(extractedContext, async () => {\n * // Now ctx.getBaggage() will return baggage from the incoming request\n * const tenantId = ctx.getBaggage('tenant.id');\n * await processWebhook(req.body);\n * });\n * });\n * ```\n */\nexport function extractTraceContext(\n headers: Record<string, string | string[] | undefined>,\n): ReturnType<typeof context.active> {\n const carrier: Record<string, string> = {};\n\n // Convert headers to flat string format expected by propagation\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n carrier[key] = Array.isArray(value) ? (value[0] ?? '') : value;\n }\n }\n\n // Extract context using OpenTelemetry's propagation system\n // Returns a Context that can be used with context.with()\n return propagation.extract(context.active(), carrier);\n}\n"],"mappings":"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"}