Framework Integration
Integrate @igniter-js/telemetry with Next.js (App Router + Middleware), Express, and Fastify. Production-ready patterns with session correlation per request.
Framework Integration
Telemetry is framework-agnostic, but these patterns make it feel native in popular frameworks. All examples are verified and production-ready.
Next.js (App Router)
Singleton Telemetry Instance
Create a shared instance once:
// lib/telemetry.ts
import { IgniterTelemetry } from "@igniter-js/telemetry";
import {
LoggerTransportAdapter,
SentryTransportAdapter,
} from "@igniter-js/telemetry/adapters";
import * as Sentry from "@sentry/nextjs";
const createTelemetry = () => {
if (process.env.NODE_ENV === "development") {
return IgniterTelemetry.create()
.withService("my-next-app")
.withEnvironment("development")
.addTransport(LoggerTransportAdapter.create({
logger: console,
format: "pretty",
}))
.build();
}
Sentry.init({ dsn: process.env.SENTRY_DSN });
return IgniterTelemetry.create()
.withService("my-next-app")
.withEnvironment("production")
.withVersion(process.env.NEXT_PUBLIC_APP_VERSION ?? "0.0.0")
.addActor("user")
.addScope("organization")
.addTransport(LoggerTransportAdapter.create({
logger: console,
format: "json",
minLevel: "info",
}))
.addTransport(SentryTransportAdapter.create({ sentry: Sentry }))
.withSampling({
debugRate: 0.0,
infoRate: 0.1,
warnRate: 1.0,
errorRate: 1.0,
})
.withRedaction({
denylistKeys: ["password", "token", "authorization"],
hashKeys: ["email", "ip"],
})
.build();
};
// Singleton — avoid globalThis pattern in Next.js
declare global {
var __telemetry: ReturnType<typeof createTelemetry> | undefined;
}
const telemetry = globalThis.__telemetry ?? createTelemetry();
if (process.env.NODE_ENV !== "production") globalThis.__telemetry = telemetry;
export { telemetry };Middleware: Session Per Request
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { telemetry } from "@/lib/telemetry";
export async function middleware(request: NextRequest) {
const userId = request.headers.get("x-user-id") ?? "anonymous";
const orgId = request.headers.get("x-org-id") ?? "unknown";
return telemetry.session()
.actor("user", userId)
.scope("organization", orgId)
.attributes({
"ctx.request.method": request.method,
"ctx.request.path": request.nextUrl.pathname,
"ctx.request.ip": request.ip ?? "unknown",
})
.run(async () => {
telemetry.emit("request.started", {
attributes: { "ctx.request.path": request.nextUrl.pathname },
});
const response = NextResponse.next();
telemetry.emit("request.completed", {
attributes: { "ctx.request.status": response.status },
});
return response;
});
}
export const config = {
matcher: "/api/:path*",
};API Route Handler
// app/api/orders/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { telemetry } from "@/lib/telemetry";
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } },
) {
try {
// Session context auto-inherited from middleware
telemetry.emit("order.fetch.started", {
attributes: { "ctx.order.id": params.id },
});
const order = await fetchOrder(params.id);
telemetry.emit("order.fetch.completed", {
attributes: {
"ctx.order.id": params.id,
"ctx.order.status": order.status,
},
});
return NextResponse.json(order);
} catch (error) {
telemetry.emit("order.fetch.failed", {
level: "error",
error: {
name: error instanceof Error ? error.name : "Error",
message: error instanceof Error ? error.message : String(error),
},
attributes: { "ctx.order.id": params.id },
});
return NextResponse.json(
{ error: "Failed to fetch order" },
{ status: 500 },
);
}
}Server Action
// app/actions/create-invoice.ts
"use server";
import { telemetry } from "@/lib/telemetry";
export async function createInvoice(formData: FormData) {
return telemetry.session()
.actor("user", formData.get("userId") as string)
.scope("organization", formData.get("orgId") as string)
.run(async () => {
telemetry.emit("igniter.billing.invoice.created", {
attributes: {
"ctx.invoice.amount": Number(formData.get("amount")),
"ctx.invoice.currency": formData.get("currency") as string,
},
});
// ... create invoice logic ...
return { success: true };
});
}Graceful Shutdown (Next.js)
// instrumentation.ts
import { telemetry } from "@/lib/telemetry";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
process.on("SIGTERM", async () => {
await telemetry.shutdown();
});
process.on("SIGINT", async () => {
await telemetry.shutdown();
});
}
}Express
Singleton Instance
// lib/telemetry.ts
import { IgniterTelemetry } from "@igniter-js/telemetry";
import { LoggerTransportAdapter } from "@igniter-js/telemetry/adapters";
export const telemetry = IgniterTelemetry.create()
.withService("express-api")
.withEnvironment(process.env.NODE_ENV ?? "development")
.addActor("user")
.addScope("organization")
.addTransport(LoggerTransportAdapter.create({
logger: console,
format: process.env.NODE_ENV === "production" ? "json" : "pretty",
}))
.build();Request Middleware
// middleware/telemetry.ts
import { Request, Response, NextFunction } from "express";
import { telemetry } from "../lib/telemetry";
export function telemetryMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const userId = (req.headers["x-user-id"] as string) ?? "anonymous";
const orgId = (req.headers["x-org-id"] as string) ?? "unknown";
telemetry.session()
.actor("user", userId)
.scope("organization", orgId)
.attributes({
"ctx.request.method": req.method,
"ctx.request.path": req.path,
"ctx.request.ip": req.ip,
})
.run(async () => {
telemetry.emit("request.started", {
attributes: { "ctx.request.path": req.path },
});
const startTime = Date.now();
// Track response completion
res.on("finish", () => {
const duration = Date.now() - startTime;
telemetry.emit("request.completed", {
attributes: {
"ctx.request.status": res.statusCode,
"ctx.request.duration_ms": duration,
},
});
});
next();
});
}Route Handler
// routes/orders.ts
import { Router } from "express";
import { telemetry } from "../lib/telemetry";
const router = Router();
router.get("/:id", async (req, res) => {
try {
// Session context auto-inherited from middleware
telemetry.emit("order.fetch.started", {
attributes: { "ctx.order.id": req.params.id },
});
const order = await fetchOrder(req.params.id);
telemetry.emit("order.fetch.completed", {
attributes: {
"ctx.order.id": req.params.id,
"ctx.order.status": order.status,
},
});
res.json(order);
} catch (error) {
telemetry.emit("order.fetch.failed", {
level: "error",
error: {
name: error instanceof Error ? error.name : "Error",
message: error instanceof Error ? error.message : String(error),
},
});
res.status(500).json({ error: "Failed to fetch order" });
}
});
export default router;App Entry Point
// app.ts
import express from "express";
import { telemetry } from "./lib/telemetry";
import { telemetryMiddleware } from "./middleware/telemetry";
import ordersRouter from "./routes/orders";
const app = express();
// Apply telemetry middleware before routes
app.use(telemetryMiddleware);
app.use("/api/orders", ordersRouter);
const server = app.listen(3000, () => {
telemetry.emit("service.booted", {
attributes: { "ctx.service.port": 3000 },
});
});
// Graceful shutdown
process.on("SIGTERM", async () => {
console.log("Shutting down...");
server.close(async () => {
await telemetry.shutdown();
process.exit(0);
});
});Fastify
Singleton Instance
// lib/telemetry.ts
import { IgniterTelemetry } from "@igniter-js/telemetry";
import { LoggerTransportAdapter } from "@igniter-js/telemetry/adapters";
export const telemetry = IgniterTelemetry.create()
.withService("fastify-api")
.withEnvironment(process.env.NODE_ENV ?? "development")
.addActor("user")
.addScope("organization")
.addTransport(LoggerTransportAdapter.create({
logger: console,
format: "json",
}))
.build();Plugin (Hook-Based)
// plugins/telemetry.ts
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import fp from "fastify-plugin";
import { telemetry } from "../lib/telemetry";
async function telemetryPlugin(fastify: FastifyInstance) {
// onRequest hook — enters session context
fastify.addHook("onRequest", async (request, reply) => {
const userId = (request.headers["x-user-id"] as string) ?? "anonymous";
const orgId = (request.headers["x-org-id"] as string) ?? "unknown";
// Store session state for the request lifecycle
(request as any).__telemetrySession = telemetry.session()
.actor("user", userId)
.scope("organization", orgId)
.attributes({
"ctx.request.method": request.method,
"ctx.request.path": request.url,
"ctx.request.ip": request.ip,
});
telemetry.emit("request.started", {
attributes: { "ctx.request.path": request.url },
});
});
// onResponse hook — emit completion
fastify.addHook("onResponse", async (request, reply) => {
const session = (request as any).__telemetrySession;
if (session) {
session.emit("request.completed", {
attributes: {
"ctx.request.status": reply.statusCode,
"ctx.request.duration_ms": reply.elapsedTime,
},
});
await session.end();
}
});
// Decorate with emit helper
fastify.decorateRequest("emitTelemetry", function (
this: FastifyRequest,
name: string,
input?: Record<string, unknown>,
) {
const session = (this as any).__telemetrySession;
if (session) {
session.emit(name, input as any);
} else {
telemetry.emit(name, input as any);
}
});
}
export default fp(telemetryPlugin, { name: "telemetry" });Route Handler
// routes/orders.ts
import { FastifyInstance } from "fastify";
export default async function ordersRoutes(fastify: FastifyInstance) {
fastify.get("/api/orders/:id", async (request, reply) => {
const { id } = request.params as { id: string };
try {
(request as any).emitTelemetry("order.fetch.started", {
attributes: { "ctx.order.id": id },
});
const order = await fetchOrder(id);
(request as any).emitTelemetry("order.fetch.completed", {
attributes: { "ctx.order.id": id, "ctx.order.status": order.status },
});
return order;
} catch (error) {
(request as any).emitTelemetry("order.fetch.failed", {
level: "error",
error: {
name: error instanceof Error ? error.name : "Error",
message: error instanceof Error ? error.message : String(error),
},
});
reply.status(500).send({ error: "Failed to fetch order" });
}
});
}App Entry
// app.ts
import Fastify from "fastify";
import { telemetry } from "./lib/telemetry";
import telemetryPlugin from "./plugins/telemetry";
import ordersRoutes from "./routes/orders";
const fastify = Fastify({ logger: true });
// Register telemetry plugin
await fastify.register(telemetryPlugin);
// Register routes
await fastify.register(ordersRoutes);
// Start
await fastify.listen({ port: 3000 });
telemetry.emit("service.booted", {
attributes: { "ctx.service.port": 3000 },
});
// Graceful shutdown
process.on("SIGTERM", async () => {
await telemetry.shutdown();
await fastify.close();
process.exit(0);
});Background Jobs
For worker processes, background jobs, and cron handlers — use manual session handles:
// workers/invoice-generator.ts
import { telemetry } from "../lib/telemetry";
export async function generateInvoices() {
const session = telemetry.session()
.actor("system", "invoice-generator", { schedule: "daily" });
try {
session.emit("batch.invoices.started");
const invoices = await fetchPendingInvoices();
for (const invoice of invoices) {
await telemetry.session()
.scope("organization", invoice.orgId)
.run(async () => {
session.emit("igniter.billing.invoice.created", {
attributes: {
"ctx.invoice.id": invoice.id,
"ctx.invoice.amount": invoice.amount,
},
});
});
}
session.emit("batch.invoices.completed", {
attributes: { "ctx.batch.count": invoices.length },
});
} catch (error) {
session.emit("batch.invoices.failed", {
level: "error",
error: {
name: error instanceof Error ? error.name : "Error",
message: error instanceof Error ? error.message : String(error),
},
});
} finally {
await session.end();
}
}Next Steps
- Best Practices — Production-tested patterns
- Sessions — Deep dive into session lifecycle
- API Reference — Complete API documentation
Emitting Events
Master the three telemetry emit modes — direct, session, and scoped execution. Learn attributes, levels, error details, source metadata, and event envelope structure.
Getting Started
Build a complete typed telemetry setup from zero to production-ready. Define events with Zod, configure transports, emit with sessions, and shut down gracefully.