Files
xboard-node/internal/nlog/nlog.go
T

324 lines
7.4 KiB
Go

package nlog
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"strings"
"sync"
"time"
)
// ANSI color codes
const (
ColorReset = "\033[0m"
ColorGray = "\033[90m" // 时间戳
ColorWhite = "\033[37m" // INFO
ColorYellow = "\033[33m" // WARN
ColorRed = "\033[31m" // ERROR
ColorCyan = "\033[36m" // 节点前缀
ColorGreen = "\033[32m" // 调试信息
ColorBlue = "\033[34m" // core 前缀
)
// NodeLog provides structured logging with node context.
// Format: LEVEL [protocol:port] message
type NodeLog struct {
prefix string // e.g., "shadowsocks:10005" or "trojan:10033"
}
// Global logger state
var (
mu sync.RWMutex
defaultNode string
nodeLoggers = make(map[string]*NodeLog)
logMu sync.RWMutex
logWriter io.Writer = os.Stdout
logMin = slog.LevelInfo
logColor = true
)
// Init configures process-wide log output (called from config.InitLogger).
// w must be non-nil. minLevel drops messages below that severity.
// color enables ANSI coloring when true (typically TTY + stdout/stderr).
func Init(w io.Writer, minLevel slog.Level, color bool) {
if w == nil {
w = os.Stdout
}
logMu.Lock()
defer logMu.Unlock()
logWriter = w
logMin = minLevel
logColor = color
}
// formatMsg appends alternating key-value pairs like slog: ("k", v, "k2", v2).
func formatMsg(msg string, args []any) string {
if len(args) == 0 {
return msg
}
var b strings.Builder
b.WriteString(msg)
for i := 0; i < len(args); i += 2 {
if i+1 < len(args) {
fmt.Fprintf(&b, " %v=%v", args[i], args[i+1])
} else {
fmt.Fprintf(&b, " %v", args[i])
}
}
return b.String()
}
// SetDefault sets the default node prefix for core-level logs.
func SetDefault(prefix string) {
mu.Lock()
defaultNode = prefix
mu.Unlock()
}
// ForNode returns a NodeLog for the given protocol and port.
func ForNode(protocol string, port int) *NodeLog {
key := fmt.Sprintf("%s:%d", normalizeProto(protocol), port)
mu.RLock()
if nl, ok := nodeLoggers[key]; ok {
mu.RUnlock()
return nl
}
mu.RUnlock()
mu.Lock()
defer mu.Unlock()
// Double check
if nl, ok := nodeLoggers[key]; ok {
return nl
}
nl := &NodeLog{prefix: key}
nodeLoggers[key] = nl
return nl
}
// Core returns a NodeLog for core/system messages.
func Core() *NodeLog {
mu.RLock()
if nl, ok := nodeLoggers["core"]; ok {
mu.RUnlock()
return nl
}
mu.RUnlock()
mu.Lock()
defer mu.Unlock()
if nl, ok := nodeLoggers["core"]; ok {
return nl
}
nl := &NodeLog{prefix: "core"}
nodeLoggers["core"] = nl
return nl
}
// normalizeProto returns the full protocol name for display.
func normalizeProto(p string) string {
switch strings.ToLower(p) {
case "shadowsocks", "ss":
return "shadowsocks"
case "vmess":
return "vmess"
case "vless":
return "vless"
case "trojan":
return "trojan"
case "hysteria2", "hysteria":
return "hysteria2"
case "tuic":
return "tuic"
case "anytls":
return "anytls"
case "naive":
return "naive"
case "http":
return "http"
case "socks":
return "socks"
default:
return p
}
}
// ─── Logging Methods ────────────────────────────────────────────────────────
func (nl *NodeLog) Debug(msg string, args ...any) {
logWithColor(slog.LevelDebug, nl.prefix, msg, args...)
}
func (nl *NodeLog) Info(msg string, args ...any) {
logWithColor(slog.LevelInfo, nl.prefix, msg, args...)
}
func (nl *NodeLog) Warn(msg string, args ...any) {
logWithColor(slog.LevelWarn, nl.prefix, msg, args...)
}
func (nl *NodeLog) Error(msg string, args ...any) {
logWithColor(slog.LevelError, nl.prefix, msg, args...)
}
// logWithColor writes one line to the configured writer (see Init).
// Format: HH:MM:SS.mmm LEVEL [prefix] message [key=value ...]
func logWithColor(level slog.Level, prefix, msg string, args ...any) {
logMu.RLock()
out := logWriter
min := logMin
color := logColor
logMu.RUnlock()
if level < min {
return
}
now := time.Now().Format("15:04:05.000")
fullMsg := formatMsg(msg, args)
var levelStr string
switch level {
case slog.LevelDebug:
levelStr = "DEBUG"
case slog.LevelInfo:
levelStr = "INFO "
case slog.LevelWarn:
levelStr = "WARN "
case slog.LevelError:
levelStr = "ERROR"
default:
levelStr = "?????"
}
if !color {
fmt.Fprintf(out, "%s %s [%s] %s\n", now, strings.TrimSpace(levelStr), prefix, fullMsg)
return
}
var levelColor string
switch level {
case slog.LevelDebug:
levelColor = ColorGreen
case slog.LevelInfo:
levelColor = ColorWhite
case slog.LevelWarn:
levelColor = ColorYellow
case slog.LevelError:
levelColor = ColorRed
default:
levelColor = ColorWhite
}
prefixColor := ColorCyan
if prefix == "core" {
prefixColor = ColorBlue
}
fmt.Fprintf(out, "%s%s%s %s%s%s %s[%s]%s %s\n",
ColorGray, now, ColorReset,
levelColor, levelStr, ColorReset,
prefixColor, prefix, ColorReset,
fullMsg,
)
}
// ─── Startup Summary ────────────────────────────────────────────────────────
// StartupSummary logs a condensed startup summary.
type StartupSummary struct {
mu sync.Mutex
nodes []nodeInfo
}
type nodeInfo struct {
Protocol string
Port int
Users int
}
func NewStartupSummary() *StartupSummary {
return &StartupSummary{}
}
func (s *StartupSummary) AddNode(protocol string, port, users int) {
s.mu.Lock()
s.nodes = append(s.nodes, nodeInfo{protocol, port, users})
s.mu.Unlock()
}
func (s *StartupSummary) Print() {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.nodes) == 0 {
Core().Warn("no nodes configured")
return
}
// Group by protocol
byProto := make(map[string][]nodeInfo)
for _, n := range s.nodes {
proto := normalizeProto(n.Protocol)
byProto[proto] = append(byProto[proto], n)
}
// Build summary line
parts := make([]string, 0, len(byProto))
for proto, nodes := range byProto {
ports := make([]string, 0, len(nodes))
for _, n := range nodes {
ports = append(ports, fmt.Sprintf("%d", n.Port))
}
parts = append(parts, fmt.Sprintf("%s:%s", proto, strings.Join(ports, ",")))
}
Core().Info(fmt.Sprintf("started %d nodes: %s", len(s.nodes), strings.Join(parts, " | ")))
}
// ─── Helpers ────────────────────────────────────────────────────────────────
// ConfigUpdated logs a config update event.
func ConfigUpdated(nl *NodeLog, users int) {
nl.Info(fmt.Sprintf("config updated, %d users", users))
}
// HotReload logs a hot-reload event (no restart).
func HotReload(nl *NodeLog, what string) {
nl.Debug(fmt.Sprintf("hot-reload: %s", what))
}
// FullRestart logs a full kernel restart.
func FullRestart(nl *NodeLog, reason string) {
nl.Info(fmt.Sprintf("kernel restart: %s", reason))
}
// ReportPushed logs a report push event.
func ReportPushed(users, online int) {
Core().Info(fmt.Sprintf("report pushed: %d users, %d online", users, online))
}
// TrackerStats logs tracker statistics.
func TrackerStats(conns, users int) {
Core().Debug(fmt.Sprintf("tracker: %d conns, %d users online", conns, users))
}
// Context-aware logging
type ctxKey struct{}
func WithNode(ctx context.Context, nl *NodeLog) context.Context {
return context.WithValue(ctx, ctxKey{}, nl)
}
func FromContext(ctx context.Context) *NodeLog {
if nl, ok := ctx.Value(ctxKey{}).(*NodeLog); ok {
return nl
}
return Core()
}