.agents/skills/apm-integrations/SKILL.md
dd-trace-js provides automatic tracing for 100+ third-party libraries. Each integration consists of two decoupled layers communicating via Node.js diagnostic channels.
┌──────────────────────────┐ diagnostic channels ┌─────────────────────────┐
│ Instrumentation │ ──────────────────────────▶ │ Plugin │
│ datadog-instrumentations │ apm:<name>:<op>:start │ datadog-plugin-<name> │
│ │ apm:<name>:<op>:finish │ │
│ Hooks into library │ apm:<name>:<op>:error │ Creates spans, sets │
│ methods, emits events │ │ tags, handles errors │
└──────────────────────────┘ └─────────────────────────┘
finish above is the legacy manual-channel completion event. tracingChannel
and Orchestrion use end / asyncEnd, as described below.
Instrumentation (packages/datadog-instrumentations/src/):
Hooks into a library's internals and publishes events with context data to named diagnostic channels. Has zero knowledge of tracing — only emits events.
Plugin (packages/datadog-plugin-<name>/src/):
Subscribes to diagnostic channel events and creates APM spans with service name, resource, tags, and error metadata. Extends a base class providing lifecycle management.
Both layers are always needed for a new integration.
Orchestrion is the required default when the work exists as a source function. It rewrites matched CJS/ESM source from JavaScript config, avoiding runtime monkey-patching and ESM's static-binding traps. Start there for top-level declarations, class/object methods, named expressions, and assignments to named receivers. Use shimmer only when the work is created entirely at runtime or the required argument/result mutation cannot happen from Orchestrion's subscriber lifecycle.
Config lives in packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/<name>.js. See Orchestrion Reference for the full config format and examples.
Shimmer (addHook + shimmer.wrap) should only be used when orchestrion cannot handle the pattern. When using shimmer, always include a code comment explaining why orchestrion is not viable. Valid reasons:
end can replace synchronous results and asyncEnd can replace native-Promise results; shimmer remains necessary for Promise subclasses, userland thenables, or APIs that require the original result's identitybindStart / subscribers can runIf none of these apply, use orchestrion. For shimmer patterns, refer to existing shimmer-based instrumentations in the codebase (e.g., packages/datadog-instrumentations/src/pg.js). Always try to use Orchestrion when beginning a new integration!
Plugins extend a base class matching the library type. The base class provides automatic channel subscriptions, span lifecycle, and type-specific tags.
Plugin
├── CompositePlugin — Multiple sub-plugins (produce + consume)
├── LogPlugin — Log correlation injection (no spans)
├── WebPlugin — Base web plugin
│ └── RouterPlugin — Web frameworks with middleware
└── TracingPlugin — Base for all span-creating plugins
├── InboundPlugin — Inbound calls
│ ├── ServerPlugin — HTTP servers
│ └── ConsumerPlugin — Message consumers (DSM)
└── OutboundPlugin — Outbound calls
├── ProducerPlugin — Message producers (DSM)
└── ClientPlugin — HTTP/RPC clients
└── StoragePlugin — Storage systems
├── DatabasePlugin — Database clients (DBM, db.* tags)
└── CachePlugin — Key-value caches
Wrong base class = complex workarounds. Always match the library type to the base class.
Touching packages/datadog-instrumentations/src/<lib>.js, its plugin counterpart, or any orchestrion config — for any reason — read the upstream library's source first. Memory of an SDK's contract drifts faster than the SDK; comments in the wrap go stale every minor version; cross-version diffs surface contract changes guessing misses (lazy → eager attachment, mode-exclusive APIs, new error paths).
Two ways to fetch the source locally:
git clone --depth 1 --branch v<x.y.z> https://github.com/<org>/<repo>.git /tmp/<lib>-versions/v<x.y.z>
npm pack when the published runtime artifact is what matters:cd /tmp/<lib>-versions && npm pack <lib>@<x.y.z>
tar -xzf <lib>-<x.y.z>.tgz -C v<x.y.z> --strip-components=1
Read the file the wrap hooks, the base classes the hooked methods inherit from, and files the wrap doesn't currently touch — a public method, an internal channel, or a metadata field the current instrumentation skipped often gives a cleaner hook (e.g., kafka cluster.brokerPool.metadata.clusterId, couchbase tracingChannel).
ctx ObjectContext flows from instrumentation to plugin:
ctx.arguments (method args) and ctx.self (instance)ctx.sql, ctx.client, etc.)ctx.currentStore (span), ctx.parentStore (parent span)ctx.result or ctx.errorrunStores() for start events — establishes async context (always)publish() for completion/error events — notification onlyhasSubscribers guard — skip publish/subscriber work when no plugin listens; orchestrion still pays wrapper setup in current templatestracingChannel (from dc-polyfill) over manual channels — it provides start/end/asyncStart/asyncEnd/error events automaticallytracing:orchestrion:<npm-package>:<channelName> (set via static prefix)tracingChannel (preferred): tracing:apm:<name>:<operation> (set via static prefix)apm:{id}:{operation} (default, no static prefix needed)bindStart and completion handlersUse bindStart to create the span and return its store. Finish in the event the instrumentation emits: usually end for synchronous work, asyncEnd for promises/callbacks, and finish only for legacy instrumentations that publish it. Orchestrion does not publish finish.
channel.publish position)When relocating a channel.publish call behind a dedupe gate, depth filter, cache-hit return, or any short-circuit, the question is not "is the publish still there?" but "what cardinality does each downstream subscriber need?". Subscribers split into two camps that look identical from inside the publish site:
args object by reference), AppSec WAF subscribers that block/log per invocation, anything walking payload identity. Drops data silently when cardinality falls below one-per-call.Before adding or moving a gate in front of a publish, grep the repo for the channel name, list its subscribers, decide per-subscriber whether the new position preserves the cardinality each needs. When cardinalities diverge, split the publish into a pre-gate (per-call) and a post-gate (per-first-occurrence) call.
Always read 1-2 references of the same type before writing or modifying code.
| Library Type | Plugin | Instrumentation | Base Class |
|---|---|---|---|
| Database | datadog-plugin-pg | src/pg.js | DatabasePlugin |
| Cache | datadog-plugin-redis | src/redis.js | CachePlugin |
| HTTP client | datadog-plugin-fetch | src/fetch.js | HttpClientPlugin (extends ClientPlugin) |
| Web framework | datadog-plugin-express | src/express.js | RouterPlugin |
| Message queue | datadog-plugin-kafkajs | src/kafkajs.js | Producer/ConsumerPlugin |
| Orchestrion | datadog-plugin-langchain | rewriter/instrumentations/langchain.js | TracingPlugin |
For the complete list by base class, see Reference Plugins.
DD_TRACE_DEBUG=true to see channel activityObject.keys(ctx) in bindStart to inspect available contexthasSubscribers guard; check channel names match between layersrunStores() (not publish()) for start eventsesmFirst: true in hooks.js (or switch to orchestrion)Follow these steps when creating or modifying an integration:
Investigate — Read the upstream library's source (see Read Upstream Source First). Read 1-2 reference integrations of the same type (see table above). Understand the instrumentation and plugin patterns before writing code.
Implement instrumentation — Create the instrumentation in packages/datadog-instrumentations/src/. Use orchestrion for instrumentation.
Implement plugin — Create the plugin in packages/datadog-plugin-<name>/src/. Extend the correct base class.
Register — Add entries in packages/dd-trace/src/plugins/index.js, index.d.ts, docs/test.ts, docs/API.md, and .github/workflows/apm-integrations.yml.
Write tests — Add unit tests and ESM integration tests. See Testing for templates.
Run tests — Validate with:
# Run plugin tests (preferred CI command — handles yarn services automatically)
PLUGINS="<name>" npm run test:plugins:ci
# If the plugin needs external services (databases, message brokers, etc.),
# check docker-compose.yml for available service names, then:
docker compose up -d <service>
PLUGINS="<name>" npm run test:plugins:ci
Verify — Confirm all tests pass before marking work as complete.
startSpan() API, ctx object details, CompositePlugin, channel subscriptions, code style