github.com/google/cloudprober@v0.11.3/logger/logger.go (about) 1 // Copyright 2017-2020 The Cloudprober Authors. 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 logger provides a logger that logs to Google Cloud Logging. It's a thin wrapper around 16 // golang/cloud/logging package. 17 package logger 18 19 import ( 20 "context" 21 "fmt" 22 "net/url" 23 "os" 24 "regexp" 25 "strings" 26 "time" 27 28 "cloud.google.com/go/compute/metadata" 29 "cloud.google.com/go/logging" 30 "flag" 31 "github.com/golang/glog" 32 "golang.org/x/oauth2/google" 33 "google.golang.org/api/option" 34 ) 35 36 var ( 37 debugLog = flag.Bool("debug_log", false, "Whether to output debug logs or not") 38 debugLogList = flag.String("debug_logname_regex", "", "Enable debug logs for only for log names that match this regex (e.g. --debug_logname_regex=.*probe1.*") 39 40 // Enable/Disable cloud logging 41 disableCloudLogging = flag.Bool("disable_cloud_logging", false, "Disable cloud logging.") 42 43 // LogPrefixEnvVar environment variable is used to determine the stackdriver 44 // log name prefix. Default prefix is "cloudprober". 45 LogPrefixEnvVar = "CLOUDPROBER_LOG_PREFIX" 46 ) 47 48 // EnvVars defines environment variables that can be used to modify the logging 49 // behavior. 50 var EnvVars = struct { 51 DisableCloudLogging, DebugLog string 52 }{ 53 "CLOUDPROBER_DISABLE_CLOUD_LOGGING", 54 "CLOUDPROBER_DEBUG_LOG", 55 } 56 57 const ( 58 // Prefix for the cloudprober stackdriver log names. 59 cloudproberPrefix = "cloudprober" 60 ) 61 62 const ( 63 // Regular Expression for all characters that are illegal for log names 64 // Ref: https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry 65 disapprovedRegExp = "[^A-Za-z0-9_/.-]" 66 67 // MaxLogEntrySize Max size of each log entry (100 KB) 68 // This limit helps avoid creating very large log lines in case someone 69 // accidentally creates a large EventMetric, which in turn is possible due to 70 // unbounded nature of "map" metric where keys are created on demand. 71 // 72 // TODO(manugarg): We can possibly get rid of this check now as the code that 73 // could cause a large map metric has been fixed now. Earlier, cloudprober's 74 // HTTP server used to respond to all URLs and used to record access to those 75 // URLs as a "map" metric. Now, it responds only to pre-configured URLs. 76 MaxLogEntrySize = 102400 77 ) 78 79 func enableDebugLog(debugLog bool, debugLogRe string, logName string) bool { 80 if !debugLog && debugLogRe == "" { 81 return false 82 } 83 84 if debugLog && debugLogRe == "" { 85 // Enable for all logs, regardless of log names. 86 return true 87 } 88 89 r, err := regexp.Compile(debugLogRe) 90 if err != nil { 91 panic(fmt.Sprintf("error while parsing log name regex (%s): %v", debugLogRe, err)) 92 } 93 94 if r.MatchString(logName) { 95 return true 96 } 97 98 return false 99 } 100 101 // Logger implements a logger that logs messages to Google Cloud Logging. It 102 // provides a suite of methods where each method corresponds to a specific 103 // logging.Level, e.g. Error(paylod interface{}). Each method takes a payload 104 // that has to either be a JSON-encodable object, a string or a []byte slice 105 // (all other types of payload will result in error). 106 // 107 // It falls back to logging through the traditional logger if: 108 // 109 // * Not running on GCE, 110 // * Logging client is uninitialized (e.g. for testing), 111 // * Logging to cloud fails for some reason. 112 // 113 // Logger{} is a valid object that will log through the traditional logger. 114 // 115 type Logger struct { 116 name string 117 logc *logging.Client 118 logger *logging.Logger 119 debugLog bool 120 disableCloudLogging bool 121 // TODO(manugarg): Logger should eventually embed the probe id and each probe 122 // should get a different Logger object (embedding that probe's probe id) but 123 // sharing the same logging client. We could then make probe id one of the 124 // metadata on all logging messages. 125 } 126 127 // NewCloudproberLog is a convenient wrapper around New that sets context to 128 // context.Background and attaches cloudprober prefix to log names. 129 func NewCloudproberLog(component string) (*Logger, error) { 130 cpPrefix := cloudproberPrefix 131 132 envLogPrefix := os.Getenv(LogPrefixEnvVar) 133 if envLogPrefix != "" { 134 cpPrefix = envLogPrefix 135 } 136 137 return New(context.Background(), cpPrefix+"."+component) 138 } 139 140 // New returns a new Logger object with cloud logging client initialized if running on GCE. 141 func New(ctx context.Context, logName string) (*Logger, error) { 142 l := &Logger{ 143 name: logName, 144 debugLog: enableDebugLog(*debugLog, *debugLogList, logName), 145 disableCloudLogging: *disableCloudLogging, 146 } 147 148 if !metadata.OnGCE() || l.disableCloudLogging { 149 return l, nil 150 } 151 152 l.Infof("Running on GCE. Logs for %s will go to Cloud (Stackdriver).", logName) 153 if err := l.EnableStackdriverLogging(ctx); err != nil { 154 return nil, err 155 } 156 return l, nil 157 } 158 159 // EnableStackdriverLogging enables logging to stackdriver. 160 func (l *Logger) EnableStackdriverLogging(ctx context.Context) error { 161 if !metadata.OnGCE() { 162 return fmt.Errorf("not running on GCE") 163 } 164 165 projectID, err := metadata.ProjectID() 166 if err != nil { 167 return err 168 } 169 170 if l.name == "" { 171 return fmt.Errorf("logName cannot be empty") 172 } 173 // Check for illegal characters in the log name 174 if match, err := regexp.Match(disapprovedRegExp, []byte(l.name)); err != nil || match { 175 if err != nil { 176 return fmt.Errorf("unable to parse logName: %v", err) 177 } 178 return fmt.Errorf("logName of %s contains an invalid character, valid characters are [A-Za-z0-9_/.-]", l.name) 179 } 180 // Any forward slashes need to be URL encoded, so we query escape to replace them 181 logName := url.QueryEscape(l.name) 182 183 l.logc, err = logging.NewClient(ctx, projectID, option.WithTokenSource(google.ComputeTokenSource(""))) 184 if err != nil { 185 return err 186 } 187 188 loggerOpts := []logging.LoggerOption{ 189 // Encourage batching of write requests. 190 // Flush logs to remote logging after 1000 entries (default is 10). 191 logging.EntryCountThreshold(1000), 192 // Maximum amount of time that an item should remain buffered in memory 193 // before being flushed to the logging service. Default is 1 second. 194 // We want flushing to be mostly driven by the buffer size (configured 195 // above), rather than time. 196 logging.DelayThreshold(10 * time.Second), 197 } 198 199 // Add instance_name to common labels if available. 200 instanceName, err := metadata.InstanceName() 201 if err != nil { 202 l.Infof("Error getting instance name on GCE. Possibly running on GKE: %v", err) 203 } else { 204 loggerOpts = append(loggerOpts, logging.CommonLabels(map[string]string{"instance_name": instanceName})) 205 } 206 207 l.logger = l.logc.Logger(logName, loggerOpts...) 208 return nil 209 } 210 211 func payloadToString(payload ...string) string { 212 if len(payload) == 1 { 213 return payload[0] 214 } 215 216 var b strings.Builder 217 for _, s := range payload { 218 b.WriteString(s) 219 } 220 return b.String() 221 } 222 223 // log sends payload ([]string) to cloud logging. If cloud logging client is 224 // not initialized (e.g. if not running on GCE) or cloud logging fails for some 225 // reason, it writes logs through the traditional logger. 226 func (l *Logger) log(severity logging.Severity, payload ...string) { 227 payloadStr := payloadToString(payload...) 228 229 if len(payloadStr) > MaxLogEntrySize { 230 truncateMsg := "... (truncated)" 231 truncateMsgLen := len(truncateMsg) 232 payloadStr = payloadStr[:MaxLogEntrySize-truncateMsgLen] + truncateMsg 233 } 234 235 if l == nil { 236 genericLog(severity, "nil", payloadStr) 237 return 238 } 239 240 if l.logger == nil { 241 genericLog(severity, l.name, payloadStr) 242 return 243 } 244 245 l.logger.Log(logging.Entry{ 246 Severity: severity, 247 Payload: payloadStr, 248 }) 249 } 250 251 // Close closes the cloud logging client if it exists. This flushes the buffer 252 // and should be called before exiting the program to ensure all logs are persisted. 253 func (l *Logger) Close() error { 254 if l != nil && l.logc != nil { 255 return l.logc.Close() 256 } 257 258 return nil 259 } 260 261 // Debug logs messages with logging level set to "Debug". 262 func (l *Logger) Debug(payload ...string) { 263 if l.debugLog { 264 l.log(logging.Debug, payload...) 265 } 266 } 267 268 // Info logs messages with logging level set to "Info". 269 func (l *Logger) Info(payload ...string) { 270 l.log(logging.Info, payload...) 271 } 272 273 // Warning logs messages with logging level set to "Warning". 274 func (l *Logger) Warning(payload ...string) { 275 l.log(logging.Warning, payload...) 276 } 277 278 // Error logs messages with logging level set to "Error". 279 func (l *Logger) Error(payload ...string) { 280 l.log(logging.Error, payload...) 281 } 282 283 // Critical logs messages with logging level set to "Critical" and 284 // exits the process with error status. The buffer is flushed before exiting. 285 func (l *Logger) Critical(payload ...string) { 286 l.log(logging.Critical, payload...) 287 if err := l.Close(); err != nil { 288 panic(fmt.Sprintf("could not close client: %v", err)) 289 } 290 os.Exit(1) 291 } 292 293 // Debugf logs formatted text messages with logging level "Debug". 294 func (l *Logger) Debugf(format string, args ...interface{}) { 295 if l != nil && l.debugLog { 296 l.log(logging.Debug, fmt.Sprintf(format, args...)) 297 } 298 } 299 300 // Infof logs formatted text messages with logging level "Info". 301 func (l *Logger) Infof(format string, args ...interface{}) { 302 l.log(logging.Info, fmt.Sprintf(format, args...)) 303 } 304 305 // Warningf logs formatted text messages with logging level "Warning". 306 func (l *Logger) Warningf(format string, args ...interface{}) { 307 l.log(logging.Warning, fmt.Sprintf(format, args...)) 308 } 309 310 // Errorf logs formatted text messages with logging level "Error". 311 func (l *Logger) Errorf(format string, args ...interface{}) { 312 l.log(logging.Error, fmt.Sprintf(format, args...)) 313 } 314 315 // Criticalf logs formatted text messages with logging level "Critical" and 316 // exits the process with error status. The buffer is flushed before exiting. 317 func (l *Logger) Criticalf(format string, args ...interface{}) { 318 l.log(logging.Critical, fmt.Sprintf(format, args...)) 319 if err := l.Close(); err != nil { 320 panic(fmt.Sprintf("could not close client: %v", err)) 321 } 322 os.Exit(1) 323 } 324 325 func genericLog(severity logging.Severity, name string, s string) { 326 // Set the caller frame depth to 3 so that can get to the actual caller of 327 // the logger. genericLog -> log -> Info* -> actualCaller 328 depth := 3 329 330 s = fmt.Sprintf("[%s] %s", name, s) 331 332 switch severity { 333 case logging.Debug, logging.Info: 334 glog.InfoDepth(depth, s) 335 case logging.Warning: 336 glog.WarningDepth(depth, s) 337 case logging.Error: 338 glog.ErrorDepth(depth, s) 339 case logging.Critical: 340 glog.FatalDepth(depth, s) 341 } 342 } 343 344 func envVarSet(key string) bool { 345 v, ok := os.LookupEnv(key) 346 if ok && strings.ToUpper(v) != "NO" && strings.ToUpper(v) != "FALSE" { 347 return true 348 } 349 return false 350 } 351 352 func init() { 353 if envVarSet(EnvVars.DisableCloudLogging) { 354 *disableCloudLogging = true 355 } 356 357 if envVarSet(EnvVars.DebugLog) { 358 *debugLog = true 359 } 360 }