AdonisJS agent
Install @unhandled/agent-adonisjs, connect it to a service with a DSN, and see what it records without extra code.
@unhandled/agent-adonisjs is Unhandled's first-party package for AdonisJS. It exports traces, logs, metrics and exceptions from an AdonisJS app to Unhandled over OTLP/HTTP. It is built on @unhandled/node, which runs the OpenTelemetry Node SDK, so every Node SDK option works in config/unhandled.ts. The agent adds route names, the signed-in user, the exception handler hook, and spans for Ace commands, mail and @adonisjs/queue. Use it for an AdonisJS API, a queue worker or a full-stack app. Other Node.js frameworks have their own packages, listed under SDKs.
Requirements
- AdonisJS 6.2 or later, or AdonisJS 7.
- Node.js 24 or later.
- An Unhandled service and its DSN. Give each deployable (API, worker, web app) its own service. See Services.
Install
Add the package
node ace add @unhandled/agent-adonisjsnode ace addinstalls the package with the package manager your app uses, then runs its configure hook. AdonisJS 6.2 has noaddcommand (it arrived in 6.3). On 6.2, install the package with your package manager and runnode ace configure @unhandled/agent-adonisjs. The same command re-runs the hook on any version.The hook creates two files and edits five:
- config/
- unhandled.tsdefineConfig with dev, dsn and environment
- otel.tsstarts the agent before the app loads
- bin/
- server.tsimport '../otel.js' added as the first import
- console.tssame import, same position
- adonisrc.tsregisters @unhandled/agent-adonisjs/unhandled_provider
- start/
- kernel.tsunhandled_middleware added first in the router stack
- env.tsvalidators for APP_ENV and UNHANDLED_DSN
- .envAPP_ENV=development and an empty UNHANDLED_DSN
If
bin/server.tsorbin/console.tsalready imports../otel.js, the command leaves it alone. In.envit only adds keys that are missing, so a second run keeps the DSN you pasted.start/env.tsacceptsdevelopment,stagingorproductionforAPP_ENVand an optional string forUNHANDLED_DSN. The command does not addAPP_NAMEorAPP_VERSION, and the agent does not need them.- config/
Set the DSN
Create the service in your project and copy the DSN from the dialog. Unhandled shows the full DSN once. After that the service page shows only the last four characters of the key. Rotate key on the service issues a new DSN and revokes the old key at once.
Set the DSN and the environment wherever that service runs:
APP_ENV=production UNHANDLED_DSN=https://[email protected]/0b4f6c2e-8a1d-4c3e-9f7a-1234567890abThe DSN holds the ingest endpoint, the API key and the service id. Unhandled files the data under the service that owns the DSN, whatever service name your app reports. The DSN section covers the format and what happens when the DSN does not parse.
Send a request
Start the app and call any route. In development that is the dev server:
node ace serve --hmrIn staging and production, build first and start the built server. The build does not copy
.env, so set the variables where the server runs:node ace build node build/bin/server.jsThe trace appears under the service and environment within seconds. If it does not, work through Troubleshooting.
Already on @adonisjs/otel
Run one package, not both. When otel.ts starts @adonisjs/otel, the configure hook stops without changing any file. When the @adonisjs/otel provider is registered or its SDK is running, the agent's provider throws at boot and names both ways out.
You can keep the official @adonisjs/otel package and send its data to Unhandled, either with unhandledDestinations() and your DSN or with four OTEL_* variables. The setup dialog shows the variables under Already using @adonisjs/otel. Keep @adonisjs/otel explains both and lists what you give up compared to the agent.
Generated files
config/unhandled.ts:
import env from '#start/env'
import { defineConfig } from '@unhandled/agent-adonisjs'
export default defineConfig({
dev: env.get('NODE_ENV') === 'development',
dsn: env.get('UNHANDLED_DSN'),
environment: env.get('APP_ENV'),
})otel.ts:
import { init } from '@unhandled/agent-adonisjs/init'
await init(import.meta.dirname)Keep otel.ts first
// Must stay the first import so auto-instrumentation can patch modules on load
import '../otel.js'
await import('reflect-metadata')
const { Ignitor, prettyPrintError } = await import('@adonisjs/core/ignitor')That is the AdonisJS 7 starter kit's shape. Apps that came from AdonisJS 6 import reflect-metadata and @adonisjs/core statically after otel.js, and that works too: both shapes record request spans.
Environment variables
UNHANDLED_DSNstringConnection string for one service. Without it the agent exports nothing to Unhandled.
APP_ENVproduction | staging | developmentThe generated config passes it to
environment. Set it in every deployment.UNHANDLED_ENVproduction | staging | developmentEnvironment used when the config does not set
environment. Takes precedence overAPP_ENVandNODE_ENV. Any other value throws at boot.UNHANDLED_VERSIONstringPins the code version. Takes precedence over
APP_VERSION.APP_VERSIONstringPins the code version when
UNHANDLED_VERSIONis unset.OTEL_SERVICE_NAMEstringService name for extra destinations and local setups.
APP_NAMEstringService name used when
OTEL_SERVICE_NAMEis unset.
The agent skips placeholder versions such as 0.0.1 and falls back to your platform, CI or git commit. The full order is in Service name and version.
The agent reads these variables from the process environment, so a bad UNHANDLED_ENV throws UnhandledConfigError rather than failing start/env.ts. To validate the optional ones with the rest of your config, add them to start/env.ts:
UNHANDLED_ENV: Env.schema.enum.optional(['development', 'staging', 'production'] as const),
UNHANDLED_VERSION: Env.schema.string.optional(),Local development
The generated config turns on dev when NODE_ENV is development. Dev mode ignores the DSN, so a laptop never writes into a production service. It exports only when you also set endpoint to a local OTLP collector or add an extra destination. Set debug: true to print spans to the console.
Under NODE_ENV=test the agent stays off unless you set enabled yourself.
What is recorded
After the install, with no other code:
- HTTP requests, as SERVER spans named after the route (
GET /orders/:id) withhttp.route,adonis.route.nameand the response status. See HTTP tracing. - Database queries, ClickHouse calls through
@clickhouse/client, Redis commands, outgoing HTTP calls and the other auto-instrumentations that@unhandled/nodeturns on. See What is recorded. - Mail sent with
@adonisjs/mail,@adonisjs/queuejobs and Ace commands. See Mail, queue and Ace commands. - Exceptions from the Adonis exception handler and from failed Ace commands, with source lines and the cause chain. See Users and exceptions.
- Pino logs, Node runtime metrics, and host CPU, memory and network metrics.
Next steps
Every option in config/unhandled.ts and its default.
Wrap your own business logic in spans.
Keep fewer traces from high-volume jobs.
Move an existing @adonisjs/otel setup over.
One of the optional packages for BullMQ, Bentocache, Vercel AI and Mastra, registered through integrations.