mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 14:56:38 +08:00
refactor(backend): extract to pkg/cap
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package logger 提供结构化日志封装
|
||||
package logger
|
||||
|
||||
const (
|
||||
errCreateLogFileDirFailed = "[Logger] create log file dir err: %w"
|
||||
)
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/uptrace/opentelemetry-go-extra/otelzap"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Config represents the logging configuration.
|
||||
type Config struct {
|
||||
Level string
|
||||
Format string
|
||||
Output string
|
||||
FilePath string
|
||||
MaxSize int
|
||||
MaxAge int
|
||||
MaxBackups int
|
||||
Compress bool
|
||||
}
|
||||
|
||||
var logger *otelzap.Logger
|
||||
|
||||
// ringBufferCapacity 环形缓冲区容量
|
||||
const ringBufferCapacity = 5000
|
||||
|
||||
// GlobalRingBuffer 全局日志环形缓冲区,供 Admin 日志查询和 WebSocket 推送使用
|
||||
var GlobalRingBuffer *LogRingBuffer
|
||||
|
||||
func doInit(cfg Config) {
|
||||
logWriter, err := getLogWriterForConfig(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("[Logger] get log writer err: %v\n", err)
|
||||
}
|
||||
|
||||
// 初始化 ring buffer(保留最近 5000 行日志),如果是多次调用 Init,不需要重复创建 GlobalRingBuffer
|
||||
if GlobalRingBuffer == nil {
|
||||
GlobalRingBuffer = NewLogRingBuffer(ringBufferCapacity)
|
||||
}
|
||||
|
||||
// 使用 multi writer 同时写入原始输出和 ring buffer
|
||||
multiWriter := zapcore.NewMultiWriteSyncer(
|
||||
logWriter,
|
||||
zapcore.AddSync(GlobalRingBuffer),
|
||||
)
|
||||
|
||||
zapLogger := zap.New(
|
||||
zapcore.NewCore(getEncoderForConfig(cfg), multiWriter, getLogLevelForConfig(cfg)),
|
||||
zap.AddCaller(),
|
||||
zap.AddCallerSkip(1),
|
||||
)
|
||||
logger = otelzap.New(
|
||||
zapLogger,
|
||||
otelzap.WithMinLevel(zapLogger.Level()),
|
||||
)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 默认使用 console stdout INFO 日志输出,避免在 Init 前或测试中发生空指针崩溃
|
||||
defaultCfg := Config{
|
||||
Level: "info",
|
||||
Format: "console",
|
||||
Output: "stdout",
|
||||
}
|
||||
doInit(defaultCfg)
|
||||
}
|
||||
|
||||
// Init initializes the logger with a custom configuration.
|
||||
func Init(cfg Config) {
|
||||
doInit(cfg)
|
||||
}
|
||||
|
||||
// DebugF 输出 Debug 级别日志
|
||||
func DebugF(ctx context.Context, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
logger.Ctx(ctx).Debug(msg, getTraceIDFields(ctx)...)
|
||||
}
|
||||
|
||||
// InfoF 输出 Info 级别日志
|
||||
func InfoF(ctx context.Context, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
logger.Ctx(ctx).Info(msg, getTraceIDFields(ctx)...)
|
||||
}
|
||||
|
||||
// WarnF 输出 Warn 级别日志
|
||||
func WarnF(ctx context.Context, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
logger.Ctx(ctx).Warn(msg, getTraceIDFields(ctx)...)
|
||||
}
|
||||
|
||||
// ErrorF 输出 Error 级别日志
|
||||
func ErrorF(ctx context.Context, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
logger.Ctx(ctx).Error(msg, getTraceIDFields(ctx)...)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LogEntry 日志条目,对应 ring buffer 中的一行日志
|
||||
type LogEntry struct {
|
||||
Index int `json:"index"` // 全局递增序号
|
||||
Data string `json:"data"` // 一行日志原文(含换行符)
|
||||
}
|
||||
|
||||
// LogRingBuffer 固定容量的环形缓冲区,存储最近的日志行
|
||||
// 支持:追加日志、按 cursor 分页查询、订阅实时推送
|
||||
type LogRingBuffer struct {
|
||||
mu sync.RWMutex
|
||||
entries []LogEntry
|
||||
cap int
|
||||
head int // 下一条写入的位置
|
||||
count int // 当前条目数
|
||||
seq int // 全局递增序号
|
||||
|
||||
subscribers map[chan LogEntry]struct{}
|
||||
subMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewLogRingBuffer 创建指定容量的日志环形缓冲区
|
||||
func NewLogRingBuffer(capacity int) *LogRingBuffer {
|
||||
return &LogRingBuffer{
|
||||
entries: make([]LogEntry, capacity),
|
||||
cap: capacity,
|
||||
subscribers: make(map[chan LogEntry]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Write 实现 io.Writer 接口,供 zapcore.WriteSyncer 调用
|
||||
// 按 '\n' 分割为独立行写入 ring buffer
|
||||
func (r *LogRingBuffer) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
data := string(p)
|
||||
start := 0
|
||||
for i := 0; i < len(data); i++ {
|
||||
if data[i] == '\n' {
|
||||
line := data[start:i]
|
||||
start = i + 1
|
||||
if len(line) > 0 {
|
||||
r.appendLine(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理最后一行(没有换行符结尾的情况)
|
||||
if start < len(data) && len(data[start:]) > 0 {
|
||||
r.appendLine(data[start:])
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Sync 实现 zapcore.WriteSyncer 接口
|
||||
func (r *LogRingBuffer) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendLine 追加一行日志到 ring buffer 并通知订阅者
|
||||
func (r *LogRingBuffer) appendLine(line string) {
|
||||
r.mu.Lock()
|
||||
entry := LogEntry{
|
||||
Index: r.seq,
|
||||
Data: line,
|
||||
}
|
||||
r.entries[r.head] = entry
|
||||
r.head = (r.head + 1) % r.cap
|
||||
if r.count < r.cap {
|
||||
r.count++
|
||||
}
|
||||
r.seq++
|
||||
r.mu.Unlock()
|
||||
|
||||
// 异步通知订阅者
|
||||
r.subMu.RLock()
|
||||
for ch := range r.subscribers {
|
||||
select {
|
||||
case ch <- entry:
|
||||
default:
|
||||
// 订阅者消费太慢,丢弃(避免阻塞日志写入)
|
||||
}
|
||||
}
|
||||
r.subMu.RUnlock()
|
||||
}
|
||||
|
||||
// Query 查询历史日志
|
||||
// cursor=0 表示查询最新日志,cursor>0 表示查询 index < cursor 的更早日志
|
||||
// limit 为返回条数上限
|
||||
// 返回日志条目(按 index 升序)和是否有更早的日志
|
||||
func (r *LogRingBuffer) Query(cursor int, limit int) ([]LogEntry, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.count == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 计算 ring buffer 中有效条目的范围
|
||||
// oldest index in ring: head - count (wrapping)
|
||||
oldestPos := (r.head - r.count + r.cap) % r.cap
|
||||
|
||||
// 将 ring buffer 中的有效条目按顺序收集
|
||||
ordered := make([]LogEntry, 0, r.count)
|
||||
for i := 0; i < r.count; i++ {
|
||||
pos := (oldestPos + i) % r.cap
|
||||
ordered = append(ordered, r.entries[pos])
|
||||
}
|
||||
|
||||
if cursor == 0 {
|
||||
// 查询最新日志:返回最后 limit 条
|
||||
if len(ordered) <= limit {
|
||||
return ordered, false
|
||||
}
|
||||
return ordered[len(ordered)-limit:], true
|
||||
}
|
||||
|
||||
// 查询 index < cursor 的更早日志
|
||||
// 找到 index < cursor 的条目
|
||||
var cut int
|
||||
for cut = len(ordered); cut > 0; cut-- {
|
||||
if ordered[cut-1].Index < cursor {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cut == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 返回 cut 之前的最后 limit 条
|
||||
start := cut - limit
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
hasMore := start > 0
|
||||
return ordered[start:cut], hasMore
|
||||
}
|
||||
|
||||
// subscribeChanSize 订阅者 channel 缓冲区大小
|
||||
const subscribeChanSize = 64
|
||||
|
||||
// Subscribe 订阅实时日志推送
|
||||
// 返回一个 channel,调用者应 defer Unsubscribe
|
||||
func (r *LogRingBuffer) Subscribe() chan LogEntry {
|
||||
ch := make(chan LogEntry, subscribeChanSize)
|
||||
r.subMu.Lock()
|
||||
r.subscribers[ch] = struct{}{}
|
||||
r.subMu.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Unsubscribe 取消订阅
|
||||
func (r *LogRingBuffer) Unsubscribe(ch chan LogEntry) {
|
||||
r.subMu.Lock()
|
||||
delete(r.subscribers, ch)
|
||||
r.subMu.Unlock()
|
||||
close(ch)
|
||||
}
|
||||
|
||||
// 确保 LogRingBuffer 实现 io.Writer 接口
|
||||
var _ io.Writer = (*LogRingBuffer)(nil)
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogRingBuffer_WriteAndQuery(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
|
||||
// Write some logs
|
||||
_, _ = rb.Write([]byte("line1\nline2\nline3\n"))
|
||||
|
||||
entries, hasMore := rb.Query(0, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Equal(t, 3, len(entries))
|
||||
assert.Equal(t, "line1", entries[0].Data)
|
||||
assert.Equal(t, "line2", entries[1].Data)
|
||||
assert.Equal(t, "line3", entries[2].Data)
|
||||
assert.Equal(t, 0, entries[0].Index)
|
||||
assert.Equal(t, 1, entries[1].Index)
|
||||
assert.Equal(t, 2, entries[2].Index)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_CapacityOverflow(t *testing.T) {
|
||||
rb := NewLogRingBuffer(3)
|
||||
|
||||
_, _ = rb.Write([]byte("a\nb\nc\nd\ne\n"))
|
||||
|
||||
entries, hasMore := rb.Query(0, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Equal(t, 3, len(entries))
|
||||
assert.Equal(t, "c", entries[0].Data)
|
||||
assert.Equal(t, "d", entries[1].Data)
|
||||
assert.Equal(t, "e", entries[2].Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_QueryLatest(t *testing.T) {
|
||||
rb := NewLogRingBuffer(10)
|
||||
|
||||
_, _ = rb.Write([]byte("a\nb\nc\nd\ne\n"))
|
||||
|
||||
// Query latest 2
|
||||
entries, hasMore := rb.Query(0, 2)
|
||||
assert.True(t, hasMore)
|
||||
assert.Equal(t, 2, len(entries))
|
||||
assert.Equal(t, "d", entries[0].Data)
|
||||
assert.Equal(t, "e", entries[1].Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_QueryByCursor(t *testing.T) {
|
||||
rb := NewLogRingBuffer(10)
|
||||
|
||||
_, _ = rb.Write([]byte("a\nb\nc\nd\ne\n"))
|
||||
|
||||
// First get all to find indices
|
||||
all, _ := rb.Query(0, 10)
|
||||
assert.Equal(t, 5, len(all))
|
||||
|
||||
// Query entries before index 3
|
||||
entries, hasMore := rb.Query(3, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Equal(t, 3, len(entries))
|
||||
assert.Equal(t, "a", entries[0].Data)
|
||||
assert.Equal(t, "b", entries[1].Data)
|
||||
assert.Equal(t, "c", entries[2].Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_QueryByCursorWithLimit(t *testing.T) {
|
||||
rb := NewLogRingBuffer(10)
|
||||
|
||||
_, _ = rb.Write([]byte("a\nb\nc\nd\ne\n"))
|
||||
|
||||
// Query 2 entries before index 4
|
||||
entries, hasMore := rb.Query(4, 2)
|
||||
assert.True(t, hasMore)
|
||||
assert.Equal(t, 2, len(entries))
|
||||
assert.Equal(t, "c", entries[0].Data)
|
||||
assert.Equal(t, "d", entries[1].Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_QueryEmpty(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
|
||||
entries, hasMore := rb.Query(0, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Nil(t, entries)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_QueryNonExistentCursor(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
_, _ = rb.Write([]byte("a\nb\n"))
|
||||
|
||||
entries, hasMore := rb.Query(999, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Equal(t, 2, len(entries))
|
||||
assert.Equal(t, "a", entries[0].Data)
|
||||
assert.Equal(t, "b", entries[1].Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_Subscribe(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
|
||||
ch := rb.Subscribe()
|
||||
defer rb.Unsubscribe(ch)
|
||||
|
||||
_, _ = rb.Write([]byte("hello\n"))
|
||||
|
||||
entry := <-ch
|
||||
assert.Equal(t, "hello", entry.Data)
|
||||
assert.Equal(t, 0, entry.Index)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_SubscribeMultiple(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
|
||||
ch1 := rb.Subscribe()
|
||||
defer rb.Unsubscribe(ch1)
|
||||
ch2 := rb.Subscribe()
|
||||
defer rb.Unsubscribe(ch2)
|
||||
|
||||
_, _ = rb.Write([]byte("msg\n"))
|
||||
|
||||
e1 := <-ch1
|
||||
e2 := <-ch2
|
||||
assert.Equal(t, "msg", e1.Data)
|
||||
assert.Equal(t, "msg", e2.Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_WriteNoNewline(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
|
||||
_, _ = rb.Write([]byte("partial"))
|
||||
|
||||
entries, _ := rb.Query(0, 10)
|
||||
assert.Equal(t, 1, len(entries))
|
||||
assert.Equal(t, "partial", entries[0].Data)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_WriteEmpty(t *testing.T) {
|
||||
rb := NewLogRingBuffer(5)
|
||||
|
||||
n, err := rb.Write([]byte(""))
|
||||
assert.Equal(t, 0, n)
|
||||
assert.NoError(t, err)
|
||||
|
||||
entries, _ := rb.Query(0, 10)
|
||||
assert.Nil(t, entries)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_QueryAfterOverflow(t *testing.T) {
|
||||
rb := NewLogRingBuffer(3)
|
||||
|
||||
_, _ = rb.Write([]byte("1\n2\n3\n4\n5\n6\n7\n"))
|
||||
|
||||
entries, hasMore := rb.Query(0, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Equal(t, 3, len(entries))
|
||||
assert.Equal(t, "5", entries[0].Data)
|
||||
assert.Equal(t, "6", entries[1].Data)
|
||||
assert.Equal(t, "7", entries[2].Data)
|
||||
|
||||
// Query by cursor - index 4 is "5", so cursor=4 should return index < 4
|
||||
older, hasMore2 := rb.Query(4, 10)
|
||||
assert.False(t, hasMore2)
|
||||
assert.Nil(t, older)
|
||||
}
|
||||
|
||||
func TestLogRingBuffer_NextCursor(t *testing.T) {
|
||||
rb := NewLogRingBuffer(10)
|
||||
|
||||
_, _ = rb.Write([]byte("a\nb\nc\nd\ne\n"))
|
||||
|
||||
// Query latest 2, should return next_cursor pointing to first returned entry
|
||||
entries, _ := rb.Query(0, 2)
|
||||
assert.Equal(t, 2, len(entries))
|
||||
// entries[0].Index = 3 ("d"), entries[1].Index = 4 ("e")
|
||||
assert.Equal(t, 3, entries[0].Index)
|
||||
|
||||
// Now use that index as cursor to get older entries
|
||||
older, hasMore := rb.Query(entries[0].Index, 10)
|
||||
assert.False(t, hasMore)
|
||||
assert.Equal(t, 3, len(older))
|
||||
assert.Equal(t, "a", older[0].Data)
|
||||
assert.Equal(t, "b", older[1].Data)
|
||||
assert.Equal(t, "c", older[2].Data)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// logDirPerm 日志目录权限
|
||||
const logDirPerm = 0750
|
||||
|
||||
func getLogWriterForConfig(cfg Config) (zapcore.WriteSyncer, error) {
|
||||
if cfg.Output == "file" {
|
||||
// 初始化日志目录
|
||||
logPath := cfg.FilePath
|
||||
logDir := filepath.Dir(logPath)
|
||||
if err := os.MkdirAll(logDir, logDirPerm); err != nil {
|
||||
return nil, fmt.Errorf(errCreateLogFileDirFailed, err)
|
||||
}
|
||||
|
||||
// 配置日志轮转
|
||||
logOutput := &lumberjack.Logger{
|
||||
Filename: logPath,
|
||||
MaxSize: cfg.MaxSize,
|
||||
MaxBackups: cfg.MaxBackups,
|
||||
MaxAge: cfg.MaxAge,
|
||||
Compress: cfg.Compress,
|
||||
}
|
||||
|
||||
return zapcore.AddSync(logOutput), nil
|
||||
}
|
||||
|
||||
return zapcore.AddSync(os.Stdout), nil
|
||||
}
|
||||
|
||||
// getEncoderForConfig 获取日志编码器
|
||||
func getEncoderForConfig(cfg Config) zapcore.Encoder {
|
||||
// 编码器配置
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
if cfg.Format == "json" {
|
||||
return zapcore.NewJSONEncoder(encoderConfig)
|
||||
}
|
||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
// getLogLevelForConfig 获取日志级别
|
||||
func getLogLevelForConfig(cfg Config) zapcore.Level {
|
||||
level := cfg.Level
|
||||
|
||||
switch level {
|
||||
case "debug":
|
||||
return zapcore.DebugLevel
|
||||
case "info":
|
||||
return zapcore.InfoLevel
|
||||
case "warn":
|
||||
return zapcore.WarnLevel
|
||||
case "error":
|
||||
return zapcore.ErrorLevel
|
||||
default:
|
||||
log.Printf("[Logger] invalid log level: %s, defaulting to info\n", level)
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func getTraceIDFields(ctx context.Context) []zap.Field {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
spanContext := span.SpanContext()
|
||||
return []zap.Field{
|
||||
zap.String("traceID", spanContext.TraceID().String()),
|
||||
zap.String("spanID", spanContext.SpanID().String()),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user