From 57da509ee1873ad420b56633989c17bebc94e321 Mon Sep 17 00:00:00 2001 From: Ravi Raju Date: Thu, 26 Apr 2018 23:15:29 -0700 Subject: [PATCH] Add JournalD Writer (#57) JournalD writer decodes each log event and map fields to journald fields. The JSON payload is kept in the `JSON` field. --- encoder_cbor.go | 4 +- journald/journald.go | 112 ++++++++++++++++++++++++++++++++++++++ journald/journald_test.go | 44 +++++++++++++++ log.go | 22 ++++++++ 4 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 journald/journald.go create mode 100644 journald/journald_test.go diff --git a/encoder_cbor.go b/encoder_cbor.go index aa72013..d345c8f 100644 --- a/encoder_cbor.go +++ b/encoder_cbor.go @@ -185,8 +185,8 @@ func appendArrayDelim(dst []byte) []byte { } func appendObjectData(dst []byte, src []byte) []byte { - // Map begin character is present in the src, which - // should not be copied when appending to existing data. + // Map begin character is present in the src, which + // should not be copied when appending to existing data. return cbor.AppendObjectData(dst, src[1:]) } diff --git a/journald/journald.go b/journald/journald.go new file mode 100644 index 0000000..fecc99b --- /dev/null +++ b/journald/journald.go @@ -0,0 +1,112 @@ +// +build !windows + +package journald + +// This file provides a zerolog writer so that logs printed +// using zerolog library can be sent to a journalD. + +// Zerolog's Top level key/Value Pairs are translated to +// journald's args - all Values are sent to journald as strings. +// And all key strings are converted to uppercase before sending +// to journald (as required by journald). + +// In addition, entire log message (all Key Value Pairs), is also +// sent to journald under the key "JSON". + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/coreos/go-systemd/journal" + "github.com/rs/zerolog" + "github.com/rs/zerolog/internal/cbor" + "io" + "strings" +) + +const defaultJournalDPrio = journal.PriNotice + +// NewJournalDWriter returns a zerolog log destination +// to be used as parameter to New() calls. Writing logs +// to this writer will send the log messages to journalD +// running in this system. +func NewJournalDWriter() io.Writer { + return journalWriter{} +} + +type journalWriter struct { +} + +// levelToJPrio converts zerolog Level string into +// journalD's priority values. JournalD has more +// priorities than zerolog. +func levelToJPrio(zLevel string) journal.Priority { + lvl, _ := zerolog.ParseLevel(zLevel) + + switch lvl { + case zerolog.DebugLevel: + return journal.PriDebug + case zerolog.InfoLevel: + return journal.PriInfo + case zerolog.WarnLevel: + return journal.PriWarning + case zerolog.ErrorLevel: + return journal.PriErr + case zerolog.FatalLevel: + return journal.PriCrit + case zerolog.PanicLevel: + return journal.PriEmerg + case zerolog.NoLevel: + return journal.PriNotice + } + return defaultJournalDPrio +} + +func (w journalWriter) Write(p []byte) (n int, err error) { + if !journal.Enabled() { + err = fmt.Errorf("Cannot connect to journalD!!") + return + } + var event map[string]interface{} + p = cbor.DecodeIfBinaryToBytes(p) + d := json.NewDecoder(bytes.NewReader(p)) + d.UseNumber() + err = d.Decode(&event) + jPrio := defaultJournalDPrio + args := make(map[string]string, 0) + if err != nil { + return + } + if l, ok := event[zerolog.LevelFieldName].(string); ok { + jPrio = levelToJPrio(l) + } + + msg := "" + for key, value := range event { + jKey := strings.ToUpper(key) + switch key { + case zerolog.LevelFieldName, zerolog.TimestampFieldName: + continue + case zerolog.MessageFieldName: + msg, _ = value.(string) + continue + } + + switch value.(type) { + case string: + args[jKey], _ = value.(string) + case json.Number: + args[jKey] = fmt.Sprint(value) + default: + b, err := json.Marshal(value) + if err != nil { + args[jKey] = fmt.Sprintf("[error: %v]", err) + } else { + args[jKey] = string(b) + } + } + } + args["JSON"] = string(p) + err = journal.Send(msg, jPrio, args) + return +} diff --git a/journald/journald_test.go b/journald/journald_test.go new file mode 100644 index 0000000..7ea40b5 --- /dev/null +++ b/journald/journald_test.go @@ -0,0 +1,44 @@ +// +build !windows + +package journald_test + +import "github.com/rs/zerolog" +import "github.com/rs/zerolog/journald" + +func ExampleNewJournalDWriter() { + log := zerolog.New(journald.NewJournalDWriter()) + log.Info().Str("foo", "bar").Uint64("small", 123).Float64("float", 3.14).Uint64("big", 1152921504606846976).Msg("Journal Test") + // Output: +} + +/* + +There is no automated way to verify the output - since the output is sent +to journald process and method to retrieve is journalctl. Will find a way +to automate the process and fix this test. + +$ journalctl -o verbose -f + +Thu 2018-04-26 22:30:20.768136 PDT [s=3284d695bde946e4b5017c77a399237f;i=329f0;b=98c0dca0debc4b98a5b9534e910e7dd6;m=7a702e35dd4;t=56acdccd2ed0a;x=4690034cf0348614] + PRIORITY=6 + _AUDIT_LOGINUID=1000 + _BOOT_ID=98c0dca0debc4b98a5b9534e910e7dd6 + _MACHINE_ID=926ed67eb4744580948de70fb474975e + _HOSTNAME=sprint + _UID=1000 + _GID=1000 + _CAP_EFFECTIVE=0 + _SYSTEMD_SLICE=-.slice + _TRANSPORT=journal + _SYSTEMD_CGROUP=/ + _AUDIT_SESSION=2945 + MESSAGE=Journal Test + FOO=bar + BIG=1152921504606846976 + _COMM=journald.test + SMALL=123 + FLOAT=3.14 + JSON={"level":"info","foo":"bar","small":123,"float":3.14,"big":1152921504606846976,"message":"Journal Test"} + _PID=27103 + _SOURCE_REALTIME_TIMESTAMP=1524807020768136 +*/ diff --git a/log.go b/log.go index e446f2f..655954b 100644 --- a/log.go +++ b/log.go @@ -148,6 +148,28 @@ func (l Level) String() string { return "" } +// ParseLevel converts a level string into a zerolog Level value. +// returns an error if the input string does not match known values. +func ParseLevel(levelStr string) (Level, error) { + switch levelStr { + case DebugLevel.String(): + return DebugLevel, nil + case InfoLevel.String(): + return InfoLevel, nil + case WarnLevel.String(): + return WarnLevel, nil + case ErrorLevel.String(): + return ErrorLevel, nil + case FatalLevel.String(): + return FatalLevel, nil + case PanicLevel.String(): + return PanicLevel, nil + case NoLevel.String(): + return NoLevel, nil + } + return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr) +} + // A Logger represents an active logging object that generates lines // of JSON output to an io.Writer. Each logging operation makes a single // call to the Writer's Write method. There is no guaranty on access