feat: transaction implement
This commit is contained in:
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user