iT邦幫忙

2022 iThome 鐵人賽

DAY 27
0
自我挑戰組

Spring In Action系列 第 27

Spring Boot Actuator

  • 分享至 

  • xImage
  •  

這段在介紹當我們已經完成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

info如果沒有額外設定的話,就只會返回{}。若有希望返回的資訊,要在application.properties設定info為開頭的properties:

#application.yaml
info:
  developer:
    email: author@fadachai.com
    phone: 2168-1688
  • /health

會返回當前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
  • /beans

我們都知道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
    }
  }
}
  • /conditions

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",
...
      ]
    }
  }
}
  • /env

這個endpoint可以取得關於app的環境變數相關資訊,例如application.properties,或者server的port,或者os的system properties等等。

而除了使用GET取得資訊外,我們還可以透過POST和DELETE來打同樣的/env endpoint來更改properties,只不過這些變更只會作用在當前啟動的這個context,若重啟app後就會遺失。

  • /mappings

這個endpoint可以讓我們檢視我們所有的controller endpoint有哪些。

  • /loggers

若想知道app的logger設定了什麼等級之類的資訊,就可以打這個endpoint。

除了一些app的config資訊外,我們甚至可以獲取app當前被多少request打以及運作中的thread狀況。

  • /httptrace
{
  "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
    },
...
  ]
}
  • threaddump
{
  "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

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 with InfoContributor

先前的內容有提到這個/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);
  }
}
  • Spring Boot提供的InfoContributor

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()
}
  • Git

在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();
}

上一篇
RSocket
下一篇
Spring Boot Admin
系列文
Spring In Action30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言