2017-05-09 14:02:44 +02:00
package accesslog
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
2017-05-22 20:39:29 +01:00
"os"
"path/filepath"
2017-09-15 15:02:03 +02:00
"sync"
2017-05-09 14:02:44 +02:00
"sync/atomic"
"time"
2017-05-22 20:39:29 +01:00
2018-11-14 10:18:03 +01:00
"github.com/containous/alice"
2019-08-03 03:58:23 +02:00
"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/types"
2018-01-22 12:16:03 +01:00
"github.com/sirupsen/logrus"
2017-05-09 14:02:44 +02:00
)
type key string
const (
2018-11-14 10:18:03 +01:00
// DataTableKey is the key within the request context used to store the Log Data Table.
2017-05-09 14:02:44 +02:00
DataTableKey key = "LogDataTable"
2017-05-25 12:25:53 +01:00
2018-11-14 10:18:03 +01:00
// CommonFormat is the common logging format (CLF).
2018-08-06 20:00:03 +02:00
CommonFormat string = "common"
2017-05-25 12:25:53 +01:00
2018-11-14 10:18:03 +01:00
// JSONFormat is the JSON logging format.
2018-08-06 20:00:03 +02:00
JSONFormat string = "json"
2017-05-09 14:02:44 +02:00
)
2018-11-14 10:18:03 +01:00
type handlerParams struct {
2018-05-23 16:46:04 +02:00
logDataTable * LogData
crr * captureRequestReader
crw * captureResponseWriter
}
2018-11-14 10:18:03 +01:00
// Handler will write each request and its response to the access log.
type Handler struct {
2018-03-23 09:28:03 +01:00
config * types . AccessLog
2018-03-14 14:12:04 +01:00
logger * logrus . Logger
file * os . File
mu sync . Mutex
httpCodeRanges types . HTTPCodeRanges
2018-11-14 10:18:03 +01:00
logHandlerChan chan handlerParams
2018-05-23 16:46:04 +02:00
wg sync . WaitGroup
2017-05-09 14:02:44 +02:00
}
2018-11-14 10:18:03 +01:00
// WrapHandler Wraps access log handler into an Alice Constructor.
func WrapHandler ( handler * Handler ) alice . Constructor {
return func ( next http . Handler ) ( http . Handler , error ) {
return http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
2019-03-18 11:30:07 +01:00
handler . ServeHTTP ( rw , req , next )
2018-11-14 10:18:03 +01:00
} ) , nil
}
}
// NewHandler creates a new Handler.
func NewHandler ( config * types . AccessLog ) ( * Handler , error ) {
2017-05-30 12:06:49 +02:00
file := os . Stdout
if len ( config . FilePath ) > 0 {
f , err := openAccessLogFile ( config . FilePath )
if err != nil {
return nil , fmt . Errorf ( "error opening access log file: %s" , err )
}
file = f
2017-05-22 20:39:29 +01:00
}
2018-11-14 10:18:03 +01:00
logHandlerChan := make ( chan handlerParams , config . BufferingSize )
2017-05-22 20:39:29 +01:00
2017-05-25 12:25:53 +01:00
var formatter logrus . Formatter
switch config . Format {
case CommonFormat :
formatter = new ( CommonLogFormatter )
case JSONFormat :
formatter = new ( logrus . JSONFormatter )
default :
2019-09-09 09:24:03 +02:00
log . WithoutContext ( ) . Errorf ( "unsupported access log format: %q, defaulting to common format instead." , config . Format )
formatter = new ( CommonLogFormatter )
2017-05-25 12:25:53 +01:00
}
2017-05-22 20:39:29 +01:00
logger := & logrus . Logger {
Out : file ,
2017-05-25 12:25:53 +01:00
Formatter : formatter ,
2017-05-22 20:39:29 +01:00
Hooks : make ( logrus . LevelHooks ) ,
Level : logrus . InfoLevel ,
}
2018-03-14 14:12:04 +01:00
2018-11-14 10:18:03 +01:00
logHandler := & Handler {
2018-05-23 16:46:04 +02:00
config : config ,
logger : logger ,
file : file ,
logHandlerChan : logHandlerChan ,
2018-03-14 14:12:04 +01:00
}
if config . Filters != nil {
2018-03-23 09:28:03 +01:00
if httpCodeRanges , err := types . NewHTTPCodeRanges ( config . Filters . StatusCodes ) ; err != nil {
2018-11-14 10:18:03 +01:00
log . WithoutContext ( ) . Errorf ( "Failed to create new HTTP code ranges: %s" , err )
2018-03-23 09:28:03 +01:00
} else {
2018-03-14 14:12:04 +01:00
logHandler . httpCodeRanges = httpCodeRanges
}
}
2018-05-23 16:46:04 +02:00
if config . BufferingSize > 0 {
logHandler . wg . Add ( 1 )
go func ( ) {
defer logHandler . wg . Done ( )
for handlerParams := range logHandler . logHandlerChan {
logHandler . logTheRoundTrip ( handlerParams . logDataTable , handlerParams . crr , handlerParams . crw )
}
} ( )
}
2018-03-14 14:12:04 +01:00
return logHandler , nil
2017-05-09 14:02:44 +02:00
}
2017-05-30 12:06:49 +02:00
func openAccessLogFile ( filePath string ) ( * os . File , error ) {
dir := filepath . Dir ( filePath )
if err := os . MkdirAll ( dir , 0755 ) ; err != nil {
return nil , fmt . Errorf ( "failed to create log path %s: %s" , dir , err )
}
file , err := os . OpenFile ( filePath , os . O_RDWR | os . O_CREATE | os . O_APPEND , 0664 )
if err != nil {
return nil , fmt . Errorf ( "error opening file %s: %s" , filePath , err )
}
return file , nil
}
2018-11-14 10:18:03 +01:00
// GetLogData gets the request context object that contains logging data.
2018-05-14 10:38:03 +02:00
// This creates data as the request passes through the middleware chain.
2018-11-14 10:18:03 +01:00
func GetLogData ( req * http . Request ) * LogData {
2018-05-14 10:38:03 +02:00
if ld , ok := req . Context ( ) . Value ( DataTableKey ) . ( * LogData ) ; ok {
return ld
}
2018-11-14 10:18:03 +01:00
return nil
2017-05-09 14:02:44 +02:00
}
2019-03-18 11:30:07 +01:00
func ( h * Handler ) ServeHTTP ( rw http . ResponseWriter , req * http . Request , next http . Handler ) {
2017-05-09 14:02:44 +02:00
now := time . Now ( ) . UTC ( )
2018-05-14 10:38:03 +02:00
core := CoreLogData {
StartUTC : now ,
StartLocal : now . Local ( ) ,
}
2017-05-09 14:02:44 +02:00
logDataTable := & LogData { Core : core , Request : req . Header }
reqWithDataTable := req . WithContext ( context . WithValue ( req . Context ( ) , DataTableKey , logDataTable ) )
var crr * captureRequestReader
if req . Body != nil {
crr = & captureRequestReader { source : req . Body , count : 0 }
reqWithDataTable . Body = crr
}
core [ RequestCount ] = nextRequestCount ( )
if req . Host != "" {
core [ RequestAddr ] = req . Host
core [ RequestHost ] , core [ RequestPort ] = silentSplitHostPort ( req . Host )
}
// copy the URL without the scheme, hostname etc
urlCopy := & url . URL {
Path : req . URL . Path ,
RawPath : req . URL . RawPath ,
RawQuery : req . URL . RawQuery ,
ForceQuery : req . URL . ForceQuery ,
Fragment : req . URL . Fragment ,
}
urlCopyString := urlCopy . String ( )
core [ RequestMethod ] = req . Method
core [ RequestPath ] = urlCopyString
core [ RequestProtocol ] = req . Proto
core [ ClientAddr ] = req . RemoteAddr
core [ ClientHost ] , core [ ClientPort ] = silentSplitHostPort ( req . RemoteAddr )
2017-08-25 13:00:03 +02:00
if forwardedFor := req . Header . Get ( "X-Forwarded-For" ) ; forwardedFor != "" {
core [ ClientHost ] = forwardedFor
}
2017-05-09 14:02:44 +02:00
crw := & captureResponseWriter { rw : rw }
next . ServeHTTP ( crw , reqWithDataTable )
2019-01-18 15:18:04 +01:00
if _ , ok := core [ ClientUsername ] ; ! ok {
core [ ClientUsername ] = usernameIfPresent ( reqWithDataTable . URL )
}
2018-01-24 18:18:03 +01:00
2017-05-09 14:02:44 +02:00
logDataTable . DownstreamResponse = crw . Header ( )
2018-05-23 16:46:04 +02:00
2018-11-14 10:18:03 +01:00
if h . config . BufferingSize > 0 {
h . logHandlerChan <- handlerParams {
2018-05-23 16:46:04 +02:00
logDataTable : logDataTable ,
crr : crr ,
crw : crw ,
}
} else {
2018-11-14 10:18:03 +01:00
h . logTheRoundTrip ( logDataTable , crr , crw )
2018-05-23 16:46:04 +02:00
}
2017-05-09 14:02:44 +02:00
}
2018-05-23 16:46:04 +02:00
// Close closes the Logger (i.e. the file, drain logHandlerChan, etc).
2018-11-14 10:18:03 +01:00
func ( h * Handler ) Close ( ) error {
close ( h . logHandlerChan )
h . wg . Wait ( )
return h . file . Close ( )
2017-05-09 14:02:44 +02:00
}
2018-11-14 10:18:03 +01:00
// Rotate closes and reopens the log file to allow for rotation by an external source.
func ( h * Handler ) Rotate ( ) error {
2017-08-11 11:04:58 +01:00
var err error
2017-09-15 15:02:03 +02:00
2018-11-14 10:18:03 +01:00
if h . file != nil {
2017-09-15 15:02:03 +02:00
defer func ( f * os . File ) {
f . Close ( )
2018-11-14 10:18:03 +01:00
} ( h . file )
2017-08-11 11:04:58 +01:00
}
2018-11-14 10:18:03 +01:00
h . file , err = os . OpenFile ( h . config . FilePath , os . O_RDWR | os . O_CREATE | os . O_APPEND , 0664 )
2017-08-11 11:04:58 +01:00
if err != nil {
return err
}
2018-11-14 10:18:03 +01:00
h . mu . Lock ( )
defer h . mu . Unlock ( )
h . logger . Out = h . file
2017-08-11 11:04:58 +01:00
return nil
}
2017-05-09 14:02:44 +02:00
func silentSplitHostPort ( value string ) ( host string , port string ) {
host , port , err := net . SplitHostPort ( value )
if err != nil {
return value , "-"
}
return host , port
}
2018-11-14 10:18:03 +01:00
func usernameIfPresent ( theURL * url . URL ) string {
if theURL . User != nil {
if name := theURL . User . Username ( ) ; name != "" {
return name
}
2017-05-09 14:02:44 +02:00
}
2018-10-25 18:00:05 +02:00
return "-"
2017-05-09 14:02:44 +02:00
}
2018-11-14 10:18:03 +01:00
// Logging handler to log frontend name, backend name, and elapsed time.
func ( h * Handler ) logTheRoundTrip ( logDataTable * LogData , crr * captureRequestReader , crw * captureResponseWriter ) {
2017-05-09 14:02:44 +02:00
core := logDataTable . Core
2018-03-23 09:28:03 +01:00
retryAttempts , ok := core [ RetryAttempts ] . ( int )
if ! ok {
retryAttempts = 0
2017-08-28 12:50:02 +02:00
}
2018-03-23 09:28:03 +01:00
core [ RetryAttempts ] = retryAttempts
2017-05-09 14:02:44 +02:00
if crr != nil {
core [ RequestContentSize ] = crr . count
}
core [ DownstreamStatus ] = crw . Status ( )
2018-03-14 14:12:04 +01:00
2018-11-14 10:18:03 +01:00
// n.b. take care to perform time arithmetic using UTC to avoid errors at DST boundaries.
2018-06-11 17:40:08 +01:00
totalDuration := time . Now ( ) . UTC ( ) . Sub ( core [ StartUTC ] . ( time . Time ) )
core [ Duration ] = totalDuration
2018-11-14 10:18:03 +01:00
if h . keepAccessLog ( crw . Status ( ) , retryAttempts , totalDuration ) {
2018-03-14 14:12:04 +01:00
core [ DownstreamContentSize ] = crw . Size ( )
if original , ok := core [ OriginContentSize ] ; ok {
o64 := original . ( int64 )
2019-02-05 17:10:03 +01:00
if crw . Size ( ) != o64 && crw . Size ( ) != 0 {
2018-03-14 14:12:04 +01:00
core [ GzipRatio ] = float64 ( o64 ) / float64 ( crw . Size ( ) )
}
2017-05-09 14:02:44 +02:00
}
2018-06-11 17:40:08 +01:00
core [ Overhead ] = totalDuration
2018-03-14 14:12:04 +01:00
if origin , ok := core [ OriginDuration ] ; ok {
2018-06-11 17:40:08 +01:00
core [ Overhead ] = totalDuration - origin . ( time . Duration )
2018-03-14 14:12:04 +01:00
}
2017-05-22 20:39:29 +01:00
2018-03-14 14:12:04 +01:00
fields := logrus . Fields { }
2017-05-22 20:39:29 +01:00
2018-03-14 14:12:04 +01:00
for k , v := range logDataTable . Core {
2018-11-14 10:18:03 +01:00
if h . config . Fields . Keep ( k ) {
2018-03-14 14:12:04 +01:00
fields [ k ] = v
}
}
2018-11-14 10:18:03 +01:00
h . redactHeaders ( logDataTable . Request , fields , "request_" )
h . redactHeaders ( logDataTable . OriginResponse , fields , "origin_" )
h . redactHeaders ( logDataTable . DownstreamResponse , fields , "downstream_" )
2017-05-22 20:39:29 +01:00
2018-11-14 10:18:03 +01:00
h . mu . Lock ( )
defer h . mu . Unlock ( )
h . logger . WithFields ( fields ) . Println ( )
2017-05-22 20:39:29 +01:00
}
2018-03-14 14:12:04 +01:00
}
2017-05-22 20:39:29 +01:00
2018-11-14 10:18:03 +01:00
func ( h * Handler ) redactHeaders ( headers http . Header , fields logrus . Fields , prefix string ) {
2018-03-14 14:12:04 +01:00
for k := range headers {
2018-11-14 10:18:03 +01:00
v := h . config . Fields . KeepHeader ( k )
2018-03-14 14:12:04 +01:00
if v == types . AccessLogKeep {
fields [ prefix + k ] = headers . Get ( k )
} else if v == types . AccessLogRedact {
fields [ prefix + k ] = "REDACTED"
}
2017-05-22 20:39:29 +01:00
}
2018-03-14 14:12:04 +01:00
}
2017-05-22 20:39:29 +01:00
2018-11-14 10:18:03 +01:00
func ( h * Handler ) keepAccessLog ( statusCode , retryAttempts int , duration time . Duration ) bool {
if h . config . Filters == nil {
2018-03-23 09:28:03 +01:00
// no filters were specified
2018-03-14 14:12:04 +01:00
return true
2018-04-23 10:54:03 +02:00
}
2018-11-14 10:18:03 +01:00
if len ( h . httpCodeRanges ) == 0 && ! h . config . Filters . RetryAttempts && h . config . Filters . MinDuration == 0 {
2018-03-23 09:28:03 +01:00
// empty filters were specified, e.g. by passing --accessLog.filters only (without other filter options)
return true
2018-04-23 10:54:03 +02:00
}
2018-11-14 10:18:03 +01:00
if h . httpCodeRanges . Contains ( statusCode ) {
2018-03-23 09:28:03 +01:00
return true
2018-04-23 10:54:03 +02:00
}
2018-11-14 10:18:03 +01:00
if h . config . Filters . RetryAttempts && retryAttempts > 0 {
2018-03-23 09:28:03 +01:00
return true
2017-05-22 20:39:29 +01:00
}
2017-05-09 14:02:44 +02:00
2019-06-17 11:48:05 +02:00
if h . config . Filters . MinDuration > 0 && ( types . Duration ( duration ) > h . config . Filters . MinDuration ) {
2018-06-11 17:40:08 +01:00
return true
}
2018-04-23 10:54:03 +02:00
return false
}
2017-05-09 14:02:44 +02:00
var requestCounter uint64 // Request ID
func nextRequestCount ( ) uint64 {
return atomic . AddUint64 ( & requestCounter , 1 )
}