mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
feat(logs): show full log event (#1096)
* feat(logs): show all extra fields in UI * feat(logs): init log level as debug then trace
This commit is contained in:
parent
7b77ff766e
commit
cbf668e87c
6 changed files with 448 additions and 88 deletions
|
@ -59,7 +59,7 @@ func main() {
|
||||||
serverEvents.CreateStreamWithOpts("logs", sse.StreamOpts{MaxEntries: 1000, AutoReplay: true})
|
serverEvents.CreateStreamWithOpts("logs", sse.StreamOpts{MaxEntries: 1000, AutoReplay: true})
|
||||||
|
|
||||||
// register SSE hook on logger
|
// register SSE hook on logger
|
||||||
log.RegisterSSEHook(serverEvents)
|
log.RegisterSSEWriter(serverEvents)
|
||||||
|
|
||||||
// setup internal eventbus
|
// setup internal eventbus
|
||||||
bus := EventBus.New()
|
bus := EventBus.New()
|
||||||
|
|
|
@ -160,7 +160,9 @@ func (h *Handler) Run() error {
|
||||||
addr = h.network.BouncerAddr
|
addr = h.network.BouncerAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
subLogger := zstdlog.NewStdLoggerWithLevel(h.log.With().Logger(), zerolog.TraceLevel)
|
// this used to be TraceLevel but was changed to DebugLevel during connect to see the info without needing to change loglevel
|
||||||
|
// we change back to TraceLevel in the handleJoined method.
|
||||||
|
subLogger := zstdlog.NewStdLoggerWithLevel(h.log.With().Logger(), zerolog.DebugLevel)
|
||||||
|
|
||||||
h.client = &ircevent.Connection{
|
h.client = &ircevent.Connection{
|
||||||
Nick: h.network.Nick,
|
Nick: h.network.Nick,
|
||||||
|
@ -318,6 +320,7 @@ func (h *Handler) resetChannelHealth() {
|
||||||
// Stop the network and quit
|
// Stop the network and quit
|
||||||
func (h *Handler) Stop() {
|
func (h *Handler) Stop() {
|
||||||
h.m.Lock()
|
h.m.Lock()
|
||||||
|
h.connectedSince = time.Time{}
|
||||||
h.manuallyDisconnected = true
|
h.manuallyDisconnected = true
|
||||||
|
|
||||||
if h.client.Connected() {
|
if h.client.Connected() {
|
||||||
|
@ -325,6 +328,8 @@ func (h *Handler) Stop() {
|
||||||
}
|
}
|
||||||
h.m.Unlock()
|
h.m.Unlock()
|
||||||
|
|
||||||
|
h.resetChannelHealth()
|
||||||
|
|
||||||
h.client.Quit()
|
h.client.Quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,7 +356,7 @@ func (h *Handler) onConnect(m ircmsg.Message) {
|
||||||
if h.haveDisconnected {
|
if h.haveDisconnected {
|
||||||
h.notificationService.Send(domain.NotificationEventIRCReconnected, domain.NotificationPayload{
|
h.notificationService.Send(domain.NotificationEventIRCReconnected, domain.NotificationPayload{
|
||||||
Subject: "IRC Reconnected",
|
Subject: "IRC Reconnected",
|
||||||
Message: fmt.Sprintf("Network: %v", h.network.Name),
|
Message: fmt.Sprintf("Network: %s", h.network.Name),
|
||||||
})
|
})
|
||||||
|
|
||||||
// reset haveDisconnected
|
// reset haveDisconnected
|
||||||
|
@ -359,12 +364,13 @@ func (h *Handler) onConnect(m ircmsg.Message) {
|
||||||
}
|
}
|
||||||
h.m.Unlock()
|
h.m.Unlock()
|
||||||
|
|
||||||
h.log.Debug().Msgf("connected to: %v", h.network.Name)
|
h.log.Debug().Msgf("connected to: %s", h.network.Name)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
h.authenticate()
|
h.authenticate()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// onDisconnect is the disconnect callback
|
// onDisconnect is the disconnect callback
|
||||||
|
@ -391,7 +397,7 @@ func (h *Handler) onDisconnect(m ircmsg.Message) {
|
||||||
// only send notification if we did not initiate disconnect/restart/stop
|
// only send notification if we did not initiate disconnect/restart/stop
|
||||||
h.notificationService.Send(domain.NotificationEventIRCDisconnected, domain.NotificationPayload{
|
h.notificationService.Send(domain.NotificationEventIRCDisconnected, domain.NotificationPayload{
|
||||||
Subject: "IRC Disconnected unexpectedly",
|
Subject: "IRC Disconnected unexpectedly",
|
||||||
Message: fmt.Sprintf("Network: %v", h.network.Name),
|
Message: fmt.Sprintf("Network: %s", h.network.Name),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// reset
|
// reset
|
||||||
|
@ -466,7 +472,7 @@ func (h *Handler) handleNickServ(msg ircmsg.Message) {
|
||||||
if contains(msg.Params[1], "invalid parameters", "help identify") {
|
if contains(msg.Params[1], "invalid parameters", "help identify") {
|
||||||
h.log.Debug().Msgf("NOTICE nickserv invalid: %v", msg.Params)
|
h.log.Debug().Msgf("NOTICE nickserv invalid: %v", msg.Params)
|
||||||
|
|
||||||
if err := h.client.Send("PRIVMSG", "NickServ", fmt.Sprintf("IDENTIFY %v %v", h.network.Auth.Account, h.network.Auth.Password)); err != nil {
|
if err := h.client.Send("PRIVMSG", "NickServ", fmt.Sprintf("IDENTIFY %s %s", h.network.Auth.Account, h.network.Auth.Password)); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -520,7 +526,7 @@ func (h *Handler) inviteCommand() {
|
||||||
if h.network.InviteCommand != "" {
|
if h.network.InviteCommand != "" {
|
||||||
h.log.Trace().Msg("on connect invite command not empty: send connect commands")
|
h.log.Trace().Msg("on connect invite command not empty: send connect commands")
|
||||||
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
|
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
|
h.log.Error().Stack().Err(err).Msgf("error sending connect command %s", h.network.InviteCommand)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -541,7 +547,7 @@ func contains(s string, substr ...string) bool {
|
||||||
|
|
||||||
// onNick handles NICK events
|
// onNick handles NICK events
|
||||||
func (h *Handler) onNick(msg ircmsg.Message) {
|
func (h *Handler) onNick(msg ircmsg.Message) {
|
||||||
h.log.Trace().Msgf("NICK event: %v params: %v", msg.Nick(), msg.Params)
|
h.log.Trace().Msgf("NICK event: %s params: %v", msg.Nick(), msg.Params)
|
||||||
if len(msg.Params) < 1 {
|
if len(msg.Params) < 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -593,7 +599,7 @@ func (h *Handler) onMessage(msg ircmsg.Message) {
|
||||||
h.log.Debug().Str("channel", channel).Str("nick", nick).Msg(cleanedMsg)
|
h.log.Debug().Str("channel", channel).Str("nick", nick).Msg(cleanedMsg)
|
||||||
|
|
||||||
if err := h.sendToAnnounceProcessor(channel, cleanedMsg); err != nil {
|
if err := h.sendToAnnounceProcessor(channel, cleanedMsg); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("could not queue line: %v", cleanedMsg)
|
h.log.Error().Stack().Err(err).Msgf("could not queue line: %s", cleanedMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -607,12 +613,12 @@ func (h *Handler) sendToAnnounceProcessor(channel string, msg string) error {
|
||||||
// check if queue exists
|
// check if queue exists
|
||||||
queue, ok := h.announceProcessors[channel]
|
queue, ok := h.announceProcessors[channel]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("queue '%v' not found", channel)
|
return errors.New("queue '%s' not found", channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it exists, add msg
|
// if it exists, add msg
|
||||||
if err := queue.AddLineToQueue(channel, msg); err != nil {
|
if err := queue.AddLineToQueue(channel, msg); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("could not queue line: %v", msg)
|
h.log.Error().Stack().Err(err).Msgf("could not queue line: %s", msg)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,7 +633,7 @@ func (h *Handler) sendToAnnounceProcessor(channel string, msg string) error {
|
||||||
func (h *Handler) JoinChannels() {
|
func (h *Handler) JoinChannels() {
|
||||||
for _, channel := range h.network.Channels {
|
for _, channel := range h.network.Channels {
|
||||||
if err := h.JoinChannel(channel.Name, channel.Password); err != nil {
|
if err := h.JoinChannel(channel.Name, channel.Password); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("error joining channel %v", channel.Name)
|
h.log.Error().Stack().Err(err).Msgf("error joining channel %s", channel.Name)
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
|
@ -645,10 +651,10 @@ func (h *Handler) JoinChannel(channel string, password string) error {
|
||||||
m.Params = []string{channel, password}
|
m.Params = []string{channel, password}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Debug().Msgf("sending JOIN command %v", strings.Join(m.Params, " "))
|
h.log.Debug().Msgf("sending JOIN command %s", strings.Join(m.Params, " "))
|
||||||
|
|
||||||
if err := h.client.SendIRCMessage(m); err != nil {
|
if err := h.client.SendIRCMessage(m); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("error handling join: %v", channel)
|
h.log.Error().Stack().Err(err).Msgf("error handling join: %s", channel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -663,7 +669,7 @@ func (h *Handler) handlePart(msg ircmsg.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := strings.ToLower(msg.Params[0])
|
channel := strings.ToLower(msg.Params[0])
|
||||||
h.log.Debug().Msgf("PART channel %v", channel)
|
h.log.Debug().Msgf("PART channel %s", channel)
|
||||||
|
|
||||||
// reset monitoring status
|
// reset monitoring status
|
||||||
if v, ok := h.channelHealth[channel]; ok {
|
if v, ok := h.channelHealth[channel]; ok {
|
||||||
|
@ -672,15 +678,15 @@ func (h *Handler) handlePart(msg ircmsg.Message) {
|
||||||
|
|
||||||
// TODO remove announceProcessor
|
// TODO remove announceProcessor
|
||||||
|
|
||||||
h.log.Debug().Msgf("Left channel %v", channel)
|
h.log.Debug().Msgf("Left channel %s", channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PartChannel parts/leaves channel
|
// PartChannel parts/leaves channel
|
||||||
func (h *Handler) PartChannel(channel string) error {
|
func (h *Handler) PartChannel(channel string) error {
|
||||||
h.log.Debug().Msgf("Leaving channel %v", channel)
|
h.log.Debug().Msgf("Leaving channel %s", channel)
|
||||||
|
|
||||||
if err := h.client.Part(channel); err != nil {
|
if err := h.client.Part(channel); err != nil {
|
||||||
h.log.Error().Err(err).Msgf("error handling part: %v", channel)
|
h.log.Error().Err(err).Msgf("error handling part: %s", channel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -699,12 +705,12 @@ func (h *Handler) handleJoined(msg ircmsg.Message) {
|
||||||
// get channel
|
// get channel
|
||||||
channel := strings.ToLower(msg.Params[1])
|
channel := strings.ToLower(msg.Params[1])
|
||||||
|
|
||||||
h.log.Debug().Msgf("JOINED: %v", channel)
|
h.log.Debug().Msgf("JOINED: %s", channel)
|
||||||
|
|
||||||
// check if channel is valid and if not lets part
|
// check if channel is valid and if not lets part
|
||||||
if valid := h.isValidHandlerChannel(channel); !valid {
|
if valid := h.isValidHandlerChannel(channel); !valid {
|
||||||
if err := h.PartChannel(msg.Params[1]); err != nil {
|
if err := h.PartChannel(msg.Params[1]); err != nil {
|
||||||
h.log.Error().Err(err).Msgf("error handling part for unwanted channel: %v", msg.Params[1])
|
h.log.Error().Err(err).Msgf("error handling part for unwanted channel: %s", msg.Params[1])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -717,7 +723,7 @@ func (h *Handler) handleJoined(msg ircmsg.Message) {
|
||||||
v.monitoring = true
|
v.monitoring = true
|
||||||
v.monitoringSince = time.Now()
|
v.monitoringSince = time.Now()
|
||||||
|
|
||||||
h.log.Trace().Msgf("set monitoring: %v", v.name)
|
h.log.Trace().Msgf("set monitoring: %s", v.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -727,17 +733,20 @@ func (h *Handler) handleJoined(msg ircmsg.Message) {
|
||||||
monitoringSince: time.Now(),
|
monitoringSince: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Trace().Msgf("add channel health monitoring: %v", channel)
|
h.log.Trace().Msgf("add channel health monitoring: %s", channel)
|
||||||
}
|
}
|
||||||
h.m.Unlock()
|
h.m.Unlock()
|
||||||
|
|
||||||
// if not valid it's considered an extra channel
|
// if not valid it's considered an extra channel
|
||||||
if valid := h.isValidChannel(channel); !valid {
|
if valid := h.isValidChannel(channel); !valid {
|
||||||
h.log.Info().Msgf("Joined extra channel %v", channel)
|
h.log.Info().Msgf("Joined extra channel %s", channel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Info().Msgf("Monitoring channel %v", channel)
|
h.log.Info().Msgf("Monitoring channel %s", channel)
|
||||||
|
|
||||||
|
// reset log level to Trace now that we are monitoring a channel
|
||||||
|
h.client.Log = zstdlog.NewStdLoggerWithLevel(h.log.With().Logger(), zerolog.TraceLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendConnectCommands sends invite commands
|
// sendConnectCommands sends invite commands
|
||||||
|
@ -758,7 +767,7 @@ func (h *Handler) sendConnectCommands(msg string) error {
|
||||||
Params: strings.Split(cmd, " "),
|
Params: strings.Split(cmd, " "),
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Debug().Msgf("sending connect command: %v", cmd)
|
h.log.Debug().Msgf("sending connect command: %s", cmd)
|
||||||
|
|
||||||
if err := h.client.SendIRCMessage(m); err != nil {
|
if err := h.client.SendIRCMessage(m); err != nil {
|
||||||
h.log.Error().Err(err).Msgf("error handling connect command: %v", m)
|
h.log.Error().Err(err).Msgf("error handling connect command: %v", m)
|
||||||
|
@ -780,17 +789,17 @@ func (h *Handler) handleInvite(msg ircmsg.Message) {
|
||||||
// get channel
|
// get channel
|
||||||
channel := strings.ToLower(msg.Params[1])
|
channel := strings.ToLower(msg.Params[1])
|
||||||
|
|
||||||
h.log.Trace().Msgf("INVITE from %v to join: %v", msg.Nick(), channel)
|
h.log.Trace().Msgf("INVITE from %s to join: %s", msg.Nick(), channel)
|
||||||
|
|
||||||
if validChannel := h.isValidHandlerChannel(channel); !validChannel {
|
if validChannel := h.isValidHandlerChannel(channel); !validChannel {
|
||||||
h.log.Trace().Msgf("invite from %v to join: %v - invalid channel, skip joining", msg.Nick(), channel)
|
h.log.Trace().Msgf("invite from %s to join: %s - invalid channel, skip joining", msg.Nick(), channel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Debug().Msgf("INVITE from %v, joining %v", msg.Nick(), channel)
|
h.log.Debug().Msgf("INVITE from %s, joining %s", msg.Nick(), channel)
|
||||||
|
|
||||||
if err := h.client.Join(msg.Params[1]); err != nil {
|
if err := h.client.Join(msg.Params[1]); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("error handling join: %v", msg.Params[1])
|
h.log.Error().Stack().Err(err).Msgf("error handling join: %s", msg.Params[1])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -816,7 +825,7 @@ func (h *Handler) NickServIdentify(password string) error {
|
||||||
|
|
||||||
// NickChange sets a new nick for our user
|
// NickChange sets a new nick for our user
|
||||||
func (h *Handler) NickChange(nick string) error {
|
func (h *Handler) NickChange(nick string) error {
|
||||||
h.log.Debug().Msgf("NICK change: %v", nick)
|
h.log.Debug().Msgf("NICK change: %s", nick)
|
||||||
|
|
||||||
h.client.SetNick(nick)
|
h.client.SetNick(nick)
|
||||||
|
|
||||||
|
@ -853,7 +862,7 @@ func (h *Handler) SendMsg(channel, msg string) error {
|
||||||
h.log.Debug().Msgf("sending msg command: %s", msg)
|
h.log.Debug().Msgf("sending msg command: %s", msg)
|
||||||
|
|
||||||
if err := h.client.Privmsg(channel, msg); err != nil {
|
if err := h.client.Privmsg(channel, msg); err != nil {
|
||||||
h.log.Error().Stack().Err(err).Msgf("error sending msg: %v", msg)
|
h.log.Error().Stack().Err(err).Msgf("error sending msg: %s", msg)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ type Logger interface {
|
||||||
Trace() *zerolog.Event
|
Trace() *zerolog.Event
|
||||||
Debug() *zerolog.Event
|
Debug() *zerolog.Event
|
||||||
With() zerolog.Context
|
With() zerolog.Context
|
||||||
RegisterSSEHook(sse *sse.Server)
|
RegisterSSEWriter(sse *sse.Server)
|
||||||
SetLogLevel(level string)
|
SetLogLevel(level string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,8 +78,10 @@ func New(cfg *domain.Config) Logger {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *DefaultLogger) RegisterSSEHook(sse *sse.Server) {
|
func (l *DefaultLogger) RegisterSSEWriter(sse *sse.Server) {
|
||||||
l.log = l.log.Hook(&ServerSentEventHook{sse: sse})
|
w := NewSSEWriter(sse)
|
||||||
|
l.writers = append(l.writers, w)
|
||||||
|
l.log = zerolog.New(io.MultiWriter(l.writers...)).With().Stack().Logger()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *DefaultLogger) SetLogLevel(level string) {
|
func (l *DefaultLogger) SetLogLevel(level string) {
|
||||||
|
|
394
internal/logger/sse_writer.go
Normal file
394
internal/logger/sse_writer.go
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
// Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/r3labs/sse/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSEWriter struct {
|
||||||
|
// SSE
|
||||||
|
SSE *sse.Server
|
||||||
|
|
||||||
|
// TimeFormat specifies the format for timestamp in output.
|
||||||
|
TimeFormat string
|
||||||
|
|
||||||
|
// PartsOrder defines the order of parts in output.
|
||||||
|
PartsOrder []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSSEWriter(sse *sse.Server, options ...func(w *SSEWriter)) SSEWriter {
|
||||||
|
w := SSEWriter{
|
||||||
|
SSE: sse,
|
||||||
|
TimeFormat: defaultTimeFormat,
|
||||||
|
PartsOrder: defaultPartsOrder(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogMessage struct {
|
||||||
|
Time string `json:"time"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m LogMessage) Bytes() ([]byte, error) {
|
||||||
|
j, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w SSEWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if w.SSE == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var evt map[string]interface{}
|
||||||
|
p = decodeIfBinaryToBytes(p)
|
||||||
|
d := json.NewDecoder(bytes.NewReader(p))
|
||||||
|
d.UseNumber()
|
||||||
|
err = d.Decode(&evt)
|
||||||
|
if err != nil {
|
||||||
|
return n, fmt.Errorf("cannot decode event: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, 100))
|
||||||
|
defer func() {
|
||||||
|
buf.Reset()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, p := range w.PartsOrder {
|
||||||
|
w.writePart(buf, evt, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.writeFields(buf, evt)
|
||||||
|
|
||||||
|
err = buf.WriteByte('\n')
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := LogMessage{
|
||||||
|
//Time: w.formatTime(evt),
|
||||||
|
Time: evt["time"].(string),
|
||||||
|
Level: w.formatLevel(evt),
|
||||||
|
Message: buf.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := m.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish too logs topic
|
||||||
|
w.SSE.Publish("logs", &sse.Event{
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return len(p), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFields appends formatted key-value pairs to buf.
|
||||||
|
func (w SSEWriter) writeFields(buf *bytes.Buffer, evt map[string]interface{}) {
|
||||||
|
var fields = make([]string, 0, len(evt))
|
||||||
|
for field := range evt {
|
||||||
|
|
||||||
|
switch field {
|
||||||
|
case zerolog.LevelFieldName, zerolog.TimestampFieldName, zerolog.MessageFieldName, zerolog.CallerFieldName:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
sort.Strings(fields)
|
||||||
|
|
||||||
|
// Write space only if something has already been written to the buffer, and if there are fields.
|
||||||
|
if buf.Len() > 0 && len(fields) > 0 {
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the "error" field to the front
|
||||||
|
ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= zerolog.ErrorFieldName })
|
||||||
|
if ei < len(fields) && fields[ei] == zerolog.ErrorFieldName {
|
||||||
|
fields[ei] = ""
|
||||||
|
fields = append([]string{zerolog.ErrorFieldName}, fields...)
|
||||||
|
var xfields = make([]string, 0, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
if field == "" { // Skip empty fields
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xfields = append(xfields, field)
|
||||||
|
}
|
||||||
|
fields = xfields
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, field := range fields {
|
||||||
|
var fn Formatter
|
||||||
|
var fv Formatter
|
||||||
|
|
||||||
|
if field == zerolog.ErrorFieldName {
|
||||||
|
fn = defaultFormatErrFieldName()
|
||||||
|
|
||||||
|
fv = defaultFormatErrFieldValue()
|
||||||
|
} else {
|
||||||
|
fn = defaultFormatFieldName()
|
||||||
|
|
||||||
|
fv = defaultFormatFieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(fn(field))
|
||||||
|
|
||||||
|
switch fValue := evt[field].(type) {
|
||||||
|
case string:
|
||||||
|
if needsQuote(fValue) {
|
||||||
|
buf.WriteString(fv(strconv.Quote(fValue)))
|
||||||
|
} else {
|
||||||
|
buf.WriteString(fv(fValue))
|
||||||
|
}
|
||||||
|
case json.Number:
|
||||||
|
buf.WriteString(fv(fValue))
|
||||||
|
default:
|
||||||
|
b, err := zerolog.InterfaceMarshalFunc(fValue)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(buf, "[error: %v]", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(buf, fv(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < len(fields)-1 { // Skip space for last field
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePart appends a formatted part to buf.
|
||||||
|
func (w SSEWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, p string) {
|
||||||
|
var f Formatter
|
||||||
|
|
||||||
|
switch p {
|
||||||
|
case zerolog.LevelFieldName:
|
||||||
|
f = defaultFormatLevel()
|
||||||
|
|
||||||
|
case zerolog.TimestampFieldName:
|
||||||
|
f = defaultFormatTimestamp(w.TimeFormat)
|
||||||
|
|
||||||
|
case zerolog.MessageFieldName:
|
||||||
|
f = defaultFormatMessage
|
||||||
|
|
||||||
|
case zerolog.CallerFieldName:
|
||||||
|
f = defaultFormatCaller()
|
||||||
|
|
||||||
|
default:
|
||||||
|
f = defaultFormatFieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = f(evt[p])
|
||||||
|
|
||||||
|
if len(s) > 0 {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteByte(' ') // Write space only if not the first part
|
||||||
|
}
|
||||||
|
buf.WriteString(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatLevel format level to string
|
||||||
|
func (w SSEWriter) formatLevel(evt map[string]interface{}) string {
|
||||||
|
var f Formatter
|
||||||
|
|
||||||
|
f = defaultFormatLevel()
|
||||||
|
|
||||||
|
var s = f(evt["level"])
|
||||||
|
|
||||||
|
if len(s) > 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTime format time to string
|
||||||
|
func (w SSEWriter) formatTime(evt map[string]interface{}) string {
|
||||||
|
var f Formatter
|
||||||
|
|
||||||
|
f = defaultFormatTimestamp(w.TimeFormat)
|
||||||
|
|
||||||
|
var s = f(evt["time"])
|
||||||
|
|
||||||
|
if len(s) > 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeFormat = time.Kitchen
|
||||||
|
)
|
||||||
|
|
||||||
|
// Formatter transforms the input into a formatted string.
|
||||||
|
type Formatter func(interface{}) string
|
||||||
|
|
||||||
|
func decodeIfBinaryToBytes(in []byte) []byte {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsQuote returns true when the string s should be quoted in output.
|
||||||
|
func needsQuote(s string) bool {
|
||||||
|
for i := range s {
|
||||||
|
if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- DEFAULT FORMATTERS ---------------------------------------------------
|
||||||
|
|
||||||
|
func defaultPartsOrder() []string {
|
||||||
|
return []string{
|
||||||
|
//zerolog.TimestampFieldName,
|
||||||
|
//zerolog.LevelFieldName,
|
||||||
|
zerolog.CallerFieldName,
|
||||||
|
zerolog.MessageFieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatTimestamp(timeFormat string) Formatter {
|
||||||
|
if timeFormat == "" {
|
||||||
|
timeFormat = defaultTimeFormat
|
||||||
|
}
|
||||||
|
return func(i interface{}) string {
|
||||||
|
t := "<nil>"
|
||||||
|
switch tt := i.(type) {
|
||||||
|
case string:
|
||||||
|
ts, err := time.ParseInLocation(zerolog.TimeFieldFormat, tt, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
t = tt
|
||||||
|
} else {
|
||||||
|
t = ts.Local().Format(timeFormat)
|
||||||
|
}
|
||||||
|
case json.Number:
|
||||||
|
i, err := tt.Int64()
|
||||||
|
if err != nil {
|
||||||
|
t = tt.String()
|
||||||
|
} else {
|
||||||
|
var sec, nsec int64
|
||||||
|
|
||||||
|
switch zerolog.TimeFieldFormat {
|
||||||
|
case zerolog.TimeFormatUnixNano:
|
||||||
|
sec, nsec = 0, i
|
||||||
|
case zerolog.TimeFormatUnixMicro:
|
||||||
|
sec, nsec = 0, int64(time.Duration(i)*time.Microsecond)
|
||||||
|
case zerolog.TimeFormatUnixMs:
|
||||||
|
sec, nsec = 0, int64(time.Duration(i)*time.Millisecond)
|
||||||
|
default:
|
||||||
|
sec, nsec = i, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.Unix(sec, nsec)
|
||||||
|
t = ts.Format(timeFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatLevel() Formatter {
|
||||||
|
return func(i interface{}) string {
|
||||||
|
var l string
|
||||||
|
if ll, ok := i.(string); ok {
|
||||||
|
switch ll {
|
||||||
|
case zerolog.LevelTraceValue:
|
||||||
|
l = "TRC"
|
||||||
|
case zerolog.LevelDebugValue:
|
||||||
|
l = "DBG"
|
||||||
|
case zerolog.LevelInfoValue:
|
||||||
|
l = "INF"
|
||||||
|
case zerolog.LevelWarnValue:
|
||||||
|
l = "WRN"
|
||||||
|
case zerolog.LevelErrorValue:
|
||||||
|
l = "ERR"
|
||||||
|
case zerolog.LevelFatalValue:
|
||||||
|
l = "FTL"
|
||||||
|
case zerolog.LevelPanicValue:
|
||||||
|
l = "PNC"
|
||||||
|
default:
|
||||||
|
l = ll
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if i == nil {
|
||||||
|
l = "???"
|
||||||
|
} else {
|
||||||
|
l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatCaller() Formatter {
|
||||||
|
return func(i interface{}) string {
|
||||||
|
var c string
|
||||||
|
if cc, ok := i.(string); ok {
|
||||||
|
c = cc
|
||||||
|
}
|
||||||
|
if len(c) > 0 {
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
if rel, err := filepath.Rel(cwd, c); err == nil {
|
||||||
|
c = rel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c = c + " >"
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatMessage(i interface{}) string {
|
||||||
|
if i == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatFieldName() Formatter {
|
||||||
|
return func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s=", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatFieldValue(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatErrFieldName() Formatter {
|
||||||
|
return func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s=", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatErrFieldValue() Formatter {
|
||||||
|
return func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s=", i)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
// Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/r3labs/sse/v2"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogMessage struct {
|
|
||||||
Time string `json:"time"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m LogMessage) ToJsonString() string {
|
|
||||||
j, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerSentEventHook struct {
|
|
||||||
sse *sse.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ServerSentEventHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
|
||||||
if h.sse != nil {
|
|
||||||
// publish too logs topic
|
|
||||||
logMsg := LogMessage{
|
|
||||||
Time: time.Now().Format(time.RFC3339),
|
|
||||||
Level: strings.ToUpper(level.String()),
|
|
||||||
Message: msg,
|
|
||||||
}
|
|
||||||
|
|
||||||
h.sse.Publish("logs", &sse.Event{
|
|
||||||
Data: []byte(logMsg.ToJsonString()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,14 +31,16 @@ type LogEvent = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LogLevel = "TRACE" | "DEBUG" | "INFO" | "ERROR" | "WARN";
|
type LogLevel = "TRC" | "DBG" | "INF" | "ERR" | "WRN" | "FTL" | "PNC";
|
||||||
|
|
||||||
const LogColors: Record<LogLevel, string> = {
|
const LogColors: Record<LogLevel, string> = {
|
||||||
"TRACE": "text-purple-300",
|
"TRC": "text-purple-300",
|
||||||
"DEBUG": "text-yellow-500",
|
"DBG": "text-yellow-500",
|
||||||
"INFO": "text-green-500",
|
"INF": "text-green-500",
|
||||||
"ERROR": "text-red-500",
|
"ERR": "text-red-500",
|
||||||
"WARN": "text-yellow-500"
|
"WRN": "text-yellow-500",
|
||||||
|
"FTL": "text-red-500",
|
||||||
|
"PNC": "text-red-600",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Logs = () => {
|
export const Logs = () => {
|
||||||
|
@ -143,7 +145,7 @@ export const Logs = () => {
|
||||||
title={entry.time}
|
title={entry.time}
|
||||||
className="font-mono text-gray-500 dark:text-gray-600 mr-2 h-full"
|
className="font-mono text-gray-500 dark:text-gray-600 mr-2 h-full"
|
||||||
>
|
>
|
||||||
{format(new Date(entry.time), "HH:mm:ss.SSS")}
|
{format(new Date(entry.time), "HH:mm:ss")}
|
||||||
</span>
|
</span>
|
||||||
{entry.level in LogColors ? (
|
{entry.level in LogColors ? (
|
||||||
<span
|
<span
|
||||||
|
@ -153,7 +155,6 @@ export const Logs = () => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{entry.level}
|
{entry.level}
|
||||||
{" "}
|
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="ml-2 text-black dark:text-gray-300">
|
<span className="ml-2 text-black dark:text-gray-300">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue