11package logging
22
33import (
4+ "fmt"
45 "github.com/icinga/icinga-go-library/strcase"
6+ "github.com/icinga/icinga-go-library/utils"
57 "github.com/pkg/errors"
68 "github.com/ssgreg/journald"
79 "go.uber.org/zap/zapcore"
10+ "strings"
811)
912
10- // priorities maps zapcore.Level to journal.Priority.
11- var priorities = map [zapcore.Level ]journald.Priority {
13+ // journaldPriorities maps zapcore.Level to journal.Priority.
14+ var journaldPriorities = map [zapcore.Level ]journald.Priority {
1215 zapcore .DebugLevel : journald .PriorityDebug ,
1316 zapcore .InfoLevel : journald .PriorityInfo ,
1417 zapcore .WarnLevel : journald .PriorityWarning ,
@@ -18,6 +21,11 @@ var priorities = map[zapcore.Level]journald.Priority{
1821 zapcore .DPanicLevel : journald .PriorityCrit ,
1922}
2023
24+ // journaldVisibleFields is a set (map to struct{}) of field keys being logged within the message for journald.
25+ var journaldVisibleFields = map [string ]struct {}{
26+ "error" : {},
27+ }
28+
2129// NewJournaldCore returns a zapcore.Core that sends log entries to systemd-journald and
2230// uses the given identifier as a prefix for structured logging context that is sent as journal fields.
2331func NewJournaldCore (identifier string , enab zapcore.LevelEnabler ) zapcore.Core {
@@ -53,30 +61,35 @@ func (c *journaldCore) With(fields []zapcore.Field) zapcore.Core {
5361}
5462
5563func (c * journaldCore ) Write (ent zapcore.Entry , fields []zapcore.Field ) error {
56- pri , ok := priorities [ent .Level ]
64+ pri , ok := journaldPriorities [ent .Level ]
5765 if ! ok {
5866 return errors .Errorf ("unknown log level %q" , ent .Level )
5967 }
6068
6169 enc := zapcore .NewMapObjectEncoder ()
62- // Ensure that all field keys are valid journald field keys. If in doubt, use encodeJournaldFieldKey.
6370 c .addFields (enc , fields )
6471 c .addFields (enc , c .context )
6572 enc .Fields ["SYSLOG_IDENTIFIER" ] = c .identifier
6673
67- message := ent .Message
74+ // Re-encode keys before passing them to journald. Unfortunately, this cannot be done within addFields or at another
75+ // earlier position since zapcore's Field.AddTo may create multiple entries, some with non-compliant names.
76+ encFields := make (map [string ]interface {})
77+ for k , v := range enc .Fields {
78+ encFields [encodeJournaldFieldKey (k )] = v
79+ }
80+
81+ message := ent .Message + visibleFieldsMsg (journaldVisibleFields , append (fields , c .context ... ))
6882 if ent .LoggerName != c .identifier {
6983 message = ent .LoggerName + ": " + message
7084 }
7185
72- return journald .Send (message , pri , enc . Fields )
86+ return journald .Send (message , pri , encFields )
7387}
7488
75- // addFields adds all given fields to enc with an altered key, prefixed with the journaldCore.identifier and sanitized
76- // via encodeJournaldFieldKey.
89+ // addFields adds all given fields to enc with an altered key, prefixed with the journaldCore.identifier.
7790func (c * journaldCore ) addFields (enc zapcore.ObjectEncoder , fields []zapcore.Field ) {
7891 for _ , field := range fields {
79- field .Key = encodeJournaldFieldKey ( c .identifier + "_" + field .Key )
92+ field .Key = c .identifier + "_" + field .Key
8093 field .AddTo (enc )
8194 }
8295}
@@ -124,3 +137,56 @@ func encodeJournaldFieldKey(key string) string {
124137
125138 return key
126139}
140+
141+ // visibleFieldsMsg creates a string to be appended to the log message including fields to be explicitly printed.
142+ //
143+ // When logging against journald, the zapcore.Fields are used as journald fields, resulting in not being shown in the
144+ // default journalctl output (short). While this is documented in our docs, missing error messages are usually confusing
145+ // for end users.
146+ //
147+ // This method takes an allow list (set, map of keys to empty struct) of key to be displayed - there is the global
148+ // variable journaldVisibleFields; parameter for testing - and a slice of zapcore.Fields, creating an output string of
149+ // the allowed fields prefixed by a whitespace separator. If there are no fields to be logged, the returned string is
150+ // empty. So the function output can be appended to the output message without further checks.
151+ func visibleFieldsMsg (visibleFieldKeys map [string ]struct {}, fields []zapcore.Field ) string {
152+ if visibleFieldKeys == nil || fields == nil {
153+ return ""
154+ }
155+
156+ enc := zapcore .NewMapObjectEncoder ()
157+
158+ for _ , field := range fields {
159+ if _ , shouldLog := visibleFieldKeys [field .Key ]; shouldLog {
160+ field .AddTo (enc )
161+ }
162+ }
163+
164+ // The internal zapcore.encodeError function[^0] can result in multiple fields. For example, an error type
165+ // implementing fmt.Formatter results in another "errorVerbose" field, containing the stack trace if the error was
166+ // created by github.com/pkg/errors including a stack[^1]. So the keys are checked again in the following loop.
167+ //
168+ // [^0]: https://github.com/uber-go/zap/blob/v1.27.0/zapcore/error.go#L47
169+ // [^1]: https://pkg.go.dev/github.com/pkg/[email protected] #WithStack 170+ visibleFields := make ([]string , 0 , len (visibleFieldKeys ))
171+ for k , v := range utils .IterateOrderedMap (enc .Fields ) {
172+ if _ , shouldLog := visibleFieldKeys [k ]; ! shouldLog {
173+ continue
174+ }
175+
176+ var encodedField string
177+ switch v .(type ) {
178+ case string , []byte , error :
179+ encodedField = fmt .Sprintf ("%s=%q" , k , v )
180+ default :
181+ encodedField = fmt .Sprintf (`%s="%v"` , k , v )
182+ }
183+
184+ visibleFields = append (visibleFields , encodedField )
185+ }
186+
187+ if len (visibleFields ) == 0 {
188+ return ""
189+ }
190+
191+ return "\t " + strings .Join (visibleFields , ", " )
192+ }
0 commit comments