mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
feat(logs): by default, log server activity to a file.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,3 +14,6 @@ erupe-ce
|
||||
*.bin
|
||||
savedata/*/
|
||||
config.json
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
|
||||
19
README.md
19
README.md
@@ -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:
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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
77
main.go
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user