cli_tools/common/utils/logging/logger.go (133 lines of code) (raw):

// 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" "github.com/golang/protobuf/proto" "github.com/GoogleCloudPlatform/compute-image-tools/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) }