internal/log: add package
Package log implements event handlers for logging.
Change-Id: I271c4381f2bf159b4bea22c37b61e765f54327f1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/463622
Reviewed-by: Julie Qiu <julieqiu@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Julie Qiu <julieqiu@google.com>
diff --git a/go.mod b/go.mod
index 2183769..f97b124 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,11 @@
require (
cloud.google.com/go/errorreporting v0.3.0
+ cloud.google.com/go/logging v1.6.1
github.com/client9/misspell v0.3.4
github.com/google/go-cmp v0.5.9
- go.opencensus.io v0.23.0
+ go.opencensus.io v0.24.0
+ golang.org/x/exp/event v0.0.0-20230125214544-b3c2aaf6208d
golang.org/x/mod v0.7.0
golang.org/x/net v0.5.0
golang.org/x/tools v0.5.0
@@ -15,8 +17,10 @@
)
require (
+ cloud.google.com/go v0.105.0 // indirect
cloud.google.com/go/compute v1.12.1 // indirect
cloud.google.com/go/compute/metadata v0.2.1 // indirect
+ cloud.google.com/go/longrunning v0.3.0 // indirect
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
@@ -27,9 +31,9 @@
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
- google.golang.org/api v0.102.0 // indirect
+ google.golang.org/api v0.103.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
+ google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
diff --git a/go.sum b/go.sum
index 01fb6ec..fab6761 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,16 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
+cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0=
cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=
+cloud.google.com/go/logging v1.6.1 h1:ZBsZK+JG+oCDT+vaxwqF2egKNRjz8soXiS6Xv79benI=
+cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw=
+cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
+cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@@ -14,6 +19,7 @@
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -54,12 +60,18 @@
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp/event v0.0.0-20230125214544-b3c2aaf6208d h1:uD2g//YGsKF7KHtDmYBg3pjgvFOIzUVfBZmKRMtgdd4=
+golang.org/x/exp/event v0.0.0-20230125214544-b3c2aaf6208d/go.mod h1:AVlZHjhWbW/3yOcmKMtJiObwBPJajBlUpQXRijFNrNc=
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM=
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -103,8 +115,8 @@
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I=
-google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
+google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
+google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
@@ -112,8 +124,8 @@
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e h1:S9GbmC1iCgvbLyAokVCwiO6tVIrU9Y7c5oMx1V/ki/Y=
-google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c h1:S34D59DS2GWOEwWNt4fYmTcFrtlOgukG2k9WsomZ7tg=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -136,6 +148,7 @@
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA=
diff --git a/internal/log/gcpjson.go b/internal/log/gcpjson.go
new file mode 100644
index 0000000..1deaba6
--- /dev/null
+++ b/internal/log/gcpjson.go
@@ -0,0 +1,86 @@
+// 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
new file mode 100644
index 0000000..2dc7b36
--- /dev/null
+++ b/internal/log/gcpjson_test.go
@@ -0,0 +1,45 @@
+// 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)
+ }
+ }
+}
diff --git a/internal/log/log.go b/internal/log/log.go
new file mode 100644
index 0000000..7976bcf
--- /dev/null
+++ b/internal/log/log.go
@@ -0,0 +1,282 @@
+// 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 implements event handlers for logging.
+package log
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "cloud.google.com/go/logging"
+ "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 lineHandler struct {
+ mu sync.Mutex // ensure a log line is not interrupted
+ w io.Writer
+}
+
+// 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
+}
+
+type Labels []event.Label
+
+func With(kvs ...interface{}) Labels {
+ return Labels(nil).With(kvs...)
+}
+
+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...)
+}
+
+var (
+ mu sync.Mutex
+ logger interface {
+ log(context.Context, logging.Severity, interface{})
+ } = stdlibLogger{}
+
+ // currentLevel holds current log level.
+ // No logs will be printed below currentLevel.
+ currentLevel = logging.Default
+)
+
+type (
+ // traceIDKey is the type of the context key for trace IDs.
+ traceIDKey struct{}
+
+ // labelsKey is the type of the context key for labels.
+ labelsKey struct{}
+)
+
+// Set the log level
+func SetLevel(v string) {
+ mu.Lock()
+ defer mu.Unlock()
+ currentLevel = toLevel(v)
+}
+
+func getLevel() logging.Severity {
+ mu.Lock()
+ defer mu.Unlock()
+ return currentLevel
+}
+
+// NewContextWithTraceID creates a new context from ctx that adds the trace ID.
+func NewContextWithTraceID(ctx context.Context, traceID string) context.Context {
+ return context.WithValue(ctx, traceIDKey{}, traceID)
+}
+
+// NewContextWithLabel creates a new context from ctx that adds a label that will
+// appear in the log entry.
+func NewContextWithLabel(ctx context.Context, key, value string) context.Context {
+ oldLabels, _ := ctx.Value(labelsKey{}).(map[string]string)
+ // Copy the labels, to preserve immutability of contexts.
+ newLabels := map[string]string{}
+ for k, v := range oldLabels {
+ newLabels[k] = v
+ }
+ newLabels[key] = value
+ return context.WithValue(ctx, labelsKey{}, newLabels)
+}
+
+// stdlibLogger uses the Go standard library logger.
+type stdlibLogger struct{}
+
+func (stdlibLogger) log(ctx context.Context, s logging.Severity, payload interface{}) {
+ var extras []string
+ traceID, _ := ctx.Value(traceIDKey{}).(string) // if not present, traceID is ""
+ if traceID != "" {
+ extras = append(extras, fmt.Sprintf("traceID %s", traceID))
+ }
+ if labels, ok := ctx.Value(labelsKey{}).(map[string]string); ok {
+ extras = append(extras, fmt.Sprint(labels))
+ }
+ var extra string
+ if len(extras) > 0 {
+ extra = " (" + strings.Join(extras, ", ") + ")"
+ }
+ log.Printf("%s%s: %+v", s, extra, payload)
+
+}
+
+// Infof logs a formatted string at the Info level.
+func Infof(ctx context.Context, format string, args ...interface{}) {
+ logf(ctx, logging.Info, format, args)
+}
+
+// Warningf logs a formatted string at the Warning level.
+func Warningf(ctx context.Context, format string, args ...interface{}) {
+ logf(ctx, logging.Warning, format, args)
+}
+
+// Errorf logs a formatted string at the Error level.
+func Errorf(ctx context.Context, format string, args ...interface{}) {
+ logf(ctx, logging.Error, format, args)
+}
+
+// Debugf logs a formatted string at the Debug level.
+func Debugf(ctx context.Context, format string, args ...interface{}) {
+ logf(ctx, logging.Debug, format, args)
+}
+
+// Fatalf logs formatted string at the Critical level followed by exiting the program.
+func Fatalf(ctx context.Context, format string, args ...interface{}) {
+ logf(ctx, logging.Critical, format, args)
+ die()
+}
+
+func logf(ctx context.Context, s logging.Severity, format string, args []interface{}) {
+ doLog(ctx, s, fmt.Sprintf(format, args...))
+}
+
+// Info logs arg, which can be a string or a struct, at the Info level.
+func Info(ctx context.Context, arg interface{}) { doLog(ctx, logging.Info, arg) }
+
+// Warning logs arg, which can be a string or a struct, at the Warning level.
+func Warning(ctx context.Context, arg interface{}) { doLog(ctx, logging.Warning, arg) }
+
+// Error logs arg, which can be a string or a struct, at the Error level.
+func Error(ctx context.Context, arg interface{}) { doLog(ctx, logging.Error, arg) }
+
+// Debug logs arg, which can be a string or a struct, at the Debug level.
+func Debug(ctx context.Context, arg interface{}) { doLog(ctx, logging.Debug, arg) }
+
+// Fatal logs arg, which can be a string or a struct, at the Critical level followed by exiting the program.
+func Fatal(ctx context.Context, arg interface{}) {
+ doLog(ctx, logging.Critical, arg)
+ die()
+}
+
+func doLog(ctx context.Context, s logging.Severity, payload interface{}) {
+ if getLevel() > s {
+ return
+ }
+ mu.Lock()
+ l := logger
+ mu.Unlock()
+ l.log(ctx, s, payload)
+}
+
+func die() {
+ os.Exit(1)
+}
+
+// toLevel returns the logging.Severity for a given string.
+// Possible input values are "", "debug", "info", "warning", "error", "fatal".
+// In case of invalid string input, it maps to DefaultLevel.
+func toLevel(v string) logging.Severity {
+ v = strings.ToLower(v)
+
+ switch v {
+ case "":
+ // default log level will print everything.
+ return logging.Default
+ case "debug":
+ return logging.Debug
+ case "info":
+ return logging.Info
+ case "warning":
+ return logging.Warning
+ case "error":
+ return logging.Error
+ case "fatal":
+ return logging.Critical
+ }
+
+ // Default log level in case of invalid input.
+ log.Printf("Error: %s is invalid LogLevel. Possible values are [debug, info, warning, error, fatal]", v)
+ return logging.Default
+}