mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
Merge branch 'dev' into fix/partial-close-stats
This commit is contained in:
64
logger/config.go
Normal file
64
logger/config.go
Normal 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
|
||||
}
|
||||
33
logger/config.telegram.json
Normal file
33
logger/config.telegram.json
Normal 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
210
logger/logger.go
Normal 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
158
logger/telegram_hook.go
Normal 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
120
logger/telegram_sender.go
Normal 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()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user