-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlogger.go
230 lines (204 loc) · 6.94 KB
/
logger.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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package logging contains logging related utilities.
package logging
import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"sync"
"time"
"github.com/TiagoMalhadas/xcweaver/runtime/colors"
"github.com/TiagoMalhadas/xcweaver/runtime/protos"
"github.com/google/uuid"
)
// SystemAttributeKey is present as the key of an attribute (with an empty value)
// in log entries generated by Service Weaver system components.
const SystemAttributeKey = "serviceweaver/system"
// IsSystemGenerated returns true for log entries generated by system components.
func IsSystemGenerated(entry *protos.LogEntry) bool {
for i := 0; i < len(entry.Attrs); i += 2 {
if entry.Attrs[i] == SystemAttributeKey {
return true
}
}
return false
}
// Options configures the log entries produced by a logger.
type Options struct {
App string // Service Weaver application (e.g., "todo")
Deployment string // Service Weaver deployment (e.g., "36105c89-85b1...")
Component string // Service Weaver component (e.g., "Todo")
Weavelet string // Service Weaver weavelet id (e.g., "36105c89-85b1...")
// Pre-assigned attributes. These will be attached to each log entry
// generated by the logger. This slice will never be appended to in place.
Attrs []string
}
// LogHandler implements a custom slog.Handler.
type LogHandler struct {
Opts Options // configures the log entries
Write func(entry *protos.LogEntry) // called on every log entry
}
var _ slog.Handler = &LogHandler{}
// Handle implements the slog.Handler interface.
func (h *LogHandler) Handle(ctx context.Context, rec slog.Record) error {
h.Write(h.makeEntry(rec))
return nil
}
// Enabled implements the slog.Handler interface.
func (h *LogHandler) Enabled(context.Context, slog.Level) bool {
// Support all logging levels.
return true
}
// WithAttrs implements the slog.Handler interface.
func (h *LogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
// Note that attributes set through explicit calls to log.WithAttrs should
// apply to all the log entries, similar to h.Opts.
//
// Note that WithAttrs results in a new logger, hence we should create a new
// handler that contains the new attributes.
rh := &LogHandler{
Opts: h.Opts,
Write: h.Write,
}
rh.Opts.Attrs = appendAttrs(rh.Opts.Attrs, attrs)
return rh
}
// WithGroup implements the slog.Handler interface.
//
// TODO(rgrandl): Implement it, so the users have the same experience as slog
// if they decide to use WithGroup.
func (h *LogHandler) WithGroup(string) slog.Handler {
return h
}
// makeEntry returns an entry that is fully populated with information captured
// by a log record.
func (h *LogHandler) makeEntry(rec slog.Record) *protos.LogEntry {
// TODO(sanjay): Is it necessary to copy opts.Attrs even if no new attrs
// are being added?
var attrs []slog.Attr
rec.Attrs(func(a slog.Attr) bool {
attrs = append(attrs, a)
return true
})
entry := protos.LogEntry{
App: h.Opts.App,
Version: h.Opts.Deployment,
Component: h.Opts.Component,
Node: h.Opts.Weavelet,
TimeMicros: rec.Time.UnixMicro(),
Level: rec.Level.String(),
File: "",
Line: -1,
Msg: rec.Message,
Attrs: appendAttrs(h.Opts.Attrs, attrs),
}
// Get the file and line information.
fs := runtime.CallersFrames([]uintptr{rec.PC})
if fs != nil {
frame, _ := fs.Next()
entry.File = frame.File
entry.Line = int32(frame.Line)
}
return &entry
}
// StderrLogger returns a logger that pretty prints log entries to stderr.
func StderrLogger(opts Options) *slog.Logger {
pp := NewPrettyPrinter(colors.Enabled())
writeText := func(entry *protos.LogEntry) {
fmt.Fprintln(os.Stderr, pp.Format(entry))
}
return slog.New(&LogHandler{Opts: opts, Write: writeText})
}
// TB is the subset of the [testing.TB] interface needed by a TestLogger.
type TB interface {
Log(args ...any)
Cleanup(func())
}
// TestLogger implements a logger for tests.
type TestLogger struct {
t TB // logs until t finishes
verbose bool // show logs?
pp *PrettyPrinter // formats log entries
mu sync.Mutex // guards finished
finished bool // has t finished?
}
// Log logs the provided log entry using t.t.Log while the test is running and
// logs to stderr afterwards.
func (t *TestLogger) Log(entry *protos.LogEntry) {
if entry.TimeMicros == 0 {
entry.TimeMicros = time.Now().UnixMicro()
}
msg := t.pp.Format(entry)
t.mu.Lock()
defer t.mu.Unlock()
switch {
case !t.verbose:
// If -test.v isn't provided, don't log anything. Note that writing to
// t.Log isn't sufficient because B.Log ignores -test.v [1]. We
// override this behavior because, otherwise, weavertest benchmark
// results are polluted with logs and are very hard to read.
//
// Also note that we still format msg and grab the lock even if
// t.verbose is false to prevent the performance of a benchmark varying
// drastically based on the presence of the -test.v flag.
//
// [1]: https://pkg.go.dev/testing#B.Log
case t.finished:
// If the test is finished, Log may panic if called. Instead, we write
// to stderr so we don't lose useful debugging information.
fmt.Fprintln(os.Stderr, msg)
default:
t.t.Log(msg)
}
}
// NewTestLogger returns a new TestLogger.
func NewTestLogger(t TB, verbose bool) *TestLogger {
pp := NewPrettyPrinter(colors.Enabled())
logger := &TestLogger{t: t, pp: pp, verbose: verbose}
t.Cleanup(func() {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.finished = true
})
return logger
}
// NewTestSlogger returns a new logger for tests.
func NewTestSlogger(t TB, verbose bool) *slog.Logger {
logger := NewTestLogger(t, verbose)
return slog.New(&LogHandler{
Opts: Options{Component: "TestLogger", Weavelet: uuid.New().String()},
Write: logger.Log,
})
}
// appendAttrs appends <name,value> pairs found in attrs to prefix
// and returns the resulting slice. It never appends in place.
func appendAttrs(prefix []string, attrs []slog.Attr) []string {
if len(attrs) == 0 {
return prefix
}
// NOTE: Copy prefix to avoid the scenario where two different
// loggers overwrite the existing slice entry. This is possible,
// for example, if two goroutines call With() on the same logger
// concurrently.
var dst []string
dst = append(dst, prefix...)
// Extract key,value pairs from attrs.
for _, attr := range attrs {
dst = append(dst, []string{attr.Key, attr.Value.String()}...)
}
return dst
}