26 Commits

Author SHA1 Message Date
brent saner
2222cea7fb v1.9.6
FIXED:
* More clear docs for bitmask
* Resolved potential issue for using PriorityAll in
  logging.logPrio.HasFlag.
2025-08-27 19:06:17 -04:00
brent saner
688abd0874 v1.9.5
FIXED:
* HasFlag would inappropriately report true for m = A, flag = A | B.
  This has been rectified, and this behavior is now explicitly
  exposed via IsOneOf.
2025-08-26 20:39:29 -04:00
brent saner
a1f87d6b51 stubbing encoding/bit 2025-08-23 19:32:48 -04:00
brent saner
07951f1f03 v1.9.4
FIXED:
* remap.ReMap.MapString() was not properly correllating groups. It is
  now.
2025-08-17 00:45:24 -04:00
brent saner
bae0abe960 v1.9.3
IMPROVED:
* Better documentation for remap
2025-08-12 00:06:51 -04:00
brent saner
368ae0cb8e v1.9.2
FIX:
* Yeah so the ReMap.Map* stuff was kind of broken hard. It's fixed now.
2025-08-04 04:26:52 +00:00
brent saner
154170c0e5 v1.9.1
ADDED:
* remap package
2025-08-02 09:39:06 -04:00
brent saner
d9bd928edb v1.9.0
ADD:
* `iox` subpackage

FIX:
* `logging` now has a way to return logWritier directly
* added significant `io.*` interface compat to logWriter -- allowing a `logging.Logger` to essentially be used for a large amount of io interaction in other libraries.
2025-07-31 03:45:32 -04:00
brent saner
dc2ed32352 v1.8.1
FIXED:
* Whoops, bit premature on 1.8.0; broke some Linux logging.
2025-02-10 13:20:36 -05:00
brent saner
e734e847c4 v1.8.0
ADDED:
* Basic macOS support (and BSD support, etc.)
* macOS has its own proprietary logging, ULS ("Unified Logging System"),
  but there doesn't seem to be native Golang support. So lolbai;
  your only options are syslog, stdlog, null log, filelog, and the
  "meta" logs (multilog, default log- which should use syslog).
2025-02-10 13:01:46 -05:00
brent saner
2203de4e32 need some general *nix errs too 2025-02-10 12:54:12 -05:00
brent saner
a0c6df14aa more general syslog support 2025-02-10 12:35:40 -05:00
brent saner
fd720f2b34 v1.7.2
FIXED:
* multierr race condition fix/now fully supports multithreading
2025-01-04 02:29:49 -05:00
brent saner
3c543a05e7 v1.7.1
FIXED:
* bitmask.MaskBit.ClearFlag now works properly. Whoooops, how long was
  that typo there?
2024-11-07 03:44:54 -05:00
brent saner
e5191383a7 v1.7.0
ADDED:
* logging.Logger objects now are able to return a stdlib *log.Logger.
2024-06-19 18:57:26 -04:00
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
70d6c2cbb3 updating logging TODO 2023-07-12 23:16:31 -04:00
a445a51c0d Adding GetDebug method to loggers. 2022-09-07 06:03:28 -04:00
a2a849600b Add docs for NullLogger. 2022-09-06 01:01:39 -04:00
94145fb4c7 Add NullLogger to MultiLogger. 2022-03-13 13:34:24 -04:00
81a2d308f0 Add NullLogger.
For when you need a Logger but don't want one. ;)
2022-03-13 13:29:31 -04:00
c4b3c6441a adding Bytes() to MaskBit 2022-02-01 18:27:45 -05:00
1c5abd4083 modifying bitmask to allow specifying an explicit value, and changing to uint instead of uint8 2022-02-01 15:36:56 -05:00
d98363c0d7 Finalizing for 1.2.0 2022-01-16 06:55:29 -05:00
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
61 changed files with 4053 additions and 506 deletions

View File

@@ -0,0 +1,19 @@
/*
Package bit aims to provide feature parity with stdlib's [encoding/hex].
It's a ludicrous tragedy that hex/base16, base32, base64 all have libraries for converting
to/from string representations... but there's nothing for binary ('01010001' etc.) whatsoever.
This package also provides some extra convenience functions and types in an attempt to provide
an abstracted bit-level fidelity in Go. A [Bit] is a bool type, in which that underlying bool
being false represents a 0 and that underlying bool being true represents a 1.
Note that a [Bit] or arbitrary-length or non-octal-aligned [][Bit] may take up more bytes in memory
than expected; a [Bit] will actually always occupy a single byte -- thus representing
`00000000 00000000` as a [][Bit] or [16][Bit] will actually occupy *sixteen bytes* in memory,
NOT 2 bytes (nor, obviously, [2][Byte])!
It is recommended instead to use a [Bits] instead of a [Bit] slice or array, as it will try to properly align to the
smallest memory allocation possible (at the cost of a few extra CPU cycles on adding/removing one or more [Bit]).
It will properly retain any appended, prepended, leading, or trailing bits that do not currently align to a byte.
*/
package bit

View File

@@ -0,0 +1,14 @@
package bit
// TODO: Provide analogues of encoding/hex, encoding/base64, etc. functions etc.
/*
TODO: Also provide interfaces for the following:
* https://pkg.go.dev/encoding#BinaryAppender
* https://pkg.go.dev/encoding#BinaryMarshaler
* https://pkg.go.dev/encoding#BinaryUnmarshaler
* https://pkg.go.dev/encoding#TextAppender
* https://pkg.go.dev/encoding#TextMarshaler
* https://pkg.go.dev/encoding#TextUnmarshaler
*/

View File

@@ -0,0 +1,34 @@
package bit
type (
// Bit aims to provide a native-like type for a single bit (Golang operates on the smallest fidelity level of *byte*/uint8).
Bit bool
// Bits is an arbitrary length of bits.
Bits struct {
/*
leading is a series of Bit that do not cleanly align to the beginning of Bits.b.
They will always be the bits at the *beginning* of the sequence.
len(Bits.leading) will *never* be more than 7;
it's converted into a byte, prepended to Bits.b, and cleared if it reaches that point.
*/
leading []Bit
// b is the condensed/memory-aligned alternative to an [][8]Bit (or []Bit, or [][]Bit, etc.).
b []byte
/*
remaining is a series of Bit that do not cleanly align to the end of Bits.b.
They will always be the bits at the *end* of the sequence.
len(Bits.remaining) will *never* be more than 7;
it's converted into a byte, appended to Bits.b, and cleared if it reaches that point.
*/
remaining []Bit
// fixedLen, if 0, represents a "slice". If >= 1, it represents an "array".
fixedLen uint
}
// Byte is this package's representation of a byte. It's primarily for convenience.
Byte byte
// Bytes is defined as a type for convenience single-call functions.
Bytes []Byte
)

5
.gitignore vendored
View File

@@ -28,6 +28,11 @@
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.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 # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out

171
bitmask/bitmask.go Normal file
View File

@@ -0,0 +1,171 @@
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.
THIS WILL RETURN FALSE FOR OR'd FLAGS.
For example:
flagA MaskBit = 0x01
flagB MaskBit = 0x02
flagComposite = flagA | flagB
m *MaskBit = NewMaskBitExplicit(uint(flagA))
m.HasFlag(flagComposite) will return false even though flagComposite is an OR
that contains flagA.
Use [MaskBit.IsOneOf] instead if you do not desire this behavior,
and instead want to test composite flag *membership*.
(MaskBit.IsOneOf will also return true for non-composite equality.)
To be more clear, if MaskBit flag is a composite MaskBit (e.g. flagComposite above),
HasFlag will only return true of ALL bits in flag are also set in MaskBit m.
*/
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
var b MaskBit = *m
if b&flag == flag {
r = true
}
return
}
/*
IsOneOf is like a "looser" form of [MaskBit.HasFlag]
in that it allows for testing composite membership.
See [MaskBit.HasFlag] for more information.
If composite is *not* an OR'd MaskBit (i.e.
it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.),
then IsOneOf will behave exactly like HasFlag.
If m is a composite MaskBit (it usually is) and composite is ALSO a composite MaskBit,
IsOneOf will return true if ANY of the flags set in m is set in composite.
*/
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) {
var b MaskBit = *m
if b&composite != 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
}

View File

@@ -1,46 +0,0 @@
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
}

View File

@@ -1,9 +1,35 @@
/* /*
Package bitmask handles a flag-like opt/bitmask system. Package bitmask handles a flag-like opt/bitmask system.
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information. See https://yourbasic.org/golang/bitmask-flag-set-clear/ for basic information on what bitmasks are and why they're useful.
To use this, set constants like thus: Specifically, in the case of Go, they allow you to essentially manage many, many, many "booleans" as part of a single value.
A single bool value in Go takes up 8 bits/1 byte, unavoidably.
However, a [bitmask.MaskBit] is backed by a uint which (depending on your platform) is either 32 bits/4 bytes or 64 bits/8 bytes.
"But wait, that takes up more memory though!"
Yep, but bitmasking lets you store a "boolean" AT EACH BIT - it operates on
whether a bit in a byte/set of bytes at a given position is 0 or 1.
Which means on 32-bit platforms, a [MaskBit] can have up to 4294967295 "booleans" in a single value (0 to (2^32)-1).
On 64-bit platforms, a [MaskBit] can have up to 18446744073709551615 "booleans" in a single value (0 to (2^64)-1).
If you tried to do that with Go bool values, that'd take up 4294967295 bytes (4 GiB)
or 18446744073709551615 bytes (16 EiB - yes, that's [exbibytes]) of RAM for 32-bit/64-bit platforms respectively.
"But that has to be so slow to unpack that!"
Nope. It's not using compression or anything, the CPU is just comparing bit "A" vs. bit "B" 32/64 times. That's super easy work for a CPU.
There's a reason Doom used bitmasking for the "dmflags" value in its server configs.
# Usage
To use this library, set constants like thus:
package main package main
@@ -11,18 +37,18 @@ To use this, set constants like thus:
"r00t2.io/goutils/bitmask" "r00t2.io/goutils/bitmask"
) )
const OPTNONE types.MaskBit = 0 const OPTNONE bitmask.MaskBit = 0
const ( const (
OPT1 types.MaskBit = 1 << iota OPT1 bitmask.MaskBit = 1 << iota
OPT2 OPT2
OPT3 OPT3
// ... // ...
) )
var MyMask *MaskBit var MyMask *bitmask.MaskBit
func main() { func main() {
MyMask = types.NewMaskBit MyMask = bitmask.NewMaskBit()
MyMask.AddFlag(OPT1) MyMask.AddFlag(OPT1)
MyMask.AddFlag(OPT3) MyMask.AddFlag(OPT3)
@@ -41,5 +67,96 @@ As would this:
But this would return false: But this would return false:
MyMask.HasFlag(OPT2) MyMask.HasFlag(OPT2)
# Technical Caveats
TARGETING
When implementing, you should always set MyMask (from Usage section above) as the actual value.
For example, if you are checking a permissions set for a user that has the value, say, 6
var userPerms uint = 6 // 0x0000000000000006
and your library has the following permission bits defined:
const PermsNone bitmask.MaskBit = 0
const (
PermsList bitmask.MaskBit = 1 << iota // 1
PermsRead // 2
PermsWrite // 4
PermsExec // 8
PermsAdmin // 16
)
And you want to see if the user has the PermsRead flag set, you would do:
userPermMask = bitmask.NewMaskBitExplicit(userPerms)
if userPermMask.HasFlag(PermsRead) {
// ...
}
NOT:
userPermMask = bitmask.NewMaskBitExplicit(PermsRead)
// Nor:
// userPermMask = PermsRead
if userPermMask.HasFlag(userPerms) {
// ...
}
This will be terribly, horribly wrong, cause incredibly unexpected results,
and quite possibly cause massive security issues. Don't do it.
COMPOSITES
If you want to define a set of flags that are a combination of other flags,
your inclination would be to bitwise-OR them together:
const (
flagA bitmask.MaskBit = 1 << iota // 1
flagB // 2
)
const (
flagAB bitmask.MaskBit = flagA | flagB // 3
)
Which is fine and dandy. But if you then have:
var myMask *bitmask.MaskBit = bitmask.NewMaskBit()
myMask.AddFlag(flagA)
You may expect this call to [MaskBit.HasFlag]:
myMask.HasFlag(flagAB)
to be true, since flagA is "in" flagAB.
It will return false - HasFlag does strict comparisons.
It will only return true if you then ALSO do:
// This would require setting flagA first.
// The order of setting flagA/flagB doesn't matter,
// but you must have both set for HasFlag(flagAB) to return true.
myMask.AddFlag(flagB)
or if you do:
// This can be done with or without additionally setting flagA.
myMask.AddFlag(flagAB)
Instead, if you want to see if a mask has membership within a composite flag,
you can use [MaskBit.IsOneOf].
# Other Options
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]
[exbibytes]: https://simple.wikipedia.org/wiki/Exbibyte
*/ */
package bitmask package bitmask

15
go.mod
View File

@@ -1,10 +1,15 @@
module r00t2.io/goutils module r00t2.io/goutils
go 1.16 go 1.24.5
require ( require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.6.0
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e golang.org/x/sys v0.34.0
r00t2.io/sysutils v1.1.1 r00t2.io/sysutils v1.14.0
)
require (
github.com/djherbis/times v1.6.0 // indirect
golang.org/x/sync v0.16.0 // indirect
) )

21
go.sum
View File

@@ -1,8 +1,13 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=

4
iox/docs.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package iox includes extensions to the stdlib `io` module.
*/
package iox

9
iox/errs.go Normal file
View File

@@ -0,0 +1,9 @@
package iox
import (
`errors`
)
var (
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
)

41
iox/funcs.go Normal file
View File

@@ -0,0 +1,41 @@
package iox
import (
`io`
)
/*
CopyBufN is a mix between io.CopyN and io.CopyBuffer.
Despite what the docs may suggest, io.CopyN does NOT *read* n bytes from src AND write n bytes to dst.
Instead, it always reads 32 KiB from src, and writes n bytes to dst.
There are, of course, cases where this is deadfully undesired.
One can, of course, use io.CopyBuffer, but this is a bit annoying since you then have to provide a buffer yourself.
This convenience-wraps io.CopyBuffer to have a similar signature to io.CopyN but properly uses n for both reading and writing.
*/
func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
var b []byte
if n <= 0 {
err = ErrBufTooSmall
return
}
b = make([]byte, n)
written, err = io.CopyBuffer(dst, src, b)
return
}
// CopyBufWith allows for specifying a buffer allocator function, otherwise acts as CopyBufN.
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
written, err = io.CopyBuffer(dst, src, bufFunc())
return
}

8
iox/types.go Normal file
View File

@@ -0,0 +1,8 @@
package iox
type (
// RuneWriter matches the behavior of *(bytes.Buffer).WriteRune and *(bufio.Writer).WriteRune
RuneWriter interface {
WriteRune(r rune) (n int, err error)
}
)

View File

@@ -1,11 +1,25 @@
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
-- https://developer.apple.com/documentation/os/logging
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code
-- no native Go support (yet)?
--- https://developer.apple.com/forums/thread/773369
- The log destinations for e.g. consts_nix.go et. al. probably should be unexported types.
- add a `log/slog` logging.Logger?
- 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
-- 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.
-- ZeroLog seems to be able to do it, take a peek there.
- Support simultaneous writing to multiple Loggers. - StdLogger2; where stdout and stderr are both logged to depending on severity level.
- make configurable via OR bitmask
- Suport remote loggers? (eventlog, syslog, systemd) - Suport remote loggers? (eventlog, syslog, journald)
- JSON logger? YAML logger? XML logger?
- DOCS. - DOCS.
-- Done, but flesh out.
- Unit/Integration tests.

View File

@@ -12,3 +12,16 @@ const (
// appendFlags are the flags used for testing the file (and opening/writing). // appendFlags are the flags used for testing the file (and opening/writing).
appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY
) )
const PriorityNone logPrio = 0
const (
PriorityEmergency logPrio = 1 << iota
PriorityAlert
PriorityCritical
PriorityError
PriorityWarning
PriorityNotice
PriorityInformational
PriorityDebug
)
const PriorityAll logPrio = PriorityEmergency | PriorityAlert | PriorityCritical | PriorityError | PriorityWarning | PriorityNotice | PriorityInformational | PriorityDebug

9
logging/consts_darwin.go Normal file
View File

@@ -0,0 +1,9 @@
package logging
var (
// defLogPaths indicates default log paths.
defLogPaths = []string{
"/var/log/golang/program.log",
"~/Library/Logs/Golang/program.log",
}
)

View File

@@ -1,32 +1,5 @@
package logging 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 ( var (
// defLogPaths indicates default log paths. // defLogPaths indicates default log paths.
defLogPaths = []string{ defLogPaths = []string{

34
logging/consts_nix.go Normal file
View File

@@ -0,0 +1,34 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
// I mean maybe it works for plan9 and ios, I don't know.
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.
// LogUndefined indicates an undefined Logger type.
const LogUndefined bitmask.MaskBit = iota
const (
// LogJournald flags a SystemDLogger Logger type. This will, for hopefully obvious reasons, only work on Linux systemd systems.
LogJournald bitmask.MaskBit = 1 << iota
// LogSyslog flags a SyslogLogger Logger type.
LogSyslog
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)

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
)

View File

@@ -3,17 +3,14 @@ package logging
import ( import (
`os` `os`
`path/filepath` `path/filepath`
`regexp`
`r00t2.io/goutils/bitmask`
) )
// Flags for logger configuration. These are used internally. // Flags for logger configuration. These are used internally.
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 0
const ( const (
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 1 << iota
// LogWinLogger indicates a WinLogger Logger type (Event Log). // LogWinLogger indicates a WinLogger Logger type (Event Log).
LogWinLogger LogWinLogger bitmask.MaskBit= 1 << iota
// LogFile flags a FileLogger Logger type. // LogFile flags a FileLogger Logger type.
LogFile LogFile
// LogStdout flags a StdLogger Logger type. // LogStdout flags a StdLogger Logger type.
@@ -28,8 +25,21 @@ var (
} }
) )
// ptrnSourceExists is a regex pattern to check for a registry entry (Event Log entry) already existing. /*
var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`) ptrnSourceExists is a regex pattern to check for a registry entry (Event Log entry) already existing.
Deprecated: this is handled differently now.
*/
// var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`)
const (
EIDMin uint32 = 1
EIDMax uint32 = 1000
)
const (
eventLogRegistryKey string = "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application"
)
// Default WinEventID, (can be) used in GetLogger and MultiLogger.AddWinLogger. // Default WinEventID, (can be) used in GetLogger and MultiLogger.AddWinLogger.
var DefaultEventID *WinEventID = &WinEventID{ var DefaultEventID *WinEventID = &WinEventID{
@@ -43,7 +53,7 @@ var DefaultEventID *WinEventID = &WinEventID{
Warning: EventWarning, Warning: EventWarning,
} }
// Default Event IDs for WinEventID. // Default Event IDs for WinEventID (DefaultEventID, specifically).
const ( const (
EventAlert uint32 = 1 << iota EventAlert uint32 = 1 << iota
EventCrit EventCrit

View File

@@ -3,18 +3,22 @@ Package logging implements and presents various loggers under a unified interfac
These particular loggers (logging.Logger) available are: These particular loggers (logging.Logger) available are:
NullLogger
StdLogger StdLogger
FileLogger FileLogger
SystemDLogger (Linux only) SystemDLogger (Linux only)
SyslogLogger (Linux only) SyslogLogger (Linux/macOS/other *NIX-like only)
WinLogger (Windows only) WinLogger (Windows only)
There is a sixth type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call. There is a seventh type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
(This is similar to stdlib's io.MultiWriter()'s return value, but with priority awareness and fmt string support).
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. As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger (similar to io.discard/io.Discard()'s return).
Note that for some Loggers, the prefix may be modified after the Logger has already initialized.
"Literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix by default.
If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger. 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". Every logging.Logger type has the following methods that correspond to certain "levels".
Alert(s string, v ...interface{}) (err error) Alert(s string, v ...interface{}) (err error)
@@ -35,13 +39,26 @@ Note that in the case of a MultiLogger, err (if not nil) will be a (r00t2.io/gou
logging.Logger types also have the following methods: logging.Logger types also have the following methods:
DoDebug(d bool) DoDebug(d bool) (err error)
SetPrefix(p string) GetDebug() (d bool)
GetPrefix() (p string) SetPrefix(p string) (err error)
Setup() GetPrefix() (p string, err error)
Shutdown() 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. 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. It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type.
Lastly, all logging.Loggers have a ToLogger() method. This returns a *log.Logger (from stdlib log), which also conforms to io.Writer inherently.
In addition. all have a ToRaw() method, which extends a Logger even further and returns an unexported type (*logging.logWriter) compatible with:
- io.ByteWriter
- io.Writer
- io.WriteCloser (Shutdown() on the Logger backend is called during Close(), rendering the underlying Logger unsafe to use afterwards)
- io.StringWriter
and, if stdlib io ever defines an e.g. RuneWriter (WriteRune(r rune) (n int, err error)), it will conform to that too (see (r00t2.io/goutils/iox).RuneWriter).
Obviously this and io.ByteWriter are fairly silly, as they're intended to be high-speed throughput-optimized methods, but if you wanted to e.g.
log every single byte on a wire as a separate log message, go ahead; I'm not your dad.
*/ */
package logging package logging

View File

@@ -1,7 +1,7 @@
package logging package logging
import ( import (
`errors` "errors"
) )
var ( var (
@@ -12,6 +12,8 @@ var (
exists with too restrictive perms to write/append to, and/or could not be created. 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") ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created")
// ErrInvalidRune is returned if a rune was expected but it is not a valid UTF-8 codepoint.
ErrInvalidRune error = errors.New("specified rune is not valid UTF-8 codepoint")
// ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist. // 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") ErrNoEntry error = errors.New("the Logger specified to be removed does not exist")
) )

View File

@@ -1,18 +1,10 @@
package logging package logging
import ( import (
`errors` "errors"
`fmt`
) )
var ( var (
// ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable. // 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") 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))
) )

19
logging/errs_nix.go Normal file
View File

@@ -0,0 +1,19 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"errors"
"fmt"
)
var (
// 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,18 +1,45 @@
package logging package logging
import ( import (
"log"
"os" "os"
) )
/*
ToLog returns a stdlib *log.Logger from a logging.Logger. It simply wraps the (logging.Logger).ToLogger() methods.
prio is an OR'd logPrio of the Priority* constants.
*/
func ToLog(l Logger, prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = l.ToLogger(prio)
return
}
// ToRaw returns a *logWriter from a logging.Logger. It is an alternative to the (logging.Logger).ToRaw() methods.
func ToRaw(l Logger, prio logPrio) (raw *logWriter) {
raw = &logWriter{
backend: l,
prio: prio,
}
return
}
// testOpen attempts to open a file for writing to test for suitability as a LogFile path. // testOpen attempts to open a file for writing to test for suitability as a LogFile path.
func testOpen(path string) (success bool, err error) { func testOpen(path string) (success bool, err error) {
var f *os.File var f *os.File
// Example #2, https://golang.org/pkg/os/#OpenFile // Example #2, https://golang.org/pkg/os/#OpenFile
if f, err = os.OpenFile(path, appendFlags, logPerm); err != nil { if f, err = os.OpenFile(path, appendFlags, logPerm); err != nil {
return return
} }
defer f.Close() if err = f.Close(); err != nil {
return
}
success = true success = true

View File

@@ -1,61 +1,91 @@
package logging package logging
import ( import (
"errors"
"fmt" "fmt"
"io" "io/fs"
"log" "log"
"os" "os"
"strings"
) )
// Setup sets up/configures a FileLogger and prepares it for use. // Setup sets up/configures a FileLogger and prepares it for use.
func (l *FileLogger) Setup() { func (l *FileLogger) Setup() (err error) {
var err error // This uses a shared handle across the import. We don't want that.
var multi io.Writer // l.Logger = log.Default()
if l.Prefix != "" {
l.Logger = log.Default() l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
l.Logger.SetPrefix(l.Prefix + " ") // l.Logger.SetPrefix(l.Prefix)
}
if l.writer, err = os.OpenFile(l.Path, appendFlags, logPerm); err != nil { 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) return
} }
// https://stackoverflow.com/a/36719588/733214
if l.EnableStdOut { l.Logger = log.New(l.writer, l.Prefix, l.LogFlags)
multi = io.MultiWriter(os.Stdout, l.writer) // l.Logger.SetOutput(multi)
} else {
multi = l.writer return
}
l.Logger.SetOutput(multi)
} }
// Shutdown cleanly shuts down a FileLogger. // Shutdown cleanly shuts down a FileLogger.
func (l *FileLogger) Shutdown() { func (l *FileLogger) Shutdown() (err error) {
var err error
if err = l.writer.Close(); err != nil { 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
// GetPrefix returns the prefix used by this FileLogger.
func (l *FileLogger) GetPrefix() string {
return l.Prefix
} }
/* /*
DoDebug sets the debug state of this FileLogger. GetPrefix returns the prefix used by this FileLogger.
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log. err will always be nil; it's there for interface-compat.
*/ */
func (l *FileLogger) DoDebug(d bool) { func (l *FileLogger) GetPrefix() (prefix string, err error) {
l.EnableDebug = d
prefix = l.Prefix
return
} }
// SetPrefix sets the prefix for this FileLogger. /*
func (l *FileLogger) SetPrefix(prefix string) { 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.Prefix = prefix + " " l.EnableDebug = d
l.Logger.SetPrefix(prefix + " ")
return
}
// 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
if prefix != "" {
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
}
l.Logger.SetPrefix(l.Prefix)
return
} }
// Alert writes an ALERT-level message to this FileLogger. // Alert writes an ALERT-level message to this FileLogger.
@@ -190,6 +220,22 @@ func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *FileLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *FileLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}
// renderWrite prepares/formats a log message to be written to this FileLogger. // renderWrite prepares/formats a log message to be written to this FileLogger.
func (l *FileLogger) renderWrite(msg, prio string) { func (l *FileLogger) renderWrite(msg, prio string) {

View File

@@ -5,7 +5,7 @@ import (
`os` `os`
`path` `path`
sysd `github.com/coreos/go-systemd/journal` sysd `github.com/coreos/go-systemd/v22/journal`
`r00t2.io/goutils/bitmask` `r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths` `r00t2.io/sysutils/paths`
) )
@@ -24,6 +24,9 @@ var (
If prefix is "\x00" (a null byte), then the default logging prefix will be used. If anything else, even an empty string, 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. 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, 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. 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 want to log to multiple files simultaneously, use a MultiLogger instead.
@@ -32,10 +35,11 @@ var (
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type), If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
use GetMultiLogger. use GetMultiLogger.
*/ */
func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logger, err error) { func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
var logPath string var logPath string
var logFlags bitmask.MaskBit var logFlags bitmask.MaskBit
var currentPrefix string
// Configure system-supported logger(s). // Configure system-supported logger(s).
if sysd.Enabled() { if sysd.Enabled() {
@@ -109,6 +113,7 @@ func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logg
StdLogger: StdLogger{ StdLogger: StdLogger{
Prefix: logPrefix, Prefix: logPrefix,
EnableDebug: enableDebug, EnableDebug: enableDebug,
LogFlags: logConfigFlags,
}, },
Path: logPath, Path: logPath,
} }
@@ -116,17 +121,26 @@ func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logg
logger = &StdLogger{ logger = &StdLogger{
Prefix: logPrefix, Prefix: logPrefix,
EnableDebug: enableDebug, EnableDebug: enableDebug,
LogFlags: logConfigFlags,
} }
} }
} }
} }
logger.Setup()
if prefix != "\x00" { if prefix != "\x00" {
logger.SetPrefix(prefix) if err = logger.SetPrefix(prefix); err != nil {
return
}
}
if err = logger.Setup(); err != nil {
return
} }
logger.Info("logger initialized of type %T with prefix %v", logger, logger.GetPrefix()) if currentPrefix, err = logger.GetPrefix(); err != nil {
return
}
logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix)
return 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)
}

25
logging/funcs_logprio.go Normal file
View File

@@ -0,0 +1,25 @@
package logging
import (
`r00t2.io/goutils/bitmask`
)
// HasFlag provides a wrapper for functionality to the underlying bitmask.MaskBit.
func (l *logPrio) HasFlag(prio logPrio) (hasFlag bool) {
var m *bitmask.MaskBit
var p *bitmask.MaskBit
if l == nil {
return
}
m = bitmask.NewMaskBitExplicit(uint(*l))
p = bitmask.NewMaskBitExplicit(uint(prio))
// Use IsOneOf instead in case PriorityAll is passed for prio.
// hasFlag = m.HasFlag(*p)
hasFlag = m.IsOneOf(*p)
return
}

211
logging/funcs_logwriter.go Normal file
View File

@@ -0,0 +1,211 @@
package logging
import (
"unicode/utf8"
"r00t2.io/goutils/multierr"
)
/*
Close calls Logger.Shutdown() on the underlying Logger.
The Logger *must not be used* after this; it will need to be re-initialized with Logger.Setup()
or a new Logger (and thuse new logWriter) must be created to replace it.
It (along with logWriter.Write()) conforms to WriteCloser().
*/
func (l *logWriter) Close() (err error) {
if err = l.backend.Shutdown(); err != nil {
return
}
return
}
/*
Write writes bytes b to the underlying Logger's priority level if the logWriter's priority level(s) match.
It conforms to io.Writer. n will *always* == len(b) on success, because otherwise n would technically be >= len(b)
(if multiple priorities are enabled), which is undefined behavior per io.Writer.
b is converted to a string to normalize to the underlying Logger.
*/
func (l *logWriter) Write(b []byte) (n int, err error) {
var s string
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
if b == nil {
return
}
s = string(b)
// Since this explicitly checks each priority level, there's no need for IsOneOf in case of PriorityAll.
if l.prio.HasFlag(PriorityEmergency) {
if err = l.backend.Emerg(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityAlert) {
if err = l.backend.Alert(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityCritical) {
if err = l.backend.Crit(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityError) {
if err = l.backend.Err(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityWarning) {
if err = l.backend.Warning(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityNotice) {
if err = l.backend.Notice(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityInformational) {
if err = l.backend.Info(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityDebug) {
if err = l.backend.Debug(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
n = len(b)
return
}
/*
WriteByte conforms a logWriter to an io.ByteWriter. (It just wraps logWriter.Write().)
You should probably never use this; the logging overhead/prefix is going to be more data than the single byte itself.
c is converted to a string to normalize to the underlying Logger.
*/
func (l *logWriter) WriteByte(c byte) (err error) {
if _, err = l.Write([]byte{c}); err != nil {
return
}
return
}
/*
WriteRune follows the same signature of (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune(); thus if `io` ever defines an io.RuneWriter interface, here ya go.
n will *always* be equal to (unicode/utf8).RuneLen(r), unless r is an "invalid rune" -- in which case n will be 0 and err will be ErrInvalidRune..
*/
func (l *logWriter) WriteRune(r rune) (n int, err error) {
var b []byte
n = utf8.RuneLen(r)
if n < 0 {
err = ErrInvalidRune
n = 0
return
}
b = make([]byte, n)
utf8.EncodeRune(b, r)
if n, err = l.Write(b); err != nil {
return
}
return
}
/*
WriteString writes string s to the underlying Logger's priority level if the logWriter's priority level(s) match.
It conforms to io.StringWriter. n will *always* == len(s) on success, because otherwise n would technically be >= len(s)
(if multiple priorities are enabled), which is undefined behavior per io.StringWriter.
*/
func (l *logWriter) WriteString(s string) (n int, err error) {
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
if l.prio.HasFlag(PriorityEmergency) {
if err = l.backend.Emerg(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityAlert) {
if err = l.backend.Alert(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityCritical) {
if err = l.backend.Crit(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityError) {
if err = l.backend.Err(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityWarning) {
if err = l.backend.Warning(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityNotice) {
if err = l.backend.Notice(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityInformational) {
if err = l.backend.Info(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityDebug) {
if err = l.backend.Debug(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
n = len(s)
return
}

View File

@@ -1,93 +1,161 @@
package logging package logging
import ( import (
`sync` "errors"
"fmt"
"log"
"sync"
`r00t2.io/goutils/multierr` "r00t2.io/goutils/multierr"
) )
// Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use. // Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use.
func (m *MultiLogger) Setup() { func (m *MultiLogger) Setup() (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func() { go func(logger Logger, lName string) {
var err2 error
defer wg.Done() defer wg.Done()
l.Setup() 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() wg.Wait()
if errs.Count() > 0 {
err = errs
return
}
return
} }
// Shutdown cleanly shuts down a MultiLogger (and all its MultiLogger.Loggers). // Shutdown cleanly shuts down a MultiLogger (and all its MultiLogger.Loggers).
func (m *MultiLogger) Shutdown() { func (m *MultiLogger) Shutdown() (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func() { go func(logger Logger, lName string) {
var err2 error
defer wg.Done() defer wg.Done()
l.Shutdown() 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() wg.Wait()
if errs.Count() > 0 {
err = errs
return
}
return
} }
// GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers). /*
func (m *MultiLogger) GetPrefix() string { GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers).
err will always be nil; it's there for interface-compat.
*/
func (m *MultiLogger) GetPrefix() (prefix string, err error) {
return m.Prefix prefix = m.Prefix
return
} }
/* /*
DoDebug sets the debug state of this MultiLogger (and all its MultiLogger.Loggers). 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. 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. If you had a logger-specific EnableDebug set, you will need to re-set it to your desired state after running this method.
*/ */
func (m *MultiLogger) DoDebug(d bool) { func (m *MultiLogger) DoDebug(d bool) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)
m.EnableDebug = d m.EnableDebug = d
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func() { go func(logger Logger, lName string) {
var err2 error
defer wg.Done() defer wg.Done()
l.DoDebug(d) 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() 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). 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. If you had a logger-specific Prefix set, you will need to re-set it to your desired prefix after running this method.
*/ */
func (m *MultiLogger) SetPrefix(prefix string) { func (m *MultiLogger) SetPrefix(prefix string) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var errs *multierr.MultiError = multierr.NewMultiError(nil)
m.Prefix = prefix m.Prefix = prefix
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func() { go func(logger Logger, lName string) {
var err2 error
defer wg.Done() defer wg.Done()
l.SetPrefix(prefix) 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() wg.Wait()
if errs.Count() > 0 {
err = errs
return
}
return
} }
// Alert writes an ALERT-level message to this MultiLogger (and all its MultiLogger.Loggers). // Alert writes an ALERT-level message to this MultiLogger (and all its MultiLogger.Loggers).
@@ -96,20 +164,16 @@ func (m *MultiLogger) Alert(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Alert(msg, rplc...); err != nil {
err = logObj.Alert(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Alert for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Alert(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -127,20 +191,16 @@ func (m *MultiLogger) Crit(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Crit(msg, rplc...); err != nil {
err = logObj.Crit(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Crit for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Crit(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -158,20 +218,16 @@ func (m *MultiLogger) Debug(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Debug(msg, rplc...); err != nil {
err = logObj.Debug(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Debug for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Debug(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -188,20 +244,16 @@ func (m *MultiLogger) Emerg(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Emerg(msg, rplc...); err != nil {
err = logObj.Emerg(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Emerg for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Emerg(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -218,20 +270,16 @@ func (m *MultiLogger) Err(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Err(msg, rplc...); err != nil {
err = logObj.Err(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Err for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Err(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -249,20 +297,16 @@ func (m *MultiLogger) Info(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Info(msg, rplc...); err != nil {
err = logObj.Info(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Info for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Info(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -280,20 +324,16 @@ func (m *MultiLogger) Notice(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Notice(msg, rplc...); err != nil {
err = logObj.Notice(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Notice for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Notice(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -311,20 +351,16 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var e *multierr.MultiError = multierr.NewMultiError(nil) var e *multierr.MultiError = multierr.NewMultiError(nil)
for _, l := range m.Loggers { for logName, l := range m.Loggers {
wg.Add(1) wg.Add(1)
go func(logObj Logger, msg string, rplc ...interface{}) { go func(logObj Logger, msg, lName string, rplc ...interface{}) {
defer wg.Done() defer wg.Done()
if rplc != nil { if err = logObj.Warning(msg, rplc...); err != nil {
err = logObj.Warning(msg, rplc...) e.AddError(errors.New(fmt.Sprintf("error on Warning for logger %v; follows (may be out of order):", lName)))
} else {
err = logObj.Warning(msg)
}
if err != nil {
e.AddError(err) e.AddError(err)
err = nil err = nil
} }
}(l, s, v) }(l, s, logName, v...)
} }
wg.Wait() wg.Wait()
@@ -335,3 +371,19 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (m *MultiLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(m.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (m *MultiLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: m, prio: prio}
return
}

View File

@@ -1,10 +1,10 @@
package logging package logging
import ( import (
`path` "path"
`github.com/google/uuid` "github.com/google/uuid"
`r00t2.io/sysutils/paths` "r00t2.io/sysutils/paths"
) )
/* /*
@@ -21,7 +21,7 @@ func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) {
m = &MultiLogger{ m = &MultiLogger{
EnableDebug: enableDebug, EnableDebug: enableDebug,
Prefix: "", Prefix: logPrefix,
Loggers: make(map[string]Logger), Loggers: make(map[string]Logger),
} }
if prefix != "\x00" { if prefix != "\x00" {
@@ -36,10 +36,19 @@ func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) {
identifier is a string to use to identify the added StdLogger in MultiLogger.Loggers. identifier is a string to use to identify the added StdLogger in MultiLogger.Loggers.
If empty, one will be automatically generated. 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) (err error) { func (m *MultiLogger) AddStdLogger(identifier string, enableStdOut, enableStdErr bool, logFlags int) (err error) {
var exists bool var exists bool
var prefix string
if identifier == "" { if identifier == "" {
identifier = uuid.New().String() identifier = uuid.New().String()
@@ -54,10 +63,19 @@ func (m *MultiLogger) AddStdLogger(identifier string) (err error) {
Logger: nil, Logger: nil,
EnableDebug: m.EnableDebug, EnableDebug: m.EnableDebug,
Prefix: m.Prefix, Prefix: m.Prefix,
LogFlags: logFlags,
EnableStdOut: enableStdOut,
EnableStdErr: enableStdErr,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
} }
m.Loggers[identifier].Setup()
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return return
} }
@@ -70,11 +88,12 @@ func (m *MultiLogger) AddStdLogger(identifier string) (err error) {
logfilePath is a string for the path to the desired logfile. logfilePath is a string for the path to the desired logfile.
*/ */
func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error) { func (m *MultiLogger) AddFileLogger(identifier string, logFlags int, logfilePath string) (err error) {
var exists bool var exists bool
var success bool var success bool
var dirPath string var dirPath string
var prefix string
if identifier == "" { if identifier == "" {
identifier = uuid.New().String() identifier = uuid.New().String()
@@ -85,7 +104,9 @@ func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error)
return return
} }
if exists, _ = paths.RealPathExists(&logfilePath); !exists { if exists, err = paths.RealPathExists(&logfilePath); err != nil {
return
} else if !exists {
if success, err = testOpen(logfilePath); err != nil { if success, err = testOpen(logfilePath); err != nil {
return return
} else if !success { } else if !success {
@@ -107,12 +128,53 @@ func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error)
Logger: nil, Logger: nil,
EnableDebug: m.EnableDebug, EnableDebug: m.EnableDebug,
Prefix: m.Prefix, Prefix: m.Prefix,
LogFlags: logFlags,
}, },
Path: logfilePath, Path: logfilePath,
} }
m.Loggers[identifier].Setup() if err = m.Loggers[identifier].Setup(); err != nil {
return
}
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return
}
/*
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 return
} }

View File

@@ -1,20 +1,19 @@
package logging package logging
import ( import (
`os` sysd "github.com/coreos/go-systemd/v22/journal"
"github.com/google/uuid"
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. 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. identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
If empty, one will be automatically generated. If empty, one will be automatically generated.
See the documentation for GetLogger for details on other arguments.
*/ */
func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (err error) { func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
var l Logger var l Logger
var exists bool var exists bool
@@ -28,7 +27,7 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (e
return return
} }
if l, err = GetLogger(m.EnableDebug, m.Prefix, logPaths...); err != nil { if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil {
return return
} }
@@ -38,14 +37,15 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (e
} }
/* /*
AddSysdLogger adds a SystemDLogger to a MultiLogger. AddSysdLogger adds a SystemDLogger to a MultiLogger.
identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers. identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
If empty, one will be automatically generated. If empty, one will be automatically generated.
*/ */
func (m *MultiLogger) AddSysdLogger(identifier string) (err error) { func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
var exists bool var exists bool
var prefix string
if identifier == "" { if identifier == "" {
identifier = uuid.New().String() identifier = uuid.New().String()
@@ -65,54 +65,15 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
EnableDebug: m.EnableDebug, EnableDebug: m.EnableDebug,
Prefix: m.Prefix, Prefix: m.Prefix,
} }
m.Loggers[identifier].Setup() if err = m.Loggers[identifier].Setup(); err != nil {
return
}
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
return }
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
/*
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
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,
}
m.Loggers[identifier].Setup()
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix())
return return
} }

View File

@@ -0,0 +1,63 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"os"
"github.com/google/uuid"
"r00t2.io/sysutils/paths"
)
/*
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,41 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own.
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.
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
}

View File

@@ -16,10 +16,12 @@ import (
logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to, 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). 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. Only the first logPaths entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a WinLogger. Currently this will almost always return a WinLogger.
*/ */
func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logPaths ...string) (err error) { func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logFlags int, logPaths ...string) (err error) {
var l Logger var l Logger
var exists bool var exists bool
@@ -34,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
} }
if logPaths != nil { if logPaths != nil {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logPaths...); l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
} else { } else {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs); l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags)
} }
if err != nil { if err != nil {
return return
@@ -63,9 +65,10 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
See GetLogger for details. See GetLogger for details.
*/ */
func (m *MultiLogger) AddWinLogger(identifier, source string, eventIDs *WinEventID) (err error) { func (m *MultiLogger) AddWinLogger(identifier string, eventIDs *WinEventID) (err error) {
var exists bool var exists bool
var prefix string
if identifier == "" { if identifier == "" {
identifier = uuid.New().String() identifier = uuid.New().String()
@@ -81,13 +84,19 @@ func (m *MultiLogger) AddWinLogger(identifier, source string, eventIDs *WinEvent
} }
m.Loggers[identifier] = &WinLogger{ m.Loggers[identifier] = &WinLogger{
Prefix: source, Prefix: m.Prefix,
EnableDebug: m.EnableDebug, EnableDebug: m.EnableDebug,
eids: eventIDs, EIDs: eventIDs,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
} }
m.Loggers[identifier].Setup()
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix()) if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return return
} }

View File

@@ -0,0 +1,94 @@
package logging
import (
"log"
)
// 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
}
// ToLogger returns a stdlib log.Logger (that doesn't actually write to anything).
func (l *NullLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(&nullWriter{}, "", 0)
return
}
// ToRaw returns a *logWriter. (This is a little less efficient than using ToLogger's log.Logger as an io.Writer if that's all you need.)
func (l *NullLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@@ -0,0 +1,58 @@
package logging
import (
"unicode/utf8"
)
// Close conforms a nullWriter to an io.WriteCloser. It obviously does nothing, and will always return with err == nil.
func (nw *nullWriter) Close() (err error) {
// NO-OP
return
}
// Write conforms a nullWriter to an io.Writer, but it writes... nothing. To avoid errors, however, in downstream code it pretends it does (n will *always* == len(b)).
func (nw *nullWriter) Write(b []byte) (n int, err error) {
if b == nil {
return
}
n = len(b)
return
}
// WriteByte conforms to an io.ByteWriter but again... nothing is actually written anywhere.
func (nw *nullWriter) WriteByte(c byte) (err error) {
// NO-OP
_ = c
return
}
/*
WriteRune conforms to the other Loggers. It WILL return the proper value for n (matching (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune() signatures,
and it WILL return an ErrInvalidRune if r is not a valid rune, but otherwise it will no-op.
*/
func (nw *nullWriter) WriteRune(r rune) (n int, err error) {
n = utf8.RuneLen(r)
if n < 0 {
err = ErrInvalidRune
n = 0
return
}
return
}
// WriteString conforms to an io.StringWriter but nothing is actually written. (n will *always* == len(s))
func (nw *nullWriter) WriteString(s string) (n int, err error) {
n = len(s)
return
}

138
logging/funcs_oldnix.go Normal file
View File

@@ -0,0 +1,138 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own.
package logging
import (
native "log"
"os"
"path"
"r00t2.io/goutils/bitmask"
"r00t2.io/sysutils/paths"
)
var (
_ = 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 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(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
}

View File

@@ -2,26 +2,66 @@ package logging
import ( import (
"fmt" "fmt"
"io"
"log" "log"
"os"
"strings"
) )
// Setup sets up/configures a StdLogger and prepares it for use. /*
func (l *StdLogger) Setup() { Setup sets up/configures a StdLogger and prepares it for use.
err will always be nil; it's there for interface-compat.
*/
func (l *StdLogger) Setup() (err error) {
l.Logger = log.Default() var multi io.Writer
l.Logger.SetPrefix(l.Prefix + " ")
// This uses a shared handle across the import. We don't want that.
// l.Logger = log.Default()
if l.Prefix != "" {
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
// l.Logger.SetPrefix(l.Prefix)
}
// (stdlib).log.std is returned by log.Default(), which uses os.Stderr 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
} }
// Shutdown cleanly shuts down a StdLogger. /*
func (l *StdLogger) Shutdown() { Shutdown cleanly shuts down a StdLogger.
err will always be nil; it's there for interface-compat.
*/
func (l *StdLogger) Shutdown() (err error) {
// NOOP // NOOP
_ = ""
return
} }
// GetPrefix returns the prefix used by this StdLogger. /*
func (l *StdLogger) GetPrefix() (prefix string) { GetPrefix returns the prefix used by this StdLogger.
err will always be nil; it's there for interface-compat.
*/
func (l *StdLogger) GetPrefix() (prefix string, err error) {
prefix = l.Prefix prefix = l.Prefix
@@ -29,17 +69,38 @@ func (l *StdLogger) GetPrefix() (prefix string) {
} }
/* /*
DoDebug sets the debug state of this StdLogger. 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. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
err will always be nil; it's there for interface-compat.
*/ */
func (l *StdLogger) DoDebug(d bool) { func (l *StdLogger) DoDebug(d bool) (err error) {
l.EnableDebug = d l.EnableDebug = d
return
} }
// SetPrefix sets the prefix for this StdLogger. // GetDebug returns the debug status of this StdLogger.
func (l *StdLogger) SetPrefix(prefix string) { func (l *StdLogger) GetDebug() (d bool) {
l.Prefix = prefix + " "
l.Logger.SetPrefix(prefix + " ") 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
if prefix != "" {
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
}
l.Logger.SetPrefix(l.Prefix)
return
} }
// Alert writes an ALERT-level message to this StdLogger. // Alert writes an ALERT-level message to this StdLogger.
@@ -174,6 +235,22 @@ func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *StdLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *StdLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}
// renderWrite prepares/formats a log message to be written to this StdLogger. // renderWrite prepares/formats a log message to be written to this StdLogger.
func (l *StdLogger) renderWrite(msg, prio string) { func (l *StdLogger) renderWrite(msg, prio string) {

View File

@@ -4,27 +4,36 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/coreos/go-systemd/journal" "github.com/coreos/go-systemd/v22/journal"
) )
// Setup sets up/configures a SystemDLogger and prepares it for use. /*
func (l *SystemDLogger) Setup() { Setup sets up/configures a SystemDLogger and prepares it for use.
err will always be nil; it's there for interface-compat.
*/
func (l *SystemDLogger) Setup() (err error) {
// NOOP // NOOP
_ = ""
return
} }
// Shutdown cleanly shuts down a SystemDLogger. /*
func (l *SystemDLogger) Shutdown() { Shutdown cleanly shuts down a SystemDLogger.
err will always be nil; it's there for interface-compat.
*/
func (l *SystemDLogger) Shutdown() (err error) {
// NOOP // NOOP
_ = ""
return
} }
// GetPrefix returns the prefix used by this SystemDLogger. /*
func (l *SystemDLogger) GetPrefix() (prefix string) { GetPrefix returns the prefix used by this SystemDLogger.
err will always be nil; it's there for interface-compat.
*/
func (l *SystemDLogger) GetPrefix() (prefix string, err error) {
prefix = l.Prefix prefix = l.Prefix
@@ -32,16 +41,34 @@ func (l *SystemDLogger) GetPrefix() (prefix string) {
} }
/* /*
DoDebug sets the debug state of this SystemDLogger. 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. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
err will always be nil; it's there for interface-compat.
*/ */
func (l *SystemDLogger) DoDebug(d bool) { func (l *SystemDLogger) DoDebug(d bool) (err error) {
l.EnableDebug = d l.EnableDebug = d
return
} }
// SetPrefix sets the prefix for this SystemDLogger. // GetDebug returns the debug status of this SystemDLogger.
func (l *SystemDLogger) SetPrefix(prefix string) { 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 l.Prefix = prefix
return
} }
// Alert writes an ALERT-level message to this SystemDLogger. // Alert writes an ALERT-level message to this SystemDLogger.
@@ -196,3 +223,19 @@ func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *SystemDLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *SystemDLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@@ -1,58 +1,85 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging package logging
import ( import (
"fmt" "fmt"
"log" "log"
"log/syslog" "log/syslog"
"r00t2.io/goutils/multierr"
) )
// Setup sets up/configures a SyslogLogger and prepares it for use. // Setup sets up/configures a SyslogLogger and prepares it for use.
func (l *SyslogLogger) Setup() { func (l *SyslogLogger) Setup() (err error) {
var err error var errs *multierr.MultiError = multierr.NewMultiError(nil)
if l.alert, err = syslog.New(syslog.LOG_ALERT|syslogFacility, l.Prefix); err != nil { 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 { 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 { 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 { 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 { 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 { 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 { 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 { if l.warning, err = syslog.New(syslog.LOG_WARNING|syslogFacility, l.Prefix); err != nil {
log.Panicln("could not open log for Warning") errs.AddError(err)
err = nil
} }
if errs.Count() > 0 {
err = errs
}
return
} }
// Shutdown cleanly shuts down a SyslogLogger. // Shutdown cleanly shuts down a SyslogLogger.
func (l *SyslogLogger) Shutdown() { func (l *SyslogLogger) Shutdown() (err error) {
var err error var errs *multierr.MultiError = multierr.NewMultiError(nil)
for _, i := range []*syslog.Writer{l.alert, l.crit, l.debug, l.emerg, l.err, l.info, l.notice, l.warning} { 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 { if err = i.Close(); err != nil {
log.Panicf("could not close log %#v\n", i) errs.AddError(err)
err = nil
} }
} }
if errs.Count() > 0 {
err = errs
}
return
} }
// GetPrefix returns the prefix used by this SyslogLogger. /*
func (l *SyslogLogger) GetPrefix() (prefix string) { GetPrefix returns the prefix used by this SyslogLogger.
err will always be nil; it's there for interface-compat.
*/
func (l *SyslogLogger) GetPrefix() (prefix string, err error) {
prefix = l.Prefix prefix = l.Prefix
@@ -60,17 +87,40 @@ func (l *SyslogLogger) GetPrefix() (prefix string) {
} }
/* /*
DoDebug sets the debug state of this SyslogLogger. 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. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
err will always be nil; it's there for interface-compat.
*/ */
func (l *SyslogLogger) DoDebug(d bool) { func (l *SyslogLogger) DoDebug(d bool) (err error) {
l.EnableDebug = d l.EnableDebug = d
return
}
// 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. // SetPrefix sets the prefix for this SyslogLogger.
func (l *SyslogLogger) SetPrefix(prefix string) { func (l *SyslogLogger) SetPrefix(prefix string) (err error) {
l.Prefix = prefix l.Prefix = prefix
l.Setup()
// We need to close the current loggers first.
if err = l.Shutdown(); err != nil {
return
}
if err = l.Setup(); err != nil {
return
}
return
} }
// Alert writes an ALERT-level message to this SyslogLogger. // Alert writes an ALERT-level message to this SyslogLogger.
@@ -219,3 +269,19 @@ func (l *SyslogLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *SyslogLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *SyslogLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

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

View File

@@ -22,6 +22,9 @@ import (
A pointer to a WinEventID struct may be specified for eventIDs to map extended logging levels (as Windows only supports three levels natively). 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. 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, 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). it will be used (assuming you have no higher-level loggers available).
@@ -32,13 +35,14 @@ import (
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type), If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
use GetMultiLogger. use GetMultiLogger.
*/ */
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths ...string) (logger Logger, err error) { func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
var logPath string var logPath string
var logFlags bitmask.MaskBit var logFlags bitmask.MaskBit
var exists bool var exists bool
var success bool var success bool
var ckLogPaths []string var ckLogPaths []string
var prefix string
if strings.TrimSpace(source) == "" { if strings.TrimSpace(source) == "" {
err = errors.New("invalid source for Windows logging") err = errors.New("invalid source for Windows logging")
@@ -86,7 +90,7 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths .
logger = &WinLogger{ logger = &WinLogger{
Prefix: source, Prefix: source,
EnableDebug: enableDebug, EnableDebug: enableDebug,
eids: eventIDs, EIDs: eventIDs,
} }
} else { } else {
if logFlags.HasFlag(LogFile) { if logFlags.HasFlag(LogFile) {
@@ -94,6 +98,7 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths .
StdLogger: StdLogger{ StdLogger: StdLogger{
Prefix: source, Prefix: source,
EnableDebug: enableDebug, EnableDebug: enableDebug,
LogFlags: logConfigFlags,
}, },
Path: logPath, Path: logPath,
} }
@@ -101,13 +106,25 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths .
logger = &StdLogger{ logger = &StdLogger{
Prefix: source, Prefix: source,
EnableDebug: enableDebug, EnableDebug: enableDebug,
LogFlags: logConfigFlags,
} }
} }
} }
logger.Setup() if err = logger.Setup(); err != nil {
return
}
if source != "\x00" {
if err = logger.SetPrefix(source); err != nil {
return
}
}
logger.Info("logger initialized of type %T with source %v", logger, logger.GetPrefix()) if prefix, err = logger.GetPrefix(); err != nil {
return
}
logger.Debug("logger initialized of type %T with source %v", logger, prefix)
return 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

@@ -1,70 +1,138 @@
package logging package logging
import ( import (
`errors` "errors"
`fmt` "fmt"
"log"
"os"
"os/exec"
"syscall"
`golang.org/x/sys/windows/svc/eventlog` "golang.org/x/sys/windows/registry"
"golang.org/x/sys/windows/svc/eventlog"
"r00t2.io/sysutils/paths"
) )
// Setup sets up/configures a WinLogger and prepares it for use. /*
func (l *WinLogger) Setup() { Setup sets up/configures a WinLogger and prepares it for use.
This will fail with an Access Denied (the first time, at least) unless running with elevated permissions unless WinLogger.Prefix is
a registered Event Log source.
var err error If a failure occurs while trying to open the log with the given WinLogger.Prefix ("source"), a new Event Log source will be registered.
If WinLogger.Executable is not empty at the time of calling WinLogger.Setup (or WinLogger.ForceService is true),
eventlog.Install will be used (with the WinLogger.ExpandKey field).
Otherwise eventlog.InstallAsEventCreate will be used.
*/
func (l *WinLogger) Setup() (err error) {
/* /*
First a sanity check on the EventIDs. A sanity check on the EventIDs.
Since we use eventcreate, all Event IDs must be 1 <= eid <= 1000. Since we use eventcreate, all Event IDs must be 1 <= eid <= 1000.
*/ */
for _, eid := range []uint32{ for _, eid := range []uint32{
l.eids.Alert, l.EIDs.Alert,
l.eids.Crit, l.EIDs.Crit,
l.eids.Debug, l.EIDs.Debug,
l.eids.Emerg, l.EIDs.Emerg,
l.eids.Err, l.EIDs.Err,
l.eids.Info, l.EIDs.Info,
l.eids.Notice, l.EIDs.Notice,
l.eids.Warning, l.EIDs.Warning,
} { } {
if !((eid <= 1000) && (1 <= eid)) { if !((eid <= EIDMax) && (EIDMin <= eid)) {
err = errors.New("event IDs must be between 1 and 1000 inclusive") err = ErrBadEid
panic(err) return
} }
} }
if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { if err = l.Install(); err != nil {
if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil { return
// 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 { if l.elog, err = eventlog.Open(l.Prefix); err != nil {
panic(err) return
} }
return
}
// Install installs/registers the WinLogger Event Log interface. You most likely do not need to run this directly.
func (l *WinLogger) Install() (err error) {
var exists bool
var doNotCreate bool
var useEventCreate bool = true
if doNotCreate, err = l.Exists(); err != nil {
return
} else if !doNotCreate {
if l.Executable != "" {
if l.Executable, err = exec.LookPath(l.Executable); err != nil {
return
}
useEventCreate = false
} else if l.ForceService {
if l.Executable, err = exec.LookPath(os.Args[0]); err != nil {
return
}
useEventCreate = false
}
if !useEventCreate {
if exists, err = paths.RealPathExists(&l.Executable); err != nil {
return
} else if !exists {
err = ErrBadBinPath
return
}
if err = eventlog.Install(l.Prefix, l.Executable, l.ExpandKey, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
return
}
} else {
if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
return
}
}
}
return
} }
// Shutdown cleanly shuts down a WinLogger. // Remove uninstalls a registered WinLogger source.
func (l *WinLogger) Shutdown() { func (l *WinLogger) Remove() (err error) {
var err error
if err = l.elog.Close(); err != nil {
panic(err)
}
if err = eventlog.Remove(l.Prefix); err != nil { if err = eventlog.Remove(l.Prefix); err != nil {
panic(err) return
} }
return
} }
// GetPrefix returns the prefix used by this WinLogger. /*
func (l *WinLogger) GetPrefix() (prefix string) { Shutdown cleanly shuts down a WinLogger but keep the source registered. Use WinLogger.Remove
(or set WinLogger.RemoveOnClose to true before calling WinLogger.Shutdown) to remove the registered source.
*/
func (l *WinLogger) Shutdown() (err error) {
if err = l.elog.Close(); err != nil {
// TODO: check for no access or file not exists syscall errors?
return
}
if l.RemoveOnClose {
if err = l.Remove(); err != nil {
return
}
}
return
}
/*
GetPrefix returns the prefix used by this WinLogger.
err will always be nil; it's there for interface-compat.
*/
func (l *WinLogger) GetPrefix() (prefix string, err error) {
prefix = l.Prefix prefix = l.Prefix
@@ -72,45 +140,65 @@ func (l *WinLogger) GetPrefix() (prefix string) {
} }
/* /*
DoDebug sets the debug state of this WinLogger. 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. Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
err will always be nil; it's there for interface-compat.
*/ */
func (l *WinLogger) DoDebug(d bool) { func (l *WinLogger) DoDebug(d bool) (err error) {
l.EnableDebug = d 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. // SetPrefix sets the prefix for this WinLogger.
func (l *WinLogger) SetPrefix(prefix string) { func (l *WinLogger) SetPrefix(prefix string) (err error) {
var err error // To properly change the prefix, we need to tear down the old event log and create a new one.
if err = l.Shutdown(); err != nil {
return
}
l.Prefix = prefix 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.Setup(); err != nil {
if err = l.elog.Close(); err != nil { return
panic(err)
} }
if err = eventlog.Remove(l.Prefix); err != nil { return
panic(err) }
}
if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { // Exists indicates if the WinLogger.Prefix is a registered source or not.
if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil { func (l *WinLogger) Exists() (e bool, err error) {
// It's an error we want to panic on.
panic(err) var regKey registry.Key
} else { var subKey registry.Key
// It already exists, so ignore the error.
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 err = nil
} }
return
} }
defer subKey.Close()
if l.elog, err = eventlog.Open(l.Prefix); err != nil { e = true
panic(err)
}
return
} }
// Alert writes an ALERT-level message to this WinLogger. // Alert writes an ALERT-level message to this WinLogger.
@@ -125,7 +213,7 @@ func (l *WinLogger) Alert(s string, v ...interface{}) (err error) {
} }
// Treat ALERT as Warning // Treat ALERT as Warning
err = l.elog.Warning(l.eids.Alert, msg) err = l.elog.Warning(l.EIDs.Alert, msg)
return return
} }
@@ -142,7 +230,7 @@ func (l *WinLogger) Crit(s string, v ...interface{}) (err error) {
} }
// Treat CRIT as Error // Treat CRIT as Error
err = l.elog.Error(l.eids.Crit, msg) err = l.elog.Error(l.EIDs.Crit, msg)
return return
} }
@@ -163,7 +251,7 @@ func (l *WinLogger) Debug(s string, v ...interface{}) (err error) {
} }
// Treat DEBUG as Info // Treat DEBUG as Info
err = l.elog.Info(l.eids.Debug, msg) err = l.elog.Info(l.EIDs.Debug, msg)
return return
@@ -181,7 +269,7 @@ func (l *WinLogger) Emerg(s string, v ...interface{}) (err error) {
} }
// Treat EMERG as Error // Treat EMERG as Error
err = l.elog.Error(l.eids.Emerg, msg) err = l.elog.Error(l.EIDs.Emerg, msg)
return return
@@ -198,7 +286,7 @@ func (l *WinLogger) Err(s string, v ...interface{}) (err error) {
msg = s msg = s
} }
err = l.elog.Error(l.eids.Err, msg) err = l.elog.Error(l.EIDs.Err, msg)
return return
@@ -215,7 +303,7 @@ func (l *WinLogger) Info(s string, v ...interface{}) (err error) {
msg = s msg = s
} }
err = l.elog.Info(l.eids.Info, msg) err = l.elog.Info(l.EIDs.Info, msg)
return return
@@ -233,7 +321,7 @@ func (l *WinLogger) Notice(s string, v ...interface{}) (err error) {
} }
// Treat NOTICE as Info // Treat NOTICE as Info
err = l.elog.Info(l.eids.Notice, msg) err = l.elog.Info(l.EIDs.Notice, msg)
return return
@@ -250,8 +338,24 @@ func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {
msg = s msg = s
} }
err = l.elog.Warning(l.eids.Warning, msg) err = l.elog.Warning(l.EIDs.Warning, msg)
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *WinLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *WinLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
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,11 +2,15 @@ package logging
import ( import (
"log" "log"
`os` "os"
"r00t2.io/goutils/bitmask"
) )
type logPrio bitmask.MaskBit
/* /*
Logger is one of the various loggers offered by this module. Logger is one of the various loggers offered by this module.
*/ */
type Logger interface { type Logger interface {
Alert(s string, v ...interface{}) (err error) Alert(s string, v ...interface{}) (err error)
@@ -17,16 +21,19 @@ type Logger interface {
Info(s string, v ...interface{}) (err error) Info(s string, v ...interface{}) (err error)
Notice(s string, v ...interface{}) (err error) Notice(s string, v ...interface{}) (err error)
Warning(s string, v ...interface{}) (err error) Warning(s string, v ...interface{}) (err error)
DoDebug(d bool) DoDebug(d bool) (err error)
SetPrefix(p string) GetDebug() (d bool)
GetPrefix() (p string) SetPrefix(p string) (err error)
Setup() GetPrefix() (p string, err error)
Shutdown() Setup() (err error)
Shutdown() (err error)
ToLogger(prio logPrio) (stdLibLog *log.Logger)
ToRaw(prio logPrio) (raw *logWriter)
} }
/* /*
StdLogger uses the log package in stdlib to perform all logging. The default is to write to STDOUT. 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. If you wish to modify the underling log.Logger object, you can access it directly via StdLogger.Logger.
*/ */
type StdLogger struct { type StdLogger struct {
// All log.Logger fields/methods are exposed. // All log.Logger fields/methods are exposed.
@@ -38,24 +45,57 @@ type StdLogger struct {
EnableDebug bool EnableDebug bool
// Prefix indicates the prefix for log entries; in shared logs, this helps differentiate the source. // Prefix indicates the prefix for log entries; in shared logs, this helps differentiate the source.
Prefix string 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. /*
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 { type FileLogger struct {
// StdLogger is used for the log formation and handling. See StdLogger for more details. // StdLogger is used for the log formation and handling. See StdLogger for more details.
StdLogger StdLogger
// Path is the path to the logfile. // Path is the path to the logfile.
Path string Path string
/*
EnableStdOut is true if the log will send to STDOUT as well as the file.
If false (default), it will only (silently) write to the log file.
You will need to run *FileLogger.Shutdown and then *FileLogger.Setup again if you wish to change this.
*/
EnableStdOut bool
// writer is used for the writing out of the log file. // writer is used for the writing out of the log file.
writer *os.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. // MultiLogger is used to contain one or more Loggers and present them all as a single Logger.
type MultiLogger struct { type MultiLogger struct {
/* /*
@@ -71,3 +111,12 @@ type MultiLogger struct {
*/ */
Loggers map[string]Logger Loggers map[string]Logger
} }
// logWriter is used as a log.Logger and is returned by <Logger>.ToLogger.
type logWriter struct {
backend Logger
prio logPrio
}
// nullWriter is used as a shortcut by NullLogger.ToLogger.
type nullWriter struct{}

View File

@@ -1,23 +1,9 @@
package logging 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 { type SystemDLogger struct {
EnableDebug bool EnableDebug bool
Prefix string Prefix string
} }
type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

22
logging/types_nix.go Normal file
View File

@@ -0,0 +1,22 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"log/syslog"
)
// 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
}

View File

@@ -4,13 +4,60 @@ import (
`golang.org/x/sys/windows/svc/eventlog` `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 { 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 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 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 elog *eventlog.Log
eids *WinEventID // 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 { type WinEventID struct {
Alert, Alert,
Crit, Crit,

View File

@@ -69,6 +69,9 @@ func (e *MultiError) Error() (errStr string) {
numErrs = len(e.Errors) numErrs = len(e.Errors)
} }
e.lock.Lock()
defer e.lock.Unlock()
for idx, err := range e.Errors { for idx, err := range e.Errors {
if (idx + 1) < numErrs { if (idx + 1) < numErrs {
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep) errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
@@ -87,6 +90,9 @@ func (e *MultiError) AddError(err error) {
return return
} }
e.lock.Lock()
defer e.lock.Unlock()
e.Errors = append(e.Errors, err) e.Errors = append(e.Errors, err)
} }

View File

@@ -1,9 +1,14 @@
package multierr package multierr
import (
`sync`
)
// MultiError is a type of error.Error that can contain multiple errors. // MultiError is a type of error.Error that can contain multiple errors.
type MultiError struct { type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called. // Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error `json:"errors"` Errors []error `json:"errors"`
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n". // ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string `json:"separator"` ErrorSep string `json:"separator"`
lock sync.Mutex
} }

4
remap/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package remap provides convenience functions around regular expressions, primarily offering maps for named capture groups.
*/
package remap

488
remap/funcs_remap.go Normal file
View File

@@ -0,0 +1,488 @@
package remap
/*
Map returns a map[string][]<match bytes> for regexes with named capture groups matched in bytes b.
Note that this supports non-unique group names; [regexp.Regexp] allows for patterns with multiple groups
using the same group name (though your IDE might complain; I know GoLand does).
Each match for each group is in a slice keyed under that group name, with that slice
ordered by the indexing done by the regex match itself.
In summary, the parameters are as follows:
# inclNoMatch
If true, then attempt to return a non-nil matches (as long as b isn't nil).
Group keys will be populated and explicitly defined as nil.
For example, if a pattern
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but b does not match then matches will be:
map[string][][]byte{
"g1": nil,
"g2": nil,
}
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of nil values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but b does not match then matches will be:
map[string][][]byte{
"g1": [][]byte{
nil,
nil,
},
"g2": [][]byte{
nil,
},
}
# mustMatch
If true, matches will be nil if the entirety of b does not match the pattern (and thus
no capture groups matched) (overrides inclNoMatch) -- explicitly:
matches == nil
Otherwise if false (and assuming inclNoMatch is false), matches will be:
map[string][][]byte{}{}
# Condition Tree
In detail, matches and/or its values may be nil or empty under the following condition tree:
IF b is nil:
THEN matches will always be nil
ELSE:
IF all of b does not match pattern
IF mustMuch is true
THEN matches == nil
ELSE
THEN matches == map[string][][]byte{} (non-nil but empty)
ELSE IF pattern has no named capture groups
IF inclNoMatch is true
THEN matches == map[string][][]byte{} (non-nil but empty)
ELSE
THEN matches == nil
ELSE
IF there are no named group matches
IF inclNoMatch is true
THEN matches is non-nil; matches[<group name>, ...] is/are defined but nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches == nil
ELSE
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == [][]byte{nil[, nil...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches[<group name>] is not defined (_, ok = matches[<group name>]; ok == false)
ELSE
matches[<group name>] == []{<match>[, <match>...]}
*/
func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][][]byte) {
var ok bool
var mIdx int
var match []byte
var grpNm string
var names []string
var matchBytes [][]byte
var tmpMap map[string][][]byte = make(map[string][][]byte)
if b == nil {
return
}
names = r.Regexp.SubexpNames()
matchBytes = r.Regexp.FindSubmatch(b)
if matchBytes == nil {
// b does not match pattern
if !mustMatch {
matches = make(map[string][][]byte)
}
return
}
if names == nil || len(names) == 0 || len(names) == 1 {
/*
no named capture groups;
technically only the last condition would be the case.
*/
if inclNoMatch {
matches = make(map[string][][]byte)
}
return
}
names = names[1:]
if len(matchBytes) == 0 || len(matchBytes) == 1 {
/*
no submatches whatsoever.
*Technically* I don't think this condition can actually be reached.
This is more of a safe-return before we re-slice.
*/
matches = make(map[string][][]byte)
if inclNoMatch {
if len(names) >= 1 {
for _, grpNm = range names {
matches[grpNm] = nil
}
}
}
return
}
matchBytes = matchBytes[1:]
for mIdx, match = range matchBytes {
grpNm = names[mIdx]
/*
Thankfully, it's actually a build error if a pattern specifies a named
capture group with an empty name.
So we don't need to worry about accounting for that,
and can just skip over grpNm == "" (which is an *unnamed* capture group).
*/
if grpNm == "" {
continue
}
if match == nil {
// group did not match
if !inclNoMatch {
continue
}
if _, ok = tmpMap[grpNm]; !ok {
if !inclNoMatchStrict {
tmpMap[grpNm] = nil
} else {
tmpMap[grpNm] = [][]byte{nil}
}
} else {
if inclNoMatchStrict {
tmpMap[grpNm] = append(tmpMap[grpNm], nil)
}
}
continue
}
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = make([][]byte, 0)
}
tmpMap[grpNm] = append(tmpMap[grpNm], match)
}
// This *technically* should be completely handled above.
if inclNoMatch {
for _, grpNm = range names {
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = nil
}
}
}
if len(tmpMap) > 0 {
matches = tmpMap
}
return
}
/*
MapString is exactly like ReMap.Map(), but operates on (and returns) strings instead.
(matches will always be nil if s == “.)
A small deviation, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
This unfortunately *does not provide any indication* if an empty string positively matched the pattern (a "hit") or if it was simply
not matched at all (a "miss"). If you need definitive determination between the two conditions, it is instead recommended to either
*not* use inclNoMatchStrict or to use ReMap.Map() instead and convert any non-nil values to strings after.
Particularly:
# inclNoMatch
If true, then attempt to return a non-nil matches (as long as s isn't empty).
Group keys will be populated and explicitly defined as nil.
For example, if a pattern
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but s does not match then matches will be:
map[string][]string{
"g1": nil,
"g2": nil,
}
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of eempty string values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but s does not match then matches will be:
map[string][]string{
"g1": []string{
"",
"",
},
"g2": []string{
"",
},
}
# mustMatch
If true, matches will be nil if the entirety of s does not match the pattern (and thus
no capture groups matched) (overrides inclNoMatch) -- explicitly:
matches == nil
Otherwise if false (and assuming inclNoMatch is false), matches will be:
map[string][]string{}{}
# Condition Tree
In detail, matches and/or its values may be nil or empty under the following condition tree:
IF s is empty:
THEN matches will always be nil
ELSE:
IF all of s does not match pattern
IF mustMuch is true
THEN matches == nil
ELSE
THEN matches == map[string][]string{} (non-nil but empty)
ELSE IF pattern has no named capture groups
IF inclNoMatch is true
THEN matches == map[string][]string{} (non-nil but empty)
ELSE
THEN matches == nil
ELSE
IF there are no named group matches
IF inclNoMatch is true
THEN matches is non-nil; matches[<group name>, ...] is/are defined but nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches == nil
ELSE
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == []string{""[, ""...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches[<group name>] is not defined (_, ok = matches[<group name>]; ok == false)
ELSE
matches[<group name>] == []{<match>[, <match>...]}
*/
func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][]string) {
var ok bool
var endIdx int
var startIdx int
var chunkIdx int
var grpNm string
var names []string
var matchStr string
/*
A slice of indices or index pairs.
For each element `e` in idxChunks,
* if `e` is nil, no group match.
* if len(e) == 1, only a single character was matched.
* otherwise len(e) == 2, the start and end of the match.
*/
var idxChunks [][]int
var matchIndices []int
var chunkIndices []int // always 2 elements; start pos and end pos
var tmpMap map[string][]string = make(map[string][]string)
/*
OK so this is a bit of a deviation.
It's not as straightforward as above, because there isn't an explicit way
like above to determine if a pattern was *matched as an empty string* vs.
*not matched*.
So instead do roundabout index-y things.
*/
if s == "" {
return
}
/*
I'm not entirely sure how serious they are about "the slice should not be modified"...
DO NOT sort or dedupe `names`! If the same name for groups is duplicated,
it will be duplicated here in proper order and the ordering is tied to
the ordering of matchIndices.
*/
names = r.Regexp.SubexpNames()[:]
matchIndices = r.Regexp.FindStringSubmatchIndex(s)
if matchIndices == nil {
// s does not match pattern at all.
if !mustMatch {
matches = make(map[string][]string)
}
return
}
if names == nil || len(names) <= 1 {
/*
No named capture groups;
technically only the last condition would be the case,
as (regexp.Regexp).SubexpNames() will ALWAYS at the LEAST
return a `[]string{""}`.
*/
if inclNoMatch {
matches = make(map[string][]string)
}
return
}
if len(matchIndices) == 0 || len(matchIndices) == 1 {
/*
No (sub)matches whatsoever.
*technically* I don't think this condition can actually be reached;
matchIndices should ALWAYS either be `nil` or len will be at LEAST 2,
and modulo 2 thereafter since they're PAIRS of indices...
Why they didn't just return a [][]int or [][2]int or something
instead of an []int, who knows.
But we're correcting that poor design.
This is more of a safe-return before we chunk the indices.
*/
matches = make(map[string][]string)
if inclNoMatch {
for _, grpNm = range names {
if grpNm != "" {
matches[grpNm] = nil
}
}
}
return
}
/*
A reslice of `matchIndices` could technically start at 2 (as long as `names` is sliced [1:])
because they're in pairs: []int{<start>, <end>, <start>, <end>, ...}
and the first pair is the entire pattern match (un-resliced names[0]).
Thus the len(matchIndices) == 2*len(names), *even* if you
Keep in mind that since the first element of names is removed,
the first pair here is skipped.
This provides a bit more consistent readability, though.
*/
idxChunks = make([][]int, len(names))
chunkIdx = 0
endIdx = 0
for startIdx = 0; endIdx < len(matchIndices); startIdx += 2 {
endIdx = startIdx + 2
// This technically should never happen.
if endIdx > len(matchIndices) {
endIdx = len(matchIndices)
}
chunkIndices = matchIndices[startIdx:endIdx]
if chunkIndices[0] == -1 || chunkIndices[1] == -1 {
// group did not match
chunkIndices = nil
} else {
if chunkIndices[0] == chunkIndices[1] {
chunkIndices = []int{chunkIndices[0]}
} else {
chunkIndices = matchIndices[startIdx:endIdx]
}
}
idxChunks[chunkIdx] = chunkIndices
chunkIdx++
}
// Now associate with names and pull the string sequence.
for chunkIdx, chunkIndices = range idxChunks {
grpNm = names[chunkIdx]
/*
Thankfully, it's actually a build error if a pattern specifies a named
capture group with an empty name.
So we don't need to worry about accounting for that,
and can just skip over grpNm == ""
(which is either an *unnamed* capture group
OR the first element in `names`, which is always
the entire match).
*/
if grpNm == "" {
continue
}
if chunkIndices == nil || len(chunkIndices) == 0 {
// group did not match
if !inclNoMatch {
continue
}
if _, ok = tmpMap[grpNm]; !ok {
if !inclNoMatchStrict {
tmpMap[grpNm] = nil
} else {
tmpMap[grpNm] = []string{""}
}
} else {
if inclNoMatchStrict {
tmpMap[grpNm] = append(tmpMap[grpNm], "")
}
}
continue
}
switch len(chunkIndices) {
case 1:
// Single character
matchStr = string(s[chunkIndices[0]])
case 2:
// Multiple characters
matchStr = s[chunkIndices[0]:chunkIndices[1]]
}
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = make([]string, 0)
}
tmpMap[grpNm] = append(tmpMap[grpNm], matchStr)
}
// This *technically* should be completely handled above.
if inclNoMatch {
for _, grpNm = range names {
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = nil
}
}
}
if len(tmpMap) > 0 {
matches = tmpMap
}
return
}

27
remap/types.go Normal file
View File

@@ -0,0 +1,27 @@
package remap
import (
"regexp"
)
type (
// ReMap provides some map-related functions around a regexp.Regexp.
ReMap struct {
*regexp.Regexp
}
// TODO?
/*
ExplicitStringMatch is used with ReMap.MapStringExplicit to indicate if a
capture group result is a hit (a group matched, but e.g. the match value is empty string)
or not (a group did not match).
*/
/*
ExplicitStringMatch struct {
Group string
IsMatch bool
Value string
}
*/
)

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
)