journal/journslog/key_map.go

98 lines
2.4 KiB
Go
Raw Normal View History

2026-01-18 13:47:50 +00:00
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 az chars into uppercase AZ (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
}