internal/util/logging.go (153 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package util import ( "fmt" "net" "net/http" "os" "strconv" "github.com/felixge/httpsnoop" "github.com/gorilla/mux" "go.elastic.co/apm/module/apmzap/v2" "go.elastic.co/apm/v2" "go.elastic.co/ecszap" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Types of available loggers. const ( ECSLogger = "ecs" DevLogger = "dev" DefaultLoggerType = ECSLogger DefaultLoggerLevel = zapcore.InfoLevel ) type LoggerOptions struct { Type string Level *zapcore.Level APMTracer *apm.Tracer } func NewLogger(options LoggerOptions) (*zap.Logger, error) { if options.Type == "" { options.Type = DefaultLoggerType } if options.Level == nil { level := DefaultLoggerLevel options.Level = &level } core, err := newLoggerCore(options) if err != nil { return nil, err } if options.APMTracer != nil { apmCore := apmzap.Core{ Tracer: options.APMTracer, } core = apmCore.WrapCore(core) } return zap.New(core, zap.AddCaller()), nil } func NewTestLogger() *zap.Logger { return NewTestLoggerLevel(zap.DebugLevel) } func NewTestLoggerLevel(level zapcore.Level) *zap.Logger { logger, err := NewLogger(LoggerOptions{ Type: DevLogger, Level: &level, }) if err != nil { panic("failed to initialize logger") } return logger } func newLoggerCore(options LoggerOptions) (zapcore.Core, error) { switch options.Type { case ECSLogger: encoderConfig := ecszap.NewDefaultEncoderConfig() return ecszap.NewCore(encoderConfig, os.Stderr, *options.Level), nil case DevLogger: encoderConfig := zap.NewDevelopmentEncoderConfig() encoder := zapcore.NewConsoleEncoder(encoderConfig) return zapcore.NewCore(encoder, os.Stderr, *options.Level), nil } return nil, fmt.Errorf("invalid logger type %q", options.Type) } // LoggingMiddleware is a middleware used to log requests to the given logger. func LoggingMiddleware(logger *zap.Logger) mux.MiddlewareFunc { // Disable logging of the file and number of the caller, because it will be the // one of the helper. logger = logger.Named("http").WithOptions(zap.WithCaller(false)) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/health": // Do not log requests to these endpoints next.ServeHTTP(w, r) default: logRequest(logger, next, w, r) } }) } } // logRequest captures information from a handler handling a request, and generates logs // using this information. func logRequest(logger *zap.Logger, handler http.Handler, w http.ResponseWriter, req *http.Request) { message, fields := captureZapFieldsForRequest(handler, w, req) logger.Info(message, fields...) } // captureZapFieldsForRequest handles a request and captures fields for zap logger. func captureZapFieldsForRequest(handler http.Handler, w http.ResponseWriter, req *http.Request) (string, []zap.Field) { resp := httpsnoop.CaptureMetrics(handler, w, req) domain, port, err := net.SplitHostPort(req.Host) if err != nil { domain = req.Host } if ip := net.ParseIP(domain); ip != nil && ip.To16() != nil && ip.To4() == nil { // For ECS, if the host part of an url is an IPv6, it must keep the brackets // when stored in `url.domain` (but not when stored in ip fields). domain = "[" + domain + "]" } sourceHost, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { sourceHost = req.RemoteAddr } fields := []zap.Field{ // Request fields. zap.String("source.address", sourceHost), zap.String("http.request.method", req.Method), zap.String("url.path", req.URL.Path), zap.String("url.domain", domain), // Response fields. zap.Int("http.response.code", resp.Code), zap.Int64("http.response.body.bytes", resp.Written), zap.Int64("event.duration", resp.Duration.Nanoseconds()), } // Fields that are not always available. if ip := net.ParseIP(sourceHost); ip != nil { fields = append(fields, zap.String("source.ip", sourceHost)) } else { fields = append(fields, zap.String("source.domain", sourceHost)) } if referer := req.Referer(); referer != "" { fields = append(fields, zap.String("http.request.referer", referer)) } if userAgent := req.UserAgent(); userAgent != "" { fields = append(fields, zap.String("user_agent.original", userAgent)) } if query := req.URL.RawQuery; query != "" { fields = append(fields, zap.String("url.query", query)) } if port != "" { if intPort, err := strconv.Atoi(port); err == nil && intPort != 0 { fields = append(fields, zap.Int("url.port", intPort)) } } message := req.Method + " " + req.URL.Path + " " + req.Proto return message, fields } // LoggerAdapter adapts a zap logger so it can be used as logger of other features as APM. type LoggerAdapter struct { *zap.Logger } // Debugf logs a message at debug level. func (a *LoggerAdapter) Debugf(format string, args ...interface{}) { if a.Logger.Level() > zapcore.DebugLevel { return } a.Logger.Debug(fmt.Sprintf(format, args...)) } // Errorf logs a message at error level. func (a *LoggerAdapter) Errorf(format string, args ...interface{}) { if a.Logger.Level() > zapcore.ErrorLevel { return } a.Logger.Error(fmt.Sprintf(format, args...)) } // Warningf logs a message at warning level. func (a *LoggerAdapter) Warningf(format string, args ...interface{}) { if a.Logger.Level() > zapcore.WarnLevel { return } a.Logger.Warn(fmt.Sprintf(format, args...)) }