Files
ggjuku/internal/app/diagnostics.go
T
2026-09-30 10:19:23 +08:00

121 lines
3.6 KiB
Go

package app
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"time"
)
const diagnosticLogBytes = 5 * 1024 * 1024
const diagnosticLogBackups = 2
type diagnosticEvent struct {
HTTPStatus int `json:"httpStatus,omitempty"`
RequestedQuality int `json:"requestedQuality,omitempty"`
Time time.Time `json:"time"`
Level string `json:"level"`
Event string `json:"event"`
Source string `json:"source,omitempty"`
DramaID string `json:"dramaId,omitempty"`
DramaTitle string `json:"dramaTitle,omitempty"`
Episode int `json:"episode,omitempty"`
Run uint64 `json:"run,omitempty"`
StartSeconds float64 `json:"startSeconds,omitempty"`
Host string `json:"host,omitempty"`
OffsetBytes int64 `json:"offsetBytes,omitempty"`
TotalBytes int64 `json:"totalBytes,omitempty"`
Attempt int `json:"attempt,omitempty"`
Quality int `json:"quality,omitempty"`
AvailableQualities []int `json:"availableQualities,omitempty"`
Message string `json:"message"`
}
type diagnosticLog struct {
mu sync.Mutex
path string
limit int64
warningOnce sync.Once
}
func newDiagnosticLog(directory string) *diagnosticLog {
return &diagnosticLog{path: filepath.Join(directory, "logs", "app.log"), limit: diagnosticLogBytes}
}
func (log *diagnosticLog) write(event diagnosticEvent) error {
event.Time = time.Now()
if event.Level == "" {
event.Level = "error"
}
data, err := json.Marshal(event)
if err != nil {
return err
}
data = append(data, '\n')
log.mu.Lock()
defer log.mu.Unlock()
if int64(len(data)) > log.limit {
return errors.New("单条诊断日志过大")
}
if err := os.MkdirAll(filepath.Dir(log.path), 0700); err != nil {
return err
}
info, err := os.Stat(log.path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if info != nil && info.Size()+int64(len(data)) > log.limit {
if err := log.rotate(); err != nil {
return err
}
}
file, err := os.OpenFile(log.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return err
}
_, writeErr := file.Write(data)
return errors.Join(writeErr, file.Close())
}
func (log *diagnosticLog) rotate() error {
oldest := log.path + "." + strconv.Itoa(diagnosticLogBackups)
if err := os.Remove(oldest); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
for index := diagnosticLogBackups; index > 0; index-- {
previous := log.path
if index > 1 {
previous += "." + strconv.Itoa(index-1)
}
if err := os.Rename(previous, log.path+"."+strconv.Itoa(index)); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
}
return nil
}
func (downloader *Downloader) recordDiagnostic(event diagnosticEvent) {
if downloader == nil || downloader.diagnostics == nil {
return
}
event.Message = truncate(redactErrorString(event.Message), 4096)
event.DramaTitle = truncate(redactErrorString(event.DramaTitle), 256)
if err := downloader.diagnostics.write(event); err != nil {
downloader.diagnostics.warningOnce.Do(func() {
fmt.Fprintf(os.Stderr, "诊断日志保存失败:%v\n", publicError(err))
})
}
}
func (downloader *Downloader) recordTaskFailure(event string, task Task, start float64, err error) {
if err == nil {
return
}
downloader.recordDiagnostic(diagnosticEvent{Event: event, Source: firstNonEmpty(task.Chapter.Source, sourceFromDramaID(task.DramaID)),
DramaID: task.DramaID, DramaTitle: task.DramaTitle, Episode: task.Index, StartSeconds: start, Message: err.Error()})
}