github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/common/utils/logging/logger.go (about) 1 // Copyright 2019 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package logging 16 17 import ( 18 "log" 19 "os" 20 "strings" 21 "sync" 22 "time" 23 24 "github.com/golang/protobuf/proto" 25 26 "github.com/GoogleCloudPlatform/compute-image-tools/proto/go/pb" 27 ) 28 29 // Logger is a logger for CLI tools. It supports 30 // string messages and structured metrics. 31 // 32 // Structured metrics are accumulated over the lifespan of the logger. 33 // 34 // To rebuild the mock, run `go generate ./...` 35 // 36 //go:generate go run github.com/golang/mock/mockgen -package mocks -source $GOFILE -destination ../../../mocks/mock_logger.go 37 type Logger interface { 38 // User messages appear in the following places: 39 // 1. Web UI and gcloud. 40 // 2. Standard output of the CLI tool. 41 // 3. Backend trace logs (all debug and user logs are combined to a single trace log). 42 User(message string) 43 // Debug messages appear in the following places: 44 // 1. Standard output of the CLI tool. 45 // 2. Backend trace logs (all debug and user logs are combined to a single trace log). 46 Debug(message string) 47 // Trace messages are saved to the logging backend (OutputInfo.serial_outputs). 48 Trace(message string) 49 // Metric merges all non-default fields into a single OutputInfo instance. 50 Metric(metric *pb.OutputInfo) 51 } 52 53 // RedirectGlobalLogsToUser redirects the standard library's static logger. 54 // All messages are written to logger's User level. 55 func RedirectGlobalLogsToUser(logger Logger) { 56 log.SetPrefix("") 57 log.SetFlags(0) 58 log.SetOutput(redirectShim{logger}) 59 } 60 61 // redirectShim forwards Go's standard logger to Logger.User. 62 type redirectShim struct { 63 writer Logger 64 } 65 66 func (l redirectShim) Write(p []byte) (n int, err error) { 67 l.writer.User(string(p)) 68 return len(p), nil 69 } 70 71 // OutputInfoReader exposes pb.OutputInfo to a consumer. 72 type OutputInfoReader interface { 73 ReadOutputInfo() *pb.OutputInfo 74 } 75 76 // ToolLogger implements Logger and OutputInfoReader. Create an instance at the 77 // start of a CLI tool's invocation, and pass that instance to dependencies that 78 // require logging. 79 type ToolLogger interface { 80 // NewLogger creates a new logger that writes to this ToolLogger, but with a 81 // different User prefix. 82 NewLogger(userPrefix string) Logger 83 Logger 84 OutputInfoReader 85 } 86 87 // defaultToolLogger is an implementation of ToolLogger that writes to an arbitrary writer. 88 // It has the following behavior for each level: 89 // 90 // User: 91 // - Writes to the underlying log.Logger with an optional prefix. The prefix is used by 92 // gcloud and the web console for filtering which logs are shown to the user. 93 // - In addition to writing to the underlying log.Logger, the messages are buffered for 94 // inclusion in OutputInfo.SerialOutputs. 95 // 96 // Debug: 97 // - Writes to the underlying log.Logger with an optional prefix. 98 // - In addition to writing to the underlying log.Logger, the messages are buffered for 99 // inclusion in OutputInfo.SerialOutputs. 100 // 101 // Trace: 102 // - Included in OutputInfo.SerialOutputs 103 type defaultToolLogger struct { 104 // userPrefix and debugPrefix are strings that are prepended to user and debug messages. 105 // The userPrefix string should be kept in sync with the matcher used by gcloud and the 106 // web UI when determining which log messages to show to the user. 107 userPrefix, debugPrefix string 108 109 // output: Destination for user and debug messages. 110 output *log.Logger 111 112 // trace: Buffer of trace messages. Cleared when ReadOutputInfo is called. 113 trace []string 114 115 // stdoutLogger: Buffer of messages sent to the output logger. Cleared when 116 // ReadOutputInfo is called. 117 userAndDebugBuffer strings.Builder 118 119 // outputInfo: View of OutputInfo that is updated when Metric is called. 120 // Reset when ReadOutputInfo info is called. 121 outputInfo *pb.OutputInfo 122 123 // timestampFormat is the format string used when writing the current time in user and debug messages. 124 timestampFormat string 125 126 // timeProvider is a function that returns the current time. Typically time.Now. Exposed for testing. 127 timeProvider func() time.Time 128 129 // mutationLock should be taken when reading or writing trace, userAndDebugBuffer, or outputInfo. 130 mutationLock sync.Mutex 131 } 132 133 func (l *defaultToolLogger) NewLogger(userPrefix string) Logger { 134 return &customPrefixLogger{userPrefix, l} 135 } 136 137 // User writes message to the underlying log.Logger, and then buffers the message 138 // for inclusion in ReadOutputInfo(). 139 func (l *defaultToolLogger) User(message string) { 140 l.writeLine(l.userPrefix, message) 141 } 142 143 // Debug writes message to the underlying log.Logger, and then buffers the message 144 // for inclusion in ReadOutputInfo(). 145 func (l *defaultToolLogger) Debug(message string) { 146 l.writeLine(l.debugPrefix, message) 147 } 148 149 // Trace buffers the message for inclusion in ReadOutputInfo(). 150 func (l *defaultToolLogger) Trace(message string) { 151 l.mutationLock.Lock() 152 defer l.mutationLock.Unlock() 153 154 l.trace = append(l.trace, message) 155 } 156 157 // Metric keeps non-nil fields from metric for inclusion in ReadOutputInfo(). 158 // Elements of list fields are appended to the underlying view. 159 func (l *defaultToolLogger) Metric(metric *pb.OutputInfo) { 160 l.mutationLock.Lock() 161 defer l.mutationLock.Unlock() 162 163 proto.Merge(l.outputInfo, metric) 164 } 165 166 // Returns a view comprised of: 167 // - Calls to Metric 168 // - All user, debug, and trace logs. User and debug logs are appended into a single 169 // member of OutputInfo.SerialLogs; each trace log is a separate member. 170 // 171 // All buffers are cleared when this is called. In other words, a subsequent call to 172 // ReadOutputInfo will return an empty object. 173 func (l *defaultToolLogger) ReadOutputInfo() *pb.OutputInfo { 174 // Locking since ReadOutputInfo has a side effect of clearing the internal state. 175 l.mutationLock.Lock() 176 defer l.mutationLock.Unlock() 177 178 ret := l.buildOutput() 179 proto.Merge(ret, l.outputInfo) 180 l.resetBuffers() 181 return ret 182 } 183 184 func (l *defaultToolLogger) resetBuffers() { 185 l.userAndDebugBuffer.Reset() 186 l.trace = []string{} 187 l.outputInfo = &pb.OutputInfo{} 188 } 189 190 func (l *defaultToolLogger) buildOutput() *pb.OutputInfo { 191 var combinedTrace []string 192 if l.userAndDebugBuffer.Len() > 0 { 193 combinedTrace = []string{l.userAndDebugBuffer.String()} 194 } 195 return &pb.OutputInfo{SerialOutputs: append(combinedTrace, l.trace...)} 196 } 197 198 // writeLine writes a message to the underlying logger, and buffer it for inclusion in OutputInfo.SerialLogs. 199 // A newline is added to message if not already present. 200 func (l *defaultToolLogger) writeLine(prefix, message string) { 201 var logLineBuilder strings.Builder 202 // If there's a prefix, ensure it ends with a colon. 203 if prefix != "" { 204 logLineBuilder.WriteString(prefix) 205 if !strings.HasSuffix(prefix, ":") { 206 logLineBuilder.WriteByte(':') 207 } 208 logLineBuilder.WriteByte(' ') 209 } 210 logLineBuilder.WriteString(l.timeProvider().Format(l.timestampFormat)) 211 logLineBuilder.WriteByte(' ') 212 logLineBuilder.WriteString(message) 213 // Ensure the log always ends with a newline. 214 if !strings.HasSuffix(message, "\n") { 215 logLineBuilder.WriteByte('\n') 216 } 217 logLine := logLineBuilder.String() 218 219 l.mutationLock.Lock() 220 defer l.mutationLock.Unlock() 221 l.output.Print(logLine) 222 l.userAndDebugBuffer.WriteString(logLine) 223 } 224 225 // NewToolLogger returns a ToolLogger that writes user and debug messages to 226 // stdout. The userPrefix string is prepended to User messages. Specify 227 // the string that gcloud and the web console uses to find its matches. 228 func NewToolLogger(userPrefix string) ToolLogger { 229 return &defaultToolLogger{ 230 userPrefix: userPrefix, 231 debugPrefix: "[debug]", 232 timestampFormat: time.RFC3339, 233 output: log.New(os.Stdout, "", 0), 234 trace: []string{}, 235 outputInfo: &pb.OutputInfo{}, 236 timeProvider: time.Now, 237 } 238 } 239 240 // customPrefixLogger is a Logger that writes to a ToolLogger using a custom prefix for User messages. 241 type customPrefixLogger struct { 242 userPrefix string 243 parent *defaultToolLogger 244 } 245 246 func (s *customPrefixLogger) User(message string) { 247 s.parent.writeLine(s.userPrefix, message) 248 } 249 250 func (s *customPrefixLogger) Debug(message string) { 251 s.parent.Debug(message) 252 } 253 254 func (s *customPrefixLogger) Trace(message string) { 255 s.parent.Trace(message) 256 } 257 258 func (s *customPrefixLogger) Metric(metric *pb.OutputInfo) { 259 s.parent.Metric(metric) 260 }