feat(logs): by default, log server activity to a file.

This commit is contained in:
Houmgaor
2025-11-18 00:18:32 +01:00
parent 7aafc71dcc
commit 9a47a876eb
12 changed files with 132 additions and 16 deletions

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@ erupe-ce
*.bin
savedata/*/
config.json
# Logs
logs/

View File

@@ -223,6 +223,25 @@ Channel servers are configured under `Entrance.Entries[].Channels[]` with indivi
}
```
### Logging
Erupe supports automatic file-based logging with rotation for production environments:
```json
{
"Logging": {
"LogToFile": true, // Enable file logging (default: true)
"LogFilePath": "logs/erupe.log", // Log file path (default: "logs/erupe.log")
"LogMaxSize": 100, // Max file size in MB before rotation (default: 100)
"LogMaxBackups": 3, // Number of old log files to keep (default: 3)
"LogMaxAge": 28, // Max days to retain old logs (default: 28)
"LogCompress": true // Compress rotated logs (default: true)
}
}
```
Logs are written to both console and file simultaneously. The log analyzer tool in `tools/loganalyzer/` provides commands to filter, analyze errors, track connections, and generate statistics from log files.
### In-game Commands
Configure available commands and their prefixes:

View File

@@ -42,6 +42,14 @@
"BonusQuestAllowance": 3,
"DailyQuestAllowance": 1
},
"Logging": {
"LogToFile": true,
"LogFilePath": "logs/erupe.log",
"LogMaxSize": 100,
"LogMaxBackups": 3,
"LogMaxAge": 28,
"LogCompress": true
},
"Discord": {
"Enabled": false,
"BotToken": "",

View File

@@ -27,6 +27,7 @@ type Config struct {
DevModeOptions DevModeOptions
GameplayOptions GameplayOptions
Logging Logging
Discord Discord
Commands []Command
Courses []Course
@@ -73,6 +74,16 @@ type GameplayOptions struct {
DailyQuestAllowance uint32 // Number of Daily Quests to allow daily
}
// Logging holds the logging configuration.
type Logging struct {
LogToFile bool // Enable/disable file logging (default: true)
LogFilePath string // File path for logs (default: "logs/erupe.log")
LogMaxSize int // Max size in MB before rotation (default: 100)
LogMaxBackups int // Number of old log files to keep (default: 3)
LogMaxAge int // Max days to retain old logs (default: 28)
LogCompress bool // Compress rotated logs (default: true)
}
// Discord holds the discord integration config.
type Discord struct {
Enabled bool
@@ -197,6 +208,15 @@ func LoadConfig() (*Config, error) {
OutputDir: "savedata",
})
viper.SetDefault("Logging", Logging{
LogToFile: true,
LogFilePath: "logs/erupe.log",
LogMaxSize: 100,
LogMaxBackups: 3,
LogMaxAge: 28,
LogCompress: true,
})
err := viper.ReadInConfig()
if err != nil {
return nil, err

1
go.mod
View File

@@ -13,6 +13,7 @@ require (
golang.org/x/crypto v0.1.0
golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f
golang.org/x/text v0.7.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (

2
go.sum
View File

@@ -607,6 +607,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

77
main.go
View File

@@ -5,6 +5,7 @@ import (
"net"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"syscall"
"time"
@@ -19,6 +20,8 @@ import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
// Temporary DB auto clean on startup for quick development & testing.
@@ -41,16 +44,76 @@ var Commit = func() string {
return "unknown"
}
// initLogger initializes the zap logger with file logging support
func initLogger(cfg *config.Config) *zap.Logger {
var zapConfig zap.Config
// Base configuration based on DevMode
if cfg.DevMode {
zapConfig = zap.NewDevelopmentConfig()
} else {
zapConfig = zap.NewProductionConfig()
}
// Configure output paths
if cfg.Logging.LogToFile {
// Ensure the log directory exists
logDir := filepath.Dir(cfg.Logging.LogFilePath)
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Printf("Failed to create log directory: %s\n", err)
os.Exit(1)
}
// Create lumberjack logger for file rotation
fileWriter := &lumberjack.Logger{
Filename: cfg.Logging.LogFilePath,
MaxSize: cfg.Logging.LogMaxSize,
MaxBackups: cfg.Logging.LogMaxBackups,
MaxAge: cfg.Logging.LogMaxAge,
Compress: cfg.Logging.LogCompress,
}
// Create encoder
encoderConfig := zapConfig.EncoderConfig
var encoder zapcore.Encoder
if cfg.DevMode {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
} else {
encoder = zapcore.NewJSONEncoder(encoderConfig)
}
// Create cores for both console and file output
consoleCore := zapcore.NewCore(
encoder,
zapcore.AddSync(os.Stderr),
zapConfig.Level,
)
fileCore := zapcore.NewCore(
encoder,
zapcore.AddSync(fileWriter),
zapConfig.Level,
)
// Combine cores
core := zapcore.NewTee(consoleCore, fileCore)
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
return logger
}
// File logging disabled, use default configuration
logger, err := zapConfig.Build()
if err != nil {
fmt.Printf("Failed to initialize logger: %s\n", err)
os.Exit(1)
}
return logger
}
func main() {
var err error
var zapLogger *zap.Logger
if config.ErupeConfig.DevMode {
zapLogger, _ = zap.NewDevelopment()
} else {
zapLogger, _ = zap.NewProduction()
}
// Initialize logger with file support
zapLogger := initLogger(config.ErupeConfig)
defer zapLogger.Sync()
logger := zapLogger.Named("main")

View File

@@ -54,7 +54,7 @@ type ConnectionStats struct {
// - Provides verbose session details including objects and stage changes
//
// Options:
// - f: Path to log file (default: "erupe.log")
// - f: Path to log file (default: "logs/erupe.log")
// - player: Filter sessions by player name (case-insensitive substring match)
// - sessions: Show individual player sessions
// - stats: Show connection statistics (default: true)
@@ -67,7 +67,7 @@ type ConnectionStats struct {
func runConnections(args []string) {
fs := flag.NewFlagSet("connections", flag.ExitOnError)
logFile := fs.String("f", "erupe.log", "Path to log file")
logFile := fs.String("f", "logs/erupe.log", "Path to log file")
player := fs.String("player", "", "Filter by player name")
showSessions := fs.Bool("sessions", false, "Show individual player sessions")
showStats := fs.Bool("stats", true, "Show connection statistics")

View File

@@ -36,7 +36,7 @@ type ErrorGroup struct {
// - Tracks which callers produced each error
//
// Options:
// - f: Path to log file (default: "erupe.log")
// - f: Path to log file (default: "logs/erupe.log")
// - group: Group errors by "message", "caller", or "logger" (default: "message")
// - stack: Show stack traces in detailed view
// - limit: Maximum number of example entries per group (default: 10)
@@ -50,7 +50,7 @@ type ErrorGroup struct {
func runErrors(args []string) {
fs := flag.NewFlagSet("errors", flag.ExitOnError)
logFile := fs.String("f", "erupe.log", "Path to log file")
logFile := fs.String("f", "logs/erupe.log", "Path to log file")
groupBy := fs.String("group", "message", "Group errors by: message, caller, or logger")
showStack := fs.Bool("stack", false, "Show stack traces")
limit := fs.Int("limit", 10, "Limit number of examples per error group")

View File

@@ -29,7 +29,7 @@ import (
func runFilter(args []string) {
fs := flag.NewFlagSet("filter", flag.ExitOnError)
logFile := fs.String("f", "erupe.log", "Path to log file")
logFile := fs.String("f", "logs/erupe.log", "Path to log file")
level := fs.String("level", "", "Filter by log level (info, warn, error, fatal)")
logger := fs.String("logger", "", "Filter by logger name (supports wildcards)")
message := fs.String("msg", "", "Filter by message content (case-insensitive)")

View File

@@ -44,7 +44,7 @@ type LogStats struct {
// patterns, peak usage times, and potential issues.
//
// Options:
// - f: Path to log file (default: "erupe.log")
// - f: Path to log file (default: "logs/erupe.log")
// - top: Number of top items to show in detailed view (default: 10)
// - detailed: Show detailed statistics including temporal patterns and top messages
//
@@ -55,7 +55,7 @@ type LogStats struct {
func runStats(args []string) {
fs := flag.NewFlagSet("stats", flag.ExitOnError)
logFile := fs.String("f", "erupe.log", "Path to log file")
logFile := fs.String("f", "logs/erupe.log", "Path to log file")
topN := fs.Int("top", 10, "Show top N messages/loggers")
detailed := fs.Bool("detailed", false, "Show detailed statistics")

View File

@@ -21,7 +21,7 @@ import (
// Both phases support filtering by log level and colorized output.
//
// Options:
// - f: Path to log file (default: "erupe.log")
// - f: Path to log file (default: "logs/erupe.log")
// - n: Number of initial lines to show (default: 10)
// - follow: Whether to continue following the file (default: true)
// - level: Filter by log level (info, warn, error, fatal)
@@ -37,7 +37,7 @@ import (
func runTail(args []string) {
fs := flag.NewFlagSet("tail", flag.ExitOnError)
logFile := fs.String("f", "erupe.log", "Path to log file")
logFile := fs.String("f", "logs/erupe.log", "Path to log file")
lines := fs.Int("n", 10, "Number of initial lines to show")
follow := fs.Bool("follow", true, "Follow the log file (like tail -f)")
level := fs.String("level", "", "Filter by log level")