mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
SSE Streaming: - Add POST /api/agent/chat/stream endpoint with real SSE streaming - Agent.HandleMessageStream() streams final LLM response via onEvent callback - Tool-calling rounds use non-streaming; tool status sent as 'tool' events - Frontend uses ReadableStream API instead of fake word-by-word setTimeout - SSE events: tool (tool execution), delta (text chunk), done (complete), error Data Maintenance: - Add cleanupOldData() on server startup (decisions >90d, equity >180d) - Add CleanOldRecords to DecisionStore and EquityStore - Add store-level logger for cleanup operations Exchange Traders: - Add GetAccountInfo() to Bitget, Bybit, Indodax, KuCoin, Lighter, OKX traders - Remove unused imports in Aster/Lighter traders
247 lines
5.9 KiB
Go
247 lines
5.9 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
// Log is the global logger instance
|
|
Log *logrus.Logger
|
|
// logFile holds the current log file handle
|
|
logFile *os.File
|
|
)
|
|
|
|
// compactFormatter is a custom formatter for cleaner log output
|
|
type compactFormatter struct {
|
|
logrus.TextFormatter
|
|
}
|
|
|
|
func (f *compactFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
level := strings.ToUpper(entry.Level.String())[0:4]
|
|
timestamp := entry.Time.Format("01-02 15:04:05")
|
|
|
|
// Skip frames to find actual caller (skip logrus + our wrapper functions)
|
|
caller := ""
|
|
for i := 3; i < 10; i++ {
|
|
_, file, line, ok := runtime.Caller(i)
|
|
if !ok {
|
|
break
|
|
}
|
|
// Skip logrus internal and our logger.go
|
|
if !strings.Contains(file, "logrus") && !strings.HasSuffix(file, "logger/logger.go") {
|
|
// Get package name from path (e.g., "nofx/manager/trader_manager.go" -> "manager")
|
|
dir := filepath.Dir(file)
|
|
pkg := filepath.Base(dir)
|
|
caller = fmt.Sprintf("%s/%s:%d", pkg, filepath.Base(file), line)
|
|
break
|
|
}
|
|
}
|
|
|
|
msg := fmt.Sprintf("%s [%s] %s %s\n", timestamp, level, caller, entry.Message)
|
|
return []byte(msg), nil
|
|
}
|
|
|
|
func init() {
|
|
// Auto-initialize default logger to ensure it works before Init is called
|
|
Log = logrus.New()
|
|
Log.SetLevel(logrus.InfoLevel)
|
|
Log.SetFormatter(&compactFormatter{})
|
|
Log.SetOutput(os.Stdout)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Initialization functions
|
|
// ============================================================================
|
|
|
|
// Init initializes the global logger
|
|
// If config is nil, uses default configuration (console output, info level)
|
|
func Init(cfg *Config) error {
|
|
Log = logrus.New()
|
|
|
|
// Use default values if no config provided
|
|
if cfg == nil {
|
|
cfg = &Config{Level: "info"}
|
|
}
|
|
|
|
// Set default values
|
|
cfg.SetDefaults()
|
|
|
|
// Set log level
|
|
level, err := logrus.ParseLevel(cfg.Level)
|
|
if err != nil {
|
|
level = logrus.InfoLevel
|
|
}
|
|
Log.SetLevel(level)
|
|
|
|
// Set compact formatter
|
|
Log.SetFormatter(&compactFormatter{})
|
|
|
|
// Setup log file output (write to both stdout and file)
|
|
logDir := "data"
|
|
if err := os.MkdirAll(logDir, 0755); err == nil {
|
|
// Clean up logs older than 30 days on startup
|
|
cleanOldLogs(logDir, 30)
|
|
logFileName := filepath.Join(logDir, fmt.Sprintf("nofx_%s.log", time.Now().Format("2006-01-02")))
|
|
f, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err == nil {
|
|
logFile = f
|
|
// Write to both stdout and file
|
|
Log.SetOutput(io.MultiWriter(os.Stdout, f))
|
|
} else {
|
|
Log.SetOutput(os.Stdout)
|
|
}
|
|
} else {
|
|
Log.SetOutput(os.Stdout)
|
|
}
|
|
|
|
Log.SetReportCaller(true)
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitWithSimpleConfig initializes logger with simplified config
|
|
// Suitable for scenarios that only need basic functionality
|
|
func InitWithSimpleConfig(level string) error {
|
|
return Init(&Config{Level: level})
|
|
}
|
|
|
|
// cleanOldLogs removes log files older than maxAge days from the log directory.
|
|
// Called automatically during Init to prevent unbounded disk usage.
|
|
func cleanOldLogs(logDir string, maxAgeDays int) {
|
|
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
|
|
entries, err := os.ReadDir(logDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
// Match nofx_YYYY-MM-DD.log pattern
|
|
if !strings.HasPrefix(name, "nofx_") || !strings.HasSuffix(name, ".log") {
|
|
continue
|
|
}
|
|
dateStr := strings.TrimPrefix(name, "nofx_")
|
|
dateStr = strings.TrimSuffix(dateStr, ".log")
|
|
t, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if t.Before(cutoff) {
|
|
path := filepath.Join(logDir, name)
|
|
if removeErr := os.Remove(path); removeErr == nil {
|
|
fmt.Printf("[logger] cleaned old log: %s\n", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the logger
|
|
func Shutdown() {
|
|
if logFile != nil {
|
|
logFile.Close()
|
|
logFile = nil
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Logging functions
|
|
// ============================================================================
|
|
|
|
// WithFields creates logger entry with fields
|
|
func WithFields(fields logrus.Fields) *logrus.Entry {
|
|
return Log.WithFields(fields)
|
|
}
|
|
|
|
// WithField creates logger entry with a single field
|
|
func WithField(key string, value interface{}) *logrus.Entry {
|
|
return Log.WithField(key, value)
|
|
}
|
|
|
|
// add debug, info, warn
|
|
func Debug(args ...interface{}) {
|
|
Log.Debug(args...)
|
|
}
|
|
|
|
func Info(args ...interface{}) {
|
|
Log.Info(args...)
|
|
}
|
|
|
|
func Warn(args ...interface{}) {
|
|
Log.Warn(args...)
|
|
}
|
|
|
|
func Debugf(format string, args ...interface{}) {
|
|
Log.Debugf(format, args...)
|
|
}
|
|
|
|
func Infof(format string, args ...interface{}) {
|
|
Log.Infof(format, args...)
|
|
}
|
|
|
|
func Warnf(format string, args ...interface{}) {
|
|
Log.Warnf(format, args...)
|
|
}
|
|
|
|
func Error(args ...interface{}) {
|
|
Log.Error(args...)
|
|
}
|
|
|
|
func Errorf(format string, args ...interface{}) {
|
|
Log.Errorf(format, args...)
|
|
}
|
|
|
|
func Fatal(args ...interface{}) {
|
|
Log.Fatal(args...)
|
|
}
|
|
|
|
func Fatalf(format string, args ...interface{}) {
|
|
Log.Fatalf(format, args...)
|
|
}
|
|
|
|
func Panic(args ...interface{}) {
|
|
Log.Panic(args...)
|
|
}
|
|
|
|
func Panicf(format string, args ...interface{}) {
|
|
Log.Panicf(format, args...)
|
|
}
|
|
|
|
// ============================================================================
|
|
// MCP Logger adapter
|
|
// ============================================================================
|
|
|
|
// MCPLogger adapter that allows MCP package to use the global logger
|
|
// Implements mcp.Logger interface
|
|
type MCPLogger struct{}
|
|
|
|
// NewMCPLogger creates MCP log adapter
|
|
func NewMCPLogger() *MCPLogger {
|
|
return &MCPLogger{}
|
|
}
|
|
|
|
func (l *MCPLogger) Debugf(format string, args ...any) {
|
|
Log.Debugf(format, args...)
|
|
}
|
|
|
|
func (l *MCPLogger) Infof(format string, args ...any) {
|
|
Log.Infof(format, args...)
|
|
}
|
|
|
|
func (l *MCPLogger) Warnf(format string, args ...any) {
|
|
Log.Warnf(format, args...)
|
|
}
|
|
|
|
func (l *MCPLogger) Errorf(format string, args ...any) {
|
|
Log.Errorf(format, args...)
|
|
}
|