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  }