Skip to Content
๐Ÿ”ง AdvancedLogging

Last Updated: 3/20/2026


Logging

Kysely provides built-in query and error logging through the log option in KyselyConfig. This is simpler than writing a plugin and covers most logging needs.

Basic Logging

Pass an array of log levels to the log option:

import { Kysely } from 'kysely' const db = new Kysely<Database>({ dialect: myDialect, log: ['query', 'error'] })

This enables logging for both queries and errors. Kysely will log to the console using the default logger (see src/util/log.ts).

Log Levels

Two log levels are available (defined in src/util/log.ts):

  • 'query' โ€” Logs every query with its SQL and duration.
  • 'error' โ€” Logs query errors with the error message, SQL, and duration.

You can enable one or both:

// Log only queries const db = new Kysely<Database>({ dialect: myDialect, log: ['query'] }) // Log only errors const db = new Kysely<Database>({ dialect: myDialect, log: ['error'] }) // Log both const db = new Kysely<Database>({ dialect: myDialect, log: ['query', 'error'] })

Default Logger Output

The default logger logs to the console:

const result = await db .selectFrom('person') .where('age', '>', 18) .selectAll() .execute()

Console output:

kysely:query: select * from "person" where "age" > $1 kysely:query: duration: 2.3ms

For errors:

kysely:error: Error: relation "person" does not exist at ...

Custom Logger Function

For more control, pass a function instead of an array. The function receives a LogEvent object (defined in src/util/log.ts):

import type { LogEvent } from 'kysely' const db = new Kysely<Database>({ dialect: myDialect, log(event: LogEvent) { if (event.level === 'query') { console.log('SQL:', event.query.sql) console.log('Parameters:', event.query.parameters) console.log('Duration:', event.queryDurationMillis, 'ms') } else if (event.level === 'error') { console.error('Query failed:', event.error) console.error('SQL:', event.query.sql) console.error('Duration:', event.queryDurationMillis, 'ms') } } })

LogEvent Structure

The LogEvent type is a union of QueryLogEvent and ErrorLogEvent:

QueryLogEvent

interface QueryLogEvent { level: 'query' isStream?: boolean query: CompiledQuery queryDurationMillis: number }
  • level โ€” Always 'query'.
  • isStream โ€” true if the query is a streaming query (rare).
  • query โ€” The compiled query with sql and parameters properties.
  • queryDurationMillis โ€” How long the query took to execute.

ErrorLogEvent

interface ErrorLogEvent { level: 'error' error: unknown query: CompiledQuery queryDurationMillis: number }
  • level โ€” Always 'error'.
  • error โ€” The error that was thrown.
  • query โ€” The compiled query that failed.
  • queryDurationMillis โ€” How long the query took before failing.

Structured Logging

Use a custom logger function to integrate with structured logging libraries:

import { createLogger } from 'winston' const logger = createLogger({ // winston configuration }) const db = new Kysely<Database>({ dialect: myDialect, log(event) { if (event.level === 'query') { logger.info('Query executed', { sql: event.query.sql, parameters: event.query.parameters, duration: event.queryDurationMillis }) } else if (event.level === 'error') { logger.error('Query failed', { error: event.error, sql: event.query.sql, parameters: event.query.parameters, duration: event.queryDurationMillis }) } } })

Masking PII

If your queries contain personally identifiable information (PII) in parameters, mask them before logging:

const db = new Kysely<Database>({ dialect: myDialect, log(event) { if (event.level === 'query') { console.log('SQL:', event.query.sql) console.log('Parameters:', maskPII(event.query.parameters)) console.log('Duration:', event.queryDurationMillis, 'ms') } } }) function maskPII(parameters: readonly unknown[]): unknown[] { return parameters.map((param) => { if (typeof param === 'string' && param.includes('@')) { return '[REDACTED EMAIL]' } // Add more masking rules as needed return param }) }

Conditional Logging

Enable logging only in development:

const db = new Kysely<Database>({ dialect: myDialect, log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : undefined })

Or use a custom logger that checks the environment:

const db = new Kysely<Database>({ dialect: myDialect, log(event) { if (process.env.NODE_ENV === 'development') { if (event.level === 'query') { console.log('SQL:', event.query.sql) } } } })

Logging Slow Queries

Log only queries that exceed a threshold:

const SLOW_QUERY_THRESHOLD_MS = 100 const db = new Kysely<Database>({ dialect: myDialect, log(event) { if (event.level === 'query' && event.queryDurationMillis > SLOW_QUERY_THRESHOLD_MS) { console.warn('Slow query detected:', { sql: event.query.sql, duration: event.queryDurationMillis }) } } })

Async Loggers

The logger function can be async:

const db = new Kysely<Database>({ dialect: myDialect, async log(event) { if (event.level === 'query') { await sendToLoggingService({ sql: event.query.sql, duration: event.queryDurationMillis }) } } })

Note: Kysely doesnโ€™t wait for the logger to complete before returning the query result. If the logger throws an error, itโ€™s silently ignored.

Logging vs. Plugins

The log option is simpler than writing a plugin, but plugins offer more power:

FeatureLoggingPlugins
Log queriesโœ…โœ…
Log errorsโœ…โœ…
Transform queriesโŒโœ…
Transform resultsโŒโœ…
Access operation node treeโŒโœ…

Use logging for observability. Use plugins for query transformation.

Combining Logging and Plugins

You can use both:

const db = new Kysely<Database>({ dialect: myDialect, log: ['query', 'error'], plugins: [new CamelCasePlugin()] })

Plugins run before logging. If a plugin transforms a query, the logger sees the transformed query.

Whatโ€™s Next

  • Plugins โ€” Learn how to write custom plugins for query transformation.
  • Dialects โ€” Understand how different databases handle logging differently.
  • Reusable Helpers โ€” Build reusable query helpers that work with logging.