init
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package errorlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"goravel/app/models"
|
||||
"goravel/app/utils/logger"
|
||||
"goravel/app/utils/traceid"
|
||||
)
|
||||
|
||||
// RecordHTTP 同时记录文件日志和数据库日志(用于系统级错误,默认 error 级别)
|
||||
// 使用场景:数据库操作失败、系统服务异常、关键业务逻辑错误等
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// if err != nil {
|
||||
// errorlog.RecordHTTP(ctx, "auth", "Failed to save admin profile", map[string]any{
|
||||
// "error": err.Error(),
|
||||
// "admin_id": admin.ID,
|
||||
// }, "Save admin profile error: %v", err)
|
||||
// return response.Error(ctx, http.StatusInternalServerError, "update_failed")
|
||||
// }
|
||||
func RecordHTTP(ctx http.Context, module, message string, attributes map[string]any, format string, args ...any) {
|
||||
RecordHTTPWithLevel(ctx, "error", module, message, attributes, format, args...)
|
||||
}
|
||||
|
||||
// RecordHTTPWithLevel 同时记录文件日志和数据库日志(可指定日志级别)
|
||||
// level: 日志级别,支持 "error", "warning", "info", "debug"
|
||||
// 使用场景:需要记录不同级别的系统日志
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// // 记录警告
|
||||
// errorlog.RecordHTTPWithLevel(ctx, "warning", "auth", "Unusual login pattern detected", map[string]any{
|
||||
// "admin_id": admin.ID,
|
||||
// "ip": ctx.Request().Ip(),
|
||||
// }, "Unusual login pattern: %s", pattern)
|
||||
//
|
||||
// // 记录信息
|
||||
// errorlog.RecordHTTPWithLevel(ctx, "info", "payment", "Payment processed successfully", map[string]any{
|
||||
// "order_id": order.ID,
|
||||
// "amount": order.Amount,
|
||||
// }, "Payment processed: %d", order.ID)
|
||||
func RecordHTTPWithLevel(ctx http.Context, level, module, message string, attributes map[string]any, format string, args ...any) {
|
||||
// 清理和验证日志级别,防止伪造
|
||||
level = sanitizeLogLevel(level)
|
||||
|
||||
// 清理日志格式字符串,防止格式字符串注入
|
||||
sanitizedFormat := sanitizeLogFormat(format)
|
||||
|
||||
// 清理日志参数,防止注入攻击
|
||||
sanitizedArgs := sanitizeLogArgs(args...)
|
||||
|
||||
// 根据级别选择不同的日志函数
|
||||
// 注意:logger 包目前只有 ErrorfHTTP,所以所有级别都使用它
|
||||
// 但在日志消息中添加级别前缀,便于区分和过滤
|
||||
levelPrefix := "[" + strings.ToUpper(level) + "] "
|
||||
formattedMessage := levelPrefix + fmt.Sprintf(sanitizedFormat, sanitizedArgs...)
|
||||
|
||||
switch level {
|
||||
case "error":
|
||||
logger.ErrorfHTTP(ctx, formattedMessage)
|
||||
case "warning":
|
||||
logger.ErrorfHTTP(ctx, formattedMessage)
|
||||
case "info", "debug":
|
||||
logger.ErrorfHTTP(ctx, formattedMessage)
|
||||
default:
|
||||
logger.ErrorfHTTP(ctx, formattedMessage)
|
||||
}
|
||||
|
||||
// 记录到数据库(所有级别都记录 trace_id)
|
||||
// 清理 message 和 attributes,防止注入
|
||||
if ctx != nil {
|
||||
sanitizedMessage := sanitizeLogString(message, 500) // 限制消息长度
|
||||
sanitizedAttributes := sanitizeAttributes(attributes)
|
||||
recordToDatabaseHTTPWithLevel(ctx, level, module, sanitizedMessage, sanitizedAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
// Record 同时记录文件日志和数据库日志(用于标准 context,默认 error 级别)
|
||||
// 使用场景:goroutine、后台任务等
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// go func(ctx context.Context) {
|
||||
// if err != nil {
|
||||
// errorlog.Record(ctx, "operation-log", "Failed to create operation log", map[string]any{
|
||||
// "error": err.Error(),
|
||||
// }, "Create operation log error: %v", err)
|
||||
// }
|
||||
// }(traceCtx)
|
||||
func Record(ctx context.Context, module, message string, attributes map[string]any, format string, args ...any) {
|
||||
RecordWithLevel(ctx, "error", module, message, attributes, format, args...)
|
||||
}
|
||||
|
||||
// RecordWithLevel 同时记录文件日志和数据库日志(可指定日志级别,用于标准 context)
|
||||
// level: 日志级别,支持 "error", "warning", "info", "debug"
|
||||
// 使用场景:goroutine、后台任务中需要记录不同级别的日志
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// go func(ctx context.Context) {
|
||||
// errorlog.RecordWithLevel(ctx, "info", "background-task", "Task completed", map[string]any{
|
||||
// "task_id": taskID,
|
||||
// }, "Background task completed: %s", taskID)
|
||||
// }(traceCtx)
|
||||
func RecordWithLevel(ctx context.Context, level, module, message string, attributes map[string]any, format string, args ...any) {
|
||||
// 清理和验证日志级别,防止伪造
|
||||
level = sanitizeLogLevel(level)
|
||||
|
||||
// 清理日志格式字符串,防止格式字符串注入
|
||||
sanitizedFormat := sanitizeLogFormat(format)
|
||||
|
||||
// 清理日志参数,防止注入攻击
|
||||
sanitizedArgs := sanitizeLogArgs(args...)
|
||||
|
||||
// 根据级别选择不同的日志函数
|
||||
// 注意:logger 包目前只有 ErrorfContext,所以所有级别都使用它
|
||||
// 但在日志消息中添加级别前缀,便于区分和过滤
|
||||
levelPrefix := "[" + strings.ToUpper(level) + "] "
|
||||
formattedMessage := levelPrefix + fmt.Sprintf(sanitizedFormat, sanitizedArgs...)
|
||||
|
||||
switch level {
|
||||
case "error":
|
||||
logger.ErrorfContext(ctx, formattedMessage)
|
||||
case "warning", "info", "debug":
|
||||
logger.ErrorfContext(ctx, formattedMessage)
|
||||
default:
|
||||
logger.ErrorfContext(ctx, formattedMessage)
|
||||
}
|
||||
|
||||
// 记录到数据库(所有级别都记录 trace_id)
|
||||
// 清理 message 和 attributes,防止注入
|
||||
if ctx != nil {
|
||||
sanitizedMessage := sanitizeLogString(message, 1000) // 限制消息长度
|
||||
sanitizedAttributes := sanitizeAttributes(attributes)
|
||||
recordToDatabaseWithLevel(ctx, level, module, sanitizedMessage, sanitizedAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
// recordToDatabaseHTTPWithLevel 将日志记录到数据库(HTTP context,支持所有级别)
|
||||
func recordToDatabaseHTTPWithLevel(ctx http.Context, level, module, message string, attributes map[string]any) {
|
||||
var contextJSON string
|
||||
if len(attributes) > 0 {
|
||||
if data, err := json.Marshal(attributes); err == nil {
|
||||
contextJSON = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
traceID := traceid.FromHTTPContext(ctx)
|
||||
if traceID == "" {
|
||||
traceID = traceid.EnsureHTTPContext(ctx, "")
|
||||
}
|
||||
|
||||
log := models.SystemLog{
|
||||
Level: level,
|
||||
Module: module,
|
||||
TraceID: traceID,
|
||||
Message: message,
|
||||
Context: contextJSON,
|
||||
IP: ctx.Request().Ip(),
|
||||
UserAgent: ctx.Request().Header("User-Agent", ""),
|
||||
}
|
||||
|
||||
_ = facades.Orm().Query().Create(&log)
|
||||
}
|
||||
|
||||
// recordToDatabaseWithLevel 将日志记录到数据库(标准 context,支持所有级别)
|
||||
func recordToDatabaseWithLevel(ctx context.Context, level, module, message string, attributes map[string]any) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
var contextJSON string
|
||||
if len(attributes) > 0 {
|
||||
if data, err := json.Marshal(attributes); err == nil {
|
||||
contextJSON = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
traceID := traceid.FromContext(ctx)
|
||||
if traceID == "" {
|
||||
var newCtx context.Context
|
||||
newCtx, traceID = traceid.EnsureContext(ctx)
|
||||
ctx = newCtx
|
||||
}
|
||||
|
||||
log := models.SystemLog{
|
||||
Level: level,
|
||||
Module: module,
|
||||
TraceID: traceID,
|
||||
Message: message,
|
||||
Context: contextJSON,
|
||||
}
|
||||
|
||||
_ = facades.Orm().Query().Create(&log)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package errorlog
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// sanitizeLogString 清理日志字符串,防止日志注入攻击
|
||||
// 移除或转义危险字符:
|
||||
// - 换行符 (\n, \r) - 可能用于伪造多行日志
|
||||
// - 制表符 (\t) - 可能用于格式化攻击
|
||||
// - 控制字符 - 可能用于隐藏攻击痕迹
|
||||
//
|
||||
// 参数:
|
||||
// - s: 要清理的字符串
|
||||
// - maxLength: 最大长度限制(0 表示不限制)
|
||||
//
|
||||
// 返回:
|
||||
// - 清理后的字符串
|
||||
func sanitizeLogString(s string, maxLength int) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
// 移除控制字符和换行符
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(s))
|
||||
|
||||
for _, r := range s {
|
||||
// 允许打印字符和空格,移除控制字符
|
||||
if unicode.IsPrint(r) || r == ' ' {
|
||||
builder.WriteRune(r)
|
||||
} else {
|
||||
// 将控制字符替换为转义序列(可选,或直接移除)
|
||||
// 这里选择移除,因为转义序列也可能被利用
|
||||
}
|
||||
}
|
||||
|
||||
result := builder.String()
|
||||
|
||||
// 限制长度
|
||||
if maxLength > 0 && len(result) > maxLength {
|
||||
result = result[:maxLength] + "...[truncated]"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sanitizeLogLevel 验证和清理日志级别,防止伪造
|
||||
// 只允许预定义的日志级别
|
||||
func sanitizeLogLevel(level string) string {
|
||||
level = strings.ToLower(strings.TrimSpace(level))
|
||||
|
||||
// 只允许预定义的级别
|
||||
allowedLevels := map[string]string{
|
||||
"error": "error",
|
||||
"warning": "warning",
|
||||
"warn": "warning", // 兼容 warn
|
||||
"info": "info",
|
||||
"debug": "debug",
|
||||
}
|
||||
|
||||
if validLevel, ok := allowedLevels[level]; ok {
|
||||
return validLevel
|
||||
}
|
||||
|
||||
// 如果级别无效,默认返回 error(最安全的级别)
|
||||
return "error"
|
||||
}
|
||||
|
||||
// sanitizeLogArgs 清理日志参数,防止注入攻击
|
||||
// 将参数转换为安全的字符串表示
|
||||
func sanitizeLogArgs(args ...any) []any {
|
||||
if len(args) == 0 {
|
||||
return args
|
||||
}
|
||||
|
||||
sanitized := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
// 字符串参数:清理控制字符
|
||||
sanitized[i] = sanitizeLogString(v, 0)
|
||||
case []byte:
|
||||
// 字节数组:转换为字符串后清理
|
||||
sanitized[i] = sanitizeLogString(string(v), 0)
|
||||
case error:
|
||||
// 错误对象:清理错误消息
|
||||
if v != nil {
|
||||
sanitized[i] = sanitizeLogString(v.Error(), 0)
|
||||
} else {
|
||||
sanitized[i] = "<nil>"
|
||||
}
|
||||
default:
|
||||
// 其他类型:保持原样(数字、布尔等通常是安全的)
|
||||
sanitized[i] = arg
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// sanitizeLogFormat 清理日志格式字符串,防止格式字符串注入
|
||||
// 移除危险的控制字符,但保留格式占位符(%v, %s 等)
|
||||
func sanitizeLogFormat(format string) string {
|
||||
if format == "" {
|
||||
return format
|
||||
}
|
||||
|
||||
// 移除换行符和控制字符,但保留格式占位符
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(format))
|
||||
|
||||
for i := 0; i < len(format); i++ {
|
||||
r := rune(format[i])
|
||||
|
||||
// 允许打印字符、空格和格式占位符
|
||||
if unicode.IsPrint(r) || r == ' ' {
|
||||
builder.WriteRune(r)
|
||||
} else if r == '\n' || r == '\r' || r == '\t' {
|
||||
// 将换行符和制表符替换为空格
|
||||
builder.WriteRune(' ')
|
||||
}
|
||||
// 其他控制字符直接移除
|
||||
}
|
||||
|
||||
result := builder.String()
|
||||
|
||||
// 限制格式字符串长度(防止过长的格式字符串攻击)
|
||||
maxFormatLength := 1000
|
||||
if len(result) > maxFormatLength {
|
||||
result = result[:maxFormatLength] + "...[truncated]"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sanitizeAttributes 清理 attributes map,防止注入攻击
|
||||
func sanitizeAttributes(attributes map[string]any) map[string]any {
|
||||
if attributes == nil || len(attributes) == 0 {
|
||||
return attributes
|
||||
}
|
||||
|
||||
sanitized := make(map[string]any, len(attributes))
|
||||
for key, value := range attributes {
|
||||
// 清理 key
|
||||
sanitizedKey := sanitizeLogString(key, 100)
|
||||
|
||||
// 清理 value
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
sanitized[sanitizedKey] = sanitizeLogString(v, 1000)
|
||||
case []byte:
|
||||
sanitized[sanitizedKey] = sanitizeLogString(string(v), 1000)
|
||||
case error:
|
||||
if v != nil {
|
||||
sanitized[sanitizedKey] = sanitizeLogString(v.Error(), 1000)
|
||||
} else {
|
||||
sanitized[sanitizedKey] = "<nil>"
|
||||
}
|
||||
case map[string]any:
|
||||
// 递归清理嵌套 map
|
||||
sanitized[sanitizedKey] = sanitizeAttributes(v)
|
||||
case []any:
|
||||
// 清理数组
|
||||
sanitizedArray := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
if str, ok := item.(string); ok {
|
||||
sanitizedArray[i] = sanitizeLogString(str, 500)
|
||||
} else {
|
||||
sanitizedArray[i] = item
|
||||
}
|
||||
}
|
||||
sanitized[sanitizedKey] = sanitizedArray
|
||||
default:
|
||||
// 其他类型(数字、布尔等)保持原样
|
||||
sanitized[sanitizedKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
Reference in New Issue
Block a user