github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/log.go (about)

     1  /*
     2   * Copyright (c) 2016, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package server
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	go_log "log"
    28  	"os"
    29  	"sync"
    30  	"sync/atomic"
    31  	"time"
    32  
    33  	"github.com/Psiphon-Inc/rotate-safe-writer"
    34  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    35  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo"
    36  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    37  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/stacktrace"
    38  	"github.com/sirupsen/logrus"
    39  )
    40  
    41  // TraceLogger adds single frame stack trace information to the underlying
    42  // logging facilities.
    43  type TraceLogger struct {
    44  	*logrus.Logger
    45  }
    46  
    47  // LogFields is an alias for the field struct in the underlying logging
    48  // package.
    49  type LogFields logrus.Fields
    50  
    51  // Add copies log fields from b to a, skipping fields which already exist,
    52  // regardless of value, in a.
    53  func (a LogFields) Add(b LogFields) {
    54  	for name, value := range b {
    55  		_, ok := a[name]
    56  		if !ok {
    57  			a[name] = value
    58  		}
    59  	}
    60  }
    61  
    62  // WithTrace adds a "trace" field containing the caller's function name
    63  // and source file line number; and "host_id" and "build_rev" fields
    64  // identifying this server and build. Use this function when the log has no
    65  // fields.
    66  func (logger *TraceLogger) WithTrace() *logrus.Entry {
    67  	return logger.WithFields(
    68  		logrus.Fields{
    69  			"trace":     stacktrace.GetParentFunctionName(),
    70  			"host_id":   logHostID,
    71  			"build_rev": logBuildRev,
    72  		})
    73  }
    74  
    75  func renameLogFields(fields LogFields) {
    76  	if _, ok := fields["trace"]; ok {
    77  		fields["fields.trace"] = fields["trace"]
    78  	}
    79  	if _, ok := fields["host_id"]; ok {
    80  		fields["fields.host_id"] = fields["host_id"]
    81  	}
    82  	if _, ok := fields["build_rev"]; ok {
    83  		fields["fields.build_rev"] = fields["build_rev"]
    84  	}
    85  }
    86  
    87  // WithTraceFields adds a "trace" field containing the caller's function name
    88  // and source file line number; and "host_id" and "build_rev" fields
    89  // identifying this server and build. Use this function when the log has
    90  // fields. Note that any existing "trace"/"host_id"/"build_rev" field will be
    91  // renamed to "field.<name>".
    92  func (logger *TraceLogger) WithTraceFields(fields LogFields) *logrus.Entry {
    93  	renameLogFields(fields)
    94  	fields["trace"] = stacktrace.GetParentFunctionName()
    95  	fields["host_id"] = logHostID
    96  	fields["build_rev"] = logBuildRev
    97  	return logger.WithFields(logrus.Fields(fields))
    98  }
    99  
   100  // LogRawFieldsWithTimestamp directly logs the supplied fields adding only
   101  // an additional "timestamp" field; and "host_id" and "build_rev" fields
   102  // identifying this server and build. The stock "msg" and "level" fields are
   103  // omitted. This log is emitted at the Error level. This function exists to
   104  // support API logs which have neither a natural message nor severity; and
   105  // omitting these values here makes it easier to ship these logs to existing
   106  // API log consumers.
   107  // Note that any existing "trace"/"host_id"/"build_rev" field will be renamed
   108  // to "field.<name>".
   109  func (logger *TraceLogger) LogRawFieldsWithTimestamp(fields LogFields) {
   110  	renameLogFields(fields)
   111  	fields["host_id"] = logHostID
   112  	fields["build_rev"] = logBuildRev
   113  	logger.WithFields(logrus.Fields(fields)).Error(
   114  		customJSONFormatterLogRawFieldsWithTimestamp)
   115  }
   116  
   117  // LogPanicRecover calls LogRawFieldsWithTimestamp with standard fields
   118  // for logging recovered panics.
   119  func (logger *TraceLogger) LogPanicRecover(recoverValue interface{}, stack []byte) {
   120  	log.LogRawFieldsWithTimestamp(
   121  		LogFields{
   122  			"event_name":    "panic",
   123  			"recover_value": recoverValue,
   124  			"stack":         string(stack),
   125  		})
   126  }
   127  
   128  type commonLogger struct {
   129  	traceLogger *TraceLogger
   130  }
   131  
   132  func (logger *commonLogger) WithTrace() common.LogTrace {
   133  	// Patch trace to be correct parent
   134  	return logger.traceLogger.WithTrace().WithField(
   135  		"trace", stacktrace.GetParentFunctionName())
   136  }
   137  
   138  func (logger *commonLogger) WithTraceFields(fields common.LogFields) common.LogTrace {
   139  	// Patch trace to be correct parent
   140  	return logger.traceLogger.WithTraceFields(LogFields(fields)).WithField(
   141  		"trace", stacktrace.GetParentFunctionName())
   142  }
   143  
   144  func (logger *commonLogger) LogMetric(metric string, fields common.LogFields) {
   145  	fields["event_name"] = metric
   146  	logger.traceLogger.LogRawFieldsWithTimestamp(LogFields(fields))
   147  }
   148  
   149  // CommonLogger wraps a TraceLogger instance with an interface that conforms
   150  // to common.Logger. This is used to make the TraceLogger available to other
   151  // packages that don't import the "server" package.
   152  func CommonLogger(traceLogger *TraceLogger) *commonLogger {
   153  	return &commonLogger{
   154  		traceLogger: traceLogger,
   155  	}
   156  }
   157  
   158  // NewLogWriter returns an io.PipeWriter that can be used to write
   159  // to the global logger. Caller must Close() the writer.
   160  func NewLogWriter() *io.PipeWriter {
   161  	return log.Writer()
   162  }
   163  
   164  // CustomJSONFormatter is a customized version of logrus.JSONFormatter
   165  type CustomJSONFormatter struct {
   166  }
   167  
   168  var (
   169  	useLogCallback int32
   170  	logCallback    atomic.Value
   171  )
   172  
   173  // setLogCallback sets a callback that is invoked with each JSON log message.
   174  // This facility is intended for use in testing only.
   175  func setLogCallback(callback func([]byte)) {
   176  	if callback == nil {
   177  		atomic.StoreInt32(&useLogCallback, 0)
   178  		return
   179  	}
   180  	atomic.StoreInt32(&useLogCallback, 1)
   181  	logCallback.Store(callback)
   182  }
   183  
   184  const customJSONFormatterLogRawFieldsWithTimestamp = "CustomJSONFormatter.LogRawFieldsWithTimestamp"
   185  
   186  // Format implements logrus.Formatter. This is a customized version
   187  // of the standard logrus.JSONFormatter adapted from:
   188  // https://github.com/Sirupsen/logrus/blob/f1addc29722ba9f7651bc42b4198d0944b66e7c4/json_formatter.go
   189  //
   190  // The changes are:
   191  // - "time" is renamed to "timestamp"
   192  // - there's an option to omit the standard "msg" and "level" fields
   193  //
   194  func (f *CustomJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   195  	data := make(logrus.Fields, len(entry.Data)+3)
   196  	for k, v := range entry.Data {
   197  		switch v := v.(type) {
   198  		case error:
   199  			// Otherwise errors are ignored by `encoding/json`
   200  			// https://github.com/Sirupsen/logrus/issues/137
   201  			data[k] = v.Error()
   202  		default:
   203  			data[k] = v
   204  		}
   205  	}
   206  
   207  	if t, ok := data["timestamp"]; ok {
   208  		data["fields.timestamp"] = t
   209  	}
   210  
   211  	data["timestamp"] = entry.Time.Format(time.RFC3339)
   212  
   213  	if entry.Message != customJSONFormatterLogRawFieldsWithTimestamp {
   214  
   215  		if m, ok := data["msg"]; ok {
   216  			data["fields.msg"] = m
   217  		}
   218  
   219  		if l, ok := data["level"]; ok {
   220  			data["fields.level"] = l
   221  		}
   222  
   223  		data["msg"] = entry.Message
   224  		data["level"] = entry.Level.String()
   225  	}
   226  
   227  	serialized, err := json.Marshal(data)
   228  	if err != nil {
   229  		return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err)
   230  	}
   231  
   232  	if atomic.LoadInt32(&useLogCallback) == 1 {
   233  		logCallback.Load().(func([]byte))(serialized)
   234  	}
   235  
   236  	return append(serialized, '\n'), nil
   237  }
   238  
   239  var log *TraceLogger
   240  var logHostID, logBuildRev string
   241  var initLogging sync.Once
   242  
   243  // InitLogging configures a logger according to the specified
   244  // config params. If not called, the default logger set by the
   245  // package init() is used.
   246  // Concurrency notes: this should only be called from the main
   247  // goroutine; InitLogging only has effect on the first call, as
   248  // the logging facilities it initializes may be in use by other
   249  // goroutines after that point.
   250  func InitLogging(config *Config) (retErr error) {
   251  
   252  	initLogging.Do(func() {
   253  
   254  		logHostID = config.HostID
   255  		logBuildRev = buildinfo.GetBuildInfo().BuildRev
   256  
   257  		level, err := logrus.ParseLevel(config.LogLevel)
   258  		if err != nil {
   259  			retErr = errors.Trace(err)
   260  			return
   261  		}
   262  
   263  		var logWriter io.Writer
   264  
   265  		if config.LogFilename != "" {
   266  
   267  			retries, create, mode := config.GetLogFileReopenConfig()
   268  			logWriter, err = rotate.NewRotatableFileWriter(
   269  				config.LogFilename, retries, create, mode)
   270  			if err != nil {
   271  				retErr = errors.Trace(err)
   272  				return
   273  			}
   274  
   275  			if !config.SkipPanickingLogWriter {
   276  
   277  				// Use PanickingLogWriter, which will intentionally
   278  				// panic when a Write fails. Set SkipPanickingLogWriter
   279  				// if this behavior is not desired.
   280  				//
   281  				// Note that NewRotatableFileWriter will first attempt
   282  				// a retry when a Write fails.
   283  				//
   284  				// It is assumed that continuing operation while unable
   285  				// to log is unacceptable; and that the psiphond service
   286  				// is managed and will restart when it terminates.
   287  				//
   288  				// It is further assumed that panicking will result in
   289  				// an error that is externally logged and reported to a
   290  				// monitoring system.
   291  				//
   292  				// TODO: An orderly shutdown may be preferred, as some
   293  				// data will be lost in a panic (e.g., server_tunnel logs).
   294  				// It may be possible to perform an orderly shutdown first
   295  				// and then panic, or perform an orderly shutdown and
   296  				// simulate a panic message that will be reported.
   297  
   298  				logWriter = NewPanickingLogWriter(config.LogFilename, logWriter)
   299  			}
   300  
   301  		} else {
   302  			logWriter = os.Stderr
   303  		}
   304  
   305  		log = &TraceLogger{
   306  			&logrus.Logger{
   307  				Out:       logWriter,
   308  				Formatter: &CustomJSONFormatter{},
   309  				Level:     level,
   310  			},
   311  		}
   312  	})
   313  
   314  	return retErr
   315  }
   316  
   317  func IsLogLevelDebug() bool {
   318  	return log.Logger.Level == logrus.DebugLevel
   319  }
   320  
   321  func init() {
   322  
   323  	// Suppress standard "log" package logging performed by other packages.
   324  	// For example, "net/http" logs messages such as:
   325  	// "http: TLS handshake error from <client-ip-addr>:<port>: [...]: i/o timeout"
   326  	go_log.SetOutput(ioutil.Discard)
   327  
   328  	log = &TraceLogger{
   329  		&logrus.Logger{
   330  			Out:       os.Stderr,
   331  			Formatter: &CustomJSONFormatter{},
   332  			Hooks:     make(logrus.LevelHooks),
   333  			Level:     logrus.DebugLevel,
   334  		},
   335  	}
   336  }