From 39e0a1fd4373da15caee9a3e75b333cce5848310 Mon Sep 17 00:00:00 2001 From: brent s Date: Sun, 16 Jan 2022 02:05:42 -0500 Subject: [PATCH] Windows Event Log, error output Adding more Event Log support, and modifying the loggers so they return errors in their operational functions. --- .gitignore | 4 + logging/TODO | 3 +- logging/consts_windows.go | 18 ++- logging/errs_windows.go | 12 ++ logging/funcs_file.go | 77 ++++++++--- logging/funcs_linux.go | 22 ++- logging/funcs_multilogger.go | 147 +++++++++++--------- logging/funcs_multilogger_mgr.go | 40 ++++-- logging/funcs_multilogger_mgr_linux.go | 28 +++- logging/funcs_multilogger_mgr_windows.go | 19 ++- logging/funcs_std.go | 62 +++++++-- logging/funcs_sysd_linux.go | 41 ++++-- logging/funcs_syslog_linux.go | 75 +++++++--- logging/funcs_test.go | 1 + logging/funcs_windows.go | 23 ++- logging/funcs_winlogger_windows.go | 169 +++++++++++++++++------ logging/types.go | 38 +++-- logging/types_windows.go | 53 ++++++- 18 files changed, 612 insertions(+), 220 deletions(-) create mode 100644 logging/errs_windows.go create mode 100644 logging/funcs_test.go diff --git a/.gitignore b/.gitignore index 5f32697..85bf646 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ # Test binary, built with `go test -c` *.test +# But DO include the actual tests. +!_test.go +!*_test.go +!*_test/ # Output of the go coverage tool, specifically when used with LiteIDE *.out diff --git a/logging/TODO b/logging/TODO index 7fca130..c575174 100644 --- a/logging/TODO +++ b/logging/TODO @@ -2,10 +2,9 @@ https://stackoverflow.com/a/24809646 https://golang.org/pkg/runtime/#Caller -- Support simultaneous writing to multiple Loggers. - - Suport remote loggers? (eventlog, syslog, systemd) - DOCS. +-- Done, but flesh out. - Unit/Integration tests. diff --git a/logging/consts_windows.go b/logging/consts_windows.go index 2c29a9e..4c9c364 100644 --- a/logging/consts_windows.go +++ b/logging/consts_windows.go @@ -3,7 +3,6 @@ package logging import ( `os` `path/filepath` - `regexp` `r00t2.io/goutils/bitmask` ) @@ -28,8 +27,21 @@ var ( } ) -// ptrnSourceExists is a regex pattern to check for a registry entry (Event Log entry) already existing. -var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`) +/* + ptrnSourceExists is a regex pattern to check for a registry entry (Event Log entry) already existing. + + Deprecated: this is handled differently now. +*/ +// var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`) + +const ( + EIDMin uint32 = 1 + EIDMax uint32 = 1000 +) + +const ( + eventLogRegistryKey string = "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application" +) // Default WinEventID, (can be) used in GetLogger and MultiLogger.AddWinLogger. var DefaultEventID *WinEventID = &WinEventID{ diff --git a/logging/errs_windows.go b/logging/errs_windows.go new file mode 100644 index 0000000..00feef0 --- /dev/null +++ b/logging/errs_windows.go @@ -0,0 +1,12 @@ +package logging + +import ( + `errors` + `fmt` +) + +var ( + ErrBadBinPath error = errors.New("evaluated binary path does not actually exist") + ErrBadPerms error = errors.New("access denied when attempting to register Event Log source") + ErrBadEid error = errors.New(fmt.Sprintf("event IDs must be between %v and %v inclusive", EIDMin, EIDMax)) +) diff --git a/logging/funcs_file.go b/logging/funcs_file.go index 678ba84..623dabb 100644 --- a/logging/funcs_file.go +++ b/logging/funcs_file.go @@ -1,61 +1,96 @@ package logging import ( + `errors` "fmt" "io" + `io/fs` "log" "os" + `strings` ) // Setup sets up/configures a FileLogger and prepares it for use. -func (l *FileLogger) Setup() { +func (l *FileLogger) Setup() (err error) { - var err error var multi io.Writer - l.Logger = log.Default() - l.Logger.SetPrefix(l.Prefix + " ") - if l.writer, err = os.OpenFile(l.Path, appendFlags, logPerm); err != nil { - log.Panicf("could not open log file \"%v\" for writing: %v\n", l.Path, err) + // This uses a shared handle across the import. We don't want that. + // l.Logger = log.Default() + if l.Prefix != "" { + l.Prefix = strings.TrimRight(l.Prefix, " ") + " " + // l.Logger.SetPrefix(l.Prefix) } + if l.writer, err = os.OpenFile(l.Path, appendFlags, logPerm); err != nil { + return + } + // https://stackoverflow.com/a/36719588/733214 - if l.EnableStdOut { + switch { + case l.EnableStdErr && l.EnableStdOut: + multi = io.MultiWriter(os.Stdout, os.Stderr, l.writer) + case l.EnableStdErr: + multi = io.MultiWriter(os.Stderr, l.writer) + case l.EnableStdOut: multi = io.MultiWriter(os.Stdout, l.writer) - } else { + default: multi = l.writer } - l.Logger.SetOutput(multi) + + l.Logger = log.New(multi, l.Prefix, l.LogFlags) + // l.Logger.SetOutput(multi) + + return } // Shutdown cleanly shuts down a FileLogger. -func (l *FileLogger) Shutdown() { - - var err error +func (l *FileLogger) Shutdown() (err error) { if err = l.writer.Close(); err != nil { - log.Panicf("could not close log file \"%v\": %v\n", l.Path, err) + if !errors.Is(err, fs.ErrClosed) { + return + } } + return } -// GetPrefix returns the prefix used by this FileLogger. -func (l *FileLogger) GetPrefix() string { - return l.Prefix +/* + GetPrefix returns the prefix used by this FileLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *FileLogger) GetPrefix() (prefix string, err error) { + + prefix = l.Prefix + + return } /* DoDebug sets the debug state of this FileLogger. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log. + err will always be nil; it's there for interface-compat. */ -func (l *FileLogger) DoDebug(d bool) { +func (l *FileLogger) DoDebug(d bool) (err error) { + l.EnableDebug = d + + return } -// SetPrefix sets the prefix for this FileLogger. -func (l *FileLogger) SetPrefix(prefix string) { +/* + SetPrefix sets the prefix for this FileLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *FileLogger) SetPrefix(prefix string) (err error) { - l.Prefix = prefix + " " - l.Logger.SetPrefix(prefix + " ") + l.Prefix = prefix + if prefix != "" { + l.Prefix = strings.TrimRight(l.Prefix, " ") + " " + } + l.Logger.SetPrefix(l.Prefix) + + return } // Alert writes an ALERT-level message to this FileLogger. diff --git a/logging/funcs_linux.go b/logging/funcs_linux.go index f765828..e849f50 100644 --- a/logging/funcs_linux.go +++ b/logging/funcs_linux.go @@ -24,6 +24,9 @@ var ( If prefix is "\x00" (a null byte), then the default logging prefix will be used. If anything else, even an empty string, is specified then that will be used instead for the prefix. + logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and + https://pkg.go.dev/log#pkg-constants for details. + logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to, it will be used (assuming you have no higher-level loggers available). Only the first logPaths entry that "works" will be used, later entries will be ignored. If you want to log to multiple files simultaneously, use a MultiLogger instead. @@ -32,10 +35,11 @@ var ( If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type), use GetMultiLogger. */ -func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logger, err error) { +func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) { var logPath string var logFlags bitmask.MaskBit + var currentPrefix string // Configure system-supported logger(s). if sysd.Enabled() { @@ -109,6 +113,7 @@ func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logg StdLogger: StdLogger{ Prefix: logPrefix, EnableDebug: enableDebug, + LogFlags: logConfigFlags, }, Path: logPath, } @@ -116,17 +121,26 @@ func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logg logger = &StdLogger{ Prefix: logPrefix, EnableDebug: enableDebug, + LogFlags: logConfigFlags, } } } } - logger.Setup() if prefix != "\x00" { - logger.SetPrefix(prefix) + if err = logger.SetPrefix(prefix); err != nil { + return + } + } + if err = logger.Setup(); err != nil { + return } - logger.Info("logger initialized of type %T with prefix %v", logger, logger.GetPrefix()) + if currentPrefix, err = logger.GetPrefix(); err != nil { + return + } + + logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix) return } diff --git a/logging/funcs_multilogger.go b/logging/funcs_multilogger.go index 1c2dc50..bf1dc88 100644 --- a/logging/funcs_multilogger.go +++ b/logging/funcs_multilogger.go @@ -7,41 +7,70 @@ import ( ) // Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use. -func (m *MultiLogger) Setup() { +func (m *MultiLogger) Setup() (err error) { var wg sync.WaitGroup + var errs *multierr.MultiError = multierr.NewMultiError(nil) for _, l := range m.Loggers { wg.Add(1) go func() { + var err2 error defer wg.Done() - l.Setup() + if err2 = l.Setup(); err2 != nil { + errs.AddError(err2) + err2 = nil + } }() } wg.Wait() + + if errs.Count() > 0 { + err = errs + return + } + + return } // Shutdown cleanly shuts down a MultiLogger (and all its MultiLogger.Loggers). -func (m *MultiLogger) Shutdown() { +func (m *MultiLogger) Shutdown() (err error) { var wg sync.WaitGroup + var errs *multierr.MultiError = multierr.NewMultiError(nil) for _, l := range m.Loggers { wg.Add(1) go func() { + var err2 error defer wg.Done() - l.Shutdown() + if err2 = l.Shutdown(); err2 != nil { + errs.AddError(err2) + err2 = nil + } }() } wg.Wait() + + if errs.Count() > 0 { + err = errs + return + } + + return } -// GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers). -func (m *MultiLogger) GetPrefix() string { +/* + GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers). + err will always be nil; it's there for interface-compat. +*/ +func (m *MultiLogger) GetPrefix() (prefix string, err error) { - return m.Prefix + prefix = m.Prefix + + return } @@ -51,21 +80,33 @@ func (m *MultiLogger) GetPrefix() string { If you had a logger-specific EnableDebug set, you will need to re-set it to your desired state after running this method. */ -func (m *MultiLogger) DoDebug(d bool) { +func (m *MultiLogger) DoDebug(d bool) (err error) { var wg sync.WaitGroup + var errs *multierr.MultiError = multierr.NewMultiError(nil) m.EnableDebug = d for _, l := range m.Loggers { wg.Add(1) go func() { + var err2 error defer wg.Done() - l.DoDebug(d) + if err2 = l.DoDebug(d); err2 != nil { + errs.AddError(err2) + err2 = nil + } }() } wg.Wait() + + if errs.Count() > 0 { + err = errs + return + } + + return } /* @@ -73,21 +114,33 @@ func (m *MultiLogger) DoDebug(d bool) { If you had a logger-specific Prefix set, you will need to re-set it to your desired prefix after running this method. */ -func (m *MultiLogger) SetPrefix(prefix string) { +func (m *MultiLogger) SetPrefix(prefix string) (err error) { var wg sync.WaitGroup + var errs *multierr.MultiError = multierr.NewMultiError(nil) m.Prefix = prefix for _, l := range m.Loggers { wg.Add(1) go func() { + var err2 error defer wg.Done() - l.SetPrefix(prefix) + if err2 = l.SetPrefix(prefix); err != nil { + errs.AddError(err2) + err2 = nil + } }() } wg.Wait() + + if errs.Count() > 0 { + err = errs + return + } + + return } // Alert writes an ALERT-level message to this MultiLogger (and all its MultiLogger.Loggers). @@ -100,16 +153,11 @@ func (m *MultiLogger) Alert(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Alert(msg, rplc...) - } else { - err = logObj.Alert(msg) - } - if err != nil { + if err = logObj.Alert(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -131,16 +179,11 @@ func (m *MultiLogger) Crit(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Crit(msg, rplc...) - } else { - err = logObj.Crit(msg) - } - if err != nil { + if err = logObj.Crit(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -162,16 +205,11 @@ func (m *MultiLogger) Debug(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Debug(msg, rplc...) - } else { - err = logObj.Debug(msg) - } - if err != nil { + if err = logObj.Debug(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -192,16 +230,11 @@ func (m *MultiLogger) Emerg(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Emerg(msg, rplc...) - } else { - err = logObj.Emerg(msg) - } - if err != nil { + if err = logObj.Emerg(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -222,16 +255,11 @@ func (m *MultiLogger) Err(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Err(msg, rplc...) - } else { - err = logObj.Err(msg) - } - if err != nil { + if err = logObj.Err(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -253,16 +281,11 @@ func (m *MultiLogger) Info(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Info(msg, rplc...) - } else { - err = logObj.Info(msg) - } - if err != nil { + if err = logObj.Info(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -284,16 +307,11 @@ func (m *MultiLogger) Notice(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Notice(msg, rplc...) - } else { - err = logObj.Notice(msg) - } - if err != nil { + if err = logObj.Notice(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() @@ -315,16 +333,11 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) { wg.Add(1) go func(logObj Logger, msg string, rplc ...interface{}) { defer wg.Done() - if rplc != nil { - err = logObj.Warning(msg, rplc...) - } else { - err = logObj.Warning(msg) - } - if err != nil { + if err = logObj.Warning(msg, rplc...); err != nil { e.AddError(err) err = nil } - }(l, s, v) + }(l, s, v...) } wg.Wait() diff --git a/logging/funcs_multilogger_mgr.go b/logging/funcs_multilogger_mgr.go index 13a71e8..a08ba02 100644 --- a/logging/funcs_multilogger_mgr.go +++ b/logging/funcs_multilogger_mgr.go @@ -21,7 +21,7 @@ func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) { m = &MultiLogger{ EnableDebug: enableDebug, - Prefix: "", + Prefix: logPrefix, Loggers: make(map[string]Logger), } if prefix != "\x00" { @@ -36,10 +36,13 @@ func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) { identifier is a string to use to identify the added StdLogger in MultiLogger.Loggers. If empty, one will be automatically generated. + + See GetLogger's logConfigFlags argument and StdLogger.LogFlags for details on logFlags. */ -func (m *MultiLogger) AddStdLogger(identifier string) (err error) { +func (m *MultiLogger) AddStdLogger(identifier string, logFlags int) (err error) { var exists bool + var prefix string if identifier == "" { identifier = uuid.New().String() @@ -54,10 +57,17 @@ func (m *MultiLogger) AddStdLogger(identifier string) (err error) { Logger: nil, EnableDebug: m.EnableDebug, Prefix: m.Prefix, + LogFlags: logFlags, + } + if err = m.Loggers[identifier].Setup(); err != nil { + return } - m.Loggers[identifier].Setup() - m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) + if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil { + return + } + + m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix) return } @@ -70,11 +80,12 @@ func (m *MultiLogger) AddStdLogger(identifier string) (err error) { logfilePath is a string for the path to the desired logfile. */ -func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error) { +func (m *MultiLogger) AddFileLogger(identifier string, enableStdOut, enableStdErr bool, logFlags int, logfilePath string) (err error) { var exists bool var success bool var dirPath string + var prefix string if identifier == "" { identifier = uuid.New().String() @@ -85,7 +96,9 @@ func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error) return } - if exists, _ = paths.RealPathExists(&logfilePath); !exists { + if exists, err = paths.RealPathExists(&logfilePath); err != nil { + return + } else if !exists { if success, err = testOpen(logfilePath); err != nil { return } else if !success { @@ -107,12 +120,21 @@ func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error) Logger: nil, EnableDebug: m.EnableDebug, Prefix: m.Prefix, + LogFlags: logFlags, }, - Path: logfilePath, + Path: logfilePath, + EnableStdOut: enableStdOut, + EnableStdErr: enableStdErr, + } + if err = m.Loggers[identifier].Setup(); err != nil { + return } - m.Loggers[identifier].Setup() - m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) + if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil { + return + } + + m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix) return } diff --git a/logging/funcs_multilogger_mgr_linux.go b/logging/funcs_multilogger_mgr_linux.go index bc9f4fb..3a1ab90 100644 --- a/logging/funcs_multilogger_mgr_linux.go +++ b/logging/funcs_multilogger_mgr_linux.go @@ -13,8 +13,10 @@ import ( identifier is a string to use to identify the added Logger in MultiLogger.Loggers. If empty, one will be automatically generated. + + See the documentation for GetLogger for details on other arguments. */ -func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (err error) { +func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) { var l Logger var exists bool @@ -28,7 +30,7 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (e return } - if l, err = GetLogger(m.EnableDebug, m.Prefix, logPaths...); err != nil { + if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil { return } @@ -46,6 +48,7 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (e func (m *MultiLogger) AddSysdLogger(identifier string) (err error) { var exists bool + var prefix string if identifier == "" { identifier = uuid.New().String() @@ -65,9 +68,15 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) { EnableDebug: m.EnableDebug, Prefix: m.Prefix, } - m.Loggers[identifier].Setup() + if err = m.Loggers[identifier].Setup(); err != nil { + return + } - m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) + if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil { + return + } + + m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix) return } @@ -84,6 +93,7 @@ func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) { var hasSyslog bool var stat os.FileInfo var devlogPath string = devlog + var prefix string if identifier == "" { identifier = uuid.New().String() @@ -110,9 +120,15 @@ func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) { EnableDebug: m.EnableDebug, Prefix: m.Prefix, } - m.Loggers[identifier].Setup() + if err = m.Loggers[identifier].Setup(); err != nil { + return + } - m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) + if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil { + return + } + + m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix) return } diff --git a/logging/funcs_multilogger_mgr_windows.go b/logging/funcs_multilogger_mgr_windows.go index 7e42c27..4b95d50 100644 --- a/logging/funcs_multilogger_mgr_windows.go +++ b/logging/funcs_multilogger_mgr_windows.go @@ -16,10 +16,12 @@ import ( logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to, it will be used (assuming you have no higher-level loggers available). + See the documentation for GetLogger for details on other arguments. + Only the first logPaths entry that "works" will be used, later entries will be ignored. Currently this will almost always return a WinLogger. */ -func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logPaths ...string) (err error) { +func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logFlags int, logPaths ...string) (err error) { var l Logger var exists bool @@ -34,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, } if logPaths != nil { - l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logPaths...); + l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...) } else { - l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs); + l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags) } if err != nil { return @@ -66,6 +68,7 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, func (m *MultiLogger) AddWinLogger(identifier, source string, eventIDs *WinEventID) (err error) { var exists bool + var prefix string if identifier == "" { identifier = uuid.New().String() @@ -85,9 +88,15 @@ func (m *MultiLogger) AddWinLogger(identifier, source string, eventIDs *WinEvent EnableDebug: m.EnableDebug, eids: eventIDs, } - m.Loggers[identifier].Setup() + if err = m.Loggers[identifier].Setup(); err != nil { + return + } - m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) + if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil { + return + } + + m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix) return } diff --git a/logging/funcs_std.go b/logging/funcs_std.go index bf051e2..dab726f 100644 --- a/logging/funcs_std.go +++ b/logging/funcs_std.go @@ -2,26 +2,45 @@ package logging import ( "fmt" - "log" + `log` + `os` + `strings` ) -// Setup sets up/configures a StdLogger and prepares it for use. -func (l *StdLogger) Setup() { +/* + Setup sets up/configures a StdLogger and prepares it for use. + err will always be nil; it's there for interface-compat. +*/ +func (l *StdLogger) Setup() (err error) { - l.Logger = log.Default() - l.Logger.SetPrefix(l.Prefix + " ") + // This uses a shared handle across the import. We don't want that. + // l.Logger = log.Default() + if l.Prefix != "" { + l.Prefix = strings.TrimRight(l.Prefix, " ") + " " + // l.Logger.SetPrefix(l.Prefix) + } + // (stdlib).log.std is returned by log.Default(), which uses os.Stderr. + l.Logger = log.New(os.Stderr, l.Prefix, l.LogFlags) + + return } -// Shutdown cleanly shuts down a StdLogger. -func (l *StdLogger) Shutdown() { +/* + Shutdown cleanly shuts down a StdLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *StdLogger) Shutdown() (err error) { // NOOP - _ = "" + return } -// GetPrefix returns the prefix used by this StdLogger. -func (l *StdLogger) GetPrefix() (prefix string) { +/* + GetPrefix returns the prefix used by this StdLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *StdLogger) GetPrefix() (prefix string, err error) { prefix = l.Prefix @@ -31,15 +50,28 @@ func (l *StdLogger) GetPrefix() (prefix string) { /* DoDebug sets the debug state of this StdLogger. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log. + err will always be nil; it's there for interface-compat. */ -func (l *StdLogger) DoDebug(d bool) { +func (l *StdLogger) DoDebug(d bool) (err error) { + l.EnableDebug = d + + return } -// SetPrefix sets the prefix for this StdLogger. -func (l *StdLogger) SetPrefix(prefix string) { - l.Prefix = prefix + " " - l.Logger.SetPrefix(prefix + " ") +/* + SetPrefix sets the prefix for this StdLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *StdLogger) SetPrefix(prefix string) (err error) { + + l.Prefix = prefix + if prefix != "" { + l.Prefix = strings.TrimRight(l.Prefix, " ") + " " + } + l.Logger.SetPrefix(l.Prefix) + + return } // Alert writes an ALERT-level message to this StdLogger. diff --git a/logging/funcs_sysd_linux.go b/logging/funcs_sysd_linux.go index fd0bfe6..bcefb2c 100644 --- a/logging/funcs_sysd_linux.go +++ b/logging/funcs_sysd_linux.go @@ -7,24 +7,33 @@ import ( "github.com/coreos/go-systemd/journal" ) -// Setup sets up/configures a SystemDLogger and prepares it for use. -func (l *SystemDLogger) Setup() { +/* + Setup sets up/configures a SystemDLogger and prepares it for use. + err will always be nil; it's there for interface-compat. +*/ +func (l *SystemDLogger) Setup() (err error) { // NOOP - _ = "" + return } -// Shutdown cleanly shuts down a SystemDLogger. -func (l *SystemDLogger) Shutdown() { +/* + Shutdown cleanly shuts down a SystemDLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *SystemDLogger) Shutdown() (err error) { // NOOP - _ = "" + return } -// GetPrefix returns the prefix used by this SystemDLogger. -func (l *SystemDLogger) GetPrefix() (prefix string) { +/* + GetPrefix returns the prefix used by this SystemDLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *SystemDLogger) GetPrefix() (prefix string, err error) { prefix = l.Prefix @@ -34,14 +43,24 @@ func (l *SystemDLogger) GetPrefix() (prefix string) { /* DoDebug sets the debug state of this SystemDLogger. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log. + err will always be nil; it's there for interface-compat. */ -func (l *SystemDLogger) DoDebug(d bool) { +func (l *SystemDLogger) DoDebug(d bool) (err error) { + l.EnableDebug = d + + return } -// SetPrefix sets the prefix for this SystemDLogger. -func (l *SystemDLogger) SetPrefix(prefix string) { +/* + SetPrefix sets the prefix for this SystemDLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *SystemDLogger) SetPrefix(prefix string) (err error) { + l.Prefix = prefix + + return } // Alert writes an ALERT-level message to this SystemDLogger. diff --git a/logging/funcs_syslog_linux.go b/logging/funcs_syslog_linux.go index ce79cc1..451122e 100644 --- a/logging/funcs_syslog_linux.go +++ b/logging/funcs_syslog_linux.go @@ -4,55 +4,79 @@ import ( "fmt" "log" "log/syslog" + + `r00t2.io/goutils/multierr` ) // Setup sets up/configures a SyslogLogger and prepares it for use. -func (l *SyslogLogger) Setup() { +func (l *SyslogLogger) Setup() (err error) { - var err error + var errs *multierr.MultiError = multierr.NewMultiError(nil) if l.alert, err = syslog.New(syslog.LOG_ALERT|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Alert") + errs.AddError(err) + err = nil } if l.crit, err = syslog.New(syslog.LOG_CRIT|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Crit") + errs.AddError(err) + err = nil } if l.debug, err = syslog.New(syslog.LOG_DEBUG|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Debug") + errs.AddError(err) + err = nil } if l.emerg, err = syslog.New(syslog.LOG_EMERG|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Emerg") + errs.AddError(err) + err = nil } if l.err, err = syslog.New(syslog.LOG_ERR|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Err") + errs.AddError(err) + err = nil } if l.info, err = syslog.New(syslog.LOG_INFO|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Info") + errs.AddError(err) + err = nil } if l.notice, err = syslog.New(syslog.LOG_NOTICE|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Notice") + errs.AddError(err) + err = nil } if l.warning, err = syslog.New(syslog.LOG_WARNING|syslogFacility, l.Prefix); err != nil { - log.Panicln("could not open log for Warning") + errs.AddError(err) + err = nil } + if errs.Count() > 0 { + err = errs + } + + return } // Shutdown cleanly shuts down a SyslogLogger. -func (l *SyslogLogger) Shutdown() { +func (l *SyslogLogger) Shutdown() (err error) { - var err error + var errs *multierr.MultiError = multierr.NewMultiError(nil) for _, i := range []*syslog.Writer{l.alert, l.crit, l.debug, l.emerg, l.err, l.info, l.notice, l.warning} { if err = i.Close(); err != nil { - log.Panicf("could not close log %#v\n", i) + errs.AddError(err) + err = nil } } + if errs.Count() > 0 { + err = errs + } + + return } -// GetPrefix returns the prefix used by this SyslogLogger. -func (l *SyslogLogger) GetPrefix() (prefix string) { +/* + GetPrefix returns the prefix used by this SyslogLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *SyslogLogger) GetPrefix() (prefix string, err error) { prefix = l.Prefix @@ -62,15 +86,30 @@ func (l *SyslogLogger) GetPrefix() (prefix string) { /* DoDebug sets the debug state of this SyslogLogger. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log. + err will always be nil; it's there for interface-compat. */ -func (l *SyslogLogger) DoDebug(d bool) { +func (l *SyslogLogger) DoDebug(d bool) (err error) { + l.EnableDebug = d + + return } // SetPrefix sets the prefix for this SyslogLogger. -func (l *SyslogLogger) SetPrefix(prefix string) { +func (l *SyslogLogger) SetPrefix(prefix string) (err error) { + l.Prefix = prefix - l.Setup() + + // We need to close the current loggers first. + if err = l.Shutdown(); err != nil { + return + } + + if err = l.Setup(); err != nil { + return + } + + return } // Alert writes an ALERT-level message to this SyslogLogger. diff --git a/logging/funcs_test.go b/logging/funcs_test.go new file mode 100644 index 0000000..2b43acc --- /dev/null +++ b/logging/funcs_test.go @@ -0,0 +1 @@ +package logging diff --git a/logging/funcs_windows.go b/logging/funcs_windows.go index 14a43cf..284437c 100644 --- a/logging/funcs_windows.go +++ b/logging/funcs_windows.go @@ -22,6 +22,9 @@ import ( A pointer to a WinEventID struct may be specified for eventIDs to map extended logging levels (as Windows only supports three levels natively). If it is nil, a default one (DefaultEventID) will be used. + logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and + https://pkg.go.dev/log#pkg-constants for details. + logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to, it will be used (assuming you have no higher-level loggers available). @@ -32,13 +35,14 @@ import ( If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type), use GetMultiLogger. */ -func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths ...string) (logger Logger, err error) { +func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) { var logPath string var logFlags bitmask.MaskBit var exists bool var success bool var ckLogPaths []string + var prefix string if strings.TrimSpace(source) == "" { err = errors.New("invalid source for Windows logging") @@ -94,6 +98,7 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths . StdLogger: StdLogger{ Prefix: source, EnableDebug: enableDebug, + LogFlags: logConfigFlags, }, Path: logPath, } @@ -101,13 +106,25 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths . logger = &StdLogger{ Prefix: source, EnableDebug: enableDebug, + LogFlags: logConfigFlags, } } } - logger.Setup() + if err = logger.Setup(); err != nil { + return + } + if source != "\x00" { + if err = logger.SetPrefix(source); err != nil { + return + } + } - logger.Info("logger initialized of type %T with source %v", logger, logger.GetPrefix()) + if prefix, err = logger.GetPrefix(); err != nil { + return + } + + logger.Debug("logger initialized of type %T with source %v", logger, prefix) return diff --git a/logging/funcs_winlogger_windows.go b/logging/funcs_winlogger_windows.go index 79ce40f..b76367c 100644 --- a/logging/funcs_winlogger_windows.go +++ b/logging/funcs_winlogger_windows.go @@ -3,17 +3,29 @@ package logging import ( `errors` `fmt` + `os` + `os/exec` + `syscall` + `golang.org/x/sys/windows/registry` `golang.org/x/sys/windows/svc/eventlog` + `r00t2.io/sysutils/paths` ) -// Setup sets up/configures a WinLogger and prepares it for use. -func (l *WinLogger) Setup() { +/* + Setup sets up/configures a WinLogger and prepares it for use. + This will fail with an Access Denied (the first time, at least) unless running with elevated permissions unless WinLogger.Prefix is + a registered Event Log source. - var err error + If a failure occurs while trying to open the log with the given WinLogger.Prefix ("source"), a new Event Log source will be registered. + If WinLogger.Executable is not empty at the time of calling WinLogger.Setup (or WinLogger.ForceService is true), + eventlog.Install will be used (with the WinLogger.ExpandKey field). + Otherwise eventlog.InstallAsEventCreate will be used. +*/ +func (l *WinLogger) Setup() (err error) { /* - First a sanity check on the EventIDs. + A sanity check on the EventIDs. Since we use eventcreate, all Event IDs must be 1 <= eid <= 1000. */ for _, eid := range []uint32{ @@ -26,45 +38,100 @@ func (l *WinLogger) Setup() { l.eids.Notice, l.eids.Warning, } { - if !((eid <= 1000) && (1 <= eid)) { - err = errors.New("event IDs must be between 1 and 1000 inclusive") - panic(err) + if !((eid <= EIDMax) && (EIDMin <= eid)) { + err = ErrBadEid + return } } - if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { - if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil { - // It's an error we want to panic on. - panic(err) - } else { - // It already exists, so ignore the error. - err = nil - } + if err = l.Install(); err != nil { + return } if l.elog, err = eventlog.Open(l.Prefix); err != nil { - panic(err) + return } + return +} + +// Install installs/registers the WinLogger Event Log interface. You most likely do not need to run this directly. +func (l *WinLogger) Install() (err error) { + + var exists bool + var doNotCreate bool + var useEventCreate bool = true + + if doNotCreate, err = l.Exists(); err != nil { + return + } else if !doNotCreate { + if l.Executable != "" { + if l.Executable, err = exec.LookPath(l.Executable); err != nil { + return + } + useEventCreate = false + } else if l.ForceService { + if l.Executable, err = exec.LookPath(os.Args[0]); err != nil { + return + } + useEventCreate = false + } + if !useEventCreate { + if exists, err = paths.RealPathExists(&l.Executable); err != nil { + return + } else if !exists { + err = ErrBadBinPath + return + } + if err = eventlog.Install(l.Prefix, l.Executable, l.ExpandKey, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { + return + } + } else { + if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { + return + } + } + } + + return + } -// Shutdown cleanly shuts down a WinLogger. -func (l *WinLogger) Shutdown() { - - var err error - - if err = l.elog.Close(); err != nil { - panic(err) - } +// Remove uninstalls a registered WinLogger source. +func (l *WinLogger) Remove() (err error) { if err = eventlog.Remove(l.Prefix); err != nil { - panic(err) + return } + return } -// GetPrefix returns the prefix used by this WinLogger. -func (l *WinLogger) GetPrefix() (prefix string) { +/* + Shutdown cleanly shuts down a WinLogger but keep the source registered. Use WinLogger.Remove + (or set WinLogger.RemoveOnClose to true before calling WinLogger.Shutdown) to remove the registered source. +*/ +func (l *WinLogger) Shutdown() (err error) { + + if err = l.elog.Close(); err != nil { + // TODO: check for no access or file not exists syscall errors? + return + } + + if l.RemoveOnClose { + if err = l.Remove(); err != nil { + return + } + } + + return +} + +/* + GetPrefix returns the prefix used by this WinLogger. + err will always be nil; it's there for interface-compat. +*/ +func (l *WinLogger) GetPrefix() (prefix string, err error) { prefix = l.Prefix @@ -74,43 +141,55 @@ func (l *WinLogger) GetPrefix() (prefix string) { /* DoDebug sets the debug state of this WinLogger. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log. + err will always be nil; it's there for interface-compat. */ -func (l *WinLogger) DoDebug(d bool) { +func (l *WinLogger) DoDebug(d bool) (err error) { l.EnableDebug = d + return } // SetPrefix sets the prefix for this WinLogger. -func (l *WinLogger) SetPrefix(prefix string) { +func (l *WinLogger) SetPrefix(prefix string) (err error) { - var err error + // To properly change the prefix, we need to tear down the old event log and create a new one. + if err = l.Shutdown(); err != nil { + return + } l.Prefix = prefix - // To properly change the prefix, we need to tear down the old event log and create a new one. - if err = l.elog.Close(); err != nil { - panic(err) + if err = l.Setup(); err != nil { + return } - if err = eventlog.Remove(l.Prefix); err != nil { - panic(err) - } + return +} - if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { - if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil { - // It's an error we want to panic on. - panic(err) - } else { - // It already exists, so ignore the error. +// Exists indicates if the WinLogger.Prefix is a registered source or not. +func (l *WinLogger) Exists() (e bool, err error) { + + var regKey registry.Key + var subKey registry.Key + + if regKey, err = registry.OpenKey(registry.LOCAL_MACHINE, eventLogRegistryKey, registry.READ); err != nil { + return + } + defer regKey.Close() + + if subKey, err = registry.OpenKey(regKey, l.Prefix, registry.READ); err != nil { + if errors.Is(err, syscall.ERROR_FILE_NOT_FOUND) { + e = false err = nil } + return } + defer subKey.Close() - if l.elog, err = eventlog.Open(l.Prefix); err != nil { - panic(err) - } + e = true + return } // Alert writes an ALERT-level message to this WinLogger. diff --git a/logging/types.go b/logging/types.go index 6abb2c7..a32b9f3 100644 --- a/logging/types.go +++ b/logging/types.go @@ -17,11 +17,11 @@ type Logger interface { Info(s string, v ...interface{}) (err error) Notice(s string, v ...interface{}) (err error) Warning(s string, v ...interface{}) (err error) - DoDebug(d bool) - SetPrefix(p string) - GetPrefix() (p string) - Setup() - Shutdown() + DoDebug(d bool) (err error) + SetPrefix(p string) (err error) + GetPrefix() (p string, err error) + Setup() (err error) + Shutdown() (err error) } /* @@ -38,20 +38,42 @@ type StdLogger struct { EnableDebug bool // Prefix indicates the prefix for log entries; in shared logs, this helps differentiate the source. Prefix string + /* + LogFlags control some of the formatting options presented as an OR'd value. + See https://pkg.go.dev/log#pkg-constants for flag details. + e.g.: + *StdLogger.LogFlags = log.Ldate | log.Lmicroseconds | log.Llongfile | log.LUTC // a very detailed log output + *StdLogger.LogFlags = log.Ldate | log.Ltime // the flags used by log.Default() (also available as simply log.LstdFlags) + The default is 0; no flags (no output except prefix if non-empty and message). + You will need to run *StdLogger.Shutdown and then *StdLogger.Setup again if you wish to change this. + */ + LogFlags int } -// FileLogger uses a StdLogger with a file handle writer to write to the file given at Path. +/* + FileLogger uses a StdLogger with a file handle writer to write to the file given at Path. + + NOTE: If you wish to change the FileLogger.StdLogger.LogFlags, do *not* run FileLogger.StdLogger.Setup after doing so as this + will instead create a logger detached from the file handler. Instead, be sure to call FileLogger.Setup. + (Alternatively, run FileLogger.Shutdown and replace your logger with a new FileLogger.) +*/ type FileLogger struct { // StdLogger is used for the log formation and handling. See StdLogger for more details. StdLogger // Path is the path to the logfile. Path string /* - EnableStdOut is true if the log will send to STDOUT as well as the file. + EnableStdOut is true if the log will send to STDOUT as well as the file (and STDERR if FileLogger.EnableStdErr == true). + If false (default), it will only (silently) write to the log file. + You will need to run FileLogger.Shutdown and then FileLogger.Setup again if you wish to change this. + */ + EnableStdOut bool + /* + EnableStdErr is true if the log will send to STDERR as well as the file (and STDOUT if FileLogger.EnableStdOut == true). If false (default), it will only (silently) write to the log file. You will need to run *FileLogger.Shutdown and then *FileLogger.Setup again if you wish to change this. */ - EnableStdOut bool + EnableStdErr bool // writer is used for the writing out of the log file. writer *os.File } diff --git a/logging/types_windows.go b/logging/types_windows.go index 0941d06..f065526 100644 --- a/logging/types_windows.go +++ b/logging/types_windows.go @@ -4,13 +4,60 @@ import ( `golang.org/x/sys/windows/svc/eventlog` ) +// WinLogger is used for logging to the Windows Event Log. These entries are viewable in the Event Viewer application, under "Windows Logs > Application". type WinLogger struct { + /* + EnableDebug indicates if the debug filter should be disabled (true) or if the filter should be enabled (false). + This prevents potential data leak of sensitive information, as some loggers (e.g. FileLogger) will otherwise write all messages. + */ EnableDebug bool - Prefix string - elog *eventlog.Log - eids *WinEventID + /* + Prefix is used as the Event Log "Source". It's named as Prefix to retain compatability with methods in the Logger interface. + */ + Prefix string + /* + Executable is used as the path for the executable implementing this logger. + If non-empty, it enables the "service" mode of Event Log (intended for "installed" software that's expected + to exist as a specific path reliably). + It can be a file within the PATHs or an absolute/relative path; an attempt to resolve the actual path will be made. If this fails or the file + does not exist, an error will be raised. + */ + Executable string + /* + ExpandKey is only used if Executable is non-empty and valid and/or ForceService is true. + If true, the WinLogger will be installed/registered with the REG_EXPAND_SZ mode - otherwise it will be installed as REG_SZ. + See the definition for the two at https://docs.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types for further details. + If you're unsure which you want, it's probably REG_SZ (WinLogger.ExpandKey == false), which is the default. + */ + ExpandKey bool + /* + ForceService, if true, will enforce WinLogger to be used as if Executable is populated and valid (it will use os.Args[0] as the Executable path). + If Executable is empty but ForceService is true and os.Args[0] is empty or invalid (not a real path, etc.), an error will be raised. + */ + ForceService bool + // RemoveOnClose should be true if the logger should be removed/unregistered from the Registry upon calling WinLogger.Shutdown. + RemoveOnClose bool + // elog is the actual writer to the Event Log. + elog *eventlog.Log + // eids is used to look up what event ID to use when writing to a WinLogger.elog. + eids *WinEventID } +/* + WinEventID is a collection of Event IDs to use for a WinLogger. + Because Event Log only supports three entry types (informational, warning, or error), + these event IDs allow you to filter the messages in a slightly more granular way. They map to their corresponding method name/Logger level. + However, this means that a WinLogger does not support custom event IDs (and thus you cannot assign individual event IDs to specific errors). + This is the price of convenience. + + An additional method set may be added in the future to support this, but this is currently an unplanned feature. + + Event IDs *must* be between the constants EIDMin and EIDMax (inclusive) unless the WinLogger is used in "service" mode + (see WinLogger.Executable and WinLogger.ForceService). + + If you need recommended defaults, you may want to use the Event* constants (e.g. EventAlert, EventDebug, etc.) + or even use the pre-populated DefaultEventID (which is assigned the above Event* constants). +*/ type WinEventID struct { Alert, Crit,