mirror of
https://github.com/jetkvm/kvm.git
synced 2026-05-21 05:20:35 +00:00
2614db6b89
* feat(ui): add log level selector in Troubleshooting Mode Add a UI dropdown in Advanced > Troubleshooting Mode that lets users set the system log verbosity (Error, Warning, Info, Debug, Trace). Changes take effect immediately without restart via new getDefaultLogLevel/setDefaultLogLevel JSON-RPC endpoints. Also downgrades the noisy wakeup_on_write permission denied warning from Warn to Debug level, and removes the INFO→WARN config migration so users can actually select INFO. Localized for all 14 languages. * chore(ui): disable no-floating-promises in oxlint The rule is not actionable for the current codebase; turn it off explicitly. * refactor(ui): drop void prefixes on JSON-RPC send in advanced settings no-floating-promises is disabled in oxlint; match the rest of the codebase. * fix(ui): localize log level dropdown and fix optimistic update - Replace hardcoded English dropdown labels with localized m.*() calls - Replace hardcoded error string with m.advanced_error_set_log_level() - Optimistically update dropdown on change and revert on RPC failure - Add 6 new i18n keys across all 14 locales * chore: add remote-agent to .gitignore and auto-sort i18n in pre-commit - Ignore the compiled e2e/remote-agent/remote-agent binary - Add lint-staged rule to run i18n:resort on message JSON changes * copy(ui): improve log level setting description Apply outcome-oriented copy: explain what the setting does for the user and when to change it, rather than restating the control's mechanics. Updated across all 14 locales. * fix(logging): scope loggers not rebuilt when config level matches base default UpdateLogLevel compared the new config level against the base default (ErrorLevel) instead of the previous config level. When switching from WARN back to ERROR, the comparison was equal so scope loggers kept their old WarnLevel filter — WRN messages continued appearing despite the user selecting Error. Compare against the previous defaultLogLevelFromConfig instead. * test(logging): add RPC probe for log level filtering Add a dedicated emitTestLog JSON-RPC method and a focused e2e spec that verifies live TRACE/DEBUG/INFO/WARN/ERROR filtering against last.log. * chore(ui): update .gitignore to exclude screenshot.png file
212 lines
4.9 KiB
Go
212 lines
4.9 KiB
Go
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 any, 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,
|
|
}
|
|
|
|
// subsystemDefaultLevels defines default log levels for specific subsystems
|
|
// that should always log at a certain level regardless of the global default.
|
|
subsystemDefaultLevels = map[string]zerolog.Level{
|
|
"diagnostics": zerolog.InfoLevel,
|
|
"supervisor": zerolog.InfoLevel,
|
|
}
|
|
)
|
|
|
|
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.SplitSeq(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()
|
|
}
|
|
|
|
scopeLevel := l.defaultLogLevel
|
|
if l.defaultLogLevelFromConfig != -2 {
|
|
scopeLevel = l.defaultLogLevelFromConfig
|
|
}
|
|
if l.defaultLogLevelFromEnv != -2 {
|
|
scopeLevel = l.defaultLogLevelFromEnv
|
|
}
|
|
|
|
// Check if this subsystem has a specific default level
|
|
if subsystemLevel, ok := subsystemDefaultLevels[scope]; ok {
|
|
// Use the more verbose level (lower value = more verbose)
|
|
if subsystemLevel < scopeLevel {
|
|
scopeLevel = subsystemLevel
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
if l.defaultLogLevelFromConfig != logLevel {
|
|
needUpdate = true
|
|
}
|
|
l.defaultLogLevelFromConfig = logLevel
|
|
} else {
|
|
l.l.Warn().Str("logLevel", configDefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR")
|
|
}
|
|
}
|
|
|
|
l.updateLogLevel()
|
|
|
|
if needUpdate {
|
|
for scope, logger := range l.scopeLoggers {
|
|
currentLevel := logger.GetLevel()
|
|
targetLevel := l.getScopeLoggerLevel(scope)
|
|
if currentLevel != targetLevel {
|
|
*logger = l.newScopeLogger(scope)
|
|
}
|
|
}
|
|
}
|
|
}
|