new logging configuration (untested)
This commit is contained in:
@@ -41,6 +41,7 @@ const CONFIGFILE_NAME = "amsterdam.yaml"
|
||||
type AmCLI struct {
|
||||
ConfigFile string `arg:"-C,--config,env:AMSTERDAM_CONFIG" help:"Location of the configuration file."`
|
||||
Debug bool `arg:"-D,--debug,env:AMSTERDAM_DEBUG" help:"Force Amsterdam to run in debug mode."`
|
||||
LogLevel string `arg:"-L,--level,--loglevel,env:AMSTERDAM_LOG_LEVEL" help:"Set the log level for the server."`
|
||||
Production bool `arg:"-P,--prod,--production,env:AMSTERDAM_PROD" help:"Force Amsterdam to run in production mode."`
|
||||
DatabaseURL string `arg:"-d,--database,env:AMSTERDAM_DATABASE_URL" help:"Database URL for Amsterdam to connect to."`
|
||||
Listen string `arg:"-l,--listen,env:AMSTERDAM_LISTEN" help:"Specifies the local address and port for Amsterdam to listen on."`
|
||||
@@ -112,6 +113,13 @@ type AmConfig struct {
|
||||
Signature string `yaml:"signature"`
|
||||
Disclaimer string `yaml:"disclaimer"`
|
||||
} `yaml:"email"`
|
||||
Logging struct {
|
||||
LogFile string `yaml:"logFile"`
|
||||
MaxLogSize string `yaml:"maxLogSize"`
|
||||
KeepLogFiles int `yaml:"keepLogFiles"`
|
||||
KeepCompressedLogFiles int `yaml:"keepCompressedLogFiles"`
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
} `yaml:"logging"`
|
||||
Rendering struct {
|
||||
CookieKey string `yaml:"cookiekey"`
|
||||
CountryList struct {
|
||||
@@ -173,6 +181,7 @@ func (c *AmConfig) ExPath(path string) string {
|
||||
// AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig.
|
||||
type AmConfigComputed struct {
|
||||
DebugMode bool // are we in debug mode?
|
||||
LogLevel string // the logging level
|
||||
Listen string // listen address
|
||||
DatabaseDriver string // name of database driver
|
||||
DatabaseDSN string // DSN for the database
|
||||
@@ -362,6 +371,11 @@ func SetupConfig() {
|
||||
} else {
|
||||
GlobalComputedConfig.DebugMode = !GlobalConfig.Site.Production
|
||||
}
|
||||
if CommandLine.LogLevel != "" {
|
||||
GlobalComputedConfig.LogLevel = CommandLine.LogLevel
|
||||
} else {
|
||||
GlobalComputedConfig.LogLevel = GlobalConfig.Logging.LogLevel
|
||||
}
|
||||
if CommandLine.Listen != "" {
|
||||
GlobalComputedConfig.Listen = CommandLine.Listen
|
||||
} else {
|
||||
|
||||
@@ -51,6 +51,12 @@ email:
|
||||
Message sent via Amsterdam Web Communities System - <http://git.erbosoft.com/amy/amsterdam>
|
||||
The Amsterdam Project is not responsible for the contents of this message
|
||||
Report abuses to: <abuse@example.com>
|
||||
logging:
|
||||
logFile: ""
|
||||
maxLogSize: "16 MiB"
|
||||
keepLogFiles: 3
|
||||
keepCompressedLogFiles: 3
|
||||
logLevel: "debug"
|
||||
rendering:
|
||||
cookiekey: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
|
||||
countryList:
|
||||
|
||||
+274
-9
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -13,23 +13,28 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/config"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/labstack/echo/v4"
|
||||
glog "github.com/labstack/gommon/log"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// init sets up the initial configuration for Logrus logging.
|
||||
func init() {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
/*----------------------------------------------------------------------------
|
||||
* Gommon-log to logrus adapter
|
||||
*----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* toglog converts a Logrus logging level to a glog one.
|
||||
* Parameters:
|
||||
@@ -105,6 +110,11 @@ func (l *EchoLogrusAdapter) Panicf(format string, args ...interface{}) { log.Pan
|
||||
func (l *EchoLogrusAdapter) Panicj(j glog.JSON) { log.WithFields(log.Fields(j)).Panic() }
|
||||
func (l *EchoLogrusAdapter) SetHeader(h string) {}
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Echo middleware adapters
|
||||
*----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// LogrusMiddleware installs Logrus logging into the Echo middleware chain.
|
||||
func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
@@ -137,3 +147,258 @@ func LogrusPanicLogging(c echo.Context, err error, stack []byte) error {
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Log output file implementation
|
||||
*----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// amLogFile represents the log output file.
|
||||
type amLogFile struct {
|
||||
mutex sync.Mutex
|
||||
wr io.WriteCloser
|
||||
curSize int64
|
||||
maxSize int64
|
||||
logPath string
|
||||
keep int
|
||||
keepCompressed int
|
||||
}
|
||||
|
||||
// Write (from io.Writer) writes to the log file.
|
||||
func (lf *amLogFile) Write(p []byte) (int, error) {
|
||||
lf.mutex.Lock()
|
||||
defer lf.mutex.Unlock()
|
||||
n, err := lf.wr.Write(p)
|
||||
lf.curSize += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close (from io.Closer) closes the log file.
|
||||
func (lf *amLogFile) Close() error {
|
||||
lf.mutex.Lock()
|
||||
defer lf.mutex.Unlock()
|
||||
err := lf.wr.Close()
|
||||
lf.wr = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// rotate closes the log file and moves it to a new name, shuffling the previously stored log files by the same amount.
|
||||
func (lf *amLogFile) rotate() error {
|
||||
if lf.keep == 0 && lf.keepCompressed == 0 {
|
||||
return nil // degenerate case, keep the log file the same
|
||||
}
|
||||
lf.mutex.Lock()
|
||||
defer lf.mutex.Unlock()
|
||||
// Close existing logfile if it's open.
|
||||
reopen := lf.wr != nil
|
||||
if reopen {
|
||||
lf.wr.Close()
|
||||
}
|
||||
// First loop: shuffle down all the uncompressed files
|
||||
for i := lf.keep; i >= 1; i-- {
|
||||
oldpath := fmt.Sprintf("%s.%d", lf.logPath, i)
|
||||
_, err := os.Stat(oldpath)
|
||||
if err == nil {
|
||||
newpath := fmt.Sprintf("%s.%d", lf.logPath, i+1)
|
||||
err = os.Rename(oldpath, newpath)
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Move the original logfile into the 1 slot.
|
||||
target := fmt.Sprintf("%s.1", lf.logPath)
|
||||
err := os.Rename(lf.logPath, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastUncompressed := fmt.Sprintf("%s.%d", lf.logPath, lf.keep+1)
|
||||
if lf.keepCompressed > 0 {
|
||||
// Second loop: shuffle down all the compressed files
|
||||
for i := lf.keep + lf.keepCompressed; i >= lf.keep+1; i-- {
|
||||
oldpath := fmt.Sprintf("%s.%d.gz", lf.logPath, i)
|
||||
_, err := os.Stat(oldpath)
|
||||
if err == nil {
|
||||
newpath := fmt.Sprintf("%s.%d.gz", lf.logPath, i+1)
|
||||
err = os.Rename(oldpath, newpath)
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Remove the last compressed file, it "fell off the end."
|
||||
oldpath := fmt.Sprintf("%s.%d.gz", lf.logPath, lf.keep+lf.keepCompressed+1)
|
||||
_, err := os.Stat(oldpath)
|
||||
if err == nil {
|
||||
os.Remove(oldpath)
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = os.Stat(lastUncompressed)
|
||||
if err == nil {
|
||||
if lf.keepCompressed > 0 {
|
||||
// Compress that last uncompressed file.
|
||||
target := fmt.Sprintf("%s.%d.gz", lf.logPath, lf.keep+1)
|
||||
var rd io.ReadCloser
|
||||
rd, err = os.Open(lastUncompressed)
|
||||
if err == nil {
|
||||
var xwr io.WriteCloser
|
||||
xwr, err = os.OpenFile(target, os.O_WRONLY|os.O_CREATE, 0o600)
|
||||
if err == nil {
|
||||
wr := gzip.NewWriter(xwr)
|
||||
_, err = io.Copy(wr, rd)
|
||||
wr.Close()
|
||||
xwr.Close()
|
||||
}
|
||||
rd.Close()
|
||||
}
|
||||
}
|
||||
// Now remove the last uncompressed file (leaving the compressed copy behind, where applicable)
|
||||
os.Remove(lastUncompressed)
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Reopen the (now empty) log file.
|
||||
if reopen {
|
||||
var err error
|
||||
lf.wr, err = os.OpenFile(lf.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lf.curSize = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryRotate sees if the log file needs to be rotated and does so.
|
||||
func (lf *amLogFile) tryRotate() {
|
||||
lf.mutex.Lock()
|
||||
defer lf.mutex.Unlock()
|
||||
if lf.curSize >= lf.maxSize {
|
||||
err := lf.rotate()
|
||||
if err != nil {
|
||||
log.Error("log rotation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// open opens the log file and sets up the structure for use.
|
||||
func (lf *amLogFile) open(path string) error {
|
||||
lf.mutex.Lock()
|
||||
defer lf.mutex.Unlock()
|
||||
lf.logPath = path
|
||||
lf.wr = nil
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// The file doesn't exist, so check the directory and make sure it does.
|
||||
dirname := filepath.Dir(path)
|
||||
fi, err = os.Stat(dirname)
|
||||
if err != nil || !fi.IsDir() {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
lf.curSize = 0
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
lf.curSize = fi.Size()
|
||||
}
|
||||
if lf.curSize >= lf.maxSize {
|
||||
err = lf.rotate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
lf.wr, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
return err
|
||||
}
|
||||
|
||||
// logScanner is a goroutine that monitors the log file to see when it needs rotating.
|
||||
func logScanner(ctx context.Context, lf *amLogFile, done chan bool) {
|
||||
d, _ := time.ParseDuration("10s")
|
||||
t := time.NewTicker(d)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
done <- true
|
||||
return
|
||||
case <-t.C:
|
||||
lf.tryRotate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetupLogging sets up the log file based on the configuration data.
|
||||
func SetupLogging() func() {
|
||||
loglevel, err := log.ParseLevel(config.GlobalComputedConfig.LogLevel)
|
||||
if err != nil {
|
||||
loglevel = log.ErrorLevel
|
||||
}
|
||||
if config.GlobalComputedConfig.DebugMode && loglevel != log.TraceLevel {
|
||||
loglevel = log.DebugLevel
|
||||
}
|
||||
var ctx context.Context = nil
|
||||
var cancelfunc context.CancelFunc = nil
|
||||
var done chan bool
|
||||
var logfile io.WriteCloser = nil
|
||||
if !config.GlobalComputedConfig.DebugMode && config.GlobalConfig.Logging.LogFile != "" {
|
||||
amlog := new(amLogFile)
|
||||
maxlog, err := humanize.ParseBytes(config.GlobalConfig.Logging.MaxLogSize)
|
||||
if err != nil {
|
||||
maxlog = 16 * 1024 * 1024 // default to 16 megabytes
|
||||
}
|
||||
amlog.maxSize = int64(maxlog)
|
||||
amlog.keep = config.GlobalConfig.Logging.KeepLogFiles
|
||||
amlog.keepCompressed = config.GlobalConfig.Logging.KeepCompressedLogFiles
|
||||
err = amlog.open(config.GlobalConfig.Logging.LogFile)
|
||||
if err == nil {
|
||||
logfile = amlog
|
||||
ctx, cancelfunc = context.WithCancel(context.Background())
|
||||
done = make(chan bool)
|
||||
go logScanner(ctx, amlog, done)
|
||||
}
|
||||
}
|
||||
if logfile == nil {
|
||||
log.SetOutput(os.Stdout)
|
||||
} else {
|
||||
log.SetOutput(logfile)
|
||||
|
||||
}
|
||||
log.SetLevel(loglevel)
|
||||
|
||||
return func() {
|
||||
if logfile != nil {
|
||||
log.SetOutput(os.Stdout)
|
||||
cancelfunc()
|
||||
<-done
|
||||
logfile.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Initialization
|
||||
*----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// init sets up the initial configuration for Logrus logging.
|
||||
func init() {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user