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  }