mirror of
https://github.com/githubhjs/CLIProxyAPIPlus.git
synced 2026-07-13 18:15:24 +00:00
Merge branch 'router-for-me:main' into main
This commit is contained in:
+1
-1
@@ -434,7 +434,7 @@ func main() {
|
|||||||
usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled)
|
usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled)
|
||||||
coreauth.SetQuotaCooldownDisabled(cfg.DisableCooling)
|
coreauth.SetQuotaCooldownDisabled(cfg.DisableCooling)
|
||||||
|
|
||||||
if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
|
if err = logging.ConfigureLogOutput(cfg.LoggingToFile, cfg.LogsMaxTotalSizeMB); err != nil {
|
||||||
log.Errorf("failed to configure log output: %v", err)
|
log.Errorf("failed to configure log output: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ incognito-browser: true
|
|||||||
# When true, write application logs to rotating files instead of stdout
|
# When true, write application logs to rotating files instead of stdout
|
||||||
logging-to-file: false
|
logging-to-file: false
|
||||||
|
|
||||||
|
# Maximum total size (MB) of log files under the logs directory. When exceeded, the oldest log
|
||||||
|
# files are deleted until within the limit. Set to 0 to disable.
|
||||||
|
logs-max-total-size-mb: 0
|
||||||
|
|
||||||
# When false, disable in-memory usage statistics aggregation
|
# When false, disable in-memory usage statistics aggregation
|
||||||
usage-statistics-enabled: false
|
usage-statistics-enabled: false
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha
|
|||||||
var authWithBypass gin.HandlerFunc
|
var authWithBypass gin.HandlerFunc
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
ampAPI.Use(auth)
|
ampAPI.Use(auth)
|
||||||
authWithBypass = wrapManagementAuth(auth, "/threads", "/auth")
|
authWithBypass = wrapManagementAuth(auth, "/threads", "/auth", "/docs")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic proxy handler that uses m.getProxy() for hot-reload support
|
// Dynamic proxy handler that uses m.getProxy() for hot-reload support
|
||||||
@@ -175,7 +175,11 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha
|
|||||||
if authWithBypass != nil {
|
if authWithBypass != nil {
|
||||||
rootMiddleware = append(rootMiddleware, authWithBypass)
|
rootMiddleware = append(rootMiddleware, authWithBypass)
|
||||||
}
|
}
|
||||||
|
engine.GET("/threads", append(rootMiddleware, proxyHandler)...)
|
||||||
engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...)
|
engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...)
|
||||||
|
engine.GET("/docs", append(rootMiddleware, proxyHandler)...)
|
||||||
|
engine.GET("/docs/*path", append(rootMiddleware, proxyHandler)...)
|
||||||
|
|
||||||
engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
|
engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
|
||||||
engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)
|
engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...)
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -865,12 +865,21 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldCfg != nil && oldCfg.LoggingToFile != cfg.LoggingToFile {
|
if oldCfg == nil || oldCfg.LoggingToFile != cfg.LoggingToFile || oldCfg.LogsMaxTotalSizeMB != cfg.LogsMaxTotalSizeMB {
|
||||||
if err := logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
|
if err := logging.ConfigureLogOutput(cfg.LoggingToFile, cfg.LogsMaxTotalSizeMB); err != nil {
|
||||||
log.Errorf("failed to reconfigure log output: %v", err)
|
log.Errorf("failed to reconfigure log output: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
if oldCfg == nil {
|
||||||
|
log.Debug("log output configuration refreshed")
|
||||||
|
} else {
|
||||||
|
if oldCfg.LoggingToFile != cfg.LoggingToFile {
|
||||||
log.Debugf("logging_to_file updated from %t to %t", oldCfg.LoggingToFile, cfg.LoggingToFile)
|
log.Debugf("logging_to_file updated from %t to %t", oldCfg.LoggingToFile, cfg.LoggingToFile)
|
||||||
}
|
}
|
||||||
|
if oldCfg.LogsMaxTotalSizeMB != cfg.LogsMaxTotalSizeMB {
|
||||||
|
log.Debugf("logs_max_total_size_mb updated from %d to %d", oldCfg.LogsMaxTotalSizeMB, cfg.LogsMaxTotalSizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldCfg == nil || oldCfg.UsageStatisticsEnabled != cfg.UsageStatisticsEnabled {
|
if oldCfg == nil || oldCfg.UsageStatisticsEnabled != cfg.UsageStatisticsEnabled {
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ type Config struct {
|
|||||||
// LoggingToFile controls whether application logs are written to rotating files or stdout.
|
// LoggingToFile controls whether application logs are written to rotating files or stdout.
|
||||||
LoggingToFile bool `yaml:"logging-to-file" json:"logging-to-file"`
|
LoggingToFile bool `yaml:"logging-to-file" json:"logging-to-file"`
|
||||||
|
|
||||||
|
// LogsMaxTotalSizeMB limits the total size (in MB) of log files under the logs directory.
|
||||||
|
// When exceeded, the oldest log files are deleted until within the limit. Set to 0 to disable.
|
||||||
|
LogsMaxTotalSizeMB int `yaml:"logs-max-total-size-mb" json:"logs-max-total-size-mb"`
|
||||||
|
|
||||||
// UsageStatisticsEnabled toggles in-memory usage aggregation; when false, usage data is discarded.
|
// UsageStatisticsEnabled toggles in-memory usage aggregation; when false, usage data is discarded.
|
||||||
UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"`
|
UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"`
|
||||||
|
|
||||||
@@ -382,6 +386,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
// Set defaults before unmarshal so that absent keys keep defaults.
|
// Set defaults before unmarshal so that absent keys keep defaults.
|
||||||
cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6)
|
cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6)
|
||||||
cfg.LoggingToFile = false
|
cfg.LoggingToFile = false
|
||||||
|
cfg.LogsMaxTotalSizeMB = 0
|
||||||
cfg.UsageStatisticsEnabled = false
|
cfg.UsageStatisticsEnabled = false
|
||||||
cfg.DisableCooling = false
|
cfg.DisableCooling = false
|
||||||
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
||||||
@@ -427,6 +432,10 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.LogsMaxTotalSizeMB < 0 {
|
||||||
|
cfg.LogsMaxTotalSizeMB = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Sync request authentication providers with inline API keys for backwards compatibility.
|
// Sync request authentication providers with inline API keys for backwards compatibility.
|
||||||
syncInlineAccessProvider(&cfg)
|
syncInlineAccessProvider(&cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -76,39 +76,45 @@ func SetupBaseLogger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
|
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
|
||||||
func ConfigureLogOutput(loggingToFile bool) error {
|
// When logsMaxTotalSizeMB > 0, a background cleaner removes the oldest log files in the logs directory
|
||||||
|
// until the total size is within the limit.
|
||||||
|
func ConfigureLogOutput(loggingToFile bool, logsMaxTotalSizeMB int) error {
|
||||||
SetupBaseLogger()
|
SetupBaseLogger()
|
||||||
|
|
||||||
writerMu.Lock()
|
writerMu.Lock()
|
||||||
defer writerMu.Unlock()
|
defer writerMu.Unlock()
|
||||||
|
|
||||||
if loggingToFile {
|
|
||||||
logDir := "logs"
|
logDir := "logs"
|
||||||
if base := util.WritablePath(); base != "" {
|
if base := util.WritablePath(); base != "" {
|
||||||
logDir = filepath.Join(base, "logs")
|
logDir = filepath.Join(base, "logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protectedPath := ""
|
||||||
|
if loggingToFile {
|
||||||
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("logging: failed to create log directory: %w", err)
|
return fmt.Errorf("logging: failed to create log directory: %w", err)
|
||||||
}
|
}
|
||||||
if logWriter != nil {
|
if logWriter != nil {
|
||||||
_ = logWriter.Close()
|
_ = logWriter.Close()
|
||||||
}
|
}
|
||||||
|
protectedPath = filepath.Join(logDir, "main.log")
|
||||||
logWriter = &lumberjack.Logger{
|
logWriter = &lumberjack.Logger{
|
||||||
Filename: filepath.Join(logDir, "main.log"),
|
Filename: protectedPath,
|
||||||
MaxSize: 10,
|
MaxSize: 10,
|
||||||
MaxBackups: 0,
|
MaxBackups: 0,
|
||||||
MaxAge: 0,
|
MaxAge: 0,
|
||||||
Compress: false,
|
Compress: false,
|
||||||
}
|
}
|
||||||
log.SetOutput(logWriter)
|
log.SetOutput(logWriter)
|
||||||
return nil
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
if logWriter != nil {
|
if logWriter != nil {
|
||||||
_ = logWriter.Close()
|
_ = logWriter.Close()
|
||||||
logWriter = nil
|
logWriter = nil
|
||||||
}
|
}
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureLogDirCleanerLocked(logDir, logsMaxTotalSizeMB, protectedPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +122,8 @@ func closeLogOutputs() {
|
|||||||
writerMu.Lock()
|
writerMu.Lock()
|
||||||
defer writerMu.Unlock()
|
defer writerMu.Unlock()
|
||||||
|
|
||||||
|
stopLogDirCleanerLocked()
|
||||||
|
|
||||||
if logWriter != nil {
|
if logWriter != nil {
|
||||||
_ = logWriter.Close()
|
_ = logWriter.Close()
|
||||||
logWriter = nil
|
logWriter = nil
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const logDirCleanerInterval = time.Minute
|
||||||
|
|
||||||
|
var logDirCleanerCancel context.CancelFunc
|
||||||
|
|
||||||
|
func configureLogDirCleanerLocked(logDir string, maxTotalSizeMB int, protectedPath string) {
|
||||||
|
stopLogDirCleanerLocked()
|
||||||
|
|
||||||
|
if maxTotalSizeMB <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxBytes := int64(maxTotalSizeMB) * 1024 * 1024
|
||||||
|
if maxBytes <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := strings.TrimSpace(logDir)
|
||||||
|
if dir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
logDirCleanerCancel = cancel
|
||||||
|
go runLogDirCleaner(ctx, filepath.Clean(dir), maxBytes, strings.TrimSpace(protectedPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopLogDirCleanerLocked() {
|
||||||
|
if logDirCleanerCancel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logDirCleanerCancel()
|
||||||
|
logDirCleanerCancel = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogDirCleaner(ctx context.Context, logDir string, maxBytes int64, protectedPath string) {
|
||||||
|
ticker := time.NewTicker(logDirCleanerInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
cleanOnce := func() {
|
||||||
|
deleted, errClean := enforceLogDirSizeLimit(logDir, maxBytes, protectedPath)
|
||||||
|
if errClean != nil {
|
||||||
|
log.WithError(errClean).Warn("logging: failed to enforce log directory size limit")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if deleted > 0 {
|
||||||
|
log.Debugf("logging: removed %d old log file(s) to enforce log directory size limit", deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanOnce()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
cleanOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enforceLogDirSizeLimit(logDir string, maxBytes int64, protectedPath string) (int, error) {
|
||||||
|
if maxBytes <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := strings.TrimSpace(logDir)
|
||||||
|
if dir == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
|
||||||
|
entries, errRead := os.ReadDir(dir)
|
||||||
|
if errRead != nil {
|
||||||
|
if os.IsNotExist(errRead) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return 0, errRead
|
||||||
|
}
|
||||||
|
|
||||||
|
protected := strings.TrimSpace(protectedPath)
|
||||||
|
if protected != "" {
|
||||||
|
protected = filepath.Clean(protected)
|
||||||
|
}
|
||||||
|
|
||||||
|
type logFile struct {
|
||||||
|
path string
|
||||||
|
size int64
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
files []logFile
|
||||||
|
total int64
|
||||||
|
)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if !isLogFileName(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, errInfo := entry.Info()
|
||||||
|
if errInfo != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
files = append(files, logFile{
|
||||||
|
path: path,
|
||||||
|
size: info.Size(),
|
||||||
|
modTime: info.ModTime(),
|
||||||
|
})
|
||||||
|
total += info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
if total <= maxBytes {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
return files[i].modTime.Before(files[j].modTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
deleted := 0
|
||||||
|
for _, file := range files {
|
||||||
|
if total <= maxBytes {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if protected != "" && filepath.Clean(file.path) == protected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if errRemove := os.Remove(file.path); errRemove != nil {
|
||||||
|
log.WithError(errRemove).Warnf("logging: failed to remove old log file: %s", filepath.Base(file.path))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total -= file.size
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLogFileName(name string) bool {
|
||||||
|
trimmed := strings.TrimSpace(name)
|
||||||
|
if trimmed == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(trimmed)
|
||||||
|
return strings.HasSuffix(lower, ".log") || strings.HasSuffix(lower, ".log.gz")
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnforceLogDirSizeLimitDeletesOldest(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
writeLogFile(t, filepath.Join(dir, "old.log"), 60, time.Unix(1, 0))
|
||||||
|
writeLogFile(t, filepath.Join(dir, "mid.log"), 60, time.Unix(2, 0))
|
||||||
|
protected := filepath.Join(dir, "main.log")
|
||||||
|
writeLogFile(t, protected, 60, time.Unix(3, 0))
|
||||||
|
|
||||||
|
deleted, err := enforceLogDirSizeLimit(dir, 120, protected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if deleted != 1 {
|
||||||
|
t.Fatalf("expected 1 deleted file, got %d", deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "old.log")); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected old.log to be removed, stat error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "mid.log")); err != nil {
|
||||||
|
t.Fatalf("expected mid.log to remain, stat error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(protected); err != nil {
|
||||||
|
t.Fatalf("expected protected main.log to remain, stat error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnforceLogDirSizeLimitSkipsProtected(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
protected := filepath.Join(dir, "main.log")
|
||||||
|
writeLogFile(t, protected, 200, time.Unix(1, 0))
|
||||||
|
writeLogFile(t, filepath.Join(dir, "other.log"), 50, time.Unix(2, 0))
|
||||||
|
|
||||||
|
deleted, err := enforceLogDirSizeLimit(dir, 100, protected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if deleted != 1 {
|
||||||
|
t.Fatalf("expected 1 deleted file, got %d", deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(protected); err != nil {
|
||||||
|
t.Fatalf("expected protected main.log to remain, stat error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "other.log")); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected other.log to be removed, stat error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLogFile(t *testing.T, path string, size int, modTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data := make([]byte, size)
|
||||||
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chtimes(path, modTime, modTime); err != nil {
|
||||||
|
t.Fatalf("set times: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user