Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39d531c501 |
@@ -128,9 +128,8 @@ error away. That is how `log/slog` works, and simplelog cannot change it.
|
|||||||
To find out whether a record was delivered, build a `slog.Record` and pass
|
To find out whether a record was delivered, build a `slog.Record` and pass
|
||||||
it to the handler yourself, for example
|
it to the handler yourself, for example
|
||||||
`slog.Default().Handler().Handle(ctx, record)`, then check the error it
|
`slog.Default().Handler().Handle(ctx, record)`, then check the error it
|
||||||
returns. Console output takes the file and line from the record's `PC`,
|
returns. A record passed to `Handle` directly gets the wrong file and
|
||||||
which you set with `runtime.Callers` as the `log/slog` documentation
|
line in console output.
|
||||||
shows. A record whose `PC` is zero prints `???:0` instead.
|
|
||||||
|
|
||||||
## Entrypoints
|
## Entrypoints
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ files it depends on: .golangci.yml, REPO_POLICIES.md, .editorconfig,
|
|||||||
* 2026-10-06: a webhook request now times out after 5 seconds, so a
|
* 2026-10-06: a webhook request now times out after 5 seconds, so a
|
||||||
server that never answers no longer stops every log call; the webhook
|
server that never answers no longer stops every log call; the webhook
|
||||||
handler also reads each answer to the end so its connection is reused
|
handler also reads each answer to the end so its connection is reused
|
||||||
* 2026-10-06: the console handler takes the file and line it prints from
|
|
||||||
the record's `PC` instead of counting stack frames, so they name the
|
|
||||||
call site whether the record came through a `slog.Logger` or straight
|
|
||||||
to `Handle`
|
|
||||||
* 2026-10-06: the tests run under the race detector, in Docker:
|
* 2026-10-06: the tests run under the race detector, in Docker:
|
||||||
`script/test` builds the `test` stage of the `Dockerfile`, which runs
|
`script/test` builds the `test` stage of the `Dockerfile`, which runs
|
||||||
`go test -race`, and a new final stage makes a plain `docker build`
|
`go test -race`, and a new final stage makes a plain `docker build`
|
||||||
|
|||||||
+9
-7
@@ -12,6 +12,10 @@ import (
|
|||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// callerSkipFrames is the number of stack frames between runtime.Caller
|
||||||
|
// and the slog call site that produced the record.
|
||||||
|
const callerSkipFrames = 4
|
||||||
|
|
||||||
// ConsoleHandler writes human-readable, colored log lines to stdout.
|
// ConsoleHandler writes human-readable, colored log lines to stdout.
|
||||||
type ConsoleHandler struct {
|
type ConsoleHandler struct {
|
||||||
// out is where records are written. Nil means os.Stdout, looked up on
|
// out is where records are written. Nil means os.Stdout, looked up on
|
||||||
@@ -49,13 +53,11 @@ func (c *ConsoleHandler) Handle(
|
|||||||
colorFunc = color.New(color.FgWhite).SprintfFunc()
|
colorFunc = color.New(color.FgWhite).SprintfFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The file and line come from the record's PC, the call site; a record
|
// Get the caller information
|
||||||
// without one prints a placeholder.
|
_, file, line, ok := runtime.Caller(callerSkipFrames)
|
||||||
file, line := "???", 0
|
if !ok {
|
||||||
|
file = "???"
|
||||||
if record.PC != 0 {
|
line = 0
|
||||||
frame, _ := runtime.CallersFrames([]uintptr{record.PC}).Next()
|
|
||||||
file, line = frame.File, frame.Line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out := c.out
|
out := c.out
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
package simplelog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// These tests sit inside the package so they can read the console line
|
|
||||||
// from a buffer instead of from stdout.
|
|
||||||
|
|
||||||
// lineOf calls logCall and returns "file:line" for the line lineOf was
|
|
||||||
// called from. Each test writes its log call inside logCall on that same
|
|
||||||
// line, so the result is the location the console line must name.
|
|
||||||
func lineOf(logCall func()) string {
|
|
||||||
_, file, line, _ := runtime.Caller(1)
|
|
||||||
|
|
||||||
logCall()
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s:%d", file, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wantLocation fails the test unless the console line names location as
|
|
||||||
// where the record "casting" was logged.
|
|
||||||
func wantLocation(t *testing.T, output, location string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if !strings.Contains(output, location+": casting") {
|
|
||||||
t.Fatalf("console line %q does not name %s", output, location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The default handler as simplelog installs it when stdout is a terminal:
|
|
||||||
// a MultiplexHandler holding a ConsoleHandler.
|
|
||||||
func TestConsoleHandlerNamesCallSiteThroughDefaultHandler(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var output bytes.Buffer
|
|
||||||
|
|
||||||
logger := slog.New(&MultiplexHandler{handlers: []ExtendedHandler{
|
|
||||||
&ConsoleHandler{out: &output},
|
|
||||||
}})
|
|
||||||
|
|
||||||
want := lineOf(func() { logger.Info("casting") })
|
|
||||||
|
|
||||||
wantLocation(t, output.String(), want)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConsoleHandlerNamesCallSiteUsedWithSlogNew(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var output bytes.Buffer
|
|
||||||
|
|
||||||
logger := slog.New(&ConsoleHandler{out: &output})
|
|
||||||
|
|
||||||
want := lineOf(func() { logger.Info("casting") })
|
|
||||||
|
|
||||||
wantLocation(t, output.String(), want)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The record is built as the log/slog package documentation shows for a
|
|
||||||
// function that logs on its caller's behalf: runtime.Callers supplies the
|
|
||||||
// PC.
|
|
||||||
func TestConsoleHandlerNamesCallSiteOfRecordPassedToHandle(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
output bytes.Buffer
|
|
||||||
pcs [1]uintptr
|
|
||||||
)
|
|
||||||
|
|
||||||
want := lineOf(func() { runtime.Callers(1, pcs[:]) })
|
|
||||||
|
|
||||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "casting", pcs[0])
|
|
||||||
|
|
||||||
err := (&ConsoleHandler{out: &output}).Handle(context.Background(), record)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Handle: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantLocation(t, output.String(), want)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConsoleHandlerPrintsPlaceholderWithoutPC(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var output bytes.Buffer
|
|
||||||
|
|
||||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "casting", 0)
|
|
||||||
|
|
||||||
err := (&ConsoleHandler{out: &output}).Handle(context.Background(), record)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Handle: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantLocation(t, output.String(), "???:0")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user