1. 移除标准库 log 导入
2. 使用 jsonLogger 输出结构化 JSON 日志
3. 添加格式化日志方法 (Infof, Errorf, Fatalf)
4. Fatalf 现在会调用 os.Exit(1)
日志格式示例:
{"timestamp":"...","level":"INFO","service":"supply-api","message":"starting supply-api in prod mode"}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
261 lines
6.1 KiB
Go
261 lines
6.1 KiB
Go
package logging
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"time"
|
||
)
|
||
|
||
// ==================== P1-010 日志规范 ====================
|
||
|
||
// LogLevel 日志级别
|
||
type LogLevel string
|
||
|
||
const (
|
||
LogLevelDebug LogLevel = "DEBUG"
|
||
LogLevelInfo LogLevel = "INFO"
|
||
LogLevelWarn LogLevel = "WARN"
|
||
LogLevelError LogLevel = "ERROR"
|
||
LogLevelFatal LogLevel = "FATAL"
|
||
)
|
||
|
||
// LogEntry 标准日志条目(JSON格式)
|
||
type LogEntry struct {
|
||
Timestamp string `json:"timestamp"` // ISO8601格式
|
||
Level string `json:"level"` // DEBUG|INFO|WARN|ERROR|FATAL
|
||
Service string `json:"service"` // 服务名称
|
||
TraceID string `json:"trace_id,omitempty"` // 追踪ID
|
||
SpanID string `json:"span_id,omitempty"` // Span ID
|
||
RequestID string `json:"request_id,omitempty"` // 请求ID
|
||
Message string `json:"message"` // 日志消息
|
||
Fields map[string]interface{} `json:"fields,omitempty"` // 额外字段
|
||
}
|
||
|
||
// Logger 日志接口
|
||
type Logger interface {
|
||
Debug(msg string, fields ...map[string]interface{})
|
||
Info(msg string, fields ...map[string]interface{})
|
||
Warn(msg string, fields ...map[string]interface{})
|
||
Error(msg string, fields ...map[string]interface{})
|
||
Fatal(msg string, fields ...map[string]interface{})
|
||
}
|
||
|
||
// jsonLogger JSON格式日志实现
|
||
type jsonLogger struct {
|
||
service string
|
||
minLevel LogLevel
|
||
output *os.File
|
||
}
|
||
|
||
// NewLogger 创建日志实例
|
||
func NewLogger(service string, minLevel LogLevel) *jsonLogger {
|
||
return &jsonLogger{
|
||
service: service,
|
||
minLevel: minLevel,
|
||
output: os.Stdout,
|
||
}
|
||
}
|
||
|
||
// shouldLog 检查是否应该记录此级别
|
||
func (l *jsonLogger) shouldLog(level LogLevel) bool {
|
||
levels := map[LogLevel]int{
|
||
LogLevelDebug: 0,
|
||
LogLevelInfo: 1,
|
||
LogLevelWarn: 2,
|
||
LogLevelError: 3,
|
||
LogLevelFatal: 4,
|
||
}
|
||
|
||
return levels[level] >= levels[l.minLevel]
|
||
}
|
||
|
||
// formatEntry 格式化日志条目
|
||
func (l *jsonLogger) formatEntry(level LogLevel, msg string, fields map[string]interface{}) *LogEntry {
|
||
entry := &LogEntry{
|
||
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
||
Level: string(level),
|
||
Service: l.service,
|
||
Message: msg,
|
||
}
|
||
|
||
// 添加fields
|
||
if fields != nil {
|
||
entry.Fields = sanitizeFields(fields)
|
||
}
|
||
|
||
return entry
|
||
}
|
||
|
||
// log 输出日志
|
||
func (l *jsonLogger) log(level LogLevel, msg string, fields map[string]interface{}) {
|
||
if !l.shouldLog(level) {
|
||
return
|
||
}
|
||
|
||
entry := l.formatEntry(level, msg, fields)
|
||
|
||
// 序列化为JSON
|
||
data, err := json.Marshal(entry)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 添加换行符
|
||
l.output.Write(append(data, '\n'))
|
||
}
|
||
|
||
func (l *jsonLogger) Debug(msg string, fields ...map[string]interface{}) {
|
||
var f map[string]interface{}
|
||
if len(fields) > 0 {
|
||
f = fields[0]
|
||
}
|
||
l.log(LogLevelDebug, msg, f)
|
||
}
|
||
|
||
func (l *jsonLogger) Info(msg string, fields ...map[string]interface{}) {
|
||
var f map[string]interface{}
|
||
if len(fields) > 0 {
|
||
f = fields[0]
|
||
}
|
||
l.log(LogLevelInfo, msg, f)
|
||
}
|
||
|
||
func (l *jsonLogger) Warn(msg string, fields ...map[string]interface{}) {
|
||
var f map[string]interface{}
|
||
if len(fields) > 0 {
|
||
f = fields[0]
|
||
}
|
||
l.log(LogLevelWarn, msg, f)
|
||
}
|
||
|
||
func (l *jsonLogger) Error(msg string, fields ...map[string]interface{}) {
|
||
var f map[string]interface{}
|
||
if len(fields) > 0 {
|
||
f = fields[0]
|
||
}
|
||
l.log(LogLevelError, msg, f)
|
||
}
|
||
|
||
func (l *jsonLogger) Fatal(msg string, fields ...map[string]interface{}) {
|
||
var f map[string]interface{}
|
||
if len(fields) > 0 {
|
||
f = fields[0]
|
||
}
|
||
l.log(LogLevelFatal, msg, f)
|
||
}
|
||
|
||
// Infof 格式化信息日志
|
||
func (l *jsonLogger) Infof(format string, args ...interface{}) {
|
||
l.Info(fmt.Sprintf(format, args...), nil)
|
||
}
|
||
|
||
// Errorf 格式化错误日志
|
||
func (l *jsonLogger) Errorf(format string, args ...interface{}) {
|
||
l.Error(fmt.Sprintf(format, args...), nil)
|
||
}
|
||
|
||
// Warnf 格式化警告日志
|
||
func (l *jsonLogger) Warnf(format string, args ...interface{}) {
|
||
l.Warn(fmt.Sprintf(format, args...), nil)
|
||
}
|
||
|
||
// Debugf 格式化调试日志
|
||
func (l *jsonLogger) Debugf(format string, args ...interface{}) {
|
||
l.Debug(fmt.Sprintf(format, args...), nil)
|
||
}
|
||
|
||
// Fatalf 格式化致命日志
|
||
func (l *jsonLogger) Fatalf(format string, args ...interface{}) {
|
||
l.Fatal(fmt.Sprintf(format, args...), nil)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// sanitizeFields 敏感字段脱敏
|
||
func sanitizeFields(fields map[string]interface{}) map[string]interface{} {
|
||
sanitized := make(map[string]interface{})
|
||
|
||
sensitiveKeys := []string{
|
||
"password", "secret", "token", "api_key", "apikey",
|
||
"credential", "authorization", "private_key",
|
||
"credit_card", "ssn", "passport",
|
||
}
|
||
|
||
for k, v := range fields {
|
||
lowerK := toLower(k)
|
||
for _, sensitive := range sensitiveKeys {
|
||
if contains(lowerK, sensitive) {
|
||
sanitized[k] = "[REDACTED]"
|
||
break
|
||
}
|
||
}
|
||
|
||
if _, ok := sanitized[k]; !ok {
|
||
// 检查嵌套map
|
||
if nestedMap, ok := v.(map[string]interface{}); ok {
|
||
sanitized[k] = sanitizeFields(nestedMap)
|
||
} else {
|
||
sanitized[k] = v
|
||
}
|
||
}
|
||
}
|
||
|
||
return sanitized
|
||
}
|
||
|
||
// String helpers
|
||
func toLower(s string) string {
|
||
result := make([]byte, len(s))
|
||
for i := 0; i < len(s); i++ {
|
||
c := s[i]
|
||
if c >= 'A' && c <= 'Z' {
|
||
c += 'a' - 'A'
|
||
}
|
||
result[i] = c
|
||
}
|
||
return string(result)
|
||
}
|
||
|
||
func contains(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// LogFieldKeys 日志字段名常量(防止拼写错误)
|
||
const (
|
||
FieldKeyTenantID = "tenant_id"
|
||
FieldKeyUserID = "user_id"
|
||
FieldKeyRequestID = "request_id"
|
||
FieldKeyTraceID = "trace_id"
|
||
FieldKeySpanID = "span_id"
|
||
FieldKeyOperation = "operation"
|
||
FieldKeyDuration = "duration_ms"
|
||
FieldKeyStatusCode = "status_code"
|
||
FieldKeyError = "error"
|
||
FieldKeyErrorCode = "error_code"
|
||
FieldKeyClientIP = "client_ip"
|
||
FieldKeyUserAgent = "user_agent"
|
||
FieldKeyMethod = "method"
|
||
FieldKeyPath = "path"
|
||
FieldKeyQuery = "query"
|
||
FieldKeyRoute = "route"
|
||
)
|
||
|
||
// 标准字段常量
|
||
var SensitiveFields = []string{
|
||
"password",
|
||
"secret",
|
||
"token",
|
||
"api_key",
|
||
"apikey",
|
||
"credential",
|
||
"authorization",
|
||
"private_key",
|
||
"credit_card",
|
||
"ssn",
|
||
}
|