Merge branch 'dev' into fix/partial-close-stats

This commit is contained in:
Icyoung
2025-11-05 16:11:53 +08:00
committed by GitHub
90 changed files with 13631 additions and 4002 deletions

64
logger/config.go Normal file
View File

@@ -0,0 +1,64 @@
package logger
import (
"github.com/sirupsen/logrus"
)
// Config 日志配置(简化版)
type Config struct {
Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info)
Telegram *TelegramConfig `json:"telegram"` // Telegram推送配置可选
}
// TelegramConfig Telegram推送配置简化版高级参数使用默认值
type TelegramConfig struct {
Enabled bool `json:"enabled"` // 是否启用(默认: false
BotToken string `json:"bot_token"` // Bot Token
ChatID int64 `json:"chat_id"` // Chat ID
MinLevel string `json:"min_level"` // 最低日志级别该级别及以上的日志会推送到Telegram可选默认: error
}
// SetDefaults 设置默认值
func (c *Config) SetDefaults() {
if c.Level == "" {
c.Level = "info"
}
}
// GetLogrusLevels 返回要推送到Telegram的日志级别
// 根据配置的MinLevel返回该级别及以上的所有日志级别
// 如果未配置或配置无效默认返回error, fatal, panic向后兼容
func (tc *TelegramConfig) GetLogrusLevels() []logrus.Level {
// 如果未配置使用默认值error向后兼容
minLevelStr := tc.MinLevel
if minLevelStr == "" {
minLevelStr = "error"
}
// 解析配置的日志级别
minLevel, err := logrus.ParseLevel(minLevelStr)
if err != nil {
// 如果解析失败使用默认值error向后兼容
minLevel = logrus.ErrorLevel
}
// 定义所有日志级别从高到低panic, fatal, error, warn, info, debug
allLevels := []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
logrus.WarnLevel,
logrus.InfoLevel,
logrus.DebugLevel,
}
// 返回所有大于等于minLevel的日志级别
var result []logrus.Level
for _, level := range allLevels {
if level <= minLevel {
result = append(result, level)
}
}
return result
}

View File

@@ -0,0 +1,33 @@
{
"traders": [
{
"id": "trader1",
"name": "AI Trader 1",
"enabled": true,
"ai_model": "deepseek",
"exchange": "binance",
"binance_api_key": "your_api_key",
"binance_secret_key": "your_secret_key",
"deepseek_key": "your_deepseek_key",
"initial_balance": 1000,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"default_coins": ["BTCUSDT", "ETHUSDT", "SOLUSDT"],
"api_server_port": 8080,
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
},
"log": {
"level": "info",
"telegram": {
"enabled": true,
"bot_token": "79472419:feafe231414",
"chat_id": -100323252626,
"min_level": "error"
}
},
"_comment": "日志配置说明level 可选值为 debug/info/warn/error默认 info。telegram 部分作为可选配置, Telegram 推送默认为 error/fatal/panic 级别min_level 如果设置为warn则推送warn级别及以上的日志"
}

210
logger/logger.go Normal file
View File

@@ -0,0 +1,210 @@
package logger
import (
"nofx/config"
"os"
"github.com/sirupsen/logrus"
)
var (
// Log 全局logger实例
Log *logrus.Logger
// telegramHook 保存hook引用用于优雅关闭
telegramHook *TelegramHook
)
// ============================================================================
// 初始化函数
// ============================================================================
// Init 初始化全局logger
// 如果config为nil使用默认配置console输出info级别
func Init(cfg *Config) error {
Log = logrus.New()
// 如果没有配置,使用默认值
if cfg == nil {
cfg = &Config{Level: "info"}
}
// 设置默认值
cfg.SetDefaults()
// 设置日志级别
level, err := logrus.ParseLevel(cfg.Level)
if err != nil {
level = logrus.InfoLevel
}
Log.SetLevel(level)
// 设置格式化器(固定使用彩色文本格式)
Log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
ForceColors: true,
})
// 设置输出目标默认stdout
Log.SetOutput(os.Stdout)
// 启用调用位置信息
Log.SetReportCaller(true)
// 添加Telegram Hook可选
if cfg.Telegram != nil && cfg.Telegram.Enabled {
if err := setupTelegramHook(cfg.Telegram); err != nil {
Log.Warnf("初始化Telegram推送失败将继续使用普通日志: %v", err)
}
}
return nil
}
// setupTelegramHook 设置Telegram Hook
func setupTelegramHook(telegramCfg *TelegramConfig) error {
hook, err := NewTelegramHook(telegramCfg)
if err != nil {
return err
}
Log.AddHook(hook)
telegramHook = hook
Log.Info("✅ Telegram日志推送已启用")
return nil
}
// InitWithSimpleConfig 使用简化配置初始化logger
// 适用于只需要基本功能的场景
func InitWithSimpleConfig(level string) error {
return Init(&Config{Level: level})
}
// InitWithTelegram 使用Telegram配置初始化logger
func InitWithTelegram(botToken string, chatID int64) error {
return Init(&Config{
Level: "info",
Telegram: &TelegramConfig{
Enabled: true,
BotToken: botToken,
ChatID: chatID,
},
})
}
// InitFromLogConfig 从config.LogConfig初始化logger
func InitFromLogConfig(logConfig *config.LogConfig) error {
if logConfig == nil {
return InitWithSimpleConfig("info")
}
cfg := &Config{
Level: logConfig.Level,
}
if cfg.Level == "" {
cfg.Level = "info"
}
// 如果启用了Telegram添加配置
if logConfig.Telegram != nil && logConfig.Telegram.Enabled {
if botToken := logConfig.Telegram.BotToken; botToken != "" && logConfig.Telegram.ChatID != 0 {
cfg.Telegram = &TelegramConfig{
Enabled: true,
BotToken: botToken,
ChatID: logConfig.Telegram.ChatID,
MinLevel: logConfig.Telegram.MinLevel,
}
}
}
return Init(cfg)
}
// InitFromParams 从参数初始化logger
// 适用于不依赖config包的场景
func InitFromParams(level string, telegramEnabled bool, botToken string, chatID int64) error {
cfg := &Config{Level: level}
if telegramEnabled && botToken != "" && chatID != 0 {
cfg.Telegram = &TelegramConfig{
Enabled: true,
BotToken: botToken,
ChatID: chatID,
}
}
return Init(cfg)
}
// Shutdown 优雅关闭logger主要用于关闭Telegram发送器
func Shutdown() {
if telegramHook != nil {
telegramHook.Stop()
telegramHook = nil
}
}
// ============================================================================
// 日志记录函数
// ============================================================================
// WithFields 创建带字段的logger entry
func WithFields(fields logrus.Fields) *logrus.Entry {
return Log.WithFields(fields)
}
// WithField 创建带单个字段的logger entry
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...)
}

158
logger/telegram_hook.go Normal file
View File

@@ -0,0 +1,158 @@
package logger
import (
"fmt"
"runtime"
"strings"
"github.com/sirupsen/logrus"
)
// TelegramHook 实现logrus.Hook接口将日志推送到Telegram
type TelegramHook struct {
sender *TelegramSender
levels []logrus.Level
enabled bool
}
// NewTelegramHook 创建Telegram Hook
func NewTelegramHook(config *TelegramConfig) (*TelegramHook, error) {
if !config.Enabled {
return &TelegramHook{enabled: false}, nil
}
if config.BotToken == "" || config.ChatID == 0 {
return nil, fmt.Errorf("telegram配置不完整: bot_token和chat_id不能为空")
}
// 创建发送器(使用默认参数)
sender, err := NewTelegramSender(config.BotToken, config.ChatID)
if err != nil {
return nil, fmt.Errorf("创建telegram发送器失败: %w", err)
}
hook := &TelegramHook{
sender: sender,
levels: config.GetLogrusLevels(),
enabled: true,
}
return hook, nil
}
// Levels 返回需要触发的日志级别
func (h *TelegramHook) Levels() []logrus.Level {
if !h.enabled {
return []logrus.Level{}
}
return h.levels
}
// Fire 当日志触发时调用
func (h *TelegramHook) Fire(entry *logrus.Entry) error {
if !h.enabled {
return nil
}
// 格式化消息
message := h.formatMessage(entry)
// 异步发送(非阻塞)
h.sender.SendAsync(message)
return nil
}
// formatMessage 格式化日志消息为Telegram格式
func (h *TelegramHook) formatMessage(entry *logrus.Entry) string {
// 级别emoji
levelEmoji := h.getLevelEmoji(entry.Level)
// 基本信息
var builder strings.Builder
builder.WriteString(fmt.Sprintf("%s *%s*: 系统日志警报\n", levelEmoji, strings.ToUpper(entry.Level.String())))
builder.WriteString(fmt.Sprintf("📝 消息: `%s`\n", escapeMarkdown(entry.Message)))
// 字段信息
if len(entry.Data) > 0 {
builder.WriteString("📊 字段:\n")
for key, value := range entry.Data {
builder.WriteString(fmt.Sprintf(" • %s: `%v`\n", key, value))
}
}
// 调用位置
if entry.HasCaller() {
file := entry.Caller.File
// 只保留相对路径
if idx := strings.Index(file, "nofx/"); idx >= 0 {
file = file[idx:]
}
builder.WriteString(fmt.Sprintf("📍 位置: `%s:%d`\n", file, entry.Caller.Line))
} else {
// 如果entry没有caller手动获取
if _, file, line, ok := runtime.Caller(8); ok {
if idx := strings.Index(file, "nofx/"); idx >= 0 {
file = file[idx:]
}
builder.WriteString(fmt.Sprintf("📍 位置: `%s:%d`\n", file, line))
}
}
// 时间戳
builder.WriteString(fmt.Sprintf("🕐 时间: `%s`", entry.Time.Format("2006-01-02 15:04:05")))
return builder.String()
}
// getLevelEmoji 获取日志级别对应的emoji
func (h *TelegramHook) getLevelEmoji(level logrus.Level) string {
switch level {
case logrus.PanicLevel:
return "🔴"
case logrus.FatalLevel:
return "🔴"
case logrus.ErrorLevel:
return "🟠"
case logrus.WarnLevel:
return "🟡"
case logrus.InfoLevel:
return "🟢"
case logrus.DebugLevel:
return "🔵"
default:
return "⚪"
}
}
// escapeMarkdown 转义Markdown特殊字符
func escapeMarkdown(text string) string {
replacer := strings.NewReplacer(
"_", "\\_",
"*", "\\*",
"[", "\\[",
"]", "\\]",
"(", "\\(",
")", "\\)",
"~", "\\~",
"`", "\\`",
">", "\\>",
"#", "\\#",
"+", "\\+",
"-", "\\-",
"=", "\\=",
"|", "\\|",
"{", "\\{",
"}", "\\}",
".", "\\.",
"!", "\\!",
)
return replacer.Replace(text)
}
// Stop 停止Hook优雅关闭
func (h *TelegramHook) Stop() {
if h.enabled && h.sender != nil {
h.sender.Stop()
}
}

120
logger/telegram_sender.go Normal file
View File

@@ -0,0 +1,120 @@
package logger
import (
"fmt"
"sync"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// TelegramSender Telegram消息发送器异步
type TelegramSender struct {
bot *tgbotapi.BotAPI
chatID int64
msgChan chan string
retryCount int
retryInterval time.Duration
wg sync.WaitGroup
stopChan chan struct{}
once sync.Once
}
// NewTelegramSender 创建Telegram发送器使用默认参数
func NewTelegramSender(botToken string, chatID int64) (*TelegramSender, error) {
bot, err := tgbotapi.NewBotAPI(botToken)
if err != nil {
return nil, fmt.Errorf("创建telegram bot失败: %w", err)
}
// 设置为静默模式不打印bot信息
bot.Debug = false
sender := &TelegramSender{
bot: bot,
chatID: chatID,
msgChan: make(chan string, 20), // 固定缓冲区大小: 20
retryCount: 3, // 固定重试次数: 3
retryInterval: 3 * time.Second, // 固定重试间隔: 3秒
stopChan: make(chan struct{}),
}
// 启动异步发送协程
sender.Start()
return sender, nil
}
// Start 启动异步发送协程
func (s *TelegramSender) Start() {
s.wg.Add(1)
go s.listenAndSend()
}
// SendAsync 异步发送消息(非阻塞)
func (s *TelegramSender) SendAsync(message string) {
select {
case s.msgChan <- message:
// 成功写入缓冲区
default:
// 缓冲区满,丢弃消息(不阻塞主流程)
fmt.Printf("[Telegram] 消息缓冲区已满,消息被丢弃\n")
}
}
// listenAndSend 监听channel并发送消息
func (s *TelegramSender) listenAndSend() {
defer s.wg.Done()
for {
select {
case msg := <-s.msgChan:
s.sendWithRetry(msg)
case <-s.stopChan:
// 清空缓冲区后退出
for len(s.msgChan) > 0 {
msg := <-s.msgChan
s.sendWithRetry(msg)
}
return
}
}
}
// sendWithRetry 发送消息(带重试)
func (s *TelegramSender) sendWithRetry(message string) {
var err error
for i := 0; i < s.retryCount; i++ {
err = s.send(message)
if err == nil {
return // 发送成功
}
// 重试前等待
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
}
// 所有重试都失败
if err != nil {
fmt.Printf("[Telegram] 发送消息失败(已重试%d次: %v\n", s.retryCount, err)
}
}
// send 发送单条消息
func (s *TelegramSender) send(message string) error {
msg := tgbotapi.NewMessage(s.chatID, message)
msg.ParseMode = tgbotapi.ModeMarkdown
_, err := s.bot.Send(msg)
return err
}
// Stop 停止发送器(优雅关闭)
func (s *TelegramSender) Stop() {
s.once.Do(func() {
close(s.stopChan)
s.wg.Wait()
})
}