-
Notifications
You must be signed in to change notification settings - Fork 21
/
logger.go
260 lines (223 loc) · 8.55 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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// Copyright 2019 Google Inc. All Rights Reserved.
//
// 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
import (
"log"
"os"
"strings"
"sync"
"time"
"google.golang.org/protobuf/proto"
"github.com/GoogleCloudPlatform/compute-image-import/proto/go/pb"
)
// Logger is a logger for CLI tools. It supports
// string messages and structured metrics.
//
// Structured metrics are accumulated over the lifespan of the logger.
//
// To rebuild the mock, run `go generate ./...`
//
//go:generate go run github.com/golang/mock/mockgen -package mocks -source $GOFILE -destination ../../../mocks/mock_logger.go
type Logger interface {
// User messages appear in the following places:
// 1. Web UI and gcloud.
// 2. Standard output of the CLI tool.
// 3. Backend trace logs (all debug and user logs are combined to a single trace log).
User(message string)
// Debug messages appear in the following places:
// 1. Standard output of the CLI tool.
// 2. Backend trace logs (all debug and user logs are combined to a single trace log).
Debug(message string)
// Trace messages are saved to the logging backend (OutputInfo.serial_outputs).
Trace(message string)
// Metric merges all non-default fields into a single OutputInfo instance.
Metric(metric *pb.OutputInfo)
}
// RedirectGlobalLogsToUser redirects the standard library's static logger.
// All messages are written to logger's User level.
func RedirectGlobalLogsToUser(logger Logger) {
log.SetPrefix("")
log.SetFlags(0)
log.SetOutput(redirectShim{logger})
}
// redirectShim forwards Go's standard logger to Logger.User.
type redirectShim struct {
writer Logger
}
func (l redirectShim) Write(p []byte) (n int, err error) {
l.writer.User(string(p))
return len(p), nil
}
// OutputInfoReader exposes pb.OutputInfo to a consumer.
type OutputInfoReader interface {
ReadOutputInfo() *pb.OutputInfo
}
// ToolLogger implements Logger and OutputInfoReader. Create an instance at the
// start of a CLI tool's invocation, and pass that instance to dependencies that
// require logging.
type ToolLogger interface {
// NewLogger creates a new logger that writes to this ToolLogger, but with a
// different User prefix.
NewLogger(userPrefix string) Logger
Logger
OutputInfoReader
}
// defaultToolLogger is an implementation of ToolLogger that writes to an arbitrary writer.
// It has the following behavior for each level:
//
// User:
// - Writes to the underlying log.Logger with an optional prefix. The prefix is used by
// gcloud and the web console for filtering which logs are shown to the user.
// - In addition to writing to the underlying log.Logger, the messages are buffered for
// inclusion in OutputInfo.SerialOutputs.
//
// Debug:
// - Writes to the underlying log.Logger with an optional prefix.
// - In addition to writing to the underlying log.Logger, the messages are buffered for
// inclusion in OutputInfo.SerialOutputs.
//
// Trace:
// - Included in OutputInfo.SerialOutputs
type defaultToolLogger struct {
// userPrefix and debugPrefix are strings that are prepended to user and debug messages.
// The userPrefix string should be kept in sync with the matcher used by gcloud and the
// web UI when determining which log messages to show to the user.
userPrefix, debugPrefix string
// output: Destination for user and debug messages.
output *log.Logger
// trace: Buffer of trace messages. Cleared when ReadOutputInfo is called.
trace []string
// stdoutLogger: Buffer of messages sent to the output logger. Cleared when
// ReadOutputInfo is called.
userAndDebugBuffer strings.Builder
// outputInfo: View of OutputInfo that is updated when Metric is called.
// Reset when ReadOutputInfo info is called.
outputInfo *pb.OutputInfo
// timestampFormat is the format string used when writing the current time in user and debug messages.
timestampFormat string
// timeProvider is a function that returns the current time. Typically time.Now. Exposed for testing.
timeProvider func() time.Time
// mutationLock should be taken when reading or writing trace, userAndDebugBuffer, or outputInfo.
mutationLock sync.Mutex
}
func (l *defaultToolLogger) NewLogger(userPrefix string) Logger {
return &customPrefixLogger{userPrefix, l}
}
// User writes message to the underlying log.Logger, and then buffers the message
// for inclusion in ReadOutputInfo().
func (l *defaultToolLogger) User(message string) {
l.writeLine(l.userPrefix, message)
}
// Debug writes message to the underlying log.Logger, and then buffers the message
// for inclusion in ReadOutputInfo().
func (l *defaultToolLogger) Debug(message string) {
l.writeLine(l.debugPrefix, message)
}
// Trace buffers the message for inclusion in ReadOutputInfo().
func (l *defaultToolLogger) Trace(message string) {
l.mutationLock.Lock()
defer l.mutationLock.Unlock()
l.trace = append(l.trace, message)
}
// Metric keeps non-nil fields from metric for inclusion in ReadOutputInfo().
// Elements of list fields are appended to the underlying view.
func (l *defaultToolLogger) Metric(metric *pb.OutputInfo) {
l.mutationLock.Lock()
defer l.mutationLock.Unlock()
proto.Merge(l.outputInfo, metric)
}
// Returns a view comprised of:
// - Calls to Metric
// - All user, debug, and trace logs. User and debug logs are appended into a single
// member of OutputInfo.SerialLogs; each trace log is a separate member.
//
// All buffers are cleared when this is called. In other words, a subsequent call to
// ReadOutputInfo will return an empty object.
func (l *defaultToolLogger) ReadOutputInfo() *pb.OutputInfo {
// Locking since ReadOutputInfo has a side effect of clearing the internal state.
l.mutationLock.Lock()
defer l.mutationLock.Unlock()
ret := l.buildOutput()
proto.Merge(ret, l.outputInfo)
l.resetBuffers()
return ret
}
func (l *defaultToolLogger) resetBuffers() {
l.userAndDebugBuffer.Reset()
l.trace = []string{}
l.outputInfo = &pb.OutputInfo{}
}
func (l *defaultToolLogger) buildOutput() *pb.OutputInfo {
var combinedTrace []string
if l.userAndDebugBuffer.Len() > 0 {
combinedTrace = []string{l.userAndDebugBuffer.String()}
}
return &pb.OutputInfo{SerialOutputs: append(combinedTrace, l.trace...)}
}
// writeLine writes a message to the underlying logger, and buffer it for inclusion in OutputInfo.SerialLogs.
// A newline is added to message if not already present.
func (l *defaultToolLogger) writeLine(prefix, message string) {
var logLineBuilder strings.Builder
// If there's a prefix, ensure it ends with a colon.
if prefix != "" {
logLineBuilder.WriteString(prefix)
if !strings.HasSuffix(prefix, ":") {
logLineBuilder.WriteByte(':')
}
logLineBuilder.WriteByte(' ')
}
logLineBuilder.WriteString(l.timeProvider().Format(l.timestampFormat))
logLineBuilder.WriteByte(' ')
logLineBuilder.WriteString(message)
// Ensure the log always ends with a newline.
if !strings.HasSuffix(message, "\n") {
logLineBuilder.WriteByte('\n')
}
logLine := logLineBuilder.String()
l.mutationLock.Lock()
defer l.mutationLock.Unlock()
l.output.Print(logLine)
l.userAndDebugBuffer.WriteString(logLine)
}
// NewToolLogger returns a ToolLogger that writes user and debug messages to
// stdout. The userPrefix string is prepended to User messages. Specify
// the string that gcloud and the web console uses to find its matches.
func NewToolLogger(userPrefix string) ToolLogger {
return &defaultToolLogger{
userPrefix: userPrefix,
debugPrefix: "[debug]",
timestampFormat: time.RFC3339,
output: log.New(os.Stdout, "", 0),
trace: []string{},
outputInfo: &pb.OutputInfo{},
timeProvider: time.Now,
}
}
// customPrefixLogger is a Logger that writes to a ToolLogger using a custom prefix for User messages.
type customPrefixLogger struct {
userPrefix string
parent *defaultToolLogger
}
func (s *customPrefixLogger) User(message string) {
s.parent.writeLine(s.userPrefix, message)
}
func (s *customPrefixLogger) Debug(message string) {
s.parent.Debug(message)
}
func (s *customPrefixLogger) Trace(message string) {
s.parent.Trace(message)
}
func (s *customPrefixLogger) Metric(metric *pb.OutputInfo) {
s.parent.Metric(metric)
}