Compare commits

...

17 Commits

Author SHA1 Message Date
brent saner ae49f42c0c
v1.6.0
Added bitmask/MaskBit.Copy()
2024-04-14 01:54:59 -04:00
brent saner b87934e8a9
v1.5.0
Added structutils
2024-04-13 13:04:17 -04:00
brent s 70d6c2cbb3
updating logging TODO 2023-07-12 23:16:31 -04:00
brent s. a445a51c0d
Adding GetDebug method to loggers. 2022-09-07 06:03:28 -04:00
brent s. a2a849600b
Add docs for NullLogger. 2022-09-06 01:01:39 -04:00
brent s. 94145fb4c7
Add NullLogger to MultiLogger. 2022-03-13 13:34:24 -04:00
brent s. 81a2d308f0
Add NullLogger.
For when you need a Logger but don't want one. ;)
2022-03-13 13:29:31 -04:00
brent s. c4b3c6441a
adding Bytes() to MaskBit 2022-02-01 18:27:45 -05:00
brent s. 1c5abd4083
modifying bitmask to allow specifying an explicit value, and changing to uint instead of uint8 2022-02-01 15:36:56 -05:00
brent s. d98363c0d7
Finalizing for 1.2.0 2022-01-16 06:55:29 -05:00
brent s. 39e0a1fd43
Windows Event Log, error output
Adding more Event Log support, and modifying the loggers so they return
errors in their operational functions.
2022-01-16 02:05:42 -05:00
brent s. 3d0d420454
fixing windows loggers 2022-01-06 04:16:44 -05:00
brent s. ef0a4d825d
multierror and multilogger cleaned up. 2022-01-05 16:36:20 -05:00
brent s. 4addc44c0f
fixing multilogger so that v is properly nil instead of an empty slice if not specified 2022-01-05 16:16:24 -05:00
brent s. 0e01306637
finalizing logging and multierror 2022-01-05 05:15:38 -05:00
brent s. 3975f8b11f
do bitmask better, add (COMPLETELY 100% EXPERIMENTAL NOT DONE YET) eventlog support to logger 2021-12-15 04:49:03 -05:00
brent s d5b1d449e5
adding small usage note for bitmasks 2021-07-27 22:30:10 -04:00
42 changed files with 3915 additions and 270 deletions

5
.gitignore vendored
View File

@ -28,6 +28,11 @@

# Test binary, built with `go test -c`
*.test
# But DO include the actual tests.
!_test.go
!*_test.go
!*_test_*.go
!*_test/

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

127
bitmask/bitmask.go Normal file
View File

@ -0,0 +1,127 @@
package bitmask

import (
"bytes"
"encoding/binary"
"errors"
"math/bits"
)

// MaskBit is a flag container.
type MaskBit uint

/*
NewMaskBit is a convenience function.
It will return a MaskBit with a (referenced) value of 0, so set your consts up accordingly.

It is highly recommended to set this default as a "None" flag (separate from your iotas!)
as shown in the example.
*/
func NewMaskBit() (m *MaskBit) {

m = new(MaskBit)

return
}

// NewMaskBitExplicit is like NewMaskBit, but allows you to specify a non-zero (0x0) value.
func NewMaskBitExplicit(value uint) (m *MaskBit) {

var v MaskBit = MaskBit(value)

m = &v

return
}

// HasFlag is true if m has MaskBit flag set/enabled.
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {

var b MaskBit = *m

if b&flag != 0 {
r = true
}
return
}

// AddFlag adds MaskBit flag to m.
func (m *MaskBit) AddFlag(flag MaskBit) {

*m |= flag

return
}

// ClearFlag removes MaskBit flag from m.
func (m *MaskBit) ClearFlag(flag MaskBit) {

*m &= flag

return
}

// ToggleFlag switches MaskBit flag in m to its inverse; if true, it is now false and vice versa.
func (m *MaskBit) ToggleFlag(flag MaskBit) {

*m ^= flag

return
}

/*
Bytes returns the current value of a MasBit as a byte slice (big-endian).

If trim is false, b will (probably) be 4 bytes long if you're on a 32-bit size system,
and b will (probably) be 8 bytes long if you're on a 64-bit size system. You can determine
the size of the resulting slice via (math/)bits.UintSize / 8.

If trim is true, it will trim leading null bytes (if any). This will lead to an unpredictable
byte slice length in b, but is most likely preferred for byte operations.

*/
func (m *MaskBit) Bytes(trim bool) (b []byte) {

var b2 []byte
var size int = bits.UintSize / 8
var err error

b2 = make([]byte, size)

switch s := bits.UintSize; s {
case 32:
binary.BigEndian.PutUint32(b2[:], uint32(*m))
case 64:
binary.BigEndian.PutUint64(b2[:], uint64(*m))
default:
err = errors.New("unsupported Uint/system bit size")
panic(err)
}

if trim {
b = bytes.TrimLeft(b2, "\x00")
return
} else {
b = b2
return
}

return
}

// Copy returns a pointer to a (new) copy of a MaskBit.
func (m *MaskBit) Copy() (newM *MaskBit) {

newM = new(MaskBit)
*newM = *m

return
}

// Value returns the current raw uint value of a MaskBit.
func (m *MaskBit) Value() (v uint) {

v = uint(*m)

return
}

53
bitmask/doc.go Normal file
View File

@ -0,0 +1,53 @@
/*
Package bitmask handles a flag-like opt/bitmask system.

See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information.

To use this, set constants like thus:

package main

import (
"r00t2.io/goutils/bitmask"
)

const OPTNONE bitmask.MaskBit = 0
const (
OPT1 bitmask.MaskBit = 1 << iota
OPT2
OPT3
// ...
)

var MyMask *bitmask.MaskBit

func main() {
MyMask = bitmask.NewMaskBit()

MyMask.AddFlag(OPT1)
MyMask.AddFlag(OPT3)

_ = MyMask
}

This would return true:

MyMask.HasFlag(OPT1)

As would this:

MyMask.HasFlag(OPT3)

But this would return false:

MyMask.HasFlag(OPT2)

If you need something with more flexibility (as always, at the cost of complexity),
you may be interested in one of the following libraries:

. github.com/alvaroloes/enumer
. github.com/abice/go-enum
. github.com/jeffreyrichter/enum/enum

*/
package bitmask

4
go.mod
View File

@ -4,5 +4,7 @@ go 1.16

require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
r00t2.io/sysutils v0.0.0-20210224054841-55ac47c86928
github.com/google/uuid v1.3.0
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
r00t2.io/sysutils v1.1.1
)

9
go.sum
View File

@ -1,5 +1,8 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/jszwec/csvutil v1.5.0/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg=
r00t2.io/sysutils v0.0.0-20210224054841-55ac47c86928 h1:aYEn20eguqsmqT3J9VjkzdhyPwmOVDGzzffcEfV18a4=
r00t2.io/sysutils v0.0.0-20210224054841-55ac47c86928/go.mod h1:XzJkBF6SHAODEszJlOcjtGoTHwYnZZNmseA6PyOujes=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=

View File

@ -1,3 +1,16 @@
- Implement code line/func/etc. (only for debug?):
https://stackoverflow.com/a/24809646
https://golang.org/pkg/runtime/#Caller
-- log.LlongFile and log.Lshortfile flags don't currently work properly for StdLogger/FileLogger; they refer to the file in logging package rather than the caller.

- StdLogger2; where stdout and stderr are both logged to depending on severity level.
- make configurable via OR bitmask

- Suport remote loggers? (eventlog, syslog, systemd)

- JSON logger? YAML logger? XML logger?

- DOCS.
-- Done, but flesh out.

- Implement io.Writer interfaces

View File

@ -1,29 +1,14 @@
package logging

import (
`log/syslog`
`os`

`r00t2.io/goutils/types`
)

const (
devlog string = "/dev/log"
// logPerm is the octal mode to use for testing the file.
logPerm os.FileMode = 0600
// logPrefix is the default log prefix.
logPrefix string = "GOLANG PROGRAM"
appendFlags int = os.O_APPEND|os.O_CREATE|os.O_WRONLY
syslogFacility syslog.Priority = syslog.LOG_USER
)

// Flags for logger configuration
const (
LogUndefined types.MaskBit = 1 << iota
LogJournald
LogSyslog
LogFile
LogStdout
)

var (
defLogPaths = []string{"/var/log/golang/program.log", "~/.local/log/golang/program.log"}
// appendFlags are the flags used for testing the file (and opening/writing).
appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY
)

36
logging/consts_linux.go Normal file
View File

@ -0,0 +1,36 @@
package logging

import (
`log/syslog`

`r00t2.io/goutils/bitmask`
)

const (
// devlog is the path to the syslog char device.
devlog string = "/dev/log"
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
syslogFacility syslog.Priority = syslog.LOG_USER
)

// Flags for logger configuration. These are used internally.
const (
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 1 << iota
// LogJournald flags a SystemDLogger Logger type.
LogJournald
// LogSyslog flags a SyslogLogger Logger type.
LogSyslog
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)

var (
// defLogPaths indicates default log paths.
defLogPaths = []string{
"/var/log/golang/program.log",
"~/.local/log/golang/program.log",
}
)

39
logging/consts_test.go Normal file
View File

@ -0,0 +1,39 @@
package logging

import (
`log`
)

/*
The following are strings written to the Logger in the various tests.
The %v is populated with the name of the type of Logger.
*/
const (
testAlert string = "This is a test ALERT-priority log message for logger %v."
testCrit string = "This is a test CRITICAL-priority (CRIT) log message for logger %v."
testDebug string = "This is a test DEBUG-priority log message for logger %v."
testEmerg string = "This is a test EMERGENCY-priority (EMERG) log message for logger %v."
testErr string = "This is a test ERROR-priority (ERR) log message for logger %v."
testInfo string = "This is a test INFO-priority log message for logger %v."
testNotice string = "This is a test NOTICE-priority log message for logger %v."
testWarning string = "This is a test WARNING-priority log message for logger %v."
)

// Prefixes to use for tests.
const (
// TestLogPrefix is used as the initial prefix.
TestLogPrefix string = "LOGGING_TESTRUN"
// TestLogAltPrefix is used as the alternative prefix to Logger.SetPrefix.
TestLogAltPrefix string = "LOGGING_TESTRUN_ALT"
)

const (
// EnvVarKeepLog is the env var key/var name to use to suppress removal of FileLogger.Path after tests complete.
EnvVarKeepLog string = "LOGGING_KEEP_TEMPLOG"
)

const (
// logFlags are used to set the log flags for StdLogger (and FileLogger.StdLogger).
// logFlags int = log.Ldate | log.Lmicroseconds | log.Llongfile | log.LUTC
logFlags int = log.Ldate | log.Lmicroseconds | log.Lshortfile | log.LUTC
)

68
logging/consts_windows.go Normal file
View File

@ -0,0 +1,68 @@
package logging

import (
`os`
`path/filepath`

`r00t2.io/goutils/bitmask`
)

// Flags for logger configuration. These are used internally.
const (
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 1 << iota
// LogWinLogger indicates a WinLogger Logger type (Event Log).
LogWinLogger
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)

var (
// defLogPaths indicates default log paths.
defLogPaths = []string{
filepath.Join(os.Getenv("ALLUSERSPROFILE"), "golang", "program.log"), // C:\ProgramData\log\golang\program.log
filepath.Join(os.Getenv("LOCALAPPDATA"), "log", "golang", "program.log"), // C:\Users\<username>\AppData\Local\log\golang\program.log
}
)

/*
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{
Alert: EventAlert,
Crit: EventCrit,
Debug: EventDebug,
Emerg: EventEmerg,
Err: EventErr,
Info: EventInfo,
Notice: EventNotice,
Warning: EventWarning,
}

// Default Event IDs for WinEventID (DefaultEventID, specifically).
const (
EventAlert uint32 = 1 << iota
EventCrit
EventDebug
EventEmerg
EventErr
EventInfo
EventNotice
EventWarning
)

49
logging/doc.go Normal file
View File

@ -0,0 +1,49 @@
/*
Package logging implements and presents various loggers under a unified interface, making them completely swappable.

These particular loggers (logging.Logger) available are:

NullLogger
StdLogger
FileLogger
SystemDLogger (Linux only)
SyslogLogger (Linux only)
WinLogger (Windows only)

There is a seventh type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger.

Note that for some Loggers, the prefix may be modified - "literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix.
If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger.

Every logging.Logger type has the following methods that correspond to certain "levels".

Alert(s string, v ...interface{}) (err error)
Crit(s string, v ...interface{}) (err error)
Debug(s string, v ...interface{}) (err error)
Emerg(s string, v ...interface{}) (err error)
Err(s string, v ...interface{}) (err error)
Info(s string, v ...interface{}) (err error)
Notice(s string, v ...interface{}) (err error)
Warning(s string, v ...interface{}) (err error)

Not all loggers implement the concept of levels, so approximations are made when/where possible.

In each of the above methods, s is the message that is optionally in a fmt.Sprintf-compatible format.
If it is, the values to fmt.Sprintf can be passed as v.

Note that in the case of a MultiLogger, err (if not nil) will be a (r00t2.io/goutils/)multierr.MultiError.

logging.Logger types also have the following methods:

DoDebug(d bool) (err error)
GetDebug() (d bool)
SetPrefix(p string) (err error)
GetPrefix() (p string, err error)
Setup() (err error)
Shutdown() (err error)

In some cases, Logger.Setup and Logger.Shutdown are no-ops. In other cases, they perform necessary initialization/cleanup and closing of the logger.
It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type.
*/
package logging

17
logging/errs.go Normal file
View File

@ -0,0 +1,17 @@
package logging

import (
`errors`
)

var (
// ErrExistingLogger indicates that the user attempted to add a Logger to a MultiLogger using an already-existing identifier.
ErrExistingLogger error = errors.New("a Logger with that identifier already exists; please remove it first")
/*
ErrInvalidFile indicates that the user attempted to add a FileLogger to a MultiLogger but the file doesn't exist,
exists with too restrictive perms to write/append to, and/or could not be created.
*/
ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created")
// ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist.
ErrNoEntry error = errors.New("the Logger specified to be removed does not exist")
)

18
logging/errs_linux.go Normal file
View File

@ -0,0 +1,18 @@
package logging

import (
`errors`
`fmt`
)

var (
// ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable.
ErrNoSysD error = errors.New("a systemd (journald) Logger was requested but systemd is unavailable on this system")
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
/*
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
the Syslog char device file is... not actually a char device file.
*/
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
)

15
logging/errs_windows.go Normal file
View File

@ -0,0 +1,15 @@
package logging

import (
`errors`
`fmt`
)

var (
// ErrBadBinPath is returned if installing a binary-registered Event Log source instead of using EventCreate.exe.
ErrBadBinPath error = errors.New("evaluated binary path does not actually exist")
// ErrBadPerms is returned if an access denied error is received when attempting to register, write to, close, etc. a source without proper perms.
ErrBadPerms error = errors.New("access denied when attempting to register Event Log source")
// ErrBadEid is returned if an event ID is within an invalid range.
ErrBadEid error = errors.New(fmt.Sprintf("event IDs must be between %v and %v inclusive", EIDMin, EIDMax))
)

View File

@ -1,134 +1,21 @@
package logging

import (
native "log"
"os"
"path"

"r00t2.io/goutils/types"

sysd "github.com/coreos/go-systemd/journal"
"r00t2.io/sysutils/paths"
)

var (
_ = sysd.Enabled()
_ = native.Logger{}
_ = os.Interrupt
)

// GetLogger returns an instance of Logger that best suits your system's capabilities.
// If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written.
// 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.
// 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.
func GetLogger(enableDebug bool, prefix string, logpaths ...string) (logger Logger, err error) {

var logPath string
var logflags types.MaskBit

// Configure system-supported logger(s).
if sysd.Enabled() {
// Use Journald.
logflags.AddFlag(LogJournald)
} else {
// If we can detect syslog, use that. If not, try to use a file logger (+ stdout).
// Last ditch, stdout.
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog

if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
}

if hasSyslog && !stat.Mode().IsRegular() {
logflags.AddFlag(LogSyslog)
} else {
var exists bool
var success bool
var ckLogPaths []string
logflags.AddFlag(LogStdout)
ckLogPaths = defLogPaths
if logpaths != nil {
ckLogPaths = logpaths
}
for _, p := range ckLogPaths {
if exists, _ = paths.RealPathExists(&p); exists {
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logflags.AddFlag(LogFile)
logPath = p
break
} else {
dirPath := path.Dir(p)
if err = paths.MakeDirIfNotExist(&dirPath); err != nil {
continue
}
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logflags.AddFlag(LogFile)
logPath = p
break
}
}
}
}

if logflags.HasFlag(LogJournald) {
logger = &SystemDLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
} else {
if logflags.HasFlag(LogSyslog) {
logger = &SyslogLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
} else {
if logflags.HasFlag(LogFile) {
logger = &FileLogger{
StdLogger: StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
},
Path: logPath,
}
} else {
logger = &StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
}
}
}

logger.Setup()
if prefix != "\x00" {
logger.SetPrefix(prefix)
}

logger.Info("logger initialized of type %T with prefix %v", logger, logger.GetPrefix())

return
}

// testOpen attempts to open a file for writing to test for suitability as a LogFile path.
func testOpen(path string) (success bool, err error) {

var f *os.File

// Example #2, https://golang.org/pkg/os/#OpenFile
if f, err = os.OpenFile(path, appendFlags, logPerm); err != nil {
return
}
defer f.Close()
if err = f.Close(); err != nil {
return
}

success = true


View File

@ -1,49 +1,94 @@
package logging

import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"strings"
)

func (l *FileLogger) Setup() {
// Setup sets up/configures a FileLogger and prepares it for use.
func (l *FileLogger) Setup() (err error) {

var err error

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)
}
// https://stackoverflow.com/a/36719588/733214
multi := io.MultiWriter(os.Stdout, l.writer)
l.Logger.SetOutput(multi)
if l.writer, err = os.OpenFile(l.Path, appendFlags, logPerm); err != nil {
return
}

l.Logger = log.New(l.writer, l.Prefix, l.LogFlags)
// l.Logger.SetOutput(multi)

return
}

func (l *FileLogger) Shutdown() {

var err error
// Shutdown cleanly shuts down a FileLogger.
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
}
err = nil
return err
}

return
}

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
}

func (l *FileLogger) DoDebug(d bool) {
/*
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) (err error) {

l.EnableDebug = d

return
}

func (l *FileLogger) SetPrefix(prefix string) {
// GetDebug returns the debug status of this FileLogger.
func (l *FileLogger) GetDebug() (d bool) {

d = l.EnableDebug

return
}

/*
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)
if prefix != "" {
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
}
l.Logger.SetPrefix(l.Prefix)

return
}

// Alert writes an ALERT-level message to this FileLogger.
func (l *FileLogger) Alert(s string, v ...interface{}) (err error) {

var msg string
@ -59,6 +104,7 @@ func (l *FileLogger) Alert(s string, v ...interface{}) (err error) {
return
}

// Crit writes an CRITICAL-level message to this FileLogger.
func (l *FileLogger) Crit(s string, v ...interface{}) (err error) {

var msg string
@ -74,6 +120,7 @@ func (l *FileLogger) Crit(s string, v ...interface{}) (err error) {
return
}

// Debug writes a DEBUG-level message to this FileLogger.
func (l *FileLogger) Debug(s string, v ...interface{}) (err error) {

if !l.EnableDebug {
@ -93,6 +140,7 @@ func (l *FileLogger) Debug(s string, v ...interface{}) (err error) {
return
}

// Emerg writes an EMERGENCY-level message to this FileLogger.
func (l *FileLogger) Emerg(s string, v ...interface{}) (err error) {

var msg string
@ -108,6 +156,7 @@ func (l *FileLogger) Emerg(s string, v ...interface{}) (err error) {
return
}

// Err writes an ERROR-level message to this FileLogger.
func (l *FileLogger) Err(s string, v ...interface{}) (err error) {

var msg string
@ -123,6 +172,7 @@ func (l *FileLogger) Err(s string, v ...interface{}) (err error) {
return
}

// Info writes an INFO-level message to this FileLogger.
func (l *FileLogger) Info(s string, v ...interface{}) (err error) {

var msg string
@ -138,6 +188,7 @@ func (l *FileLogger) Info(s string, v ...interface{}) (err error) {
return
}

// Notice writes a NOTICE-level message to this FileLogger.
func (l *FileLogger) Notice(s string, v ...interface{}) (err error) {

var msg string
@ -153,6 +204,7 @@ func (l *FileLogger) Notice(s string, v ...interface{}) (err error) {
return
}

// Warning writes a WARNING/WARN-level message to this FileLogger.
func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {

var msg string
@ -168,6 +220,7 @@ func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {
return
}

// renderWrite prepares/formats a log message to be written to this FileLogger.
func (l *FileLogger) renderWrite(msg, prio string) {

s := fmt.Sprintf("[%v] %v", prio, msg)

146
logging/funcs_linux.go Normal file
View File

@ -0,0 +1,146 @@
package logging

import (
native `log`
`os`
`path`

sysd `github.com/coreos/go-systemd/journal`
`r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths`
)

var (
_ = sysd.Enabled()
_ = native.Logger{}
_ = os.Interrupt
)

/*
GetLogger returns an instance of Logger that best suits your system's capabilities.

If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written.

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.

If you call GetLogger, you will only get a single ("best") logger your system supports.
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, 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() {
// Use Journald.
logFlags.AddFlag(LogJournald)
} else {
// If we can detect syslog, use that. If not, try to use a file logger (+ stdout).
// Last ditch, stdout.
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog

if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
}

if hasSyslog && !stat.Mode().IsRegular() {
logFlags.AddFlag(LogSyslog)
} else {
var exists bool
var success bool
var ckLogPaths []string

logFlags.AddFlag(LogStdout)
ckLogPaths = defLogPaths
if logPaths != nil {
ckLogPaths = logPaths
}
for _, p := range ckLogPaths {
if exists, _ = paths.RealPathExists(&p); exists {
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
} else {
dirPath := path.Dir(p)
if err = paths.MakeDirIfNotExist(dirPath); err != nil {
continue
}
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
}
}
}
}

if logFlags.HasFlag(LogJournald) {
logger = &SystemDLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
} else {
if logFlags.HasFlag(LogSyslog) {
logger = &SyslogLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
} else {
if logFlags.HasFlag(LogFile) {
logger = &FileLogger{
StdLogger: StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
},
Path: logPath,
}
} else {
logger = &StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
}
}
}
}

if prefix != "\x00" {
if err = logger.SetPrefix(prefix); err != nil {
return
}
}
if err = logger.Setup(); err != nil {
return
}

if currentPrefix, err = logger.GetPrefix(); err != nil {
return
}

logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix)

return
}

270
logging/funcs_linux_test.go Normal file
View File

@ -0,0 +1,270 @@
package logging

import (
`fmt`
`os`
`testing`
)

/*
TestSysDLogger tests functionality for SystemDLogger.
*/
func TestSysDLogger(t *testing.T) {

var l *SystemDLogger
var ltype string = "SystemDLogger"
var prefix string
var err error

l = &SystemDLogger{
EnableDebug: true,
Prefix: TestLogPrefix,
}

if err = l.Setup(); err != nil {
t.Fatalf("error when running Setup: %v", err.Error())
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if prefix != TestLogPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else if l.EnableDebug {
t.Fatalf("did not properly set Debug filter state")
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

/*
TestSyslogLogger tests functionality for SyslogLogger.
*/
func TestSyslogLogger(t *testing.T) {

var l *SyslogLogger
var ltype string = "SyslogLogger"
var prefix string
var err error

l = &SyslogLogger{
EnableDebug: true,
Prefix: TestLogPrefix,
}

if err = l.Setup(); err != nil {
t.Fatalf("error when running Setup: %v", err.Error())
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if prefix != TestLogPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else if l.EnableDebug {
t.Fatalf("did not properly set Debug filter state")
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

// TestDefaultLogger tests GetLogger.
func TestDefaultLogger(t *testing.T) {

var l Logger
var tempfile *os.File
var tempfilePath string
var keepLog bool
var ltype string
var prefix string
var testPrefix string
var err error

if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
}
tempfilePath = tempfile.Name()
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
if err = tempfile.Close(); err != nil {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
}

if l, err = GetLogger(true, TestLogPrefix, logFlags, tempfilePath); err != nil {
t.Fatalf("error when spawning default Linux logger via GetLogger: %v", err.Error())
}

ltype = fmt.Sprintf("%T", l)

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
testPrefix = TestLogPrefix + " "
} else {
testPrefix = TestLogPrefix
}

if prefix != testPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

_, keepLog = os.LookupEnv(EnvVarKeepLog)

if !keepLog {
if err = os.Remove(tempfilePath); err != nil {
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
}
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

View File

@ -0,0 +1,372 @@
package logging

import (
"errors"
"fmt"
"sync"

"r00t2.io/goutils/multierr"
)

// Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use.
func (m *MultiLogger) Setup() (err error) {

var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logger Logger, lName string) {
var err2 error
defer wg.Done()
if err2 = logger.Setup(); err2 != nil {
errs.AddError(errors.New(fmt.Sprintf("error on Setup for logger %v; follows (may be out of order):", lName)))
errs.AddError(err2)
err2 = nil
}
}(l, logName)
}

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() (err error) {

var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logger Logger, lName string) {
var err2 error
defer wg.Done()
if err2 = logger.Shutdown(); err2 != nil {
errs.AddError(errors.New(fmt.Sprintf("error on Shutdown for logger %v; follows (may be out of order):", lName)))
errs.AddError(err2)
err2 = nil
}
}(l, logName)
}

wg.Wait()

if errs.Count() > 0 {
err = errs
return
}

return
}

/*
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) {

prefix = m.Prefix

return

}

/*
DoDebug sets the debug state of this MultiLogger (and all its MultiLogger.Loggers).
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.

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) (err error) {

var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)

m.EnableDebug = d

for logName, l := range m.Loggers {
wg.Add(1)
go func(logger Logger, lName string) {
var err2 error
defer wg.Done()
if err2 = l.DoDebug(d); err2 != nil {
errs.AddError(errors.New(fmt.Sprintf("error on DoDebug for logger %v; follows (may be out of order):", lName)))
errs.AddError(err2)
err2 = nil
}
}(l, logName)
}

wg.Wait()

if errs.Count() > 0 {
err = errs
return
}

return
}

// GetDebug returns the debug status of this MultiLogger.
func (m *MultiLogger) GetDebug() (d bool) {

d = m.EnableDebug

return
}

/*
SetPrefix sets the prefix for this MultiLogger (and all its MultiLogger.Loggers).

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) (err error) {

var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)

m.Prefix = prefix

for logName, l := range m.Loggers {
wg.Add(1)
go func(logger Logger, lName string) {
var err2 error
defer wg.Done()
if err2 = l.SetPrefix(prefix); err != nil {
errs.AddError(errors.New(fmt.Sprintf("error on SetPrefix for logger %v; follows (may be out of order):", lName)))
errs.AddError(err2)
err2 = nil
}
}(l, logName)
}

wg.Wait()

if errs.Count() > 0 {
err = errs
return
}

return
}

// Alert writes an ALERT-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Alert(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Alert(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Alert for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}

return
}

// Crit writes an CRITICAL-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Crit(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Crit(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Crit for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}

return
}

// Debug writes a DEBUG-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Debug(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Debug(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Debug for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}
return
}

// Emerg writes an EMERGENCY-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Emerg(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Emerg(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Emerg for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}
return
}

// Err writes an ERROR-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Err(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Err(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Err for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}

return
}

// Info writes an INFO-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Info(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Info(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Info for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}

return
}

// Notice writes a NOTICE-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Notice(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Notice(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Notice for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}

return
}

// Warning writes a WARNING/WARN-level message to this MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {

var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil)

for logName, l := range m.Loggers {
wg.Add(1)
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done()
if err = logObj.Warning(msg, rplc...); err != nil {
e.AddError(errors.New(fmt.Sprintf("error on Warning for logger %v; follows (may be out of order):", lName)))
e.AddError(err)
err = nil
}
}(l, s, logName, v...)
}

wg.Wait()

if !e.IsEmpty() {
err = e
}

return
}

View File

@ -0,0 +1,195 @@
package logging

import (
"path"

"github.com/google/uuid"
"r00t2.io/sysutils/paths"
)

/*
GetMultiLogger returns a MultiLogger.
If you call GetLogger, you will only get a single ("best") logger your system supports.
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
use GetMultiLogger.

Remember to add at least one Logger (e.g. MultiLogger.AddStdLogger), otherwise no entries will actually be logged.

If you want to modify e.g. if debug is enabled for a specific Logger, reference the Logger directly (e.g. MultiLogger.Loggers[identifier].SetDebug(false)).
*/
func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) {

m = &MultiLogger{
EnableDebug: enableDebug,
Prefix: logPrefix,
Loggers: make(map[string]Logger),
}
if prefix != "\x00" {
m.Prefix = prefix
}

return
}

/*
AddStdLogger adds a StdLogger to a MultiLogger.

identifier is a string to use to identify the added StdLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.

enableStdOut indicates that messages should be logged to STDOUT;
it is *strongly encouraged* to set at least one of enableStdOut or enableStdErr to true.

enableStdErr indicates that messages should be logged to STDERR;
it is *strongly encouraged* to set at least one of enableStdErr or enableStdOut to true.

See GetLogger's logConfigFlags argument and StdLogger.LogFlags for details on logFlags.
*/
func (m *MultiLogger) AddStdLogger(identifier string, enableStdOut, enableStdErr bool, logFlags int) (err error) {

var exists bool
var prefix string

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

m.Loggers[identifier] = &StdLogger{
Logger: nil,
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
LogFlags: logFlags,
EnableStdOut: enableStdOut,
EnableStdErr: enableStdErr,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}

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
}

/*
AddFileLogger adds a FileLogger to a MultiLogger.

identifier is a string to use to identify the added FileLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.

logfilePath is a string for the path to the desired logfile.
*/
func (m *MultiLogger) AddFileLogger(identifier string, logFlags int, logfilePath string) (err error) {

var exists bool
var success bool
var dirPath string
var prefix string

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

if exists, err = paths.RealPathExists(&logfilePath); err != nil {
return
} else if !exists {
if success, err = testOpen(logfilePath); err != nil {
return
} else if !success {
dirPath = path.Dir(logfilePath)
if err = paths.MakeDirIfNotExist(dirPath); err != nil {
return
}
if success, err = testOpen(dirPath); err != nil {
return
} else if !success {
err = ErrInvalidFile
return
}
}
}

m.Loggers[identifier] = &FileLogger{
StdLogger: StdLogger{
Logger: nil,
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
LogFlags: logFlags,
},
Path: logfilePath,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}

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
}

/*
AddNullLogger adds a NullLogger to a MultiLogger.

identifier is a string to use to identify the added NullLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddNullLogger(identifier string) (err error) {

var exists bool
var prefix string

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

m.Loggers[identifier] = &NullLogger{}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}

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
}

// RemoveLogger will let you remove a Logger from MultiLogger.Loggers.
func (m *MultiLogger) RemoveLogger(identifier string) (err error) {

var exists bool

if _, exists = m.Loggers[identifier]; !exists {
err = ErrNoEntry
return
}

delete(m.Loggers, identifier)

return
}

View File

@ -0,0 +1,134 @@
package logging

import (
`os`

sysd `github.com/coreos/go-systemd/journal`
`github.com/google/uuid`
`r00t2.io/sysutils/paths`
)

/*
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.

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, logFlags int, logPaths ...string) (err error) {

var l Logger
var exists bool

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil {
return
}

m.Loggers[identifier] = l

return
}

/*
AddSysdLogger adds a SystemDLogger to a MultiLogger.

identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {

var exists bool
var prefix string

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

if !sysd.Enabled() {
err = ErrNoSysD
return
}

m.Loggers[identifier] = &SystemDLogger{
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}

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
}

/*
AddSyslogLogger adds a SyslogLogger to a MultiLogger.

identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {

var exists bool
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
var prefix string

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
} else if !hasSyslog {
err = ErrNoSyslog
return
}

if stat.Mode().IsRegular() {
err = ErrInvalidDevLog
return
}

m.Loggers[identifier] = &SyslogLogger{
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}

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
}

View File

@ -0,0 +1,102 @@
package logging

import (
`github.com/google/uuid`
)

/*
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.

identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
If empty, one will be automatically generated.

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.

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, logFlags int, logPaths ...string) (err error) {

var l Logger
var exists bool

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

if logPaths != nil {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
} else {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags)
}
if err != nil {
return
}

m.Loggers[identifier] = l

return
}

/*
AddWinLogger adds a WinLogger to a MultiLogger. Note that this is a VERY generalized interface to the Windows Event Log.

If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
you will want to set up your own logger (golang.org/x/sys/windows/svc/eventlog).

identifier is a string to use to identify the added WinLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.

A blank source will return an error as it's used as the source name. Other functions, struct fields, etc. will refer to this as the "prefix".

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.

See GetLogger for details.
*/
func (m *MultiLogger) AddWinLogger(identifier string, eventIDs *WinEventID) (err error) {

var exists bool
var prefix string

if identifier == "" {
identifier = uuid.New().String()
}

if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}

if eventIDs == nil {
eventIDs = DefaultEventID
}

m.Loggers[identifier] = &WinLogger{
Prefix: m.Prefix,
EnableDebug: m.EnableDebug,
EIDs: eventIDs,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}

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
}

View File

@ -0,0 +1,74 @@
package logging

// Setup does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Setup() (err error) {
return
}

// DoDebug does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) DoDebug(d bool) (err error) {
return
}

// GetDebug returns the debug status of this NullLogger. It will always return true. 🙃
func (n *NullLogger) GetDebug() (d bool) {

d = true

return
}

// SetPrefix does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) SetPrefix(p string) (err error) {
return
}

// GetPrefix does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) GetPrefix() (p string, err error) {
return
}

// Shutdown does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Shutdown() (err error) {
return
}

// Alert does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Alert(s string, v ...interface{}) (err error) {
return
}

// Crit does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Crit(s string, v ...interface{}) (err error) {
return
}

// Debug does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Debug(s string, v ...interface{}) (err error) {
return
}

// Emerg does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Emerg(s string, v ...interface{}) (err error) {
return
}

// Err does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Err(s string, v ...interface{}) (err error) {
return
}

// Info does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Info(s string, v ...interface{}) (err error) {
return
}

// Notice does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Notice(s string, v ...interface{}) (err error) {
return
}

// Warning does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Warning(s string, v ...interface{}) (err error) {
return
}

View File

@ -2,35 +2,108 @@ package logging

import (
"fmt"
"io"
"log"
"os"
"strings"
)

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)
var multi io.Writer

// 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 but we have flags for that.
// https://stackoverflow.com/a/36719588/733214
switch {
case l.EnableStdErr && l.EnableStdOut:
multi = io.MultiWriter(os.Stdout, os.Stderr)
case l.EnableStdErr:
multi = os.Stderr
case l.EnableStdOut:
multi = os.Stdout
default:
multi = nil
}
if multi != nil {
l.Logger = log.New(multi, l.Prefix, l.LogFlags)
} else {
// This honestly should throw an error.
l.Logger = &log.Logger{}
l.Logger.SetPrefix(l.Prefix)
l.Logger.SetFlags(l.LogFlags)
}

return
}

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
}

func (l *StdLogger) DoDebug(d bool) {
/*
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

return
}

/*
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) (err error) {

l.EnableDebug = d

return
}

func (l *StdLogger) SetPrefix(prefix string) {
// GetDebug returns the debug status of this StdLogger.
func (l *StdLogger) GetDebug() (d bool) {

d = l.EnableDebug

return
}

/*
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
l.Logger.SetPrefix(prefix)
}

func (l *StdLogger) GetPrefix() string {
return l.Prefix
if prefix != "" {
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
}
l.Logger.SetPrefix(l.Prefix)

return
}

// Alert writes an ALERT-level message to this StdLogger.
func (l *StdLogger) Alert(s string, v ...interface{}) (err error) {

var msg string
@ -46,6 +119,7 @@ func (l *StdLogger) Alert(s string, v ...interface{}) (err error) {
return
}

// Crit writes an CRITICAL-level message to this StdLogger.
func (l *StdLogger) Crit(s string, v ...interface{}) (err error) {

var msg string
@ -61,6 +135,7 @@ func (l *StdLogger) Crit(s string, v ...interface{}) (err error) {
return
}

// Debug writes a DEBUG-level message to this StdLogger.
func (l *StdLogger) Debug(s string, v ...interface{}) (err error) {

if !l.EnableDebug {
@ -80,6 +155,7 @@ func (l *StdLogger) Debug(s string, v ...interface{}) (err error) {
return
}

// Emerg writes an EMERGENCY-level message to this StdLogger.
func (l *StdLogger) Emerg(s string, v ...interface{}) (err error) {

var msg string
@ -95,6 +171,7 @@ func (l *StdLogger) Emerg(s string, v ...interface{}) (err error) {
return
}

// Err writes an ERROR-level message to this StdLogger.
func (l *StdLogger) Err(s string, v ...interface{}) (err error) {

var msg string
@ -110,6 +187,7 @@ func (l *StdLogger) Err(s string, v ...interface{}) (err error) {
return
}

// Info writes an INFO-level message to this StdLogger.
func (l *StdLogger) Info(s string, v ...interface{}) (err error) {

var msg string
@ -125,6 +203,7 @@ func (l *StdLogger) Info(s string, v ...interface{}) (err error) {
return
}

// Notice writes a NOTICE-level message to this StdLogger.
func (l *StdLogger) Notice(s string, v ...interface{}) (err error) {

var msg string
@ -140,6 +219,7 @@ func (l *StdLogger) Notice(s string, v ...interface{}) (err error) {
return
}

// Warning writes a WARNING/WARN-level message to this StdLogger.
func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {

var msg string
@ -155,6 +235,7 @@ func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {
return
}

// renderWrite prepares/formats a log message to be written to this StdLogger.
func (l *StdLogger) renderWrite(msg, prio string) {

s := fmt.Sprintf("[%v] %v", prio, msg)

View File

@ -7,32 +7,71 @@ import (
"github.com/coreos/go-systemd/journal"
)

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
}

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
}

func (l *SystemDLogger) DoDebug(d bool) {
/*
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

return
}

/*
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) (err error) {

l.EnableDebug = d

return
}

func (l *SystemDLogger) SetPrefix(prefix string) {
// GetDebug returns the debug status of this SystemDLogger.
func (l *SystemDLogger) GetDebug() (d bool) {

d = l.EnableDebug

return
}

/*
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
}

func (l *SystemDLogger) GetPrefix() string {
return l.Prefix
}

// Alert writes an ALERT-level message to this SystemDLogger.
func (l *SystemDLogger) Alert(s string, v ...interface{}) (err error) {

var msg string
@ -48,6 +87,7 @@ func (l *SystemDLogger) Alert(s string, v ...interface{}) (err error) {
return
}

// Crit writes an CRITICAL-level message to this SystemDLogger.
func (l *SystemDLogger) Crit(s string, v ...interface{}) (err error) {

var msg string
@ -63,6 +103,7 @@ func (l *SystemDLogger) Crit(s string, v ...interface{}) (err error) {
return
}

// Debug writes a DEBUG-level message to this SystemDLogger.
func (l *SystemDLogger) Debug(s string, v ...interface{}) (err error) {

if !l.EnableDebug {
@ -82,6 +123,7 @@ func (l *SystemDLogger) Debug(s string, v ...interface{}) (err error) {
return
}

// Emerg writes an EMERGENCY-level message to this SystemDLogger.
func (l *SystemDLogger) Emerg(s string, v ...interface{}) (err error) {

var msg string
@ -97,6 +139,7 @@ func (l *SystemDLogger) Emerg(s string, v ...interface{}) (err error) {
return
}

// Err writes an ERROR-level message to this SystemDLogger.
func (l *SystemDLogger) Err(s string, v ...interface{}) (err error) {

var msg string
@ -112,6 +155,7 @@ func (l *SystemDLogger) Err(s string, v ...interface{}) (err error) {
return
}

// Info writes an INFO-level message to this SystemDLogger.
func (l *SystemDLogger) Info(s string, v ...interface{}) (err error) {

var msg string
@ -127,6 +171,7 @@ func (l *SystemDLogger) Info(s string, v ...interface{}) (err error) {
return
}

// Notice writes a NOTICE-level message to this SystemDLogger.
func (l *SystemDLogger) Notice(s string, v ...interface{}) (err error) {

var msg string
@ -142,6 +187,7 @@ func (l *SystemDLogger) Notice(s string, v ...interface{}) (err error) {
return
}

// Warning writes a WARNING/WARN-level message to this SystemDLogger.
func (l *SystemDLogger) Warning(s string, v ...interface{}) (err error) {

var msg string
@ -157,7 +203,9 @@ func (l *SystemDLogger) Warning(s string, v ...interface{}) (err error) {
return
}

// renderWrite prepares/formats a log message to be written to this SystemDLogger.
func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) {

// TODO: implement code line, etc.
// https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
// CODE_FILE=, CODE_LINE=, CODE_FUNC=

View File

@ -4,64 +4,123 @@ import (
"fmt"
"log"
"log/syslog"

"r00t2.io/goutils/multierr"
)

func (l *SyslogLogger) Setup() {
// Setup sets up/configures a SyslogLogger and prepares it for use.
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
}

func (l *SyslogLogger) Shutdown() {
// Shutdown cleanly shuts down a SyslogLogger.
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
}

func (l *SyslogLogger) DoDebug(d bool) {
/*
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

return
}

/*
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) (err error) {

l.EnableDebug = d

return
}

func (l *SyslogLogger) SetPrefix(prefix string) {
// GetDebug returns the debug status of this SyslogLogger.
func (l *SyslogLogger) GetDebug() (d bool) {

d = l.EnableDebug

return
}

// SetPrefix sets the prefix for this SyslogLogger.
func (l *SyslogLogger) SetPrefix(prefix string) (err error) {

l.Prefix = prefix
l.Setup()
}

func (l *SyslogLogger) GetPrefix() string {
return l.Prefix

// 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.
func (l *SyslogLogger) Alert(s string, v ...interface{}) (err error) {

var msg string
@ -79,6 +138,7 @@ func (l *SyslogLogger) Alert(s string, v ...interface{}) (err error) {
return
}

// Crit writes an CRITICAL-level message to this SyslogLogger.
func (l *SyslogLogger) Crit(s string, v ...interface{}) (err error) {

var msg string
@ -96,6 +156,7 @@ func (l *SyslogLogger) Crit(s string, v ...interface{}) (err error) {
return
}

// Debug writes a DEBUG-level message to this SyslogLogger.
func (l *SyslogLogger) Debug(s string, v ...interface{}) (err error) {

if !l.EnableDebug {
@ -117,6 +178,7 @@ func (l *SyslogLogger) Debug(s string, v ...interface{}) (err error) {
return
}

// Emerg writes an EMERGENCY-level message to this SyslogLogger.
func (l *SyslogLogger) Emerg(s string, v ...interface{}) (err error) {

var msg string
@ -134,6 +196,7 @@ func (l *SyslogLogger) Emerg(s string, v ...interface{}) (err error) {
return
}

// Err writes an ERROR-level message to this SyslogLogger.
func (l *SyslogLogger) Err(s string, v ...interface{}) (err error) {

var msg string
@ -151,6 +214,7 @@ func (l *SyslogLogger) Err(s string, v ...interface{}) (err error) {
return
}

// Info writes an INFO-level message to this SyslogLogger.
func (l *SyslogLogger) Info(s string, v ...interface{}) (err error) {

var msg string
@ -168,6 +232,7 @@ func (l *SyslogLogger) Info(s string, v ...interface{}) (err error) {
return
}

// Notice writes a NOTICE-level message to this SyslogLogger.
func (l *SyslogLogger) Notice(s string, v ...interface{}) (err error) {

var msg string
@ -185,6 +250,7 @@ func (l *SyslogLogger) Notice(s string, v ...interface{}) (err error) {
return
}

// Warning writes a WARNING/WARN-level message to this SyslogLogger.
func (l *SyslogLogger) Warning(s string, v ...interface{}) (err error) {
var msg string

196
logging/funcs_test.go Normal file
View File

@ -0,0 +1,196 @@
package logging

import (
`os`
`testing`
)

/*
TestStdLogger tests functionality for StdLogger.
*/
func TestStdLogger(t *testing.T) {

var l *StdLogger
var ltype string = "StdLogger"
var prefix string
var err error

l = &StdLogger{
EnableDebug: true,
Prefix: TestLogPrefix,
LogFlags: logFlags,
EnableStdOut: false,
EnableStdErr: true,
}

if err = l.Setup(); err != nil {
t.Fatalf("error when running Setup: %v", err.Error())
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if prefix != (TestLogPrefix + " ") { // StdLogger adds a space at the end.
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else if l.EnableDebug {
t.Fatalf("did not properly set Debug filter state")
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

/*
TestFileLogger tests functionality for FileLogger.
If the appropriate env var is set (see the EnvVarKeepLog constant), the temporary log file that is created will not be cleaned up.
*/
func TestFileLogger(t *testing.T) {

var l *FileLogger
var ltype string = "FileLogger"
var prefix string
var tempfile *os.File
var tempfilePath string
var keepLog bool
var err error

if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
}
tempfilePath = tempfile.Name()
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
if err = tempfile.Close(); err != nil {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
}

l = &FileLogger{
StdLogger: StdLogger{
EnableDebug: true,
Prefix: TestLogPrefix,
LogFlags: logFlags,
},
Path: tempfilePath,
}

if err = l.Setup(); err != nil {
t.Fatalf("error when running Setup: %v", err.Error())
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if prefix != (TestLogPrefix + " ") { // StdLogger (and thus FileLogger) adds a space at the end.
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else if l.EnableDebug {
t.Fatalf("did not properly set Debug filter state")
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

_, keepLog = os.LookupEnv(EnvVarKeepLog)

if !keepLog {
if err = os.Remove(tempfilePath); err != nil {
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
}
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

131
logging/funcs_windows.go Normal file
View File

@ -0,0 +1,131 @@
package logging

import (
`errors`
`path`
`strings`

`r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths`
)

/*
GetLogger returns an instance of Logger that best suits your system's capabilities. Note that this is a VERY generalized interface to the Windows Event Log.

If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
you will want to set up your own logger (golang.org/x/sys/windows/svc/eventlog).

If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).

A blank source will return an error as it's used as the source name. Other functions, struct fields, etc. will refer to this as the "prefix".

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).

Only the first logPaths entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a WinLogger.

If you call GetLogger, you will only get a single ("best") logger your system supports.
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, 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")
return
}

// Configure system-supported logger(s). The Windows Event Logger (should) ALWAYS be available.
logFlags.AddFlag(LogWinLogger)
if eventIDs == nil {
eventIDs = DefaultEventID
}

if logPaths != nil {
ckLogPaths = logPaths
ckLogPaths = append(ckLogPaths, defLogPaths...)

for _, p := range ckLogPaths {
if exists, _ = paths.RealPathExists(&p); exists {
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
} else {
dirPath := path.Dir(p)
if err = paths.MakeDirIfNotExist(dirPath); err != nil {
continue
}
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
}
}
}

if logFlags.HasFlag(LogWinLogger) {
logger = &WinLogger{
Prefix: source,
EnableDebug: enableDebug,
EIDs: eventIDs,
}
} else {
if logFlags.HasFlag(LogFile) {
logger = &FileLogger{
StdLogger: StdLogger{
Prefix: source,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
},
Path: logPath,
}
} else {
logger = &StdLogger{
Prefix: source,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
}
}
}

if err = logger.Setup(); err != nil {
return
}
if source != "\x00" {
if err = logger.SetPrefix(source); err != nil {
return
}
}

if prefix, err = logger.GetPrefix(); err != nil {
return
}

logger.Debug("logger initialized of type %T with source %v", logger, prefix)

return

}

View File

@ -0,0 +1,205 @@
package logging

import (
`fmt`
`os`
`testing`
)

/*
TestWinLogger tests functionality for WinLogger.
You will probably need to run it with an Administrator shell.
*/
func TestWinLogger(t *testing.T) {

var l *WinLogger
var ltype string = "WinLogger"
var prefix string
var exists bool
var err error

l = &WinLogger{
EnableDebug: true,
Prefix: TestLogPrefix,
RemoveOnClose: true,
EIDs: DefaultEventID,
}

if exists, err = l.Exists(); err != nil {
t.Fatalf("error when checking for existence of registered Event Log source '%v': %v", TestLogPrefix, err.Error())
} else {
t.Logf("Prefix (source) '%v' exists before setup: %v", TestLogPrefix, exists)
}

if err = l.Setup(); err != nil {
t.Fatalf("error when running Setup: %v", err.Error())
}

if exists, err = l.Exists(); err != nil {
t.Fatalf("error when checking for existence of registered Event Log source '%v': %v", TestLogPrefix, err.Error())
} else {
t.Logf("Prefix (source) '%v' exists after setup: %v", TestLogPrefix, exists)
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if prefix != TestLogPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else if l.EnableDebug {
t.Fatalf("did not properly set Debug filter state")
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

// TestDefaultLogger tests GetLogger.
func TestDefaultLogger(t *testing.T) {

var l Logger
var tempfile *os.File
var tempfilePath string
var keepLog bool
var ltype string
var prefix string
var testPrefix string
var err error

if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
}
tempfilePath = tempfile.Name()
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
if err = tempfile.Close(); err != nil {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
}

if l, err = GetLogger(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
}

ltype = fmt.Sprintf("%T", l)

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
testPrefix = TestLogPrefix + " "
} else {
testPrefix = TestLogPrefix
}

if prefix != testPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

_, keepLog = os.LookupEnv(EnvVarKeepLog)

if !keepLog {
if err = os.Remove(tempfilePath); err != nil {
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
}
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

View File

@ -0,0 +1,344 @@
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.
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.

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) {

/*
A sanity check on the EventIDs.
Since we use eventcreate, all Event IDs must be 1 <= eid <= 1000.
*/
for _, eid := range []uint32{
l.EIDs.Alert,
l.EIDs.Crit,
l.EIDs.Debug,
l.EIDs.Emerg,
l.EIDs.Err,
l.EIDs.Info,
l.EIDs.Notice,
l.EIDs.Warning,
} {
if !((eid <= EIDMax) && (EIDMin <= eid)) {
err = ErrBadEid
return
}
}

if err = l.Install(); err != nil {
return
}

if l.elog, err = eventlog.Open(l.Prefix); err != nil {
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

}

// Remove uninstalls a registered WinLogger source.
func (l *WinLogger) Remove() (err error) {

if err = eventlog.Remove(l.Prefix); err != nil {
return
}

return
}

/*
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

return
}

/*
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) (err error) {

l.EnableDebug = d

return
}

// GetDebug returns the debug status of this WinLogger.
func (l *WinLogger) GetDebug() (d bool) {

d = l.EnableDebug

return
}

// SetPrefix sets the prefix for this WinLogger.
func (l *WinLogger) SetPrefix(prefix string) (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

if err = l.Setup(); err != nil {
return
}

return
}

// 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()

e = true

return
}

// Alert writes an ALERT-level message to this WinLogger.
func (l *WinLogger) Alert(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

// Treat ALERT as Warning
err = l.elog.Warning(l.EIDs.Alert, msg)

return
}

// Crit writes an CRITICAL-level message to this WinLogger.
func (l *WinLogger) Crit(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

// Treat CRIT as Error
err = l.elog.Error(l.EIDs.Crit, msg)

return
}

// Debug writes a DEBUG-level message to this WinLogger.
func (l *WinLogger) Debug(s string, v ...interface{}) (err error) {

if !l.EnableDebug {
return
}

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

// Treat DEBUG as Info
err = l.elog.Info(l.EIDs.Debug, msg)

return

}

// Emerg writes an EMERGENCY-level message to this WinLogger.
func (l *WinLogger) Emerg(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

// Treat EMERG as Error
err = l.elog.Error(l.EIDs.Emerg, msg)

return

}

// Err writes an ERROR-level message to this WinLogger.
func (l *WinLogger) Err(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

err = l.elog.Error(l.EIDs.Err, msg)

return

}

// Info writes an INFO-level message to this WinLogger.
func (l *WinLogger) Info(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

err = l.elog.Info(l.EIDs.Info, msg)

return

}

// Notice writes a NOTICE-level message to this WinLogger.
func (l *WinLogger) Notice(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

// Treat NOTICE as Info
err = l.elog.Info(l.EIDs.Notice, msg)

return

}

// Warning writes a WARNING/WARN-level message to this WinLogger.
func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {

var msg string

if v != nil {
msg = fmt.Sprintf(s, v...)
} else {
msg = s
}

err = l.elog.Warning(l.EIDs.Warning, msg)

return

}

View File

@ -0,0 +1,121 @@
package logging

import (
`os`
`testing`
)

// TestMultiLogger tests GetMultiLogger and MultiLogger methods.
func TestMultiLogger(t *testing.T) {

var l *MultiLogger
var tempfile *os.File
var tempfilePath string
var keepLog bool
var ltype string = "MultiLogger"
var prefix string
var testPrefix string
var err error

if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
}
tempfilePath = tempfile.Name()
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
if err = tempfile.Close(); err != nil {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
}

l = GetMultiLogger(true, TestLogPrefix)

if err = l.AddStdLogger("StdLogger", false, true, logFlags); err != nil {
t.Fatalf("error when adding StdLogger to MultiLogger: %v", err.Error())
}
if err = l.AddFileLogger("FileLogger", logFlags, tempfilePath); err != nil {
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
}

if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
}

if err = l.AddSysdLogger("SystemDLogger"); err != nil {
t.Fatalf("error when adding SystemDLogger to MultiLogger: %v", err.Error())
}
if err = l.AddSyslogLogger("SyslogLogger"); err != nil {
t.Fatalf("error when adding SyslogLogger to MultiLogger: %v", err.Error())
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
testPrefix = TestLogPrefix + " "
} else {
testPrefix = TestLogPrefix
}

if prefix != testPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

_, keepLog = os.LookupEnv(EnvVarKeepLog)

if !keepLog {
if err = os.Remove(tempfilePath); err != nil {
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
}
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

View File

@ -0,0 +1,118 @@
package logging

import (
`os`
`testing`
)

// TestMultiLogger tests GetMultiLogger and MultiLogger methods.
func TestMultiLogger(t *testing.T) {

var l *MultiLogger
var tempfile *os.File
var tempfilePath string
var keepLog bool
var ltype string = "MultiLogger"
var prefix string
var testPrefix string
var err error

if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
}
tempfilePath = tempfile.Name()
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
if err = tempfile.Close(); err != nil {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
}

l = GetMultiLogger(true, TestLogPrefix)

if err = l.AddStdLogger("StdLogger", false, true, logFlags); err != nil {
t.Fatalf("error when adding StdLogger to MultiLogger: %v", err.Error())
}
if err = l.AddFileLogger("FileLogger", logFlags, tempfilePath); err != nil {
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
}

if err = l.AddDefaultLogger("DefaultLogger", DefaultEventID, logFlags, tempfilePath); err != nil {
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
}

if err = l.AddWinLogger("WinLogger", DefaultEventID); err != nil {
t.Fatalf("error when adding WinLogger to MultiLogger: %v", err.Error())
}

t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)

if err = l.Alert(testAlert, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Crit(testCrit, ltype); err != nil {
t.Fatalf("error for Crit: %v", err.Error())
}

if err = l.Debug(testDebug, ltype); err != nil {
t.Fatalf("error for Debug: %v", err.Error())
}

if err = l.Emerg(testEmerg, ltype); err != nil {
t.Fatalf("error for Emerg: %v", err.Error())
}

if err = l.Err(testErr, ltype); err != nil {
t.Fatalf("error for Err: %v", err.Error())
}

if err = l.Info(testInfo, ltype); err != nil {
t.Fatalf("error for Alert: %v", err.Error())
}

if err = l.Notice(testNotice, ltype); err != nil {
t.Fatalf("error for Notice: %v", err.Error())
}

if err = l.Warning(testWarning, ltype); err != nil {
t.Fatalf("error for Warning: %v", err.Error())
}

if prefix, err = l.GetPrefix(); err != nil {
t.Fatalf("error when fetching prefix: %v", err.Error())
}

if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
testPrefix = TestLogPrefix + " "
} else {
testPrefix = TestLogPrefix
}

if prefix != testPrefix {
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
}
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
} else {
_ = l.SetPrefix(TestLogPrefix)
}

if err = l.DoDebug(false); err != nil {
t.Fatalf("error when changing debug to false: %v", err.Error())
} else {
_ = l.DoDebug(true)
}

if err = l.Shutdown(); err != nil {
t.Fatalf("Error when running Shutdown: %v", err.Error())
}

_, keepLog = os.LookupEnv(EnvVarKeepLog)

if !keepLog {
if err = os.Remove(tempfilePath); err != nil {
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
}
}

t.Logf("Logger %v passed all logging targets.", ltype)
}

View File

@ -2,52 +2,106 @@ package logging

import (
"log"
"log/syslog"
"os"
)

/*
Logger is one of the various loggers offered by this module.
*/
type Logger interface {
Alert(string, ...interface{}) error
Crit(string, ...interface{}) error
Debug(string, ...interface{}) error
Emerg(string, ...interface{}) error
Err(string, ...interface{}) error
Info(string, ...interface{}) error
Notice(string, ...interface{}) error
Warning(string, ...interface{}) error
DoDebug(bool)
SetPrefix(string)
GetPrefix() string
Setup()
Shutdown()
}

type SystemDLogger struct {
EnableDebug bool
Prefix string
}

type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
Alert(s string, v ...interface{}) (err error)
Crit(s string, v ...interface{}) (err error)
Debug(s string, v ...interface{}) (err error)
Emerg(s string, v ...interface{}) (err error)
Err(s string, v ...interface{}) (err error)
Info(s string, v ...interface{}) (err error)
Notice(s string, v ...interface{}) (err error)
Warning(s string, v ...interface{}) (err error)
DoDebug(d bool) (err error)
GetDebug() (d bool)
SetPrefix(p string) (err error)
GetPrefix() (p string, err error)
Setup() (err error)
Shutdown() (err error)
}

/*
StdLogger uses the log package in stdlib to perform all logging. The default is to write to STDOUT.
If you wish to modify the underling log.Logger object, you can access it directly via StdLogger.Logger.
*/
type StdLogger struct {
// All log.Logger fields/methods are exposed.
*log.Logger
/*
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
// 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
/*
EnableStdOut is true if the log will send to STDOUT.
If false (default), no output will be written to STDOUT.
You will need to run StdLogger.Shutdown and then StdLogger.Setup again if you wish to change this.

If EnableStdOut is false and EnableStdErr is false, no logging output will occur by default
and StdLogger.Logger will be largely useless.
It will be up to you to modify the underlying log.Logger to behave as you want.
*/
EnableStdOut bool
/*
EnableStdErr is true if the log will send to STDERR.
If false (default), no output will be written to STDERR.
You will need to run StdLogger.Shutdown and then StdLogger.Setup again if you wish to change this.

If EnableStdErr is false and EnableStdOut is false, no logging output will occur by default
and StdLogger.Logger will be largely useless.
It will be up to you to modify the underlying log.Logger to behave as you want.
*/
EnableStdErr bool
}

/*
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 string
// Path is the path to the logfile.
Path string
// writer is used for the writing out of the log file.
writer *os.File
}

// NullLogger is used mainly for test implementations, mockup code, etc. It does absolutely nothing with all messages sent to it.
type NullLogger struct{}

// MultiLogger is used to contain one or more Loggers and present them all as a single Logger.
type MultiLogger 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 indicates the prefix for log entries; in shared logs, this helps differentiate the source.
Prefix string
/*
Loggers contains a map of map[logname]Logger. It can be used to set log-specific options, or replace a Logger
with one of a different type or options.
*/
Loggers map[string]Logger
}

27
logging/types_linux.go Normal file
View File

@ -0,0 +1,27 @@
package logging

import (
`log/syslog`
)

/*
SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems.
*/
type SystemDLogger struct {
EnableDebug bool
Prefix string
}

// SyslogLogger writes to syslog on syslog-enabled systems.
type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

70
logging/types_windows.go Normal file
View File

@ -0,0 +1,70 @@
package logging

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 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,
Debug,
Emerg,
Err,
Info,
Notice,
Warning uint32
}

63
multierr/doc.go Normal file
View File

@ -0,0 +1,63 @@
/*
Package multierr provides a simple way of handling multiple errors and consolidating them into a single error.

Example:

package main

import (
`r00t2.io/goutils/multierr`
)

func main() {

var err error
var errs []error

errs = make([]error, 0)

for _, i := range someSlice {
go func() {
if err = i.DoSomething(); err != nil {
errs = append(errs, err)
}
}()
}

if errs != nil && len(errs) != 0 {
// err now contains multiple errors presented as a single error interface.
err = multierr.NewErrors(errs...)
}
}

MultiError also has a shorthand, making the above much less verbose:

package main

import (
`r00t2.io/goutils/multierr`
)

func main() {

var err error
var multierror *multierr.MultiError = multierr.NewMultiError(nil)

for _, i := range someSlice {
go func() {
if err = i.DoSomething(); err != nil {
multierror.AddError(err)
}
}()
}

// multierror now contains any/all errors above. If calling in a function, you'll probably want to do:
// if !multierror.IsEmpty() {
// err = multierror
// }

}

In the above, the multierror assignment can still be used as an error.
*/
package multierr

110
multierr/funcs.go Normal file
View File

@ -0,0 +1,110 @@
package multierr

import (
`fmt`
)

/*
NewErrors returns a new MultiError (as an error) based on/initialized with a slice of error.Error (errs).
Any nil errors are trimmed.
If there are no actual errors after trimming, err will be nil.
*/
func NewErrors(errs ...error) (err error) {

if errs == nil || len(errs) == 0 {
return
}

var realErrs []error = make([]error, 0)

for _, e := range errs {
if e == nil {
continue
}
realErrs = append(realErrs, e)
}

if len(realErrs) == 0 {
return
}

err = &MultiError{
Errors: realErrs,
ErrorSep: "\n",
}

return
}

// NewMultiError will provide a MultiError (true type), optionally initialized with errors.
func NewMultiError(errs ...error) (m *MultiError) {

var realErrs []error = make([]error, 0)

if errs != nil {
for _, e := range errs {
if e == nil {
continue
}
realErrs = append(realErrs, e)
}
}

m = &MultiError{
Errors: realErrs,
ErrorSep: "\n",
}

return
}

// Error returns a string representation of a MultiError (to conform with the error interface).
func (e *MultiError) Error() (errStr string) {

var numErrs int

if e == nil || len(e.Errors) == 0 {
return
} else {
numErrs = len(e.Errors)
}

for idx, err := range e.Errors {
if (idx + 1) < numErrs {
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
} else {
errStr += err.Error()
}
}

return
}

// AddError is a shorthand way of adding an error to a MultiError.
func (e *MultiError) AddError(err error) {

if err == nil {
return
}

e.Errors = append(e.Errors, err)

}

// Count returns the number of errors in a MultiError.
func (e *MultiError) Count() (n int) {

n = len(e.Errors)

return
}

// IsEmpty is a shorthand for testing if e.Errors is empty.
func (e *MultiError) IsEmpty() (empty bool) {

if e.Count() == 0 {
empty = true
}

return
}

9
multierr/types.go Normal file
View File

@ -0,0 +1,9 @@
package multierr

// MultiError is a type of error.Error that can contain multiple errors.
type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error `json:"errors"`
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string `json:"separator"`
}

5
structutils/consts.go Normal file
View File

@ -0,0 +1,5 @@
package structutils

const (
TagMapTrim tagMapOpt = iota
)

362
structutils/funcs.go Normal file
View File

@ -0,0 +1,362 @@
/*
GoUtils - a library to assist with various Golang-related functions
Copyright (C) 2020 Brent Saner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package structutils

import (
`reflect`
`strings`
)

/*
TagToBoolMap takes struct field `field` and tag name `tagName`,
optionally with options `opts`, and returns a map of the tag values.
The tag value string is assumed to be in the form of:
option[,option,option...]
and returns a map[string]bool (map[option]true).

If field does not have tag tagName, m will be nil.

See the TagMap* constants for opts.
*/
func TagToBoolMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (m map[string]bool) {

var s string
var optSplit []string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)

s = field.Tag.Get(tagName)

if strings.TrimSpace(s) == "" {
return
}

optSplit = strings.Split(s, ",")
if optSplit == nil || len(optSplit) == 0 {
return
}
m = make(map[string]bool)
for _, o := range optSplit {
if tagOpts[TagMapTrim] {
o = strings.TrimSpace(o)
}
m[o] = true
}

return
}

/*
TagToBoolMapWithValue is like TagToBoolMap but additionally assumes the first value is an "identifier".
The tag value string is assumed to be in the form of:
value,option[,option,option...]
and returns a map[string]bool (map[option]true) with the value.
*/
func TagToBoolMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, m map[string]bool) {

var s string
var optSplit []string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)

s = field.Tag.Get(tagName)

if strings.TrimSpace(s) == "" {
return
}

optSplit = strings.Split(s, ",")
if optSplit == nil || len(optSplit) == 0 {
return
}
m = make(map[string]bool)
for idx, o := range optSplit {
if idx == 0 {
if tagOpts[TagMapTrim] {
o = strings.TrimSpace(o)
}
value = o
continue
}
if tagOpts[TagMapTrim] {
o = strings.TrimSpace(o)
}
m[o] = true
}

return
}

/*
TagToMixedMap combines TagToBoolMap and TagToStringMap.
It takes struct field `field` and tag name `tagName`,
and returns all single-value options in mapBool, and all key/value options in mapString.

If field does not have tag tagName, m will be nil.

See the TagMap* constants for opts.
*/
func TagToMixedMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (mapBool map[string]bool, mapString map[string]string) {

var s string
var valStr string
var split []string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)

s = field.Tag.Get(tagName)

if strings.TrimSpace(s) == "" {
return
}

split = strings.Split(s, ",")
if split == nil || len(split) == 0 {
return
}
mapBool = make(map[string]bool)
mapString = make(map[string]string)
for _, valStr = range split {
if strings.Contains(valStr, "=") {
kvSplit = strings.SplitN(valStr, "=", 2)
if kvSplit == nil || len(kvSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = kvSplit[1]
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
mapString[k] = v
} else {
if tagOpts[TagMapTrim] {
valStr = strings.TrimSpace(valStr)
}
mapBool[valStr] = true
}
}

return
}

/*
TagToMixedMapWithValue combines TagToBoolMapWithValue and TagToStringMapWithValue.
It takes struct field `field` and tag name `tagName`,
and returns all single-value options in mapBool, and all key/value options in mapString
along with the first single-value option as value..

If field does not have tag tagName, m will be nil.

See the TagMap* constants for opts.
*/
func TagToMixedMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, mapBool map[string]bool, mapString map[string]string) {

var s string
var idx int
var valStr string
var split []string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)

s = field.Tag.Get(tagName)

if strings.TrimSpace(s) == "" {
return
}

split = strings.Split(s, ",")
if split == nil || len(split) == 0 {
return
}
mapBool = make(map[string]bool)
mapString = make(map[string]string)
for idx, valStr = range split {
if idx == 0 {
if tagOpts[TagMapTrim] {
valStr = strings.TrimSpace(valStr)
}
value = valStr
continue
}
if strings.Contains(valStr, "=") {
kvSplit = strings.SplitN(valStr, "=", 2)
if kvSplit == nil || len(kvSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = kvSplit[1]
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
mapString[k] = v
} else {
if tagOpts[TagMapTrim] {
valStr = strings.TrimSpace(valStr)
}
mapBool[valStr] = true
}
}

return
}

/*
TagToStringMap takes struct field `field` and tag name `tagName`,
optionally with options `opts`, and returns a map of the tag values.
The tag value string is assumed to be in the form of:
key=value[,key=value,key=value...]
and returns a map[string]string (map[key]value).
It is proccessed in order; later duplicate keys overwrite previous ones.

If field does not have tag tagName, m will be nil.

If only a key is provided with no value, the value in the map will be an empty string.
(e.g. "foo,bar=baz" => map[string]string{"foo": "", "bar: "baz"}

See the TagMap* constants for opts.
*/
func TagToStringMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (m map[string]string) {

var s string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)

s = field.Tag.Get(tagName)

if strings.TrimSpace(s) == "" {
return
}

kvSplit = strings.Split(s, ",")
if kvSplit == nil || len(kvSplit) == 0 {
return
}
for _, kv := range kvSplit {
valSplit = strings.SplitN(kv, "=", 2)
if valSplit == nil || len(valSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = valSplit[1]
// It's not possible to have more than 2.
}
if m == nil {
m = make(map[string]string)
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
m[k] = v
}

return
}

/*
TagToStringMapWithValue is like TagToStringMap but additionally assumes the first value is an "identifier".
The tag value string is assumed to be in the form of:
value,key=value[,key=value,key=value...]
and returns a map[string]string (map[key]value) with the value.
*/
func TagToStringMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, m map[string]string) {

var s string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)

s = field.Tag.Get(tagName)

if strings.TrimSpace(s) == "" {
return
}

kvSplit = strings.Split(s, ",")
if kvSplit == nil || len(kvSplit) == 0 {
return
}
for idx, kv := range kvSplit {
if idx == 0 {
if tagOpts[TagMapTrim] {
kv = strings.TrimSpace(kv)
}
value = kv
continue
}
valSplit = strings.SplitN(kv, "=", 2)
if valSplit == nil || len(valSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = valSplit[1]
// It's not possible to have more than 2.
}
if m == nil {
m = make(map[string]string)
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
m[k] = v
}

return
}

func getTagMapOpts(opts []tagMapOpt) (optMap map[tagMapOpt]bool) {

optMap = make(map[tagMapOpt]bool)

if opts == nil {
return
}

return
}

5
structutils/types.go Normal file
View File

@ -0,0 +1,5 @@
package structutils

type (
tagMapOpt uint8
)

View File

@ -1,33 +0,0 @@
package types

type BitMask interface {
HasFlag(bit MaskBit) bool
AddFlag(bit MaskBit)
ClearFlag(bit MaskBit)
ToggleFlag(bit MaskBit)
}

// BitMasks
type MaskBit uint8

func (f MaskBit) HasFlag(flag MaskBit) (r bool) {
if f&flag != 0 {
r = true
}
return
}

func (f *MaskBit) AddFlag(flag MaskBit) {
*f |= flag
return
}

func (f *MaskBit) ClearFlag(flag MaskBit) {
*f &= flag
return
}

func (f *MaskBit) ToggleFlag(flag MaskBit) {
*f ^= flag
return
}