Set up OpenTelemetry with Inngest TypeScript only
Inngest's Extended Traces enables you to export your application OpenTelemetry traces into Inngest's Traces for a unified observability and debugging experience, both in the DevServer and Platform.
This guide covers how to connect OpenTelemetry with Inngest Traces and how to create custom spans.
Set up Inngest Extended Traces with an existing OpenTelemetry client
If your application already uses an OpenTelemetry client, use your existing provider, exporters, resources, sampling, and instrumentations, then add Inngest's span processor to export spans to Inngest Traces.
Here's a Node.js application OpenTelemetry setup (exporting traces via OTLP) using Inngest Extended Traces. This example uses @opentelemetry/auto-instrumentations-node; if your application already has an instrumentation list, keep using that list instead.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { Inngest } from "inngest";
import { InngestSpanProcessor, extendedTracesMiddleware } from "inngest/experimental";
export const inngest = new Inngest({
id: "nodejs-open-telemetry-example",
name: "NodeJS Open Telemetry Example",
// We'll create the provider and instrumentation later, so use "off" to prevent middleware from doing it.
middleware: [extendedTracesMiddleware({ behaviour: "off" })],
});
// Configure OTLP endpoint for Jaeger using the HTTP exporter
// Jaeger typically accepts OTLP HTTP on http://localhost:4318/v1/traces
// Override via OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
const traceExporter = new OTLPTraceExporter({
url:
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
"http://localhost:4318/v1/traces",
});
const sdk = new NodeSDK({
traceExporter: traceExporter,
spanProcessors: [new InngestSpanProcessor(inngest)],
// Add the instrumentation your app needs. The getNodeAutoInstrumentations function captures common Node.js libraries; some libraries require their own instrumentation package.
instrumentations: [getNodeAutoInstrumentations()],
serviceName: "nodejs-open-telemetry-example",
});
sdk.start();
console.log("OpenTelemetry SDK started");
console.log(
`Tracing endpoint: ${
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
"http://localhost:4318/v1/traces"
}`
);
With this setup, your application OTel traces (e.g., Express API) continue to be exported to your OTLP compatible ingestor (e.g., Jaeger), while Inngest Extended Traces are exported to the Inngest server.
Which Extended Traces behaviour mode should I use?
If you create the OpenTelemetry provider yourself and add InngestSpanProcessor, set behaviour to "off".
If you want the middleware to attach to an already-registered provider, set behaviour to "extendProvider".
Avoid behaviour: "createProvider" for new setups. That path is deprecated in favor of @inngest/otel.
Set up Inngest Extended Traces without an existing OpenTelemetry client
If your application does not already configure OpenTelemetry, use @inngest/otel to install the Node.js provider and common instrumentations.
Preload it before your application:
node --import @inngest/otel/node ./app.js
If --import is not available, use a small bootstrap file as your process entrypoint. Because ESM static imports are evaluated before the importing module runs, keep the dynamic await import("./app.js"); a static import "./app.js" in the same file can load app modules before instrumentation is registered.
import "@inngest/otel/node";
await import("./app.js");
Then configure your Inngest client to use the Extended Traces middleware:
import { Inngest } from "inngest";
import { extendedTracesMiddleware } from "inngest/experimental";
export const inngest = new Inngest({
id: "my-app",
middleware: [extendedTracesMiddleware()],
});
Because @inngest/otel is preloaded, the middleware's default behavior extends the existing provider. This configuration enriches your Inngest Traces with database queries, HTTP request spans, and other spans from the instrumentation included in @inngest/otel.
How to create custom spans with Inngest Extended Traces
Once Inngest Extended Traces is properly set up in your application, your Inngest workflow will receive an additional tracer argument:
import { inngest } from './client'
export const userOnboarding = inngest.createFunction(
{ id: "user-onboarding", triggers: { event: "user.onboarding" } },
async ({ event, step, tracer }) => {
// ...
}
);
The tracer object is an @opentelemetry/api's Tracer instance enabling you to create custom traces as follows:
import { inngest } from './client'
import { sendEmail } from './emails'
export const userOnboarding = inngest.createFunction(
{ id: "user-onboarding", triggers: { event: "user.onboarding" } },
async ({ event, step, tracer }) => {
await step.run("create-user", async () => {
// ...
});
await step.run("send-welcome-email", async () => {
tracer.startActiveSpan("call-email-service", async (span) => {
span.setAttributes({ name, email });
await sendEmail({ name, email })
span.end();
});
});
}
);
Using tracer.startActiveSpan(), we create a custom call-email-service span to track the performance of the external sendEmail() service.
Our custom span is properly displayed within our Inngest workflow run Traces:
![]()