// Copyright (C) MongoDB, Inc. 2023-present. // // 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 // Package logger provides the internal logging solution for the MongoDB Go // Driver. package logger import ( "fmt" "os" "strconv" "strings" ) // DefaultMaxDocumentLength is the default maximum number of bytes that can be // logged for a stringified BSON document. const DefaultMaxDocumentLength = 1000 // TruncationSuffix are trailing ellipsis "..." appended to a message to // indicate to the user that truncation occurred. This constant does not count // toward the max document length. const TruncationSuffix = "..." const logSinkPathEnvVar = "MONGODB_LOG_PATH" const maxDocumentLengthEnvVar = "MONGODB_LOG_MAX_DOCUMENT_LENGTH" // LogSink represents a logging implementation, this interface should be 1-1 // with the exported "LogSink" interface in the mongo/options package. type LogSink interface { // Info logs a non-error message with the given key/value pairs. The // level argument is provided for optional logging. Info(level int, msg string, keysAndValues ...interface{}) // Error logs an error, with the given message and key/value pairs. Error(err error, msg string, keysAndValues ...interface{}) } // Logger represents the configuration for the internal logger. type Logger struct { ComponentLevels map[Component]Level // Log levels for each component. Sink LogSink // LogSink for log printing. MaxDocumentLength uint // Command truncation width. logFile *os.File // File to write logs to. } // New will construct a new logger. If any of the given options are the // zero-value of the argument type, then the constructor will attempt to // source the data from the environment. If the environment has not been set, // then the constructor will the respective default values. func New(sink LogSink, maxDocLen uint, compLevels map[Component]Level) (*Logger, error) { logger := &Logger{ ComponentLevels: selectComponentLevels(compLevels), MaxDocumentLength: selectMaxDocumentLength(maxDocLen), } sink, logFile, err := selectLogSink(sink) if err != nil { return nil, err } logger.Sink = sink logger.logFile = logFile return logger, nil } // Close will close the logger's log file, if it exists. func (logger *Logger) Close() error { if logger.logFile != nil { return logger.logFile.Close() } return nil } // LevelComponentEnabled will return true if the given LogLevel is enabled for // the given LogComponent. If the ComponentLevels on the logger are enabled for // "ComponentAll", then this function will return true for any level bound by // the level assigned to "ComponentAll". // // If the level is not enabled (i.e. LevelOff), then false is returned. This is // to avoid false positives, such as returning "true" for a component that is // not enabled. For example, without this condition, an empty LevelComponent // would be considered "enabled" for "LevelOff". func (logger *Logger) LevelComponentEnabled(level Level, component Component) bool { if level == LevelOff { return false } if logger.ComponentLevels == nil { return false } return logger.ComponentLevels[component] >= level || logger.ComponentLevels[ComponentAll] >= level } // Print will synchronously print the given message to the configured LogSink. // If the LogSink is nil, then this method will do nothing. Future work could be done to make // this method asynchronous, see buffer management in libraries such as log4j. // // It's worth noting that many structured logs defined by DBX-wide // specifications include a "message" field, which is often shared with the // message arguments passed to this print function. The "Info" method used by // this function is implemented based on the go-logr/logr LogSink interface, // which is why "Print" has a message parameter. Any duplication in code is // intentional to adhere to the logr pattern. func (logger *Logger) Print(level Level, component Component, msg string, keysAndValues ...interface{}) { // If the level is not enabled for the component, then // skip the message. if !logger.LevelComponentEnabled(level, component) { return } // If the sink is nil, then skip the message. if logger.Sink == nil { return } logger.Sink.Info(int(level)-DiffToInfo, msg, keysAndValues...) } // Error logs an error, with the given message and key/value pairs. // It functions similarly to Print, but may have unique behavior, and should be // preferred for logging errors. func (logger *Logger) Error(err error, msg string, keysAndValues ...interface{}) { if logger.Sink == nil { return } logger.Sink.Error(err, msg, keysAndValues...) } // selectMaxDocumentLength will return the integer value of the first non-zero // function, with the user-defined function taking priority over the environment // variables. For the environment, the function will attempt to get the value of // "MONGODB_LOG_MAX_DOCUMENT_LENGTH" and parse it as an unsigned integer. If the // environment variable is not set or is not an unsigned integer, then this // function will return the default max document length. func selectMaxDocumentLength(maxDocLen uint) uint { if maxDocLen != 0 { return maxDocLen } maxDocLenEnv := os.Getenv(maxDocumentLengthEnvVar) if maxDocLenEnv != "" { maxDocLenEnvInt, err := strconv.ParseUint(maxDocLenEnv, 10, 32) if err == nil { return uint(maxDocLenEnvInt) } } return DefaultMaxDocumentLength } const ( logSinkPathStdout = "stdout" logSinkPathStderr = "stderr" ) // selectLogSink will return the first non-nil LogSink, with the user-defined // LogSink taking precedence over the environment-defined LogSink. If no LogSink // is defined, then this function will return a LogSink that writes to stderr. func selectLogSink(sink LogSink) (LogSink, *os.File, error) { if sink != nil { return sink, nil, nil } path := os.Getenv(logSinkPathEnvVar) lowerPath := strings.ToLower(path) if lowerPath == string(logSinkPathStderr) { return NewIOSink(os.Stderr), nil, nil } if lowerPath == string(logSinkPathStdout) { return NewIOSink(os.Stdout), nil, nil } if path != "" { logFile, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { return nil, nil, fmt.Errorf("unable to open log file: %w", err) } return NewIOSink(logFile), logFile, nil } return NewIOSink(os.Stderr), nil, nil } // selectComponentLevels returns a new map of LogComponents to LogLevels that is // the result of merging the user-defined data with the environment, with the // user-defined data taking priority. func selectComponentLevels(componentLevels map[Component]Level) map[Component]Level { selected := make(map[Component]Level) // Determine if the "MONGODB_LOG_ALL" environment variable is set. var globalEnvLevel *Level if all := os.Getenv(mongoDBLogAllEnvVar); all != "" { level := ParseLevel(all) globalEnvLevel = &level } for envVar, component := range componentEnvVarMap { // If the component already has a level, then skip it. if _, ok := componentLevels[component]; ok { selected[component] = componentLevels[component] continue } // If the "MONGODB_LOG_ALL" environment variable is set, then // set the level for the component to the value of the // environment variable. if globalEnvLevel != nil { selected[component] = *globalEnvLevel continue } // Otherwise, set the level for the component to the value of // the environment variable. selected[component] = ParseLevel(os.Getenv(envVar)) } return selected } // truncate will truncate a string to the given width, appending "..." to the // end of the string if it is truncated. This routine is safe for multi-byte // characters. func truncate(str string, width uint) string { if width == 0 { return "" } if len(str) <= int(width) { return str } // Truncate the byte slice of the string to the given width. newStr := str[:width] // Check if the last byte is at the beginning of a multi-byte character. // If it is, then remove the last byte. if newStr[len(newStr)-1]&0xC0 == 0xC0 { return newStr[:len(newStr)-1] + TruncationSuffix } // Check if the last byte is in the middle of a multi-byte character. If // it is, then step back until we find the beginning of the character. if newStr[len(newStr)-1]&0xC0 == 0x80 { for i := len(newStr) - 1; i >= 0; i-- { if newStr[i]&0xC0 == 0xC0 { return newStr[:i] + TruncationSuffix } } } return newStr + TruncationSuffix } // FormatMessage formats a BSON document for logging. The document is truncated // to the given width. func FormatMessage(msg string, width uint) string { if len(msg) == 0 { return "{}" } return truncate(msg, width) }