feat: transaction implement

This commit is contained in:
2026-08-15 02:30:36 +08:00
parent 6515c93378
commit b4e18c6f0c
21 changed files with 3886 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
package logging
import (
"errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
)
// New 创建同时写入 stdout 和本地文件的结构化文本日志。
// stdout 由 systemd/journald 收集,本地文件用于现场诊断和受控远程读取。
func New(filePath string) (*slog.Logger, io.Closer, error) {
return NewWithConsole(os.Stdout, filePath)
}
// NewWithConsole 允许调用方指定控制台 writer,主要用于测试和嵌入运行。
func NewWithConsole(console io.Writer, filePath string) (*slog.Logger, io.Closer, error) {
if console == nil {
return nil, nil, errors.New("console writer is required")
}
if filePath == "" {
return nil, nil, errors.New("log file path is required")
}
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, nil, fmt.Errorf("resolve log file path: %w", err)
}
if err := os.MkdirAll(filepath.Dir(absPath), 0o750); err != nil {
return nil, nil, fmt.Errorf("create log directory: %w", err)
}
file, err := os.OpenFile(absPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o640)
if err != nil {
return nil, nil, fmt.Errorf("open log file: %w", err)
}
handler := slog.NewTextHandler(io.MultiWriter(console, file), &slog.HandlerOptions{Level: slog.LevelInfo})
return slog.New(handler), &syncFileCloser{file: file}, nil
}
type syncFileCloser struct {
file *os.File
}
func (c *syncFileCloser) Close() error {
if c == nil || c.file == nil {
return nil
}
file := c.file
c.file = nil
return errors.Join(file.Sync(), file.Close())
}
+35
View File
@@ -0,0 +1,35 @@
package logging
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLoggerWritesConsoleAndFile(t *testing.T) {
t.Parallel()
var console bytes.Buffer
path := filepath.Join(t.TempDir(), "daemon.log")
logger, closer, err := NewWithConsole(&console, path)
if err != nil {
t.Fatalf("create logger: %v", err)
}
logger.Info("transaction created", "transaction_id", "transaction-1")
if err := closer.Close(); err != nil {
t.Fatalf("close logger: %v", err)
}
fileContent, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read log file: %v", err)
}
for name, content := range map[string]string{
"console": console.String(),
"file": string(fileContent),
} {
if !strings.Contains(content, "transaction created") || !strings.Contains(content, "transaction_id=transaction-1") {
t.Fatalf("%s log is missing fields: %q", name, content)
}
}
}