mlog: Make default message handler human readable

The JSON message handler is left as a separate implementation, for any
that want to use it.
This commit is contained in:
Brian Picciano 2023-09-10 15:12:30 +02:00
parent 07f3889a70
commit 47c8c5b850
2 changed files with 155 additions and 81 deletions

155
mlog/message_handler.go Normal file
View File

@ -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)
}

View File

@ -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 {