Spring Boot如何自定義監控指標

2023-03-04 06:00:52

1.建立專案

pom.xml引入相關依賴

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.olive</groupId>
	<artifactId>prometheus-meter-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.7.RELEASE</version>
		<relativePath />
	</parent>
	<properties>
		<java.version>1.8</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<!-- Micrometer Prometheus registry  -->
		<dependency>
			<groupId>io.micrometer</groupId>
			<artifactId>micrometer-registry-prometheus</artifactId>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>${spring-boot.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
</project>

2.自定義指標

  • 方式一

直接使用micrometer核心包的類進行指標定義和註冊

package com.olive.monitor;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;

@Component
public class NativeMetricsMontior {

	/**
	 * 支付次數
	 */
	private Counter payCount;

	/**
	 * 支付金額統計
	 */
	private DistributionSummary payAmountSum;

	@Autowired
	private MeterRegistry registry;

	@PostConstruct
	private void init() {
		payCount = registry.counter("pay_request_count", "payCount", "pay-count");
		payAmountSum = registry.summary("pay_amount_sum", "payAmountSum", "pay-amount-sum");
	}

	public Counter getPayCount() {
		return payCount;
	}

	public DistributionSummary getPayAmountSum() {
		return payAmountSum;
	}

}
  • 方式二

通過引入micrometer-registry-prometheus包,該包結合prometheus,對micrometer進行了封裝

<dependency>
			<groupId>io.micrometer</groupId>
			<artifactId>micrometer-registry-prometheus</artifactId>
		</dependency>

同樣定義兩個metrics

package com.olive.monitor;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Counter;

@Component
public class PrometheusMetricsMonitor {

	/**
	 * 訂單發起次數
	 */
	private Counter orderCount;

	/**
	 * 金額統計
	 */
	private Counter orderAmountSum;
	
	@Autowired
	private CollectorRegistry registry;
	
	
	@PostConstruct
	private void init() {
		orderCount = Counter.build().name("order_request_count")
				.help("order request count.")
				.labelNames("orderCount")
				.register();
		orderAmountSum = Counter.build().name("order_amount_sum")
				.help("order amount sum.")
				.labelNames("orderAmountSum")
				.register();
		registry.register(orderCount);
		registry.register(orderAmountSum);
	}

	public Counter getOrderCount() {
		return orderCount;
	}

	public Counter getOrderAmountSum() {
		return orderAmountSum;
	}

}

prometheus 4種常用Metrics

Counter

連續增加不會減少的計數器,可以用於記錄只增不減的型別,例如:網站存取人數,系統執行時間等。

對於Counter型別的指標,只包含一個inc()的方法,就是用於計數器+1.

一般而言,Counter型別的metric指標在冥冥中我們使用_total結束,如http_requests_total.

Gauge

可增可減的儀表盤,曲線圖

對於這類可增可減的指標,用於反應應用的當前狀態。

例如在監控主機時,主機當前空閒的記憶體大小,可用記憶體大小等等。

對於Gauge指標的物件則包含兩個主要的方法inc()和dec(),用於增加和減少計數。

Histogram

主要用來統計資料的分佈情況,這是一種特殊的metrics資料型別,代表的是一種近似的百分比估算數值,統計所有離散的指標資料在各個取值區段內的次數。例如:我們想統計一段時間內http請求響應小於0.005秒、小於0.01秒、小於0.025秒的資料分佈情況。那麼使用Histogram採集每一次http請求的時間,同時設定bucket。

Summary

Summary和Histogram非常相似,都可以統計事件發生的次數或者大小,以及其分佈情況,他們都提供了對時間的計數_count以及值的彙總_sum,也都提供了可以計算統計樣本分佈情況的功能,不同之處在於Histogram可以通過histogram_quantile函數在伺服器計算分位數。而Sumamry的分位數則是直接在使用者端進行定義的。因此對於分位數的計算,Summary在通過PromQL進行查詢的時候有更好的效能表現,而Histogram則會消耗更多的資源,但是相對於使用者端而言Histogram消耗的資源就更少。用哪個都行,根據實際場景自由調整即可。

3. 測試

定義兩個controller分別使用NativeMetricsMontiorPrometheusMetricsMonitor

package com.olive.controller;

import java.util.Random;

import javax.annotation.Resource;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.olive.monitor.NativeMetricsMontior;

@RestController
public class PayController {

	@Resource
	private NativeMetricsMontior monitor;

	@RequestMapping("/pay")
	public String pay(@RequestParam("amount") Double amount) throws Exception {
		// 統計支付次數
		monitor.getPayCount().increment();

		Random random = new Random();
		//int amount = random.nextInt(100);
		if(amount==null) {
			amount = 0.0;
		}
		// 統計支付總金額
		monitor.getPayAmountSum().record(amount);
		return "支付成功, 支付金額: " + amount;
	}

}
package com.olive.controller;

import java.util.Random;

import javax.annotation.Resource;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.olive.monitor.PrometheusMetricsMonitor;

@RestController
public class OrderController {

	@Resource
	private PrometheusMetricsMonitor monitor;

	@RequestMapping("/order")
	public String order(@RequestParam("amount") Double amount) throws Exception {
		// 訂單總數
		monitor.getOrderCount()
			.labels("orderCount")
			.inc();

		Random random = new Random();
		//int amount = random.nextInt(100);
		if(amount==null) {
			amount = 0.0;
		}
		// 統計訂單總金額
		monitor.getOrderAmountSum()
			.labels("orderAmountSum")
			.inc(amount);
		return "下單成功, 訂單金額: " + amount;
	}

}

啟動服務

存取http://127.0.0.1:9595/actuator/prometheus;正常看到監測資料

改變amount多次方式http://127.0.0.1:8080/order?amount=100http://127.0.0.1:8080/pay?amount=10後;再存取http://127.0.0.1:9595/actuator/prometheus。檢視監控資料

4.專案中的應用

專案中按照上面說的方式進行資料埋點監控不太現實;在spring專案中基本通過AOP進行埋點監測。比如寫一個切面Aspect;這樣的方式就非常友好。能在入口就做了資料埋點監測,無須在controller裡進行程式碼編寫。

package com.olive.aspect;

import java.time.LocalDate;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import io.micrometer.core.instrument.Metrics;

@Aspect
@Component
public class PrometheusMetricsAspect {

    // 切入所有controller包下的請求方法
    @Pointcut("execution(* com.olive.controller..*.*(..))")
    public void controllerPointcut() {
    }

    @Around("controllerPointcut()")
    public Object MetricsCollector(ProceedingJoinPoint joinPoint) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String userId = StringUtils.hasText(request.getParameter("userId")) ? 
        		request.getParameter("userId") : "no userId";
        
        // 獲取api url
        String api = request.getServletPath();
        // 獲取請求方法
        String method = request.getMethod();
        long startTs = System.currentTimeMillis();
        LocalDate now = LocalDate.now();
        String[] tags = new String[10];
        tags[0] = "api";
        tags[1] = api;
        tags[2] = "method";
        tags[3] = method;
        tags[4] = "day";
        tags[5] = now.toString();
        tags[6] = "userId";
        tags[7] = userId;
        
        String amount = StringUtils.hasText(request.getParameter("amount")) ? 
        		request.getParameter("amount") : "0.0";
        
        tags[8] = "amount";
        tags[9] = amount;
        // 請求次數加1
        //自定義的指標名稱:custom_http_request_all,指標包含資料
        Metrics.counter("custom_http_request_all", tags).increment();
        Object object = null;
        try {
            object = joinPoint.proceed();
        } catch (Exception e) {
            //請求失敗次數加1
            Metrics.counter("custom_http_request_error", tags).increment();
            throw e;
        } finally {
            long endTs = System.currentTimeMillis() - startTs;
            //記錄請求響應時間
           Metrics.timer("custom_http_request_time", tags).record(endTs, TimeUnit.MILLISECONDS);
        }
        return object;
    }
}

編寫好切面後,重啟服務;存取controller的介面,同樣可以進行自定義監控指標埋點