98 lines
2.4 KiB
Go
98 lines
2.4 KiB
Go
package journslog
|
||
|
||
import (
|
||
_ "log/slog"
|
||
"strings"
|
||
"sync"
|
||
|
||
"src.lwithers.me.uk/go/journal"
|
||
)
|
||
|
||
// KeyMapper maps [slog.Attr] keys onto [journal.AttrKey] objects. Such a mapper
|
||
// is necessary because systemd-journald is very restrictive about suitable
|
||
// attribute keys.
|
||
type KeyMapper interface {
|
||
KeyMap(slogAttrKey string) journal.AttrKey
|
||
}
|
||
|
||
// DefaultKeyMapper returns a [KeyMapper] with the package's default behaviour.
|
||
// The returned mapper contains an internal, persistent map and a [sync.RWMutex]
|
||
// to cache map results.
|
||
//
|
||
// The default behaviour is to strip leading underscores; to turn lowercase
|
||
// ASCII ‘a’–‘z’ chars into uppercase ‘A’–‘Z’ (uppercase is passed through
|
||
// verbatim), to accept underscores, and to map all other chars to underscores.
|
||
// If the result is an empty string, it instead returns the key "BAD_ATTR_KEY".
|
||
func DefaultKeyMapper() KeyMapper {
|
||
return &defaultKeyMapper{
|
||
badKey: journal.MustAttrKey("BAD_ATTR_KEY"),
|
||
keys: make(map[string]journal.AttrKey),
|
||
}
|
||
}
|
||
|
||
type defaultKeyMapper struct {
|
||
badKey journal.AttrKey
|
||
keys map[string]journal.AttrKey
|
||
lock sync.RWMutex
|
||
}
|
||
|
||
func (dkm *defaultKeyMapper) KeyMap(slogAttrKey string) journal.AttrKey {
|
||
if key, ok := dkm.mapFast(slogAttrKey); ok {
|
||
return key
|
||
}
|
||
return dkm.mapSlow(slogAttrKey)
|
||
}
|
||
|
||
func (dkm *defaultKeyMapper) mapFast(slogAttrKey string) (journal.AttrKey, bool) {
|
||
dkm.lock.RLock()
|
||
defer dkm.lock.RUnlock()
|
||
key, ok := dkm.keys[slogAttrKey]
|
||
return key, ok
|
||
}
|
||
|
||
func (dkm *defaultKeyMapper) mapSlow(slogAttrKey string) journal.AttrKey {
|
||
key := dkm.mapFunc(slogAttrKey)
|
||
|
||
// it's OK if another goroutine in the slow path just wrote the key, we
|
||
// will overwrite its result but the result will still be correct
|
||
dkm.lock.Lock()
|
||
defer dkm.lock.Unlock()
|
||
dkm.keys[slogAttrKey] = key
|
||
return key
|
||
}
|
||
|
||
func (dkm *defaultKeyMapper) mapFunc(slogAttrKey string) journal.AttrKey {
|
||
key, err := journal.NewAttrKey(slogAttrKey)
|
||
if err == nil {
|
||
return key
|
||
}
|
||
|
||
var s strings.Builder
|
||
for _, r := range slogAttrKey {
|
||
switch {
|
||
case r >= 'a' && r <= 'z':
|
||
s.WriteRune(r - 'a' + 'A')
|
||
case r >= '0' && r <= '9',
|
||
r >= 'A' && r <= 'Z':
|
||
s.WriteRune(r)
|
||
default:
|
||
if s.Len() > 0 {
|
||
s.WriteRune('_')
|
||
}
|
||
}
|
||
}
|
||
if s.Len() == 0 {
|
||
return dkm.badKey
|
||
}
|
||
|
||
skey := s.String()
|
||
if len(skey) > 255 {
|
||
skey = skey[:255] // known to only contain ASCII chars
|
||
}
|
||
|
||
if key, err = journal.NewAttrKey(skey); err != nil {
|
||
return dkm.badKey
|
||
}
|
||
return key
|
||
}
|