324 lines
7.4 KiB
Go
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()
|
|
}
|