~bigbes/sr-ht-dolt

b4fd2233c89ca5c415284b61b83d41eb3d4982c1 — Eugene Blikh 9 days ago 4d17862
log: take the logrus bridge from auxilia

The bridge was written here because remotesrv.ServerArgs.Logger demands a
*logrus.Entry and nothing else, and letting it log around our handler meant
its records skipped the masks. None of that is specific to dolt or to
SourceHut, so it now lives in auxilia beside scribe, where the next library
that demands a logrus entry can reach it.
5 files changed, 7 insertions(+), 480 deletions(-)

M cmd/doltsrht/main.go
M go.mod
M go.sum
D internal/logrusbridge/logrusbridge.go
D internal/logrusbridge/logrusbridge_test.go
M cmd/doltsrht/main.go => cmd/doltsrht/main.go +1 -1
@@ 31,6 31,7 @@ import (
	"github.com/vaughan0/go-ini"

	"go.bigb.es/auxilia/culpa"
	"go.bigb.es/auxilia/logrusbridge"
	"go.bigb.es/auxilia/scribe"

	"sourcecraft.dev/bigbes/sr-ht-core/config"


@@ 38,7 39,6 @@ import (
	"sourcecraft.dev/bigbes/sr-ht-core/server"

	"sourcecraft.dev/bigbes/sr-ht-dolt/authn"
	"sourcecraft.dev/bigbes/sr-ht-dolt/internal/logrusbridge"
	"sourcecraft.dev/bigbes/sr-ht-dolt/remoteapi"
	"sourcecraft.dev/bigbes/sr-ht-dolt/storage"
	"sourcecraft.dev/bigbes/sr-ht-dolt/web"

M go.mod => go.mod +2 -2
@@ 9,10 9,10 @@ require (
	github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee
	github.com/go-chi/chi/v5 v5.3.1
	github.com/lib/pq v1.10.9
	github.com/sirupsen/logrus v1.9.3
	github.com/sirupsen/logrus v1.9.4
	github.com/stretchr/testify v1.11.1
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
	go.bigb.es/auxilia v0.5.0
	go.bigb.es/auxilia v0.6.2-0.20260808203729-6d4c5ff0b513
	google.golang.org/grpc v1.79.3
	gopkg.in/go-jose/go-jose.v2 v2.6.3
	sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152

M go.sum => go.sum +4 -5
@@ 416,8 416,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=


@@ 469,8 469,8 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.bigb.es/auxilia v0.5.0 h1:S5+btW6++4CQDOfAEZe1UxrXRl6nxtmu0rI3uIXwCaQ=
go.bigb.es/auxilia v0.5.0/go.mod h1:hBkJvydQRfmgSTR2U4PvYgJcZnh7Bj/otukrk14iJU4=
go.bigb.es/auxilia v0.6.2-0.20260808203729-6d4c5ff0b513 h1:XbfQqibynw3gIIrBCPJ19pNz3vdi+b8+mMRfKvpzr0E=
go.bigb.es/auxilia v0.6.2-0.20260808203729-6d4c5ff0b513/go.mod h1:MexCwc01Pv015P4H4U0MOr+wwCqWqsApmYGO4PrELzs=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=


@@ 562,7 562,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

D internal/logrusbridge/logrusbridge.go => internal/logrusbridge/logrusbridge.go +0 -252
@@ 1,252 0,0 @@
// Package logrusbridge routes a logrus logger's records into log/slog.
//
// It exists for a shape of dependency that keeps recurring and has no good
// answer at the call site: a third-party library logs, and the only way it will
// accept a logger is as a *logrus.Entry. dolt's remotesrv is this service's
// example — its ServerArgs.Logger field is typed *logrus.Entry and nothing
// else — but the shape is not dolt's and not SourceHut's, which is why this is
// a general adapter rather than a line in the daemon's wiring.
//
// The reason to bridge rather than to leave the library logging on its own is
// not tidiness of format. A process that has configured one handler — a level,
// a destination, and above all a set of masks over the fields that may carry a
// credential — has configured it for the records it emits itself. Every record
// the third-party library writes goes around all of that: its own format, its
// own stream, its own idea of what is worth printing, and no mask between a
// field named "token" and the operator's journal. In a library that serves
// remote requests, the paths most likely to put a credential in a log line are
// exactly the ones this process did not write. Routing them through the same
// handler is what makes a masking rule a property of the process instead of a
// property of the code that remembered to use it.
//
// The bridge is a logrus.Hook, which is the seam logrus ships for precisely
// this, plus two settings that make the hook the *only* exit: the output goes
// to io.Discard and the formatter produces nothing, so no record is formatted
// on its way to being thrown away.
//
// The logrus logger is left at its most permissive level and the slog handler
// does the filtering. That is deliberate: it puts both halves of the process's
// output under one level, so raising the service's log level to debug reveals
// the bridged library's debug records too, from the same config key, instead of
// requiring a second knob nobody remembers exists.
//
// # Candidate for auxilia
//
// This belongs beside scribe in go.bigb.es/auxilia rather than in a service: it
// depends on nothing but logrus and the standard library, and any Go program
// with a logrus-shaped dependency wants it. It is here because it was needed
// here first. It does not belong in sr-ht-ecore — that is a SourceHut library,
// and this has nothing to do with SourceHut.
package logrusbridge

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"slices"
	"sync"

	"github.com/sirupsen/logrus"
)

// An Option configures Entry and Hook.
type Option func(*config)

type config struct {
	target *slog.Logger
}

// WithLogger sends the bridged records to l instead of to slog's default
// logger.
//
// The default is resolved at the moment a record is fired, not when the bridge
// is built, so a bridge constructed before slog.SetDefault still lands in the
// handler the process ends up with. Pass this only when the destination is not
// the process default — a test capturing records is the usual reason.
func WithLogger(l *slog.Logger) Option {
	return func(c *config) { c.target = l }
}

// Hook returns a logrus.Hook that forwards every record it is given to slog.
//
// Use it when the caller already holds a *logrus.Logger it wants redirected.
// Adding the hook does not stop that logger writing through its own formatter
// and output as well; silencing those is the caller's to do, and Entry is the
// version that has done it.
func Hook(opts ...Option) logrus.Hook {
	var cfg config
	for _, opt := range opts {
		opt(&cfg)
	}
	return &hook{cfg: cfg}
}

// Entry returns a *logrus.Entry whose every record goes to slog and nowhere
// else — the value to hand to a library that will accept nothing but one.
//
// The logger behind it is fresh rather than logrus' standard one: redirecting
// the standard logger would capture every other user of it in the process,
// which is a decision for that process and not for the library being handed
// this entry.
func Entry(opts ...Option) *logrus.Entry {
	l := logrus.New()
	// The two halves of "the hook is the only exit". Discarding the output
	// alone would still pay for formatting every record on its way to nowhere,
	// and the formatter is the expensive half.
	l.SetOutput(io.Discard)
	l.SetFormatter(noopFormatter{})
	// Everything reaches the hook; the slog handler decides what survives.
	l.SetLevel(logrus.TraceLevel)
	// Off, and worth saying why: logrus computes the caller by walking the
	// stack on every record, and a library handed this entry may log per
	// request. See hook.Fire for what happens if a caller turns it on anyway.
	l.SetReportCaller(false)
	l.AddHook(Hook(opts...))
	return logrus.NewEntry(l)
}

// noopFormatter formats a record into nothing. A nil slice is a valid return:
// logrus writes it to the output, and io.Discard accepts it.
type noopFormatter struct{}

func (noopFormatter) Format(*logrus.Entry) ([]byte, error) { return nil, nil }

type hook struct {
	cfg config

	// reported bounds the complaint about a panicking destination handler to
	// one line for the life of the process.
	reported sync.Once
}

// Levels asks for every record. The filtering belongs to the slog handler, for
// the reason given in the package comment.
func (h *hook) Levels() []logrus.Level { return logrus.AllLevels }

// Fire hands one logrus record to slog.
//
// It always returns nil. A non-nil error makes logrus print "Failed to fire
// hook" to os.Stderr, which is the one destination this bridge exists to keep
// records away from, and there is nothing a logging call can usefully do about
// its own failure anyway.
//
// It also never panics. This runs inside the logging path of a library that
// may be serving a request, so a panicking destination handler would otherwise
// take the request with it; losing a log line is the better of the two, and the
// once-only notice on stderr keeps a permanently broken handler from being
// silent forever.
func (h *hook) Fire(e *logrus.Entry) (err error) {
	defer func() {
		if r := recover(); r != nil {
			h.reported.Do(func() {
				fmt.Fprintf(os.Stderr,
					"logrusbridge: the destination slog handler panicked, dropping bridged records: %v\n", r)
			})
		}
		err = nil
	}()

	logger := h.cfg.target
	if logger == nil {
		logger = slog.Default()
	}

	ctx := e.Context
	if ctx == nil {
		ctx = context.Background()
	}

	level := Level(e.Level)
	if !logger.Enabled(ctx, level) {
		// Checked before the record is built rather than left to the handler:
		// the logrus side is wide open, so a library's per-chunk debug line
		// reaches here on every chunk and must cost an interface call, not an
		// allocation and a sort.
		return nil
	}

	// The record's time is the entry's, which logrus stamps before it fires
	// hooks. Taking time.Now() here would date every bridged record to when the
	// bridge got round to it.
	//
	// The PC is zero, so a handler with source locations enabled prints none
	// for a bridged record. That is the honest answer: the only PC this
	// function could name is its own, inside logrus, which tells the reader
	// nothing about where the line came from. When the caller has enabled
	// ReportCaller the real frame is attached below as attributes instead — a
	// runtime.Frame's PC is not the return address slog.Record expects, and a
	// source location that is quietly one line off is worse than none.
	rec := slog.NewRecord(e.Time, level, e.Message, 0)

	// Fields become attributes rather than a formatted blob. That is the whole
	// point of routing through a structured handler: a mask keyed on the
	// attribute path can only fire if the field is still an attribute when it
	// gets there.
	//
	// Sorted, because a logrus.Fields is a map and its iteration order is
	// random: unsorted, one library's records would shuffle their columns from
	// line to line.
	if len(e.Data) > 0 {
		keys := make([]string, 0, len(e.Data))
		for k := range e.Data {
			keys = append(keys, k)
		}
		slices.Sort(keys)
		attrs := make([]slog.Attr, 0, len(keys))
		for _, k := range keys {
			attrs = append(attrs, slog.Any(k, e.Data[k]))
		}
		rec.AddAttrs(attrs...)
	}

	if e.Caller != nil {
		rec.AddAttrs(slog.Group("source",
			slog.String("file", e.Caller.File),
			slog.Int("line", e.Caller.Line),
			slog.String("function", e.Caller.Function),
		))
	}

	// Handed to the handler rather than to logger.Log: the Logger methods build
	// their own record, stamping it with the current time and with a PC walked
	// out of this function's stack — the two things this bridge is carrying
	// from somewhere else. Attributes and groups the caller put on the logger
	// are on the handler it returns, so nothing is lost by going round it.
	//
	//nolint:errcheck // Handle's error has no reader; see the doc comment.
	_ = logger.Handler().Handle(ctx, rec)
	return nil
}

// Level maps a logrus level onto the slog level that means the same thing.
//
// logrus has three levels above Error and slog has none: Fatal and Panic
// describe what logrus does *after* the record — exit, or panic — rather than
// how bad the record is, and both arrive here as errors because that is what
// they are. Trace and Debug both land on Debug for the same reason in reverse:
// slog draws no line there, and inventing one with a negative custom level
// would make "debug" in a config file mean different things on the two sides of
// the bridge.
//
// Both are delivered. logrus fires its hooks before it writes, before
// Logger.Exit and before the panic (verified against logrus v1.9.3,
// Entry.log), so a record that ends the process still reaches slog first.
func Level(l logrus.Level) slog.Level {
	switch l {
	case logrus.TraceLevel, logrus.DebugLevel:
		return slog.LevelDebug
	case logrus.InfoLevel:
		return slog.LevelInfo
	case logrus.WarnLevel:
		return slog.LevelWarn
	case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
		return slog.LevelError
	default:
		// logrus defines no other level. An unknown one is treated as the most
		// serious rather than the least, so a level added upstream shows up
		// instead of disappearing.
		return slog.LevelError
	}
}

D internal/logrusbridge/logrusbridge_test.go => internal/logrusbridge/logrusbridge_test.go +0 -220
@@ 1,220 0,0 @@
package logrusbridge

import (
	"bytes"
	"context"
	"log/slog"
	"sync"
	"testing"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"go.bigb.es/auxilia/scribe"
)

// capture is a slog.Handler that keeps the records it is given, so a test can
// assert on the record itself — its time above all — rather than on a
// handler's rendering of it.
type capture struct {
	mu      sync.Mutex
	level   slog.Level
	records []slog.Record
}

func (c *capture) Enabled(_ context.Context, l slog.Level) bool { return l >= c.level }

func (c *capture) Handle(_ context.Context, r slog.Record) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.records = append(c.records, r.Clone())
	return nil
}

func (c *capture) WithAttrs([]slog.Attr) slog.Handler { return c }
func (c *capture) WithGroup(string) slog.Handler      { return c }

func (c *capture) only(t *testing.T) slog.Record {
	t.Helper()
	c.mu.Lock()
	defer c.mu.Unlock()
	require.Len(t, c.records, 1, "expected exactly one bridged record")
	return c.records[0]
}

// attrs flattens a record's attributes into a map for assertions.
func attrs(r slog.Record) map[string]any {
	m := make(map[string]any, r.NumAttrs())
	r.Attrs(func(a slog.Attr) bool {
		m[a.Key] = a.Value.Any()
		return true
	})
	return m
}

// The record that comes out the slog side is the one that went in the logrus
// side: same level, same message, same fields as attributes, and the time the
// record was made rather than the time the bridge got to it.
func TestBridgedRecordKeepsLevelMessageFieldsAndTime(t *testing.T) {
	c := &capture{level: slog.LevelDebug}
	e := Entry(WithLogger(slog.New(c)))

	made := time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC)
	e.WithTime(made).
		WithFields(logrus.Fields{"repo": "~alice/widgets", "chunks": 3}).
		Warn("chunk transfer stalled")

	rec := c.only(t)
	assert.Equal(t, slog.LevelWarn, rec.Level)
	assert.Equal(t, "chunk transfer stalled", rec.Message)
	assert.True(t, rec.Time.Equal(made), "record time %s, want %s", rec.Time, made)

	got := attrs(rec)
	assert.Equal(t, "~alice/widgets", got["repo"])
	assert.EqualValues(t, 3, got["chunks"])
	assert.Len(t, got, 2, "fields must arrive as attributes and nothing else")
}

// A field is an attribute by the time it reaches the handler, which is the only
// reason a mask can fire on it. This is the property the bridge exists for: a
// credential in a third-party library's log field is masked by the process's
// own rules rather than printed around them.
func TestASensitiveFieldIsMaskedByTheDestinationHandler(t *testing.T) {
	var buf bytes.Buffer
	dest := slog.New(scribe.NewTintHandler(
		scribe.WithWriter(&buf),
		scribe.WithLevel(slog.LevelDebug),
		scribe.WithNoColor(true),
		scribe.WithMaskKeys("token", "authorization"),
	))

	e := Entry(WithLogger(dest))
	e.WithFields(logrus.Fields{
		"token":         "s3cret-working-token",
		"authorization": "Bearer s3cret-jwt",
		"repo":          "~alice/widgets",
	}).Error("upload refused")

	out := buf.String()
	assert.NotContains(t, out, "s3cret-working-token")
	assert.NotContains(t, out, "s3cret-jwt")
	assert.Contains(t, out, "***")
	assert.Contains(t, out, "~alice/widgets", "an unmasked field must still be readable")
}

func TestLevelMapping(t *testing.T) {
	for _, tc := range []struct {
		in   logrus.Level
		want slog.Level
	}{
		{logrus.TraceLevel, slog.LevelDebug},
		{logrus.DebugLevel, slog.LevelDebug},
		{logrus.InfoLevel, slog.LevelInfo},
		{logrus.WarnLevel, slog.LevelWarn},
		{logrus.ErrorLevel, slog.LevelError},
		{logrus.FatalLevel, slog.LevelError},
		{logrus.PanicLevel, slog.LevelError},
	} {
		assert.Equal(t, tc.want, Level(tc.in), "logrus %s", tc.in)
	}
}

// The logrus side is left wide open so that the slog handler decides what
// survives: raising the service's log level must reveal the bridged library's
// debug records too, from the same setting.
func TestTheSlogHandlerDoesTheFiltering(t *testing.T) {
	quiet := &capture{level: slog.LevelWarn}
	e := Entry(WithLogger(slog.New(quiet)))

	e.Debug("per-chunk noise")
	e.Info("also below the bar")
	quiet.mu.Lock()
	assert.Empty(t, quiet.records, "the destination's level must gate bridged records")
	quiet.mu.Unlock()

	e.Warn("this one counts")
	assert.Equal(t, "this one counts", quiet.only(t).Message)

	// And the logrus logger itself must not be the one filtering, or the two
	// halves of the process would need two settings.
	assert.Equal(t, logrus.TraceLevel, e.Logger.GetLevel())
}

// The hook is the only exit: nothing is formatted and nothing is written the
// logrus way, so the destination handler is the whole of the output.
func TestNothingLeavesThroughLogrus(t *testing.T) {
	c := &capture{level: slog.LevelDebug}
	e := Entry(WithLogger(slog.New(c)))

	var buf bytes.Buffer
	e.Logger.SetOutput(&buf) // stand in for io.Discard so we can look at it

	e.Info("hello")

	assert.Empty(t, buf.String(), "the formatter must produce nothing")
	assert.Equal(t, "hello", c.only(t).Message)
}

// logrus fires its hooks before it writes, before Logger.Exit and before the
// panic, so a record that ends the process still reaches slog first. Verified
// here rather than trusted, because it is the arm nobody would notice missing
// until they were reading a crash without its last line.
func TestFatalAndPanicRecordsReachTheHook(t *testing.T) {
	t.Run("fatal", func(t *testing.T) {
		c := &capture{level: slog.LevelDebug}
		e := Entry(WithLogger(slog.New(c)))
		exited := false
		e.Logger.ExitFunc = func(int) { exited = true }

		e.Fatal("cannot serve")

		assert.True(t, exited, "logrus must still exit after the hook")
		rec := c.only(t)
		assert.Equal(t, slog.LevelError, rec.Level)
		assert.Equal(t, "cannot serve", rec.Message)
	})

	t.Run("panic", func(t *testing.T) {
		c := &capture{level: slog.LevelDebug}
		e := Entry(WithLogger(slog.New(c)))

		assert.Panics(t, func() { e.Panic("unrecoverable") })

		rec := c.only(t)
		assert.Equal(t, slog.LevelError, rec.Level)
		assert.Equal(t, "unrecoverable", rec.Message)
	})
}

// The default logger is resolved when the record fires and not when the bridge
// is built, so a bridge handed to a library during startup still lands in the
// handler the process installs.
func TestTheDefaultLoggerIsResolvedWhenTheRecordFires(t *testing.T) {
	previous := slog.Default()
	t.Cleanup(func() { slog.SetDefault(previous) })

	e := Entry() // built before the default is installed

	c := &capture{level: slog.LevelDebug}
	slog.SetDefault(slog.New(c))

	e.Info("late binding")
	assert.Equal(t, "late binding", c.only(t).Message)
}

// A destination that panics costs a log line and not the request it was logging.
func TestAPanickingDestinationDoesNotEscape(t *testing.T) {
	e := Entry(WithLogger(slog.New(panicHandler{})))
	assert.NotPanics(t, func() { e.Info("into the void") })
}

type panicHandler struct{}

func (panicHandler) Enabled(context.Context, slog.Level) bool { return true }
func (panicHandler) Handle(context.Context, slog.Record) error {
	panic("the handler is broken")
}
func (panicHandler) WithAttrs([]slog.Attr) slog.Handler { return panicHandler{} }
func (panicHandler) WithGroup(string) slog.Handler      { return panicHandler{} }