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>
This commit is contained in:
@@ -17,8 +17,6 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"geniusrun/backend/internal/log"
|
||||
)
|
||||
|
||||
//go:embed wrapper/wrapper.py
|
||||
@@ -149,14 +147,14 @@ func (c *subprocessClient) ensureStarted(ctx context.Context) error {
|
||||
if c.scriptPath == "" {
|
||||
f, err := os.CreateTemp("", "geniusrun-garmin-wrapper-*.py")
|
||||
if err != nil {
|
||||
return fmt.Errorf("write embedded wrapper script: %w", err)
|
||||
return fmt.Errorf("write wrapper script: %w", err)
|
||||
}
|
||||
if _, err := f.WriteString(wrapperScript); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("write embedded wrapper script: %w", err)
|
||||
return fmt.Errorf("write wrapper script: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("write embedded wrapper script: %w", err)
|
||||
return fmt.Errorf("write wrapper script: %w", err)
|
||||
}
|
||||
c.scriptPath = f.Name()
|
||||
}
|
||||
@@ -189,7 +187,7 @@ func (c *subprocessClient) ensureStarted(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("spawn garmin wrapper subprocess: %w", err)
|
||||
return fmt.Errorf("spawn wrapper subprocess: %w", err)
|
||||
}
|
||||
// wrapper.py logs auth/rate-limit diagnostics to stderr as JSON lines
|
||||
// (see its _log helper); forwardWrapperStderr re-emits them through the
|
||||
@@ -216,19 +214,22 @@ func (c *subprocessClient) ensureStarted(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// roundTrip sends one request and returns its result payload, or an error
|
||||
// execute sends one request and returns its result payload, or an error
|
||||
// if the wrapper reported one. Callers must hold c.mu and have already
|
||||
// called ensureStarted. Logs exactly one "Garmin wrapper call" line
|
||||
// regardless of outcome (see internal/applog) -- cmd/params are always
|
||||
// safe to log in full here: Garmin credentials only ever reach the
|
||||
// subprocess via env vars at spawn time (see ensureStarted), never through
|
||||
// these wire params.
|
||||
func (c *subprocessClient) roundTrip(ctx context.Context, cmdName string, params any) (result json.RawMessage, err error) {
|
||||
// called ensureStarted. Logs exactly one type=wrapper line regardless of
|
||||
// outcome (the record's meaning is carried by its fields, no msg) --
|
||||
// cmd/params are always safe to log in full here: Garmin credentials only
|
||||
// ever reach the subprocess via env vars at spawn time (see
|
||||
// ensureStarted), never through these wire params.
|
||||
func (c *subprocessClient) execute(ctx context.Context, cmd string, params any) (result json.RawMessage, err error) {
|
||||
start := time.Now()
|
||||
var errorType, traceback string // Python-side failure detail, set from the error response
|
||||
defer func() {
|
||||
attrs := []slog.Attr{
|
||||
slog.String("cmd", cmdName),
|
||||
slog.String("type", "wrapper"),
|
||||
slog.String("class", "garmin.subprocessClient"),
|
||||
slog.String("method", "execute"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Any("params", params),
|
||||
slog.Int64("duration_ms", time.Since(start).Milliseconds()),
|
||||
}
|
||||
@@ -246,41 +247,41 @@ func (c *subprocessClient) roundTrip(ctx context.Context, cmdName string, params
|
||||
attrs = append(attrs, slog.String("traceback", traceback))
|
||||
}
|
||||
}
|
||||
applog.FromContext(ctx).LogAttrs(context.Background(), level, "Garmin wrapper call", attrs...)
|
||||
slog.Default().LogAttrs(ctx, level, "", attrs...)
|
||||
}()
|
||||
|
||||
c.nextID++
|
||||
id := c.nextID
|
||||
|
||||
if err = c.enc.Encode(wireRequest{ID: id, Cmd: cmdName, Params: params}); err != nil {
|
||||
err = fmt.Errorf("write %s request: %w", cmdName, err)
|
||||
if err = c.enc.Encode(wireRequest{ID: id, Cmd: cmd, Params: params}); err != nil {
|
||||
err = fmt.Errorf("write %s request: %w", cmd, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.scanner.Scan() {
|
||||
if serr := c.scanner.Err(); serr != nil {
|
||||
err = fmt.Errorf("read %s response: %w", cmdName, serr)
|
||||
err = fmt.Errorf("read %s response: %w", cmd, serr)
|
||||
} else {
|
||||
err = fmt.Errorf("read %s response: subprocess closed its output", cmdName)
|
||||
err = fmt.Errorf("read %s response: subprocess closed its output", cmd)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp wireResponse
|
||||
if uerr := json.Unmarshal(c.scanner.Bytes(), &resp); uerr != nil {
|
||||
err = fmt.Errorf("parse %s response: %w", cmdName, uerr)
|
||||
err = fmt.Errorf("parse %s response: %w", cmd, uerr)
|
||||
return nil, err
|
||||
}
|
||||
if resp.ID != id {
|
||||
err = fmt.Errorf("%s response id mismatch: got %d, want %d", cmdName, resp.ID, id)
|
||||
err = fmt.Errorf("%s response id mismatch: got %d, want %d", cmd, resp.ID, id)
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error != "" {
|
||||
errorType, traceback = resp.ErrorType, resp.Traceback
|
||||
if resp.NotFound {
|
||||
err = fmt.Errorf("%s: %s: %w", cmdName, resp.Error, ErrNotFound)
|
||||
err = fmt.Errorf("%s: %s: %w", cmd, resp.Error, ErrNotFound)
|
||||
} else {
|
||||
err = fmt.Errorf("%s: %s", cmdName, resp.Error)
|
||||
err = fmt.Errorf("%s: %s", cmd, resp.Error)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -295,7 +296,7 @@ func (c *subprocessClient) Authenticate(ctx context.Context) (AuthResult, error)
|
||||
if err := c.ensureStarted(ctx); err != nil {
|
||||
return AuthResult{}, err
|
||||
}
|
||||
raw, err := c.roundTrip(ctx, "authenticate", nil)
|
||||
raw, err := c.execute(ctx, "authenticate", nil)
|
||||
if err != nil {
|
||||
return AuthResult{}, err
|
||||
}
|
||||
@@ -313,7 +314,7 @@ func (c *subprocessClient) CompleteMFA(ctx context.Context, code string) (AuthRe
|
||||
if err := c.ensureStarted(ctx); err != nil {
|
||||
return AuthResult{}, err
|
||||
}
|
||||
raw, err := c.roundTrip(ctx, "complete_mfa", map[string]any{"code": code})
|
||||
raw, err := c.execute(ctx, "complete_mfa", map[string]any{"code": code})
|
||||
if err != nil {
|
||||
return AuthResult{}, err
|
||||
}
|
||||
@@ -374,7 +375,7 @@ func (c *subprocessClient) close() error {
|
||||
// the corresponding level with their attrs preserved. Anything that isn't
|
||||
// such a line (a Python startup crash before _log exists, a chatty
|
||||
// third-party library printing directly) is wrapped as a warn-level
|
||||
// "garmin wrapper stderr" record rather than passed through raw, so the
|
||||
// type=wrapper record carrying the raw "line" rather than passed through, so the
|
||||
// backend's combined output is JSON no matter what the subprocess does.
|
||||
func forwardWrapperStderr(r io.Reader) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
@@ -383,19 +384,21 @@ func forwardWrapperStderr(r io.Reader) {
|
||||
line := scanner.Text()
|
||||
var entry map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &entry); err != nil || entry["msg"] == nil {
|
||||
slog.Warn("garmin wrapper stderr", "line", line)
|
||||
slog.Warn("", "type", "wrapper", "class", "garmin", "method", "forwardWrapperStderr", "line", line)
|
||||
continue
|
||||
}
|
||||
msg, _ := entry["msg"].(string)
|
||||
levelName, _ := entry["level"].(string)
|
||||
delete(entry, "msg")
|
||||
delete(entry, "level")
|
||||
attrs := make([]slog.Attr, 0, len(entry)+1)
|
||||
attrs = append(attrs, slog.String("source", "garmin-wrapper"))
|
||||
// class is the Python module; method arrives in the entry itself
|
||||
// (wrapper.py's _log stamps the emitting function).
|
||||
attrs := make([]slog.Attr, 0, len(entry)+2)
|
||||
attrs = append(attrs, slog.String("type", "wrapper"), slog.String("class", "wrapper"))
|
||||
for k, v := range entry {
|
||||
attrs = append(attrs, slog.Any(k, v))
|
||||
}
|
||||
slog.LogAttrs(context.Background(), wrapperLogLevel(levelName), msg, attrs...)
|
||||
slog.Default().LogAttrs(context.Background(), wrapperLogLevel(levelName), msg, attrs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +439,7 @@ func (c *subprocessClient) GetActivities(ctx context.Context, startDate, endDate
|
||||
if err := c.ensureStarted(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := c.roundTrip(ctx, "call", callParams{
|
||||
raw, err := c.execute(ctx, "call", callParams{
|
||||
Method: "get_activities_by_date",
|
||||
Args: map[string]any{"startdate": startDate, "enddate": endDate},
|
||||
})
|
||||
@@ -471,7 +474,7 @@ func (c *subprocessClient) GetActivitySplits(ctx context.Context, activityID int
|
||||
if err := c.ensureStarted(ctx); err != nil {
|
||||
return ActivitySplits{}, err
|
||||
}
|
||||
raw, err := c.roundTrip(ctx, "call", callParams{
|
||||
raw, err := c.execute(ctx, "call", callParams{
|
||||
Method: "get_activity_splits",
|
||||
Args: map[string]any{"activity_id": strconv.FormatInt(activityID, 10)},
|
||||
})
|
||||
@@ -506,7 +509,7 @@ func (c *subprocessClient) GetActivityDetails(ctx context.Context, activityID in
|
||||
if err := c.ensureStarted(ctx); err != nil {
|
||||
return ActivityDetails{}, err
|
||||
}
|
||||
raw, err := c.roundTrip(ctx, "call", callParams{
|
||||
raw, err := c.execute(ctx, "call", callParams{
|
||||
Method: "get_activity_details",
|
||||
Args: map[string]any{"activity_id": strconv.FormatInt(activityID, 10)},
|
||||
})
|
||||
@@ -529,7 +532,7 @@ func (c *subprocessClient) GetWorkoutByID(ctx context.Context, workoutID int64)
|
||||
if err := c.ensureStarted(ctx); err != nil {
|
||||
return Workout{}, err
|
||||
}
|
||||
raw, err := c.roundTrip(ctx, "call", callParams{
|
||||
raw, err := c.execute(ctx, "call", callParams{
|
||||
Method: "get_workout_by_id",
|
||||
Args: map[string]any{"workout_id": strconv.FormatInt(workoutID, 10)},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user