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.3msFor 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โtrueif the query is a streaming query (rare).queryโ The compiled query withsqlandparametersproperties.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:
| Feature | Logging | Plugins |
|---|---|---|
| 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.