do bitmask better, add (COMPLETELY 100% EXPERIMENTAL NOT DONE YET) eventlog support to logger

This commit is contained in:
brent s. 2021-12-15 04:49:03 -05:00
parent d5b1d449e5
commit 3975f8b11f
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
17 changed files with 698 additions and 224 deletions

46
bitmask/bitmasks.go Normal file
View File

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

// MaskBit is a flag container.
type MaskBit uint8

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

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

45
bitmask/doc.go Normal file
View File

@ -0,0 +1,45 @@
/*
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 types.MaskBit = 0
const (
OPT1 types.MaskBit = 1 << iota
OPT2
OPT3
// ...
)

var MyMask *MaskBit

func main() {
MyMask = types.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)
*/
package bitmask

View File

@ -1,3 +1,11 @@
- Implement code line/func/etc. (only for debug?): - Implement code line/func/etc. (only for debug?):
https://stackoverflow.com/a/24809646 https://stackoverflow.com/a/24809646
https://golang.org/pkg/runtime/#Caller https://golang.org/pkg/runtime/#Caller

- Support simultaneous writing to multiple Loggers.

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

- DOCS.

- Unit/Integration tests.

View File

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


import ( import (
`log/syslog`
`os` `os`

`r00t2.io/goutils/types`
) )


const ( const (
devlog string = "/dev/log"
logPerm os.FileMode = 0600 logPerm os.FileMode = 0600
logPrefix string = "GOLANG PROGRAM" logPrefix string = "GOLANG PROGRAM"
appendFlags int = os.O_APPEND|os.O_CREATE|os.O_WRONLY 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"}
) )

28
logging/consts_linux.go Normal file
View File

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

import (
`log/syslog`

`r00t2.io/goutils/bitmask`
)

const (
devlog string = "/dev/log"
syslogFacility syslog.Priority = syslog.LOG_USER
)

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

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

48
logging/consts_windows.go Normal file
View File

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

import (
`os`
`path/filepath`
`regexp`
)

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

var (
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
}
)

var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`)

// Default WinEventID
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.
const (
EventAlert uint32 = 1 << iota
EventCrit
EventDebug
EventEmerg
EventErr
EventInfo
EventNotice
EventWarning
)

View File

@ -1,126 +1,9 @@
package logging package logging


import ( import (
native "log"
"os" "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
}

func testOpen(path string) (success bool, err error) { func testOpen(path string) (success bool, err error) {
var f *os.File var f *os.File



123
logging/funcs_linux.go Normal file
View File

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

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

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
}

View File

@ -27,8 +27,11 @@ func (l *StdLogger) SetPrefix(prefix string) {
l.Logger.SetPrefix(prefix) l.Logger.SetPrefix(prefix)
} }


func (l *StdLogger) GetPrefix() string { func (l *StdLogger) GetPrefix() (prefix string) {
return l.Prefix
prefix = l.Prefix

return
} }


func (l *StdLogger) Alert(s string, v ...interface{}) (err error) { func (l *StdLogger) Alert(s string, v ...interface{}) (err error) {

View File

@ -29,8 +29,11 @@ func (l *SystemDLogger) SetPrefix(prefix string) {
l.Prefix = prefix l.Prefix = prefix
} }


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

return
} }


func (l *SystemDLogger) Alert(s string, v ...interface{}) (err error) { func (l *SystemDLogger) Alert(s string, v ...interface{}) (err error) {
@ -158,6 +161,7 @@ func (l *SystemDLogger) Warning(s string, v ...interface{}) (err error) {
} }


func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) { func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) {

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

View File

@ -58,8 +58,11 @@ func (l *SyslogLogger) SetPrefix(prefix string) {
l.Setup() l.Setup()
} }


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

return
} }


func (l *SyslogLogger) Alert(s string, v ...interface{}) (err error) { func (l *SyslogLogger) Alert(s string, v ...interface{}) (err error) {

100
logging/funcs_windows.go Normal file
View File

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

import (
`errors`
`strings`
)

/*
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.
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 until multiple logging destination support is added.
*/
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths ...string) (logger Logger, err error) {

var logPath string
var logFlags types.MaskBit
var exists bool
var success bool
var ckLogPaths []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,
},
Path: logPath,
}
} else {
logger = &StdLogger{
Prefix: source,
EnableDebug: enableDebug,
}
}
}

logger.Setup()

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

return

}

View File

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

import (
`errors`
)

func (l *WinLogger) Setup() {

var err error

/*
First 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 !(1 <= eid <= 1000) {
err = errors.New("event IDs must be between 1 and 1000 inclusive")
panic(err)
}
}

if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil {
// It's an error we want to panic on.
panic(err)
} else {
// It already exists, so ignore the error.
err = nil
}
}

if l.elog, err = eventlog.Open(l.Prefix); err != nil {
panic(err)
}

}

func (l *WinLogger) Shutdown() {

var err error

if err = l.elog.Close(); err != nil {
panic(err)
}

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

}

func (l *WinLogger) DoDebug(d bool) {

l.EnableDebug = d

}

func (l *WinLogger) SetPrefix(prefix string) {

var err error

l.Prefix = prefix

// To properly change the prefix, we need to tear down the old event log and create a new one.
if err = l.elog.Close(); err != nil {
panic(err)
}

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

if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil {
// It's an error we want to panic on.
panic(err)
} else {
// It already exists, so ignore the error.
err = nil
}
}

if l.elog, err = eventlog.Open(l.Prefix); err != nil {
panic(err)
}

}

func (l *WinLogger) GetPrefix() (prefix string) {

prefix = l.Prefix

return
}

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
}

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
}

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

}

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

}

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.Error, msg)

return

}

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

}

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

}

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

@ -2,7 +2,6 @@ package logging


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


@ -22,24 +21,6 @@ type Logger interface {
Shutdown() 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
}

type StdLogger struct { type StdLogger struct {
*log.Logger *log.Logger
EnableDebug bool EnableDebug bool

23
logging/types_linux.go Normal file
View File

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

import (
`log/syslog`
)

type SystemDLogger struct {
EnableDebug bool
Prefix string
}

type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

23
logging/types_windows.go Normal file
View File

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

import (
`golang.org/x/sys/windows/svc/eventlog`
)

type WinLogger struct {
EnableDebug bool
Prefix string
elog *eventlog.Log
eids *WinEventID
}

type WinEventID struct {
Alert,
Crit,
Debug,
Emerg,
Err,
Info,
Notice,
Warning uint32
}

View File

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

/*
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information.
To use this, set constants like thus:
const (
OPT1 types.MaskBit = 1 << iota
OPT2
OPT3
// ...
)

type Object struct {
Opts BitMask
}

o := Object{
BitMask: uint8(0)
}

o.AddFlag(OPT1)
o.AddFlag(OPT3)


This would return true:
o.HasFlag(OPT1)
As would this:
o.HasFlag(OPT3)
But this would return false:
o.HasFlag(OPT2)

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