2020-04-29 20:51:32 +00:00
package logger
import (
2020-12-08 18:16:25 +00:00
"fmt"
2020-11-25 06:55:13 +00:00
"io"
2020-04-29 20:51:32 +00:00
"os"
2020-12-02 20:58:17 +00:00
"path"
2021-01-14 11:53:35 +00:00
"path/filepath"
2021-01-17 20:31:41 +00:00
"sync"
2020-04-29 20:51:32 +00:00
2021-01-13 23:13:23 +00:00
"github.com/mattn/go-colorable"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
fallbacklog "github.com/rs/zerolog/log"
2020-11-15 01:49:44 +00:00
"github.com/urfave/cli/v2"
2021-01-21 23:03:47 +00:00
"golang.org/x/term"
2020-12-02 20:58:17 +00:00
"gopkg.in/natefinch/lumberjack.v2"
2020-11-15 01:49:44 +00:00
)
const (
EnableTerminalLog = false
DisableTerminalLog = true
LogLevelFlag = "loglevel"
LogFileFlag = "logfile"
LogDirectoryFlag = "log-directory"
LogTransportLevelFlag = "transport-loglevel"
LogSSHDirectoryFlag = "log-directory"
LogSSHLevelFlag = "log-level"
2020-12-08 18:16:25 +00:00
dirPermMode = 0744 // rwxr--r--
filePermMode = 0644 // rw-r--r--
2020-04-29 20:51:32 +00:00
)
2020-12-02 20:58:17 +00:00
func fallbackLogger ( err error ) * zerolog . Logger {
failLog := fallbacklog . With ( ) . Logger ( )
fallbacklog . Error ( ) . Msgf ( "Falling back to a default logger due to logger setup failure: %s" , err )
return & failLog
}
2021-01-16 15:56:44 +00:00
type resilientMultiWriter struct {
writers [ ] io . Writer
}
// This custom resilientMultiWriter is an alternative to zerolog's so that we can make it resilient to individual
// writer's errors. E.g., when running as a Windows service, the console writer fails, but we don't want to
// allow that to prevent all logging to fail due to breaking the for loop upon an error.
func ( t resilientMultiWriter ) Write ( p [ ] byte ) ( n int , err error ) {
for _ , w := range t . writers {
_ , _ = w . Write ( p )
}
return len ( p ) , nil
}
2020-11-25 06:55:13 +00:00
func newZerolog ( loggerConfig * Config ) * zerolog . Logger {
var writers [ ] io . Writer
2020-11-15 01:49:44 +00:00
2020-11-25 06:55:13 +00:00
if loggerConfig . ConsoleConfig != nil {
2020-12-02 20:58:17 +00:00
writers = append ( writers , createConsoleLogger ( * loggerConfig . ConsoleConfig ) )
2020-11-15 01:49:44 +00:00
}
2020-12-08 18:16:25 +00:00
if loggerConfig . FileConfig != nil {
2021-01-15 17:24:20 +00:00
fileLogger , err := createFileWriter ( * loggerConfig . FileConfig )
2020-12-08 18:16:25 +00:00
if err != nil {
return fallbackLogger ( err )
}
writers = append ( writers , fileLogger )
}
2020-12-02 20:58:17 +00:00
if loggerConfig . RollingConfig != nil {
rollingLogger , err := createRollingLogger ( * loggerConfig . RollingConfig )
if err != nil {
return fallbackLogger ( err )
}
writers = append ( writers , rollingLogger )
}
2020-11-15 01:49:44 +00:00
2021-01-16 15:56:44 +00:00
multi := resilientMultiWriter { writers }
2020-11-15 01:49:44 +00:00
2020-11-25 06:55:13 +00:00
level , err := zerolog . ParseLevel ( loggerConfig . MinLevel )
2020-11-15 01:49:44 +00:00
if err != nil {
2020-12-02 20:58:17 +00:00
return fallbackLogger ( err )
2020-11-15 01:49:44 +00:00
}
2020-11-25 06:55:13 +00:00
log := zerolog . New ( multi ) . With ( ) . Timestamp ( ) . Logger ( ) . Level ( level )
2020-11-15 01:49:44 +00:00
2020-11-25 06:55:13 +00:00
return & log
2020-11-15 01:49:44 +00:00
}
2020-11-25 06:55:13 +00:00
func CreateTransportLoggerFromContext ( c * cli . Context , disableTerminal bool ) * zerolog . Logger {
2020-11-15 01:49:44 +00:00
return createFromContext ( c , LogTransportLevelFlag , LogDirectoryFlag , disableTerminal )
}
2020-11-25 06:55:13 +00:00
func CreateLoggerFromContext ( c * cli . Context , disableTerminal bool ) * zerolog . Logger {
2020-11-15 01:49:44 +00:00
return createFromContext ( c , LogLevelFlag , LogDirectoryFlag , disableTerminal )
}
2020-11-25 06:55:13 +00:00
func CreateSSHLoggerFromContext ( c * cli . Context , disableTerminal bool ) * zerolog . Logger {
2020-11-15 01:49:44 +00:00
return createFromContext ( c , LogSSHLevelFlag , LogSSHDirectoryFlag , disableTerminal )
}
func createFromContext (
c * cli . Context ,
logLevelFlagName ,
logDirectoryFlagName string ,
disableTerminal bool ,
2020-11-25 06:55:13 +00:00
) * zerolog . Logger {
2020-11-15 01:49:44 +00:00
logLevel := c . String ( logLevelFlagName )
logFile := c . String ( LogFileFlag )
logDirectory := c . String ( logDirectoryFlagName )
2020-11-25 06:55:13 +00:00
loggerConfig := CreateConfig (
logLevel ,
disableTerminal ,
logDirectory ,
logFile ,
)
2020-11-15 01:49:44 +00:00
2020-12-08 18:16:25 +00:00
log := newZerolog ( loggerConfig )
if incompatibleFlagsSet := logFile != "" && logDirectory != "" ; incompatibleFlagsSet {
log . Error ( ) . Msgf ( "Your config includes values for both %s and %s, but they are incompatible. %s takes precedence." , LogFileFlag , logDirectoryFlagName , LogFileFlag )
}
return log
2020-11-15 01:49:44 +00:00
}
2020-11-25 06:55:13 +00:00
func Create ( loggerConfig * Config ) * zerolog . Logger {
if loggerConfig == nil {
2021-01-13 23:13:23 +00:00
loggerConfig = & Config {
defaultConfig . ConsoleConfig ,
nil ,
nil ,
defaultConfig . MinLevel ,
}
2020-04-29 20:51:32 +00:00
}
2020-11-25 06:55:13 +00:00
return newZerolog ( loggerConfig )
2020-08-14 21:51:00 +00:00
}
2020-12-02 20:58:17 +00:00
func createConsoleLogger ( config ConsoleConfig ) io . Writer {
2021-01-21 23:03:47 +00:00
consoleOut := os . Stderr
2020-12-02 20:58:17 +00:00
return zerolog . ConsoleWriter {
2021-01-21 23:03:47 +00:00
Out : colorable . NewColorable ( consoleOut ) ,
NoColor : config . noColor || ! term . IsTerminal ( int ( consoleOut . Fd ( ) ) ) ,
2020-12-02 20:58:17 +00:00
}
}
2021-01-17 20:31:41 +00:00
type fileInitializer struct {
once sync . Once
writer io . Writer
creationError error
}
var (
singleFileInit fileInitializer
rotatingFileInit fileInitializer
)
2021-01-15 17:24:20 +00:00
func createFileWriter ( config FileConfig ) ( io . Writer , error ) {
2021-01-17 20:31:41 +00:00
singleFileInit . once . Do ( func ( ) {
2020-12-08 18:16:25 +00:00
2021-01-17 20:31:41 +00:00
var logFile io . Writer
fullpath := config . Fullpath ( )
// Try to open the existing file
logFile , err := os . OpenFile ( fullpath , os . O_APPEND | os . O_WRONLY , filePermMode )
2020-12-08 18:16:25 +00:00
if err != nil {
2021-01-17 20:31:41 +00:00
// If the existing file wasn't found, or couldn't be opened, just ignore
// it and recreate a new one.
logFile , err = createDirFile ( config )
// If creating a new logfile fails, then we have no choice but to error out.
if err != nil {
singleFileInit . creationError = err
return
}
2020-12-08 18:16:25 +00:00
}
2021-01-17 20:31:41 +00:00
singleFileInit . writer = logFile
} )
return singleFileInit . writer , singleFileInit . creationError
2020-12-08 18:16:25 +00:00
}
2021-01-15 17:24:20 +00:00
func createDirFile ( config FileConfig ) ( io . Writer , error ) {
2020-12-08 18:16:25 +00:00
if config . Dirname != "" {
err := os . MkdirAll ( config . Dirname , dirPermMode )
if err != nil {
return nil , fmt . Errorf ( "unable to create directories for new logfile: %s" , err )
}
}
mode := os . FileMode ( filePermMode )
2021-01-14 11:53:35 +00:00
fullPath := filepath . Join ( config . Dirname , config . Filename )
2021-01-14 16:23:56 +00:00
logFile , err := os . OpenFile ( fullPath , os . O_CREATE | os . O_WRONLY | os . O_APPEND , mode )
2020-12-08 18:16:25 +00:00
if err != nil {
return nil , fmt . Errorf ( "unable to create a new logfile: %s" , err )
}
return logFile , nil
}
2020-12-02 20:58:17 +00:00
func createRollingLogger ( config RollingConfig ) ( io . Writer , error ) {
2021-01-17 20:31:41 +00:00
rotatingFileInit . once . Do ( func ( ) {
if err := os . MkdirAll ( config . Dirname , dirPermMode ) ; err != nil {
rotatingFileInit . creationError = err
return
}
rotatingFileInit . writer = & lumberjack . Logger {
Filename : path . Join ( config . Dirname , config . Filename ) ,
MaxBackups : config . maxBackups ,
MaxSize : config . maxSize ,
MaxAge : config . maxAge ,
}
} )
2020-12-02 20:58:17 +00:00
2021-01-17 20:31:41 +00:00
return rotatingFileInit . writer , rotatingFileInit . creationError
2020-12-02 20:58:17 +00:00
}