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