1. 概述
本文將探討 Micrometer 指標,重點關注標籤功能。我們將使用 Micrometer 在 Spring Boot 應用程序中,應用各種模式創建簡單的指標,例如計數器和定時器。
我們將首先使用 Micrometer 的 Builder API 創建具有可變標籤值的儀表盤。此外,我們還將研究 MeterProvider 作為一種替代方案,以避免潛在的性能問題。我們還將使用 Spring AOP 和 Micrometer 相關的註解,以聲明式的方式記錄方法調用。
最後,我們將涵蓋命名約定和選擇標籤值的最佳實踐,以及需要避免的常見陷阱。
2. 使用 Builder API
我們將使用一個簡單的 Spring Boot 服務,該服務包含一個公共函數foo(),它接受客户端設備類型作為String。為了本文中的代碼示例,我們將嘗試統計和計時每個對foo()的調用,並將指標與作為參數傳遞的設備類型一起標記。
@Service
class DummyService {
// logger, constructor
public String foo(String deviceType) {
log.info("foo({})", deviceType);
String response = invokeSomeLogic();
return response;
}
private void invokeSomeLogic() { /* ... */ }
}首先,我們需要將 Spring Boot Actuator 依賴添加到我們的 pom.xml 文件中,這將包含 Micrometer 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>一種簡單的方法是使用 Micrometer 的 Fluent API 創建不同類型的計量器。
當調用 foo() 時,我們使用 Counter.builder 創建一個計量器,然後將標籤作為鍵值對添加。
Micrometer 使用 MeterRegistry 來管理和存儲我們創建的所有計量器。
因此,我們需要將其注入到我們的服務中,並在構建鏈的最後一步 – 通過調用 register() – 使用它。這實際上會構建並註冊計量器。
最後,我們只需調用創建的 Counter 實例上的 increment() 方法:
@Component
class DummyService {
private final MeterRegistry meterRegistry;
// logger, constructor
public String foo(String deviceType) {
log.info("foo({})", deviceType);
Counter.builder("foo.count")
.tag("device.type", deviceType)
.register(meterRegistry)
.increment();
String response = Timer.builder("foo.time")
.tag("device.type", deviceType)
.register(meterRegistry)
.record(this::invokeSomeLogic);
return response;
}
private void invokeSomeLogic() { /* ... */ }
}如我們所見,註冊一個 Timer 與註冊一個 Counter 非常相似。主要區別在於,使用 increment() 像 Counter 那樣,Timer 使用諸如 record() 這樣的方法。 在我們的例子中,我們使用 record() 將一個要執行的函數傳遞進去。 Timer 會包裹函數調用,測量其執行時間,記錄時間,並返回函數的返回值。
重要的是要知道,一個計器被其名稱和一組標籤唯一地標識。 例如,即使我們使用構建器模式創建多個名為 "foo.count" 的 Counter,並且它們的標籤 "device.type" 都設置為 "mobile",它們實際上仍然指向同一個底層指標。 因此,無論哪個被增加,計數器都會更新到相同的值。
另一方面,這種方法有一個缺點,即每次調用 foo() 時都會創建構建器對象。 根據方法被調用頻率,這可能會增加垃圾回收的開銷,並對性能產生影響。
3. 暴露指標
現在我們已經開始監控 foo() 方法的調用,可以通過 Spring Boot Actuator 暴露這些指標。為此,我們更新配置中的 management.endpoints.web.exposure.include 屬性,將其設置為 metrics,或者使用 '*’ 暴露所有端點:
management:
endpoints.web.exposure.include: '*'因此,我們可以現在查看 foo() 的指標,位於 /actuator/metrics/foo.count 和 /actuator/metrics/foo.time 處。例如,foo.time 暴露的內容如下:
{
"name": "foo.time",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 100
},
{
"statistic": "TOTAL_TIME",
"value": 5.5068953
},
{
"statistic": "MAX",
"value": 0
}
],
"availableTags": [
{
"tag": "device.type",
"values": [ "mobile", "tablet", "smart_tv", "wearable", "desktop" ]
}
]
}如我們所見,每個指標也顯示了可用的標籤列表。 我們可以使用這些標籤來縮小指標的範圍。 例如,要檢查針對 桌面 設備類型的調用,我們可以使用端點 /actuator/metrics/foo.time?tag=device.type:desktop。
4. 使用計量服務提供者
正如前面所述,使用 Micrometer 的 Builder API 的缺點是在每次函數調用時創建 Builder 對象。我們可以通過利用 MeterProvider API 來避免這個問題。 MeterProvider 遵循模板設計模式的一種變體,允許我們重用計量器並減少垃圾回收開銷。
在構造函數中,我們仍然會使用 Counter.builder(),但不是調用 register(),而是使用 withRegistry(meterRegistry) 變體。 這返回一個 MeterProvider<Counter>,我們可以將其存儲為服務的字段並根據需要重用它。
相同的策略也適用於其他計量器類型,如 Timers,API 返回一個 MeterProvider<Timer>:
@Service
class DummyService {
private final MeterRegistry meterRegistry;
private final Meter.MeterProvider<Counter> counterProvider;
private final Meter.MeterProvider<Timer> timerProvider;
// logger
public DummyService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.counterProvider = Counter.builder("bar.count")
.withRegistry(meterRegistry);
this.timerProvider = Timer.builder("bar.time")
.withRegistry(meterRegistry);
}
// ...
}現在,我們可以使用 counterProvider 和 meterProvider 字段來記錄指標,同時在攔截對新方法的調用時,我們稱之為 bar()。
public String bar(String device) {
log.info("bar({})", device);
counterProvider.withTag("device.type", device)
.increment();
String response = timerProvider.withTag("device.type", device)
.record(this::invokeSomeLogic);
return response;
}如我們所見,我們可以使用 MeterProvider 動態添加標籤,然後調用與計量相關的特定方法,例如,對於 Counter,調用 increment() 方法;對於 Timer,調用 record() 方法。
5. 使用 AOP
除了 Builder 和 MeterProvider API 之外,我們還可以使用面向切面編程 (AOP) 聲明性地定義指標。
我們的組件是一個 Spring 託管的 Bean,並使用了 @Service 標註;因此,我們可以利用 Spring 的 AOP 支持自動記錄方法調用指標,並避免手動管理指標。 要做到這一點,我們只需使用適當的 Micrometer 標註對要觀察的方法進行標註。
例如,我們可以創建一個新的方法 buzz(),並使用 Micrometer 的 @Counted 和 @Timed 進行標註。
@Timed("buzz.time")
@Counted("buzz.time")
public String buzz(String device) {
log.info("buzz({})", device);
return invokeSomeLogic();
}通過這樣做,我們將自動記錄方法的調用並通過 Actuator 暴露指標。但是,由於這依賴於 AOP,因此對 buzz() 的調用必須通過 Spring 代理進行,同一類中直接的內部調用將不會被攔截。
動態標記對 @Timed 和 @Counted 註解支持,但需要額外的配置。首先,我們需要從配置中啓用註解觀察:
management:
observations.annotations.enabled: true
# ...此外,我們需要定義一個 MeterTagAnnotationHandler Bean,以幫助儀表評估標註的參數。雖然它可以配置為解析 SpEL 表達式,但我們將保持其簡單,並始終返回標註對象的 toString() 方法:
@Bean
MeterTagAnnotationHandler meterTagAnnotationHandler() {
return new MeterTagAnnotationHandler(
aClass -> Object::toString,
aClass -> (exp, param) -> pram.toString()
);
}最後,我們將對 buzz() 方法的參數添加 @MeterTag 註解。
@Timed(value = "buzz.time")
@Counted(value = "buzz.count")
public String buzz(@MeterTag("device.type") String device) {
log.info("buzz({})", device);
return invokeSomeLogic();
}因此, 攔截對 Timer 的調用,將動態地添加 device.type 標籤。
另一方面,如果我們希望配置 @Counted 以相同的方式工作,則需要註冊 CountedAspect Bean 並手動設置 CountedMeterTagAnnotationHandler 實現。
@Bean
public CountedAspect countedAspect() {
CountedAspect aspect = new CountedAspect();
CountedMeterTagAnnotationHandler tagAnnotationHandler = new CountedMeterTagAnnotationHandler(
aClass -> Object::toString,
aClass -> (exp, param) -> "");
aspect.setMeterTagAnnotationHandler(tagAnnotationHandler);
return aspect;
}6. 最佳實踐與約定
Micrometer 鼓勵使用描述性的、小寫字母分隔的名稱(如指標和標籤),例如 <em >“foo.time”</em > 和“device.type”。 指標名稱應能提供自身的有意義的上下文,而標籤則為過濾或分組提供額外的維度。
我們應避免使用過於通用的標籤,如 `“service”,這些標籤會聚合無關的指標。 通用的名稱會使人們難以理解指標的含義,並降低收集數據的有用性。
我們應謹慎使用動態標籤值。 極度變化的數值會產生許多獨特的指標組合,從而增加指標的基數,並給監控系統帶來壓力。 在我們的演示中,使用 `“device.type” 是安全的,因為它具有有限的數值集,從而保持基數較低。
7. 結論
在本教程中,我們探討了使用具有可變標籤值的創建 Micrometer 指標的不同方法。我們首先了解到指標的唯一標識符是其名稱和標籤,並使用 Builder API 手動創建了 Counter 和 Timer 指標。
接下來,我們發現這種方法會導致在每次調用方法時創建 Builder 實例,從而給垃圾收集器帶來壓力。為了解決這個問題,我們探索了使用 MeterProvider 以重用指標並減少開銷的方法。
最後,我們利用 Spring AOP 和 Micrometer 專有註解(如 @Timed 和 @Counted)來記錄方法調用,實現聲明式記錄。我們還討論了使用動態標籤和遵循一致命名規範的最佳實踐。