Skip to content

Commit

Permalink
call 911 (#912)
Browse files Browse the repository at this point in the history
chore: add bunzerolog options
  • Loading branch information
max107 authored Oct 1, 2023
1 parent 4953367 commit 8a43835
Show file tree
Hide file tree
Showing 5 changed files with 420 additions and 43 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Patterns for files created by this project.
# For other files, use global gitignore.
*.s3db
.idea
87 changes: 87 additions & 0 deletions extra/bunzerolog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# bunzerolog

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

## Installation

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

## Features

- Supports setting a `*zerolog.Logger` instance or uses the global logger if not set.
- Supports setting a `*zerolog.Logger` instance using the context.
- 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 bunzerolog package:
```go
import "github.com/uptrace/bun/extra/bunzerolog"
```

Then, create a new QueryHook and add the hook to `*bun.DB` instance:
```go
import "github.com/rs/zerolog"

db := bun.NewDB(sqldb, dialect)

hook := bunzerolog.NewQueryHook(
bunzerolog.WithQueryLogLevel(zerolog.DebugLevel),
bunzerolog.WithSlowQueryLogLevel(zerolog.WarnLevel),
bunzerolog.WithErrorQueryLogLevel(zerolog.ErrorLevel),
bunzerolog.WithSlowQueryThreshold(3 * time.Second),
)

db.AddQueryHook(hook)
```

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

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

```go
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
hook := bunzerolog.NewQueryHook(
bunzerolog.WithLogger(logger),
// other options...
)
```

If a `*zerolog.Logger` instance is not set, the logger from the context will be used.

## Custom Log Formatting

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

```go
customFormat := func(ctx context.Context, event *bun.QueryEvent, zerevent *zerolog.Event) *zerolog.Event {
duration := h.now().Sub(event.StartTime)

return zerevent.
Err(event.Err).
Str("request_id", requestid.FromContext(ctx)).
Str("query", event.Query).
Str("operation", event.Operation()).
Str("duration", duration.String())
}

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

## Options

- `WithLogger(logger *zerolog.Logger)`: Sets a `*zerolog.Logger` instance. If not set, the logger from context will be used.
- `WithQueryLogLevel(level zerolog.Level)`: Sets the log level for general queries.
- `WithSlowQueryLogLevel(level zerolog.Level)`: Sets the log level for slow queries.
- `WithErrorQueryLogLevel(level zerolog.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.
43 changes: 0 additions & 43 deletions extra/bunzerolog/hook.go

This file was deleted.

132 changes: 132 additions & 0 deletions extra/bunzerolog/zerohook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package bunzerolog

// 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"
"time"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"

"github.com/uptrace/bun"
)

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

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

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

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

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

// WithErrorQueryLogLevel sets the log level for queries that result in an error.
func WithErrorQueryLogLevel(level zerolog.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 LogFormatFn) Option {
return func(h *QueryHook) {
h.logFormat = f
}
}

type LogFormatFn func(ctx context.Context, event *bun.QueryEvent, zeroctx *zerolog.Event) *zerolog.Event

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

// NewQueryHook initializes a new QueryHook with the given options.
func NewQueryHook(opts ...Option) *QueryHook {
h := &QueryHook{
queryLogLevel: zerolog.DebugLevel,
slowQueryLogLevel: zerolog.WarnLevel,
errorLogLevel: zerolog.ErrorLevel,
now: time.Now,
}

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

// use default format
if h.logFormat == nil {
h.logFormat = func(ctx context.Context, event *bun.QueryEvent, zerevent *zerolog.Event) *zerolog.Event {
duration := h.now().Sub(event.StartTime)

return zerevent.
Err(event.Err).
Str("query", event.Query).
Str("operation", event.Operation()).
Str("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
}

l := h.logger
if l == nil {
l = log.Ctx(ctx)
}

h.logFormat(ctx, event, l.WithLevel(level)).Send()
}
Loading

0 comments on commit 8a43835

Please sign in to comment.