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 }