跳到主要内容
Prometheus + Grafana 实现 Java 应用数据监控系统 | 极客日志
Java AI java
Prometheus + Grafana 实现 Java 应用数据监控系统 综述由AI生成 如何使用 Prometheus 和 Grafana 搭建 Java 应用的数据监控系统。首先简述了可观测性概念,随后详细讲解了 Prometheus 和 Grafana 的安装与整合步骤。核心部分展示了如何在 Spring Boot 项目中集成 Actuator 和 Micrometer,通过自定义监控上下文、指标收集器和监听器来采集 AI 模型调用相关的业务指标(如请求次数、Token 消耗、响应时间)。最后提供了 Prometheus 配置及 Grafana 看板导入方法,实现了从数据采集到可视化的完整闭环。
城市逃兵 发布于 2026/3/30 更新于 2026/5/22 25 浏览1.什么是可观测
**可观测(Observability)**作为现代运维理念,强调系统在运行时应具备全面的、深入的、可理解的状态获取能力。通过收集和分析系统的各种可观测数据,构建一个全方位、立体化的监控与分析体系,运维团队能够在复杂、动态的 IT 环境中实时了解系统内部的健康状况、性能表现以及故障原因,并基于这些信息做出准确的决策,实现快速问题定位、预防性维护以及持续优化。
2.使用 Prometheus + Grafana 实现监控
Prometheus
Prometheus 是一个开源的监控系统,专门为时序数据的收集、存储和查询而设计。
Prometheus 的核心理念是将所有监控数据以 时间序列 的形式存储。根据它的 数据模型 ,每个时间序列都由指标名称和一组标签唯一标识。比如 http_requests_total{method="POST", handler="/api/xxx"} 就表示一个记录 POST 请求接口总数的时间序列。
这样一来,Prometheus 能够高效地处理监控场景中的时间范围查询,比如过去一小时内各个接口的平均响应时间、CPU 使用率超过 80% 的服务器列表等。
访问 Prometheus 下载页面 ,选择对应的操作系统和架构。
进入对应文件夹,双击 prometheus.exe 启动。
查看默认配置文件 prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
rule_files:
scrape_configs:
- job_name: "prometheus"
static_configs:
[ ]
-
targets:
"localhost:9090"
labels:
app:
"prometheus"
Grafana Grafana 是一个开源的数据可视化平台,专门用于创建监控看板。它可以连接多种数据源(包括 Prometheus、MySQL、PostgreSQL、Elasticsearch 等),并提供丰富的图表类型和可视化选项。
启动 Grafana,Windows 系统直接执行 grafana-server.exe。
Grafana 整合 Prometheus 查看看板详情,一个仪表板可以包含多个 Panel(图表面板)。
每个 Panel 都可以查看具体的数据、状态和查询语句。
右上角可以将整个仪表板导出为 JSON 格式,便于分享和备份。
开发实现
1.引入依赖
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-actuator</artifactId >
</dependency >
<dependency >
<groupId > io.micrometer</groupId >
<artifactId > micrometer-registry-prometheus</artifactId >
</dependency >
Prometheus 可以定期访问 /actuator/prometheus 端点拉取指标数据,实现对 Spring Boot 应用的持续监控和告警。
2.编写配置 在 application.yml 中添加 Actuator 配置,暴露监控端点:
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
show-details: always
3.监控上下文 由于需要在监听器中获取业务维度信息(比如 appId、userId),我们可以通过 ThreadLocal 来传递这些参数。
新建 monitor 包,定义监控上下文类 MonitorContext:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MonitorContext implements Serializable {
private String userId;
private String appId;
@Serial
private static final long serialVersionUID = 1L ;
}
@Slf4j
public class MonitorContextHolder {
private static final ThreadLocal<MonitorContext> CONTEXT_HOLDER = new ThreadLocal <>();
public static void setContext (MonitorContext context) {
CONTEXT_HOLDER.set(context);
}
public static MonitorContext getContext () {
return CONTEXT_HOLDER.get();
}
public static void clearContext () {
CONTEXT_HOLDER.remove();
}
}
在 AppServiceImpl 的 chatToGen 方法中设置上下文(这个方法是与 ai 对话的重要方法),并在 AI 调用流结束时清理:
@Override
public Flux<String> chatToGen (Long appId, String message, User loginUser) {
ThrowUtils.throwIf(appId == null || appId <= 0 , ErrorCode.PARAMS_ERROR, "应用 ID 不能为空" );
ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "用户消息不能为空" );
App app = this .getById(appId);
ThrowUtils.throwIf(app == null , ErrorCode.NOT_FOUND_ERROR, "应用不存在" );
if (!app.getUserId().equals(loginUser.getId())) {
throw new BusinessException (ErrorCode.NO_AUTH_ERROR, "无权限访问该应用" );
}
String chatGenType = app.getChatGenType();
ChatTypeEnum chatTypeEnum = ChatTypeEnum.getEnumByValue(chatGenType);
if (chatTypeEnum == null ) {
throw new BusinessException (ErrorCode.SYSTEM_ERROR, "不支持的对话类型" );
}
chatHistoryService.addChatMessage(appId, message, ChatHistoryMessageTypeEnum.USER.getValue(), loginUser.getId());
MonitorContextHolder.setContext(
MonitorContext.builder()
.userId(loginUser.getId().toString())
.appId(appId.toString())
.build()
);
Flux<String> contentFlux = aiChatFacade.generateAndSaveStreamFacade(message, chatTypeEnum, appId);
StringBuilder aiResponseBuilder = new StringBuilder ();
return contentFlux
.map(chunk -> {
aiResponseBuilder.append(chunk);
return chunk;
})
.doOnComplete(() -> {
String aiResponse = aiResponseBuilder.toString();
if (StrUtil.isNotBlank(aiResponse)) {
chatHistoryService.addChatMessage(appId, aiResponse, ChatHistoryMessageTypeEnum.AI.getValue(), loginUser.getId());
}
})
.doOnError(error -> {
String errorMessage = "AI 回复失败:" + error.getMessage();
chatHistoryService.addChatMessage(appId, errorMessage, ChatHistoryMessageTypeEnum.AI.getValue(), loginUser.getId());
})
.doFinally(signalType -> {
MonitorContextHolder.clearContext();
});
}
注意清理时机应该是在流结束时,而不是方法返回值之前,这样能确保整个请求周期内都能获取到上下文信息。
4.指标收集器 在 monitor 包下编写指标收集器,负责收集业务数据并转换为 Prometheus 指标。
这一步不用想太多,尽量把 最细粒度的数据 按照维度分类统计就好。
指标收集器需要提供几个方法,分别统计请求信息、错误信息、Token 消耗、响应时间:
@Component
@Slf4j
public class AiModelMetricsCollector {
@Resource
private MeterRegistry meterRegistry;
private final ConcurrentMap<String, Counter> requestCountersCache = new ConcurrentHashMap <>();
private final ConcurrentMap<String, Counter> errorCountersCache = new ConcurrentHashMap <>();
private final ConcurrentMap<String, Counter> tokenCountersCache = new ConcurrentHashMap <>();
private final ConcurrentMap<String, Timer> responseTimersCache = new ConcurrentHashMap <>();
public void recordRequest (String userId, String appId, String modelName, String status) {
String key = String.format("%s_%s_%s_%s" , userId, appId, modelName, status);
Counter counter = requestCountersCache.computeIfAbsent(key, k ->
Counter.builder("ai_model_requests_total" )
.description("AI 模型总请求次数" )
.tag("user_id" , userId)
.tag("app_id" , appId)
.tag("model_name" , modelName)
.tag("status" , status)
.register(meterRegistry)
);
counter.increment();
}
public void recordError (String userId, String appId, String modelName, String errorMessage) {
String key = String.format("%s_%s_%s_%s" , userId, appId, modelName, errorMessage);
Counter counter = errorCountersCache.computeIfAbsent(key, k ->
Counter.builder("ai_model_errors_total" )
.description("AI 模型错误次数" )
.tag("user_id" , userId)
.tag("app_id" , appId)
.tag("model_name" , modelName)
.tag("error_message" , errorMessage)
.register(meterRegistry)
);
counter.increment();
}
public void recordTokenUsage (String userId, String appId, String modelName, String tokenType, long tokenCount) {
String key = String.format("%s_%s_%s_%s" , userId, appId, modelName, tokenType);
Counter counter = tokenCountersCache.computeIfAbsent(key, k ->
Counter.builder("ai_model_tokens_total" )
.description("AI 模型 Token 消耗总数" )
.tag("user_id" , userId)
.tag("app_id" , appId)
.tag("model_name" , modelName)
.tag("token_type" , tokenType)
.register(meterRegistry)
);
counter.increment(tokenCount);
}
public void recordResponseTime (String userId, String appId, String modelName, Duration duration) {
String key = String.format("%s_%s_%s" , userId, appId, modelName);
Timer timer = responseTimersCache.computeIfAbsent(key, k ->
Timer.builder("ai_model_response_duration_seconds" )
.description("AI 模型响应时间" )
.tag("user_id" , userId)
.tag("app_id" , appId)
.tag("model_name" , modelName)
.register(meterRegistry)
);
timer.record(duration);
}
}
选择合适的指标类型:Counter 用于计数(请求次数、错误次数、Token 数量);Timer 用于时间测量(AI 模型响应时间)。
使用缓存避免统计对象重复注册:Micrometer 会为相同的维度组合创建唯一的指标,通过缓存可以重用同一个 Counter / Timer 对象,避免每次调用都执行 Counter.builder()...register() 操作。
5.AI 调用监听器 在 monitor 包下编写 AI 模型监控监听器,这是整个监控体系的核心:
@Component
@Slf4j
public class AiModelMonitorListener implements ChatModelListener {
private static final String REQUEST_START_TIME_KEY = "request_start_time" ;
private static final String MONITOR_CONTEXT_KEY = "monitor_context" ;
@Resource
private AiModelMetricsCollector aiModelMetricsCollector;
@Override
public void onRequest (ChatModelRequestContext requestContext) {
requestContext.attributes().put(REQUEST_START_TIME_KEY, Instant.now());
MonitorContext context = MonitorContextHolder.getContext();
if (context == null ) {
context = MonitorContext.builder()
.userId("unknown" )
.appId("unknown" )
.build();
}
requestContext.attributes().put(MONITOR_CONTEXT_KEY, context);
String userId = context.getUserId();
String appId = context.getAppId();
String modelName = requestContext.chatRequest().modelName();
aiModelMetricsCollector.recordRequest(userId, appId, modelName, "started" );
}
@Override
public void onResponse (ChatModelResponseContext responseContext) {
Map<Object, Object> attributes = responseContext.attributes();
MonitorContext context = (MonitorContext) attributes.get(MONITOR_CONTEXT_KEY);
String userId = context.getUserId();
String appId = context.getAppId();
String modelName = responseContext.chatResponse().modelName();
aiModelMetricsCollector.recordRequest(userId, appId, modelName, "success" );
recordResponseTime(attributes, userId, appId, modelName);
recordTokenUsage(responseContext, userId, appId, modelName);
}
@Override
public void onError (ChatModelErrorContext errorContext) {
Map<Object, Object> attributes = errorContext.attributes();
MonitorContext context = (MonitorContext) attributes.get(MONITOR_CONTEXT_KEY);
if (context == null ) {
log.warn("监控上下文为空,跳过错误监控" );
return ;
}
String userId = context.getUserId();
String appId = context.getAppId();
String modelName = errorContext.chatRequest().modelName();
String errorMessage = errorContext.error().getMessage();
aiModelMetricsCollector.recordRequest(userId, appId, modelName, "error" );
aiModelMetricsCollector.recordError(userId, appId, modelName, errorMessage);
recordResponseTime(attributes, userId, appId, modelName);
}
private void recordResponseTime (Map<Object, Object> attributes, String userId, String appId, String modelName) {
Instant startTime = (Instant) attributes.get(REQUEST_START_TIME_KEY);
Duration responseTime = Duration.between(startTime, Instant.now());
aiModelMetricsCollector.recordResponseTime(userId, appId, modelName, responseTime);
}
private void recordTokenUsage (ChatModelResponseContext responseContext, String userId, String appId, String modelName) {
TokenUsage tokenUsage = responseContext.chatResponse().metadata().tokenUsage();
if (tokenUsage != null ) {
aiModelMetricsCollector.recordTokenUsage(userId, appId, modelName, "input" , tokenUsage.inputTokenCount());
aiModelMetricsCollector.recordTokenUsage(userId, appId, modelName, "output" , tokenUsage.outputTokenCount());
aiModelMetricsCollector.recordTokenUsage(userId, appId, modelName, "total" , tokenUsage.totalTokenCount());
}
}
}
线程切换问题:请求监听在主线程,但响应监听可能在另一个线程,所以要通过 AI context 的 attributes 传递参数。
时间计算:在请求开始时记录时间戳,在响应完成时计算耗时。
错误处理:即使发生错误也要记录响应时间,便于分析错误请求的特征。
6.注册监听器到 AI 模型中 在 config 包下新建 AiModelConfig 类:
@Configuration
public class AiModelConfig {
@Resource
private AiModelMonitorListener aiModelMonitorListener;
@Value("${langchain4j.community.dashscope.streaming-chat-model.api-key}")
private String apiKey;
@Value("${langchain4j.community.dashscope.streaming-chat-model.model-name}")
private String modelName;
@Value("${langchain4j.community.dashscope.streaming-chat-model.max-tokens:8129}")
private Integer maxTokens;
@Bean
@Primary
public StreamingChatModel streamingChatModel () {
return QwenStreamingChatModel.builder()
.apiKey(apiKey)
.modelName(modelName)
.maxTokens(maxTokens)
.listeners(List.of(aiModelMonitorListener))
.build();
}
}
7.测试验证 通过前端发起一次 AI 对话请求,触发请求监听,能够获取到上下文信息。
8.Prometheus 配置 现在需要配置 Prometheus 定期 从我们的应用拉取监控数据 ,可以通过修改 Prometheus 目录下的 prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
rule_files:
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090' ]
- job_name: 'AlzAssistant'
metrics_path: '/api/actuator/prometheus'
static_configs:
- targets: ['localhost:8126' ]
scrape_interval: 10s
scrape_timeout: 10s
在 Prometheus 的 Status -> Target health 页面可以查看抓取状态。
9.Grafana 可视化监控配置 有了数据后,接下来在 Grafana 中创建可视化看板。
1)手动配置看板
选择图表类型为 Bar Chart,设置基础信息。
在查询编辑器中填写 PromQL 表达式:topk(10, sum(ai_model_tokens_total{token_type="total"}) by (app_id))。
查询编辑器中,勾选 Instant 选项。这样只会返回当前时刻的值,而不是时间序列。
查询编辑器中,修改面板类型,将面板类型从 Time series 改为 Table。
2)通过 json 配置来导入看板 我们可以利用 AI 生成 json 配置,提示词如下:
帮我根据分析需求、数据上报相关代码、示例从 Prometheus 收集到的数据,来生成 Grafana 看板的 JSON 导入代码,全部汇总到一个看板中。
相关的规范参考:https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/
// ... 补充分析需求
// ... 补充 AiModelMetricsCollector.java 的代码
HELP ai_model_requests_total AI 模型总请求次数
TYPE ai_model_requests_total counter
ai_model_requests_total{app_id ="313129227198590976" ,model_name="deepseek-chat" ,status="started" ,user_id="302588523967918080" } 2.0
ai_model_requests_total{app_id ="313129227198590976" ,model_name="deepseek-chat" ,status="success" ,user_id="302588523967918080" } 2.0
HELP ai_model_response_duration_seconds AI 模型响应时间
TYPE ai_model_response_duration_seconds summary
ai_model_response_duration_seconds_count{app_id ="313129227198590976" ,model_name="deepseek-chat" ,user_id="302588523967918080" } 2
ai_model_response_duration_seconds_sum{app_id ="313129227198590976" ,model_name="deepseek-chat" ,user_id="302588523967918080" } 91.285863
HELP ai_model_tokens_total AI 模型 Token 消耗总数
TYPE ai_model_tokens_total counter
ai_model_tokens_total{app_id ="313129227198590976" ,model_name="deepseek-chat" ,token_type="input" ,user_id="302588523967918080" } 1321.0
ai_model_tokens_total{app_id ="313129227198590976" ,model_name="deepseek-chat" ,token_type="output" ,user_id="302588523967918080" } 519.0
ai_model_tokens_total{app_id ="313129227198590976" ,model_name="deepseek-chat" ,token_type="total" ,user_id="302588523967918080" } 1840.0
附:json 配置示例 相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online