mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
network enhanecment / refactor (#361)
* chore(network): improve connectivity check * refactor(network): rewrite network and timesync component * feat(display): show cloud connection status * chore: change logging verbosity * chore(websecure): update log message * fix(ota): validate root certificate when downloading update * feat(ui): add network settings tab * fix(display): cloud connecting animation * fix: golintci issues * feat: add network settings tab * feat(timesync): query servers in parallel * refactor(network): move to internal/network package * feat(timesync): add metrics * refactor(log): move log to internal/logging package * refactor(mdms): move mdns to internal/mdns package * feat(developer): add pprof endpoint * feat(logging): add a simple logging streaming endpoint * fix(mdns): do not start mdns until network is up * feat(network): allow users to update network settings from ui * fix(network): handle errors when net.IPAddr is nil * fix(mdns): scopedLogger SIGSEGV * fix(dhcp): watch directory instead of file to catch fsnotify.Create event * refactor(nbd): move platform-specific code to different files * refactor(native): move platform-specific code to different files * chore: fix linter issues * chore(dev_deploy): allow to override PION_LOG_TRACE
This commit is contained in:
197
internal/logging/logger.go
Normal file
197
internal/logging/logger.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
l *zerolog.Logger
|
||||
scopeLoggers map[string]*zerolog.Logger
|
||||
scopeLevels map[string]zerolog.Level
|
||||
scopeLevelMutex sync.Mutex
|
||||
|
||||
defaultLogLevelFromEnv zerolog.Level
|
||||
defaultLogLevelFromConfig zerolog.Level
|
||||
defaultLogLevel zerolog.Level
|
||||
}
|
||||
|
||||
const (
|
||||
defaultLogLevel = zerolog.ErrorLevel
|
||||
)
|
||||
|
||||
type logOutput struct {
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func (w *logOutput) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
// TODO: write to file or syslog
|
||||
if sseServer != nil {
|
||||
// use a goroutine to avoid blocking the Write method
|
||||
go func() {
|
||||
sseServer.Message <- string(p)
|
||||
}()
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var (
|
||||
consoleLogOutput io.Writer = zerolog.ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: time.RFC3339,
|
||||
PartsOrder: []string{"time", "level", "scope", "component", "message"},
|
||||
FieldsExclude: []string{"scope", "component"},
|
||||
FormatPartValueByName: func(value interface{}, name string) string {
|
||||
val := fmt.Sprintf("%s", value)
|
||||
if name == "component" {
|
||||
if value == nil {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
return val
|
||||
},
|
||||
}
|
||||
fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}}
|
||||
defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput)
|
||||
|
||||
zerologLevels = map[string]zerolog.Level{
|
||||
"DISABLE": zerolog.Disabled,
|
||||
"NOLEVEL": zerolog.NoLevel,
|
||||
"PANIC": zerolog.PanicLevel,
|
||||
"FATAL": zerolog.FatalLevel,
|
||||
"ERROR": zerolog.ErrorLevel,
|
||||
"WARN": zerolog.WarnLevel,
|
||||
"INFO": zerolog.InfoLevel,
|
||||
"DEBUG": zerolog.DebugLevel,
|
||||
"TRACE": zerolog.TraceLevel,
|
||||
}
|
||||
)
|
||||
|
||||
func NewLogger(zerologLogger zerolog.Logger) *Logger {
|
||||
return &Logger{
|
||||
l: &zerologLogger,
|
||||
scopeLoggers: make(map[string]*zerolog.Logger),
|
||||
scopeLevels: make(map[string]zerolog.Level),
|
||||
scopeLevelMutex: sync.Mutex{},
|
||||
defaultLogLevelFromEnv: -2,
|
||||
defaultLogLevelFromConfig: -2,
|
||||
defaultLogLevel: defaultLogLevel,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) updateLogLevel() {
|
||||
l.scopeLevelMutex.Lock()
|
||||
defer l.scopeLevelMutex.Unlock()
|
||||
|
||||
l.scopeLevels = make(map[string]zerolog.Level)
|
||||
|
||||
finalDefaultLogLevel := l.defaultLogLevel
|
||||
|
||||
for name, level := range zerologLevels {
|
||||
env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name))
|
||||
|
||||
if env == "" {
|
||||
env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name))
|
||||
}
|
||||
|
||||
if env == "" {
|
||||
env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name))
|
||||
}
|
||||
|
||||
if env == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.ToLower(env) == "all" {
|
||||
l.defaultLogLevelFromEnv = level
|
||||
|
||||
if finalDefaultLogLevel > level {
|
||||
finalDefaultLogLevel = level
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
scopes := strings.Split(strings.ToLower(env), ",")
|
||||
for _, scope := range scopes {
|
||||
l.scopeLevels[scope] = level
|
||||
}
|
||||
}
|
||||
|
||||
l.defaultLogLevel = finalDefaultLogLevel
|
||||
}
|
||||
|
||||
func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level {
|
||||
if l.scopeLevels == nil {
|
||||
l.updateLogLevel()
|
||||
}
|
||||
|
||||
var scopeLevel zerolog.Level
|
||||
if l.defaultLogLevelFromConfig != -2 {
|
||||
scopeLevel = l.defaultLogLevelFromConfig
|
||||
}
|
||||
if l.defaultLogLevelFromEnv != -2 {
|
||||
scopeLevel = l.defaultLogLevelFromEnv
|
||||
}
|
||||
|
||||
// if the scope is not in the map, use the default level from the root logger
|
||||
if level, ok := l.scopeLevels[scope]; ok {
|
||||
scopeLevel = level
|
||||
}
|
||||
|
||||
return scopeLevel
|
||||
}
|
||||
|
||||
func (l *Logger) newScopeLogger(scope string) zerolog.Logger {
|
||||
scopeLevel := l.getScopeLoggerLevel(scope)
|
||||
logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger()
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func (l *Logger) getLogger(scope string) *zerolog.Logger {
|
||||
logger, ok := l.scopeLoggers[scope]
|
||||
if !ok || logger == nil {
|
||||
scopeLogger := l.newScopeLogger(scope)
|
||||
l.scopeLoggers[scope] = &scopeLogger
|
||||
}
|
||||
|
||||
return l.scopeLoggers[scope]
|
||||
}
|
||||
|
||||
func (l *Logger) UpdateLogLevel(configDefaultLogLevel string) {
|
||||
needUpdate := false
|
||||
|
||||
if configDefaultLogLevel != "" {
|
||||
if logLevel, ok := zerologLevels[configDefaultLogLevel]; ok {
|
||||
l.defaultLogLevelFromConfig = logLevel
|
||||
} else {
|
||||
l.l.Warn().Str("logLevel", configDefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR")
|
||||
}
|
||||
|
||||
if l.defaultLogLevelFromConfig != l.defaultLogLevel {
|
||||
needUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
l.updateLogLevel()
|
||||
|
||||
if needUpdate {
|
||||
for scope, logger := range l.scopeLoggers {
|
||||
currentLevel := logger.GetLevel()
|
||||
targetLevel := l.getScopeLoggerLevel(scope)
|
||||
if currentLevel != targetLevel {
|
||||
*logger = l.newScopeLogger(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
internal/logging/pion.go
Normal file
63
internal/logging/pion.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/pion/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type pionLogger struct {
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
// Print all messages except trace.
|
||||
func (c pionLogger) Trace(msg string) {
|
||||
c.logger.Trace().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Tracef(format string, args ...interface{}) {
|
||||
c.logger.Trace().Msgf(format, args...)
|
||||
}
|
||||
|
||||
func (c pionLogger) Debug(msg string) {
|
||||
c.logger.Debug().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Debugf(format string, args ...interface{}) {
|
||||
c.logger.Debug().Msgf(format, args...)
|
||||
}
|
||||
func (c pionLogger) Info(msg string) {
|
||||
c.logger.Info().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Infof(format string, args ...interface{}) {
|
||||
c.logger.Info().Msgf(format, args...)
|
||||
}
|
||||
func (c pionLogger) Warn(msg string) {
|
||||
c.logger.Warn().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Warnf(format string, args ...interface{}) {
|
||||
c.logger.Warn().Msgf(format, args...)
|
||||
}
|
||||
func (c pionLogger) Error(msg string) {
|
||||
c.logger.Error().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Errorf(format string, args ...interface{}) {
|
||||
c.logger.Error().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// customLoggerFactory satisfies the interface logging.LoggerFactory
|
||||
// This allows us to create different loggers per subsystem. So we can
|
||||
// add custom behavior.
|
||||
type pionLoggerFactory struct{}
|
||||
|
||||
func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger {
|
||||
logger := rootLogger.getLogger(subsystem).With().
|
||||
Str("scope", "pion").
|
||||
Str("component", subsystem).
|
||||
Logger()
|
||||
|
||||
return pionLogger{logger: &logger}
|
||||
}
|
||||
|
||||
var defaultLoggerFactory = &pionLoggerFactory{}
|
||||
|
||||
func GetPionDefaultLoggerFactory() logging.LoggerFactory {
|
||||
return defaultLoggerFactory
|
||||
}
|
||||
20
internal/logging/root.go
Normal file
20
internal/logging/root.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package logging
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
|
||||
var (
|
||||
rootZerologLogger = zerolog.New(defaultLogOutput).With().
|
||||
Str("scope", "jetkvm").
|
||||
Timestamp().
|
||||
Stack().
|
||||
Logger()
|
||||
rootLogger = NewLogger(rootZerologLogger)
|
||||
)
|
||||
|
||||
func GetRootLogger() *Logger {
|
||||
return rootLogger
|
||||
}
|
||||
|
||||
func GetSubsystemLogger(subsystem string) *zerolog.Logger {
|
||||
return rootLogger.getLogger(subsystem)
|
||||
}
|
||||
137
internal/logging/sse.go
Normal file
137
internal/logging/sse.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
//go:embed sse.html
|
||||
var sseHTML embed.FS
|
||||
|
||||
type sseEvent struct {
|
||||
Message chan string
|
||||
NewClients chan chan string
|
||||
ClosedClients chan chan string
|
||||
TotalClients map[chan string]bool
|
||||
}
|
||||
|
||||
// New event messages are broadcast to all registered client connection channels
|
||||
type sseClientChan chan string
|
||||
|
||||
var (
|
||||
sseServer *sseEvent
|
||||
sseLogger *zerolog.Logger
|
||||
)
|
||||
|
||||
func init() {
|
||||
sseServer = newSseServer()
|
||||
sseLogger = GetSubsystemLogger("sse")
|
||||
}
|
||||
|
||||
// Initialize event and Start procnteessing requests
|
||||
func newSseServer() (event *sseEvent) {
|
||||
event = &sseEvent{
|
||||
Message: make(chan string),
|
||||
NewClients: make(chan chan string),
|
||||
ClosedClients: make(chan chan string),
|
||||
TotalClients: make(map[chan string]bool),
|
||||
}
|
||||
|
||||
go event.listen()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// It Listens all incoming requests from clients.
|
||||
// Handles addition and removal of clients and broadcast messages to clients.
|
||||
func (stream *sseEvent) listen() {
|
||||
for {
|
||||
select {
|
||||
// Add new available client
|
||||
case client := <-stream.NewClients:
|
||||
stream.TotalClients[client] = true
|
||||
sseLogger.Info().
|
||||
Int("total_clients", len(stream.TotalClients)).
|
||||
Msg("new client connected")
|
||||
|
||||
// Remove closed client
|
||||
case client := <-stream.ClosedClients:
|
||||
delete(stream.TotalClients, client)
|
||||
close(client)
|
||||
sseLogger.Info().Int("total_clients", len(stream.TotalClients)).Msg("client disconnected")
|
||||
|
||||
// Broadcast message to client
|
||||
case eventMsg := <-stream.Message:
|
||||
for clientMessageChan := range stream.TotalClients {
|
||||
select {
|
||||
case clientMessageChan <- eventMsg:
|
||||
// Message sent successfully
|
||||
default:
|
||||
// Failed to send, dropping message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *sseEvent) serveHTTP() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
clientChan := make(sseClientChan)
|
||||
stream.NewClients <- clientChan
|
||||
|
||||
go func() {
|
||||
<-c.Writer.CloseNotify()
|
||||
|
||||
for range clientChan {
|
||||
}
|
||||
|
||||
stream.ClosedClients <- clientChan
|
||||
}()
|
||||
|
||||
c.Set("clientChan", clientChan)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func sseHeadersMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Method == "GET" && c.NegotiateFormat(gin.MIMEHTML) == gin.MIMEHTML {
|
||||
c.FileFromFS("/sse.html", http.FS(sseHTML))
|
||||
c.Status(http.StatusOK)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AttachSSEHandler(router *gin.RouterGroup) {
|
||||
router.StaticFS("/log-stream", http.FS(sseHTML))
|
||||
router.GET("/log-stream", sseHeadersMiddleware(), sseServer.serveHTTP(), func(c *gin.Context) {
|
||||
v, ok := c.Get("clientChan")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
clientChan, ok := v.(sseClientChan)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
if msg, ok := <-clientChan; ok {
|
||||
c.SSEvent("message", msg)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
})
|
||||
}
|
||||
319
internal/logging/sse.html
Normal file
319
internal/logging/sse.html
Normal file
@@ -0,0 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Server Sent Event</title>
|
||||
<style>
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
font-family: 'Hack', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#loading {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.log-entry > span {
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.log-entry > span:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.log-entry.log-entry-trace .log-level {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.log-entry.log-entry-debug .log-level {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.log-entry.log-entry-info .log-level {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.log-entry.log-entry-warn .log-level {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.log-entry.log-entry-error .log-level,
|
||||
.log-entry.log-entry-fatal .log-level,
|
||||
.log-entry.log-entry-panic .log-level {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.log-entry.log-entry-info .log-message,
|
||||
.log-entry.log-entry-warn .log-message,
|
||||
.log-entry.log-entry-error .log-message,
|
||||
.log-entry.log-entry-fatal .log-message,
|
||||
.log-entry.log-entry-panic .log-message {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: #666;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
font-size: 12px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.log-scope {
|
||||
font-size: 12px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.log-component {
|
||||
font-size: 12px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.log-extras {
|
||||
color: #000;
|
||||
}
|
||||
.log-extras .log-extras-header {
|
||||
font-weight: bold;
|
||||
color:cornflowerblue;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div id="header">
|
||||
<span id="loading">
|
||||
Connecting to log stream...
|
||||
</span>
|
||||
|
||||
<span id="stats">
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div id="event-data">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
class LogStream {
|
||||
constructor(url, eventDataElement, loadingElement, statsElement) {
|
||||
this.url = url;
|
||||
this.eventDataElement = eventDataElement;
|
||||
this.loadingElement = loadingElement;
|
||||
this.statsElement = statsElement;
|
||||
this.stream = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 10;
|
||||
this.reconnectDelay = 1000; // Start with 1 second
|
||||
this.maxReconnectDelay = 30000; // Max 30 seconds
|
||||
this.isConnecting = false;
|
||||
|
||||
this.totalMessages = 0;
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.isConnecting) return;
|
||||
this.isConnecting = true;
|
||||
|
||||
this.loadingElement.innerText = "Connecting to log stream...";
|
||||
|
||||
this.stream = new EventSource(this.url);
|
||||
|
||||
this.stream.onopen = () => {
|
||||
this.isConnecting = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.reconnectDelay = 1000;
|
||||
this.loadingElement.innerText = "Log stream connected.";
|
||||
|
||||
|
||||
this.totalMessages = 0;
|
||||
this.totalBytes = 0;
|
||||
};
|
||||
|
||||
this.stream.onmessage = (event) => {
|
||||
this.totalBytes += event.data.length;
|
||||
this.totalMessages++;
|
||||
|
||||
const data = JSON.parse(event.data);
|
||||
this.addLogEntry(data);
|
||||
this.updateStats();
|
||||
};
|
||||
|
||||
this.stream.onerror = () => {
|
||||
this.isConnecting = false;
|
||||
this.loadingElement.innerText = "Log stream disconnected.";
|
||||
this.stream.close();
|
||||
this.handleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
this.statsElement.innerHTML = `Messages: <strong>${this.totalMessages}</strong>, Bytes: <strong>${this.totalBytes}</strong> `;
|
||||
}
|
||||
|
||||
handleReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this.loadingElement.innerText = "Failed to reconnect after multiple attempts";
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 1, this.maxReconnectDelay);
|
||||
|
||||
this.loadingElement.innerText = `Reconnecting in ${this.reconnectDelay/1000} seconds... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`;
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
|
||||
addLogEntry(data) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "log-entry log-entry-" + data.level;
|
||||
|
||||
const timestamp = document.createElement("span");
|
||||
timestamp.className = "log-timestamp";
|
||||
timestamp.innerText = data.time;
|
||||
el.appendChild(timestamp);
|
||||
|
||||
const level = document.createElement("span");
|
||||
level.className = "log-level";
|
||||
level.innerText = this.shortLogLevel(data.level);
|
||||
el.appendChild(level);
|
||||
|
||||
const scope = document.createElement("span");
|
||||
scope.className = "log-scope";
|
||||
scope.innerText = data.scope;
|
||||
el.appendChild(scope);
|
||||
|
||||
const component = document.createElement("span");
|
||||
component.className = "log-component";
|
||||
component.innerText = data.component;
|
||||
el.appendChild(component);
|
||||
|
||||
const message = document.createElement("span");
|
||||
message.className = "log-message";
|
||||
message.innerText = data.message;
|
||||
el.appendChild(message);
|
||||
|
||||
this.addLogExtras(el, data);
|
||||
|
||||
this.eventDataElement.appendChild(el);
|
||||
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
shortLogLevel(level) {
|
||||
switch (level) {
|
||||
case "trace":
|
||||
return "TRC";
|
||||
case "debug":
|
||||
return "DBG";
|
||||
case "info":
|
||||
return "INF";
|
||||
case "warn":
|
||||
return "WRN";
|
||||
case "error":
|
||||
return "ERR";
|
||||
case "fatal":
|
||||
return "FTL";
|
||||
case "panic":
|
||||
return "PNC";
|
||||
default:
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
addLogExtras(el, data) {
|
||||
const excludeKeys = [
|
||||
"timestamp",
|
||||
"time",
|
||||
"level",
|
||||
"scope",
|
||||
"component",
|
||||
"message",
|
||||
];
|
||||
|
||||
const extras = {};
|
||||
for (const key in data) {
|
||||
if (excludeKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
extras[key] = data[key];
|
||||
}
|
||||
|
||||
for (const key in extras) {
|
||||
const extra = document.createElement("span");
|
||||
extra.className = "log-extras log-extras-" + key;
|
||||
|
||||
const extraKey = document.createElement("span");
|
||||
extraKey.className = "log-extras-header";
|
||||
extraKey.innerText = key + '=';
|
||||
extra.appendChild(extraKey);
|
||||
|
||||
const extraValue = document.createElement("span");
|
||||
extraValue.className = "log-extras-value";
|
||||
|
||||
let value = extras[key];
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
extraValue.innerText = value;
|
||||
extra.appendChild(extraValue);
|
||||
|
||||
el.appendChild(extra);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.stream) {
|
||||
this.stream.close();
|
||||
this.stream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the log stream when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const logStream = new LogStream(
|
||||
"/developer/log-stream",
|
||||
document.getElementById("event-data"),
|
||||
document.getElementById("loading"),
|
||||
document.getElementById("stats"),
|
||||
);
|
||||
|
||||
// Clean up when the page is unloaded
|
||||
window.addEventListener('beforeunload', () => {
|
||||
logStream.disconnect();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</html>
|
||||
32
internal/logging/utils.go
Normal file
32
internal/logging/utils.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
|
||||
|
||||
func GetDefaultLogger() *zerolog.Logger {
|
||||
return &defaultLogger
|
||||
}
|
||||
|
||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error {
|
||||
// TODO: move rootLogger to logging package
|
||||
if l == nil {
|
||||
l = &defaultLogger
|
||||
}
|
||||
|
||||
l.Error().Err(err).Msgf(format, args...)
|
||||
|
||||
if err == nil {
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
|
||||
err_msg := err.Error() + ": %v"
|
||||
err_args := append(args, err)
|
||||
|
||||
return fmt.Errorf(err_msg, err_args...)
|
||||
}
|
||||
Reference in New Issue
Block a user