Skip to content

Commit b859ff4

Browse files
authored
feat(logger): Wrapper to extract and log context (#4863)
Add a contextHandler to wrap around our slog.Handler so that key-value stored in context will be automatically added to the log as attributes. Services will have to explicitly register what context keys they want to show up in logs. To register a context key to be logged (after initializing the logger): ``` logger.RegisterContextKey(ctxKey, logKey) ```
1 parent 9856fa0 commit b859ff4

3 files changed

Lines changed: 61 additions & 14 deletions

File tree

go/cmd/gitter/gitter.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
type contextKey string
3434

3535
const (
36-
urlKey contextKey = "url"
36+
urlKey contextKey = "repoURL"
3737
)
3838

3939
const getGitEndpoint = "/getgit"
@@ -54,7 +54,7 @@ const shutdownTimeout = 10 * time.Second
5454
// runCmd executes a command with context cancellation handled by sending SIGINT.
5555
// It logs cancellation errors separately as requested.
5656
func runCmd(ctx context.Context, dir string, env []string, name string, args ...string) error {
57-
logger.DebugContext(ctx, "Running command", slog.String("cmd", name), slog.String("url", ctx.Value(urlKey).(string)), slog.Any("args", args))
57+
logger.DebugContext(ctx, "Running command", slog.String("cmd", name), slog.Any("args", args))
5858
cmd := exec.CommandContext(ctx, name, args...)
5959
if dir != "" {
6060
cmd.Dir = dir
@@ -64,7 +64,7 @@ func runCmd(ctx context.Context, dir string, env []string, name string, args ...
6464
}
6565
// Use SIGINT instead of SIGKILL for graceful shutdown of subprocesses
6666
cmd.Cancel = func() error {
67-
logger.DebugContext(ctx, "SIGINT sent to command", slog.String("cmd", name), slog.String("url", ctx.Value(urlKey).(string)), slog.Any("args", args))
67+
logger.DebugContext(ctx, "SIGINT sent to command", slog.String("cmd", name), slog.Any("args", args))
6868
return cmd.Process.Signal(syscall.SIGINT)
6969
}
7070
// Ensure it eventually dies if it ignores SIGINT
@@ -74,13 +74,13 @@ func runCmd(ctx context.Context, dir string, env []string, name string, args ...
7474
if err != nil {
7575
if ctx.Err() != nil {
7676
// Log separately if cancelled
77-
logger.WarnContext(ctx, "Command cancelled", slog.String("cmd", name), slog.String("url", ctx.Value(urlKey).(string)), slog.Any("err", ctx.Err()))
77+
logger.WarnContext(ctx, "Command cancelled", slog.String("cmd", name), slog.Any("err", ctx.Err()))
7878
return fmt.Errorf("command %s cancelled: %w", name, ctx.Err())
7979
}
8080

8181
return fmt.Errorf("command %s failed: %w, output: %s", name, err, out)
8282
}
83-
logger.DebugContext(ctx, "Command completed successfully", slog.String("cmd", name), slog.String("url", ctx.Value(urlKey).(string)), slog.String("out", string(out)))
83+
logger.DebugContext(ctx, "Command completed successfully", slog.String("cmd", name), slog.String("out", string(out)))
8484

8585
return nil
8686
}
@@ -137,7 +137,7 @@ func fetchBlob(ctx context.Context, url string, forceUpdate bool) ([]byte, error
137137

138138
// Check if we need to fetch
139139
if forceUpdate || !ok || time.Since(accessTime) > fetchTimeout {
140-
logger.InfoContext(ctx, "Fetching git blob", slog.String("url", ctx.Value(urlKey).(string)), slog.Duration("sinceAccessTime", time.Since(accessTime)))
140+
logger.InfoContext(ctx, "Fetching git blob", slog.Duration("sinceAccessTime", time.Since(accessTime)))
141141
if _, err := os.Stat(path.Join(repoPath, ".git")); os.IsNotExist(err) {
142142
// Clone
143143
err := runCmd(ctx, "", []string{"GIT_TERMINAL_PROMPT=0"}, "git", "clone", "--", url, repoPath)
@@ -156,7 +156,7 @@ func fetchBlob(ctx context.Context, url string, forceUpdate bool) ([]byte, error
156156
if err != nil && isIndexLockError(err) {
157157
// index.lock exists, likely a previous git reset got terminated and wasn't cleaned up properly.
158158
// We can remove the file and retry the command
159-
logger.WarnContext(ctx, "index.lock exists, attempting to remove and retry", slog.String("url", ctx.Value(urlKey).(string)))
159+
logger.WarnContext(ctx, "index.lock exists, attempting to remove and retry")
160160
indexLockPath := filepath.Join(repoPath, ".git", "index.lock")
161161
if err := os.Remove(indexLockPath); err != nil {
162162
return nil, fmt.Errorf("failed to remove index.lock in %s: %w", repoPath, err)
@@ -169,7 +169,7 @@ func fetchBlob(ctx context.Context, url string, forceUpdate bool) ([]byte, error
169169
}
170170
}
171171

172-
logger.InfoContext(ctx, "Archiving git blob", slog.String("url", ctx.Value(urlKey).(string)))
172+
logger.InfoContext(ctx, "Archiving git blob")
173173
// Archive
174174
// tar --zstd -cf <archivePath> -C "<gitStorePath>/<repoDirName>" .
175175
// using -C to archive the relative path so it unzips nicely
@@ -201,6 +201,7 @@ func fetchBlob(ctx context.Context, url string, forceUpdate bool) ([]byte, error
201201

202202
func main() {
203203
logger.InitGlobalLogger()
204+
logger.RegisterContextKey(urlKey, "repoURL")
204205
defer logger.Close()
205206

206207
port := flag.Int("port", 8888, "Listen port")
@@ -283,7 +284,7 @@ func gitHandler(w http.ResponseWriter, r *http.Request) {
283284

284285
ctx := context.WithValue(r.Context(), urlKey, url)
285286

286-
logger.InfoContext(ctx, "Received request", slog.String("url", url), slog.Bool("forceUpdate", forceUpdate), slog.String("remoteAddr", r.RemoteAddr))
287+
logger.InfoContext(ctx, "Received request", slog.Bool("forceUpdate", forceUpdate), slog.String("remoteAddr", r.RemoteAddr))
287288
// If request came from a local ip, don't do the check
288289
if !isLocalRequest(r) {
289290
// Check if url starts with protocols: http(s)://, git://, ssh://, (s)ftp://
@@ -308,7 +309,7 @@ func gitHandler(w http.ResponseWriter, r *http.Request) {
308309
})
309310

310311
if err != nil {
311-
logger.ErrorContext(ctx, "Error fetching/archiving blob", slog.String("url", ctx.Value(urlKey).(string)), slog.Any("error", err))
312+
logger.ErrorContext(ctx, "Error fetching blob", slog.Any("error", err))
312313
if isAuthError(err) {
313314
http.Error(w, fmt.Sprintf("Error fetching blob: %v", err), http.StatusForbidden)
314315
return
@@ -322,11 +323,11 @@ func gitHandler(w http.ResponseWriter, r *http.Request) {
322323
w.Header().Set("Content-Disposition", "attachment; filename=\"git-blob.zst\"")
323324
w.WriteHeader(http.StatusOK)
324325
if _, err := io.Copy(w, bytes.NewReader(fileData.([]byte))); err != nil {
325-
logger.ErrorContext(ctx, "Error copying file", slog.String("url", url), slog.Any("error", err))
326+
logger.ErrorContext(ctx, "Error copying file", slog.Any("error", err))
326327
http.Error(w, "Error copying file", http.StatusInternalServerError)
327328

328329
return
329330
}
330331

331-
logger.InfoContext(ctx, "Request completed successfully", slog.String("url", ctx.Value(urlKey).(string)))
332+
logger.InfoContext(ctx, "Request completed successfully")
332333
}

go/logger/contexthandler.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package logger
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"sync"
7+
)
8+
9+
var (
10+
ctxKeysMu sync.RWMutex
11+
ctxKeys = make(map[any]string)
12+
)
13+
14+
// RegisterContextKey registers a context key that should be automatically
15+
// extracted and logged if present in the context.
16+
func RegisterContextKey(ctxKey any, logKey string) {
17+
ctxKeysMu.Lock()
18+
defer ctxKeysMu.Unlock()
19+
ctxKeys[ctxKey] = logKey
20+
}
21+
22+
// contextHandler is a wrapper around slog.Handler that automatically
23+
// extracts registered context keys and logs their values as part of the attributes.
24+
type contextHandler struct {
25+
slog.Handler
26+
}
27+
28+
func (h *contextHandler) Handle(ctx context.Context, r slog.Record) error {
29+
ctxKeysMu.RLock()
30+
for ctxKey, logKey := range ctxKeys {
31+
if val := ctx.Value(ctxKey); val != nil {
32+
r.AddAttrs(slog.Any(logKey, val))
33+
}
34+
}
35+
ctxKeysMu.RUnlock()
36+
37+
return h.Handler.Handle(ctx, r)
38+
}
39+
40+
func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
41+
return &contextHandler{h.Handler.WithAttrs(attrs)}
42+
}
43+
44+
func (h *contextHandler) WithGroup(name string) slog.Handler {
45+
return &contextHandler{h.Handler.WithGroup(name)}
46+
}

go/logger/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
)
2121

2222
var (
23-
slogLogger = slog.New(newLocalHandler(os.Stdout))
23+
slogLogger = slog.New(&contextHandler{newLocalHandler(os.Stdout)})
2424
once sync.Once
2525
tp *sdktrace.TracerProvider
2626
projectID string
@@ -49,7 +49,7 @@ func InitGlobalLogger() {
4949
initTracing(context.Background(), projectID, serviceName)
5050
}
5151
handler := slog.NewJSONHandler(os.Stdout, cloudHandlerOptions())
52-
slogLogger = slog.New(handler)
52+
slogLogger = slog.New(&contextHandler{handler})
5353
})
5454
}
5555

0 commit comments

Comments
 (0)