這段在介紹當我們已經完成app的開發,準備要佈署到機器前可以使用的一些監控工具。本章節會介紹Spring Boot Actuator。
引入以下dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
而actuator幫我們做到的是開了許多的endpoint,讓我們可以透過打http request到這些endpoint後,取得Spring Boot app相對應的資訊,詳細的endpoint可以在以下查看:
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.endpoints
預設的base path會是/actuator,所以例如我們想要打/health,那就得打/actuator/health。
預設只會先開啟/health,若要開啟其他的endpoint需要再application.properties設定:
#application.yaml
management:
endpoints:
web:
exposure:
include: health,info,beans,conditions
#application.yaml
management:
endpoints:
web:
exposure:
include: '*'
exclude: threaddump,heapdump
接下來會針對這些endpoint來做介紹。
info如果沒有額外設定的話,就只會返回{}。若有希望返回的資訊,要在application.properties設定info為開頭的properties:
#application.yaml
info:
developer:
email: author@fadachai.com
phone: 2168-1688
會返回當前app的狀態,有4種狀態:
1.UP,app可聯繫到且正常運作
2.DOWN,app聯繫不到或沒有在運作
3.UNKNOWN,無法辨識狀態
4.OUT_OF_SERVICE,可聯繫到app但是可能有部分功能是沒有運作的
$ curl localhost:8081/actuator/health
{"status":"UP"}
這些狀態的判斷標準會由各個功能組件的health indicator來代表。預設只會顯示綜合的狀態,若要開啟顯示細節,需在application.properties設定:
#application.yaml
management:
endpoint:
health:
show-details: always
我們都知道Spring是一個IoC的管理框架,若我們想知道實際上app裡的各個bean之間的依賴關係,就可以使用這個endpoint來得知。
{
"contexts": {
"application-1": {
"beans": {
...
"orderController": {
"aliases": [],
"scope": "singleton",
"type": "demo.api.OrderController",
"resource": "file [/Users/Documents/Workspaces/
demo/target/classes/demo/api/OrderController.class]",
"dependencies": [
"orderRepository"
]
},
...
},
"parentId": null
}
}
}
Spring Boot會幫我進行auto-configuration,這也是為什麼Spring Boot會蔚為流行的原因之一。不過若我們想知道Spring Boot幫我們進行了哪些auto-configuration,就可以打這個endpoint得知。
{
"contexts": {
"application-1": {
"positiveMatches": {
...
"MongoDataAutoConfiguration#mongoTemplate": [
{
"condition": "OnBeanCondition",
"message": "@ConditionalOnMissingBean (types:
org.springframework.data.mongodb.core.MongoTemplate;
SearchStrategy: all) did not find any beans"
}
],
...
},
"negativeMatches": {
...
"DispatcherServletAutoConfiguration": {
"notMatched": [
{
"condition": "OnClassCondition",
"message": "@ConditionalOnClass did not find required
class 'org.springframework.web.servlet.
DispatcherServlet'"
}
],
"matched": []
},
...
},
"unconditionalClasses": [
...
"org.springframework.boot.autoconfigure.context.
ConfigurationPropertiesAutoConfiguration",
...
]
}
}
}
這個endpoint可以取得關於app的環境變數相關資訊,例如application.properties,或者server的port,或者os的system properties等等。
而除了使用GET取得資訊外,我們還可以透過POST和DELETE來打同樣的/env endpoint來更改properties,只不過這些變更只會作用在當前啟動的這個context,若重啟app後就會遺失。
這個endpoint可以讓我們檢視我們所有的controller endpoint有哪些。
若想知道app的logger設定了什麼等級之類的資訊,就可以打這個endpoint。
除了一些app的config資訊外,我們甚至可以獲取app當前被多少request打以及運作中的thread狀況。
{
"traces": [
{
"timestamp": "2022-10-10T10:10:10.10Z",
"principal": null,
"session": null,
"request": {
"method": "GET",
"uri": "http://localhost:8081/ingredients",
"headers": {
"Host": ["localhost:8081"],
"User-Agent": ["curl/7.54.0"],
"Accept": ["*/*"]
},
"remoteAddress": null
},
"response": {
"status": 200,
"headers": {
"Content-Type": ["application/json;charset=UTF-8"]
}
},
"timeTaken": 4
},
...
]
}
{
"threadName": "reactor-http-nio-7",
"threadId": 216,
"blockedTime": -1,
"blockedCount": 0,
"waitedTime": -1,
"waitedCount": 0,
"lockName": null,
"lockOwnerId": -1,
"lockOwnerName": null,
"inNative": true,
"suspended": false,
"threadState": "RUNNABLE",
"stackTrace": [
{
"methodName": "tanduachi",
"fileName": "FaDaChai.java",
"lineNumber": 168,
"className": "com.fafa.FaDaChai",
"nativeMethod": true
},
{
"methodName": "feishuntein",
"fileName": "FaDaChai.java",
"lineNumber": 888,
"className": "com.fafa.FaDaChai",
"nativeMethod": false
},
...
],
"lockedMonitors": [
{
"className": "com.stock.channel.MingPai",
"identityHashCode": 1686545686,
"lockedStackDepth": 3,
"lockedStackFrame": {
"methodName": "naiShen",
"fileName": "NeiShenImpl.java",
"lineNumber": 77,
"className": "com.stock.channel.MingPai",
"nativeMethod": false
}
},
...
],
"lockedSynchronizers": [],
"lockInfo": null
}
metrics有很多種可以打api到app拿資訊,以下會以http.server.requests為例:
$ curl localhost:8081/actuator/metrics/http.server.requests
{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 1688 },
{ "statistic": "TOTAL_TIME", "value": 16.012458638 },
{ "statistic": "MAX", "value": 0.018653471 }
],
"availableTags": [
{ "tag": "exception",
"values": [ "ResponseStatusException",
"IllegalArgumentException", "none" ] },
{ "tag": "method", "values": [ "GET" ] },
{ "tag": "uri",
"values": [
"/actuator/metrics/{requiredMetricName}",
"/actuator/health", "/actuator/info", "/ingredients",
"/actuator/metrics", "/**" ] },
{ "tag": "status", "values": [ "404", "500", "200" ] }
]
}
也可以詢問更詳細的部分:
$ curl localhost:8081/actuator/metrics/http.server.requests?tag=status:404
{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 88 },
{ "statistic": "TOTAL_TIME", "value": 0.321568742 },
{ "statistic": "MAX", "value": 0 }
],
"availableTags": [
{ "tag": "exception",
"values": [ "ResponseStatusException", "none" ] },
{ "tag": "method", "values": [ "GET" ] },
{ "tag": "uri",
"values": [
"/actuator/metrics/{requiredMetricName}", "/**" ] }
]
}
除了現成的Actuator endpoints外,我們也可以透過幾個方法來客製這些資訊的來源。
先前的內容有提到這個/info的endpoint,我們可以在application.properties定義info開頭的properties;另一種定義info的方法是建置一個InfoContributor的component:
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.actuate.info.Info.Builder;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
import tacos.data.TacoRepository;
@Component
public class OrderCountInfoContributor implements InfoContributor {
private OrderRepository orderRepo;
public OrderCountInfoContributor(OrderRepository orderRepo) {
this.orderRepo = orderRepo;
}
@Override
public void contribute(Builder builder) {
long orderCount = orderRepo.count().block();
Map<String, Object> orderMap = new HashMap<String, Object>();
orderMap.put("count", orderCount);
builder.withDetail("order-stats", orderMap);
}
}
1.build-info
在pom.xml加入build-info即可開啟此資訊:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
或在build.gradle加上:
springBoot {
buildInfo()
}
在pom.xml加上:
<build>
<plugins>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugins>
</build>
或build.gradle:
plugins {
id "com.gorylenko.gradle-git-properties" version "2.3.1"
}
就可以透過/info來取得git相關的資訊。
若今天我們有介接一些外部系統,它們不像Spring Boot有這些現成的health監控指標,有沒有可能幫它們定義一個health監控指標呢?有的,只要建置HealthIndicator即可做到:
import java.util.Calendar;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class WackoHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour > 12) {
return Health
.outOfService()
.withDetail("reason",
"I'm out of service after lunchtime")
.withDetail("hour", hour)
.build();
}
if (Math.random() <= 0.1) {
return Health
.down()
.withDetail("reason", "I break 10% of the time")
.build();
}
return Health
.up()
.withDetail("reason", "All is good!")
.build();
}
}
除了health indicator,我們也可以自己定義metric,透過MeterRegistry:
import java.util.List;
import org.springframework.data.rest.core.event.AbstractRepositoryEventListener;
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.MeterRegistry;
@Component
public class OrderMetrics extends AbstractRepositoryEventListener<Order> {
private MeterRegistry meterRegistry;
public OrderMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void onAfterCreate(Order order) {
List<Product> products = order.getProducts();
for (Product product : products) {
meterRegistry.counter("onlineshop",
"product", product.getId()).increment();
}
}
}
$ curl localhost:8081/actuator/metrics/onlineshop
{
"name": "onlineshop",
"measurements": [ { "statistic": "COUNT", "value": 68 }
],
"availableTags": [
{
"tag": "product",
"values": [ "BOOK", "CHEESE", "WINE", "HAM",
"SODA", "SWORD", "ARMOR", "POTION"]
}
]
}
Spring Boot Actuator的endpoint有做過SpringMVC的話可能直覺會覺得是透過@Controller搭配@RequestMapping建置的,但其實是透過@Endpoint的annotation,而他不只支援HTTP的request,也支援JMX MBeans。若要讓endpoint只支援HTTP可以改貼@WebEndPoint;若要只支援JMX可改貼@JmxEndpoint。以下為一個客製範例:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
@Component
@Endpoint(id="books", enableByDefault=true)
public class BooksEndpoint {
private List<Book> books = new ArrayList<>();
@ReadOperation
public List<Book> books() {
return books;
}
@WriteOperation
public List<Book> addBook(String title) {
books.add(new Book(title));
return books;
}
@DeleteOperation
public List<Book> deleteBook(int index) {
if (index < books.size()) {
books.remove(index);
}
return books;
}
}
該如何secure這些Actuator的endpoints呢?就要透過Spring Security了,而Spring Boot也很方便地提供了EndpointRequest可以做到這件事情:
// allow all endpoints to ADMIN role
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}
// allow all endpoints exclude 'health', 'info' to ADMIN role
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(
EndpointRequest.toAnyEndpoint()
.excluding("health", "info"))
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}
// allow 'beans', 'threaddump', 'loggers' to ADMIN role
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(EndpointRequest.to(
"beans", "threaddump", "loggers"))
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}