{"_id":"@coralogix/opentelemetry-profiling","name":"@coralogix/opentelemetry-profiling","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@coralogix/opentelemetry-profiling","version":"0.2.0","description":"OpenTelemetry Node.js Profiling SDK","main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=20"},"scripts":{"prepare":"npm run build && husky","proto:fetch":"npx cx-protofetch fetch","proto:generate":"mkdir -p src/generated && npx pbjs -t static-module -w commonjs --no-create --no-verify --no-convert --no-delimited -r otel-profiling-node -p proto -o src/generated/otlp.js proto/opentelemetry/proto/common/v1/common.proto proto/opentelemetry/proto/resource/v1/resource.proto proto/opentelemetry/proto/profiles/v1development/profiles.proto proto/opentelemetry/proto/collector/profiles/v1development/profiles_service.proto && npx pbts -o src/generated/otlp.d.ts src/generated/otlp.js","proto":"npm run proto:fetch && npm run proto:generate","build":"npm run proto && tsc","test":"vitest run","lint":"eslint src/ test/","lint:fix":"eslint src/ test/ --fix","format":"prettier --write src/","clean":"rm -rf dist proto src/generated"},"dependencies":{"@datadog/pprof":"^5.13.0","pprof-format":"^2.2.0","protobufjs":"^7.0.0"},"peerDependencies":{"@opentelemetry/api":">=1.0.0"},"peerDependenciesMeta":{"@opentelemetry/api":{"optional":true}},"devDependencies":{"@opentelemetry/api":"^1.9.1","@opentelemetry/context-async-hooks":"^2.6.1","@opentelemetry/sdk-trace-base":"^2.6.1","@types/node":"^20.0.0","@typescript-eslint/eslint-plugin":"^8.57.1","@typescript-eslint/parser":"^8.57.1","cx-protofetch":"^0.1.15","eslint":"^9.39.4","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.5","husky":"^9.1.7","prettier":"^3.8.1","protobufjs-cli":"^2.0.0","typescript":"^5.0.0","vitest":"^1.6.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"license":"Apache-2.0","gitHead":"778586d621eeffb9879e0886c001d7089de92499","_id":"@coralogix/opentelemetry-profiling@0.2.0","_nodeVersion":"18.17.0","_npmVersion":"9.6.7","dist":{"integrity":"sha512-TkmjB8O3CQa/ks+54627rRHzOJdKogchfVW1usrJaDcOyjft5dnGkAkn0utjUz3hpzBiv5iX9EVAahlqbO3u/A==","shasum":"cff9898cf51debf0916b2c94e0ff7fb0e7d4ab57","tarball":"https://registry.npmjs.org/@coralogix/opentelemetry-profiling/-/opentelemetry-profiling-0.2.0.tgz","fileCount":30,"unpackedSize":277240,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIA0R0NnjRIlUUeJJWF49FJkSC7Pc1LDBQ2aDSNU7H3+jAiAgBQ2V18/L10wm2En25B/ZnpDFhy7nOYWF5RHMg590pA=="}]},"_npmUser":{"name":"coralogixnpm","email":"npm@coralogix.com"},"directories":{},"maintainers":[{"name":"coralogixnpm","email":"npm@coralogix.com"},{"name":"ashley-hunter-cx","email":"ashley.hunter@coralogix.com"},{"name":"cx-shaharkazaz","email":"shahar.kazaz@coralogix.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/opentelemetry-profiling_0.2.0_1785407431872_0.7807834756110836"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-30T10:30:31.700Z","0.2.0":"2026-07-30T10:30:31.990Z","modified":"2026-07-30T10:30:32.282Z"},"maintainers":[{"name":"coralogixnpm","email":"npm@coralogix.com"},{"name":"ashley-hunter-cx","email":"ashley.hunter@coralogix.com"},{"name":"cx-shaharkazaz","email":"shahar.kazaz@coralogix.com"}],"description":"OpenTelemetry Node.js Profiling SDK","license":"Apache-2.0","readme":"# @coralogix/opentelemetry-profiling\n\nOpenTelemetry continuous profiling SDK for Node.js. Collects wall-clock and heap profiles using [`@datadog/pprof`](https://github.com/nicovak/dd-pprof) and exports them as OTLP (default) or native pprof.\n\n## Features\n\n- **Wall-clock profiling** — samples JS stacks at regular intervals (default 100Hz), capturing both on-CPU and idle time\n- **Heap profiling** — samples memory allocations to identify where memory is being consumed\n- **Trace-profile correlation** — attaches `trace_id`/`span_id` labels to samples for active OTel spans\n- **Span attribute extraction** — copies selected span attributes (e.g. `http.route`) onto profiling samples\n- **Source map support** — maps compiled JS filenames/lines back to original TypeScript sources\n- **OTLP gRPC export (default)** — sends OTLP profiles to OTel Collectors / OTLP-compatible backends\n- **Pprof push export** — opt-in alternative that sends raw pprof to the OTel Collector's `pprofreceiver` push endpoint, skipping OTLP conversion entirely\n- **OTel environment variables** — respects `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_PROFILES_EXPORTER`, etc.\n- **Console exporter** — debug exporter rendering raw pprof contents\n\n## Installation\n\n```bash\nnpm install @coralogix/opentelemetry-profiling\n```\n\nFor trace-profile correlation:\n\n```bash\nnpm install @opentelemetry/api @opentelemetry/sdk-trace-node\n```\n\n## Quick Start\n\n```typescript\nimport { ProfilingProvider } from '@coralogix/opentelemetry-profiling';\n\nconst provider = new ProfilingProvider({\n  serviceName: 'my-service',\n});\n\nawait provider.start();\n\n// Your application code...\n\n// On shutdown\nawait provider.stop();\n```\n\n## Configuration\n\n### Programmatic\n\n```typescript\nimport {\n  ProfilingProvider,\n  ConsoleProfileExporter,\n  OtlpGrpcProfileExporter,\n} from '@coralogix/opentelemetry-profiling';\n\nconst provider = new ProfilingProvider({\n  // Service identification\n  serviceName: 'my-service',\n  resource: {\n    'deployment.environment': 'production',\n    'service.version': '1.2.3',\n  },\n\n  // Profiler toggles\n  wallProfilingEnabled: true,   // default: true\n  heapProfilingEnabled: true,   // default: true\n\n  // Collection interval — how often profiles are flushed to the exporter\n  collectionIntervalMs: 10_000, // default: 10000 (10s)\n\n  // Wall profiler tuning\n  wallSamplingIntervalMicros: 10_000, // default: 10000 (100Hz)\n\n  // Heap profiler tuning\n  heapSamplingIntervalBytes: 524_288, // default: 524288 (512KB)\n  heapStackDepth: 64,                 // default: 64\n\n  // Trace-profile correlation (requires @opentelemetry/api)\n  traceCorrelation: true,\n\n  // Copy these span attributes onto profiling samples\n  spanAttributeKeys: ['http.route', 'rpc.method'],\n\n  // Source maps — map compiled JS back to original TypeScript sources\n  sourceMapSearchPaths: ['./dist'],\n\n  // Exporters — defaults to [OtlpGrpcProfileExporter]\n  exporters: [\n    new OtlpGrpcProfileExporter({\n      endpoint: 'http://localhost:4317',\n      headers: { 'x-api-key': 'secret' },\n    }),\n    new ConsoleProfileExporter({ verbosity: 'basic' }),\n  ],\n});\n```\n\n### Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `OTEL_SERVICE_NAME` | Service name | — |\n| `OTEL_RESOURCE_ATTRIBUTES` | Comma-separated `key=value` pairs | — |\n| `OTEL_PROFILES_EXPORTER` | Comma-separated exporters: `pprof`, `otlp`, `console`, `none` | `otlp` |\n| `OTEL_EXPORTER_PPROF_ENDPOINT` | Pprof push endpoint | `http://localhost:4040/v1/pprof` |\n| `OTEL_EXPORTER_PPROF_HEADERS` | Headers for pprof push (comma-separated `k=v`) | — |\n| `OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT` | Profiles-specific OTLP gRPC endpoint | — |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | General OTLP gRPC endpoint (fallback) | `http://localhost:4317` |\n| `OTEL_EXPORTER_OTLP_PROFILES_HEADERS` | Profiles-specific OTLP headers | — |\n| `OTEL_EXPORTER_OTLP_HEADERS` | General OTLP headers (fallback) | — |\n| `OTEL_PROFILING_WALL_ENABLED` | Enable wall-clock profiler (`true`/`false`) | `true` |\n| `OTEL_PROFILING_HEAP_ENABLED` | Enable heap profiler (`true`/`false`) | `true` |\n| `OTEL_PROFILING_COLLECTION_INTERVAL_MS` | Profile collection interval | `10000` |\n| `OTEL_PROFILING_WALL_SAMPLING_INTERVAL_MICROS` | Wall-clock sample interval (microseconds) | `10000` (100Hz) |\n| `OTEL_PROFILING_HEAP_SAMPLING_INTERVAL_BYTES` | Heap allocation sample interval (bytes) | profiler default |\n| `OTEL_PROFILING_HEAP_STACK_DEPTH` | Max stack frames for heap samples | profiler default |\n| `OTEL_PROFILING_HEAP_SAMPLE_TYPES` | Heap sample types: `bytes`, `objects`, or `both` | `both` |\n| `OTEL_PROFILING_TRACE_CORRELATION` | Attach `trace_id`/`span_id` labels (`true`/`false`) | `true` |\n| `OTEL_PROFILING_SPAN_ATTRIBUTE_KEYS` | Comma-separated span attribute keys to copy onto samples | — |\n| `OTEL_PROFILING_SOURCE_MAP_SEARCH_PATHS` | Comma-separated directories scanned for source maps | — |\n\nEnvironment variables are overridden by programmatic config.\n\n## Exporters\n\n### Pprof push (opt-in)\n\nSends raw pprof bytes (gzipped) over HTTP/1.1 to the OTel Collector's [`pprofreceiver`](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/pprofreceiver) push endpoint (`POST /v1/pprof`). No pprof→OTLP conversion happens in the SDK — the collector does it.\n\nSelect via `OTEL_PROFILES_EXPORTER=pprof`.\n\nResource 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](#using-with-the-opentelemetry-collector).\n\n```typescript\nimport { PprofPushProfileExporter } from '@coralogix/opentelemetry-profiling';\n\nconst exporter = new PprofPushProfileExporter({\n  endpoint: 'http://collector:4040/v1/pprof',\n  headers: { authorization: 'Bearer …' },\n});\n```\n\n### OTLP gRPC (default)\n\nSends 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.\n\nSelected by default; also `OTEL_PROFILES_EXPORTER=otlp` (or `otlp_grpc`).\n\n```typescript\nimport { OtlpGrpcProfileExporter } from '@coralogix/opentelemetry-profiling';\n\nconst exporter = new OtlpGrpcProfileExporter({\n  endpoint: 'http://localhost:4317',\n});\n```\n\n### Console\n\nPrints raw pprof contents to stdout. Three verbosity levels:\n\n```typescript\nimport { ConsoleProfileExporter } from '@coralogix/opentelemetry-profiling';\n\n// basic — one summary line\nnew ConsoleProfileExporter({ verbosity: 'basic' });\n\n// normal (default) — summary + resource attrs + table sizes + period info\nnew ConsoleProfileExporter({ verbosity: 'normal' });\n\n// detailed — also includes top-N functions by leaf sample value\nnew ConsoleProfileExporter({ verbosity: 'detailed', topN: 10 });\n```\n\n### Custom\n\nImplement the `ProfileExporter` interface. Each exporter receives raw pprof — encode or convert as needed.\n\n```typescript\nimport { ProfileExporter, ProfileData, buildRequest, encodeRequest } from '@coralogix/opentelemetry-profiling';\n\nclass MyExporter implements ProfileExporter {\n  async export(data: ProfileData): Promise<void> {\n    // data.profile     — pprof-format Profile object\n    // data.profileType — 'wall' | 'heap'\n    // data.startedAt / data.stoppedAt — collection window\n    // data.resource    — resolved resource attributes\n  }\n\n  async shutdown(): Promise<void> {\n    // cleanup\n  }\n}\n```\n\nIf your exporter needs an OTLP `ExportProfilesServiceRequest`, build it on demand:\n\n```typescript\nconst request = buildRequest(\n  [{ profile: data.profile, profileType: data.profileType, startedAt: data.startedAt, stoppedAt: data.stoppedAt }],\n  data.resource,\n);\nconst encoded = encodeRequest(request);\n```\n\n## Trace-Profile Correlation\n\nWhen `@opentelemetry/api` is installed and a `TracerProvider` is active, enabling `traceCorrelation` links profiling samples to the spans they were captured in.\n\n```typescript\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { ProfilingProvider } from '@coralogix/opentelemetry-profiling';\n\n// Set up tracing first\nconst tracerProvider = new NodeTracerProvider();\ntracerProvider.register();\n\n// Then profiling\nconst profiling = new ProfilingProvider({\n  serviceName: 'my-service',\n  traceCorrelation: true,\n});\nawait profiling.start();\n```\n\nEach wall-clock sample captured while a span is active gets `trace_id` and `span_id` labels in the pprof output.\n\n- **Pprof push path**: labels travel through the collector's pprof receiver translator as ordinary sample attributes. Backends key off the `trace_id`/`span_id` attributes directly.\n- **OTLP path**: the converter promotes these labels into the OTLP `Link` table (`sample.linkIndex`).\n\nEither shape preserves the same information; only the structural layout differs.\n\n### Span Attribute Extraction\n\nUse `spanAttributeKeys` to copy specific span attributes onto profiling samples. This enables grouping and filtering profiles by attributes like HTTP route or RPC method.\n\n```typescript\nconst profiling = new ProfilingProvider({\n  traceCorrelation: true,\n  spanAttributeKeys: ['http.route', 'rpc.method', 'rpc.service'],\n});\n```\n\nAttributes 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.\n\n> **Note**: Span attribute extraction only works for wall-clock profiles. Heap profiles use V8's `AllocationProfiler` which has no per-allocation context capture, so there's no way to know which span was active when a given allocation occurred.\n\n## Using with the OpenTelemetry Collector\n\n### Default: pprof push\n\nThis SDK sends raw pprof to the collector's [`pprofreceiver`](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/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`:\n\n```yaml\nreceivers:\n  pprof:\n    server:\n      endpoint: 0.0.0.0:4040\n      include_metadata: true\n\nprocessors:\n  transform:\n    profile_statements:\n      - set(resource.attributes, ParseKeyValue(metadata[\"otel-resource-attributes\"], \"=\", \",\"))\n\nexporters:\n  debug:\n    verbosity: detailed\n\nservice:\n  pipelines:\n    profiles:\n      receivers: [pprof]\n      processors: [transform]\n      exporters: [debug]\n```\n\nWithout the `transform` rule, profiles will arrive with no `service.name` or other resource attributes — the header carries them, but nothing promotes them by default.\n\n### OTLP gRPC\n\nIf you opt out of pprof push (`OTEL_PROFILES_EXPORTER=otlp`), the standard OTLP receiver applies:\n\n```yaml\nreceivers:\n  otlp:\n    protocols:\n      grpc:\n        endpoint: 0.0.0.0:4317\n\nexporters:\n  debug:\n    verbosity: detailed\n\nservice:\n  pipelines:\n    profiles:\n      receivers: [otlp]\n      exporters: [debug]\n```\n\n> **Note**: The collector must have the `--feature-gates=+service.profilesSupport` flag set (or run a version where profiles support is GA).\n\n## How It Works\n\n1. **`@datadog/pprof`** drives the actual sampling — it uses a native addon with `setitimer(SIGALRM)` for wall-clock sampling and V8's `AllocationProfiler` for heap sampling\n2. **Collection loop** runs every `collectionIntervalMs` (default 10s), calling `stop(restart=true)` on the wall profiler and `profile()` on the heap profiler\n3. **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.\n\n### Wall-clock vs CPU profiling\n\nThe wall profiler samples at regular wall-clock intervals regardless of whether JavaScript is executing. This means:\n- On-CPU work (computation) shows as function frames\n- Off-CPU time (I/O wait, event loop idle) shows as `(idle)`\n- Both are captured, giving a complete picture of where time is spent\n\n### Async stack limitation\n\nWhen 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.\n\n### Source Maps\n\nWhen 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:\n\n```typescript\nconst provider = new ProfilingProvider({\n  sourceMapSearchPaths: ['./dist'],\n});\nawait provider.start();\n```\n\nThe 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`).\n\nRequires your build to emit source maps (e.g. `\"sourceMap\": true` in `tsconfig.json`).\n\n## Examples\n\nSee the [`examples/`](./examples) directory:\n\n- [`basic.ts`](./examples/basic.ts) — minimal setup with console exporter\n- [`otlp-collector.ts`](./examples/otlp-collector.ts) — export to an OTel Collector\n- [`source-maps.ts`](./examples/source-maps.ts) — map compiled JS back to TypeScript sources\n\n## API Reference\n\n### `ProfilingProviderConfig`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `serviceName` | `string` | — | Service name (optional; omitted from resource if unset) |\n| `resource` | `Record<string, string \\| number \\| boolean>` | — | Additional resource attributes |\n| `exporters` | `ProfileExporter[]` | `[OtlpGrpcProfileExporter]` | Exporter instances |\n| `traceCorrelation` | `boolean` | `true` | Link samples to active OTel spans |\n| `spanAttributeKeys` | `string[]` | `[]` | Span attributes to copy onto samples |\n| `wallProfilingEnabled` | `boolean` | `true` | Enable wall-clock profiling |\n| `heapProfilingEnabled` | `boolean` | `true` | Enable heap profiling |\n| `collectionIntervalMs` | `number` | `10000` | How often profiles are flushed |\n| `wallSamplingIntervalMicros` | `number` | `10000` | Wall profiler sampling interval (100Hz) |\n| `heapSamplingIntervalBytes` | `number` | `524288` | Heap profiler sampling interval (512KB) |\n| `heapStackDepth` | `number` | `64` | Max stack depth for heap samples |\n| `sourceMapSearchPaths` | `string[]` | — | Directories to scan for `.js.map` files |\n\n### `ProfilingProvider`\n\n| Method | Description |\n|--------|-------------|\n| `new ProfilingProvider(config?)` | Create a provider with optional config |\n| `start(): Promise<void>` | Start profilers and begin periodic collection |\n| `stop(): Promise<void>` | Stop profilers, flush final profiles, shutdown exporter |\n\n### `ProfileData`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `profile` | `Profile` (from `pprof-format`) | Raw pprof profile |\n| `profileType` | `'wall' \\| 'heap'` | Type of profile |\n| `startedAt` | `Date` | Start of collection window |\n| `stoppedAt` | `Date` | End of collection window |\n| `resource` | `ResourceAttributes` | Resolved resource attributes for this profile |\n\n### `ProfileExporter`\n\n| Method | Description |\n|--------|-------------|\n| `export(data: ProfileData): Promise<void>` | Export a single profile |\n| `shutdown(): Promise<void>` | Clean up resources |\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md","_rev":"1-d6bb89d4f4d1f59599a2e34160c4ae97"}