171 lines
3.1 KiB
Go
171 lines
3.1 KiB
Go
// Package applog is the process-wide logger for HA Emby MCP.
|
|
package applog
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type Level int
|
|
|
|
const (
|
|
LevelDebug Level = iota
|
|
LevelInfo
|
|
LevelWarn
|
|
)
|
|
|
|
type Settings struct {
|
|
Level Level
|
|
DebugREST bool
|
|
DebugMCP bool
|
|
DebugEmby bool
|
|
}
|
|
|
|
var (
|
|
mu sync.RWMutex
|
|
cur = Settings{Level: LevelInfo}
|
|
std = log.New(os.Stderr, "", log.LstdFlags)
|
|
)
|
|
|
|
func Configure(s Settings) {
|
|
mu.Lock()
|
|
cur = s
|
|
mu.Unlock()
|
|
Infof("log level=%s debug[rest=%v mcp=%v emby=%v]", s.Level, s.DebugREST, s.DebugMCP, s.DebugEmby)
|
|
}
|
|
|
|
func Current() Settings {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return cur
|
|
}
|
|
|
|
func ParseLevel(s string) Level {
|
|
switch strings.ToUpper(strings.TrimSpace(s)) {
|
|
case "DEBUG":
|
|
return LevelDebug
|
|
case "WARN", "WARNING":
|
|
return LevelWarn
|
|
default:
|
|
return LevelInfo
|
|
}
|
|
}
|
|
|
|
func (l Level) String() string {
|
|
switch l {
|
|
case LevelDebug:
|
|
return "DEBUG"
|
|
case LevelWarn:
|
|
return "WARN"
|
|
default:
|
|
return "INFO"
|
|
}
|
|
}
|
|
|
|
func enabled(min Level) bool {
|
|
return Current().Level <= min
|
|
}
|
|
|
|
func Infof(format string, args ...any) {
|
|
if enabled(LevelInfo) {
|
|
std.Printf("INFO "+format, args...)
|
|
}
|
|
}
|
|
|
|
func Warnf(format string, args ...any) {
|
|
if enabled(LevelWarn) {
|
|
std.Printf("WARN "+format, args...)
|
|
}
|
|
}
|
|
|
|
func Debugf(format string, args ...any) {
|
|
if enabled(LevelDebug) {
|
|
std.Printf("DEBUG "+format, args...)
|
|
}
|
|
}
|
|
|
|
func REST(format string, args ...any) {
|
|
s := Current()
|
|
if s.Level == LevelDebug && s.DebugREST {
|
|
std.Printf("DEBUG rest "+format, args...)
|
|
}
|
|
}
|
|
|
|
func MCP(format string, args ...any) {
|
|
s := Current()
|
|
if s.Level == LevelDebug && s.DebugMCP {
|
|
std.Printf("DEBUG mcp "+format, args...)
|
|
}
|
|
}
|
|
|
|
func Emby(format string, args ...any) {
|
|
s := Current()
|
|
if s.Level == LevelDebug && s.DebugEmby {
|
|
std.Printf("DEBUG emby "+format, args...)
|
|
}
|
|
}
|
|
|
|
// Body returns a log-safe body: truncated and with secrets stripped.
|
|
func Body(b []byte) string {
|
|
s := string(b)
|
|
s = redact(s)
|
|
const max = 8192
|
|
if len(s) > max {
|
|
return s[:max] + "…(truncated)"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func redact(s string) string {
|
|
// crude redaction of common secret fields
|
|
lower := strings.ToLower(s)
|
|
if strings.Contains(lower, "\"pw\"") || strings.Contains(lower, "password") || strings.Contains(lower, "accesstoken") {
|
|
s = replaceJSONField(s, "Pw")
|
|
s = replaceJSONField(s, "pw")
|
|
s = replaceJSONField(s, "Password")
|
|
s = replaceJSONField(s, "password")
|
|
s = replaceJSONField(s, "AccessToken")
|
|
s = replaceJSONField(s, "accessToken")
|
|
}
|
|
return s
|
|
}
|
|
|
|
func replaceJSONField(s, key string) string {
|
|
// "key":"value" → "key":"***"
|
|
needle := `"` + key + `"`
|
|
out := s
|
|
for {
|
|
i := strings.Index(out, needle)
|
|
if i < 0 {
|
|
return out
|
|
}
|
|
rest := out[i+len(needle):]
|
|
colon := strings.Index(rest, ":")
|
|
if colon < 0 {
|
|
return out
|
|
}
|
|
rest = rest[colon+1:]
|
|
j := 0
|
|
for j < len(rest) && (rest[j] == ' ' || rest[j] == '\t' || rest[j] == '\n') {
|
|
j++
|
|
}
|
|
if j >= len(rest) || rest[j] != '"' {
|
|
return out
|
|
}
|
|
k := j + 1
|
|
for k < len(rest) && rest[k] != '"' {
|
|
if rest[k] == '\\' && k+1 < len(rest) {
|
|
k += 2
|
|
continue
|
|
}
|
|
k++
|
|
}
|
|
if k >= len(rest) {
|
|
return out
|
|
}
|
|
out = out[:i+len(needle)] + rest[:j] + `"***"` + rest[k+1:]
|
|
}
|
|
}
|