internal/worker/log: use slog
Use the log/slog package instead of golang.org/x/event.
This removes x/event from the repo.
Until Go 1.22 comes out, use golang.org/x/exp/slog, because
we still test one version back (Go 1.20), and log/slog appeared
in Go 1.21.
Change-Id: Ia0f645069c778741ebb8c359161816a303a2175b
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/552675
TryBot-Result: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
index 2654f0b..adbf91b 100644
--- a/cmd/worker/main.go
+++ b/cmd/worker/main.go
@@ -20,7 +20,6 @@
"text/tabwriter"
"time"
- "golang.org/x/exp/event"
"golang.org/x/vulndb/internal/cvelistrepo"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/gitrepo"
@@ -88,8 +87,8 @@
dieWithUsage("%v", err)
}
- ctx := event.WithExporter(context.Background(),
- event.NewExporter(log.NewLineHandler(os.Stderr), nil))
+ ctx := context.Background()
+
if img := os.Getenv("DOCKER_IMAGE"); img != "" {
log.Infof(ctx, "running in docker image %s", img)
}
diff --git a/go.mod b/go.mod
index 6f2b609..d774f3e 100644
--- a/go.mod
+++ b/go.mod
@@ -25,8 +25,7 @@
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
- golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
- golang.org/x/exp/event v0.0.0-20231219180239-dc181d75b848
+ golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
golang.org/x/mod v0.14.0
golang.org/x/oauth2 v0.15.0
golang.org/x/sync v0.5.0
diff --git a/go.sum b/go.sum
index bb34321..f7c0fb1 100644
--- a/go.sum
+++ b/go.sum
@@ -217,8 +217,8 @@
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
-golang.org/x/exp/event v0.0.0-20231219180239-dc181d75b848 h1:Fg3nHyeMRa3zOGwrxMfgAYbhOJ6tr/inMof9Eaq4Fgk=
-golang.org/x/exp/event v0.0.0-20231219180239-dc181d75b848/go.mod h1:R4Nj8+ac1oNAVEp6jJGgYrsz0uySlWGocGQ5A74W3DY=
+golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
+golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
diff --git a/internal/observe/observe.go b/internal/observe/observe.go
index 19df286..3fa3fe2 100644
--- a/internal/observe/observe.go
+++ b/internal/observe/observe.go
@@ -8,11 +8,12 @@
import (
"context"
+ "golang.org/x/exp/slog"
"net/http"
"strings"
- "golang.org/x/exp/event"
"golang.org/x/vulndb/internal/derrors"
+ "golang.org/x/vulndb/internal/worker/log"
mexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric"
texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
@@ -25,17 +26,13 @@
tnoop "go.opentelemetry.io/otel/trace/noop"
)
-// An Observer handles tracing and metrics exporting.
+// An Observer handles tracing, logging and metrics exporting.
type Observer struct {
ctx context.Context
tracerProvider *sdktrace.TracerProvider
tracer trace.Tracer
propagator propagation.TextMapPropagator
-
- // LogHandlerFunc is invoked in [Observer.Observe] to obtain an
- // [event.Handler] for logging to be added to the [event.Exporter] in
- // addition to the tracing and metrics handlers.
- LogHandlerFunc func(*http.Request) event.Handler
+ baseLogger *slog.Logger
}
// NewObserver creates an Observer.
@@ -77,20 +74,21 @@
gcppropagator.CloudTraceOneWayPropagator{},
propagation.TraceContext{},
propagation.Baggage{}),
+ baseLogger: slog.New(log.NewGoogleCloudHandler(slog.LevelDebug)),
}, nil
}
type key struct{}
+const (
+ traceIDHeader = "X-Cloud-Trace-Context"
+ gcpTraceKey = "logging.googleapis.com/trace"
+)
+
// Observe adds metrics and tracing to an http.Handler.
func (o *Observer) Observe(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var otherHandler event.Handler
- if o.LogHandlerFunc != nil {
- otherHandler = o.LogHandlerFunc(r)
- }
- exporter := event.NewExporter(eventHandler{o, otherHandler}, nil)
- ctx := event.WithExporter(r.Context(), exporter)
+ ctx := log.NewContext(r.Context(), o.baseLogger.With(gcpTraceKey, r.Header.Get(traceIDHeader)))
ctx = o.propagator.Extract(ctx, propagation.HeaderCarrier(r.Header))
ctx = context.WithValue(ctx, key{}, o)
defer o.tracerProvider.ForceFlush(o.ctx)
@@ -98,19 +96,6 @@
})
}
-type eventHandler struct {
- o *Observer
- eh event.Handler
-}
-
-// Event implements event.Handler.
-func (h eventHandler) Event(ctx context.Context, ev *event.Event) context.Context {
- if h.eh != nil {
- return h.eh.Event(ctx, ev)
- }
- return ctx
-}
-
func Start(ctx context.Context, name string) (context.Context, trace.Span) {
if obs, ok := ctx.Value(key{}).(*Observer); ok {
return obs.tracer.Start(ctx, name)
diff --git a/internal/worker/log/gcpjson.go b/internal/worker/log/gcpjson.go
deleted file mode 100644
index da45595..0000000
--- a/internal/worker/log/gcpjson.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2021 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package log
-
-import (
- "context"
- "fmt"
- "io"
- "sync"
- "time"
-
- "golang.org/x/exp/event"
-)
-
-// NewGCPJSONLogger returns a handler which logs events in a format that is
-// understood by Google Cloud Platform logging.
-func NewGCPJSONHandler(w io.Writer, traceID string) event.Handler {
- return &gcpJSONHandler{w: w, traceID: traceID}
-}
-
-type gcpJSONHandler struct {
- traceID string
- mu sync.Mutex // ensure a log line is not interrupted
- w io.Writer
-}
-
-// Event implements event.Handler.Event.
-// It handles Log events and ignores all others.
-// See https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
-// for treatment of special fields.
-func (h *gcpJSONHandler) Event(ctx context.Context, ev *event.Event) context.Context {
- if ev.Kind != event.LogKind {
- return ctx
- }
- h.mu.Lock()
- defer h.mu.Unlock()
-
- fmt.Fprintf(h.w, `{"time": %q`, ev.At.Format(time.RFC3339))
- if h.traceID != "" {
- fmt.Fprintf(h.w, `, "logging.googleapis.com/trace": %q`, h.traceID)
- }
- gcpLabels := map[string]string{}
- for _, l := range ev.Labels {
- var key string
- switch l.Name {
- case "msg":
- key = "message"
- case "level":
- key = "severity"
- default:
- gcpLabels[l.Name] = l.String() // already quoted, regardless of label kind
- continue
- }
- fmt.Fprintf(h.w, ", %q: ", key)
- switch {
- case !l.HasValue():
- fmt.Fprint(h.w, "null")
- case l.IsInt64():
- fmt.Fprintf(h.w, "%d", l.Int64())
- case l.IsUint64():
- fmt.Fprintf(h.w, "%d", l.Uint64())
- case l.IsFloat64():
- fmt.Fprintf(h.w, "%g", l.Float64())
- case l.IsBool():
- fmt.Fprintf(h.w, "%t", l.Bool())
- default:
- fmt.Fprintf(h.w, "%q", l.String())
- }
- }
- if len(gcpLabels) > 0 {
- fmt.Fprintf(h.w, `, "logging.googleapis.com/labels": {`)
- first := true
- for k, v := range gcpLabels {
- if !first {
- fmt.Fprint(h.w, ", ")
- }
- first = false
- fmt.Fprintf(h.w, "%q: %q", k, v)
- }
- fmt.Fprint(h.w, "}")
- }
- fmt.Fprint(h.w, "}\n")
- return ctx
-}
diff --git a/internal/worker/log/gcpjson_test.go b/internal/worker/log/gcpjson_test.go
deleted file mode 100644
index 2ed5409..0000000
--- a/internal/worker/log/gcpjson_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2021 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package log
-
-import (
- "bytes"
- "context"
- "testing"
- "time"
-
- "golang.org/x/exp/event"
- "golang.org/x/exp/event/severity"
-)
-
-func TestGCPJSON(t *testing.T) {
- now := time.Date(2002, 3, 4, 5, 6, 7, 0, time.UTC)
- for _, test := range []struct {
- ev event.Event
- want string
- }{
- {
- ev: event.Event{
- At: now,
- Kind: event.LogKind,
- Labels: []event.Label{
- event.String("msg", "hello"),
- event.Int64("count", 17),
- severity.Info.Label(),
- },
- },
- want: `{"time": "2002-03-04T05:06:07Z", "logging.googleapis.com/trace": "tid", "message": "hello", "severity": "info", "logging.googleapis.com/labels": {"count": "17"}}
-`,
- },
- } {
- var buf bytes.Buffer
- h := &gcpJSONHandler{w: &buf, traceID: "tid"}
- h.Event(context.Background(), &test.ev)
- got := buf.String()
- if got != test.want {
- t.Errorf("%+v:\ngot %s\nwant %s", test.ev, got, test.want)
- }
- }
-}
diff --git a/internal/worker/log/log.go b/internal/worker/log/log.go
index 2e98142..41e6b01 100644
--- a/internal/worker/log/log.go
+++ b/internal/worker/log/log.go
@@ -8,129 +8,101 @@
import (
"context"
"fmt"
+ "golang.org/x/exp/slog"
"io"
- "reflect"
- "strings"
- "sync"
+ "os"
"time"
-
- "golang.org/x/exp/event"
- "golang.org/x/exp/event/severity"
)
-// NewLineHandler returns an event Handler that writes log events one per line
-// in an easy-to-read format:
-//
-// time level message label1=value1 label2=value2 ...
-func NewLineHandler(w io.Writer) event.Handler {
- return &lineHandler{w: w}
+type Attrs []slog.Attr
+
+func With(args ...any) Attrs {
+ var r slog.Record
+ r.Add(args...)
+ var as Attrs
+ r.Attrs(func(a slog.Attr) bool {
+ as = append(as, a)
+ return true
+ })
+ return as
}
-type lineHandler struct {
- mu sync.Mutex // ensure a log line is not interrupted
- w io.Writer
+func (as Attrs) Debugf(ctx context.Context, format string, args ...interface{}) {
+ as.logf(ctx, slog.LevelDebug, format, args...)
}
-// Event implements event.Handler.Event for log events.
-func (h *lineHandler) Event(ctx context.Context, ev *event.Event) context.Context {
- if ev.Kind != event.LogKind {
- return ctx
- }
- h.mu.Lock()
- defer h.mu.Unlock()
-
- var msg, level string
- var others []string
- for _, lab := range ev.Labels {
- switch lab.Name {
- case "msg":
- msg = lab.String()
- case "level":
- level = strings.ToUpper(lab.String())
- default:
- others = append(others, fmt.Sprintf("%s=%s", lab.Name, lab.String()))
- }
- }
- var s string
- if len(others) > 0 {
- s = " " + strings.Join(others, " ")
- }
- if level != "" {
- level = " " + level
- }
- fmt.Fprintf(h.w, "%s%s %s%s\n", ev.At.Format("2006/01/02 15:04:05"), level, msg, s)
- return ctx
+func (as Attrs) Infof(ctx context.Context, format string, args ...interface{}) {
+ as.logf(ctx, slog.LevelInfo, format, args...)
}
-type Labels []event.Label
-
-func With(kvs ...interface{}) Labels {
- return Labels(nil).With(kvs...)
+func (as Attrs) Warningf(ctx context.Context, format string, args ...interface{}) {
+ as.logf(ctx, slog.LevelWarn, format, args...)
}
-func (ls Labels) With(kvs ...interface{}) Labels {
- if len(kvs)%2 != 0 {
- panic("args must be key-value pairs")
- }
- for i := 0; i < len(kvs); i += 2 {
- ls = append(ls, pairToLabel(kvs[i].(string), kvs[i+1]))
- }
- return ls
-}
-
-func pairToLabel(name string, value interface{}) event.Label {
- if d, ok := value.(time.Duration); ok {
- return event.Duration(name, d)
- }
- v := reflect.ValueOf(value)
- switch v.Kind() {
- case reflect.String:
- return event.String(name, v.String())
- case reflect.Bool:
- return event.Bool(name, v.Bool())
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return event.Int64(name, v.Int())
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- return event.Uint64(name, v.Uint())
- case reflect.Float32, reflect.Float64:
- return event.Float64(name, v.Float())
- default:
- return event.Value(name, value)
- }
-}
-
-func (l Labels) logf(ctx context.Context, s severity.Level, format string, args ...interface{}) {
- event.Log(ctx, fmt.Sprintf(format, args...), append(l, s.Label())...)
-}
-
-func (l Labels) Debugf(ctx context.Context, format string, args ...interface{}) {
- l.logf(ctx, severity.Debug, format, args...)
-}
-
-func (l Labels) Infof(ctx context.Context, format string, args ...interface{}) {
- l.logf(ctx, severity.Info, format, args...)
-}
-
-func (l Labels) Warningf(ctx context.Context, format string, args ...interface{}) {
- l.logf(ctx, severity.Warning, format, args...)
-}
-
-func (l Labels) Errorf(ctx context.Context, format string, args ...interface{}) {
- l.logf(ctx, severity.Error, format, args...)
+func (as Attrs) Errorf(ctx context.Context, format string, args ...interface{}) {
+ as.logf(ctx, slog.LevelError, format, args...)
}
func Debugf(ctx context.Context, format string, args ...interface{}) {
- Labels(nil).logf(ctx, severity.Debug, format, args...)
+ Attrs{}.logf(ctx, slog.LevelDebug, format, args...)
}
func Infof(ctx context.Context, format string, args ...interface{}) {
- Labels(nil).logf(ctx, severity.Info, format, args...)
+ Attrs{}.logf(ctx, slog.LevelInfo, format, args...)
}
func Warningf(ctx context.Context, format string, args ...interface{}) {
- Labels(nil).logf(ctx, severity.Warning, format, args...)
+ Attrs{}.logf(ctx, slog.LevelWarn, format, args...)
}
func Errorf(ctx context.Context, format string, args ...interface{}) {
- Labels(nil).logf(ctx, severity.Error, format, args...)
+ Attrs{}.logf(ctx, slog.LevelError, format, args...)
+}
+
+func (as Attrs) logf(ctx context.Context, level slog.Level, format string, args ...interface{}) {
+ FromContext(ctx).LogAttrs(ctx, level, fmt.Sprintf(format, args...), as...)
+}
+
+type key struct{}
+
+func FromContext(ctx context.Context) *slog.Logger {
+ if l, ok := ctx.Value(key{}).(*slog.Logger); ok {
+ return l
+ }
+ return slog.Default()
+}
+
+func NewContext(ctx context.Context, l *slog.Logger) context.Context {
+ return context.WithValue(ctx, key{}, l)
+}
+
+// NewGoogleCloudHandler returns a Handler that outputs JSON for the Google
+// Cloud logging service.
+// See https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
+// for treatment of special fields.
+func NewGoogleCloudHandler(level slog.Leveler) slog.Handler {
+ return newGoogleCloudHandler(level, os.Stderr)
+}
+
+func newGoogleCloudHandler(level slog.Leveler, w io.Writer) slog.Handler {
+ return slog.NewJSONHandler(w, &slog.HandlerOptions{
+ Level: level,
+ ReplaceAttr: gcpReplaceAttr,
+ })
+}
+
+func gcpReplaceAttr(groups []string, a slog.Attr) slog.Attr {
+ switch a.Key {
+ case "time":
+ if a.Value.Kind() == slog.KindTime {
+ a.Value = slog.StringValue(a.Value.Time().Format(time.RFC3339))
+ }
+ case "msg":
+ a.Key = "message"
+ case "level":
+ a.Key = "severity"
+ case "traceID":
+ a.Key = "logging.googleapis.com/trace"
+ }
+ return a
}
diff --git a/internal/worker/log/log_test.go b/internal/worker/log/log_test.go
new file mode 100644
index 0000000..c97c253
--- /dev/null
+++ b/internal/worker/log/log_test.go
@@ -0,0 +1,42 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package log
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "golang.org/x/exp/slog"
+ "testing"
+ "time"
+)
+
+// TODO(jba): is it important to put additional attrs under "logging.googleapis.com/labels"?
+
+func TestGoogleCloudHandler(t *testing.T) {
+ var buf bytes.Buffer
+ l := slog.New(newGoogleCloudHandler(slog.LevelInfo, &buf))
+ l = l.With("logging.googleapis.com/trace", "tid")
+ now := time.Now()
+ l.Info("hello", slog.String("foo", "bar"), slog.Int("count", 17))
+ got := buf.String()
+ want := fmt.Sprintf(`{"time":%q,"severity":"INFO","message":"hello","logging.googleapis.com/trace":"tid","foo":"bar","count":17}
+`, now.Format(time.RFC3339))
+ if got != want {
+ t.Errorf("\ngot %s\nwant %s", got, want)
+ }
+
+ buf.Reset()
+ ctx := NewContext(context.Background(), l)
+ now = time.Now()
+ With("a", "b").Warningf(ctx, "hi")
+ got = buf.String()
+ want = fmt.Sprintf(`{"time":%q,"severity":"WARN","message":"hi","logging.googleapis.com/trace":"tid","a":"b"}
+`, now.Format(time.RFC3339))
+ if got != want {
+ t.Errorf("\ngot %s\nwant %s", got, want)
+ }
+
+}
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 6aa2794..ca62ee0 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -22,7 +22,6 @@
"cloud.google.com/go/errorreporting"
"github.com/google/safehtml/template"
"github.com/jba/metrics"
- "golang.org/x/exp/event"
"golang.org/x/sync/errgroup"
"golang.org/x/vulndb/internal/cvelistrepo"
"golang.org/x/vulndb/internal/derrors"
@@ -52,8 +51,6 @@
observer *observe.Observer
}
-const traceIDHeader = "X-Cloud-Trace-Context"
-
func NewServer(ctx context.Context, cfg Config) (_ *Server, err error) {
defer derrors.Wrap(&err, "NewServer(%q)", cfg.Namespace)
@@ -66,11 +63,6 @@
// This function will be called for each request.
// It lets us install a log handler that knows about the request's
// trace ID.
- s.observer.LogHandlerFunc = func(r *http.Request) event.Handler {
- traceID := r.Header.Get(traceIDHeader)
- return log.NewGCPJSONHandler(os.Stderr, traceID)
- }
-
if cfg.UseErrorReporting {
reportingClient, err := errorreporting.NewClient(ctx, cfg.Project, errorreporting.Config{
ServiceName: serviceID,
diff --git a/internal/worker/worker_test.go b/internal/worker/worker_test.go
index 249ec56..388069c 100644
--- a/internal/worker/worker_test.go
+++ b/internal/worker/worker_test.go
@@ -13,7 +13,6 @@
"fmt"
"math"
"net/http"
- "os"
"sort"
"strings"
"testing"
@@ -21,7 +20,6 @@
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
- "golang.org/x/exp/event"
"golang.org/x/vulndb/internal/cveschema"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/gitrepo"
@@ -29,7 +27,6 @@
"golang.org/x/vulndb/internal/issues/githubtest"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
- "golang.org/x/vulndb/internal/worker/log"
"golang.org/x/vulndb/internal/worker/store"
)
@@ -97,8 +94,7 @@
}
func TestCreateIssues(t *testing.T) {
- ctx := event.WithExporter(context.Background(),
- event.NewExporter(log.NewLineHandler(os.Stderr), nil))
+ ctx := context.Background()
mstore := store.NewMemStore()
ic, mux := githubtest.Setup(ctx, t, &issues.Config{
@@ -391,8 +387,7 @@
}
func TestUpdateGHSAs(t *testing.T) {
- ctx := event.WithExporter(context.Background(),
- event.NewExporter(log.NewLineHandler(os.Stderr), nil))
+ ctx := context.Background()
sas := []*ghsa.SecurityAdvisory{
{
ID: "g1",