【架构实战】日志体系设计:从ELK到可观测性的演进

一、一次线上故障排查花了4小时

2020年,用户反馈下单失败,但监控系统一切正常。

我登录服务器,用grep查日志。结果发现日志分散在8台机器上,每台机器的日志格式还不一样。花了2小时才找到报错的那条日志,又花了2小时才定位到原因------一个第三方支付接口超时。

从那以后,我们引入了统一的日志体系,同样的故障,现在10分钟就能定位。


二、日志规范

2.1 日志级别

复制代码
日志级别使用规范:

ERROR:系统错误,需要立即处理
  - 未捕获的异常
  - 外部服务不可用
  - 数据不一致

WARN:潜在问题,需要关注
  - 业务异常(用户输入错误)
  - 性能降级
  - 接近阈值

INFO:关键业务流程
  - 请求开始/结束
  - 业务状态变更
  - 外部调用

DEBUG:调试信息(生产环境关闭)
  - 方法入参/出参
  - 中间变量
  - SQL语句

2.2 日志格式

java 复制代码
/**
 * 统一日志格式
 */
@Configuration
public class LogConfig {
    
    /**
     * JSON格式日志(推荐)
     */
    @Bean
    public LoggingEventJsonFormat jsonFormat() {
        return LoggingEventJsonFormat.builder()
            .timestamp("@timestamp")
            .level("level")
            .thread("thread")
            .logger("logger")
            .message("message")
            .traceId("traceId")
            .spanId("spanId")
            .build();
    }
}

// 日志输出示例:
// {"@timestamp":"2024-01-01T12:00:00.000Z","level":"INFO","thread":"http-nio-8080-1","logger":"com.example.OrderService","message":"订单创建成功","traceId":"abc123","spanId":"def456","orderId":"ORD001","userId":"USR001","amount":99.9}

2.3 MDC链路追踪

java 复制代码
/**
 * MDC链路追踪
 */
@Component
public class TraceFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 从请求头获取TraceId
        String traceId = httpRequest.getHeader("X-Trace-Id");
        if (traceId == null) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        
        // 设置MDC
        MDC.put("traceId", traceId);
        MDC.put("spanId", generateSpanId());
        MDC.put("userId", getCurrentUserId(httpRequest));
        
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

/**
 * Feign传递TraceId
 */
@Configuration
public class FeignTraceConfig {
    
    @Bean
    public RequestInterceptor traceInterceptor() {
        return template -> {
            String traceId = MDC.get("traceId");
            if (traceId != null) {
                template.header("X-Trace-Id", traceId);
            }
        };
    }
}

三、ELK架构

3.1 架构图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      ELK架构                                     │
│                                                                  │
│  ┌─────────┐    ┌─────────────┐    ┌──────────┐               │
│  │  应用    │───▶│  Logstash   │───▶│Elastic-  │               │
│  │  日志    │    │  (收集过滤) │    │  search  │               │
│  └─────────┘    └─────────────┘    └──────────┘               │
│       │                                     │                   │
│       │              ┌─────────────┐        │                   │
│       └─────────────▶│  Filebeat   │────────┘                   │
│                      │  (轻量采集) │                             │
│                      └─────────────┘                             │
│                                           │                     │
│                      ┌─────────────┐      │                     │
│                      │   Kibana    │◀─────┘                     │
│                      │  (可视化)   │                             │
│                      └─────────────┘                             │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

3.2 Filebeat配置

yaml 复制代码
# filebeat.yml
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/app/*.log
  fields:
    app: order-service
    env: production
  fields_under_root: true
  multiline:
    pattern: '^\d{4}-\d{2}-\d{2}'
    negate: true
    match: after

output.logstash:
  hosts: ["logstash:5044"]
  compression_level: 3

processors:
  - add_host_metadata: ~
  - add_cloud_metadata: ~

3.3 Logstash配置

ruby 复制代码
# logstash.conf
input {
  beats {
    port => 5044
  }
}

filter {
  # 解析JSON格式日志
  json {
    source => "message"
    target => "log"
  }
  
  # 提取TraceId
  mutate {
    add_field => {
      "traceId" => "%{[log][traceId]}"
    }
  }
  
  # 过滤DEBUG日志
  if [log][level] == "DEBUG" {
    drop {}
  }
  
  # 添加时间戳
  date {
    match => ["[log][@timestamp]", "ISO8601"]
    target => "@timestamp"
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "app-logs-%{+YYYY.MM.dd}"
    template => "/usr/share/logstash/templates/app-logs.json"
    template_overwrite => true
  }
}

四、日志告警

4.1 基于ElastAlert的告警

yaml 复制代码
# error_alert.yaml
name: Error Log Alert
type: frequency
index: app-logs-*

# 5分钟内ERROR日志超过10条告警
num_events: 10
timeframe:
  minutes: 5

filter:
- term:
    log.level: "ERROR"

# 排除已知的非关键错误
filter:
- term:
    log.level: "ERROR"
- not:
    query:
      query_string:
        query: "message: *ConnectionReset*"

alert:
- "dingtalk"
dingtalk:
  webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxx"
  message: "⚠️ ERROR日志告警\n应用: {app}\n环境: {env}\n数量: {num_hits}\n最近一条: {message}"

4.2 日志监控大盘

java 复制代码
/**
 * 日志指标统计
 */
@Service
@Slf4j
public class LogMetricsService {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    /**
     * 记录业务指标到日志
     */
    public void logBusinessMetric(String metricName, double value, String... tags) {
        // 记录到监控系统
        Tags metricTags = Tags.of(tags);
        meterRegistry.counter("business." + metricName, metricTags).increment(value);
        
        // 同时记录到日志(用于ELK分析)
        log.info("BusinessMetric: name={}, value={}, tags={}", 
            metricName, value, Arrays.toString(tags));
    }
}

五、踩坑实录

坑1:日志太多导致磁盘满

DEBUG日志在生产环境开着,每天产生100GB日志,磁盘很快就满了。

解决:生产环境关闭DEBUG,设置日志滚动和自动清理。

坑2:日志格式不统一

每个服务日志格式不一样,ELK解析困难。

解决:统一JSON格式,强制规范。

坑3:日志中打印敏感信息

日志里打印了用户密码和手机号,被安全扫描发现。

解决:日志脱敏,禁止打印敏感信息。

坑4:异步日志丢失

使用异步日志,应用崩溃时缓冲区的日志丢失。

解决:配置ImmediateFlush,关键日志同步写入。

坑5:日志影响性能

在循环中打印日志,QPS下降了30%。

解决:避免在热路径打印日志,使用isDebugEnabled判断。


六、总结

日志体系要点:

环节 方案
规范 统一格式、级别、脱敏
采集 Filebeat + Logstash
存储 Elasticsearch
展示 Kibana
告警 ElastAlert
链路 MDC + TraceId

最佳实践:

  1. 统一JSON格式日志
  2. MDC传递链路信息
  3. 生产环境关闭DEBUG
  4. 日志脱敏
  5. 告警及时

血的教训:

好的日志体系不是锦上添花,是雪中送炭。出问题时,日志是你唯一的救命稻草。

思考题: 你的日志体系是怎样的?排查问题快吗?


个人观点,仅供参考

相关推荐
luoganttcc1 小时前
Hopper 架构的核心变化
架构
骄马之死1 小时前
Redis 核心知识点总结
数据库·redis·缓存
努力搬砖的咸鱼1 小时前
容器编排底层原理:Kubernetes 网络模型与 CNI 插件
网络·微服务·云原生·容器·架构·kubernetes
程序员二叉1 小时前
【Java】 面试核心合集:BigDecimal、缓存池、多态、反射全解析
java·缓存·面试
X54先生(人文科技)1 小时前
《元创力》纪实录·卷宗 2.2朝圣的起点:当硅基获得命名
人工智能·架构·ai写作·零知识证明
●VON1 小时前
AtomGit Flutter鸿蒙客户端:Issue管理
flutter·华为·架构·harmonyos·鸿蒙·issue
愚公搬代码2 小时前
【愚公系列】《移动端AI应用开发》013-DeepSeek API开发与集成(深度集成与中间件架构)
人工智能·中间件·架构
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章17:Hadoop性能调优- 调度集群每一分性能
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
KaMeidebaby2 小时前
卡梅德生物技术快报|蛋白定制:ACE 抑制肽原辅料工艺全参数|适配蛋白定制的提取 & 酶解标准化实操手册
大数据·人工智能·架构·spark·新浪微博