docs

Kysely

Trace Kysely queries, streams and transactions and export pg pool gauges with @unhandled/instrumentation-kysely.

@unhandled/instrumentation-kysely wraps a Kysely dialect so every query, stream and transaction becomes a CLIENT span in queries. It also exports connection pool gauges for pg. It supports Kysely ^0.28.16 and ^0.29.6 with PostgresDialect (pg) and PostgresJSDialect (kysely-postgres-js).

The package is manual only. It has no /integration entry, so the AdonisJS agent never enables it for you.

Install

Terminal
npm install @unhandled/instrumentation-kysely

Wrap the dialect

Create the dialect after OpenTelemetry has started and pass the wrapped dialect to Kysely:

ts
import { Kysely, PostgresDialect } from 'kysely'
import pg from 'pg'
import { observeDialect } from '@unhandled/instrumentation-kysely'
import type { ShopDatabase } from '#types/db'

const connection = { host: 'db.internal', port: 5432, database: 'shop' }
export const pool = new pg.Pool({ ...connection, user: 'shop', max: 10 })

export const db = new Kysely<ShopDatabase>({
  dialect: observeDialect({
    dialect: new PostgresDialect({ pool }),
    dbSystem: 'postgresql',
    connection,
  }),
})

Options

OptionDefaultEffect
dialectrequiredThe dialect to wrap.
dbSystemrequiredValue of db.system.name, such as postgresql.
recordQueryTexttruefalse leaves db.query.text off every span.
recordErrorstruefalse keeps ERROR status but drops the status message and the exception event.
requireParentSpanfalseOnly create spans when a span is already active.
connectionnone{ host, port, database }, reported as server.address, server.port and db.namespace.

Query parameters are never recorded, but literals inlined in SQL are, and error messages can echo data. Turn off recordQueryText and recordErrors if that matters. requireParentSpan keeps queries that run outside requests and jobs from showing up as background spans.

What it records

Every span is CLIENT kind and named <OPERATION> <table>, such as SELECT orders, read from the SQL text. Without a table the name is the operation alone; without a parsable operation it is the dbSystem value.

  • A transaction produces a BEGIN span, one span per query, then COMMIT or ROLLBACK.
  • A stream is one span that lasts until the stream is drained, closed or fails.
AttributeValue
db.system.namedbSystem
db.query.textSQL with placeholders, when recordQueryText is on
db.operation.nameUpper-cased first keyword, such as SELECT
db.collection.nameTarget table, when it can be parsed
db.namespaceconnection.database
server.addressconnection.host
server.portconnection.port
db.transactiontrue on BEGIN, COMMIT, ROLLBACK and queries inside a transaction
error.typeError class name on failure

The SQL parser is exported as parseSql(sql), returning { operation, collection }.

Pool metrics

registerPgPoolMetrics() reports four gauges for a pg.Pool. Call it after the OpenTelemetry metrics provider is set up, and call the returned function before pool.end().

ts
import { registerPgPoolMetrics } from '@unhandled/instrumentation-kysely'
import { pool } from '#services/db'

const stopPoolMetrics = registerPgPoolMetrics({ pool, max: 10, poolName: 'primary' })

process.once('SIGTERM', async () => {
  stopPoolMetrics()
  await pool.end()
})
GaugeMeaning
db.client.connection.usedConnections checked out
db.client.connection.idleOpen connections sitting idle
db.client.connection.pending_requestsCallers waiting for a connection
db.client.connection.maxThe max you passed

Each point carries db.client.connection.pool.name set to poolName. The names are exported as POOL_METRIC_NAMES. Anything other than a pg.Pool throws a TypeError; postgres.js exposes no pool counters. The gauges show up in metrics.

Duplicate spans

If you also enable OpenTelemetry's pg instrumentation, every query is traced twice, once here and once at the driver. Keep one of them. postgres.js has no OpenTelemetry instrumentation of its own, so wrapping its dialect here never duplicates spans.