OpenTelemetry instrumentation for Node.js HTTP and HTTPS modules enabling automatic telemetry collection for client and server operations
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Hook functions provide custom logic for filtering requests and adding attributes during HTTP instrumentation lifecycle events. These functions allow fine-grained control over which requests are instrumented and what telemetry data is collected.
Functions used to determine whether incoming or outgoing requests should be instrumented.
/**
* Function to determine if an incoming request should be ignored by instrumentation
* @param request - The incoming HTTP request
* @returns true to ignore the request, false to instrument it
*/
interface IgnoreIncomingRequestFunction {
(request: IncomingMessage): boolean;
}
/**
* Function to determine if an outgoing request should be ignored by instrumentation
* @param request - The outgoing HTTP request options
* @returns true to ignore the request, false to instrument it
*/
interface IgnoreOutgoingRequestFunction {
(request: RequestOptions): boolean;
}Usage Examples:
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import type { IncomingMessage, RequestOptions } from "http";
const instrumentation = new HttpInstrumentation({
// Ignore specific incoming request patterns
ignoreIncomingRequestHook: (request: IncomingMessage) => {
const url = request.url || '';
const userAgent = request.headers['user-agent'] || '';
// Ignore health checks, metrics endpoints, and bot traffic
return url.startsWith('/health') ||
url.startsWith('/metrics') ||
url.startsWith('/_internal') ||
userAgent.includes('bot') ||
userAgent.includes('crawler');
},
// Ignore specific outgoing request patterns
ignoreOutgoingRequestHook: (request: RequestOptions) => {
const hostname = request.hostname || request.host || '';
const path = request.path || '';
// Ignore internal services and health checks
return hostname.includes('internal.company.com') ||
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
path.includes('/health');
}
});Functions used to add custom attributes to spans at different points in the request lifecycle.
/**
* Function for adding custom attributes to spans after both request and response are processed
* @param span - The current span
* @param request - The HTTP request (ClientRequest for outgoing, IncomingMessage for incoming)
* @param response - The HTTP response (IncomingMessage for outgoing, ServerResponse for incoming)
*/
interface HttpCustomAttributeFunction {
(
span: Span,
request: ClientRequest | IncomingMessage,
response: IncomingMessage | ServerResponse
): void;
}
/**
* Function for adding custom attributes to spans during request processing
* @param span - The current span
* @param request - The HTTP request (ClientRequest for outgoing, IncomingMessage for incoming)
*/
interface HttpRequestCustomAttributeFunction {
(span: Span, request: ClientRequest | IncomingMessage): void;
}
/**
* Function for adding custom attributes to spans during response processing
* @param span - The current span
* @param response - The HTTP response (IncomingMessage for outgoing, ServerResponse for incoming)
*/
interface HttpResponseCustomAttributeFunction {
(span: Span, response: IncomingMessage | ServerResponse): void;
}Usage Examples:
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import type { Span } from "@opentelemetry/api";
import type { ClientRequest, IncomingMessage, ServerResponse } from "http";
const instrumentation = new HttpInstrumentation({
// Add custom attributes during request processing
requestHook: (span: Span, request: ClientRequest | IncomingMessage) => {
// Common attributes for both incoming and outgoing requests
const userAgent = request.headers['user-agent'];
const contentType = request.headers['content-type'];
const requestId = request.headers['x-request-id'];
if (userAgent) {
span.setAttribute('http.user_agent.original', userAgent);
span.setAttribute('http.user_agent.is_mobile', userAgent.includes('Mobile'));
}
if (contentType) {
span.setAttribute('http.request.content_type', contentType);
}
if (requestId) {
span.setAttribute('http.request.id', requestId);
}
// Differentiate between incoming and outgoing requests
if ('method' in request) {
// This is an IncomingMessage (incoming request)
span.setAttribute('http.direction', 'incoming');
span.setAttribute('http.client.ip', request.socket.remoteAddress || '');
} else {
// This is a ClientRequest (outgoing request)
span.setAttribute('http.direction', 'outgoing');
}
},
// Add custom attributes during response processing
responseHook: (span: Span, response: IncomingMessage | ServerResponse) => {
const contentLength = response.headers['content-length'];
const cacheControl = response.headers['cache-control'];
const server = response.headers['server'];
if (contentLength) {
span.setAttribute('http.response.body.size', parseInt(contentLength));
}
if (cacheControl) {
span.setAttribute('http.response.cache_control', cacheControl);
}
if (server) {
span.setAttribute('http.response.server', server);
}
// Add custom performance categorization
if ('statusCode' in response && response.statusCode) {
const category = response.statusCode < 300 ? 'success' :
response.statusCode < 400 ? 'redirect' :
response.statusCode < 500 ? 'client_error' : 'server_error';
span.setAttribute('http.response.category', category);
}
},
// Add comprehensive attributes after complete request/response cycle
applyCustomAttributesOnSpan: (
span: Span,
request: ClientRequest | IncomingMessage,
response: IncomingMessage | ServerResponse
) => {
// Calculate request processing time
const endTime = Date.now();
const startTime = span.startTime[0] * 1000 + span.startTime[1] / 1_000_000;
const duration = endTime - startTime;
// Add performance categories
span.setAttribute('http.duration.category',
duration < 50 ? 'very_fast' :
duration < 200 ? 'fast' :
duration < 1000 ? 'moderate' :
duration < 5000 ? 'slow' : 'very_slow'
);
// Add size categories for responses
const contentLength = response.headers['content-length'];
if (contentLength) {
const size = parseInt(contentLength);
span.setAttribute('http.response.size.category',
size < 1024 ? 'small' :
size < 102400 ? 'medium' :
size < 1048576 ? 'large' : 'very_large'
);
}
}
});Functions used to add custom attributes before spans are created, allowing attributes to be set at span initialization.
/**
* Function for adding custom attributes before an incoming request span is started
* @param request - The incoming HTTP request
* @returns Object containing attributes to add to the span
*/
interface StartIncomingSpanCustomAttributeFunction {
(request: IncomingMessage): Attributes;
}
/**
* Function for adding custom attributes before an outgoing request span is started
* @param request - The outgoing HTTP request options
* @returns Object containing attributes to add to the span
*/
interface StartOutgoingSpanCustomAttributeFunction {
(request: RequestOptions): Attributes;
}Usage Examples:
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import type { Attributes } from "@opentelemetry/api";
import type { IncomingMessage, RequestOptions } from "http";
const instrumentation = new HttpInstrumentation({
// Add attributes at the start of incoming request spans
startIncomingSpanHook: (request: IncomingMessage): Attributes => {
const attributes: Attributes = {};
// Extract tenant/customer information
const tenantId = request.headers['x-tenant-id'];
const customerId = request.headers['x-customer-id'];
const apiVersion = request.headers['api-version'];
if (tenantId) {
attributes['tenant.id'] = tenantId;
}
if (customerId) {
attributes['customer.id'] = customerId;
}
if (apiVersion) {
attributes['api.version'] = apiVersion;
}
// Add request classification
const url = request.url || '';
if (url.startsWith('/api/v1/')) {
attributes['api.type'] = 'rest';
attributes['api.version'] = 'v1';
} else if (url.startsWith('/graphql')) {
attributes['api.type'] = 'graphql';
} else if (url.startsWith('/webhook')) {
attributes['api.type'] = 'webhook';
}
return attributes;
},
// Add attributes at the start of outgoing request spans
startOutgoingSpanHook: (request: RequestOptions): Attributes => {
const attributes: Attributes = {};
const hostname = request.hostname || request.host || '';
const path = request.path || '';
// Service identification
if (hostname.includes('api.stripe.com')) {
attributes['service.name'] = 'stripe';
attributes['service.type'] = 'payment';
} else if (hostname.includes('api.github.com')) {
attributes['service.name'] = 'github';
attributes['service.type'] = 'code_repository';
} else if (hostname.includes('amazonaws.com')) {
attributes['service.name'] = 'aws';
attributes['service.type'] = 'cloud_service';
}
// Operation classification
const method = request.method?.toUpperCase() || 'GET';
if (method === 'GET') {
attributes['operation.type'] = 'read';
} else if (method === 'POST' || method === 'PUT') {
attributes['operation.type'] = 'write';
} else if (method === 'DELETE') {
attributes['operation.type'] = 'delete';
}
// Add timeout information if specified
if (request.timeout) {
attributes['http.client.timeout'] = request.timeout;
}
return attributes;
}
});const instrumentation = new HttpInstrumentation({
requestHook: (span, request) => {
// Only add attributes for certain content types
const contentType = request.headers['content-type'] || '';
if (contentType.includes('application/json')) {
span.setAttribute('request.format', 'json');
// Add content length for JSON requests
const contentLength = request.headers['content-length'];
if (contentLength) {
span.setAttribute('request.json.size', parseInt(contentLength));
}
} else if (contentType.includes('multipart/form-data')) {
span.setAttribute('request.format', 'multipart');
} else if (contentType.includes('application/x-www-form-urlencoded')) {
span.setAttribute('request.format', 'form');
}
}
});const instrumentation = new HttpInstrumentation({
applyCustomAttributesOnSpan: (span, request, response) => {
// Enhanced error context for failed requests
if ('statusCode' in response && response.statusCode && response.statusCode >= 400) {
span.setAttribute('error.type', 'http_error');
span.setAttribute('error.status_code', response.statusCode);
// Add more context for specific error ranges
if (response.statusCode >= 500) {
span.setAttribute('error.category', 'server_error');
span.setAttribute('error.severity', 'high');
} else if (response.statusCode >= 400) {
span.setAttribute('error.category', 'client_error');
span.setAttribute('error.severity', 'medium');
}
// Add request details for debugging
if ('url' in request) {
span.setAttribute('error.request.url', request.url || '');
}
if ('method' in request) {
span.setAttribute('error.request.method', request.method || '');
}
}
}
});Install with Tessl CLI
npx tessl i tessl/npm-opentelemetry--instrumentation-http