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
"github.com/Sirupsen/logrus"
2017-05-25 12:25:53 +01:00
"github.com/containous/traefik/types"
2017-05-09 14:02:44 +02:00
)
type key string
const (
// DataTableKey is the key within the request context used to
// store the Log Data Table
DataTableKey key = "LogDataTable"
2017-05-25 12:25:53 +01:00
// CommonFormat is the common logging format (CLF)
CommonFormat = "common"
// JSONFormat is the JSON logging format
JSONFormat = "json"
2017-05-09 14:02:44 +02:00
)
// LogHandler will write each request and its response to the access log.
type LogHandler struct {
2017-08-11 11:04:58 +01:00
logger * logrus . Logger
file * os . File
filePath string
2017-09-15 15:02:03 +02:00
mu sync . Mutex
2017-05-09 14:02:44 +02:00
}
// NewLogHandler creates a new LogHandler
2017-05-25 12:25:53 +01:00
func NewLogHandler ( config * types . AccessLog ) ( * LogHandler , 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
}
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 :
return nil , fmt . Errorf ( "unsupported access log format: %s" , config . Format )
}
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 ,
}
2017-08-11 11:04:58 +01:00
return & LogHandler { logger : logger , file : file , filePath : config . FilePath } , 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
}
2017-05-09 14:02:44 +02:00
// GetLogDataTable gets the request context object that contains logging data. This accretes
// data as the request passes through the middleware chain.
func GetLogDataTable ( req * http . Request ) * LogData {
return req . Context ( ) . Value ( DataTableKey ) . ( * LogData )
}
func ( l * LogHandler ) ServeHTTP ( rw http . ResponseWriter , req * http . Request , next http . HandlerFunc ) {
now := time . Now ( ) . UTC ( )
core := make ( CoreLogData )
logDataTable := & LogData { Core : core , Request : req . Header }
core [ StartUTC ] = now
core [ StartLocal ] = now . Local ( )
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 [ RequestLine ] = fmt . Sprintf ( "%s %s %s" , req . Method , urlCopyString , req . Proto )
core [ ClientAddr ] = req . RemoteAddr
core [ ClientHost ] , core [ ClientPort ] = silentSplitHostPort ( req . RemoteAddr )
core [ ClientUsername ] = usernameIfPresent ( req . URL )
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 )
logDataTable . DownstreamResponse = crw . Header ( )
l . logTheRoundTrip ( logDataTable , crr , crw )
}
// Close closes the Logger (i.e. the file etc).
func ( l * LogHandler ) Close ( ) error {
2017-05-22 20:39:29 +01:00
return l . file . Close ( )
2017-05-09 14:02:44 +02:00
}
2017-08-11 11:04:58 +01:00
// Rotate closes and reopens the log file to allow for rotation
// by an external source.
func ( l * LogHandler ) Rotate ( ) error {
var err error
2017-09-15 15:02:03 +02:00
if l . file != nil {
defer func ( f * os . File ) {
f . Close ( )
} ( l . file )
2017-08-11 11:04:58 +01:00
}
l . file , err = os . OpenFile ( l . filePath , os . O_RDWR | os . O_CREATE | os . O_APPEND , 0664 )
if err != nil {
return err
}
2017-09-15 15:02:03 +02:00
l . mu . Lock ( )
defer l . mu . Unlock ( )
2017-08-11 11:04:58 +01:00
l . logger . Out = l . file
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
}
func usernameIfPresent ( theURL * url . URL ) string {
username := "-"
if theURL . User != nil {
if name := theURL . User . Username ( ) ; name != "" {
username = name
}
}
return username
}
// Logging handler to log frontend name, backend name, and elapsed time
func ( l * LogHandler ) logTheRoundTrip ( logDataTable * LogData , crr * captureRequestReader , crw * captureResponseWriter ) {
core := logDataTable . Core
2017-08-28 12:50:02 +02:00
if core [ RetryAttempts ] == nil {
core [ RetryAttempts ] = 0
}
2017-05-09 14:02:44 +02:00
if crr != nil {
core [ RequestContentSize ] = crr . count
}
core [ DownstreamStatus ] = crw . Status ( )
core [ DownstreamStatusLine ] = fmt . Sprintf ( "%03d %s" , crw . Status ( ) , http . StatusText ( crw . Status ( ) ) )
core [ DownstreamContentSize ] = crw . Size ( )
if original , ok := core [ OriginContentSize ] ; ok {
o64 := original . ( int64 )
if o64 != crw . Size ( ) && 0 != crw . Size ( ) {
core [ GzipRatio ] = float64 ( o64 ) / float64 ( crw . Size ( ) )
}
}
// n.b. take care to perform time arithmetic using UTC to avoid errors at DST boundaries
total := time . Now ( ) . UTC ( ) . Sub ( core [ StartUTC ] . ( time . Time ) )
core [ Duration ] = total
if origin , ok := core [ OriginDuration ] ; ok {
core [ Overhead ] = total - origin . ( time . Duration )
} else {
core [ Overhead ] = total
}
2017-05-22 20:39:29 +01:00
fields := logrus . Fields { }
for k , v := range logDataTable . Core {
fields [ k ] = v
}
for k := range logDataTable . Request {
fields [ "request_" + k ] = logDataTable . Request . Get ( k )
}
for k := range logDataTable . OriginResponse {
fields [ "origin_" + k ] = logDataTable . OriginResponse . Get ( k )
}
for k := range logDataTable . DownstreamResponse {
fields [ "downstream_" + k ] = logDataTable . DownstreamResponse . Get ( k )
}
2017-09-15 15:02:03 +02:00
l . mu . Lock ( )
defer l . mu . Unlock ( )
2017-05-22 20:39:29 +01:00
l . logger . WithFields ( fields ) . Println ( )
2017-05-09 14:02:44 +02:00
}
//-------------------------------------------------------------------------------------------------
var requestCounter uint64 // Request ID
func nextRequestCount ( ) uint64 {
return atomic . AddUint64 ( & requestCounter , 1 )
}