火柴人武林大会
156.74M · 2026-02-04
在实际的生产环境中,我们经常面临以下问题:
通过扩展日志组件进行监控,可以带来以下优势:
本监控系统采用分层架构设计,包括以下几层:
采集层:通过自定义Logback Appender拦截日志事件,将日志转换为结构化数据。
处理层:使用OGNL表达式引擎解析日志内容,提取业务指标并进行聚合计算。
存储层:采用内存存储(可扩展至Redis/MySQL),支持时间窗口数据保留。
可视化层:提供REST API,支持多种图表类型的时间序列数据渲染。
应用层:业务系统和监控平台通过API获取监控数据并展示。
系统由以下核心组件构成:
Logback是Java生态中最流行的日志框架之一,其强大的扩展机制允许我们通过自定义Appender来处理日志事件。
Appender是Logback中负责将日志事件输出到特定目标的组件。我们可以通过继承UnsynchronizedAppenderBase来实现自定义Appender:
package com.example.monitor.logback;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import com.alibaba.fastjson2.JSON;
import com.example.monitor.entity.LogEvent;
import com.example.monitor.service.MonitorService;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 自定义Logback Appender - 上报日志到监控系统
*
*/
@Data
public class MonitorAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private static final Logger logger = LoggerFactory.getLogger(MonitorAppender.class);
/**
* 监控服务
*/
private MonitorService monitorService;
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 应用名称
*/
private String appName = "default";
/**
* 环境
*/
private String environment = "prod";
/**
* 异步执行器
*/
private ExecutorService executorService;
/**
* 批量发送大小
*/
private int batchSize = 100;
/**
* 主机名
*/
private String hostName;
@Override
public void start() {
if (enabled) {
try {
hostName = InetAddress.getLocalHost().getHostName();
executorService = Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r, "monitor-appender");
thread.setDaemon(true);
return thread;
});
addInfo("MonitorAppender启动成功 - 应用: " + appName + ", 环境: " + environment);
super.start();
} catch (Exception e) {
addError("MonitorAppender启动失败", e);
}
} else {
addInfo("MonitorAppender未启用");
}
}
@Override
public void stop() {
if (executorService != null) {
executorService.shutdown();
}
super.stop();
}
@Override
protected void append(ILoggingEvent event) {
if (!isStarted() || !enabled) {
return;
}
try {
LogEvent logEvent = convertToLogEvent(event);
// 异步发送,避免影响业务性能
executorService.submit(() -> {
try {
if (monitorService != null) {
monitorService.collectLog(logEvent);
}
} catch (Exception e) {
logger.warn("发送日志到监控服务失败: {}", e.getMessage());
}
});
} catch (Exception e) {
addError("转换日志事件失败", e);
}
}
/**
* 转换Logback事件到自定义日志事件
*/
private LogEvent convertToLogEvent(ILoggingEvent event) {
LogEvent logEvent = new LogEvent();
logEvent.setId(UUID.randomUUID().toString());
logEvent.setLevel(event.getLevel().toString());
logEvent.setLoggerName(event.getLoggerName());
logEvent.setMessage(event.getFormattedMessage());
logEvent.setThreadName(event.getThreadName());
logEvent.setTimestamp(LocalDateTime.ofInstant(
Instant.ofEpochMilli(event.getTimeStamp()),
ZoneId.systemDefault()
));
logEvent.setAppName(appName);
logEvent.setHostName(hostName);
// 获取MDC数据
Map<String, String> mdcMap = event.getMDCPropertyMap();
if (mdcMap != null && !mdcMap.isEmpty()) {
logEvent.setMdcMap(new HashMap<>(mdcMap));
// 提取traceId
logEvent.setTraceId(mdcMap.get("traceId"));
if (logEvent.getTraceId() == null) {
logEvent.setTraceId(mdcMap.get("spanId"));
}
}
// 提取异常信息
if (event.getThrowableProxy() != null) {
logEvent.setThrowable(event.getThrowableProxy().getMessage());
}
// 提取业务数据(从MDC或消息中解析JSON)
extractBusinessData(logEvent, event);
return logEvent;
}
/**
* 从日志中提取业务数据
*/
private void extractBusinessData(LogEvent logEvent, ILoggingEvent event) {
Map<String, Object> businessData = new HashMap<>();
// 从MDC中提取
if (logEvent.getMdcMap() != null) {
for (Map.Entry<String, String> entry : logEvent.getMdcMap().entrySet()) {
if (entry.getKey().startsWith("biz.")) {
businessData.put(entry.getKey().substring(4), entry.getValue());
}
}
}
// 尝试从消息中解析JSON
String message = event.getFormattedMessage();
if (message != null && message.contains("{")) {
try {
int start = message.indexOf("{");
int end = message.lastIndexOf("}") + 1;
String jsonStr = message.substring(start, end);
Map<String, Object> jsonData = JSON.parseObject(jsonStr, Map.class);
businessData.putAll(jsonData);
} catch (Exception e) {
// 忽略解析错误
}
}
if (!businessData.isEmpty()) {
logEvent.setBusinessData(businessData);
}
}
}
为了避免日志采集影响业务性能,我们采用异步采集机制:
OGNL(Object-Graph Navigation Language)是一种强大的表达式语言,具有以下优势:
以下是一些典型的OGNL表达式示例:
// 判断日志级别
level == 'ERROR' ? 1 : 0
// 提取MDC中的traceId
mdcMap.get('traceId')
// 从业务数据中提取金额
businessData.get('amount') != null ? businessData.get('amount') : 0
// 计算响应时间统计
businessData.get('responseTime') > 1000 ? 1 : 0
// 条件判断
businessData.get('status') == 'success' ? 1 : 0
系统内置了以下OGNL函数:
| 函数名 | 说明 | 示例 |
|---|---|---|
| sum(values) | 求和 | sum(list) |
| avg(values) | 平均值 | avg(list) |
| max(values) | 最大值 | max(list) |
| min(values) | 最小值 | min(list) |
| count(values) | 计数 | count(list) |
| duration(start, end) | 时长计算 | duration(start, end) |
| rate(count, time) | 速率计算 | rate(100, 60) |
OGNL表达式的评估流程包括以下步骤:
我们定义了LogEvent实体来封装日志数据:
package com.example.monitor.entity;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 指标配置实体类
*
*/
@Data
public class MetricConfig {
/**
* 配置ID
*/
private Long id;
/**
* 指标名称
*/
private String name;
/**
* 指标编码
*/
private String code;
/**
* OGNL表达式
*/
private String expression;
/**
* 指标类型
* COUNTER-计数器
* GAUGE-仪表盘
* TIMING-计时器
*/
private String type;
/**
* 聚合方式
* SUM/COUNT/AVG/MAX/MIN
*/
private String aggregation;
/**
* 时间窗口(秒)
*/
private Integer timeWindow;
/**
* 标签配置
*/
private List<String> tagKeys;
/**
* 描述
*/
private String description;
/**
* 是否启用
*/
private Boolean enabled;
/**
* 阈值配置
*/
private ThresholdConfig thresholdConfig;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 阈值配置内部类
*/
@Data
public static class ThresholdConfig {
/**
* 警告阈值
*/
private Double warning;
/**
* 严重阈值
*/
private Double critical;
/**
* 阈值类型
* GREATER_THAN-大于
* LESS_THAN-小于
* EQUAL-等于
*/
private String type;
}
}
指标配置决定了如何从日志中提取监控数据:
public class MetricConfig {
private Long id;
private String name; // 指标名称
private String code; // 指标编码
private String expression; // OGNL表达式
private String type; // 类型:COUNTER/GAUGE/TIMING
private String aggregation; // 聚合方式:SUM/AVG/MAX/MIN/COUNT
private Integer timeWindow; // 时间窗口(秒)
private ThresholdConfig thresholdConfig; // 阈值配置
private Boolean enabled;
}
系统支持三种指标类型:
完整的日志采集流程如下:
系统支持以下图表类型的数据生成:
| 图表类型 | 适用场景 | 数据结构 |
|---|---|---|
| 时间序列图 | 趋势分析 | labels[], values[], statistics, trend |
| 仪表盘图 | 实时状态 | value, min, max, status |
| 饼图 | 分布统计 | labels[], values[], colors[], percentages[] |
| 热力图 | 多维分析 | xAxis[], yAxis[], data[][] |
时间序列图是最常用的监控图表,用于展示指标随时间的变化趋势:
/**
* 生成时间序列图表数据
*/
public Map<String, Object> generateTimeSeriesData(String metricName, List<Metric> metrics, int points) {
Map<String, Object> chartData = new HashMap<>();
// 生成时间轴标签
List<String> labels = metrics.stream()
.skip(Math.max(0, metrics.size() - points))
.map(m -> m.getTimestamp().format(DateTimeFormatter.ofPattern("HH:mm:ss")))
.collect(Collectors.toList());
// 生成数值序列
List<Double> values = metrics.stream()
.skip(Math.max(0, metrics.size() - points))
.map(Metric::getValue)
.collect(Collectors.toList());
// 计算统计信息
double max = values.stream().mapToDouble(Double::doubleValue).max().orElse(0);
double min = values.stream().mapToDouble(Double::doubleValue).min().orElse(0);
double avg = values.stream().mapToDouble(Double::doubleValue).average().orElse(0);
// 构建图表数据
chartData.put("labels", labels);
chartData.put("values", values);
chartData.put("metricName", metricName);
Map<String, Object> stats = new HashMap<>();
stats.put("max", round(max, 2));
stats.put("min", round(min, 2));
stats.put("avg", round(avg, 2));
stats.put("count", values.size());
chartData.put("statistics", stats);
// 生成趋势分析
String trend = analyzeTrend(values);
chartData.put("trend", trend);
return chartData;
}
系统使用线性回归算法分析指标趋势:
private String analyzeTrend(List<Double> values) {
int n = values.size();
double sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
for (int i = 0; i < n; i++) {
sumX += i;
sumY += values.get(i);
sumXY += i * values.get(i);
sumXX += i * i;
}
double slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
double avg = sumY / n;
if (slope > avg * 0.1) return "up";
else if (slope < -avg * 0.1) return "down";
else return "stable";
}
系统各组件之间的交互关系如下:
从配置到展示的完整监控工作流:
LogEvent:日志事件实体,包含完整的日志信息和上下文数据
Metric:指标数据实体,记录单个指标值和时间戳
MetricConfig:指标配置实体,定义指标的计算规则和聚合方式
<!-- logback-spring.xml -->
<appender name="MONITOR" class="com.example.monitor.logback.MonitorAppender">
<appName>payment-service</appName>
<enabled>true</enabled>
</appender>
// 业务代码
@Component
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
public PaymentResult process(PaymentRequest request) {
long startTime = System.currentTimeMillis();
try {
// 业务处理
return doProcess(request);
} finally {
long responseTime = System.currentTimeMillis() - startTime;
MDC.put("biz.responseTime", String.valueOf(responseTime));
logger.info("支付处理完成: responseTime={}ms", responseTime);
MDC.remove("biz.responseTime");
}
}
}
// 指标配置
MetricConfig config = new MetricConfig();
config.setName("payment.response_time");
config.setExpression("businessData.get('responseTime')");
config.setType("TIMING");
config.setAggregation("AVG");
config.setTimeWindow(60);
// 订单服务
logger.info("订单创建: orderId={}, amount={}", orderId, amount);
// 指标配置
MetricConfig config = new MetricConfig();
config.setName("order.total_amount");
config.setExpression("businessData.get('amount')");
config.setType("GAUGE");
config.setAggregation("SUM");
config.setTimeWindow(300);
// 指标配置
MetricConfig config = new MetricConfig();
config.setName("app.error_rate");
config.setExpression("level == 'ERROR' ? 1 : 0");
config.setType("GAUGE");
config.setAggregation("AVG");
config.setTimeWindow(60);
当前实现使用内存存储,生产环境建议扩展为Redis或MySQL:
// Redis存储扩展示例
public class RedisMetricStorage implements MetricStorage {
@Autowired
private RedisTemplate<String, Metric> redisTemplate;
public void store(Metric metric) {
String key = "metric:" + metric.getName();
redisTemplate.opsForList().rightPush(key, metric);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
}
可以集成钉钉、企业微信等告警通道:
public class AlertService {
public void checkAndAlert(Metric metric, MetricConfig config) {
if (metric.getValue() > config.getThresholdConfig().getCritical()) {
sendAlert("告警:指标 " + metric.getName() + " 超过阈值");
}
}
}
本文详细介绍了一个基于Logback和OGNL的日志监控可视化系统的设计与实现。通过扩展Logback日志组件,我们实现了一种零侵入、灵活配置的监控数据采集方案。OGNL表达式的引入使得指标定义变得极其灵活,而完善的可视化渲染能力则让监控数据一目了然。