// Package applog provides geniusrun's structured JSON logging: a // log/slog-based logger writing to stdout, plus the App helper that tags // 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. package applog import ( "io" "log/slog" "path/filepath" "runtime" "strings" ) // NewLogger builds a JSON-handler *slog.Logger writing to w at the given // level ("debug"|"info"|"warn"|"error", case-insensitive; anything else // 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. func NewLogger(level string, w io.Writer) *slog.Logger { 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 }, })) } // 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 } 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 } }