Skip to main content
This guide covers everything you need to instrument your AdonisJS application with Monocle. You will learn how to:
  • Install and configure the agent
  • Track authenticated users across traces
  • Capture exceptions and custom messages
  • Use tracing helpers and decorators
  • Configure instrumentations (mail, cache, jobs, AI, MCP)
  • Fan out telemetry to multiple destinations

Prerequisites

  • AdonisJS 6.2.0+ or AdonisJS 7.0.0+
  • A Monocle account with an API key

Installation

Install and configure the package using the Ace CLI.
node ace add @monocle.sh/adonisjs-agent
  1. Creates config/monocle.ts configuration file
  2. Creates otel.ts initialization file at project root
  3. Adds the otel.ts import as the first import in bin/server.ts and bin/console.ts
  4. Registers the Monocle provider in adonisrc.ts
  5. Registers the Monocle middleware as the first router middleware
  6. Adds required environment variables to .env and start/env.ts
After running the command, add your API key to .env:
.env
MONOCLE_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
You can find your API key in your application settings on the Monocle dashboard. That’s it. Your application now has automatic tracing for HTTP requests, database queries, Redis operations, and more.
The import order is critical.OpenTelemetry must initialize before any other code loads. The SDK needs to patch libraries like http, pg, and redis before they are imported. That is why otel.ts is imported as the very first line in bin/server.ts.If you move or remove the import '../otel.js' line, auto-instrumentation will not work.

Environment Variables

The agent requires these environment variables:
VariableDescriptionRequired
MONOCLE_API_KEYYour Monocle API keyYes
APP_NAMEService name for identification in MonocleYes
APP_VERSIONService version (e.g., git sha, semver)Yes
APP_ENVEnvironment: development, staging, or productionYes
The APP_ENV variable determines which environment your telemetry data is associated with in Monocle. You can filter traces, logs, and metrics by environment using the environment selector in the dashboard.

Configuration

The configuration file is located at config/monocle.ts. Here is a minimal setup:
config/monocle.ts
import { defineConfig } from "@monocle.sh/adonisjs-agent";
import env from "#start/env";

export default defineConfig({
  apiKey: env.get("MONOCLE_API_KEY"),
  serviceName: env.get("APP_NAME"),
  serviceVersion: env.get("APP_VERSION"),
  environment: env.get("APP_ENV"),
});

Full configuration reference

All options are optional except apiKey, serviceName, serviceVersion, and environment.
config/monocle.ts
import { defineConfig } from "@monocle.sh/adonisjs-agent";
import env from "#start/env";

export default defineConfig({
  apiKey: env.get("MONOCLE_API_KEY"),
  serviceName: env.get("APP_NAME"),
  serviceVersion: env.get("APP_VERSION"),
  environment: env.get("APP_ENV"),

  // Custom ingestion endpoint (default: https://ingest.monocle.sh)
  endpoint: "https://ingest.monocle.sh",

  // Skip OPTIONS/CORS preflight requests (default: true)
  ignoreOptionsRequests: true,

  // Enable gzip compression (default: true)
  compression: true,

  // Trace batching configuration
  batch: {
    maxExportBatchSize: 512,
    scheduledDelayMillis: 5000,
    exportTimeoutMillis: 30000,
    maxQueueSize: 2048,
  },

  // Host metrics: CPU, memory, network (default: enabled)
  hostMetrics: {
    enabled: true,
  },

  // Sampling ratio (0.0 to 1.0). Use 0.1 for high-traffic production.
  // Default: 1.0 (100% of traces sampled)
  samplingRatio: 1.0,

  // Feature toggles (all enabled by default except CLI)
  mail: { enabled: true },
  cache: { enabled: true },
  queue: { enabled: true },
  bullmq: { enabled: true },
  ai: { enabled: true },
  cli: { enabled: true },
});

Customizing instrumentations

You can configure individual OpenTelemetry instrumentations by passing options keyed by their package name.
config/monocle.ts
export default defineConfig({
  // ...

  instrumentations: {
    "@opentelemetry/instrumentation-http": {
      // Add URLs to ignore (merged with defaults by default)
      ignoredUrls: ["/internal/*", "/api/ping"],

      // Set to false to replace default ignored URLs instead of merging
      mergeIgnoredUrls: true,

      // Ignore static files like css, js, images (default: true)
      ignoreStaticFiles: true,
    },

    // Disable a specific instrumentation entirely
    "@opentelemetry/instrumentation-pg": { enabled: false },
  },
});
Default ignored URLs include /health, /healthz, /ready, /readiness, /metrics, /favicon.ico and other common health check paths.

User Context

To associate telemetry data with authenticated users, call Monocle.setUser() in your authentication middleware. This lets you filter traces and exceptions by user in the dashboard.
app/middleware/silent_auth_middleware.ts
import type { NextFn } from "@adonisjs/core/types/http";
import type { HttpContext } from "@adonisjs/core/http";
import { Monocle } from "@monocle.sh/adonisjs-agent";

export default class SilentAuthMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    await ctx.auth.check();

    if (ctx.auth.user) {
      Monocle.setUser({
        id: ctx.auth.user.id,
        email: ctx.auth.user.email,
        name: ctx.auth.user.fullName,
      });
    }

    return next();
  }
}
The name field takes priority in the Monocle UI, falling back to email, then id.
Do not include sensitive data (passwords, tokens, API keys) in user attributes. These values are sent to Monocle and will be visible in trace viewers.

Exception Tracking

Exceptions thrown during a request are automatically captured by the agent. It hooks into AdonisJS’s ExceptionHandler.report() method and respects your ignore rules (ignoreExceptions, ignoreStatuses, ignoreCodes).

Manual exception capture

You can manually capture exceptions anywhere in your code.
app/services/payment_service.ts
import { Monocle } from "@monocle.sh/adonisjs-agent";

try {
  await processPayment(order);
} catch (error) {
  Monocle.captureException(error, {
    user: { id: "123", email: "user@example.com" },
    tags: { component: "payment" },
    extra: { orderId: order.id, amount: order.total },
  });
  throw error;
}

Capture messages

Use Monocle.captureMessage() to capture custom messages when you want to track something that is not an exception. Messages appear in the Exceptions dashboard alongside errors.
app/services/order_service.ts
import { Monocle } from "@monocle.sh/adonisjs-agent";

// Simple message
Monocle.captureMessage("User failed to complete checkout");

// With severity level and context
Monocle.captureMessage("Order processing took too long", {
  level: "warning",
  user: { id: "123" },
  tags: { feature: "checkout" },
  extra: { orderId: "order-456", durationMs: 15000 },
});
Supported levels: debug, info, log, warning, error, fatal.

Tracing Helpers

Creating custom spans

Use record() to wrap a section of code in a span. This is useful when you want to measure the duration of a specific operation that is not automatically instrumented.
app/services/report_service.ts
import { record } from "@monocle.sh/adonisjs-agent/helpers";

const report = await record("report.generate", async (span) => {
  span.setAttributes({ "report.type": "monthly", "report.userId": userId });

  const data = await fetchReportData(userId);
  return generatePdf(data);
});
record() handles both sync and async callbacks, automatically records exceptions, and closes the span when done.

Decorators

The @span() decorator wraps a single method in a span. The @spanAll() decorator wraps every public method in a class.
app/services/order_service.ts
import { inject } from "@adonisjs/core";
import { span, spanAll } from "@monocle.sh/adonisjs-agent/decorators";

// Wrap a single method
export class OrderService {
  @span()
  async findById(id: string) {
    return Order.findOrFail(id);
  }

  @span({ name: "order.create", attributes: { operation: "create" } })
  async create(data: CreateOrderData) {
    return Order.create(data);
  }
}

// Or wrap all methods at once
@spanAll({ prefix: "user" })
export class UserService {
  async findById(id: string) { /* span: user.findById */ }
  async create(data: UserData) { /* span: user.create */ }
}

Other helpers

import {
  getCurrentSpan,
  setAttributes,
  recordEvent,
  injectTraceContext,
  extractTraceContext,
} from "@monocle.sh/adonisjs-agent/helpers";

// Get the current active span
const span = getCurrentSpan();

// Add attributes to the current span
setAttributes({ "order.total": 99.99, "order.currency": "EUR" });

// Record an event on the current span
recordEvent("order.confirmed", { orderId: "123" });

// Distributed tracing: inject context into outgoing headers
const headers = {};
injectTraceContext(headers);

// Distributed tracing: extract context from incoming headers
const context = extractTraceContext(request.headers);

Instrumentations

HTTP

HTTP tracing is configured with the top-level http option. Alongside the standard HTTP instrumentation options like ignoredUrls, Monocle adds two HTTP-specific features:
  • sanitizeUrls to redact sensitive query parameters from URL span attributes
  • captureBodies to record request and response payloads on spans
Both features are disabled by default. Configure sanitizeUrls to enable URL sanitization. For body capture, set captureBodies.enabled: true, or opt in per side with incoming.request.enabled, incoming.response.enabled, and so on.
config/monocle.ts
export default defineConfig({
  // ...

  // High-level HTTP tracing options.
  http: {
    // Ignore noisy routes before spans are exported.
    ignoredUrls: ["/internal/*", "/api/ping"],

    // Redact sensitive query params in URL span attributes.
    sanitizeUrls: {
      queryParams: ["code", "state", "token", "id_token", "session_state"],
      censor: "REDACTED",
    },

    // Capture request and response payloads on HTTP spans.
    captureBodies: {
      enabled: true,

      // Server-side traffic handled by your Adonis app.
      incoming: {
        request: {
          // Limit the number of bytes stored on the span.
          maxBodySize: 16_384,
        },
        response: {},
      },

      // Client-side traffic instrumented by node:http / node:https.
      outgoing: {
        request: {},
        response: {},
      },

      // Declarative redaction merged with Monocle's built-in safe defaults.
      redact: {
        bodyKeys: ["password", "token", "secret"],
        headers: ["authorization", "cookie"],
        queryParams: ["token"],
        censor: "REDACTED",
      },

      // Final hook to mutate or replace the captured payload.
      transform: (record) => {
        if (!record.http.url?.includes("/oauth/callback")) return;

        record.http.body = { redacted: true };
      },
    },
  },
});

URL sanitization

sanitizeUrls rewrites URL span attributes such as http.url, url.full, http.target, and url.query. If you do not set sanitizeUrls.censor, Monocle uses ***.
config/monocle.ts
export default defineConfig({
  // ...

  http: {
    sanitizeUrls: {
      queryParams: ["code", "state", "id_token", "session_state"],
      censor: "REDACTED",
    },
  },
});
For example, an OAuth callback like:
/oauth/callback?code=oauth-code&state=oauth-state&id_token=jwt-token
becomes:
/oauth/callback?code=REDACTED&state=REDACTED&id_token=REDACTED

Body capture scope

incoming captures traffic handled by your Adonis app:
  • incoming.request for the HTTP request body sent to your server
  • incoming.response for the HTTP response body returned by your server
outgoing captures traffic created by Node HTTP clients inside your app:
  • outgoing.request for the request body your app sends to another service
  • outgoing.response for the response body returned by that service
captureBodies.outgoing only applies to traffic instrumented by @opentelemetry/instrumentation-http, which means node:http and node:https. It does not cover fetch or undici.

Body capture defaults

captureBodies is opt-in because payload capture can expose sensitive data. When enabled, Monocle:
  • captures application/json and application/x-www-form-urlencoded by default
  • stores at most 10_000 bytes per request or response unless you increase maxBodySize
  • shows captured payloads in the HTTP Body tab of the trace details view

Redaction order

captureBodies.redact applies declarative redaction rules first. captureBodies.transform runs after that and lets you mutate or replace the captured payload programmatically. The declarative redact rules extend Monocle’s built-in safe defaults. Common secrets such as password, token, code, access_token, refresh_token, authorization, and cookie are redacted automatically even if you do not list them yourself.

Advanced overrides

If you need low-level OpenTelemetry overrides, you can still configure instrumentations["@opentelemetry/instrumentation-http"]. When both are present, the low-level instrumentation config takes priority over the top-level http option.
Body capture should be enabled deliberately and reviewed carefully. Payloads can contain credentials, personal data, or OAuth values. Keep capture scoped to the endpoints you need, prefer the smallest possible maxBodySize, and use redact or transform to remove anything sensitive before it is exported.

Mail

If @adonisjs/mail is installed, the agent automatically instruments all emails sent through the mail service. Every email appears as a span in your traces with subject, recipients, and transport info. You can monitor email activity in the dedicated Emails dashboard. To disable:
config/monocle.ts
export default defineConfig({
  // ...
  mail: false,
});

Cache

If @adonisjs/cache (Bentocache) is installed, the agent instruments cache operations (get, set, getOrSet, delete, etc.). You can monitor hit rates, latency, and per-key breakdowns in the Cache dashboard. Cache keys are automatically sanitized to reduce cardinality. UUIDs, numeric IDs, and hex hashes are replaced with *. For example, users:550e8400-e29b-41d4-a716-446655440000 becomes users:*.
config/monocle.ts
export default defineConfig({
  // ...
  cache: {
    // Only trace cache ops within a request/command (default: false)
    requireParentSpan: false,

    // Include cache key names in spans (default: true)
    includeKeys: true,

    // Custom key sanitizer
    keySanitizer: (key) => key.replace(/session:.+/, "session:*"),
  },
});
To disable entirely:
config/monocle.ts
export default defineConfig({
  // ...
  cache: false,
});

Background Jobs

@adonisjs/queue

If @adonisjs/queue (boringqueue) is installed, the agent instruments job dispatch and execution. Jobs appear in the Jobs dashboard with execution count, error rate, and latency metrics.
config/monocle.ts
export default defineConfig({
  // ...
  queue: {
    // How consumer spans relate to producer spans
    // 'link': consumer span links to producer (default)
    // 'parent': consumer span is a child of the producer span
    executionSpanLinkMode: "link",
  },
});

BullMQ

BullMQ is also supported. The agent instruments Queue.add, Queue.addBulk, FlowProducer.add, FlowProducer.addBulk, and Worker processing. Jobs show up in the same Jobs dashboard.
config/monocle.ts
export default defineConfig({
  // ...
  bullmq: {
    executionSpanLinkMode: "link",
  },
});
To disable either:
config/monocle.ts
export default defineConfig({
  // ...
  queue: false,
  bullmq: false,
});

AI SDK

If the Vercel AI SDK (ai package) is installed, the agent auto-instruments generateText, streamText, generateObject, streamObject, embed, embedMany, and rerank calls. You can monitor AI activity, costs, and token usage in the AI Agents dashboard.
config/monocle.ts
export default defineConfig({
  // ...
  ai: {
    // Record prompt/input data in spans (default: true)
    recordInputs: true,

    // Record response/output data in spans (default: true)
    recordOutputs: true,
  },
});

Conversation tracking

Use withConversationId() to group multiple AI calls into a single conversation flow. This enables the Conversations tab in the AI dashboard.
app/services/chat_service.ts
import { withConversationId, getConversationId } from "@monocle.sh/adonisjs-agent/ai";

const response = await withConversationId("conv-123", async () => {
  return generateText({
    model: openai("gpt-4o"),
    prompt: "Hello!",
  });
});
To disable:
config/monocle.ts
export default defineConfig({
  // ...
  ai: false,
});

CLI Commands

Ace CLI command execution is traced by default. Scaffolding commands (make:*, generate:*) and long-running queue workers are excluded automatically. Command arguments and flags are captured as span attributes (cli.args.*, cli.flags.*).
config/monocle.ts
export default defineConfig({
  // ...
  cli: {
    // Glob patterns to include (all commands by default)
    include: ["migration:*", "db:*"],
    // Glob patterns to exclude (defaults below)
    exclude: ["make:*", "generate:*", "queue:work", "queue:listen"],
  },
});
To disable entirely:
config/monocle.ts
export default defineConfig({
  // ...
  cli: false,
});
CLI tracing requires otel.ts in bin/console.ts.Just like bin/server.ts, your bin/console.ts file must import otel.ts as its very first import for CLI instrumentation to work. The node ace configure command adds this automatically, but if you set up manually or upgraded from an older version, make sure it is there:
bin/console.ts
import '../otel.js' // Must be the first import

import { Kernel } from '@adonisjs/core/ace'
// ...

MCP Server

If you are building an MCP server with @modelcontextprotocol/sdk, you can instrument it to track tool calls, clients, and performance in the MCP Monitoring dashboard.
app/mcp/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { instrumentMcpServer } from "@monocle.sh/adonisjs-agent/mcp";

const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
});

// Instrument before registering tools
instrumentMcpServer(server, {
  // Capture tool arguments in spans (default: false)
  recordInputs: true,
  // Capture tool results in spans (default: false)
  recordOutputs: true,
});

// Register tools, then connect transport as usual
server.tool("my-tool", async () => { /* ... */ });
server.connect(transport);

Multi-Destination OTLP

You can send telemetry to additional OTLP backends alongside Monocle. The Monocle destination is always injected automatically.
config/monocle.ts
import { defineConfig, destinations } from "@monocle.sh/adonisjs-agent";

export default defineConfig({
  // ...

  destinations: [
    destinations.otlp({
      name: "grafana",
      url: "https://otlp-gateway.grafana.net/otlp",
      headers: {
        Authorization: `Basic ${btoa("instance_id:api_key")}`,
      },
    }),
  ],
});
Each destination receives the full telemetry data (traces, metrics, logs).

Disabling Telemetry

If no API key is provided, telemetry is automatically disabled. This is useful for local development.
.env.development
MONOCLE_API_KEY=

Migrating from @adonisjs/otel

If you are already using @adonisjs/otel, switching to the Monocle agent is straightforward. The agent wraps @adonisjs/otel and provides the same API with additional capabilities.

1. Swap the package

node ace remove @adonisjs/otel
node ace add @monocle.sh/adonisjs-agent

2. Rename config file

Rename config/otel.ts to config/monocle.ts and replace defineConfig import:
config/monocle.ts
import { defineConfig } from "@adonisjs/otel"; 
import { defineConfig } from "@monocle.sh/adonisjs-agent"; 

3. Update all imports

Replace all @adonisjs/otel imports with their Monocle equivalents:
import { record, getCurrentSpan } from "@adonisjs/otel/helpers"; 
import { record, getCurrentSpan } from "@monocle.sh/adonisjs-agent/helpers"; 

import { span, spanAll } from "@adonisjs/otel/decorators"; 
import { span, spanAll } from "@monocle.sh/adonisjs-agent/decorators"; 

Why use Monocle’s helpers

The Monocle agent re-exports most helpers from @adonisjs/otel unchanged (getCurrentSpan, setAttributes, recordEvent, etc.). However, record, handleError, @span, and @spanAll are enhanced versions that add source code context extraction. When an exception is caught by these helpers, the agent reads the actual source files from disk and attaches the surrounding lines of code to the span. This is what powers the source code preview in the Monocle exception viewer. You get the exact line that errored with surrounding context, without needing source maps. The agent also fully serializes the error cause chain (Error.cause), including AggregateError children, so nested errors are never lost. These enhancements only work when importing from @monocle.sh/adonisjs-agent. If you keep using @adonisjs/otel imports directly, you will get standard OpenTelemetry error recording without the extra context.