讀古今文學網 > Spring Boot實戰 > 7.4 定制Actuator >

7.4 定制Actuator

雖然Actuator提供了很多運行中Spring Boot應用程序的內部工作細節,但難免和你的需求有所偏差。也許你並不需要它提供的所有功能,想要關閉一些也說不定。或者,你需要對Actuator稍作擴展,增加一些自定義的度量信息,以滿足你對應用程序的需求。

實際上,Actuator有多種定制方式,包括以下五項。

  • 重命名端點。

  • 啟用和禁用端點。

  • 自定義度量信息。

  • 創建自定義倉庫來存儲跟蹤數據。

  • 插入自定義的健康指示器。

接下來,我們會瞭解如何定制Actuator,以滿足我們的需要。先來看一個最簡單的定制:重命名Actuator端點。

7.4.1 修改端點ID

每個Actuator端點都有一個ID用來決定端點的路徑,比方說,/beans端點的默認ID就是beans

如果端點的路徑是由ID決定的,那麼可以通過修改ID來改變端點的路徑。你要做的就是設置一個屬性,屬性名是endpoints.endpoint-id.id

我們用/shutdown端點來做個演示,它會響應發往/shutdown的POST請求。假設你想讓它處理發往/kill的POST請求,可以通過如下YAML為/shutdown賦予一個新的ID,也就是新的路徑:

endpoints:
  shutdown:
    id: kill

  

重命名端點、修改其路徑的理由很多。最明顯的理由就是,端點的命名要和團隊的術語保持一致。你也可能想重命名端點,讓那些熟悉默認名稱的人找不到它,借此增加一些安全感。

遺憾的是,重命名端點並不能真的起到保護作用,頂多是讓黑客慢點找到它們。我們會在7.5節看到如何保護這些Actuator端點。現在先讓我們來看看如何禁用某個(或全部)不希望別人訪問的端點。

7.4.2 啟用和禁用端點

雖然Actuator的端點都很有用,但你不一定需要全部這些端點。默認情況下,所有端點(除了/shutdown)都啟用。我們已經看過如何設置endpoints.shutdown.enabledtrue,以此開啟/shutdown端點(詳見7.1.1節)。用同樣的方式,你可以禁用其他的端點,將endpoints.endpoint-id.enabled設置為false

例如,要禁用/metrics端點,你要做的就是將endpoints.metrics.enabled屬性設置為false。在application.yml裡做如下設置:

endpoints:
  metrics:
    enabled: false

  

如果你只想打開一兩個端點,那就先禁用全部端點,然後啟用那幾個你要的,這樣更方便。例如,考慮如下application.yml片段:

endpoints:
  enabled: false
  metrics:
    enabled: true

  

正如以上片段所示,endpoints.enabled設置為false就能禁用Actuator的全部端點,然後將endpoints.metrics.enabled設置為true重新啟用/metrics端點。

7.4.3 添加自定義度量信息

在7.1.2節中,你看到了如何從/metrics端點獲得運行中應用程序的內部度量信息,包括內存、垃圾回收和線程信息。這些都是非常有用且信息量很大的度量值,但你可能還想定義自己的度量,用來捕獲應用程序中的特定信息。

比方說,我們想要知道用戶往閱讀列表裡保存了多少次圖書,最簡單的方法就是在每次調用ReadingListControlleraddToReadingList方法時增加計數器值。計數器很容易實現,但這個不斷變化的總計值如何同/metrics端點發佈的度量信息一起發佈出來呢?

再假設我們想要獲得最後保存圖書的時間戳。時間戳可以通過調用System.currentTimeMillis來獲取,但如何在/metrics端點裡報告該時間戳呢?

實際上,自動配置允許Actuator創建CounterService的實例,並將其註冊為Spring的應用程序上下文中的Bean。CounterService這個接口裡定義了三個方法,分別用來增加、減少或重置特定名稱的度量值,代碼如下:

package org.springframework.boot.actuate.metrics;

public interface CounterService {
  void increment(String metricName);
  void decrement(String metricName);
  void reset(String metricName);
}

  

Actuator的自動配置還會配置一個GaugeService類型的Bean。該接口與CounterService類似,能將某個值記錄到特定名稱的度量值裡。GaugeService看起來是這樣的:

package org.springframework.boot.actuate.metrics;

public interface GaugeService {
  void submit(String metricName, double value);
}

  

你無需實現這些接口。Spring Boot已經提供了兩者的實現。我們所要做的就是把它們的實例注入所需的Bean,在適當的時候調用其中的方法,更新想要的度量值。

針對上文提到的需求,我們需要把CounterServiceGaugeService Bean注入ReadingListController,然後在addToReadingList方法裡調用其中的方法。代碼清單7-9是ReadingListController裡的相關變動:

代碼清單7-9 使用注入的CounterServiceGaugeService

@Controller
@RequestMapping(\"/\")
@ConfigurationProperties(\"amazon\")
public class ReadingListController {

  ...

  private CounterService counterService;

  @Autowired
  public ReadingListController(
      ReadingListRepository readingListRepository,
      AmazonProperties amazonProperties,
      CounterService counterService,
      GaugeService gaugeService) {       ←---注入CounterService和GaugeService
    this.readingListRepository = readingListRepository;
    this.amazonProperties = amazonProperties;
    this.counterService = counterService;
    this.gaugeService = gaugeService;
  }

  ...

  @RequestMapping(method=RequestMethod.POST)
  public String addToReadingList(Reader reader, Book book) {
    book.setReader(reader);
    readingListRepository.save(book);

    counterService.increment(\"books.saved\");     ←---增加books.saved的值

    gaugeService.submit(
            \"books.last.saved\", System.currentTimeMillis); ←---記錄books.last.saved的值
    return \"redirect:/\";
  }

}

  

修改後的ReadingListController使用了自動織入機制,通過控制器的構造方法注入CounterServiceGaugeService,隨後把它們保存在實例變量裡。此後,addToReadingList方法每次處理請求時都會調用counterService.increment (\"books.saved\")gaugeService.submit(\"books.last.saved\")來調整度量值。

儘管CounterServiceGaugeService用起來很簡單,但還是有一些度量值很難通過增加計數器或記錄指標值來捕獲。對於那些情況,我們可以實現PublicMetrics接口,提供自己需要的度量信息。該接口定義了一個metrics方法,返回一個Metric對象的集合:

package org.springframework.boot.actuate.endpoint;

public interface PublicMetrics {
  Collection<Metric<?>> metrics;
}

  

為瞭解PublicMetrics的使用方法,這裡假設我們想報告一些源自Spring應用程序上下文的度量值——應用程序上下文啟動的時間、Bean及Bean定義的數量,這些都包含進來會很有意思。順便再報告一下添加了@Controller註解的Bean的數量。代碼清單7-10給出了相應PublicMetrics實現的代碼。

代碼清單7-10 發佈自定義度量信息

package readinglist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.PublicMetrics;
import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

@Component
public class ApplicationContextMetrics implements PublicMetrics {

  private ApplicationContext context;

  @Autowired
  public ApplicationContextMetrics(ApplicationContext context) {
    this.context = context;
  }

  @Override
  public Collection<Metric<?>> metrics {
    List<Metric<?>> metrics = new ArrayList<Metric<?>>;
    metrics.add(new Metric<Long>(\"spring.context.startup-date\",   ←---記錄啟動時間
        context.getStartupDate));

    metrics.add(new Metric<Integer>(\"spring.beans.definitions\",   ←---記錄Bean定義數量
        context.getBeanDefinitionCount));

    metrics.add(new Metric<Integer>(\"spring.beans\",
        context.getBeanNamesForType(Object.class).length));      ←---記錄Bean數量

    metrics.add(new Metric<Integer>(\"spring.controllers\",
        context.getBeanNamesForAnnotation(Controller.class).length));   ←---記錄控制器類型的Bean數量

    return metrics;
  }

}

  

Actuator會調用metrics方法,收集ApplicationContextMetrics提供的度量信息。該方法調用了所注入的ApplicationContext上的方法,獲取我們想要報告為度量的數量。每個度量值都會創建一個Metrics實例,指定度量的名稱和值,將其加入要返回的列表。

創建ApplicationContextMetrics,並在ReadingListController裡使用CounterServiceGaugeService之後,我們可以在/metrics端點的響應中找到如下條目:

{
  ...
  spring.context.startup-date: 1429398980443,
  spring.beans.definitions: 261,
  spring.beans: 272,
  spring.controllers: 2,
  books.count: 1,
  gauge.books.save.time: 1429399793260,
  ...
}

 

當然,這些度量的實際值會根據添加了多少書、何時啟動應用程序及何時保存最後一本書而發生變化。在這個例子裡,你一定會好奇為什麼spring.controllers是2。因為這裡算上了ReadingListController以及Spring Boot提供的BasicErrorController

7.4.4 創建自定義跟蹤倉庫

默認情況下,/trace端點報告的跟蹤信息都存儲在內存倉庫裡,100個條目封頂。一旦倉庫滿了,就開始移除老的條目,給新的條目騰出空間。在開發階段這沒什麼問題,但在生產環境中,大流量會造成跟蹤信息還沒來得及看就被丟棄。

為了避免這個問題,你可以聲明自己的InMemoryTraceRepository Bean,將它的容量調整至100以上。如下配置類可以將容量調整至1000個條目:

package readinglist;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ActuatorConfig {

  @Bean
  public InMemoryTraceRepository traceRepository {
    InMemoryTraceRepository traceRepo = new InMemoryTraceRepository;
    traceRepo.setCapacity(1000);
    return traceRepo;
  }

}

  

倉庫容量翻了10倍,跟蹤信息的保存時間應該會更久。不過,繁忙到一定程度,應用程序還是可能在你查看這些信息前將其丟棄。這是一個內存存儲的倉庫,還要避免容量增長太多,影響應用程序的內存使用。

除了上述方法,我們還可以將那些跟蹤條目存儲在其他地方——既不消耗內存,又能長久保存的地方。只需實現Spring Boot的TraceRepository接口即可:

package org.springframework.boot.actuate.trace;
import java.util.List;
import java.util.Map;

public interface TraceRepository {
  List<Trace> findAll;
  void add(Map<String, Object> traceInfo);
}

  

如你所見,TraceRepository只要求我們實現兩個方法:一個方法查找所有存儲的Trace對象,另一個保存了一個Trace,包含跟蹤信息的Map對象。

作為演示,假設我們創建了一個使用MongoDB數據庫存儲跟蹤信息的TraceRepository實例。代碼清單7-11演示了如何實現這個TraceRepository

代碼清單7-11 往MongoDB保存跟蹤數據

package readinglist;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.stereotype.Service;

@Service
public class MongoTraceRepository implements TraceRepository {

  private MongoOperations mongoOps;

  @Autowired
  public MongoTraceRepository(MongoOperations mongoOps) {    ←---注入MongoOperations
    this.mongoOps = mongoOps;
  }

  @Override
  public List<Trace> findAll {
    return mongoOps.findAll(Trace.class);    ←---獲取所有跟蹤條目
  }

  @Override
  public void add(Map<String, Object> traceInfo) {
    mongoOps.save(new Trace(new Date, traceInfo));    ←---保存一個跟蹤條目
  }

}

  

findAll方法很直白,用注入的MongoOperations來查找全部Trace對象。add方法稍微有趣一點,用當前時間和含有跟蹤信息的Map創建了一個Trace對象,然後通過MongoOperations.save將其保存下來。唯一的問題是,MongoOperations是哪裡來的?

為了使用MongoTraceRepository,我們需要保證Spring應用程序上下文裡先有一個MongoOperations Bean。得益於Spring Boot的起步依賴和自動配置,做到這一點只需添加MongoDB起步依賴即可。你需要如下Gradle依賴:

compile(\"org.springframework.boot:spring-boot-starter-data-mongodb\")

  

如果你用的是Maven,則需要如下依賴:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

  

添加了這個起步依賴後,Spring Data MongoDB和所依賴的庫會添加到應用程序的Classpath裡。Spring Boot會自動配置所需的Bean,以便使用MongoDB數據庫。這些Bean裡就包括MongoOperations。另外,你需要確保和MongoOperations通訊的MongoDB服務器正常運行。

7.4.5 插入自定義健康指示器

如前文所述,Actuator自帶了很多健康指示器,能滿足常見需求,比如報告應用程序使用的數據庫和消息代理的健康情況。但如果你的應用程序需要和一些沒有健康指示器的系統交互,那該怎麼辦呢?

我們的閱讀列表裡有指向Amazon的圖書鏈接,可以報告一下Amazon是否可以訪問。當然,Amazon不太可能宕機,但不怕一萬就怕萬一,所以讓我們為Amazon創建一個健康指示器吧。代碼清單7-12演示了相關HealthIndicator的實現。

代碼清單7-12 自定義一個Amazon健康指示器

package readinglist;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class AmazonHealth implements HealthIndicator {

  @Override
  public Health health {

    try {
      RestTemplate rest = new RestTemplate;
      rest.getForObject(\"http://www.amazon.com\", String.class);   ←---向Amazon發送請求
      return Health.up.build;
    } catch (Exception e) {
      return Health.down.build;    ←---報告DOWN狀態
    }
  }

}

  

AmazonHealth類並沒有什麼花哨的地方。health方法只是使用Spring的RestTemplate向Amazon首頁發起了一個GET請求。如果請求成功,則返回一個表明Amazon狀態為UPHealth對象。如果請求發生異常,則health返回一個標明Amazon狀態為DOWNHealth對象。

下面是/health端點響應的一個片段。這裡可以看出,如果Amazon不可訪問,你會看到什麼。

{
    \"amazonHealth\": {
        \"status\": \"DOWN\"
    },
    ...
}

  

你不會相信我等Amazon宕機等了多久,就為了能看到上面的結果!1

1實際上我並沒有等太久。我只是把電腦的網絡斷開了。沒有網就沒有Amazon。

除了簡單的狀態之外,如果你還想向健康記錄裡添加其他附加信息,可以調用Health構造器的withDetail方法。例如,要添加異常消息,將其作為健康記錄的reason字段,可以讓catch塊返回這樣一個Health對像:

return Health.down.withDetail(\"reason\", e.getMessage).build;

  

修改後,當Amazon無法訪問時,健康記錄看起來是這樣的:

\"amazonHealth\": {
    \"reason\": \"I/O error on GET request for
               \"http://www.amazon.com\":www.amazon.com;
               nested exception is java.net.UnknownHostException:
               www.amazon.com\",
    \"status\": \"DOWN\"
},

  

如果有很多附加信息,可以多次調用withDetail方法,每次設置一個要放入健康記錄的附加字段。