...

Source file src/go.mongodb.org/mongo-driver/internal/logger/logger.go

Documentation: go.mongodb.org/mongo-driver/internal/logger

     1  // Copyright (C) MongoDB, Inc. 2023-present.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License"); you may
     4  // not use this file except in compliance with the License. You may obtain
     5  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
     6  
     7  // Package logger provides the internal logging solution for the MongoDB Go
     8  // Driver.
     9  package logger
    10  
    11  import (
    12  	"fmt"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  )
    17  
    18  // DefaultMaxDocumentLength is the default maximum number of bytes that can be
    19  // logged for a stringified BSON document.
    20  const DefaultMaxDocumentLength = 1000
    21  
    22  // TruncationSuffix are trailing ellipsis "..." appended to a message to
    23  // indicate to the user that truncation occurred. This constant does not count
    24  // toward the max document length.
    25  const TruncationSuffix = "..."
    26  
    27  const logSinkPathEnvVar = "MONGODB_LOG_PATH"
    28  const maxDocumentLengthEnvVar = "MONGODB_LOG_MAX_DOCUMENT_LENGTH"
    29  
    30  // LogSink represents a logging implementation, this interface should be 1-1
    31  // with the exported "LogSink" interface in the mongo/options package.
    32  type LogSink interface {
    33  	// Info logs a non-error message with the given key/value pairs. The
    34  	// level argument is provided for optional logging.
    35  	Info(level int, msg string, keysAndValues ...interface{})
    36  
    37  	// Error logs an error, with the given message and key/value pairs.
    38  	Error(err error, msg string, keysAndValues ...interface{})
    39  }
    40  
    41  // Logger represents the configuration for the internal logger.
    42  type Logger struct {
    43  	ComponentLevels   map[Component]Level // Log levels for each component.
    44  	Sink              LogSink             // LogSink for log printing.
    45  	MaxDocumentLength uint                // Command truncation width.
    46  	logFile           *os.File            // File to write logs to.
    47  }
    48  
    49  // New will construct a new logger. If any of the given options are the
    50  // zero-value of the argument type, then the constructor will attempt to
    51  // source the data from the environment. If the environment has not been set,
    52  // then the constructor will the respective default values.
    53  func New(sink LogSink, maxDocLen uint, compLevels map[Component]Level) (*Logger, error) {
    54  	logger := &Logger{
    55  		ComponentLevels:   selectComponentLevels(compLevels),
    56  		MaxDocumentLength: selectMaxDocumentLength(maxDocLen),
    57  	}
    58  
    59  	sink, logFile, err := selectLogSink(sink)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	logger.Sink = sink
    65  	logger.logFile = logFile
    66  
    67  	return logger, nil
    68  }
    69  
    70  // Close will close the logger's log file, if it exists.
    71  func (logger *Logger) Close() error {
    72  	if logger.logFile != nil {
    73  		return logger.logFile.Close()
    74  	}
    75  
    76  	return nil
    77  }
    78  
    79  // LevelComponentEnabled will return true if the given LogLevel is enabled for
    80  // the given LogComponent. If the ComponentLevels on the logger are enabled for
    81  // "ComponentAll", then this function will return true for any level bound by
    82  // the level assigned to "ComponentAll".
    83  //
    84  // If the level is not enabled (i.e. LevelOff), then false is returned. This is
    85  // to avoid false positives, such as returning "true" for a component that is
    86  // not enabled. For example, without this condition, an empty LevelComponent
    87  // would be considered "enabled" for "LevelOff".
    88  func (logger *Logger) LevelComponentEnabled(level Level, component Component) bool {
    89  	if level == LevelOff {
    90  		return false
    91  	}
    92  
    93  	if logger.ComponentLevels == nil {
    94  		return false
    95  	}
    96  
    97  	return logger.ComponentLevels[component] >= level ||
    98  		logger.ComponentLevels[ComponentAll] >= level
    99  }
   100  
   101  // Print will synchronously print the given message to the configured LogSink.
   102  // If the LogSink is nil, then this method will do nothing. Future work could be done to make
   103  // this method asynchronous, see buffer management in libraries such as log4j.
   104  //
   105  // It's worth noting that many structured logs defined by DBX-wide
   106  // specifications include a "message" field, which is often shared with the
   107  // message arguments passed to this print function. The "Info" method used by
   108  // this function is implemented based on the go-logr/logr LogSink interface,
   109  // which is why "Print" has a message parameter. Any duplication in code is
   110  // intentional to adhere to the logr pattern.
   111  func (logger *Logger) Print(level Level, component Component, msg string, keysAndValues ...interface{}) {
   112  	// If the level is not enabled for the component, then
   113  	// skip the message.
   114  	if !logger.LevelComponentEnabled(level, component) {
   115  		return
   116  	}
   117  
   118  	// If the sink is nil, then skip the message.
   119  	if logger.Sink == nil {
   120  		return
   121  	}
   122  
   123  	logger.Sink.Info(int(level)-DiffToInfo, msg, keysAndValues...)
   124  }
   125  
   126  // Error logs an error, with the given message and key/value pairs.
   127  // It functions similarly to Print, but may have unique behavior, and should be
   128  // preferred for logging errors.
   129  func (logger *Logger) Error(err error, msg string, keysAndValues ...interface{}) {
   130  	if logger.Sink == nil {
   131  		return
   132  	}
   133  
   134  	logger.Sink.Error(err, msg, keysAndValues...)
   135  }
   136  
   137  // selectMaxDocumentLength will return the integer value of the first non-zero
   138  // function, with the user-defined function taking priority over the environment
   139  // variables. For the environment, the function will attempt to get the value of
   140  // "MONGODB_LOG_MAX_DOCUMENT_LENGTH" and parse it as an unsigned integer. If the
   141  // environment variable is not set or is not an unsigned integer, then this
   142  // function will return the default max document length.
   143  func selectMaxDocumentLength(maxDocLen uint) uint {
   144  	if maxDocLen != 0 {
   145  		return maxDocLen
   146  	}
   147  
   148  	maxDocLenEnv := os.Getenv(maxDocumentLengthEnvVar)
   149  	if maxDocLenEnv != "" {
   150  		maxDocLenEnvInt, err := strconv.ParseUint(maxDocLenEnv, 10, 32)
   151  		if err == nil {
   152  			return uint(maxDocLenEnvInt)
   153  		}
   154  	}
   155  
   156  	return DefaultMaxDocumentLength
   157  }
   158  
   159  const (
   160  	logSinkPathStdout = "stdout"
   161  	logSinkPathStderr = "stderr"
   162  )
   163  
   164  // selectLogSink will return the first non-nil LogSink, with the user-defined
   165  // LogSink taking precedence over the environment-defined LogSink. If no LogSink
   166  // is defined, then this function will return a LogSink that writes to stderr.
   167  func selectLogSink(sink LogSink) (LogSink, *os.File, error) {
   168  	if sink != nil {
   169  		return sink, nil, nil
   170  	}
   171  
   172  	path := os.Getenv(logSinkPathEnvVar)
   173  	lowerPath := strings.ToLower(path)
   174  
   175  	if lowerPath == string(logSinkPathStderr) {
   176  		return NewIOSink(os.Stderr), nil, nil
   177  	}
   178  
   179  	if lowerPath == string(logSinkPathStdout) {
   180  		return NewIOSink(os.Stdout), nil, nil
   181  	}
   182  
   183  	if path != "" {
   184  		logFile, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
   185  		if err != nil {
   186  			return nil, nil, fmt.Errorf("unable to open log file: %w", err)
   187  		}
   188  
   189  		return NewIOSink(logFile), logFile, nil
   190  	}
   191  
   192  	return NewIOSink(os.Stderr), nil, nil
   193  }
   194  
   195  // selectComponentLevels returns a new map of LogComponents to LogLevels that is
   196  // the result of merging the user-defined data with the environment, with the
   197  // user-defined data taking priority.
   198  func selectComponentLevels(componentLevels map[Component]Level) map[Component]Level {
   199  	selected := make(map[Component]Level)
   200  
   201  	// Determine if the "MONGODB_LOG_ALL" environment variable is set.
   202  	var globalEnvLevel *Level
   203  	if all := os.Getenv(mongoDBLogAllEnvVar); all != "" {
   204  		level := ParseLevel(all)
   205  		globalEnvLevel = &level
   206  	}
   207  
   208  	for envVar, component := range componentEnvVarMap {
   209  		// If the component already has a level, then skip it.
   210  		if _, ok := componentLevels[component]; ok {
   211  			selected[component] = componentLevels[component]
   212  
   213  			continue
   214  		}
   215  
   216  		// If the "MONGODB_LOG_ALL" environment variable is set, then
   217  		// set the level for the component to the value of the
   218  		// environment variable.
   219  		if globalEnvLevel != nil {
   220  			selected[component] = *globalEnvLevel
   221  
   222  			continue
   223  		}
   224  
   225  		// Otherwise, set the level for the component to the value of
   226  		// the environment variable.
   227  		selected[component] = ParseLevel(os.Getenv(envVar))
   228  	}
   229  
   230  	return selected
   231  }
   232  
   233  // truncate will truncate a string to the given width, appending "..." to the
   234  // end of the string if it is truncated. This routine is safe for multi-byte
   235  // characters.
   236  func truncate(str string, width uint) string {
   237  	if width == 0 {
   238  		return ""
   239  	}
   240  
   241  	if len(str) <= int(width) {
   242  		return str
   243  	}
   244  
   245  	// Truncate the byte slice of the string to the given width.
   246  	newStr := str[:width]
   247  
   248  	// Check if the last byte is at the beginning of a multi-byte character.
   249  	// If it is, then remove the last byte.
   250  	if newStr[len(newStr)-1]&0xC0 == 0xC0 {
   251  		return newStr[:len(newStr)-1] + TruncationSuffix
   252  	}
   253  
   254  	// Check if the last byte is in the middle of a multi-byte character. If
   255  	// it is, then step back until we find the beginning of the character.
   256  	if newStr[len(newStr)-1]&0xC0 == 0x80 {
   257  		for i := len(newStr) - 1; i >= 0; i-- {
   258  			if newStr[i]&0xC0 == 0xC0 {
   259  				return newStr[:i] + TruncationSuffix
   260  			}
   261  		}
   262  	}
   263  
   264  	return newStr + TruncationSuffix
   265  }
   266  
   267  // FormatMessage formats a BSON document for logging. The document is truncated
   268  // to the given width.
   269  func FormatMessage(msg string, width uint) string {
   270  	if len(msg) == 0 {
   271  		return "{}"
   272  	}
   273  
   274  	return truncate(msg, width)
   275  }
   276  

View as plain text