Skip to content

Commit

Permalink
chore: add custom slog handler for cloud logging
Browse files Browse the repository at this point in the history
  • Loading branch information
ucpr committed Jul 12, 2024
1 parent 59376d5 commit f227afd
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 57 deletions.
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ go 1.22
toolchain go1.22.1

require (
cloud.google.com/go/logging v1.8.1
cloud.google.com/go/logging v1.7.0
cloud.google.com/go/pubsub v1.33.0
github.com/google/wire v0.5.0
github.com/hamba/avro/v2 v2.18.0
github.com/prometheus/client_golang v1.17.0
github.com/sethvargo/go-envconfig v1.0.1
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
go.mongodb.org/mongo-driver v1.13.0
go.opentelemetry.io/otel/trace v1.28.0
go.uber.org/mock v0.4.0
)

Expand All @@ -21,7 +22,7 @@ require (
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/longrunning v0.5.0 // indirect
cloud.google.com/go/longrunning v0.4.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down Expand Up @@ -49,6 +50,7 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
Expand Down
16 changes: 10 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94=
cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk=
cloud.google.com/go/kms v1.11.0 h1:0LPJPKamw3xsVpkel1bDtK0vVJec3EyqdQOLitiD030=
cloud.google.com/go/kms v1.11.0/go.mod h1:hwdiYC0xjnWsKQQCQQmIQnS9asjYVSK6jtXm+zFqXLM=
cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU=
cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI=
cloud.google.com/go/longrunning v0.5.0 h1:DK8BH0+hS+DIvc9a2TPnteUievsTCH4ORMAASSb7JcQ=
cloud.google.com/go/longrunning v0.5.0/go.mod h1:0JNuqRShmscVAhIACGtskSAWtqtOoPkwP0YF1oVEchc=
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE=
cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ=
cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g=
cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down Expand Up @@ -134,8 +134,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
Expand All @@ -149,6 +149,10 @@ go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWU
go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
Expand Down
1 change: 1 addition & 0 deletions pkg/log/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Simple logging package based on log/slog.
| LOG_FORMAT | Log format (`gcp`, `json`, `text`) | `text` | |
| SERVICE | Service name. use for gcp log label. | | |
| ENV | Environment. use for gcp log label. | | |
| GOOGLE_PROJECT_ID | Google Project Id. use for gcp logging | | |
149 changes: 149 additions & 0 deletions pkg/log/gcp_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package log

import (
"context"
"fmt"
"io"
"log/slog"
"runtime"

"cloud.google.com/go/logging"
"go.opentelemetry.io/otel/trace"
)

const (
cloudLoggingSourceKey = "logging.googleapis.com/sourceLocation"
cloudLoggingLabelsKey = "logging.googleapis.com/labels"
cloudLoggingTraceKey = "logging.googleapis.com/trace"
cloudLoggingTraceSpanKey = "logging.googleapis.com/spanId"
cloudLoggingTraceSampledKey = "logging.googleapis.com/trace_sampled"

cloudLoggingMessageKey = "message"
cloudLoggingSeverityKey = "severity"
cloudLoggingEnvironmentLabel = "env"
cloudLoggingServiceLabel = "app"
cloudLoggingTraceFormat = "projects/%s/traces/%s"
)

type CloudLoggingHandler struct {
handler slog.Handler
environment string
service string
projectID string
}

type CloudLoggingHandlerOptions struct {
AddSource bool
Level slog.Level
Environment string
Service string
ProjectID string
ReplaceAttr func(groups []string, a slog.Attr) slog.Attr
}

// Ensure CloudLoggingHandler implements slog.Handler.
var _ slog.Handler = (*CloudLoggingHandler)(nil)

// NewCloudLoggingHandler creates a new CloudLoggingHandler.
func NewCloudLoggingHandler(out io.Writer, options *CloudLoggingHandlerOptions) *CloudLoggingHandler {
replaceAttr := func(groups []string, attr slog.Attr) slog.Attr {
cattr := attrReplacerForCloudLogging(groups, attr)
if options.ReplaceAttr != nil {
cattr = options.ReplaceAttr(groups, cattr)
}
return cattr
}

handler := slog.NewJSONHandler(out, &slog.HandlerOptions{
AddSource: options.AddSource,
Level: options.Level,
ReplaceAttr: replaceAttr,
})

return &CloudLoggingHandler{
handler: handler,
environment: options.Environment,
service: options.Service,
projectID: options.ProjectID,
}
}

// Enabled implements the slog.Handler interface.
func (h *CloudLoggingHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.handler.Enabled(ctx, level)
}

// Handle implements the slog.Handler interface.
func (h *CloudLoggingHandler) Handle(ctx context.Context, record slog.Record) error {
record.AddAttrs(
slog.Group(cloudLoggingLabelsKey,
slog.String(cloudLoggingServiceLabel, h.service),
slog.String(cloudLoggingEnvironmentLabel, h.environment),
),
)
record.Level = h.toCloudLoggingLevel(record.Level)

// set trace
sc := trace.SpanContextFromContext(ctx)
if sc.IsValid() {
trace := fmt.Sprintf(cloudLoggingTraceFormat, h.projectID, sc.TraceID().String())
record.AddAttrs(
slog.String(cloudLoggingTraceKey, trace),
slog.String(cloudLoggingTraceSpanKey, sc.SpanID().String()),
slog.Bool(cloudLoggingTraceSampledKey, sc.TraceFlags().IsSampled()),
)
}

return h.handler.Handle(ctx, record)
}

// WithAttrs implements the slog.Handler interface.
func (h *CloudLoggingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h.handler.WithAttrs(attrs)
}

// WithGroup implements the slog.Handler interface.
func (h *CloudLoggingHandler) WithGroup(name string) slog.Handler {
return h.handler.WithGroup(name)
}

func (h *CloudLoggingHandler) toCloudLoggingLevel(level slog.Level) slog.Level {
switch level {
case SeverityDebug:
return slog.Level(logging.Debug)
case SeverityInfo:
return slog.Level(logging.Info)
case SeverityNotice:
return slog.Level(logging.Notice)
case SeverityWarning:
return slog.Level(logging.Warning)
case SeverityError:
return slog.Level(logging.Error)
case SeverityCritical:
return slog.Level(logging.Critical)
}
return slog.Level(logging.Info)
}

// attrReplacerForCloudLogging is default attribute replacer.
func attrReplacerForCloudLogging(_ []string, attr slog.Attr) slog.Attr {
switch attr.Key {
case slog.MessageKey:
attr.Key = cloudLoggingMessageKey
case slog.LevelKey:
attr.Key = cloudLoggingSeverityKey
attr.Value = slog.StringValue(logging.Severity(attr.Value.Any().(slog.Level)).String())
case slog.SourceKey:
attr.Key = cloudLoggingSourceKey
// Replace the value of the "source" attribute with the value of the "sourceLocation" attribute.
const skip = 9
_, file, line, ok := runtime.Caller(skip)
if !ok {
return attr
}
v := fmt.Sprintf("%s:%d", file, line)
attr.Value = slog.StringValue(v)
}

return attr
}
98 changes: 50 additions & 48 deletions pkg/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,91 +6,93 @@ import (
"log/slog"
"os"
"runtime"

"cloud.google.com/go/logging"
"strings"
)

//nolint:gochecknoglobals
var (
SeverityDefault = slog.Level(logging.Default)
SeverityDebug = slog.Level(logging.Debug)
SeverityInfo = slog.Level(logging.Info)
SeverityNotice = slog.Level(logging.Notice)
SeverityWarning = slog.Level(logging.Warning)
SeverityError = slog.Level(logging.Error)
SeverityCritical = slog.Level(logging.Critical)
SeverityDebug = slog.LevelDebug // -4
SeverityInfo = slog.LevelInfo // 0
SeverityNotice = slog.Level(2) // 2
SeverityWarning = slog.LevelWarn // 4
SeverityError = slog.LevelError // 8
SeverityCritical = slog.Level(10) // 10
)

// format is the log format type
type format string

//nolint:unused
const (
// formatGCP is the Google Cloud Platform format.
formatGCP format = "gcp"
// formatJSON is the JSON format.
formatJSON format = "json"
// formatText is the text format.
formatText format = "text"
)

// logger is the global logger.
// it is initialized by init() and should not be modified.
//
//nolint:gochecknoglobals
var logger *slog.Logger

// init initializes the logger.
func init() {
logFormat := os.Getenv("LOG_FORMAT")
logFormat := strings.ToLower(os.Getenv("LOG_FORMAT"))
service := os.Getenv("SERVICE")
env := os.Getenv("ENV")
googleProjectID := os.Getenv("GOOGLE_PROJECT_ID")

handler := newHandler(logFormat, service, env)
handler := newHandler(newHandlerOptions{
format: format(logFormat),
service: service,
env: env,
googleProjectID: googleProjectID,
})
logger = slog.New(handler)
}

type newHandlerOptions struct {
format format
service string
env string
googleProjectID string
}

// newHandler returns a slog.Handler based on the given format.
func newHandler(format, service, env string) slog.Handler {
switch format {
case "gcp":
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
func newHandler(opts newHandlerOptions) slog.Handler {
switch opts.format {
case formatGCP:
return NewCloudLoggingHandler(os.Stdout, &CloudLoggingHandlerOptions{
AddSource: true,
Level: SeverityDefault,
ReplaceAttr: attrReplacerForCloudLogging,
Level: SeverityInfo,
Environment: opts.env,
Service: opts.service,
ProjectID: opts.googleProjectID,
})
handler.WithAttrs([]slog.Attr{
slog.Group("logging.googleapis.com/labels",
slog.String("app", service),
slog.String("env", env),
),
})
return handler
case "json":
case formatJSON:
return slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: SeverityDefault,
Level: SeverityInfo,
ReplaceAttr: attrReplacerForDefault,
})
}

return slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: SeverityDefault,
Level: SeverityInfo,
ReplaceAttr: attrReplacerForDefault,
})
}

// attrReplacerForDefault is default attribute replacer.
func attrReplacerForDefault(groups []string, attr slog.Attr) slog.Attr {
// Replace the value of the "severity" attribute with the value of the "level" attribute.
level, ok := attr.Value.Any().(slog.Level)
if ok {
attr.Value = toLogLevel(level)
}
return attr
}

// attrReplacerForCloudLogging replaces slog's default attributes for Google Cloud Logging.
func attrReplacerForCloudLogging(groups []string, attr slog.Attr) slog.Attr {
switch attr.Key {
case slog.MessageKey:
attr.Key = "message"
case slog.LevelKey:
attr.Key = "severity"
attr.Value = slog.StringValue(logging.Severity(attr.Value.Any().(slog.Level)).String())
// Replace the value of the "severity" attribute with the value of the "level" attribute.
level, ok := attr.Value.Any().(slog.Level)
if ok {
attr.Value = toLogLevel(level)
}
attr.Value = toLogLevel(attr.Value.Any().(slog.Level))
case slog.SourceKey:
attr.Key = "logging.googleapis.com/sourceLocation"
// Replace the value of the "source" attribute with the value of the "sourceLocation" attribute.
const skip = 7
const skip = 9
_, file, line, ok := runtime.Caller(skip)
if !ok {
return attr
Expand Down

0 comments on commit f227afd

Please sign in to comment.