55 "github.com/pkg/errors"
66 "github.com/ssgreg/journald"
77 "go.uber.org/zap/zapcore"
8- "strings"
98)
109
1110// priorities maps zapcore.Level to journal.Priority.
@@ -25,15 +24,13 @@ func NewJournaldCore(identifier string, enab zapcore.LevelEnabler) zapcore.Core
2524 return & journaldCore {
2625 LevelEnabler : enab ,
2726 identifier : identifier ,
28- identifierU : strings .ToUpper (identifier ),
2927 }
3028}
3129
3230type journaldCore struct {
3331 zapcore.LevelEnabler
34- context []zapcore.Field
35- identifier string
36- identifierU string
32+ context []zapcore.Field
33+ identifier string
3734}
3835
3936func (c * journaldCore ) Check (ent zapcore.Entry , ce * zapcore.CheckedEntry ) * zapcore.CheckedEntry {
@@ -62,6 +59,7 @@ func (c *journaldCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
6259 }
6360
6461 enc := zapcore .NewMapObjectEncoder ()
62+ // Ensure that all field keys are valid journald field keys. If in doubt, use encodeJournaldFieldKey.
6563 c .addFields (enc , fields )
6664 c .addFields (enc , c .context )
6765 enc .Fields ["SYSLOG_IDENTIFIER" ] = c .identifier
@@ -74,11 +72,55 @@ func (c *journaldCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
7472 return journald .Send (message , pri , enc .Fields )
7573}
7674
75+ // addFields adds all given fields to enc with an altered key, prefixed with the journaldCore.identifier and sanitized
76+ // via encodeJournaldFieldKey.
7777func (c * journaldCore ) addFields (enc zapcore.ObjectEncoder , fields []zapcore.Field ) {
7878 for _ , field := range fields {
79- field .Key = c .identifierU +
80- "_" +
81- strcase .ScreamingSnake (field .Key )
79+ field .Key = encodeJournaldFieldKey (c .identifier + "_" + field .Key )
8280 field .AddTo (enc )
8381 }
8482}
83+
84+ // encodeJournaldFieldKey alters a string to be used as a journald field key.
85+ //
86+ // When journald receives a field with an invalid key, it silently discards this field. This makes syntactically correct
87+ // keys a necessity. Unfortunately, there was no specific documentation about the field key syntax available. This
88+ // function follows the logic enforced in systemd's journal_field_valid function[0].
89+ //
90+ // This boils down to:
91+ // - Key length MUST be within (0, 64] characters.
92+ // - Key MUST start with [A-Z].
93+ // - Key characters MUST be [A-Z0-9_].
94+ //
95+ // [0]: https://github.com/systemd/systemd/blob/11d5e2b5fbf9f6bfa5763fd45b56829ad4f0777f/src/libsystemd/sd-journal/journal-file.c#L1703
96+ func encodeJournaldFieldKey (key string ) string {
97+ if len (key ) == 0 {
98+ // While this is definitely an error, panicking would be too destructive and silently dropping fields is against
99+ // the very idea of ensuring key conformity.
100+ return "EMPTY_KEY"
101+ }
102+
103+ isAsciiUpper := func (r rune ) bool { return 'A' <= r && r <= 'Z' }
104+ isAsciiDigit := func (r rune ) bool { return '0' <= r && r <= '9' }
105+
106+ keyParts := []rune (strcase .ScreamingSnake (key ))
107+ for i , r := range keyParts {
108+ if isAsciiUpper (r ) || isAsciiDigit (r ) || r == '_' {
109+ continue
110+ }
111+ keyParts [i ] = '_'
112+ }
113+ key = string (keyParts )
114+
115+ if ! isAsciiUpper (rune (key [0 ])) {
116+ // Escape invalid leading characters with a generic "ESC_" prefix. This was seen as a safer choice instead of
117+ // iterating over the key and removing parts.
118+ key = "ESC_" + key
119+ }
120+
121+ if len (key ) > 64 {
122+ key = key [:64 ]
123+ }
124+
125+ return key
126+ }
0 commit comments