Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ one of them does not pull anything else in.
| [deferred](./deferred) | Helpers for the `defer` calls. |
| [dispatcher](./dispatcher) | Routing of keyed events to handlers, with priorities, propagation control and wildcards. |
| [protomap](./protomap) | Deserialization of protobuf messages by their descriptor name. |
| [slogger](./slogger) | A `log/slog` logger with a configurable format, a hot-reloadable level and a writer. |
| [slogger](./slogger) | A `log/slog` logger with a configurable format, a hot-reloadable level and context helpers. |
| [strcase](./strcase) | Detection of the case of a string and conversion between the case styles. |
| [sync](./sync) | A `sync.Once` that keeps retrying until the guarded function succeeds. |
| [time](./time) | A duration type that is serialized as a human-readable string. |
Expand Down
92 changes: 92 additions & 0 deletions slogger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
A `log/slog` logger with a configurable format, a hot-reloadable log level and a
custom writer.

The module is split into two packages:

- [`github.com/nbgrp/pkg/slogger`](./slogger.go) — the logger constructor.
- [`github.com/nbgrp/pkg/slogger/ctxlog`](./ctxlog/ctxlog.go) — the helpers that
pass the logger and its attributes through the
[context](https://pkg.go.dev/context).

## Public API

The `slogger` package provides:

```go
type Options struct {
// Leveler returns the current log level.
Expand All @@ -20,6 +29,7 @@ type Options struct {
}

func New(ctx context.Context, opts Options) (*slog.Logger, error)
func NewToContext(ctx context.Context, opts Options) (context.Context, error)
```

- `Leveler` — the current log level: `"DEBUG"`, `"INFO"`, `"WARN"` or `"ERROR"`
Expand All @@ -29,6 +39,10 @@ func New(ctx context.Context, opts Options) (*slog.Logger, error)
change. Without it the level is set once, at startup.
- `Writer` — the destination of the records; `os.Stdout` by default.

`NewToContext` is the same as `New`, but it puts the logger into the returned
context, so it can be used with the [`ctxlog`](#context-logger) helpers. The
returned context is nil if the logger cannot be initialized.

## Usage

```go
Expand Down Expand Up @@ -135,3 +149,81 @@ time=... level=ERROR msg="Failed to parse log level while hot-reload update" err

The logger is safe for concurrent use, as any `slog.Logger` is. The `Leveler` is
called from the watcher goroutine, so it must be safe for concurrent use as well.

## Context logger

The `ctxlog` package passes the logger through the
[context](https://pkg.go.dev/context): the logger is put into the context once,
and the package-level functions take it from there. The code that writes the log
stays independent of the way the logger is built and of the attributes of the
current request.

### Public API

```go
func ToContext(ctx context.Context, logger *slog.Logger) context.Context
func FromContext(ctx context.Context) *slog.Logger
func Debug(ctx context.Context, msg string, args ...any)
func Info(ctx context.Context, msg string, args ...any)
func Warn(ctx context.Context, msg string, args ...any)
func Error(ctx context.Context, msg string, args ...any)
func With(ctx context.Context, args ...any) context.Context
func Log(ctx context.Context, level slog.Level, msg string, args ...any)
func LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr)
```

- `ToContext` puts the logger into the context, `FromContext` takes it from
there. If the context has no logger, `slog.Default()` is used.
- `With` returns a derived context with the given attributes bound to it: they
are added to every record written with that context. Without the arguments it
returns the context unchanged.
- `Log` and `LogAttrs` write a record with an arbitrary level; `Debug`, `Info`,
`Warn` and `Error` are the shortcuts for `slog.LevelDebug`, `slog.LevelInfo`,
`slog.LevelWarn` and `slog.LevelError`.

### Usage

```go
ctx, err := slogger.NewToContext(ctx, slogger.Options{
Leveler: func() string { return "INFO" },
Format: "json",
})
if err != nil {
return err
}

ctxlog.Info(ctx, "Server started", slog.String("addr", ":8080"))
```

The logger can also be put into the context explicitly:

```go
logger, err := slogger.New(ctx, slogger.Options{
Leveler: func() string { return "INFO" },
Format: "json",
})
if err != nil {
return err
}

ctx = ctxlog.ToContext(ctx, logger)
```

The arguments are passed to `slog` as is, so all the forms the standard library
understands are supported:

```go
ctxlog.Info(ctx, "Server started", "addr", ":8080") // a key-value pair
ctxlog.Info(ctx, "Server started", slog.String("addr", ":8080")) // a slog.Attr
```

The attributes common to all the records of the scope are bound to the context:

```go
ctx = ctxlog.With(ctx, "request_id", requestID)

ctxlog.Info(ctx, "Started")
```

The context passed to the logging call is passed to the handler as well, so a
custom handler can take the values from it.
53 changes: 53 additions & 0 deletions slogger/ctxlog/ctxlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: BSD-3-Clause

package ctxlog

import (
"context"
"log/slog"
)

type loggerKey struct{}

func ToContext(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerKey{}, logger)
}

func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
return l
}
return slog.Default()
}

func Debug(ctx context.Context, msg string, args ...any) {
FromContext(ctx).DebugContext(ctx, msg, args...)
}

func Info(ctx context.Context, msg string, args ...any) {
FromContext(ctx).InfoContext(ctx, msg, args...)
}

func Warn(ctx context.Context, msg string, args ...any) {
FromContext(ctx).WarnContext(ctx, msg, args...)
}

func Error(ctx context.Context, msg string, args ...any) {
FromContext(ctx).ErrorContext(ctx, msg, args...)
}

func With(ctx context.Context, args ...any) context.Context {
if len(args) == 0 {
return ctx
}

return ToContext(ctx, FromContext(ctx).With(args...))
}

func Log(ctx context.Context, level slog.Level, msg string, args ...any) {
FromContext(ctx).Log(ctx, level, msg, args...)
}

func LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
FromContext(ctx).LogAttrs(ctx, level, msg, attrs...)
}
Loading
Loading