-
Notifications
You must be signed in to change notification settings - Fork 0
/
soglog.go
160 lines (134 loc) · 4.45 KB
/
soglog.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package soglog
import (
"bytes"
"context"
"fmt"
"log/slog"
"os"
"runtime"
"strconv"
"cloud.google.com/go/logging"
"go.opentelemetry.io/otel/trace"
)
// See: https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
const (
keySource string = "logging.googleapis.com/sourceLocation"
keyLabel string = "logging.googleapis.com/labels"
keyTrace string = "logging.googleapis.com/trace"
keySpan string = "logging.googleapis.com/spanID"
keyTraceSampled string = "logging.googleapis.com/trace_sampled"
keyTime string = "timestamp"
keyMessage string = "message"
keySeverity string = "severity"
keyStack string = "stack_trace"
traceFmt = "projects/%s/traces/%s"
)
var _ slog.Handler = (*CloudLoggingHandler)(nil)
type CloudLoggingHandler struct {
handler slog.Handler
projectID string
isStackTraced bool
LabelFieldInjector labelFieldInjector
}
type labelFieldInjector func(ctx context.Context) (key, value string, found bool)
// Options struct provides options to enable stack tracing and to inject custom label fields.
type Options struct {
// IsStackTraced indicates whether to include a stack trace in error logs.
// If set to true, a stack trace will be added to the log entries when the log level is Error.
IsStackTraced bool
// LabelFieldInjector is a function that injects custom label fields into the log entries.
// This function takes a context and returns a key-value pair to be added as labels,
LabelFieldInjector labelFieldInjector
}
// NewCloudLoggingHandler creates a new CloudLoggingHandler with optional settings.
// If no options are provided, stack traces will not be included in error logs and no additional label fields will be injected.
// Example usage:
// slog.SetDefault(slog.New(soglog.NewCloudLoggingHandler("your-project-id", &soglog.Options{IsStackTraced: true, LabelFieldInjector: yourLabelFieldInjector})
func NewCloudLoggingHandler(projectID string, options ...*Options) *CloudLoggingHandler {
opts := &Options{}
if len(options) > 0 {
opts = options[0]
}
return &CloudLoggingHandler{
handler: slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
ReplaceAttr: replaceAttr,
}),
projectID: projectID,
isStackTraced: opts.IsStackTraced,
LabelFieldInjector: opts.LabelFieldInjector,
}
}
func (h *CloudLoggingHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.handler.Enabled(ctx, level)
}
func (h *CloudLoggingHandler) Handle(ctx context.Context, rec slog.Record) error {
// set trace info
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.IsValid() {
rec.AddAttrs(
slog.String(keyTrace, fmt.Sprintf(traceFmt, h.projectID, spanCtx.TraceID().String())),
slog.String(keySpan, spanCtx.SpanID().String()),
slog.Bool(keyTraceSampled, spanCtx.IsSampled()),
)
}
// add customized label
if h.LabelFieldInjector != nil {
key, value, found := h.LabelFieldInjector(ctx)
if found {
rec.AddAttrs(slog.Group(keyLabel, slog.String(key, value)))
}
}
// set stack trace
if h.isStackTraced && rec.Level.String() == slog.LevelError.String() {
rec.AddAttrs(
// skip 3 [this func, slog.(*Logger).log, slog.ErrorContext]
slog.String(keyStack, string(newStackFrames(3))),
)
}
return h.handler.Handle(ctx, rec)
}
func (h *CloudLoggingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &CloudLoggingHandler{handler: h.handler.WithAttrs(attrs)}
}
func (h *CloudLoggingHandler) WithGroup(name string) slog.Handler {
return &CloudLoggingHandler{handler: h.handler.WithGroup(name)}
}
func replaceAttr(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.TimeKey:
a.Key = keyTime
case slog.MessageKey:
a.Key = keyMessage
case slog.SourceKey:
a.Key = keySource
case slog.LevelKey:
a.Key = keySeverity
if a.Value.String() == slog.LevelWarn.String() {
a.Value = slog.StringValue(logging.Warning.String())
}
}
return a
}
func newStackFrames(skip int) []byte {
const numFrames = 32
pcs := [numFrames]uintptr{}
// skip [runtime.Callers, this function]
n := runtime.Callers(skip+2, pcs[:])
buf := bytes.Buffer{}
frames := runtime.CallersFrames(pcs[:n])
for {
f, ok := frames.Next()
if !ok {
break
}
buf.WriteString(f.Function)
buf.WriteString("(...)\n\t")
buf.WriteString(f.File)
buf.Write([]byte{':'})
buf.WriteString(strconv.Itoa(f.Line))
buf.Write([]byte{'\n'})
}
return buf.Bytes()
}