@coralogix/opentelemetry-profiling
v0.2.0
Published
OpenTelemetry Node.js Profiling SDK
Downloads
220
Keywords
Readme
@coralogix/opentelemetry-profiling
OpenTelemetry continuous profiling SDK for Node.js. Collects wall-clock and heap profiles using @datadog/pprof and exports them as OTLP (default) or native pprof.
Features
- Wall-clock profiling — samples JS stacks at regular intervals (default 100Hz), capturing both on-CPU and idle time
- Heap profiling — samples memory allocations to identify where memory is being consumed
- Trace-profile correlation — attaches
trace_id/span_idlabels to samples for active OTel spans - Span attribute extraction — copies selected span attributes (e.g.
http.route) onto profiling samples - Source map support — maps compiled JS filenames/lines back to original TypeScript sources
- OTLP gRPC export (default) — sends OTLP profiles to OTel Collectors / OTLP-compatible backends
- Pprof push export — opt-in alternative that sends raw pprof to the OTel Collector's
pprofreceiverpush endpoint, skipping OTLP conversion entirely - OTel environment variables — respects
OTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES,OTEL_PROFILES_EXPORTER, etc. - Console exporter — debug exporter rendering raw pprof contents
Installation
npm install @coralogix/opentelemetry-profilingFor trace-profile correlation:
npm install @opentelemetry/api @opentelemetry/sdk-trace-nodeQuick Start
import { ProfilingProvider } from '@coralogix/opentelemetry-profiling';
const provider = new ProfilingProvider({
serviceName: 'my-service',
});
await provider.start();
// Your application code...
// On shutdown
await provider.stop();Configuration
Programmatic
import {
ProfilingProvider,
ConsoleProfileExporter,
OtlpGrpcProfileExporter,
} from '@coralogix/opentelemetry-profiling';
const provider = new ProfilingProvider({
// Service identification
serviceName: 'my-service',
resource: {
'deployment.environment': 'production',
'service.version': '1.2.3',
},
// Profiler toggles
wallProfilingEnabled: true, // default: true
heapProfilingEnabled: true, // default: true
// Collection interval — how often profiles are flushed to the exporter
collectionIntervalMs: 10_000, // default: 10000 (10s)
// Wall profiler tuning
wallSamplingIntervalMicros: 10_000, // default: 10000 (100Hz)
// Heap profiler tuning
heapSamplingIntervalBytes: 524_288, // default: 524288 (512KB)
heapStackDepth: 64, // default: 64
// Trace-profile correlation (requires @opentelemetry/api)
traceCorrelation: true,
// Copy these span attributes onto profiling samples
spanAttributeKeys: ['http.route', 'rpc.method'],
// Source maps — map compiled JS back to original TypeScript sources
sourceMapSearchPaths: ['./dist'],
// Exporters — defaults to [OtlpGrpcProfileExporter]
exporters: [
new OtlpGrpcProfileExporter({
endpoint: 'http://localhost:4317',
headers: { 'x-api-key': 'secret' },
}),
new ConsoleProfileExporter({ verbosity: 'basic' }),
],
});Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| OTEL_SERVICE_NAME | Service name | — |
| OTEL_RESOURCE_ATTRIBUTES | Comma-separated key=value pairs | — |
| OTEL_PROFILES_EXPORTER | Comma-separated exporters: pprof, otlp, console, none | otlp |
| OTEL_EXPORTER_PPROF_ENDPOINT | Pprof push endpoint | http://localhost:4040/v1/pprof |
| OTEL_EXPORTER_PPROF_HEADERS | Headers for pprof push (comma-separated k=v) | — |
| OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT | Profiles-specific OTLP gRPC endpoint | — |
| OTEL_EXPORTER_OTLP_ENDPOINT | General OTLP gRPC endpoint (fallback) | http://localhost:4317 |
| OTEL_EXPORTER_OTLP_PROFILES_HEADERS | Profiles-specific OTLP headers | — |
| OTEL_EXPORTER_OTLP_HEADERS | General OTLP headers (fallback) | — |
| OTEL_PROFILING_WALL_ENABLED | Enable wall-clock profiler (true/false) | true |
| OTEL_PROFILING_HEAP_ENABLED | Enable heap profiler (true/false) | true |
| OTEL_PROFILING_COLLECTION_INTERVAL_MS | Profile collection interval | 10000 |
| OTEL_PROFILING_WALL_SAMPLING_INTERVAL_MICROS | Wall-clock sample interval (microseconds) | 10000 (100Hz) |
| OTEL_PROFILING_HEAP_SAMPLING_INTERVAL_BYTES | Heap allocation sample interval (bytes) | profiler default |
| OTEL_PROFILING_HEAP_STACK_DEPTH | Max stack frames for heap samples | profiler default |
| OTEL_PROFILING_HEAP_SAMPLE_TYPES | Heap sample types: bytes, objects, or both | both |
| OTEL_PROFILING_TRACE_CORRELATION | Attach trace_id/span_id labels (true/false) | true |
| OTEL_PROFILING_SPAN_ATTRIBUTE_KEYS | Comma-separated span attribute keys to copy onto samples | — |
| OTEL_PROFILING_SOURCE_MAP_SEARCH_PATHS | Comma-separated directories scanned for source maps | — |
Environment variables are overridden by programmatic config.
Exporters
Pprof push (opt-in)
Sends raw pprof bytes (gzipped) over HTTP/1.1 to the OTel Collector's pprofreceiver push endpoint (POST /v1/pprof). No pprof→OTLP conversion happens in the SDK — the collector does it.
Select via OTEL_PROFILES_EXPORTER=pprof.
Resource attributes are sent as a single HTTP header Otel-Resource-Attributes: k=v,k=v (same syntax as OTEL_RESOURCE_ATTRIBUTES). The SDK adds profiler.type=wall|heap to the resource for each export so backends can distinguish profile kinds. The collector must be configured with include_metadata: true and a transform processor that promotes the header into resource attributes — see Using with the OpenTelemetry Collector.
import { PprofPushProfileExporter } from '@coralogix/opentelemetry-profiling';
const exporter = new PprofPushProfileExporter({
endpoint: 'http://collector:4040/v1/pprof',
headers: { authorization: 'Bearer …' },
});OTLP gRPC (default)
Sends profiles to an OTLP-compatible collector via gRPC (HTTP/2 + protobuf). Uses Node's built-in http2 module — no @grpc/grpc-js dependency. Pprof→OTLP conversion happens in the SDK.
Selected by default; also OTEL_PROFILES_EXPORTER=otlp (or otlp_grpc).
import { OtlpGrpcProfileExporter } from '@coralogix/opentelemetry-profiling';
const exporter = new OtlpGrpcProfileExporter({
endpoint: 'http://localhost:4317',
});Console
Prints raw pprof contents to stdout. Three verbosity levels:
import { ConsoleProfileExporter } from '@coralogix/opentelemetry-profiling';
// basic — one summary line
new ConsoleProfileExporter({ verbosity: 'basic' });
// normal (default) — summary + resource attrs + table sizes + period info
new ConsoleProfileExporter({ verbosity: 'normal' });
// detailed — also includes top-N functions by leaf sample value
new ConsoleProfileExporter({ verbosity: 'detailed', topN: 10 });Custom
Implement the ProfileExporter interface. Each exporter receives raw pprof — encode or convert as needed.
import { ProfileExporter, ProfileData, buildRequest, encodeRequest } from '@coralogix/opentelemetry-profiling';
class MyExporter implements ProfileExporter {
async export(data: ProfileData): Promise<void> {
// data.profile — pprof-format Profile object
// data.profileType — 'wall' | 'heap'
// data.startedAt / data.stoppedAt — collection window
// data.resource — resolved resource attributes
}
async shutdown(): Promise<void> {
// cleanup
}
}If your exporter needs an OTLP ExportProfilesServiceRequest, build it on demand:
const request = buildRequest(
[{ profile: data.profile, profileType: data.profileType, startedAt: data.startedAt, stoppedAt: data.stoppedAt }],
data.resource,
);
const encoded = encodeRequest(request);Trace-Profile Correlation
When @opentelemetry/api is installed and a TracerProvider is active, enabling traceCorrelation links profiling samples to the spans they were captured in.
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { ProfilingProvider } from '@coralogix/opentelemetry-profiling';
// Set up tracing first
const tracerProvider = new NodeTracerProvider();
tracerProvider.register();
// Then profiling
const profiling = new ProfilingProvider({
serviceName: 'my-service',
traceCorrelation: true,
});
await profiling.start();Each wall-clock sample captured while a span is active gets trace_id and span_id labels in the pprof output.
- Pprof push path: labels travel through the collector's pprof receiver translator as ordinary sample attributes. Backends key off the
trace_id/span_idattributes directly. - OTLP path: the converter promotes these labels into the OTLP
Linktable (sample.linkIndex).
Either shape preserves the same information; only the structural layout differs.
Span Attribute Extraction
Use spanAttributeKeys to copy specific span attributes onto profiling samples. This enables grouping and filtering profiles by attributes like HTTP route or RPC method.
const profiling = new ProfilingProvider({
traceCorrelation: true,
spanAttributeKeys: ['http.route', 'rpc.method', 'rpc.service'],
});Attributes are read from the span at profile collection time, not when the span is activated. This means attributes set after span creation (e.g. http.route set by Express after route matching) are captured correctly.
Note: Span attribute extraction only works for wall-clock profiles. Heap profiles use V8's
AllocationProfilerwhich has no per-allocation context capture, so there's no way to know which span was active when a given allocation occurred.
Using with the OpenTelemetry Collector
Default: pprof push
This SDK sends raw pprof to the collector's pprofreceiver push endpoint. Resource attributes are conveyed in the Otel-Resource-Attributes HTTP header, so the receiver needs include_metadata: true and a transform processor that lifts the header into resource.attributes:
receivers:
pprof:
server:
endpoint: 0.0.0.0:4040
include_metadata: true
processors:
transform:
profile_statements:
- set(resource.attributes, ParseKeyValue(metadata["otel-resource-attributes"], "=", ","))
exporters:
debug:
verbosity: detailed
service:
pipelines:
profiles:
receivers: [pprof]
processors: [transform]
exporters: [debug]Without the transform rule, profiles will arrive with no service.name or other resource attributes — the header carries them, but nothing promotes them by default.
OTLP gRPC
If you opt out of pprof push (OTEL_PROFILES_EXPORTER=otlp), the standard OTLP receiver applies:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
exporters:
debug:
verbosity: detailed
service:
pipelines:
profiles:
receivers: [otlp]
exporters: [debug]Note: The collector must have the
--feature-gates=+service.profilesSupportflag set (or run a version where profiles support is GA).
How It Works
@datadog/pprofdrives the actual sampling — it uses a native addon withsetitimer(SIGALRM)for wall-clock sampling and V8'sAllocationProfilerfor heap sampling- Collection loop runs every
collectionIntervalMs(default 10s), callingstop(restart=true)on the wall profiler andprofile()on the heap profiler - Per-exporter encoding — the provider hands raw pprof to each configured exporter. The pprof push exporter gzip-encodes the bytes and POSTs them. The OTLP exporter converts pprof → OTLP locally (dictionary-based string/function/location/stack tables), encodes as protobuf, frames it with gRPC length-prefixed encoding, and sends via HTTP/2. Conversion only happens for exporters that need it.
Wall-clock vs CPU profiling
The wall profiler samples at regular wall-clock intervals regardless of whether JavaScript is executing. This means:
- On-CPU work (computation) shows as function frames
- Off-CPU time (I/O wait, event loop idle) shows as
(idle) - Both are captured, giving a complete picture of where time is spent
Async stack limitation
When Node.js hits an await, the call stack is unwound — there's no stack to sample. This means a function like await db.query() appears as (idle) samples, not as samples attributed to db.query. Enabling traceCorrelation helps bridge this gap by attributing idle time to the span that was active.
Source Maps
When profiling TypeScript or bundled applications, V8 reports function names and filenames from the compiled JS output. Enable sourceMapSearchPaths to map these back to original source files:
const provider = new ProfilingProvider({
sourceMapSearchPaths: ['./dist'],
});
await provider.start();The provider scans the specified directories for .js.map files at startup. Profiles will then show original filenames and line numbers (e.g. src/handler.ts:42 instead of dist/handler.js:120).
Requires your build to emit source maps (e.g. "sourceMap": true in tsconfig.json).
Examples
See the examples/ directory:
basic.ts— minimal setup with console exporterotlp-collector.ts— export to an OTel Collectorsource-maps.ts— map compiled JS back to TypeScript sources
API Reference
ProfilingProviderConfig
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| serviceName | string | — | Service name (optional; omitted from resource if unset) |
| resource | Record<string, string \| number \| boolean> | — | Additional resource attributes |
| exporters | ProfileExporter[] | [OtlpGrpcProfileExporter] | Exporter instances |
| traceCorrelation | boolean | true | Link samples to active OTel spans |
| spanAttributeKeys | string[] | [] | Span attributes to copy onto samples |
| wallProfilingEnabled | boolean | true | Enable wall-clock profiling |
| heapProfilingEnabled | boolean | true | Enable heap profiling |
| collectionIntervalMs | number | 10000 | How often profiles are flushed |
| wallSamplingIntervalMicros | number | 10000 | Wall profiler sampling interval (100Hz) |
| heapSamplingIntervalBytes | number | 524288 | Heap profiler sampling interval (512KB) |
| heapStackDepth | number | 64 | Max stack depth for heap samples |
| sourceMapSearchPaths | string[] | — | Directories to scan for .js.map files |
ProfilingProvider
| Method | Description |
|--------|-------------|
| new ProfilingProvider(config?) | Create a provider with optional config |
| start(): Promise<void> | Start profilers and begin periodic collection |
| stop(): Promise<void> | Stop profilers, flush final profiles, shutdown exporter |
ProfileData
| Field | Type | Description |
|-------|------|-------------|
| profile | Profile (from pprof-format) | Raw pprof profile |
| profileType | 'wall' \| 'heap' | Type of profile |
| startedAt | Date | Start of collection window |
| stoppedAt | Date | End of collection window |
| resource | ResourceAttributes | Resolved resource attributes for this profile |
ProfileExporter
| Method | Description |
|--------|-------------|
| export(data: ProfileData): Promise<void> | Export a single profile |
| shutdown(): Promise<void> | Clean up resources |
License
Apache-2.0
