mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 17:43:21 +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
|
*.bin
|
||||||
savedata/*/
|
savedata/*/
|
||||||
config.json
|
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
|
### In-game Commands
|
||||||
|
|
||||||
Configure available commands and their prefixes:
|
Configure available commands and their prefixes:
|
||||||
|
|||||||
@@ -42,6 +42,14 @@
|
|||||||
"BonusQuestAllowance": 3,
|
"BonusQuestAllowance": 3,
|
||||||
"DailyQuestAllowance": 1
|
"DailyQuestAllowance": 1
|
||||||
},
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogToFile": true,
|
||||||
|
"LogFilePath": "logs/erupe.log",
|
||||||
|
"LogMaxSize": 100,
|
||||||
|
"LogMaxBackups": 3,
|
||||||
|
"LogMaxAge": 28,
|
||||||
|
"LogCompress": true
|
||||||
|
},
|
||||||
"Discord": {
|
"Discord": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"BotToken": "",
|
"BotToken": "",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Config struct {
|
|||||||
|
|
||||||
DevModeOptions DevModeOptions
|
DevModeOptions DevModeOptions
|
||||||
GameplayOptions GameplayOptions
|
GameplayOptions GameplayOptions
|
||||||
|
Logging Logging
|
||||||
Discord Discord
|
Discord Discord
|
||||||
Commands []Command
|
Commands []Command
|
||||||
Courses []Course
|
Courses []Course
|
||||||
@@ -73,6 +74,16 @@ type GameplayOptions struct {
|
|||||||
DailyQuestAllowance uint32 // Number of Daily Quests to allow daily
|
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.
|
// Discord holds the discord integration config.
|
||||||
type Discord struct {
|
type Discord struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
@@ -197,6 +208,15 @@ func LoadConfig() (*Config, error) {
|
|||||||
OutputDir: "savedata",
|
OutputDir: "savedata",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
viper.SetDefault("Logging", Logging{
|
||||||
|
LogToFile: true,
|
||||||
|
LogFilePath: "logs/erupe.log",
|
||||||
|
LogMaxSize: 100,
|
||||||
|
LogMaxBackups: 3,
|
||||||
|
LogMaxAge: 28,
|
||||||
|
LogCompress: true,
|
||||||
|
})
|
||||||
|
|
||||||
err := viper.ReadInConfig()
|
err := viper.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -13,6 +13,7 @@ require (
|
|||||||
golang.org/x/crypto v0.1.0
|
golang.org/x/crypto v0.1.0
|
||||||
golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f
|
golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f
|
||||||
golang.org/x/text v0.7.0
|
golang.org/x/text v0.7.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/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 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
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.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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/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"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,6 +20,8 @@ import (
|
|||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Temporary DB auto clean on startup for quick development & testing.
|
// Temporary DB auto clean on startup for quick development & testing.
|
||||||
@@ -41,16 +44,76 @@ var Commit = func() string {
|
|||||||
return "unknown"
|
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() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var zapLogger *zap.Logger
|
// Initialize logger with file support
|
||||||
if config.ErupeConfig.DevMode {
|
zapLogger := initLogger(config.ErupeConfig)
|
||||||
zapLogger, _ = zap.NewDevelopment()
|
|
||||||
} else {
|
|
||||||
zapLogger, _ = zap.NewProduction()
|
|
||||||
}
|
|
||||||
|
|
||||||
defer zapLogger.Sync()
|
defer zapLogger.Sync()
|
||||||
logger := zapLogger.Named("main")
|
logger := zapLogger.Named("main")
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ type ConnectionStats struct {
|
|||||||
// - Provides verbose session details including objects and stage changes
|
// - Provides verbose session details including objects and stage changes
|
||||||
//
|
//
|
||||||
// Options:
|
// 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)
|
// - player: Filter sessions by player name (case-insensitive substring match)
|
||||||
// - sessions: Show individual player sessions
|
// - sessions: Show individual player sessions
|
||||||
// - stats: Show connection statistics (default: true)
|
// - stats: Show connection statistics (default: true)
|
||||||
@@ -67,7 +67,7 @@ type ConnectionStats struct {
|
|||||||
func runConnections(args []string) {
|
func runConnections(args []string) {
|
||||||
fs := flag.NewFlagSet("connections", flag.ExitOnError)
|
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")
|
player := fs.String("player", "", "Filter by player name")
|
||||||
showSessions := fs.Bool("sessions", false, "Show individual player sessions")
|
showSessions := fs.Bool("sessions", false, "Show individual player sessions")
|
||||||
showStats := fs.Bool("stats", true, "Show connection statistics")
|
showStats := fs.Bool("stats", true, "Show connection statistics")
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type ErrorGroup struct {
|
|||||||
// - Tracks which callers produced each error
|
// - Tracks which callers produced each error
|
||||||
//
|
//
|
||||||
// Options:
|
// 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")
|
// - group: Group errors by "message", "caller", or "logger" (default: "message")
|
||||||
// - stack: Show stack traces in detailed view
|
// - stack: Show stack traces in detailed view
|
||||||
// - limit: Maximum number of example entries per group (default: 10)
|
// - limit: Maximum number of example entries per group (default: 10)
|
||||||
@@ -50,7 +50,7 @@ type ErrorGroup struct {
|
|||||||
func runErrors(args []string) {
|
func runErrors(args []string) {
|
||||||
fs := flag.NewFlagSet("errors", flag.ExitOnError)
|
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")
|
groupBy := fs.String("group", "message", "Group errors by: message, caller, or logger")
|
||||||
showStack := fs.Bool("stack", false, "Show stack traces")
|
showStack := fs.Bool("stack", false, "Show stack traces")
|
||||||
limit := fs.Int("limit", 10, "Limit number of examples per error group")
|
limit := fs.Int("limit", 10, "Limit number of examples per error group")
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
func runFilter(args []string) {
|
func runFilter(args []string) {
|
||||||
fs := flag.NewFlagSet("filter", flag.ExitOnError)
|
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)")
|
level := fs.String("level", "", "Filter by log level (info, warn, error, fatal)")
|
||||||
logger := fs.String("logger", "", "Filter by logger name (supports wildcards)")
|
logger := fs.String("logger", "", "Filter by logger name (supports wildcards)")
|
||||||
message := fs.String("msg", "", "Filter by message content (case-insensitive)")
|
message := fs.String("msg", "", "Filter by message content (case-insensitive)")
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type LogStats struct {
|
|||||||
// patterns, peak usage times, and potential issues.
|
// patterns, peak usage times, and potential issues.
|
||||||
//
|
//
|
||||||
// Options:
|
// 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)
|
// - top: Number of top items to show in detailed view (default: 10)
|
||||||
// - detailed: Show detailed statistics including temporal patterns and top messages
|
// - detailed: Show detailed statistics including temporal patterns and top messages
|
||||||
//
|
//
|
||||||
@@ -55,7 +55,7 @@ type LogStats struct {
|
|||||||
func runStats(args []string) {
|
func runStats(args []string) {
|
||||||
fs := flag.NewFlagSet("stats", flag.ExitOnError)
|
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")
|
topN := fs.Int("top", 10, "Show top N messages/loggers")
|
||||||
detailed := fs.Bool("detailed", false, "Show detailed statistics")
|
detailed := fs.Bool("detailed", false, "Show detailed statistics")
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
// Both phases support filtering by log level and colorized output.
|
// Both phases support filtering by log level and colorized output.
|
||||||
//
|
//
|
||||||
// Options:
|
// 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)
|
// - n: Number of initial lines to show (default: 10)
|
||||||
// - follow: Whether to continue following the file (default: true)
|
// - follow: Whether to continue following the file (default: true)
|
||||||
// - level: Filter by log level (info, warn, error, fatal)
|
// - level: Filter by log level (info, warn, error, fatal)
|
||||||
@@ -37,7 +37,7 @@ import (
|
|||||||
func runTail(args []string) {
|
func runTail(args []string) {
|
||||||
fs := flag.NewFlagSet("tail", flag.ExitOnError)
|
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")
|
lines := fs.Int("n", 10, "Number of initial lines to show")
|
||||||
follow := fs.Bool("follow", true, "Follow the log file (like tail -f)")
|
follow := fs.Bool("follow", true, "Follow the log file (like tail -f)")
|
||||||
level := fs.String("level", "", "Filter by log level")
|
level := fs.String("level", "", "Filter by log level")
|
||||||
|
|||||||
Reference in New Issue
Block a user