Skip to content

Commit

Permalink
Merge pull request #1 from ichizero/implement-errlog
Browse files Browse the repository at this point in the history
feat: Implement errlog
  • Loading branch information
ichizero authored Mar 2, 2024
2 parents 0b44e64 + d5aadf5 commit de06f66
Show file tree
Hide file tree
Showing 10 changed files with 510 additions and 17 deletions.
24 changes: 7 additions & 17 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ output:
linters: # https://golangci-lint.run/usage/linters/
enable-all: true
disable:
- depguard
- forbidigo
- exhaustruct
- nlreturn
- paralleltest
- testableexamples
- varnamelen
- wrapcheck
- goerr113 # Use errorlint instead.
- gofmt # Use gofumpt instead.
- testpackage
Expand All @@ -26,25 +33,8 @@ linters: # https://golangci-lint.run/usage/linters/
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
linters-settings:
varnamelen:
max-distance: 15
ignore-decls:
- E comparable
issues:
exclude-rules:
- path: _test\.go
linters:
- containedctx
- funlen
- varnamelen
- dupl
- gocognit
- cyclop
- maintidx
- lll
- goconst
- contextcheck
- forcetypeassert
- text: 'shadow: declaration of "(err|ctx)" shadows declaration at'
linters: [ govet ]
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
# errlog

[![Test](https://github.com/ichizero/errlog/actions/workflows/test.yml/badge.svg)](https://github.com/ichizero/errlog/actions/workflows/test.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/ichizero/errlog.svg)](https://pkg.go.dev/github.com/ichizero/errlog)
[![Codecov](https://codecov.io/gh/ichizero/errlog/branch/main/graph/badge.svg)](https://codecov.io/gh/ichizero/errlog)
[![Go Report Card](https://goreportcard.com/badge/github.com/ichizero/errlog)](https://goreportcard.com/report/github.com/ichizero/errlog)

`errlog` is a error logging package based on [log/slog](https://pkg.go.dev/log/slog) standard library.
It provides error logging with stack trace and source location.
It does not require any third-party package.

## 🚀 Installation

```bash
go get github.com/ichizero/errlog
```

## 🧐 Usage

### Initialize logger
`errlog.NewHandler` wraps `slog.Handler`, so you can provide `*slog.JSONHandler`, `*slog.TextHandler`,
or any other handler.

```go
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})
hErr := errlog.NewHandler(h, &errlog.HandlerOptions{OverrideSource: true, SuppressStackTrace: false})
slog.SetDefault(slog.New(hErr))
```

### Logging error with stack trace

#### With errlog.Err
`errlog.Err` wraps error with stack trace and returns `slog.Attr` with key `error`.

```go
err := errors.New("test error")
slog.ErrorContext(ctx, "test", errlog.Err(err))
```

#### With custom error

`errlog.NewHandler` outputs stack trace with the error that implements `errlog.StackTrace` interface,
so you can provide custom error with stack trace.

```go
type yourCustomError struct {
err error
stack []uintptr
}

func (e yourCustomError) Stack() []uintptr {
return e.stack
}
```

If so, you can log stack trace without using `errlog.Err`.

```go
err := newYourCustomError("error")
slog.ErrorContext(ctx, "test", slog.Any("error", err))
```

#### Example usage

```go
package main

import (
"context"
"errors"
"log/slog"
"os"

"github.com/ichizero/errlog"
)

func main() {
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})
hErr := errlog.NewHandler(h, &errlog.HandlerOptions{OverrideSource: true, SuppressStackTrace: false})
slog.SetDefault(slog.New(hErr))

ctx := context.Background()

err := errors.New("test error")
slog.ErrorContext(ctx, "test", errlog.Err(err))

err = errlog.WrapError(err)
slog.ErrorContext(ctx, "test", slog.Any("error", err))
}
```
24 changes: 24 additions & 0 deletions errlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package errlog

import (
"log/slog"
)

const (
ErrorKey = "error"
StackTraceKey = "stack_trace"
)

// Err returns an attribute that contains the given error.
// If the error does not implement the StackTracer interface, it will be wrapped with the stack trace.
func Err(err error) slog.Attr {
if _, ok := err.(StackTracer); !ok {
err = wrapError(err, 1)
}
return slog.Any(ErrorKey, err)
}

// WrapError wraps the given error with a stack trace.
func WrapError(err error) error {
return wrapError(err, 1)
}
36 changes: 36 additions & 0 deletions errlog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package errlog_test

import (
"bytes"
"encoding/json"
"log/slog"
"testing"
"testing/slogtest"

"github.com/ichizero/errlog"
)

func TestRun(t *testing.T) {
t.Parallel()

var buf bytes.Buffer

newHandler := func(*testing.T) slog.Handler {
buf.Reset()

h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{AddSource: true})
return errlog.NewHandler(h, &errlog.HandlerOptions{OverrideSource: true, SuppressStackTrace: false})
}

result := func(t *testing.T) map[string]any {
t.Helper()

m := map[string]any{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
t.Fatal(err)
}
return m
}

slogtest.Run(t, newHandler, result)
}
24 changes: 24 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package errlog_test

import (
"context"
"errors"
"log/slog"
"os"

"github.com/ichizero/errlog"
)

func Example() {
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})
hErr := errlog.NewHandler(h, &errlog.HandlerOptions{OverrideSource: true, SuppressStackTrace: false})
slog.SetDefault(slog.New(hErr))

ctx := context.Background()

err := errors.New("test error")
slog.ErrorContext(ctx, "test", errlog.Err(err))

err = errlog.WrapError(err)
slog.ErrorContext(ctx, "test", slog.Any("error", err))
}
Empty file added go.sum
Empty file.
92 changes: 92 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package errlog

import (
"context"
"log/slog"
)

// Handler is a slog.Handler that adds error and stack trace information to log records.
type Handler struct {
base slog.Handler
opts HandlerOptions
}

var _ slog.Handler = (*Handler)(nil)

// HandlerOptions contains options for the Handler.
type HandlerOptions struct {
// SuppressStackTrace suppresses the stack trace from being added to log records.
SuppressStackTrace bool
// OverrideSource overrides the source location of the log record with the source location of the error.
OverrideSource bool
// StackTraceFormatter is a function that formats the stack trace.
StackTraceFormatter func(stack []uintptr) string
}

// NewHandler returns a new Handler that wraps the given base slog.handler.
func NewHandler(base slog.Handler, opts *HandlerOptions) *Handler {
if opts == nil {
opts = &HandlerOptions{}
}

return &Handler{
base: base,
opts: *opts,
}
}

// Enabled is a thin wrapper around the base handler's Enabled method.
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
return h.base.Enabled(ctx, level)
}

// WithAttrs is a thin wrapper around the base handler's WithAttrs method.
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &Handler{base: h.base.WithAttrs(attrs)}
}

// WithGroup is a thin wrapper around the base handler's WithGroup method.
func (h *Handler) WithGroup(name string) slog.Handler {
return &Handler{base: h.base.WithGroup(name)}
}

// Handle adds error and stack trace information to the log record.
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
r.Attrs(func(a slog.Attr) bool {
const stopIter = false

if a.Key != ErrorKey {
return true
}

err, ok := a.Value.Any().(error)
if !ok {
return stopIter
}

a.Value = slog.StringValue(err.Error())

stack := make([]uintptr, 0)
if str, ok := err.(StackTracer); ok {
stack = str.Stack()
}

if len(stack) == 0 {
return stopIter
}

if h.opts.OverrideSource {
r.PC = stack[0]
}

if !h.opts.SuppressStackTrace {
if h.opts.StackTraceFormatter != nil {
r.AddAttrs(slog.String(StackTraceKey, h.opts.StackTraceFormatter(stack)))
return stopIter
}
r.AddAttrs(slog.String(StackTraceKey, formatStack(stack)))
}
return stopIter
})
return h.base.Handle(ctx, r)
}
Loading

0 comments on commit de06f66

Please sign in to comment.