Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

log: implement logging to file #723

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion command/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ var app = &cli.App{
},
Usage: "log level: (trace, debug, info, error)",
},
&cli.StringFlag{
Name: "log-file",
Value: log.NoLogFile,
Usage: "sets file for logging",
},
&cli.BoolFlag{
Name: "install-completion",
Usage: "get completion installation instructions for your shell (only available for bash, pwsh, and zsh)",
Expand Down Expand Up @@ -96,12 +101,24 @@ var app = &cli.App{
workerCount := c.Int("numworkers")
printJSON := c.Bool("json")
logLevel := c.String("log")
logFile := c.String("log-file")
isStat := c.Bool("stat")
endpointURL := c.String("endpoint-url")

log.Init(logLevel, printJSON)
// If validation fails, initialize the logger without a log file. Then we log the error
err := log.IsValidLogFile(logFile)
if err != nil {
logFile = log.NoLogFile
}

log.Init(logLevel, printJSON, log.LogFile(logFile))
parallel.Init(workerCount)

if err != nil {
printError(commandFromContext(c), c.Command.Name, err)
return err
}

if retryCount < 0 {
err := fmt.Errorf("retry count cannot be a negative value")
printError(commandFromContext(c), c.Command.Name, err)
Expand Down
108 changes: 94 additions & 14 deletions log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,59 +17,99 @@ var outputCh = make(chan output, 10000)

var global *Logger

// LoggerOptions stores parameters that are used to create a logger
type LoggerOptions struct {
logFile string
}

// LoggerOption is type for setter functions that specify LoggerOptions parameters
type LoggerOption func(*LoggerOptions)

// LogFile is setter for LoggerOptions.logFile parameter
func LogFile(logFile string) LoggerOption {
return func(args *LoggerOptions) {
args.logFile = logFile
}
}

// Init inits global logger.
func Init(level string, json bool) {
global = New(level, json)
func Init(level string, json bool, options ...LoggerOption) {
global = New(level, json, options...)
}

// Trace prints message in trace mode.
func Trace(msg Message) {
global.printf(LevelTrace, msg, os.Stdout)
global.printf(LevelTrace, msg, global.logFiles.stdout)
}

// Debug prints message in debug mode.
func Debug(msg Message) {
global.printf(LevelDebug, msg, os.Stdout)
global.printf(LevelDebug, msg, global.logFiles.stdout)
}

// Info prints message in info mode.
func Info(msg Message) {
global.printf(LevelInfo, msg, os.Stdout)
global.printf(LevelInfo, msg, global.logFiles.stdout)
}

// Stat prints stat message regardless of the log level with info print formatting.
// It uses printfHelper instead of printf to ignore the log level condition.
func Stat(msg Message) {
global.printfHelper(LevelInfo, msg, os.Stdout)
global.printfHelper(LevelInfo, msg, global.logFiles.stdout)
}

// Error prints message in error mode.
func Error(msg Message) {
global.printf(LevelError, msg, os.Stderr)
global.printf(LevelError, msg, global.logFiles.stderr)
}

// Close closes logger and its channel.
func Close() {
if global != nil {
close(outputCh)
global.logFiles.Close()

<-global.donech
}
}

// LogFiles stores pointers to stdout and stderr files
type LogFiles struct {
stdout *os.File
stderr *os.File
}

// Logger is a structure for logging messages.
type Logger struct {
donech chan struct{}
json bool
level LogLevel
donech chan struct{}
json bool
level LogLevel
logFiles *LogFiles
}

// NoLogFile is default value that disables the use of the log file
const NoLogFile string = ""

// New creates new logger.
func New(level string, json bool) *Logger {
func New(level string, json bool, options ...LoggerOption) *Logger {
// Default logger options
args := &LoggerOptions{
logFile: NoLogFile,
}

// Apply setters to LoggerOptions struct
for _, setter := range options {
setter(args)
}

logFiles := GetLogFiles(args.logFile)

logLevel := LevelFromString(level)
logger := &Logger{
donech: make(chan struct{}),
json: json,
level: logLevel,
donech: make(chan struct{}),
json: json,
level: logLevel,
logFiles: logFiles,
}
go logger.out()
return logger
Expand Down Expand Up @@ -152,3 +192,43 @@ func LevelFromString(s string) LogLevel {
return LevelInfo
}
}

// Close closes stdout and stderr if file pointers are not os.Stdout or os.Stderr
func (files *LogFiles) Close() {
if files.stdout != os.Stdout {
files.stdout.Close()
}
}

// IsValidLogFile checks the path to the file is correct
func IsValidLogFile(logFile string) error {
if logFile == NoLogFile {
return nil
}

file, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}

file.Close()
return nil
}

// GetLogFiles creates and/or opens a file for logging if the path to the file was specified.
// Otherwise uses os.Stdout and os.Stderr for logging
func GetLogFiles(logFile string) *LogFiles {
if logFile == NoLogFile {
return &LogFiles{
stdout: os.Stdout,
stderr: os.Stderr,
}
}

file, _ := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)

return &LogFiles{
stdout: file,
stderr: file,
}
}
Loading