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"
访问 http://localhost:9090 进入管理界面。
建议开启本地时间(不然时间可能少 8 个小时)。
Grafana
Grafana 是一个开源的数据可视化平台,专门用于创建监控看板。它可以连接多种数据源(包括 Prometheus、MySQL、PostgreSQL、Elasticsearch 等),并提供丰富的图表类型和可视化选项。
访问 Grafana 下载页面,根据操作系统选择对应的安装包。
按照对应系统的 安装文档 进行安装。
启动 Grafana,Windows 系统直接执行 grafana-server.exe。
访问 http://localhost:3000 查看看板,默认登录账号密码都是 admin,可以参考 起始文档 来学习如何使用 Grafana 构造看板。
Grafana 整合 Prometheus
添加 Prometheus 数据源。
配置 Prometheus 服务器地址。
测试连接。
点击导出。
进入看板页面,查看导入的看板。
查看看板详情,一个仪表板可以包含多个 Panel(图表面板)。
每个 Panel 都可以查看具体的数据、状态和查询语句。
右上角可以将整个仪表板导出为 JSON 格式,便于分享和备份。
同样也可以通过导入 JSON 快速创建仪表板。
开发实现
1.引入依赖
在 pom.xml 中添加必要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
-
spring-boot-starter-actuator:Actuator 提供生产就绪的监控基础设施,暴露各种管理和监控端点。它是应用与外部监控系统交互的窗口,但本身不负责指标数据的收集。
-
micrometer-core:Micrometer 是真正的指标收集引擎,负责收集 JVM、HTTP、数据库等各种指标数据。它提供统一的 API 让开发者可以创建自定义指标(类似于一个门面),是整个监控体系的数据生产者。
-
micrometer-registry-prometheus:Prometheus Registry 专门负责将 Micrometer 收集的指标数据转换为 Prometheus 格式。它创建 /actuator/prometheus 端点,让 Prometheus 服务器可以直接拉取标准格式的监控数据。
Prometheus 可以定期访问 /actuator/prometheus 端点拉取指标数据,实现对 Spring Boot 应用的持续监控和告警。
2.编写配置
在 application.yml 中添加 Actuator 配置,暴露监控端点:
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
show-details: always
重启项目后,可以访问端点验证配置。比如健康检查端点:localhost:8126/api/actuator/health。
Prometheus 指标端点:localhost:8126/api/actuator/prometheus,可以看到 Spring Boot 默认提供的各种系统指标。
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);
();
contentFlux
.map(chunk -> {
aiResponseBuilder.append(chunk);
chunk;
})
.doOnComplete(() -> {
aiResponseBuilder.toString();
(StrUtil.isNotBlank(aiResponse)) {
chatHistoryService.addChatMessage(appId, aiResponse, ChatHistoryMessageTypeEnum.AI.getValue(), loginUser.getId());
}
})
.doOnError(error -> {
+ 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();
}
{
String.format(, userId, appId, modelName, errorMessage);
errorCountersCache.computeIfAbsent(key, k ->
Counter.builder()
.description()
.tag(, userId)
.tag(, appId)
.tag(, modelName)
.tag(, errorMessage)
.register(meterRegistry)
);
counter.increment();
}
{
String.format(, userId, appId, modelName, tokenType);
tokenCountersCache.computeIfAbsent(key, k ->
Counter.builder()
.description()
.tag(, userId)
.tag(, appId)
.tag(, modelName)
.tag(, tokenType)
.register(meterRegistry)
);
counter.increment(tokenCount);
}
{
String.format(, userId, appId, modelName);
responseTimersCache.computeIfAbsent(key, k ->
Timer.builder()
.description()
.tag(, userId)
.tag(, appId)
.tag(, 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();
requestContext.chatRequest().modelName();
aiModelMetricsCollector.recordRequest(userId, appId, modelName, );
}
{
Map<Object, Object> attributes = responseContext.attributes();
(MonitorContext) attributes.get(MONITOR_CONTEXT_KEY);
context.getUserId();
context.getAppId();
responseContext.chatResponse().modelName();
aiModelMetricsCollector.recordRequest(userId, appId, modelName, );
recordResponseTime(attributes, userId, appId, modelName);
recordTokenUsage(responseContext, userId, appId, modelName);
}
{
Map<Object, Object> attributes = errorContext.attributes();
(MonitorContext) attributes.get(MONITOR_CONTEXT_KEY);
(context == ) {
log.warn();
;
}
context.getUserId();
context.getAppId();
errorContext.chatRequest().modelName();
errorContext.error().getMessage();
aiModelMetricsCollector.recordRequest(userId, appId, modelName, );
aiModelMetricsCollector.recordError(userId, appId, modelName, errorMessage);
recordResponseTime(attributes, userId, appId, modelName);
}
{
(Instant) attributes.get(REQUEST_START_TIME_KEY);
Duration.between(startTime, Instant.now());
aiModelMetricsCollector.recordResponseTime(userId, appId, modelName, responseTime);
}
{
responseContext.chatResponse().metadata().tokenUsage();
(tokenUsage != ) {
aiModelMetricsCollector.recordTokenUsage(userId, appId, modelName, , tokenUsage.inputTokenCount());
aiModelMetricsCollector.recordTokenUsage(userId, appId, modelName, , tokenUsage.outputTokenCount());
aiModelMetricsCollector.recordTokenUsage(userId, appId, modelName, , tokenUsage.totalTokenCount());
}
}
}
有几个重要细节需要注意:
- 线程切换问题:请求监听在主线程,但响应监听可能在另一个线程,所以要通过 AI context 的 attributes 传递参数。
- 时间计算:在请求开始时记录时间戳,在响应完成时计算耗时。
- 错误处理:即使发生错误也要记录响应时间,便于分析错误请求的特征。
6.注册监听器到 AI 模型中
需要将监听器注册到 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)手动配置看板
创建仪表板。
手动配置图表,以应用 Token 消耗排行为例。
配置步骤:
- 选择图表类型为 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=,token_type=,user_id=}
进入 Dashboards 页面。
点击右上角,选择 import。
复制粘贴到下方的框框里即可,点击 load。
效果如图。
附:json 配置示例
生成的配置可直接用于导入 Grafana 看板。