一、为什么需要记录领域操作与事件
想象你正在开发一个电商系统,用户下单后需要经历库存扣减、支付处理、物流通知等多个步骤。如果某个环节出了问题,比如库存扣减成功但支付失败,这时候如果没有详细的日志记录,排查问题就像在黑暗中摸象。
领域驱动设计(DDD)强调用业务语言建模系统,而日志就是系统的"黑匣子"。好的日志策略能帮你:
- 还原业务操作的完整轨迹
- 快速定位异常发生的具体环节
- 分析用户行为模式
- 满足审计合规要求
举个常见问题:用户投诉"我的订单没生效",但系统显示"已完成"。通过检查日志发现,订单状态更新时发生了并发冲突,导致最终状态错误。这就是典型的需要领域事件日志的场景。
二、基础日志记录的实现方式
我们以C#/.NET Core为例,演示最基本的领域日志实现:
// 技术栈:.NET Core + Serilog
public class OrderService
{
private readonly ILogger<OrderService> _logger;
// 通过依赖注入获取日志实例
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void CreateOrder(Order order)
{
try
{
// 记录领域操作开始
_logger.LogInformation("创建订单开始 | 订单ID:{OrderId} 用户:{UserId}",
order.Id, order.UserId);
// 业务逻辑处理...
ProcessPayment(order);
UpdateInventory(order);
// 记录关键领域事件
_logger.LogInformation("订单创建成功 | 订单ID:{OrderId} 状态:{Status}",
order.Id, order.Status);
}
catch (Exception ex)
{
// 结构化异常日志
_logger.LogError(ex, "订单创建失败 | 订单ID:{OrderId} 错误:{ErrorMessage}",
order.Id, ex.Message);
throw;
}
}
}
这种方式的优点是简单直接,但存在明显不足:
- 日志分散在各处,难以形成完整业务流
- 缺乏统一的事件格式标准
- 无法追溯跨服务的领域操作
三、进阶事件溯源模式
更专业的做法是采用事件溯源(Event Sourcing),我们改造上面的例子:
// 技术栈:.NET Core + EventStore
public class OrderAggregate
{
private readonly List<IDomainEvent> _changes = new();
public void CreateOrder(Order order)
{
// 生成领域事件
var @event = new OrderCreatedEvent(
order.Id,
order.UserId,
DateTime.UtcNow);
// 应用事件到当前聚合
Apply(@event);
// 记录未提交的事件
_changes.Add(@event);
}
private void Apply(OrderCreatedEvent @event)
{
// 更新聚合内部状态...
}
// 获取所有未提交的事件
public IEnumerable<IDomainEvent> GetChanges() => _changes.AsReadOnly();
}
// 领域事件基类
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}
// 具体领域事件
public record OrderCreatedEvent(
Guid OrderId,
Guid UserId,
DateTime OccurredOn) : IDomainEvent;
配套的事件存储实现:
public class EventStoreService
{
private readonly ILogger _logger;
public void SaveEvents(Guid aggregateId, IEnumerable<IDomainEvent> events)
{
foreach (var @event in events)
{
// 结构化存储每个事件
_logger.LogInformation("""
[领域事件存储]
聚合ID: {AggregateId}
事件类型: {EventType}
发生时间: {OccurredOn}
详细数据: {EventData}
""",
aggregateId,
@event.GetType().Name,
@event.OccurredOn,
JsonSerializer.Serialize(@event));
}
}
}
这种方案的优点:
- 完整保存业务状态变化历史
- 可以通过重放事件重建任意时间点的状态
- 天然支持审计日志需求
典型应用场景:
- 金融交易系统
- 医疗记录系统
- 需要回滚/补偿的业务流程
四、分布式系统中的日志关联
在微服务架构下,一个订单处理可能涉及多个服务。我们需要通过关联ID串联日志:
// 技术栈:.NET Core + OpenTelemetry
public class OrderProcessingService
{
public async Task ProcessOrder(Order order)
{
// 生成全局唯一的跟踪ID
using var activity = ActivitySource.StartActivity("ProcessOrder");
// 将跟踪ID注入日志上下文
using (_logger.BeginScope(new Dictionary<string, object>
{
["TraceId"] = activity?.TraceId.ToString(),
["SpanId"] = activity?.SpanId.ToString()
}))
{
_logger.LogInformation("开始处理分布式订单");
// 调用库存服务
await _inventoryService.ReduceStockAsync(order);
// 调用支付服务
await _paymentService.ProcessPaymentAsync(order);
}
}
}
配套的日志查询示例:
-- 在ELK中查询特定订单的所有相关日志
trace_id:"7b3d5f1a1b2c4d3e" AND service_name:("OrderService" OR "PaymentService")
关键技术点:
- 使用OpenTelemetry实现分布式追踪
- 日志系统需要支持结构化查询
- 服务间传递追踪上下文(headers)
五、性能与安全的平衡之道
记录日志时需要注意:
- 敏感信息过滤:
// 在日志过滤器中添加规则
builder.Services.AddLogging(logging =>
{
logging.AddFilter((provider, category, logLevel) =>
{
// 自动脱敏信用卡号
if (logState is IReadOnlyList<KeyValuePair<string, object>> state)
{
foreach (var item in state)
{
if (item.Value?.ToString().ContainsCreditCardNumber())
{
return false; // 跳过记录
}
}
}
return true;
});
});
- 日志分级策略:
- DEBUG:开发环境详细日志
- INFO:关键业务事件
- WARN:可自动恢复的异常
- ERROR:需要人工干预的错误
- 性能优化技巧:
// 避免昂贵的字符串拼接
// 错误写法
_logger.LogInformation("订单数据:" + order.ToJsonString());
// 正确写法 - 使用结构化日志模板
_logger.LogInformation("订单数据:{OrderData}", order);
六、现代日志架构的最佳实践
推荐的技术组合方案:
- 采集层:
- 应用内:Serilog/NLog
- 容器:Fluentd
- 传输层:
- Kafka/Pulsar作为日志总线
- 存储分析层:
- Elasticsearch集群
- Grafana可视化
- 预警系统:
- 基于Prometheus的异常检测
- 关键错误触发Slack通知
示例告警规则:
# Prometheus告警规则
- alert: HighErrorRate
expr: rate(log_errors_total[1m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "高错误率发生在 {{ $labels.service }}"
七、从日志到业务洞察
高级应用案例——用户行为分析:
// 记录用户搜索行为
public class ProductSearchService
{
public void LogSearchBehavior(SearchRequest request)
{
_logger.LogInformation("""
[用户行为] 商品搜索
用户ID: {UserId}
搜索词: {Keyword}
筛选条件: {Filters}
结果数量: {ResultCount}
响应时间: {ElapsedMs}ms
""",
request.UserId,
request.Keyword,
request.Filters,
request.ResultCount,
request.ElapsedMilliseconds);
}
}
通过分析这类日志可以:
- 发现热门搜索趋势
- 优化搜索结果排序
- 识别无效搜索条件
- 改进推荐算法
八、常见陷阱与解决方案
- 日志爆炸问题:
- 现象:单个请求产生数百条日志
- 解决:使用采样策略,如只记录10%的DEBUG日志
- 日志不一致:
- 现象:多个服务时间戳不同步
- 解决:部署NTP时间同步服务
- 日志丢失:
- 现象:高峰期日志不完整
- 解决:使用本地缓存+异步批量上传
- 查询性能差:
- 现象:搜索1周前的日志超时
- 解决:按日期分索引+冷热数据分离
九、面向未来的演进方向
- 智能化日志分析:
- 使用ML自动分类错误类型
- 预测可能发生的连锁故障
- 可观测性增强:
- 将日志、指标、追踪三者关联
- 实现1-click根因分析
- 边缘计算场景:
- 在设备端进行日志预处理
- 仅上传异常摘要信息
评论