Prometheus监控组件在SpringBoot项目中使用实践

Prometheus监控组件在SpringBoot项目中使用实践

  • 时间:2024/7/29

  • 背景:本人最近参与的一个项目,要监控远程软硬件以及本地软硬件,实现远程监控以及告警功能。

  • 开发环境: JDK1.8,Maven,PostgreSQL,SpringBoot2.5.7,Prometheus

  • 需解决问题:1、如何获取远程软硬件监控信息 2、如何获取本地软硬件监控信息 3、远程和本地监控信息是否统一处理 4、如何设置Prometheus告警发送,并通过Java转发给前端

  • PostgreSQL,Prometheus依赖

    #PostgreSQL jdbc依赖
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.5</version>
        <scope>runtime</scope>
    </dependency>
    
    #Prometheus依赖
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
    
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient</artifactId>
        <version>0.11.0</version>
    </dependency>
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient_common</artifactId>
        <version>0.11.0</version>
    </dependency>
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient_hotspot</artifactId>
        <version>0.11.0</version>
    </dependency>
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient_httpserver</artifactId>
        <version>0.11.0</version>
    </dependency>
    
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient</artifactId>
        <version>0.11.0</version>
    </dependency>
    

​ 如果你对Prometheus完全不了解,可以先看第五部分Prometheus的简单介绍。第一到第四部分围绕项目中遇到的问题展开。

1、如何获取远程软硬件监控信息

1.需求分析

​ 由于要监控的远程服务器软硬件,本身属于涉密相关,无法通过传统的远程ssh等方式直接连接等方式来获取数据。所以第三方给我们提供了一个接口,和调用该接口的token,通过调用该接口拿到远程软硬件监控信息。

2.实现思路

​ 因此只需要在我们SpringBoot中写一个Ctroller接口来调用第三方接口并将其返回的监控数据转成Prometheus需要的格式。然后在Prometheus设置定时从我们这个接口拿数据(这块是运维人员来设置的)

3.主要问题与解答

​ 根据上述"实现思路",分析可得出以下2个问题

  1. 如何将第三方接口返回的监控数据转成Prometheus需要的格式?

    答:Gauge类,来自package io.prometheus.client包。我们根据第三方接口返回的内容来创建不同的Gauge对象。

    以某个第三方接口返回的值为例,返回的内容如下:

    image-20240801170321151

    ​ 根据接口返回值,我们可以设置Gauge对象如下。name和help字段都是固定可以如下这样写,labelNames字段用来标记该Gauge(labelNames里面可以人为约定字段,如tag就是我们人为指定的一个字段,可用来存放一些信息,可用来区分不同的queryTime,后面也可以从tag中取值),register(commandRegistry)是将Gauge对象注册到commandRegistry注册器中(注意Gauge对象必须注册到一个注册器中,否则Prometheus接收不到该数据)。

        //在Controller类中定义注册器commandRegistry
        private static final CollectorRegistry commandRegistry = new CollectorRegistry();
        private static final Gauge queryTime = Gauge.build()
                .name("query_time")
                .help("Query Time")
                .labelNames("tag")
                .register(commandRegistry); //将queryTime注册到commandRegistry注册器中
    
        private static final Gauge commandAircraftId = Gauge.build()
                .name("command_aircraft_id")
                .help("Command Aircraft Id")
                .labelNames("tag")
                .register(commandRegistry);
    
        private static final Gauge commandAircraftName = Gauge.build()
                .name("command_aircraft_name")
                .help("Command Aircraft Name")
                .labelNames("tag")
                .register(commandRegistry);
    
        private static final Gauge commandAircraftStatus = Gauge.build()
                .name("command_aircraft_status")
                .help("Command Aircraft Status")
                .labelNames("tag")
                .register(commandRegistry);
    

    ​ 然后怎么使用这些Gauge对象呢?还是以上述第三方接口为例,请看下面一段代码。这里面的resultList是调用第三方接口接收到的数据。注意guaua对象可以通过set来指定值,但值只能是数字类型的,不能是字符串类型

    /**
         * 将List<CommandAircraftInfo.Result>转成Gauge并注册
         */
    private void resultsToGauge(List<CommandAircraftInfo.Result> resultList) {
        for (int i = 0; i < resultList.size(); i++) {
            CommandAircraftInfo.Result result = resultList.get(i);
    	    //重重重重点
            //Guaua对象可通过set来设置值,但值只能是数字类型的。这里是将Guaua对象queryTime设置为当前时间戳
            queryTime.labels(serverConfig.getCommandChinese() + i).setToCurrentTime(); 
            //将Guaua对象commandAircraftId的值设为result.getCommandAircraftId()
            commandAircraftId.labels(serverConfig.getCommandChinese() + i).set(null != result.getCommandAircraftId() ? Double.parseDouble(result.getCommandAircraftId()) : -1); //-1表示没有id
            //因为result.getCommandAircraftName()是字符串,不能用set赋值,就set(i)表示是第几个
            commandAircraftName.labels(serverConfig.getCommandChinese() + i).set(i);
            //同id,可以用set赋值
            commandAircraftStatus.labels(serverConfig.getCommandChinese() + i).set(null != result.getCommandAircraftStatus() ? Double.parseDouble(result.getCommandAircraftStatus()) : -1); //-1表示未知情况
            
        }
    }
    
    1. 如何将转成Prometheus需要的格式的数据写到Prometheus中?

      答:response.setContentType(TextFormat.CONTENT_TYPE_004); Writer writer = response.getWriter(); TextFormat.write004(writer, commandRegistry.metricFamilySamples()); writer.close();

      下面为调用第三方API获得相关数据并提交给Prometheus的接口代码

    /**
     * 调用第三方API获得相关数据并提交给Prometheus
     *
     * @param response
     * @throws IOException
     */
    @GetMapping("/metric")
    public void metric(HttpServletResponse response) throws IOException {
        RestTemplate restTemplate = new RestTemplate();
        String url = "http://" + serverConfig.getCommandAircraftIp() + ":" + serverConfig.getCommandAircraftPort() + serverConfig.getCommandAircraftInfo() + serverConfig.getIPC_TOKEN(); //调用的API地址
        CommandAircraftInfo commandAircraftInfo = restTemplate.getForObject(url, CommandAircraftInfo.class);
    
        if (commandAircraftInfo != null && "200".equals(commandAircraftInfo.getStatus())) {
            List<CommandAircraftInfo.Result> resultList = commandAircraftInfo.getResult();
            //如果resultList.size()大于commandAircraftSum
            commandAircraftSum = resultList.size(); //更新指挥机数量
            
            resultsToGauge(resultList);
            response.setContentType(TextFormat.CONTENT_TYPE_004);
            Writer writer = response.getWriter();
            TextFormat.write004(writer, commandRegistry.metricFamilySamples());
            writer.close();
        } else {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
    

2、如何获取本地软硬件监控信息

​ Prometheus可以通过配置来监控本地软硬件信息,监控远程软硬件信息。

​ 实际只要将每一项中的 targets和 metrics_path配置完整即可。

​ 例如:targets: [‘10.0.4.225:9401’] 和 metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/fusion/hardware/metric 写完整。拼在一起就是10.0.4.225:9401:/bdfl2/api/v1/rest/services/cms/monitor/fusion/hardware/metric,这个路径就算我们后端服务中将第三方接口中数据转成Prometheus格式的接口路径。点进去如下

image-20240802160028221

​ 再点击Metrics,进入Prometheus监控数据界面

image-20240802160114977

​ 本项目详细prometheus.yml配置文件如下,

global:
  scrape_interval:     5s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.

# alertmanager的地址
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093
 
# 读取告警触发条件规则
rule_files:
  - 'rules/*.yml'

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['prometheus:9090']
        labels:
          instance: prometheus
          tags: 10.0.4.29 #prometheus部署服务器IP/服务器名称 (这里就是用来监控本地服务器的硬件信息)
          
#####################主机监控############################
  - job_name: 'node_exporter'
    # 抓取间隔 
    scrape_interval: 8s
    static_configs:
      - targets: ['10.0.4.225:9401']
        labels:
          instance: 10.0.4.225
          group: '服务器-10.0.4.225' #标识:区别不同的服务器
      - targets: ['10.0.4.222:9400']
        labels:
          instance: 10.0.4.222
          group: '服务器-10.0.4.222' #标识:区别不同的服务器 
                   
#####################数据库监控#####################
  - job_name: 'postgresql'
    static_configs:
      - targets: ['10.0.4.32:9187']
        labels:
          instance: postgres
          tags: 10.0.4.225 #postgresql部署服务器IP/服务器名称
          group: 'pg数据库-10.0.4.225' #标识:区别不同的服务器的postgresql数据库
          
#####################nginx监控#####################          
  - job_name: 'nginx_exporter'
    static_configs:
      - targets: ['10.0.4.32:9113']
        labels:
          instance: nginx 
          tags: 10.0.4.225 #nginx部署服务器IP/服务器名称
          group: 'nginx-10.0.4.225' #标识:区别不同的服务器的nginx服务
                       
##################tcp监控###################################
  - job_name: "tcp_port_status"
    metrics_path: /probe #Prometheus从目标HTTP服务抓取的度量指标(metrics)的路径
    params:
      module: [tcp_connect]
    static_configs:
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225:9602
          tags: bdfl-server #被监控应用服务说明
          group: 'bdfl-server-10.0.4.225' #标识:区别不同的服务
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 10.0.4.32:9115
        
# # # # # # # # # 某某机 # # # # # # # # # # # # # #       
  - job_name: 'rongheji-hardware'
    scrape_interval: 8s
    metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/fusion/hardware/metric
    static_configs:
      - targets: ['10.0.4.94:9602']
        labels:
          instance: 10.0.4.94 #某某机IP地址或编号
          group: '某某机-10.0.4.94' #标识:区别不同的融合处理机
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225 #某某机IP地址或编号
          group: '某某机-10.0.4.225' #标识:区别不同的某某机
                                 
  - job_name: 'rongheji-software'
    scrape_interval: 8s
    metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/fusion/software/metric
    static_configs:
      - targets: ['10.0.4.94:9602']
        labels:
          instance: 10.0.4.94 #某某机IP地址或编号
          group: '某某机-10.0.4.94' #标识:区别不同的某某机
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225 #某某机IP地址或编号
          group: '某某机-10.0.4.225' #标识:区别不同的某某机
          
                        
# # # # # # # # #指挥机 # # # # # # # # # # # # # #       
  - job_name: 'Command-metric'
    scrape_interval: 8s
    metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/command/aircraft/metric
    static_configs:
      - targets: ['10.0.4.94:9602']
        labels:
          instance: 10.0.4.94 #指挥机IP地址或编号
          group: '指挥机-10.0.4.94' #标识:区别不同的指挥机
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225 #指挥机IP地址或编号  
          group: '指挥机-10.0.4.225' #标识:区别不同的指挥机
                       
  - job_name: 'Command-starry'
    scrape_interval: 8s
    metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/command/aircraft/starry-sky-info
    static_configs:
      - targets: ['10.0.4.94:9602']
        labels:
          instance: 10.0.4.94 #指挥机IP地址或编号
          group: '指挥机-10.0.4.94' #标识:区别不同的指挥机
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225 #指挥机IP地址或编号
          group: '指挥机-10.0.4.225' #标识:区别不同的指挥机
      
  - job_name: 'Command-wave'
    scrape_interval: 8s
    metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/command/aircraft/wave-beam-info
    static_configs:
      - targets: ['10.0.4.94:9602']
        labels:
          instance: 10.0.4.94 #指挥机IP地址或编号
          group: '指挥机-10.0.4.94' #标识:区别不同的指挥机
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225 #指挥机IP地址或编号
          group: '指挥机-10.0.4.225' #标识:区别不同的指挥机
      
# # # # # # # # # 某某卡 # # # # # # # # # # # # # #       
  - job_name: 'Command-userCard'
    scrape_interval: 8s
    metrics_path: /bdfl2/api/v1/rest/services/cms/monitor/command/aircraft/user-card-info
    static_configs:
      - targets: ['10.0.4.94:9602']
        labels:
          instance: 10.0.4.94 #指挥机IP地址或编号
          group: '指挥机-10.0.4.94' #标识:区别不同的指挥机
      - targets: ['10.0.4.225:9602']
        labels:
          instance: 10.0.4.225 #指挥机IP地址或编号
          group: '指挥机-10.0.4.225' #标识:区别不同的指挥机

3、远程和本地监控信息是否统一处理

​ 可能有同学会有疑问,为何后端不将第三方接口数据直接返回给前端,反而要这么麻烦转成Prometheus的格式,让Prometheus拿到呢。因为Prometheus有非常强大告警功能,而后端将第三方数据返回给前端,要自己做报警功能,根据数据来判断是否报警,这增加了业务的复杂性。所以最好让远程和本地要监控的软硬件信息都统一从Prometheus中获取,统一由Prometheus发出警告。

4、如何设置Prometheus告警发送,并通过Java转发给前端

​ 见prometheus.yml配置了警告规则,然后Prometheus会提供一个获取所有告警信息的接口给你。你可以通过这个接口拿到告警信息,剩下的就根据业务来对告警信息做相应的处理就行了。比如我这的业务就是定时扫描该接口拿到告警数据保存到数据库,然后展示给前端。

​ 我这边Prometheus提供的接口为http://10.0.4.32:9093/api/v2/alerts/groups?silenced=false&inhibited=false&active=true,点进去显示如下,可以用json工具JSON在线解析及格式化验证 - JSON中文网打开,方便看

image-20240802164502563

​ 对了有的报警没有处理,一直再,那保存到数据库中是每次都保存岂不是很麻烦?Prometheus的alerts中给你提供了一个字段叫**“fingerprint”,可用来唯一标识该告警信息,你只需要把这个字段也保存到数据存,然后存告警信息前判断一下告警信息的"fingerprint"**在数据库里有没有。没有就存,有的话就不存。我的定时保存告警代码在service层中,如下

@Slf4j
@Service
public class AlertRemindServiceImpl extends ServiceImpl<AlertRemindMapper, AlertRemind> implements AlertRemindService {

    private final AlertRemindMapper alertRemindMapper;

    private final ServerConfig serverConfig;
    public AlertRemindServiceImpl(AlertRemindMapper reminderMapper, ServerConfig serverConfig) {
        this.alertRemindMapper = reminderMapper;
        this.serverConfig = serverConfig;
    }
    
    
    @Scheduled(fixedDelay = 10 * 1000)
    public void refreshShortMessageByRest() {
        log.info("向Prometheus获取全部告警信息!");
        RestTemplate restTemplate = new RestTemplate();
        //调用Prometheus告警api,List<PrometheusAlert>
        ResponseEntity<List> prometheusAlerts = restTemplate.getForEntity(serverConfig.getPrometheusAlertUrl(), List.class);
        if (CollectionUtils.isEmpty(prometheusAlerts.getBody())) {
            log.error("Prometheus告警数据为空!");
            return;
        }
        prometheusAlerts.getBody().forEach(item -> {
            JSONObject jsonObject = new JSONObject((HashMap<String, Object>) item);
            PrometheusAlert prometheusAlert = jsonObject.toJavaObject(PrometheusAlert.class);
            for (int i = 0; i < prometheusAlert.getAlerts().size(); i++) {
                String fingerprint = prometheusAlert.getAlerts().get(i).getFingerprint();

                if (StringUtils.isEmpty(fingerprint)) {
                    log.error("告警信息fingerprint为空!");
                    continue;
                }

                if (StringUtils.isEmpty(prometheusAlert.getAlerts().get(i).getLabels().getGroup())) {
                    log.error("告警信息group为空!");
                    continue;
                }

                //判断该alert的是否要保存(若description和summary已经在数据库中则不需要保存)
                if (judgeIsOrNotSave(fingerprint)) {
                    //需要保存
                    AlertRemind alertRemind = new AlertRemind();
                    alertRemind.setDescription(prometheusAlert.getAlerts().get(i).getAnnotations().getDescription());
                    alertRemind.setSummary(prometheusAlert.getAlerts().get(i).getAnnotations().getSummary());
                    alertRemind.setAfterRead(0);
                    alertRemind.setType(prometheusAlert.getAlerts().get(i).getLabels().getName());
                    alertRemind.setId(SnowFlakeIdGenerator.getInstance().nextId());
                    String startsAt = prometheusAlert.getAlerts().get(i).getStartsAt();
                    String endsAt = prometheusAlert.getAlerts().get(i).getEndsAt();
                    //将时间格式2024-07-29T01:59:04.561Z 转成时间戳并保存
                    alertRemind.setCreateTime(Instant.parse(startsAt).toEpochMilli());
                    alertRemind.setUpdateTime(Instant.parse(endsAt).toEpochMilli());
                    alertRemind.setSeverity(prometheusAlert.getAlerts().get(i).getLabels().getSeverity());
                    alertRemind.setFingerprint(prometheusAlert.getAlerts().get(i).getFingerprint());
                    alertRemind.setGroupUrl(prometheusAlert.getAlerts().get(i).getLabels().getGroup());
                    this.save(alertRemind);
                    log.info("新增告警信息:{}", alertRemind);
                }else {
                    //不需要保存
                    log.info("告警信息已存在,fingerprint:{}", fingerprint);
                }
            }
        });
    }
    private boolean judgeIsOrNotSave(String fingerprint) {
        LambdaQueryWrapper<AlertRemind> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StringUtils.isNotEmpty(fingerprint),AlertRemind::getFingerprint, fingerprint);
        return this.count(wrapper) == 0;
    }
}

5、Prometheus简单介绍(这部分内容来自GPT4-0,可跳过)

Prometheus 是一个开源的系统监控和报警工具包,是监控和告警解决方案的一个重要组成部分。Prometheus 的设计思想主要是通过时间序列数据进行监控和告警。下面是对 Prometheus 的一些关键特性的简单介绍:

1. 时间序列数据存储

Prometheus 使用时间序列数据存储,即以时间为索引的数据点。每个数据点都有一个时间戳和一个对应的值。数据点通过拉取(pull)模型从目标采集。

2. 多维数据模型

Prometheus 使用多维数据模型,通过指标名称和键值对(标签)来识别数据。例如,一个 CPU 使用率的指标可以有不同的标签来区分主机、实例或应用程序。

3. 高效的数据查询语言 - PromQL

Prometheus 提供了一个功能强大的查询语言 PromQL,用于对收集到的时间序列数据进行查询和分析。用户可以使用 PromQL 来生成即时的图表、统计数据或触发告警。

4. 自定义告警管理

Prometheus 内置了告警管理功能,通过定义告警规则,当某些条件满足时,会触发告警。告警规则使用 PromQL 表达式来定义告警条件。Prometheus 还可以将告警发送到 Alertmanager 进行进一步处理,例如发送邮件、短信或其他通知。

5. 易于扩展的生态系统

Prometheus 有着丰富的生态系统,支持多种导出器(exporters)和集成。导出器用于将不同系统或服务的指标数据导出到 Prometheus。例如,Node Exporter 可以用于监控 Linux 服务器的硬件和操作系统指标。

6. 服务发现和静态配置

Prometheus 支持多种服务发现机制,可以自动发现监控目标,例如 Kubernetes、Consul、Etcd 等。也可以通过静态配置方式手动添加监控目标。

7. 图形展示

Prometheus 本身提供了一个简单的 web 界面来展示监控数据和进行 PromQL 查询。此外,Prometheus 也与 Grafana 等图形展示工具高度集成,用户可以使用 Grafana 创建更复杂和美观的监控仪表盘。

8. 支持远程存储

虽然 Prometheus 内置的存储适用于中小规模的监控需求,但对于大规模或长期数据存储需求,可以使用远程存储解决方案,例如 Cortex、Thanos 等。

使用 Prometheus 的基本流程

  1. 安装 Prometheus:可以通过二进制文件、Docker 或 Kubernetes 部署 Prometheus。
  2. 配置 Prometheus:通过 prometheus.yml 文件配置数据采集目标和告警规则。
  3. 启动 Prometheus:运行 Prometheus 服务,开始数据采集。
  4. 定义告警规则:根据监控需求定义告警规则,并配置 Alertmanager 进行通知处理。
  5. 数据查询和可视化:使用 PromQL 查询数据,使用内置 web 界面或 Grafana 进行数据可视化。

示例配置

下面是一个简单的 prometheus.yml 配置示例:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']

这个配置文件定义了一个名为 node 的抓取任务,它会每隔 15 秒从 localhost:9100 采集数据。

总的来说,Prometheus 是一个功能强大且灵活的监控和告警系统,适用于多种场景下的监控需求。其开源特性和活跃的社区使得它成为 DevOps 和 SRE 工具链中的重要组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值