2026-07-26 14:28:22 +02:00
|
|
|
// Package applog provides geniusrun's structured JSON logging: a
|
refactor(log): unified schema — mandatory type/class/method, request_id removed
Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 19:45:30 +02:00
|
|
|
// log/slog-based logger writing to stdout, plus the App helper that tags
|
2026-08-04 19:59:48 +02:00
|
|
|
// records with the schema fields every geniusrun log line carries --
|
|
|
|
|
// "type" ("app" | "http" | "wrapper"), "package" (Go package; absent on
|
|
|
|
|
// Python-emitted lines), "file" (Go/Python source file basename), "class"
|
|
|
|
|
// (Go receiver type or Python class, omitted when there is none), and
|
|
|
|
|
// "method" (the emitting Go/Python function). "msg" is optional:
|
|
|
|
|
// NewLogger's handler drops it when empty.
|
2026-07-26 14:28:22 +02:00
|
|
|
package applog
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"io"
|
|
|
|
|
"log/slog"
|
2026-08-04 19:59:48 +02:00
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
2026-07-26 14:28:22 +02:00
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// NewLogger builds a JSON-handler *slog.Logger writing to w at the given
|
|
|
|
|
// level ("debug"|"info"|"warn"|"error", case-insensitive; anything else
|
refactor(log): unified schema — mandatory type/class/method, request_id removed
Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 19:45:30 +02:00
|
|
|
// defaults to info). Empty msg values are omitted from the output --
|
|
|
|
|
// records whose meaning is fully carried by type/class/method and attrs
|
|
|
|
|
// (e.g. the HTTP request line) don't need one.
|
2026-07-26 14:28:22 +02:00
|
|
|
func NewLogger(level string, w io.Writer) *slog.Logger {
|
refactor(log): unified schema — mandatory type/class/method, request_id removed
Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 19:45:30 +02:00
|
|
|
return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
|
|
|
|
|
Level: parseLevel(level),
|
|
|
|
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
|
|
|
|
if len(groups) == 0 && a.Key == slog.MessageKey && a.Value.String() == "" {
|
|
|
|
|
return slog.Attr{}
|
|
|
|
|
}
|
|
|
|
|
return a
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 19:59:48 +02:00
|
|
|
// App returns the default logger tagged with the schema fields for an
|
|
|
|
|
// ordinary application record (type=app), deriving the emitting location
|
|
|
|
|
// from the caller via the runtime: package, file, class (the receiver
|
|
|
|
|
// type, omitted for free functions), method. Derivation instead of
|
|
|
|
|
// hand-typed strings means the labels can never drift from the code. The
|
|
|
|
|
// http and wrapper emitters (internal/api's logging middleware,
|
|
|
|
|
// internal/garmin's execute/forwardWrapperStderr) tag their own type and
|
|
|
|
|
// location instead.
|
|
|
|
|
func App() *slog.Logger {
|
|
|
|
|
pkg, file, class, method := location(1)
|
|
|
|
|
logger := slog.Default().With("type", "app", "package", pkg, "file", file)
|
|
|
|
|
if class != "" {
|
|
|
|
|
logger = logger.With("class", class)
|
|
|
|
|
}
|
|
|
|
|
return logger.With("method", method)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// location resolves the caller (skip frames above this function's caller)
|
|
|
|
|
// into the log schema's package/file/class/method fields. A method's
|
|
|
|
|
// runtime name looks like "geniusrun/backend/internal/api.(*Server).X";
|
|
|
|
|
// a free function's like "geniusrun/backend/internal/api.X"; a closure
|
|
|
|
|
// gets its parent's name plus ".funcN", kept verbatim in method.
|
|
|
|
|
func location(skip int) (pkg, file, class, method string) {
|
|
|
|
|
pc, path, _, ok := runtime.Caller(skip + 1)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "unknown", "unknown", "", "unknown"
|
|
|
|
|
}
|
|
|
|
|
file = filepath.Base(path)
|
|
|
|
|
full := runtime.FuncForPC(pc).Name()
|
|
|
|
|
base := full[strings.LastIndex(full, "/")+1:]
|
|
|
|
|
pkg, rest, _ := strings.Cut(base, ".")
|
|
|
|
|
if strings.HasPrefix(rest, "(*") {
|
|
|
|
|
if end := strings.Index(rest, ")."); end != -1 {
|
|
|
|
|
class = rest[2:end]
|
|
|
|
|
rest = rest[end+2:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return pkg, file, class, rest
|
2026-07-26 14:28:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseLevel(level string) slog.Level {
|
|
|
|
|
switch strings.ToLower(level) {
|
|
|
|
|
case "debug":
|
|
|
|
|
return slog.LevelDebug
|
|
|
|
|
case "warn":
|
|
|
|
|
return slog.LevelWarn
|
|
|
|
|
case "error":
|
|
|
|
|
return slog.LevelError
|
|
|
|
|
default:
|
|
|
|
|
return slog.LevelInfo
|
|
|
|
|
}
|
|
|
|
|
}
|