# knex-tiny-logger

[![](https://img.shields.io/npm/v/knex-tiny-logger.svg?style=flat-square)](https://npmjs.com/package/knex-tiny-logger)

> Zero-config query logging for Knex. Tiny by default, flexible when needed.

## Install

```bash
# npm
npm install knex-tiny-logger knex

# pnpm
pnpm add knex-tiny-logger knex

# yarn
yarn add knex-tiny-logger knex

# bun
bun add knex-tiny-logger knex

# aube
aube add knex-tiny-logger knex
```

Requires Node.js 20 or newer. Bun 1.3 or newer is also supported.

## Usage

```ts
import createKnex from 'knex'
import knexTinyLogger from 'knex-tiny-logger'

const knex = knexTinyLogger(
  createKnex({
    client: 'pg',
    connection: process.env.DATABASE_URL,
  }),
)
```

By default, `knexTinyLogger` uses `defaultLogger`: plain string logs, no extra runtime dependencies.

```text
SQL (3.421 ms) select 1 as id
SQL ERROR (2.104 ms) select * from missing_table
```

## Default Logger

```ts
import knexTinyLogger, { defaultLogger } from 'knex-tiny-logger'

knexTinyLogger(knex, {
  logger: defaultLogger({ bindings: false }),
})
```

The default logger formats SQL before writing it. By default, it asks Knex to interpolate bindings into the logged SQL.

Set `bindings: false` to write the original SQL with placeholders, or replace formatting completely:

```ts
knexTinyLogger(knex, {
  logger: defaultLogger({
    formatter(query) {
      return query.sql
    },
  }),
})
```

The built-in formatter is also exported if you want the same SQL formatting in a custom logger:

```ts
import { defaultQueryFormatter } from 'knex-tiny-logger'

const formatQuery = defaultQueryFormatter()

knexTinyLogger(knex, {
  logger: {
    onEnd(query) {
      console.log(formatQuery(query), query.durationMs)
    },
  },
})
```

`write` can be a function or a stream-like target:

```ts
knexTinyLogger(knex, {
  logger: defaultLogger({ write: process.stdout }),
})
```

## Colorful Logs

The colorful logger is the same string logger experience, with output colored by query
state. It supports the same `bindings`, `formatter`, and `write` options as `defaultLogger`,
and has no extra runtime dependencies.

By default the whole message is colored by state: the `SQL` / `SQL ERROR` label and the SQL
body are cyan for successful queries and red for failed ones.

```ts
import knexTinyLogger from 'knex-tiny-logger'
import { colorfulLogger } from 'knex-tiny-logger/colorful'

knexTinyLogger(knex, {
  logger: colorfulLogger(),
})
```

### Syntax highlighting

Set `highlight: true` to syntax-highlight the SQL body instead. The label still carries the
query state (cyan or red); the body's tokens are colored by an ANSI theme. `formatter`, when
provided, controls the SQL string before highlighting is applied.

```ts
import { colorfulLogger, colorfulSyntaxThemes } from 'knex-tiny-logger/colorful'

knexTinyLogger(knex, {
  logger: colorfulLogger({ highlight: true, theme: colorfulSyntaxThemes.dracula }),
})
```

#### Themes

`colorfulSyntaxThemes.default` is used when no theme is given. It's a 16-color ANSI theme
that adapts to your terminal's palette, so it works everywhere.

The named themes are fixed 24-bit truecolor and need a truecolor-capable terminal:

- **Dark** — `dracula`, `nord`, `monokai`, `oneDark`, `solarizedDark`, `tokyoNight`, `catppuccinMocha`
- **Light** — `solarizedLight`, `githubLight`, `oneLight`, `catppuccinLatte`

#### Customizing a theme

Call `.extend()` to override individual token colors with raw ANSI strings, or pass `false`
to leave a token uncolored:

```ts
knexTinyLogger(knex, {
  logger: colorfulLogger({
    highlight: true,
    theme: colorfulSyntaxThemes.dracula.extend({
      keyword: false,
      fn: '\x1b[31m',
    }),
  }),
})
```

You can also reuse the same syntax coloring in custom formatters:

```ts
import { defaultQueryFormatter } from 'knex-tiny-logger'
import { colorfulSyntaxFormatter, colorfulSyntaxThemes } from 'knex-tiny-logger/colorful'

const formatter = colorfulSyntaxFormatter(defaultQueryFormatter(), {
  theme: colorfulSyntaxThemes.solarizedLight,
})
```

## Pino

The pino adapter keeps query data structured.

```ts
import knexTinyLogger from 'knex-tiny-logger'
import { pinoLogger } from 'knex-tiny-logger/pino'

knexTinyLogger(knex, {
  logger: pinoLogger(pino),
})
```

The pino adapter logs `sql`, `bindings`, and `durationMs`; errors also include `err`.
Bindings are included by default. Set `bindings: false` to omit them from the structured payload.

## Custom Logger

```ts
import type { Logger } from 'knex-tiny-logger'

const logger: Logger = {
  onEnd(query) {
    console.log(query.sql, query.durationMs)
  },
  onError(query) {
    console.error(query.sql, query.error)
  },
}

knexTinyLogger(knex, { logger })
```

Passing a function uses the default string formatting and writes each log message to that function:

```ts
import { defaultLogger } from 'knex-tiny-logger'

knexTinyLogger(knex, { logger: console.log })

// same as
knexTinyLogger(knex, {
  logger: defaultLogger({ write: console.log }),
})
```

## Logger Errors

Logger errors are caught so logging does not break queries. By default they are reported with `console.error`.

```ts
knexTinyLogger(knex, {
  logger,
  onLoggerError(event) {
    diagnostics.warn(event.error)
  },
})
```

## Tracing

For lower-level integrations:

```ts
import { createTracer } from 'knex-tiny-logger/tracer'

const spans = new Map()

const tracer = createTracer(knex, {
  onStart(query) {
    spans.set(query.queryId, tracerProvider.startSpan('sql', {
      sql: query.sql,
      bindings: query.bindings,
    }))
  },
  onEnd(query) {
    spans.get(query.queryId)?.end({ durationMs: query.durationMs })
    spans.delete(query.queryId)
  },
  onError(query) {
    spans.get(query.queryId)?.fail(query.error)
    spans.delete(query.queryId)
  },
})

tracer.dispose()
```

The tracer exposes complete query lifecycles with duration, SQL, bindings, and
errors. Knex transaction control statements are ignored because they do not
emit matching successful responses.

### OpenTelemetry

<details>
<summary>Manual span bridge example</summary>

Install the OpenTelemetry API and tracing SDK:

```bash
pnpm add @opentelemetry/api @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node
```

Then bridge Knex query events to OpenTelemetry spans:

```ts
import { type Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
import { createTracer } from 'knex-tiny-logger/tracer'

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({ 'service.name': 'my-service' }),
  spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
})
provider.register()

const otelTracer = provider.getTracer('my-service')
const spans = new Map<string, Span>()

const tracer = createTracer(knex, {
  onStart(query) {
    spans.set(
      query.queryId,
      otelTracer.startSpan('postgresql', {
        kind: SpanKind.CLIENT,
        startTime: query.startedAt,
        attributes: {
          'db.system.name': 'postgresql',
          'db.query.text': query.sql,
        },
      }),
    )
  },
  onEnd(query) {
    spans.get(query.queryId)?.end()
    spans.delete(query.queryId)
  },
  onError(query) {
    const span = spans.get(query.queryId)
    if (span && query.error instanceof Error) {
      span.recordException(query.error)
      span.setAttribute('error.type', query.error.name)
    }
    span?.setStatus({ code: SpanStatusCode.ERROR })
    span?.end()
    spans.delete(query.queryId)
  },
})

// During application shutdown, after outstanding queries settle:
tracer.dispose()
await provider.shutdown()
```

The example intentionally omits bindings because they often contain secrets or personal data.
If raw SQL contains literals, sanitize it before recording `db.query.text`.
Some drivers also include rendered SQL in error messages; redact `query.error` before
`recordException` when that data is sensitive.

See the [runnable SQLite example](examples/sqlite-opentelemetry.mjs). Replace
`ConsoleSpanExporter` with an OTLP exporter to send the same spans to a collector.

</details>

## License

[MIT](LICENSE.md)
