internal/log: write GCP-compatible logs on Cloud Run
When the server detects that it is on Cloud Run, it
outputs logs in a JSON format that will produce
properly formatted log entries.
Change-Id: Ifed944ad5916f372cac74d7e17b68f2fa32b3d04
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/472276
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
index 005f580..0597b1c 100644
--- a/cmd/worker/main.go
+++ b/cmd/worker/main.go
@@ -39,7 +39,13 @@
flag.Parse()
ctx := context.Background()
- slog.SetDefault(slog.New(log.NewLineHandler(os.Stderr)))
+ var h slog.Handler
+ if config.OnCloudRun() || *devMode {
+ h = log.NewGoogleCloudHandler()
+ } else {
+ h = log.NewLineHandler(os.Stderr)
+ }
+ slog.SetDefault(slog.New(h))
if err := runServer(ctx); err != nil {
log.Error(ctx, "fail", err)
os.Exit(1)
diff --git a/internal/log/cloud_handler.go b/internal/log/cloud_handler.go
new file mode 100644
index 0000000..96ab5f3
--- /dev/null
+++ b/internal/log/cloud_handler.go
@@ -0,0 +1,37 @@
+// 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 (
+ "os"
+ "time"
+
+ "golang.org/x/exp/slog"
+)
+
+// 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() slog.Handler {
+ return slog.HandlerOptions{ReplaceAttr: gcpReplaceAttr}.
+ NewJSONHandler(os.Stderr)
+}
+
+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/log/gcpjson.go b/internal/log/gcpjson.go
deleted file mode 100644
index 1deaba6..0000000
--- a/internal/log/gcpjson.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// 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 (
- "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/log/gcpjson_test.go b/internal/log/gcpjson_test.go
deleted file mode 100644
index 2dc7b36..0000000
--- a/internal/log/gcpjson_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// 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"
- "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)
- }
- }
-}