Skip to content

Commit

Permalink
feat: bunslog.QueryHook for Bun logging using log/slog (#904)
Browse files Browse the repository at this point in the history
* feat: logging package for Bun that uses `log/slog`

* fix: documentation and log level adjustments

* chore: bump Go version to 1.21.x in tests
  • Loading branch information
hirasawayuki authored Sep 27, 2023
1 parent 2dee174 commit 4953367
Show file tree
Hide file tree
Showing 6 changed files with 451 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [1.19.x, 1.20.x]
go-version: [1.21.x]

services:
postgres:
Expand Down
77 changes: 77 additions & 0 deletions extra/bunslog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# bunslog

bunslog is a logging package for Bun that uses slog.
This package enables SQL queries executed by Bun to be logged and displayed using slog.

## Installation

```bash
go get github.com/uptrace/bun/extra/bunslog
```

## Features

- Supports setting a `*slog.Logger` instance or uses the global logger if not set.
- Logs general SQL queries with configurable log levels.
- Logs slow SQL queries based on a configurable duration threshold.
- Logs SQL queries that result in errors, for easier debugging.
- Allows for custom log formatting.

## Usage

First, import the bunslog package:
```go
import "github.com/uptrace/bun/extra/bunslog"
```

Then, create a new QueryHook and add the hook to `*bun.DB` instance:
```go
db := bun.NewDB(sqldb, dialect)

hook := bunslog.NewQueryHook(
bunslog.WithQueryLogLevel(slog.LevelDebug),
bunslog.WithSlowQueryLogLevel(slog.LevelWarn),
bunslog.WithErrorQueryLogLevel(slog.LevelError),
bunslog.WithSlowQueryThreshold(3 * time.Second),
)

db.AddQueryHook(hook)
```

## Setting a Custom `*slog.Logger` Instance

To set a `*slog.Logger` instance, you can use the WithLogger option:

```go
logger := slog.NewLogger()
hook := bunslog.NewQueryHook(
bunslog.WithLogger(logger),
// other options...
)
```

If a `*slog.Logger` instance is not set, the global logger will be used.

## Custom Log Formatting

To customize the log format, you can use the WithLogFormat option:

```go
customFormat := func(event *bun.QueryEvent) []slog.Attr {
// your custom formatting logic here
}

hook := bunslog.NewQueryHook(
bunslog.WithLogFormat(customFormat),
// other options...
)
```

## Options

- `WithLogger(logger *slog.Logger)`: Sets a `*slog.Logger` instance. If not set, the global logger will be used.
- `WithQueryLogLevel(level slog.Level)`: Sets the log level for general queries.
- `WithSlowQueryLogLevel(level slog.Level)`: Sets the log level for slow queries.
- `WithErrorQueryLogLevel(level slog.Level)`: Sets the log level for queries that result in errors.
- `WithSlowQueryThreshold(threshold time.Duration)`: Sets the duration threshold for identifying slow queries.
- `WithLogFormat(f logFormat)`: Sets the custom format for slog output.
134 changes: 134 additions & 0 deletions extra/bunslog/bunslog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package bunslog

// bunslog provides logging functionalities for Bun using slog.
// This package allows SQL queries issued by Bun to be displayed using slog.

import (
"context"
"database/sql"
"errors"
"log/slog"
"time"

"github.com/uptrace/bun"
)

// Option is a function that configures a QueryHook.
type Option func(*QueryHook)

// WithLogger sets the *slog.Logger instance.
func WithLogger(logger *slog.Logger) Option {
return func(h *QueryHook) {
h.logger = logger
}
}

// WithQueryLogLevel sets the log level for general queries.
func WithQueryLogLevel(level slog.Level) Option {
return func(h *QueryHook) {
h.queryLogLevel = level
}
}

// WithSlowQueryLogLevel sets the log level for slow queries.
func WithSlowQueryLogLevel(level slog.Level) Option {
return func(h *QueryHook) {
h.slowQueryLogLevel = level
}
}

// WithErrorQueryLogLevel sets the log level for queries that result in an error.
func WithErrorQueryLogLevel(level slog.Level) Option {
return func(h *QueryHook) {
h.errorLogLevel = level
}
}

// WithSlowQueryThreshold sets the duration threshold for identifying slow queries.
func WithSlowQueryThreshold(threshold time.Duration) Option {
return func(h *QueryHook) {
h.slowQueryThreshold = threshold
}
}

// WithLogFormat sets the custom format for slog output.
func WithLogFormat(f logFormat) Option {
return func(h *QueryHook) {
h.logFormat = f
}
}

type logFormat func(event *bun.QueryEvent) []slog.Attr

// QueryHook is a hook for Bun that enables logging with slog.
// It implements bun.QueryHook interface.
type QueryHook struct {
logger *slog.Logger
queryLogLevel slog.Level
slowQueryLogLevel slog.Level
errorLogLevel slog.Level
slowQueryThreshold time.Duration
logFormat func(event *bun.QueryEvent) []slog.Attr
now func() time.Time
}

// NewQueryHook initializes a new QueryHook with the given options.
func NewQueryHook(opts ...Option) *QueryHook {
h := &QueryHook{
queryLogLevel: slog.LevelDebug,
slowQueryLogLevel: slog.LevelWarn,
errorLogLevel: slog.LevelError,
now: time.Now,
}

for _, opt := range opts {
opt(h)
}

// use default format
if h.logFormat == nil {
h.logFormat = func(event *bun.QueryEvent) []slog.Attr {
duration := h.now().Sub(event.StartTime)

return []slog.Attr{
slog.Any("error", event.Err),
slog.String("operation", event.Operation()),
slog.String("query", event.Query),
slog.String("duration", duration.String()),
}
}
}

return h
}

// BeforeQuery is called before a query is executed.
func (h *QueryHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context {
return ctx
}

// AfterQuery is called after a query is executed.
// It logs the query based on its duration and whether it resulted in an error.
func (h *QueryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
level := h.queryLogLevel
duration := h.now().Sub(event.StartTime)
if h.slowQueryThreshold > 0 && h.slowQueryThreshold <= duration {
level = h.slowQueryLogLevel
}

if event.Err != nil && !errors.Is(event.Err, sql.ErrNoRows) {
level = h.errorLogLevel
}

attrs := h.logFormat(event)
if h.logger != nil {
h.logger.LogAttrs(ctx, level, "", attrs...)
return
}

slog.LogAttrs(ctx, level, "", attrs...)
}

var (
_ bun.QueryHook = (*QueryHook)(nil)
)
Loading

0 comments on commit 4953367

Please sign in to comment.