autotel
Version:
Write Once, Observe Anywhere
299 lines (297 loc) • 9.63 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_config = require('./config.cjs');
const require_track = require('./track-Cb3Q4QmS.cjs');
let _opentelemetry_api = require("@opentelemetry/api");
//#region src/http.ts
/**
* HTTP Instrumentation Helpers
*
* Optional import: Not included in main bundle
* Import from: 'autotel/http'
*
* Provides decorators and utilities for HTTP client instrumentation.
* Works with fetch, axios, and other HTTP clients.
*
* @example
* ```typescript
* import { HttpInstrumented } from 'autotel/http'
*
* @HttpInstrumented()
* class ApiClient {
* async getUser(id: string) {
* return fetch(`/api/users/${id}`)
* }
* }
* ```
*/
/**
* Decorator for auto-instrumenting HTTP client methods
*
* @example Basic usage
* ```typescript
* @HttpInstrumented()
* class ApiClient {
* async fetchUser(userId: string) {
* const res = await fetch(`https://api.example.com/users/${userId}`)
* return res.json()
* }
*
* async createOrder(order: Order) {
* const res = await fetch('https://api.example.com/orders', {
* method: 'POST',
* body: JSON.stringify(order)
* })
* return res.json()
* }
* }
* ```
*
* @example Advanced usage with custom extractors
* ```typescript
* @HttpInstrumented({
* serviceName: 'payment-gateway',
* urlExtractor: (args) => {
* const config = args[0] as RequestConfig
* return config.url
* },
* attributesFromArgs: (args) => ({
* 'http.request_id': args[0]?.requestId,
* 'http.retry_count': args[0]?.retryCount || 0
* })
* })
* class PaymentClient {
* async charge(config: RequestConfig) {
* return axios(config)
* }
* }
* ```
*/
function HttpInstrumented(options = {}) {
const serviceName = options.serviceName || "http-client";
const slowRequestThresholdMs = options.slowRequestThresholdMs ?? 3e3;
return function(target, _context) {
return class extends target {
constructor(...args) {
super(...args);
const proto = target.prototype;
const methodNames = Object.getOwnPropertyNames(proto).filter((name) => name !== "constructor" && typeof proto[name] === "function" && !name.startsWith("_"));
for (const methodName of methodNames) {
const originalMethod = proto[methodName];
if (originalMethod.constructor.name === "AsyncFunction" || originalMethod.toString().startsWith("async ")) {
const wrappedMethod = async (...args) => {
const tracer = require_config.getConfig().tracer;
const url = options.urlExtractor ? options.urlExtractor(args) : args[0];
const method = options.methodExtractor ? options.methodExtractor(methodName, args) : inferHttpMethod(methodName);
const spanName = url ? `HTTP ${method} ${extractPath(url)}` : `HTTP ${method}`;
return tracer.startActiveSpan(spanName, async (span) => {
const startTime = performance.now();
try {
span.setAttributes({
"http.method": method,
"http.url": url || "unknown",
"service.name": serviceName,
"operation.name": `${serviceName}.${methodName}`
});
if (url) {
const urlObj = parseUrl(url);
span.setAttributes({
"http.scheme": urlObj.protocol,
"http.host": urlObj.host,
"http.target": urlObj.path
});
}
if (options.attributesFromArgs) span.setAttributes(options.attributesFromArgs(args));
const result = await originalMethod.apply(this, args);
const duration = performance.now() - startTime;
const statusCode = extractStatusCode(result);
if (statusCode) {
span.setAttribute("http.status_code", statusCode);
if (statusCode >= 400) span.setStatus({
code: _opentelemetry_api.SpanStatusCode.ERROR,
message: `HTTP ${statusCode}`
});
else span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
} else span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
span.setAttributes({ "http.duration_ms": duration });
if (duration > slowRequestThresholdMs) {
span.setAttribute("http.slow_request", true);
span.setAttribute("http.slow_request_threshold_ms", slowRequestThresholdMs);
}
return result;
} catch (error) {
const duration = performance.now() - startTime;
span.setStatus({
code: _opentelemetry_api.SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : "Unknown error"
});
span.setAttributes({
"http.duration_ms": duration,
"error.type": error instanceof Error ? error.constructor.name : "Unknown",
"error.message": error instanceof Error ? error.message : "Unknown error"
});
throw error;
} finally {
span.end();
}
});
};
this[methodName] = wrappedMethod;
}
}
}
};
};
}
/**
* Helper: Trace a single HTTP request
*
* @example
* ```typescript
* import { traceHttpRequest } from 'autotel/http'
*
* const data = await traceHttpRequest(
* 'GET /api/users',
* () => fetch('https://api.example.com/users')
* )
* ```
*/
async function traceHttpRequest(spanName, fn, attributes) {
return require_config.getConfig().tracer.startActiveSpan(spanName, async (span) => {
try {
if (attributes) span.setAttributes(attributes);
const result = await fn();
span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({
code: _opentelemetry_api.SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : "Unknown error"
});
throw error;
} finally {
span.end();
}
});
}
function inferHttpMethod(methodName) {
const lower = methodName.toLowerCase();
if (lower.includes("get") || lower.includes("fetch") || lower.includes("list")) return "GET";
if (lower.includes("post") || lower.includes("create")) return "POST";
if (lower.includes("put") || lower.includes("update")) return "PUT";
if (lower.includes("delete") || lower.includes("remove")) return "DELETE";
if (lower.includes("patch")) return "PATCH";
return "GET";
}
function extractPath(url) {
try {
return new URL(url).pathname;
} catch {
return url.split("?")[0] || url;
}
}
function parseUrl(url) {
try {
const urlObj = new URL(url);
return {
protocol: urlObj.protocol.replace(":", ""),
host: urlObj.host,
path: urlObj.pathname + urlObj.search
};
} catch {
return {
protocol: "http",
host: "unknown",
path: url
};
}
}
function extractStatusCode(result) {
if (result && typeof result === "object") {
if ("status" in result && typeof result.status === "number") return result.status;
if ("statusCode" in result && typeof result.statusCode === "number") return result.statusCode;
}
}
/**
* Inject trace context into HTTP headers (for distributed tracing)
*
* This includes W3C Trace Context (traceparent, tracestate) and W3C Baggage headers.
* Uses OpenTelemetry's propagation system for full compatibility.
*
* @example
* ```typescript
* import { injectTraceContext } from 'autotel/http'
*
* const headers = injectTraceContext({
* 'Content-Type': 'application/json'
* })
*
* fetch('/api/users', { headers })
* ```
*
* @example With baggage
* ```typescript
* import { trace, withBaggage, injectTraceContext } from 'autotel'
*
* export const createOrder = trace((ctx) => async (order: Order) => {
* return await withBaggage({
* baggage: { 'tenant.id': order.tenantId },
* fn: async () => {
* const headers = injectTraceContext();
* // Headers now include 'baggage' header with tenant.id
* await fetch('/api/charge', { headers });
* },
* });
* });
* ```
*/
function injectTraceContext(headers = {}) {
const currentContext = require_track.getActiveContextWithBaggage();
_opentelemetry_api.propagation.inject(currentContext, headers);
return headers;
}
/**
* Extract trace context from HTTP headers (for distributed tracing)
*
* This extracts W3C Trace Context (traceparent, tracestate) and W3C Baggage headers.
* Uses OpenTelemetry's propagation system for full compatibility.
*
* Returns a context that can be used with context.with() to run code
* with the extracted trace context and baggage.
*
* @example
* ```typescript
* import { extractTraceContext, trace } from 'autotel'
* import { context } from 'autotel'
*
* // In Express middleware
* app.use((req, res, next) => {
* const extractedContext = extractTraceContext(req.headers);
* context.with(extractedContext, () => {
* next();
* });
* });
* ```
*
* @example In a traced function
* ```typescript
* export const handleWebhook = trace((ctx) => async (req: Request) => {
* const extractedContext = extractTraceContext(req.headers);
* return await context.with(extractedContext, async () => {
* // Now ctx.getBaggage() will return baggage from the incoming request
* const tenantId = ctx.getBaggage('tenant.id');
* await processWebhook(req.body);
* });
* });
* ```
*/
function extractTraceContext(headers) {
const carrier = {};
for (const [key, value] of Object.entries(headers)) if (value !== void 0) carrier[key] = Array.isArray(value) ? value[0] ?? "" : value;
return _opentelemetry_api.propagation.extract(_opentelemetry_api.context.active(), carrier);
}
//#endregion
exports.HttpInstrumented = HttpInstrumented;
exports.extractTraceContext = extractTraceContext;
exports.injectTraceContext = injectTraceContext;
exports.traceHttpRequest = traceHttpRequest;
//# sourceMappingURL=http.cjs.map