diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 17c6259..342a646 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -8,7 +8,7 @@ jobs:
test:
strategy:
matrix:
- go-version: [1.18.x]
+ go-version: [1.18.x, 1.21.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -32,7 +32,7 @@ jobs:
run: go test -race -covermode=atomic -coverprofile="profile.cov" ./...
- name: Send Coverage
- if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.18.x'
+ if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.21.x'
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
@@ -43,9 +43,9 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
- go-version: 1.18.x
+ go-version: 1.21.x
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
- version: v1.46.2
+ version: latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4db9243..b0a80b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [8.1.0] - 2023-08-16
+### Added
+- log.G as shorthand for adding a set of Grouped fields. This ability has always been present but is now fully supported in the default logger and with helper function for ease of use.
+- slog support added in Go 1.21+ both to use as an slog.Handler or redirect.
+
+### Fixed
+- errors.Chain handling from default withErrorFn handler after dep upgrade.
+
+## [8.0.2] - 2023-06-22
+### Fixed
+- Corrected removal of default logger upon registering a custom one.
+
## [8.0.1] - 2022-06-23
### Fixed
- Handling un-hashable tag values during dedupe process.
@@ -30,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed ability to remove individual log levels externally; RemoveHandler+AddHandler can do the same.
-[Unreleased]: https://github.com/go-playground/log/compare/v8.0.1...HEAD
+[Unreleased]: https://github.com/go-playground/log/compare/v8.1.0...HEAD
+[8.1.0]: https://github.com/go-playground/log/compare/v8.0.2...v8.1.0
+[8.0.2]: https://github.com/go-playground/log/compare/v8.0.1...v8.0.2
[8.0.1]: https://github.com/go-playground/log/compare/v8.0.0...v8.0.1
[8.0.0]: https://github.com/go-playground/log/compare/v7.0.2...v8.0.0
\ No newline at end of file
diff --git a/README.md b/README.md
index 9fde236..4c8fac3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
## log
-![Project status](https://img.shields.io/badge/version-8.0.1-green.svg)
+![Project status](https://img.shields.io/badge/version-8.1.0-green.svg)
[![Test](https://github.com/go-playground/log/actions/workflows/go.yml/badge.svg)](https://github.com/go-playground/log/actions/workflows/go.yml)
[![Coverage Status](https://coveralls.io/repos/github/go-playground/log/badge.svg?branch=master)](https://coveralls.io/github/go-playground/log?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/log)](https://goreportcard.com/report/github.com/go-playground/log)
@@ -152,9 +152,19 @@ func main() {
// logging with fields can be used with any of the above
log.WithField("key", "value").Info("test info")
}
-
```
+#### Go 1.21+ slog compatibility
+
+There is a compatibility layer for slog, which allows redirecting slog to this logger and ability to output to an slog.Handler+.
+
+| type | Definition |
+|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Handler](_examples/slog/hanlder/main.go) | This example demonstrates how to redirect the std log and slog to this logger by using it as an slog.Handler. |
+| [Redirect](_examples/slog/redirect/main.go) | This example demonstrates how to redirect the std log and slog to this logger and output back out to any slog.Handler, as well as any other handler(s) registered with this logger. |
+
+```go
+
Log Level Definitions
---------------------
diff --git a/_examples/slog/handler/main.go b/_examples/slog/handler/main.go
new file mode 100644
index 0000000..aaf8662
--- /dev/null
+++ b/_examples/slog/handler/main.go
@@ -0,0 +1,20 @@
+//go:build go1.21
+// +build go1.21
+
+package main
+
+import (
+ "github.com/go-playground/log/v8"
+ stdlog "log"
+ "log/slog"
+)
+
+func main() {
+
+ // This example demonstrates how to redirect the std log and slog to this logger by using it as
+ // an slog.Handler.
+ log.RedirectGoStdLog(true)
+ log.WithFields(log.G("grouped", log.F("key", "value"))).Debug("test")
+ stdlog.Println("test stdlog")
+ slog.Info("test slog", slog.Group("group", "key", "value"))
+}
diff --git a/_examples/slog/redirect/main.go b/_examples/slog/redirect/main.go
new file mode 100644
index 0000000..92eba05
--- /dev/null
+++ b/_examples/slog/redirect/main.go
@@ -0,0 +1,24 @@
+//go:build go1.21
+// +build go1.21
+
+package main
+
+import (
+ "github.com/go-playground/log/v8"
+ slogredirect "github.com/go-playground/log/v8/handlers/slog"
+ stdlog "log"
+ "log/slog"
+ "os"
+)
+
+func main() {
+
+ // This example demonstrates how to redirect the std log and slog to this logger and output back out to any
+ // slog.Handler, as well as any other handler(s) registered with this logger.
+ log.AddHandler(slogredirect.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+ ReplaceAttr: slogredirect.ReplaceAttrFn, // for custom log level output
+ })), log.AllLevels...)
+ log.WithFields(log.G("grouped", log.F("key", "value"))).Debug("test")
+ stdlog.Println("test stdlog")
+ slog.Info("test slog", slog.Group("group", "key", "value"))
+}
diff --git a/benchmarks/benchmark_test.go b/benchmarks/benchmark_test.go
index 330549e..0f8df5b 100644
--- a/benchmarks/benchmark_test.go
+++ b/benchmarks/benchmark_test.go
@@ -7,7 +7,7 @@ import (
"testing"
"time"
- "github.com/go-playground/log/v8"
+ log "github.com/go-playground/log/v8"
"github.com/go-playground/log/v8/handlers/json"
)
diff --git a/default_logger.go b/default_logger.go
index a873e72..d08ce67 100644
--- a/default_logger.go
+++ b/default_logger.go
@@ -72,49 +72,73 @@ func (c *Logger) Log(e Entry) {
buff.B = append(buff.B, space)
buff.B = append(buff.B, e.Message...)
- for _, f := range e.Fields {
- buff.B = append(buff.B, space)
- buff.B = append(buff.B, f.Key...)
- buff.B = append(buff.B, equals)
+ c.addFields("", buff, e.Fields)
+ buff.B = append(buff.B, newLine)
+
+ c.m.Lock()
+ _, _ = c.writer.Write(buff.B)
+ c.m.Unlock()
+
+ BytePool().Put(buff)
+}
+
+func (c *Logger) addFields(prefix string, buff *Buffer, fields []Field) {
+ for _, f := range fields {
switch t := f.Value.(type) {
case string:
+ printKey(buff, prefix+f.Key)
buff.B = append(buff.B, t...)
case int:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
case int8:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
case int16:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
case int32:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
case int64:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendInt(buff.B, t, base10)
case uint:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
case uint8:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
case uint16:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
case uint32:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
case uint64:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendUint(buff.B, t, base10)
case float32:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendFloat(buff.B, float64(t), 'f', -1, 32)
case float64:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendFloat(buff.B, t, 'f', -1, 64)
case bool:
+ printKey(buff, prefix+f.Key)
buff.B = strconv.AppendBool(buff.B, t)
+ case []Field:
+ c.addFields(prefix+f.Key+".", buff, t)
default:
+ printKey(buff, prefix+f.Key)
buff.B = append(buff.B, fmt.Sprintf(v, f.Value)...)
}
}
- buff.B = append(buff.B, newLine)
-
- c.m.Lock()
- _, _ = c.writer.Write(buff.B)
- c.m.Unlock()
+}
- BytePool().Put(buff)
+func printKey(buff *Buffer, key string) {
+ buff.B = append(buff.B, space)
+ buff.B = append(buff.B, key...)
+ buff.B = append(buff.B, equals)
}
diff --git a/errors.go b/errors.go
index cf62286..7855d40 100644
--- a/errors.go
+++ b/errors.go
@@ -102,13 +102,11 @@ func formatLink(l *errors.Link, b []byte) []byte {
b = extractSource(b, l.Source)
if l.Prefix != "" {
b = append(b, l.Prefix...)
- }
- if _, ok := l.Err.(errors.Chain); !ok {
- if l.Prefix != "" {
+ if l.Err != nil {
b = append(b, ": "...)
+ b = append(b, l.Err.Error()...)
}
- b = append(b, l.Err.Error()...)
}
return b
}
diff --git a/go.mod b/go.mod
index 50d963e..541866c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,9 @@ module github.com/go-playground/log/v8
go 1.18
require (
- github.com/go-playground/errors/v5 v5.2.3
- github.com/go-playground/pkg/v5 v5.6.0
- golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
+ github.com/go-playground/errors/v5 v5.3.1
+ github.com/go-playground/pkg/v5 v5.21.2
+ golang.org/x/term v0.11.0
)
-require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
+require golang.org/x/sys v0.11.0 // indirect
diff --git a/go.sum b/go.sum
index fe88bf8..199d2da 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
-github.com/go-playground/errors/v5 v5.2.3 h1:RPxaFHgJZjgk/OFkUcfytJgRQKRINLtueVxgOdnfPpg=
-github.com/go-playground/errors/v5 v5.2.3/go.mod h1:DincxRGwraWmq39TZDqtnOtHGOJ+AbNbO0OmBzX6MLw=
-github.com/go-playground/pkg/v5 v5.6.0 h1:97YpRFzIcS5NFP2Uzxj8cPYz4zTuwweyXADYcA1KfUQ=
-github.com/go-playground/pkg/v5 v5.6.0/go.mod h1:TvZ2nNtNh6VfoNteY9ApA2BXt1ZwJliFZ4hzPAwLS9Y=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
-golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+github.com/go-playground/errors/v5 v5.3.1 h1:J2qU+9Whg863g3SATXKSJyFA91Zz85pYD3+obr5Oodk=
+github.com/go-playground/errors/v5 v5.3.1/go.mod h1:LcLhmzQ/RuEntAs9r38NSV+xtbHffhMx/1yuuEroc7M=
+github.com/go-playground/pkg/v5 v5.21.2 h1:DgVr88oMI3pfMFkEN9E6hp9YGG8NHc+019LRJfnUOfU=
+github.com/go-playground/pkg/v5 v5.21.2/go.mod h1:UgHNntEQnMJSygw2O2RQ3LAB0tprx81K90c/pOKh7cU=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
diff --git a/handlers/json/json.go b/handlers/json/json.go
index 4322b7e..025564f 100644
--- a/handlers/json/json.go
+++ b/handlers/json/json.go
@@ -6,7 +6,7 @@ import (
"io"
"sync"
- "github.com/go-playground/log/v8"
+ log "github.com/go-playground/log/v8"
)
// Handler implementation.
diff --git a/handlers/json/json_test.go b/handlers/json/json_test.go
index 0f82684..bbc57f5 100644
--- a/handlers/json/json_test.go
+++ b/handlers/json/json_test.go
@@ -5,7 +5,7 @@ import (
"strings"
"testing"
- "github.com/go-playground/log/v8"
+ log "github.com/go-playground/log/v8"
)
func TestJSONLogger(t *testing.T) {
diff --git a/handlers/slog/slog_redirect.go b/handlers/slog/slog_redirect.go
new file mode 100644
index 0000000..3bf5c4b
--- /dev/null
+++ b/handlers/slog/slog_redirect.go
@@ -0,0 +1,59 @@
+//go:build go1.21
+// +build go1.21
+
+package slog
+
+import (
+ "context"
+ log "github.com/go-playground/log/v8"
+ "log/slog"
+)
+
+// Handler implementation.
+type Handler struct {
+ handler slog.Handler
+}
+
+// New handler wraps an slog.Handler for log output.
+//
+// Calling this function automatically calls the slog.RedirectGoStdLog function in order to intercept and forward
+// the Go standard library log output to this handler.
+func New(handler slog.Handler) *Handler {
+ log.RedirectGoStdLog(true)
+ return &Handler{handler: handler}
+}
+
+// Log handles the log entry
+func (h *Handler) Log(e log.Entry) {
+ r := slog.NewRecord(e.Timestamp, slog.Level(e.Level), e.Message, 0)
+ r.AddAttrs(h.convertFields(e.Fields)...)
+ _ = h.handler.Handle(context.Background(), r)
+}
+
+func (h *Handler) convertFields(fields []log.Field) []slog.Attr {
+ attrs := make([]slog.Attr, 0, len(fields))
+ for _, f := range fields {
+ switch t := f.Value.(type) {
+ case []log.Field:
+ a := h.convertFields(t)
+ arr := make([]any, 0, len(a))
+ for _, v := range a {
+ arr = append(arr, v)
+ }
+ attrs = append(attrs, slog.Group(f.Key, arr...))
+ default:
+ attrs = append(attrs, slog.Any(f.Key, f.Value))
+ }
+ }
+ return attrs
+}
+
+// ReplaceAttrFn can be used with slog.HandlerOptions to replace attributes.
+// This function replaces the "level" attribute to get the custom log levels of this package.
+var ReplaceAttrFn = func(groups []string, a slog.Attr) slog.Attr {
+ if a.Key == slog.LevelKey {
+ level := log.Level(a.Value.Any().(slog.Level))
+ a.Value = slog.StringValue(level.String())
+ }
+ return a
+}
diff --git a/handlers/slog/slog_redirect_test.go b/handlers/slog/slog_redirect_test.go
new file mode 100644
index 0000000..c7123a2
--- /dev/null
+++ b/handlers/slog/slog_redirect_test.go
@@ -0,0 +1,46 @@
+//go:build go1.21
+// +build go1.21
+
+package slog
+
+import (
+ "bytes"
+ "encoding/json"
+ "github.com/go-playground/log/v8"
+ "log/slog"
+ "strings"
+ "testing"
+ "testing/slogtest"
+)
+
+func TestSlogRedirect(t *testing.T) {
+ var buff bytes.Buffer
+ log.AddHandler(New(slog.NewJSONHandler(&buff, &slog.HandlerOptions{
+ ReplaceAttr: ReplaceAttrFn, // for custom log level output
+ })), log.AllLevels...)
+ h := slog.Default().Handler()
+
+ results := func() []map[string]any {
+ var ms []map[string]any
+ for _, line := range bytes.Split(buff.Bytes(), []byte{'\n'}) {
+ if len(line) == 0 {
+ continue
+ }
+ var m map[string]any
+ if err := json.Unmarshal(line, &m); err != nil {
+ panic(err) // In a real test, use t.Fatal.
+ }
+ ms = append(ms, m)
+ }
+ return ms
+ }
+ err := slogtest.TestHandler(h, results)
+ if err != nil {
+ // if a single error and is time key errors, is ok this logger always sets that.
+ // sad this its the only way to hook into these errors because none of concrete and
+ // Joined errors has no way to reach into them when not.
+ if strings.Count(err.Error(), "\n") != 0 || !strings.Contains(err.Error(), "unexpected key \"time\": a Handler should ignore a zero Record.Time") {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/log.go b/log.go
index 53275df..6ed855f 100644
--- a/log.go
+++ b/log.go
@@ -1,12 +1,8 @@
package log
import (
- "bufio"
"context"
- "io"
- stdlog "log"
"os"
- "strings"
"sync"
"time"
@@ -48,9 +44,7 @@ var (
}{
name: "log",
}
- rw sync.RWMutex
- stdLogWriter *io.PipeWriter
- redirectComplete chan struct{}
+ rw = new(sync.RWMutex)
)
// Field is a single Field key and value
@@ -59,54 +53,6 @@ type Field struct {
Value interface{} `json:"value"`
}
-// RedirectGoStdLog is used to redirect Go's internal std log output to this logger.
-func RedirectGoStdLog(redirect bool) {
- if (redirect && stdLogWriter != nil) || (!redirect && stdLogWriter == nil) {
- // already redirected or already not redirected
- return
- }
- if !redirect {
- stdlog.SetOutput(os.Stderr)
- // will stop scanner reading PipeReader
- _ = stdLogWriter.Close()
- stdLogWriter = nil
- <-redirectComplete
- return
- }
-
- ready := make(chan struct{})
- redirectComplete = make(chan struct{})
-
- // last option is to redirect
- go func() {
- var r *io.PipeReader
- r, stdLogWriter = io.Pipe()
- defer func() {
- _ = r.Close()
- }()
-
- stdlog.SetOutput(stdLogWriter)
- defer func() {
- close(redirectComplete)
- redirectComplete = nil
- }()
-
- scanner := bufio.NewScanner(r)
- close(ready)
- for scanner.Scan() {
- txt := scanner.Text()
- if strings.Contains(txt, "error") {
- WithField("stdlog", true).Error(txt)
- } else if strings.Contains(txt, "warning") {
- WithField("stdlog", true).Warn(txt)
- } else {
- WithField("stdlog", true).Notice(txt)
- }
- }
- }()
- <-ready
-}
-
// SetExitFunc sets the provided function as the exit function used in Fatal(),
// Fatalf(), Panic() and Panicf(). This is primarily used when wrapping this library,
// you can set this to enable testing (with coverage) of your Fatal() and Fatalf()
@@ -148,7 +94,9 @@ func HandleEntry(e Entry) {
if !e.start.IsZero() {
e = e.WithField("duration", time.Since(e.start))
}
- e.Timestamp = time.Now()
+ if e.Timestamp.IsZero() {
+ e.Timestamp = time.Now()
+ }
rw.RLock()
for _, h := range logHandlers[e.Level] {
@@ -163,6 +111,11 @@ func F(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
+// G creates a new group of fields using the supplied key as the groups name.
+func G(key string, fields ...Field) Field {
+ return F(key, fields)
+}
+
// AddHandler adds a new log handlers and accepts which log levels that
// handlers will be triggered for
func AddHandler(h Handler, levels ...Level) {
diff --git a/log_pre_1.21.go b/log_pre_1.21.go
new file mode 100644
index 0000000..c781198
--- /dev/null
+++ b/log_pre_1.21.go
@@ -0,0 +1,66 @@
+//go:build !go1.21
+// +build !go1.21
+
+package log
+
+import (
+ "bufio"
+ "io"
+ stdlog "log"
+ "os"
+ "strings"
+)
+
+var (
+ stdLogWriter *io.PipeWriter
+ redirectComplete chan struct{}
+)
+
+// RedirectGoStdLog is used to redirect Go's internal std log output to this logger AND registers a handler for slog
+// that redirects slog output to this logger.
+func RedirectGoStdLog(redirect bool) {
+ if (redirect && stdLogWriter != nil) || (!redirect && stdLogWriter == nil) {
+ // already redirected or already not redirected
+ return
+ }
+ if !redirect {
+ stdlog.SetOutput(os.Stderr)
+ // will stop scanner reading PipeReader
+ _ = stdLogWriter.Close()
+ stdLogWriter = nil
+ <-redirectComplete
+ return
+ }
+
+ ready := make(chan struct{})
+ redirectComplete = make(chan struct{})
+
+ // last option is to redirect
+ go func() {
+ var r *io.PipeReader
+ r, stdLogWriter = io.Pipe()
+ defer func() {
+ _ = r.Close()
+ }()
+
+ stdlog.SetOutput(stdLogWriter)
+ defer func() {
+ close(redirectComplete)
+ redirectComplete = nil
+ }()
+
+ scanner := bufio.NewScanner(r)
+ close(ready)
+ for scanner.Scan() {
+ txt := scanner.Text()
+ if strings.Contains(txt, "error") {
+ WithField("stdlog", true).Error(txt)
+ } else if strings.Contains(txt, "warning") {
+ WithField("stdlog", true).Warn(txt)
+ } else {
+ WithField("stdlog", true).Notice(txt)
+ }
+ }
+ }()
+ <-ready
+}
diff --git a/log_test.go b/log_test.go
index 858e327..0bc8393 100644
--- a/log_test.go
+++ b/log_test.go
@@ -393,7 +393,7 @@ func TestWithTrace(t *testing.T) {
}
}
-func TestDefaultFields(t *testing.T) {
+func TestDefaultsAndGroupFields(t *testing.T) {
logHandlers = map[Level][]Handler{}
buff := new(bytes.Buffer)
th := &testHandler{
@@ -401,9 +401,9 @@ func TestDefaultFields(t *testing.T) {
}
AddHandler(th, AllLevels...)
WithDefaultFields(F("key", "value"))
- Info("info")
- if buff.String() != "INFO info key=value\n" {
- t.Errorf("Expected '%s' Got '%s'", "INFO info key=value\n", buff.String())
+ WithFields(G("group", F("gkey", "gvalue"))).Info("info")
+ if buff.String() != "INFO info key=value group=[{gkey gvalue}]\n" {
+ t.Errorf("Expected '%s' Got '%s'", "INFO info key=value group=[{gkey gvalue}]\n", buff.String())
}
}
diff --git a/slog.go b/slog.go
new file mode 100644
index 0000000..b47363f
--- /dev/null
+++ b/slog.go
@@ -0,0 +1,205 @@
+//go:build go1.21
+// +build go1.21
+
+package log
+
+import (
+ "context"
+ runtimeext "github.com/go-playground/pkg/v5/runtime"
+ "log/slog"
+ "runtime"
+)
+
+var _ slog.Handler = (*slogHandler)(nil)
+
+type slogHandler struct {
+ // List of Groups, each subsequent group belongs to the previous group, except the first
+ // which are the top level fields fields before any grouping.
+ groups []Field
+}
+
+// Enabled returns if the current logging level is enabled. In the case of this log package in this Level has a
+// handler registered.
+func (s *slogHandler) Enabled(_ context.Context, level slog.Level) bool {
+ rw.RLock()
+ _, enabled := logHandlers[convertSlogLevel(level)]
+ rw.RUnlock()
+ return enabled
+}
+
+func (s *slogHandler) Handle(ctx context.Context, record slog.Record) error {
+
+ var current Field
+ if len(s.groups) == 0 {
+ current = G("")
+ } else {
+ group := s.groups[len(s.groups)-1]
+ last := group.Value.([]Field)
+ fields := make([]Field, len(last), len(last)+record.NumAttrs()+1)
+ copy(fields, last)
+
+ current = F(group.Key, fields)
+ }
+
+ if record.NumAttrs() > 0 {
+ record.Attrs(func(attr slog.Attr) bool {
+ current.Value = s.convertAttrToField(current.Value.([]Field), attr)
+ return true
+ })
+ }
+ if record.Level >= slog.LevelError && record.PC != 0 {
+ fs := runtime.CallersFrames([]uintptr{record.PC})
+ f, _ := fs.Next()
+ sourceBuff := BytePool().Get()
+ sourceBuff.B = extractSource(sourceBuff.B, runtimeext.Frame{Frame: f})
+ current.Value = append(current.Value.([]Field), F(slog.SourceKey, string(sourceBuff.B[:len(sourceBuff.B)-1])))
+ BytePool().Put(sourceBuff)
+ }
+
+ for i := len(s.groups) - 2; i >= 0; i-- {
+ group := s.groups[i]
+ gf := group.Value.([]Field)
+ copied := make([]Field, len(gf), len(gf)+1)
+ copy(copied, gf)
+ current = G(group.Key, append(copied, current)...)
+ }
+
+ var e Entry
+ if current.Key == "" {
+ e = Entry{Fields: current.Value.([]Field)}
+ } else {
+ e = Entry{Fields: []Field{current}}
+ }
+ e.Message = record.Message
+ e.Level = convertSlogLevel(record.Level)
+ e.Timestamp = record.Time
+
+ HandleEntry(e)
+ return nil
+}
+
+func (s *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+ var groups []Field
+ if len(s.groups) == 0 {
+ groups = []Field{G("", s.convertAttrsToFields(nil, attrs)...)}
+ } else {
+ groups = make([]Field, len(s.groups))
+ copy(groups, s.groups)
+
+ l := len(groups) - 1
+ current := groups[l]
+ currentFields := current.Value.([]Field)
+ copiedFields := make([]Field, len(currentFields), len(currentFields)+len(attrs))
+ copy(copiedFields, currentFields)
+ groups[l].Value = s.convertAttrsToFields(copiedFields, attrs)
+ }
+
+ return &slogHandler{
+ groups: groups,
+ }
+}
+
+func (s *slogHandler) convertAttrsToFields(fields []Field, attrs []slog.Attr) []Field {
+ for _, attr := range attrs {
+ if attr.Key == "" {
+ continue
+ }
+ if attr.Key == slog.TimeKey && attr.Value.Time().IsZero() {
+ continue
+ }
+ fields = s.convertAttrToField(fields, attr)
+ }
+ return fields
+}
+
+func (s *slogHandler) convertAttrToField(fields []Field, attr slog.Attr) []Field {
+ var value any
+
+ switch attr.Value.Kind() {
+ case slog.KindLogValuer:
+ return s.convertAttrToField(fields, slog.Attr{Key: attr.Key, Value: attr.Value.LogValuer().LogValue()})
+
+ case slog.KindGroup:
+ attrs := attr.Value.Group()
+ groupedFields := make([]Field, 0, len(attrs))
+ value = s.convertAttrsToFields(groupedFields, attrs)
+
+ default:
+ value = attr.Value.Any()
+ }
+ return append(fields, F(attr.Key, value))
+}
+
+func (s *slogHandler) WithGroup(name string) slog.Handler {
+ groups := make([]Field, len(s.groups), len(s.groups)+1)
+ copy(groups, s.groups)
+
+ return &slogHandler{
+ groups: append(groups, G(name)),
+ }
+}
+
+func convertSlogLevel(level slog.Level) Level {
+ switch level {
+ case slog.LevelDebug:
+ return DebugLevel
+ case slog.LevelInfo:
+ return InfoLevel
+ case SlogNoticeLevel:
+ return NoticeLevel
+ case slog.LevelWarn:
+ return WarnLevel
+ case slog.LevelError:
+ return ErrorLevel
+ case SlogPanicLevel:
+ return PanicLevel
+ case SlogAlertLevel:
+ return AlertLevel
+ case SlogFatalLevel:
+ return FatalLevel
+ default:
+ switch {
+ case level > slog.LevelInfo && level < slog.LevelWarn:
+ return NoticeLevel
+ case level > slog.LevelError && level <= SlogPanicLevel:
+ return PanicLevel
+ case level > SlogPanicLevel && level <= SlogAlertLevel:
+ return AlertLevel
+ case level > SlogAlertLevel && level <= SlogFatalLevel:
+ return FatalLevel
+ }
+ return InfoLevel
+ }
+}
+
+var (
+ prevSlogLogger *slog.Logger
+)
+
+// RedirectGoStdLog is used to redirect Go's internal std log output to this logger AND registers a handler for slog
+// that redirects slog output to this logger.
+//
+// If you intend to use this log interface with another slog handler then you should not use this function and instead
+// register a handler with slog directly and register the slog redirect, found under the handlers package or other
+// custom redirect handler with this logger.
+func RedirectGoStdLog(redirect bool) {
+ if redirect {
+ prevSlogLogger = slog.Default()
+ slog.SetDefault(slog.New(&slogHandler{}))
+ } else if prevSlogLogger != nil {
+ slog.SetDefault(prevSlogLogger)
+ prevSlogLogger = nil
+ }
+}
+
+// slog log levels.
+const (
+ SlogDebugLevel slog.Level = slog.LevelDebug
+ SlogInfoLevel slog.Level = slog.LevelInfo
+ SlogWarnLevel slog.Level = slog.LevelWarn
+ SlogErrorLevel slog.Level = slog.LevelError
+ SlogNoticeLevel slog.Level = slog.LevelInfo + 2
+ SlogPanicLevel slog.Level = slog.LevelError + 4
+ SlogAlertLevel slog.Level = SlogPanicLevel + 4
+ SlogFatalLevel slog.Level = SlogAlertLevel + 4 // same as syslog CRITICAL
+)