Skip to content

Commit

Permalink
support trace information replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
ktong committed Feb 25, 2024
1 parent df0b7bc commit 7fb4e0b
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 112 deletions.
105 changes: 52 additions & 53 deletions gcp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func New(opts ...Option) slog.Handler {
&slog.HandlerOptions{
AddSource: true,
Level: option.level,
ReplaceAttr: replaceAttr,
ReplaceAttr: replaceAttr(),
},
)
if option.project != "" || option.service != "" {
Expand All @@ -71,61 +71,63 @@ func New(opts ...Option) slog.Handler {
return handler
}

func replaceAttr(groups []string, attr slog.Attr) slog.Attr { //nolint:cyclop
if len(groups) > 0 {
return attr
}
func replaceAttr() func(groups []string, attr slog.Attr) slog.Attr { //nolint:cyclop
return func(groups []string, attr slog.Attr) slog.Attr {
if len(groups) > 0 {
return attr
}

// Replace attributes to match GCP Cloud Logging format.
//
// See: https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
switch attr.Key {
// Maps the slog levels to the correct [severity] for GCP Cloud Logging.
//
// See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
case slog.LevelKey:
var severity string
if level, ok := attr.Value.Resolve().Any().(slog.Level); ok {
switch {
case level >= slog.LevelError:
severity = "ERROR"
case level >= slog.LevelWarn:
severity = "WARNING"
case level >= slog.LevelInfo:
severity = "INFO"
default:
severity = "DEBUG"
// Replace attributes to match GCP Cloud Logging format.
//
// See: https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
switch attr.Key {
// Maps the slog levels to the correct [severity] for GCP Cloud Logging.
//
// See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
case slog.LevelKey:
var severity string
if level, ok := attr.Value.Resolve().Any().(slog.Level); ok {
switch {
case level >= slog.LevelError:
severity = "ERROR"
case level >= slog.LevelWarn:
severity = "WARNING"
case level >= slog.LevelInfo:
severity = "INFO"
default:
severity = "DEBUG"
}
}
}

return slog.String("severity", severity)
return slog.String("severity", severity)

// Format event timestamp according to GCP JSON formats.
//
// See: https://cloud.google.com/logging/docs/agent/logging/configuration#timestamp-processing
case slog.TimeKey:
time := attr.Value.Resolve().Time()

return slog.Attr{
Key: "timestamp",
Value: slog.GroupValue(
slog.Int64("seconds", time.Unix()),
slog.Int64("nanos", int64(time.Nanosecond())),
),
}
// Format event timestamp according to GCP JSON formats.
//
// See: https://cloud.google.com/logging/docs/agent/logging/configuration#timestamp-processing
case slog.TimeKey:
time := attr.Value.Resolve().Time()

case slog.SourceKey:
attr.Key = "logging.googleapis.com/sourceLocation"
return slog.Attr{
Key: "timestamp",
Value: slog.GroupValue(
slog.Int64("seconds", time.Unix()),
slog.Int64("nanos", int64(time.Nanosecond())),
),
}

return attr
case slog.SourceKey:
attr.Key = "logging.googleapis.com/sourceLocation"

case slog.MessageKey:
attr.Key = "message"
return attr

return attr
case slog.MessageKey:
attr.Key = "message"

default:
return attr
return attr

default:
return attr
}
}
}

Expand All @@ -135,7 +137,7 @@ type (
groups []group

project string
contextProvider func(context.Context) TraceContext
contextProvider func(context.Context) (traceID [16]byte, spanID [8]byte, traceFlags byte)

service string
version string
Expand All @@ -157,13 +159,10 @@ func (h logHandler) Handle(ctx context.Context, record slog.Record) error { //no
// Associate logs with a trace and span.
//
// See: https://cloud.google.com/trace/docs/trace-log-integration
if h.project != "" {
if h.project != "" && h.contextProvider != nil {
const sampled = 0x1

if traceContext := h.contextProvider(ctx); traceContext.TraceID() != [16]byte{} {
traceID := traceContext.TraceID()
spanID := traceContext.SpanID()
traceFlags := traceContext.TraceFlags()
if traceID, spanID, traceFlags := h.contextProvider(ctx); traceID != [16]byte{} {
handler = handler.WithAttrs([]slog.Attr{
slog.String("logging.googleapis.com/trace", "projects/"+h.project+"/traces/"+hex.EncodeToString(traceID[:])),
slog.String("logging.googleapis.com/spanId", hex.EncodeToString(spanID[:])),
Expand Down
60 changes: 23 additions & 37 deletions gcp/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package gcp_test
import (
"bytes"
"context"
"encoding/hex"
"errors"
"log/slog"
"os"
Expand Down Expand Up @@ -77,18 +76,18 @@ func testCases() []struct {
}{
{
description: "default",
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":37},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":42},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":50},"message":"error","g":{"h":{"b":"B"}}}
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":36},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":41},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":49},"message":"error","g":{"h":{"b":"B"}}}
`,
},
{
description: "with level",
opts: []gcp.Option{
gcp.WithLevel(slog.LevelWarn),
},
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":42},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":50},"message":"error","g":{"h":{"b":"B"}}}
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":41},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":49},"message":"error","g":{"h":{"b":"B"}}}
`,
},
{
Expand All @@ -97,9 +96,9 @@ func testCases() []struct {
gcp.WithErrorReporting("test", "dev"),
},
err: errors.New("an error"),
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":37},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":42},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":50},"message":"error","context":{"reportLocation":{"filePath":"/handler_test.go","lineNumber":50,"functionName":"github.com/nil-go/sloth/gcp_test.TestHandler.func1"}},"serviceContext":{"service":"test","version":"dev"},"stack_trace":"error\n\n\ngithub.com/nil-go/sloth/gcp_test.TestHandler.func1()\n\t/handler_test.go:50"g":{"h":{"b":"B","error":"an error"}}}
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":36},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":41},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":49},"message":"error","context":{"reportLocation":{"filePath":"/handler_test.go","lineNumber":49,"functionName":"github.com/nil-go/sloth/gcp_test.TestHandler.func1"}},"serviceContext":{"service":"test","version":"dev"},"stack_trace":"error\n\n\ngithub.com/nil-go/sloth/gcp_test.TestHandler.func1()\n\t/handler_test.go:49"g":{"h":{"b":"B","error":"an error"}}}
`,
},
{
Expand All @@ -114,9 +113,9 @@ func testCases() []struct {
}),
},
err: errors.New("an error"),
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":37},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":42},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":50},"message":"error","context":{"reportLocation":{"filePath":"/handler_test.go","lineNumber":50,"functionName":"github.com/nil-go/sloth/gcp_test.TestHandler.func1"}},"serviceContext":{"service":"test","version":"dev"},"stack_trace":"error\n\n\ngithub.com/nil-go/sloth/gcp_test.testCases.func1()\n\t/handler_test.go:111"g":{"h":{"b":"B","error":"an error"}}}
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":36},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":41},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":49},"message":"error","context":{"reportLocation":{"filePath":"/handler_test.go","lineNumber":49,"functionName":"github.com/nil-go/sloth/gcp_test.TestHandler.func1"}},"serviceContext":{"service":"test","version":"dev"},"stack_trace":"error\n\n\ngithub.com/nil-go/sloth/gcp_test.testCases.func1()\n\t/handler_test.go:110"g":{"h":{"b":"B","error":"an error"}}}
`,
},
{
Expand All @@ -125,19 +124,24 @@ func testCases() []struct {
gcp.WithErrorReporting("test", "dev"),
},
err: stackError{errors.New("an error")},
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":37},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":42},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":50},"message":"error","context":{"reportLocation":{"filePath":"/handler_test.go","lineNumber":50,"functionName":"github.com/nil-go/sloth/gcp_test.TestHandler.func1"}},"serviceContext":{"service":"test","version":"dev"},"stack_trace":"error\n\n\ngithub.com/nil-go/sloth/gcp_test.stackError.Callers()\n\t/handler_test.go:152"g":{"h":{"b":"B","error":"an error"}}}
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":36},"message":"info","a":"A"}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":41},"message":"warn","g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":49},"message":"error","context":{"reportLocation":{"filePath":"/handler_test.go","lineNumber":49,"functionName":"github.com/nil-go/sloth/gcp_test.TestHandler.func1"}},"serviceContext":{"service":"test","version":"dev"},"stack_trace":"error\n\n\ngithub.com/nil-go/sloth/gcp_test.stackError.Callers()\n\t/handler_test.go:156"g":{"h":{"b":"B","error":"an error"}}}
`,
},
{
description: "with trace",
opts: []gcp.Option{
gcp.WithTrace("test", func(context.Context) gcp.TraceContext { return traceContext{} }),
gcp.WithTrace("test"),
gcp.WithTraceContext(func(context.Context) (traceID [16]byte, spanID [8]byte, traceFlags byte) {
return [16]byte{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54},
[8]byte{0, 240, 103, 170, 11, 169, 2, 183},
1
}),
},
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":37},"message":"info","a":"A","logging.googleapis.com/trace":"projects/test/traces/4bf92f3577b34da6a3ce929d0e0e4736","logging.googleapis.com/spanId":"00f067aa0ba902b7","logging.googleapis.com/trace_sampled":true}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":42},"message":"warn","logging.googleapis.com/trace":"projects/test/traces/4bf92f3577b34da6a3ce929d0e0e4736","logging.googleapis.com/spanId":"00f067aa0ba902b7","logging.googleapis.com/trace_sampled":true,"g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":50},"message":"error","logging.googleapis.com/trace":"projects/test/traces/4bf92f3577b34da6a3ce929d0e0e4736","logging.googleapis.com/spanId":"00f067aa0ba902b7","logging.googleapis.com/trace_sampled":true,"g":{"h":{"b":"B"}}}
expected: `{"timestamp":{"seconds":100,"nanos":1000},"severity":"INFO","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":36},"message":"info","a":"A","logging.googleapis.com/trace":"projects/test/traces/4bf92f3577b34da6a3ce929d0e0e4736","logging.googleapis.com/spanId":"00f067aa0ba902b7","logging.googleapis.com/trace_sampled":true}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"WARNING","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":41},"message":"warn","logging.googleapis.com/trace":"projects/test/traces/4bf92f3577b34da6a3ce929d0e0e4736","logging.googleapis.com/spanId":"00f067aa0ba902b7","logging.googleapis.com/trace_sampled":true,"g":{"b":"B","a":"A"}}
{"timestamp":{"seconds":100,"nanos":1000},"severity":"ERROR","logging.googleapis.com/sourceLocation":{"function":"github.com/nil-go/sloth/gcp_test.TestHandler.func1","file":"/handler_test.go","line":49},"message":"error","logging.googleapis.com/trace":"projects/test/traces/4bf92f3577b34da6a3ce929d0e0e4736","logging.googleapis.com/spanId":"00f067aa0ba902b7","logging.googleapis.com/trace_sampled":true,"g":{"h":{"b":"B"}}}
`,
},
}
Expand All @@ -163,21 +167,3 @@ func record(level slog.Level, message string, attrs ...any) slog.Record {

return record
}

type traceContext struct{}

func (traceContext) TraceID() [16]byte {
b, _ := hex.DecodeString("4bf92f3577b34da6a3ce929d0e0e4736")

return [16]byte(b)
}

func (traceContext) SpanID() [8]byte {
b, _ := hex.DecodeString("00f067aa0ba902b7")

return [8]byte(b)
}

func (traceContext) TraceFlags() byte {
return 1
}
18 changes: 7 additions & 11 deletions gcp/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,23 @@ func WithWriter(writer io.Writer) Option {
//
// [trace information]: https://cloud.google.com/trace/docs/trace-log-integration
// [GCP Cloud Trace]: https://cloud.google.com/trace
func WithTrace(project string, contextProvider func(context.Context) TraceContext) Option {
func WithTrace(project string) Option {
if project == "" {
panic("cannot add trace information with empty project")
}
if contextProvider == nil {
panic("cannot add trace information with nil context provider")
}

return func(options *options) {
options.project = project
options.contextProvider = contextProvider
}
}

// TraceContext providers the [W3C Trace Context].
// WithTraceContext providers the [W3C Trace Context].
//
// [W3C Trace Context]: https://www.w3.org/TR/trace-context/#trace-id
type TraceContext interface {
TraceID() [16]byte
SpanID() [8]byte
TraceFlags() byte
func WithTraceContext(provider func(context.Context) (traceID [16]byte, spanID [8]byte, traceFlags byte)) Option {
return func(options *options) {
options.contextProvider = provider
}
}

// WithErrorReporting enables logs reported as [error events] to [GCP Error Reporting].
Expand Down Expand Up @@ -90,7 +86,7 @@ type (

// For trace.
project string
contextProvider func(context.Context) TraceContext
contextProvider func(context.Context) (traceID [16]byte, spanID [8]byte, traceFlags byte)

// For error reporting.
service string
Expand Down
12 changes: 1 addition & 11 deletions gcp/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package gcp_test

import (
"context"
"testing"

"github.com/nil-go/sloth/gcp"
Expand All @@ -22,19 +21,10 @@ func TestOption_panic(t *testing.T) {
{
description: "project is empty",
option: func() gcp.Option {
return gcp.WithTrace("", func(context.Context) gcp.TraceContext {
return traceContext{}
})
return gcp.WithTrace("")
},
err: "cannot add trace information with empty project",
},
{
description: "context provider is nil",
option: func() gcp.Option {
return gcp.WithTrace("test", nil)
},
err: "cannot add trace information with nil context provider",
},
{
description: "service is empty",
option: func() gcp.Option {
Expand Down

0 comments on commit 7fb4e0b

Please sign in to comment.