Add the canonical .golangci.yml (v2 schema, all linters enabled with a
small documented disable list) and pin the Dockerfile lint stage to
golangci/golangci-lint:v2.12.2 by tag and digest, replacing the old
v1.64.8 digest-only pin.
Fix all findings surfaced by the v1 to v2 jump without changing any
exported signatures or behavior:
- add package and exported-symbol doc comments (revive)
- rename unused handler parameters to underscore (revive)
- check or explicitly discard error returns (errcheck, errchkjson)
- wrap errors with %w instead of %v (err113)
- use http.NewRequestWithContext instead of http.Post (noctx)
- replace fmt.Println with fmt.Fprintln(os.Stdout, ...) (forbidigo)
- name magic numbers as constants (mnd)
- add explicit slog.LevelDebug case (exhaustive)
- interface{} to any (modernize)
- move tests to the simplelog_test package (testpackage) and add
t.Parallel() (paralleltest)
- move NewWebhookHandler above its methods (funcorder)
- whitespace, line-length, and blank-line fixes (wsl_v5, whitespace,
nlreturn, lll, embeddedstructfieldcheck)
- nolint with justification for the intentional init/global design
(gochecknoinits, gochecknoglobals) and interface-returning
constructor (ireturn)
171 lines
4.1 KiB
Go
171 lines
4.1 KiB
Go
// Package simplelog installs a multiplexing slog handler as the process
|
|
// default on import. It logs human-readable colored output when stdout is
|
|
// a terminal, JSON otherwise, and can additionally POST each record to a
|
|
// webhook configured via the LOGGER_WEBHOOK_URL environment variable.
|
|
package simplelog
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/mattn/go-isatty"
|
|
)
|
|
|
|
//nolint:gochecknoglobals // webhook destination is read once from the environment
|
|
var webhookURL = os.Getenv("LOGGER_WEBHOOK_URL")
|
|
|
|
//nolint:gochecknoglobals // package-level default logger state is the package's design
|
|
var (
|
|
ourCustomLogger *slog.Logger
|
|
ourCustomHandler slog.Handler
|
|
)
|
|
|
|
//nolint:gochecknoinits // installs itself as slog default on import by design
|
|
func init() {
|
|
ourCustomHandler = NewMultiplexHandler()
|
|
ourCustomLogger = slog.New(ourCustomHandler)
|
|
slog.SetDefault(ourCustomLogger)
|
|
}
|
|
|
|
// MultiplexHandler fans each log record out to a set of underlying
|
|
// handlers.
|
|
type MultiplexHandler struct {
|
|
handlers []ExtendedHandler
|
|
}
|
|
|
|
// NewMultiplexHandler returns a handler that writes colored console
|
|
// output when stdout is a terminal and JSON otherwise, plus an optional
|
|
// webhook handler when LOGGER_WEBHOOK_URL is set.
|
|
func NewMultiplexHandler() slog.Handler {
|
|
cl := &MultiplexHandler{}
|
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
|
cl.handlers = append(cl.handlers, NewConsoleHandler())
|
|
} else {
|
|
cl.handlers = append(cl.handlers, NewJSONHandler())
|
|
}
|
|
|
|
if webhookURL != "" {
|
|
handler, err := NewWebhookHandler(webhookURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize Webhook handler: %v", err)
|
|
}
|
|
|
|
cl.handlers = append(cl.handlers, handler)
|
|
}
|
|
|
|
return cl
|
|
}
|
|
|
|
// Handle forwards the record to every underlying handler, stopping at
|
|
// the first error.
|
|
func (cl *MultiplexHandler) Handle(
|
|
ctx context.Context,
|
|
record slog.Record,
|
|
) error {
|
|
for _, handler := range cl.handlers {
|
|
err := handler.Handle(ctx, record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Enabled reports whether the handler processes records at the given
|
|
// level; it always returns true.
|
|
func (cl *MultiplexHandler) Enabled(
|
|
_ context.Context,
|
|
_ slog.Level,
|
|
) bool {
|
|
// send us all events
|
|
return true
|
|
}
|
|
|
|
// WithAttrs returns a new MultiplexHandler whose underlying handlers
|
|
// each carry the given attributes.
|
|
func (cl *MultiplexHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
newHandlers := make([]ExtendedHandler, len(cl.handlers))
|
|
for i, handler := range cl.handlers {
|
|
newHandlers[i] = handler.WithAttrs(attrs)
|
|
}
|
|
|
|
return &MultiplexHandler{handlers: newHandlers}
|
|
}
|
|
|
|
// WithGroup returns a new MultiplexHandler whose underlying handlers
|
|
// each use the given group name.
|
|
func (cl *MultiplexHandler) WithGroup(name string) slog.Handler {
|
|
newHandlers := make([]ExtendedHandler, len(cl.handlers))
|
|
for i, handler := range cl.handlers {
|
|
newHandlers[i] = handler.WithGroup(name)
|
|
}
|
|
|
|
return &MultiplexHandler{handlers: newHandlers}
|
|
}
|
|
|
|
// ExtendedEvent describes an Event augmented with caller file and line
|
|
// information.
|
|
type ExtendedEvent interface {
|
|
GetID() uuid.UUID
|
|
GetTimestamp() time.Time
|
|
GetLevel() string
|
|
GetMessage() string
|
|
GetData() json.RawMessage
|
|
GetFile() string
|
|
GetLine() int
|
|
}
|
|
|
|
type extendedEvent struct {
|
|
Event
|
|
|
|
File string `json:"file"`
|
|
Line int `json:"line"`
|
|
}
|
|
|
|
func (e extendedEvent) GetID() uuid.UUID {
|
|
return e.ID
|
|
}
|
|
|
|
func (e extendedEvent) GetTimestamp() time.Time {
|
|
return e.Timestamp
|
|
}
|
|
|
|
func (e extendedEvent) GetLevel() string {
|
|
return e.Level
|
|
}
|
|
|
|
func (e extendedEvent) GetMessage() string {
|
|
return e.Message
|
|
}
|
|
|
|
func (e extendedEvent) GetData() json.RawMessage {
|
|
return e.Data
|
|
}
|
|
|
|
func (e extendedEvent) GetFile() string {
|
|
return e.File
|
|
}
|
|
|
|
func (e extendedEvent) GetLine() int {
|
|
return e.Line
|
|
}
|
|
|
|
// NewExtendedEvent wraps baseEvent with the caller file and line it was
|
|
// logged from.
|
|
//
|
|
//nolint:ireturn // returning the interface is this constructor's public API
|
|
func NewExtendedEvent(baseEvent Event, file string, line int) ExtendedEvent {
|
|
return extendedEvent{
|
|
Event: baseEvent,
|
|
File: file,
|
|
Line: line,
|
|
}
|
|
}
|