I am using my app as a SystemD service and need to prepend every message with an entry level <LEVEL>
for JournalD like:
<6> this is info
<7> this is debug
<4> this is warning
Otherwise, JournalD treats all the entries the same level and I want to use its advanced capabilities for displaying logs only of certain level.
How can I prepend every log entry with the correct level label (like for Info it would be <6>
) with uber-zap library?
EDIT: This is the relevant part of my logger configuration:
var config zap.Config
if production {
config = zap.NewProductionConfig()
config.Encoding = `console`
config.EncoderConfig.TimeKey = "" // no time as SystemD adds timestamp
} else {
config = zap.NewDevelopmentConfig()
}
config.DisableStacktrace = true
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // colors
config.OutputPaths = []string{"stdout"}
You can use a custom encoder that embeds a zapcore.Encoder
.
Embedding the encoder gives you the implementation of all methods "for free" with the same configuration you have now. Then you can implement only EncodeEntry
with the additional logic you require.
NOTE: You still have to implement Clone()
if you plan to use structured logging, e.g. logger.With()
. More info: Why custom encoding is lost after calling logger.With in Uber Zap?
Back to your main question, this is a working example; see the comments in code for additional explanation:
type prependEncoder struct {
// embed a zapcore encoder
// this makes prependEncoder implement the interface without extra work
zapcore.Encoder
// zap buffer pool
pool buffer.Pool
}
// implementing only EncodeEntry
func (e *prependEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// new log buffer
buf := e.pool.Get()
// prepend the JournalD prefix based on the entry level
buf.AppendString(e.toJournaldPrefix(entry.Level))
buf.AppendString(" ")
// calling the embedded encoder's EncodeEntry to keep the original encoding format
consolebuf, err := e.Encoder.EncodeEntry(entry, fields)
if err != nil {
return nil, err
}
// just write the output into your own buffer
_, err = buf.Write(consolebuf.Bytes())
if err != nil {
return nil, err
}
return buf, nil
}
// some mapper function
func (e *prependEncoder) toJournaldPrefix(lvl zapcore.Level) string {
switch lvl {
case zapcore.DebugLevel:
return "<7>"
case zapcore.InfoLevel:
return "<6>"
case zapcore.WarnLevel:
return "<4>"
}
return ""
}
Later you construct a logger with a custom core that uses the custom encoder. You initialize the embedded field with the same encoder you are using now. The options you see below mimic the options you currently have.
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
"os"
)
func getConfig() zap.Config {
// your current config options
return config
}
func main() {
cfg := getConfig()
// constructing our prependEncoder with a ConsoleEncoder using your original configs
enc := &prependEncoder{
Encoder: zapcore.NewConsoleEncoder(cfg.EncoderConfig),
pool: buffer.NewPool(),
}
logger := zap.New(
zapcore.NewCore(
enc,
os.Stdout,
zapcore.DebugLevel,
),
// this mimics the behavior of NewProductionConfig.Build
zap.ErrorOutput(os.Stderr),
)
logger.Info("this is info")
logger.Debug("this is debug")
logger.Warn("this is warn")
}
Test run output (INFO is printed in blue, DEBUG in pink and WARN in yellow as per your zapcore.CapitalColorLevelEncoder
):
<6> INFO this is info
<7> DEBUG this is debug
<4> WARN this is warn