feat: add structured logging system for Docker deployment

添加日志系统,支持 Docker logs 捕获:
- 新增 logger 模块,提供 Info/Warn/Error/Debug 等日志级别
- 任务生命周期日志:创建、开始、完成、失败、取消
- 用户登录/登出日志
- 服务启动和 FFmpeg 状态日志
- 数据库初始化日志

日志格式:[时间] [级别] 消息,便于 Docker logs 查看

Co-Authored-By: AI
This commit is contained in:
2026-04-09 11:02:33 +08:00
parent 04e1045c4f
commit 9b73ea9b68
5 changed files with 182 additions and 18 deletions
+21 -13
View File
@@ -8,6 +8,7 @@ import (
"log" "log"
"net/http" "net/http"
"bilidown/internal/logger"
"bilidown/internal/router" "bilidown/internal/router"
"bilidown/internal/util" "bilidown/internal/util"
@@ -28,12 +29,10 @@ var ffmpegAvailable bool
func main() { func main() {
ffmpegAvailable = checkFFmpeg() ffmpegAvailable = checkFFmpeg()
logger.FFmpegStatus(ffmpegAvailable)
mustInitTables() mustInitTables()
mustRunServer() mustRunServer()
fmt.Printf("Bilidown %s server running at %s\n", VERSION, urlLocal) logger.ServerStarted(HTTP_PORT, VERSION)
if !ffmpegAvailable {
fmt.Println("Warning: FFmpeg is not installed. Video download will not work.")
}
select {} // 保持运行 select {} // 保持运行
} }
@@ -48,7 +47,7 @@ func mustRunServer() {
// 从嵌入的文件系统中获取静态文件 // 从嵌入的文件系统中获取静态文件
staticFS, err := fs.Sub(staticFiles, "static") staticFS, err := fs.Sub(staticFiles, "static")
if err != nil { if err != nil {
log.Fatal("Failed to load static files:", err) logger.Fatal("加载静态文件失败: " + err.Error())
} }
// 前端静态文件 // 前端静态文件
http.Handle("/", http.FileServer(http.FS(staticFS))) http.Handle("/", http.FileServer(http.FS(staticFS)))
@@ -77,7 +76,7 @@ func mustInitTables() {
"name" TEXT PRIMARY KEY NOT NULL, "name" TEXT PRIMARY KEY NOT NULL,
"value" TEXT "value" TEXT
)`); err != nil { )`); err != nil {
log.Fatalln("create table field:", err) logger.Fatal("创建 field 表失败: " + err.Error())
} }
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "log" ( if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "log" (
@@ -85,7 +84,7 @@ func mustInitTables() {
"content" TEXT NOT NULL, "content" TEXT NOT NULL,
"create_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP "create_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP
)`); err != nil { )`); err != nil {
log.Fatalln("create table log:", err) logger.Fatal("创建 log 表失败: " + err.Error())
} }
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "task" ( if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "task" (
@@ -102,21 +101,23 @@ func mustInitTables() {
"download_type" text NOT NULL DEFAULT 'merge', "download_type" text NOT NULL DEFAULT 'merge',
"create_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP "create_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP
)`); err != nil { )`); err != nil {
log.Fatalln("create table task:", err) logger.Fatal("创建 task 表失败: " + err.Error())
} }
if _, err := util.GetCurrentFolder(db); err != nil { if _, err := util.GetCurrentFolder(db); err != nil {
log.Fatalln("util.GetCurrentFolder:", err) logger.Fatal("获取下载目录失败: " + err.Error())
} }
if err := initHistoryTask(db); err != nil { if err := initHistoryTask(db); err != nil {
log.Fatalln("initHistoryTask:", err) logger.Fatal("初始化历史任务失败: " + err.Error())
} }
// 添加可能缺失的列(用于数据库迁移) // 添加可能缺失的列(用于数据库迁移)
if err := addMissingColumns(db); err != nil { if err := addMissingColumns(db); err != nil {
log.Fatalln("addMissingColumns:", err) logger.Fatal("添加缺失列失败: " + err.Error())
} }
logger.Info("数据库初始化完成")
} }
// addMissingColumns 添加可能缺失的列(用于数据库迁移) // addMissingColumns 添加可能缺失的列(用于数据库迁移)
@@ -131,7 +132,14 @@ func addMissingColumns(db *sql.DB) error {
// initHistoryTask 将上一次程序运行时未完成的任务状态变为 error // initHistoryTask 将上一次程序运行时未完成的任务状态变为 error
func initHistoryTask(db *sql.DB) error { func initHistoryTask(db *sql.DB) error {
util.SqliteLock.Lock() util.SqliteLock.Lock()
_, err := db.Exec(`UPDATE "task" SET "status" = 'error' WHERE "status" IN ('waiting', 'running')`) result, err := db.Exec(`UPDATE "task" SET "status" = 'error' WHERE "status" IN ('waiting', 'running')`)
util.SqliteLock.Unlock() util.SqliteLock.Unlock()
return err if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
logger.Infof("重置 %d 个未完成任务状态为 error", rowsAffected)
}
return nil
} }
+145
View File
@@ -0,0 +1,145 @@
package logger
import (
"fmt"
"log"
"os"
"sync"
"time"
)
var (
// 日志级别
level = "info"
mu sync.Mutex
)
// SetLevel 设置日志级别
func SetLevel(l string) {
mu.Lock()
level = l
mu.Unlock()
}
// 日志格式:[时间] [级别] 消息
func formatLog(levelStr, msg string) string {
return fmt.Sprintf("[%s] [%s] %s", time.Now().Format("2006-01-02 15:04:05"), levelStr, msg)
}
// Info 输出信息日志
func Info(msg string) {
log.Println(formatLog("INFO", msg))
}
// Infof 输出格式化信息日志
func Infof(format string, args ...interface{}) {
Info(fmt.Sprintf(format, args...))
}
// Warn 输出警告日志
func Warn(msg string) {
log.Println(formatLog("WARN", msg))
}
// Warnf 输出格式化警告日志
func Warnf(format string, args ...interface{}) {
Warn(fmt.Sprintf(format, args...))
}
// Error 输出错误日志
func Error(msg string) {
log.Println(formatLog("ERROR", msg))
}
// Errorf 输出格式化错误日志
func Errorf(format string, args ...interface{}) {
Error(fmt.Sprintf(format, args...))
}
// Debug 输出调试日志(仅在 debug 别别时输出)
func Debug(msg string) {
mu.Lock()
currentLevel := level
mu.Unlock()
if currentLevel == "debug" {
log.Println(formatLog("DEBUG", msg))
}
}
// Debugf 输出格式化调试日志
func Debugf(format string, args ...interface{}) {
Debug(fmt.Sprintf(format, args...))
}
// Fatal 输出致命错误日志并退出
func Fatal(msg string) {
log.Println(formatLog("FATAL", msg))
os.Exit(1)
}
// Fatalf 输出格式化致命错误日志并退出
func Fatalf(format string, args ...interface{}) {
Fatal(fmt.Sprintf(format, args...))
}
// Task 相关日志
// TaskCreated 任务创建日志
func TaskCreated(taskID int64, title string, bvid string) {
Infof("任务创建 #%d: %s (%s)", taskID, title, bvid)
}
// TaskStarted 任务开始日志
func TaskStarted(taskID int64, title string) {
Infof("任务开始 #%d: %s", taskID, title)
}
// TaskCompleted 任务完成日志
func TaskCompleted(taskID int64, title string, duration time.Duration) {
Infof("任务完成 #%d: %s (耗时 %.1f秒)", taskID, title, duration.Seconds())
}
// TaskFailed 任务失败日志
func TaskFailed(taskID int64, title string, err error) {
Errorf("任务失败 #%d: %s - %v", taskID, title, err)
}
// TaskCancelled 任务取消日志
func TaskCancelled(taskID int64, title string) {
Warnf("任务取消 #%d: %s", taskID, title)
}
// User 用户相关日志
// UserLogin 用户登录日志
func UserLogin() {
Info("用户登录成功")
}
// UserLogout 用户登出日志
func UserLogout() {
Info("用户登出")
}
// Server 服务相关日志
// ServerStarted 服务启动日志
func ServerStarted(port int, version string) {
Infof("服务启动 %s,端口 %d", version, port)
}
// FFmpegStatus FFmpeg 状态日志
func FFmpegStatus(available bool) {
if available {
Info("FFmpeg 检测成功")
} else {
Warn("FFmpeg 未安装,视频下载功能不可用")
}
}
// API API 相关日志
// APICall API 调用日志
func APICall(method, path string) {
Debugf("API %s %s", method, path)
}
+3
View File
@@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"bilidown/internal/bilibili" "bilidown/internal/bilibili"
"bilidown/internal/logger"
"bilidown/internal/util" "bilidown/internal/util"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
@@ -87,6 +88,7 @@ func getQRStatus(w http.ResponseWriter, r *http.Request) {
util.Res{Success: false, Message: err.Error()}.Write(w) util.Res{Success: false, Message: err.Error()}.Write(w)
return return
} }
logger.UserLogin()
util.Res{Success: true, Message: "登录成功"}.Write(w) util.Res{Success: true, Message: "登录成功"}.Write(w)
} }
@@ -98,5 +100,6 @@ func logout(w http.ResponseWriter, r *http.Request) {
util.Res{Success: false, Message: err.Error()}.Write(w) util.Res{Success: false, Message: err.Error()}.Write(w)
return return
} }
logger.UserLogout()
util.Res{Success: true, Message: "退出成功"}.Write(w) util.Res{Success: true, Message: "退出成功"}.Write(w)
} }
+2
View File
@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"bilidown/internal/logger"
"bilidown/internal/util" "bilidown/internal/util"
"bilidown/internal/util/res_error" "bilidown/internal/util/res_error"
) )
@@ -109,5 +110,6 @@ func saveFields(w http.ResponseWriter, r *http.Request) {
util.Res{Success: false, Message: err.Error()}.Write(w) util.Res{Success: false, Message: err.Error()}.Write(w)
return return
} }
logger.Info("设置保存成功")
util.Res{Success: true, Message: "保存成功"}.Write(w) util.Res{Success: true, Message: "保存成功"}.Write(w)
} }
+11 -5
View File
@@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -20,6 +19,7 @@ import (
"bilidown/internal/bilibili" "bilidown/internal/bilibili"
"bilidown/internal/common" "bilidown/internal/common"
"bilidown/internal/logger"
"bilidown/internal/util" "bilidown/internal/util"
) )
@@ -83,6 +83,7 @@ func CancelTask(taskID int64) bool {
if task.ID == taskID && (task.Status == "waiting" || task.Status == "running") { if task.ID == taskID && (task.Status == "waiting" || task.Status == "running") {
task.Cancelled = true task.Cancelled = true
task.Status = "error" task.Status = "error"
logger.TaskCancelled(task.ID, task.Title)
return true return true
} }
} }
@@ -111,6 +112,7 @@ func (task *Task) Create(db *sql.DB) error {
task.ID, err = result.LastInsertId() task.ID, err = result.LastInsertId()
task.CreateAt = time.Now() task.CreateAt = time.Now()
logger.TaskCreated(task.ID, task.Title, task.Bvid)
return err return err
} }
@@ -155,6 +157,7 @@ func (task *Task) Start() {
} }
task.UpdateStatus(db, "running") task.UpdateStatus(db, "running")
logger.TaskStarted(task.ID, task.Title)
if task.DownloadType == "audio" { if task.DownloadType == "audio" {
// 仅音频模式:只下载音频,重命名音频文件为输出文件 // 仅音频模式:只下载音频,重命名音频文件为输出文件
@@ -178,9 +181,10 @@ func (task *Task) Start() {
} }
// 添加元数据 // 添加元数据
if err := task.addMetadata(outputPath); err != nil { if err := task.addMetadata(outputPath); err != nil {
log.Printf("添加元数据失败 (任务ID: %d): %v", task.ID, err) logger.Warnf("添加元数据失败 #%d: %v", task.ID, err)
} }
task.UpdateStatus(db, "done") task.UpdateStatus(db, "done")
logger.TaskCompleted(task.ID, task.Title, time.Since(task.CreateAt))
return return
} else if task.DownloadType == "video" { } else if task.DownloadType == "video" {
// 仅视频模式:只下载视频,重命名视频文件为输出文件 // 仅视频模式:只下载视频,重命名视频文件为输出文件
@@ -204,9 +208,10 @@ func (task *Task) Start() {
} }
// 添加元数据 // 添加元数据
if err := task.addMetadata(outputPath); err != nil { if err := task.addMetadata(outputPath); err != nil {
log.Printf("添加元数据失败 (任务ID: %d): %v", task.ID, err) logger.Warnf("添加元数据失败 #%d: %v", task.ID, err)
} }
task.UpdateStatus(db, "done") task.UpdateStatus(db, "done")
logger.TaskCompleted(task.ID, task.Title, time.Since(task.CreateAt))
return return
} else { } else {
// 合并模式:下载音频和视频,然后合并 // 合并模式:下载音频和视频,然后合并
@@ -262,7 +267,7 @@ func (task *Task) Start() {
GlobalMergeSem.Release() GlobalMergeSem.Release()
// 添加元数据 // 添加元数据
if err := task.addMetadata(outputPath); err != nil { if err := task.addMetadata(outputPath); err != nil {
log.Printf("添加元数据失败 (任务ID: %d): %v", task.ID, err) logger.Warnf("添加元数据失败 #%d: %v", task.ID, err)
} }
task.UpdateStatus(db, "done") task.UpdateStatus(db, "done")
} }
@@ -356,8 +361,9 @@ func (task *Task) UpdateStatus(db *sql.DB, status TaskStatus, errs ...error) err
if err != nil { if err != nil {
err = util.CreateLog(db, fmt.Sprintf("Task-%d-Error: %v", task.ID, err)) err = util.CreateLog(db, fmt.Sprintf("Task-%d-Error: %v", task.ID, err))
if err != nil { if err != nil {
log.Fatalln("CreateLog:", err) logger.Fatal("CreateLog: " + err.Error())
} }
logger.TaskFailed(task.ID, task.Title, err)
} }
} }
task.Status = status task.Status = status