From 47c8c5b8504f02617f225f9850d5b250708326d9 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 10 Sep 2023 15:12:30 +0200 Subject: [PATCH] mlog: Make default message handler human readable The JSON message handler is left as a separate implementation, for any that want to use it. --- mlog/message_handler.go | 155 ++++++++++++++++++++++++++++++++++++++++ mlog/mlog.go | 81 --------------------- 2 files changed, 155 insertions(+), 81 deletions(-) create mode 100644 mlog/message_handler.go diff --git a/mlog/message_handler.go b/mlog/message_handler.go new file mode 100644 index 0000000..6360246 --- /dev/null +++ b/mlog/message_handler.go @@ -0,0 +1,155 @@ +package mlog + +import ( + "encoding/json" + "fmt" + "io" + "path" + "sync" + + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" +) + +// MessageHandler is a type which can process Messages in some way. +// +// NOTE that Logger does not handle thread-safety, that must be done inside the +// MessageHandler if necessary. +type MessageHandler interface { + Handle(FullMessage) error + + // Sync flushes any buffered data to the handler's output, e.g. a file or + // network connection. If the handler doesn't buffer data then this will be + // a no-op. + Sync() error +} + +func maybeSyncWriter(w io.Writer) error { + if s, ok := w.(interface{ Sync() error }); ok { + return s.Sync() + } else if f, ok := w.(interface{ Flush() error }); ok { + return f.Flush() + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////// + +type msgHandler struct { + l sync.Mutex + out io.Writer + aa mctx.Annotations +} + +// NewMessageHandler initializes and returns a MessageHandler which will write +// all messages to the given io.Writer in a human-readable format. +// +// If the io.Writer also implements a Sync or Flush method then that will be +// called when Sync is called on the returned MessageHandler. +func NewMessageHandler(out io.Writer) MessageHandler { + return &msgHandler{ + out: out, + aa: mctx.Annotations{}, + } +} + +func (h *msgHandler) Sync() error { + h.l.Lock() + defer h.l.Unlock() + return maybeSyncWriter(h.out) +} + +func (h *msgHandler) Handle(msg FullMessage) error { + h.l.Lock() + defer h.l.Unlock() + + var namespaceStr string + + if len(msg.Namespace) > 0 { + namespaceStr = "[" + path.Join(msg.Namespace...) + "] " + } + + var annotationsStr string + + if ss := mctx.EvaluateAnnotations(msg.Context, h.aa).StringSlice(true); len(ss) > 0 { + for i := range ss { + annotationsStr += fmt.Sprintf(" %q=%q", ss[i][0], ss[i][1]) + } + } + + fmt.Fprintf( + h.out, "%s %s%s%s\n", + msg.Level.String(), + namespaceStr, + msg.Description, + annotationsStr, + ) + + for k := range h.aa { + delete(h.aa, k) + } + + return nil +} + +//////////////////////////////////////////////////////////////////////////////// + +type jsonMsgHandler struct { + l sync.Mutex + out io.Writer + enc *json.Encoder + aa mctx.Annotations +} + +// NewJSONMessageHandler initializes and returns a MessageHandler which will +// write all messages to the given io.Writer as JSON objects. +// +// If the io.Writer also implements a Sync or Flush method then that will be +// called when Sync is called on the returned MessageHandler. +func NewJSONMessageHandler(out io.Writer) MessageHandler { + return &jsonMsgHandler{ + out: out, + enc: json.NewEncoder(out), + aa: mctx.Annotations{}, + } +} + +type messageJSON struct { + TimeDate string `json:"td"` + Timestamp int64 `json:"ts"` + Level string `json:"level"` + Namespace []string `json:"ns,omitempty"` + Description string `json:"descr"` + LevelInt int `json:"level_int"` + + // key -> value + Annotations map[string]string `json:"annotations,omitempty"` +} + +const msgTimeFormat = "06/01/02 15:04:05.000000" + +func (h *jsonMsgHandler) Handle(msg FullMessage) error { + h.l.Lock() + defer h.l.Unlock() + + msgJSON := messageJSON{ + TimeDate: msg.Time.UTC().Format(msgTimeFormat), + Timestamp: msg.Time.UnixNano(), + Level: msg.Level.String(), + LevelInt: msg.Level.Int(), + Namespace: msg.Namespace, + Description: msg.Description, + Annotations: mctx.EvaluateAnnotations(msg.Context, h.aa).StringMap(), + } + + for k := range h.aa { + delete(h.aa, k) + } + + return h.enc.Encode(msgJSON) +} + +func (h *jsonMsgHandler) Sync() error { + h.l.Lock() + defer h.l.Unlock() + return maybeSyncWriter(h.out) +} diff --git a/mlog/mlog.go b/mlog/mlog.go index 8928ded..a6b358b 100644 --- a/mlog/mlog.go +++ b/mlog/mlog.go @@ -4,14 +4,11 @@ // The log methods take in a message string and a Context. The Context can be // loaded with additional annotations which will be included in the log entry as // well (see mctx package). -// package mlog import ( "context" - "encoding/json" "errors" - "io" "io/ioutil" "os" "strings" @@ -112,84 +109,6 @@ type FullMessage struct { Namespace []string } -// MessageHandler is a type which can process Messages in some way. -// -// NOTE that Logger does not handle thread-safety, that must be done inside the -// MessageHandler if necessary. -type MessageHandler interface { - Handle(FullMessage) error - - // Sync flushes any buffered data to the handler's output, e.g. a file or - // network connection. If the handler doesn't buffer data then this will be - // a no-op. - Sync() error -} - -type messageHandler struct { - l sync.Mutex - out io.Writer - enc *json.Encoder - aa mctx.Annotations -} - -// NewMessageHandler initializes and returns a MessageHandler which will write -// all messages to the given io.Writer in a thread-safe way. If the io.Writer -// also implements a Sync or Flush method then that will be called when Sync is -// called on the returned MessageHandler. -func NewMessageHandler(out io.Writer) MessageHandler { - return &messageHandler{ - out: out, - enc: json.NewEncoder(out), - aa: mctx.Annotations{}, - } -} - -type messageJSON struct { - TimeDate string `json:"td"` - Timestamp int64 `json:"ts"` - Level string `json:"level"` - Namespace []string `json:"ns,omitempty"` - Description string `json:"descr"` - LevelInt int `json:"level_int"` - - // key -> value - Annotations map[string]string `json:"annotations,omitempty"` -} - -const msgTimeFormat = "06/01/02 15:04:05.000000" - -func (h *messageHandler) Handle(msg FullMessage) error { - h.l.Lock() - defer h.l.Unlock() - - msgJSON := messageJSON{ - TimeDate: msg.Time.UTC().Format(msgTimeFormat), - Timestamp: msg.Time.UnixNano(), - Level: msg.Level.String(), - LevelInt: msg.Level.Int(), - Namespace: msg.Namespace, - Description: msg.Description, - Annotations: mctx.EvaluateAnnotations(msg.Context, h.aa).StringMap(), - } - - for k := range h.aa { - delete(h.aa, k) - } - - return h.enc.Encode(msgJSON) -} - -func (h *messageHandler) Sync() error { - h.l.Lock() - defer h.l.Unlock() - if s, ok := h.out.(interface{ Sync() error }); ok { - return s.Sync() - } else if f, ok := h.out.(interface{ Flush() error }); ok { - return f.Flush() - } - return nil -} - // LoggerOpts are optional parameters to NewLogger. All fields are optional. A // nil value of LoggerOpts is equivalent to an empty one. type LoggerOpts struct {