Appearance
Spring In Action 6th:Actuator
1. 介绍 Actuator
在机器中,执行器(Actuator)是一个负责控制和移动机械的组件。在 Spring Boot 应用程序中,Spring Boot Actuator 扮演了同样的角色,使我们能够看到正在运行的应用程序的内部,并在一定程度上控制应用程序的行为。
使用 Actuator 暴露的端点,我们可以询问关于正在运行的 Spring Boot 应用程序的内部状态的一些事情,例如以下内容:
- 应用程序环境中有哪些配置属性?
- 应用程序中各个包的日志级别是什么?
- 应用程序消耗了多少内存?
- 一个给定的 HTTP 端点被请求了多少次?
- 应用程序和它协调的任何外部服务的健康状况如何?
要在 Spring Boot 应用程序中启用 Actuator,你只需要将 Actuator 的启动器依赖添加到你的构建中:
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>1
2
3
4
2
3
4
一旦 Actuator 启动器成为项目构建的一部分,应用程序将配备有几个开箱即用的 Actuator 端点,包括表 1.1 中描述的那些端点:
| HTTP 方法 | 路径 | 描述 |
|---|---|---|
| GET | /auditevents | 生成已触发的任何审计事件的报告 |
| GET | /beans | 描述 Spring 应用程序上下文中的所有 beans |
| GET | /conditions | 生成一个报告,该报告记录了自动配置条件的通过或失败状态,这些状态最终影响了在应用程序上下文中创建的 beans |
| GET | /configprops | 描述所有配置属性以及当前值 |
| GET, POST, DELETE | /env | 生成所有可用于 Spring 应用程序的属性源及其属性的报告 |
| GET | /env/{toMatch} | 描述单个环境属性的值 |
| GET | /health | 返回应用程序的总体健康状况和(可能)外部依赖应用程序的健康状况 |
| GET | /heapdump | 下载堆转储 |
| GET | /httptrace | 生成最近 100 次请求的跟踪 |
| GET | /info | 返回关于应用程序的任何开发者定义的信息 |
| GET | /loggers | 生成应用程序中的包列表以及它们的配置和有效日志级别 |
| GET, POST | /loggers/{name} | 返回给定记录器的配置和有效日志级别,可以通过 POST 请求设置有效的日志级别 |
| GET | /mappings | 生成所有 HTTP 映射及其对应处理方法的报告 |
| GET | /metrics | 返回所有度量类别的列表 |
| GET | /metrics/{name} | 返回给定度量的多维值集 |
| GET | /scheduledtasks | 列出所有计划任务 |
| GET | /threaddump | 返回所有应用程序线程的报告 |
除了 HTTP 端点外,表 1.1 中的所有 Actuator 端点(唯一的例外是 /heapdump)也都作为 JMX MBeans 被暴露出来。我们将后续介绍 JMX。
1.1. 配置 Actuator 基本路径
默认情况下,表 1.1 中显示的所有端点的路径都以 /actuator 为前缀。这意味着,例如,如果你希望从 Actuator 中获取有关你的应用程序的健康信息,那么发出一个针对 /actuator/health 的 GET 请求将返回你需要的信息。
Actuator 的前缀路径可以通过设置 management.endpoints.web.base-path 属性来更改。例如,如果你希望前缀是 /management,你可以像这样设置:
YAML
management:
endpoints:
web:
base-path: /management1
2
3
4
2
3
4
当你按照这种方式设置了这个属性后,你需要对 /management/health 发起一个 GET 请求,以获取应用程序的健康信息。
无论你是否决定更改 Actuator 的基础路径,为了简洁起见,本章中的所有 Actuator 端点都将不带基础路径进行引用。例如,当提到 /health 端点时,实际上是指 /{base path}/health 端点,或者更准确地说,如果基础路径没有被更改,那么就是 /actuator/health 端点。
1.2. 启用和禁用 Actuator 端点
你可能已经注意到,只有 /health 端点是默认启用的。大多数 Actuator 端点都包含敏感信息,应该被保护起来。你可以使用 Spring Security 来锁定 Actuator,但是因为 Actuator 本身并没有被保护,所以大多数的端点默认是被禁用的,需要你选择你希望暴露的端点。
有两个配置属性,management.endpoints.web.exposure.include 和 management.endpoints.web.exposure.exclude,可以用来控制哪些端点被暴露。使用 management.endpoints.web.exposure.include 来指定你想要暴露的端点。例如,如果你只希望暴露 /health、/info、/beans 和 /conditions 端点,你可以使用以下的配置来指定:
YAML
management:
endpoints:
web:
exposure:
include: health,info,beans,conditions1
2
3
4
5
2
3
4
5
management.endpoints.web.exposure.include 属性也接受星号 * 作为通配符,表示应该暴露所有的 Actuator 端点,如下所示:
YAML
management:
endpoints:
web:
exposure:
include: '*'1
2
3
4
5
2
3
4
5
如果你想暴露除了少数几个端点以外的所有端点,通常更容易的做法是使用通配符将它们全部包含进来,然后明确地排除一些。例如,要暴露所有的 Actuator 端点,除了 /threaddump 和 /heapdump,你可以像这样设置 management.endpoints.web.exposure.include 和 management.endpoints.web.exposure.exclude 这两个属性:
YAML
management:
endpoints:
web:
exposure:
include: '*'
exclude: threaddump,heapdump1
2
3
4
5
6
2
3
4
5
6
如果你决定暴露的端点不止 /health 和 /info,那么配置 Spring Security 来限制对其他端点的访问可能是个好主意。我们将在保护 Actuator 小节中看到如何保护 Actuator 端点。但是现在,让我们看看你如何可以使用 Actuator 暴露的 HTTP 端点。
2. 使用 Actuator 端点
Actuator 可以通过表 1.1 中列出的 HTTP 端点,为运行中的应用程序提供一系列有趣且有用的信息。作为 HTTP 端点,这些可以像任何 REST API 一样被使用,你可以使用任何你喜欢的 HTTP 客户端,包括 Spring 的 RestTemplate、WebClient 和 JavaScript 应用程序,或者简单地使用 curl 命令行客户端。
为了探索 Actuator 端点,我们将在本章中使用 curl 命令行客户端。在后续,我将向你介绍 Spring Boot Admin,它在应用程序的 Actuator 端点之上构建了一个用户友好的 Web 应用程序。
为了对 Actuator 提供的端点有所了解,对 Actuator 的基础路径发起 GET 请求将为每个端点提供 HATEOAS 链接。使用 curl 对 /actuator 发起请求,你可能会得到类似这样的响应:
Bash
$ curl localhost:8080/actuator
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"auditevents": {
"href": "http://localhost:8080/actuator/auditevents",
"templated": false
},
"beans": {
"href": "http://localhost:8080/actuator/beans",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
因为不同的库可能会贡献他们自己的额外 Actuator 端点,而且因为一些端点可能没有被导出,实际的结果可能会因应用程序而异。
无论如何,从 Actuator 的基础路径返回的链接集合都作为一个地图,指向 Actuator 所能提供的所有内容。让我们开始探索 Actuator,首先是提供应用程序基本信息的两个端点:/health 和 /info 端点。
2.1. 获取重要的应用程序信息
在我们典型的看医生的过程开始时,我们通常会被问到两个非常基本的问题:你是谁?你感觉如何?虽然医生或护士选择的词语可能会有所不同,但他们最终想要了解一些他们正在治疗的人的信息,以及你为什么要来看他们。
Actuator 的 /info 和 /health 端点回答的就是这些基本问题。/info 端点告诉你一些关于应用程序的信息,而 /health 端点告诉你应用程序的健康状况。这就像是 Spring Boot 应用程序的 “自我介绍” 和 “健康报告”。这两个端点为我们提供了关于应用程序的基本信息和健康状况的重要信息。这对于理解和监控应用程序的运行状态非常有帮助。所以,让我们开始探索这两个端点吧。
2.1.1. 询问应用程序的信息
要了解运行中的 Spring Boot 应用程序的一些信息,你可以询问 /info 端点。然而,默认情况下,/info 端点并不是很有信息量。当你使用 curl 发起请求时,你可能会看到如下内容:
Bash
$ curl localhost:8080/actuator/info
{}1
2
2
虽然 /info 端点看起来似乎不是很有用,但最好将其视为一个干净的画布,你可以在上面绘制任何你想展示的信息。
我们有几种方式可以为 /info 端点提供信息,但最直接的方式是创建一个或多个配置属性,其中属性名以 info 为前缀。例如,假设你希望 /info 端点的响应中包含支持联系信息,包括电子邮件地址和电话号码。为此,你可以在 application.yml 文件中配置以下属性:
YAML
info:
contact:
email: support@tacocloud.com
phone: 822-625-68311
2
3
4
2
3
4
info.contact.email 属性和 info.contact.phone 属性对于 Spring Boot 或应用程序上下文中的任何 bean 来说都没有特殊的含义。然而,由于它们的前缀是 info,/info 端点现在会在其响应中回显这些属性的值,如下所示:
JSON
{
"contact": {
"email": "support@tacocloud.com",
"phone": "822-625-6831"
}
}1
2
3
4
5
6
2
3
4
5
6
在向 /info 端点提供信息小节,我们将探讨一些其他的方法,用于向 /info 端点填充有关应用程序的有用信息。
2.1.2. 检查应用程序的健康状况
发出 HTTP GET 请求到 /health 端点会得到一个简单的 JSON 响应,其中包含了你的应用程序的健康状态。例如,当你使用 curl 获取 /health 端点时,你可能会看到如下内容:
Bash
$ curl localhost:8080/actuator/health
{"status":"UP"}1
2
2
你可能会想,有一个报告应用程序是 UP 的端点能有多大用处。如果应用程序是 down 的,它会报告什么呢?
事实证明,这里显示的状态是一个或多个健康指标的聚合状态。健康指标报告应用程序交互的外部系统的健康状况,如数据库、消息代理,甚至 Spring Cloud 组件,如 Eureka 和 Config Server。每个指标的健康状态可能是以下之一:
UP:外部系统是 up 的,并且可以访问;DOWN:外部系统是 down 的或无法访问;UNKNOWN:外部系统的状态不清楚;OUT_OF_SERVICE:外部系统可以访问,但目前不可用;
然后,所有健康指标的健康状态被聚合成应用程序的整体健康状态,应用以下规则:
- 如果所有健康指标都是
UP,那么应用程序的健康状态就是UP; - 如果一个或多个健康指标是
DOWN,那么应用程序的健康状态就是DOWN; - 如果一个或多个健康指标是
OUT_OF_SERVICE,那么应用程序的健康状态就是OUT_OF_SERVICE; UNKNOWN的健康状态被忽略,不会被汇总到应用程序的聚合健康中;
默认情况下,只有聚合状态会在对 /health 的请求的响应中返回。然而,你可以配置 management.endpoint.health.show-details 属性,以显示所有健康指标的完整细节,如下所示:
YAML
management:
endpoint:
health:
show-details: always1
2
3
4
2
3
4
management.endpoint.health.show-details 属性默认为 never,但也可以设置为 always,以始终显示所有健康指标的完整细节,或者设置为 when-authorized,只有当请求客户端完全授权时才显示完整细节。
现在,当你对 /health 端点发出 GET 请求时,你会得到完整的健康指标细节。以下是一个示例,展示了一个与 Mongo 文档数据库集成的服务可能会看到的情况:
JSON
{
"status": "UP",
"details": {
"mongo": {
"status": "UP",
"details": {
"version": "3.5.5"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 499963170816,
"free": 177284784128,
"threshold": 10485760
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
所有的应用程序,无论有没有其他的外部依赖,都会有一个名为 diskSpace 的文件系统健康指标。diskSpace 健康指标表示的是文件系统的健康状况(希望是 UP),这是由剩余的可用空间决定的。如果可用的磁盘空间低于阈值,它将报告 DOWN 状态。
在前面的示例中,还有一个 mongo 健康指标,它报告的是 Mongo 数据库的状态。显示的细节包括 Mongo 数据库的版本。
自动配置确保只有与应用程序相关的健康指标才会出现在 /health 端点的响应中。除了 mongo 和 diskSpace 健康指标外,Spring Boot 还为其他几个外部数据库和系统提供了健康指标,包括以下内容:
- Cassandra
- Config Server
- Couchbase
- Eureka
- Hystrix
- JDBC 数据源
- Elasticsearch
- InfluxDB
- JMS 消息代理
- LDAP
- 邮件服务器
- Neo4j
- Rabbit 消息代理
- Redis
- Solr
此外,第三方库可能会贡献他们自己的健康指标。我们将在自定义健康指标小节学习如何编写自定义健康指标。
如你所见,/health 和 /info 端点提供了关于运行应用程序的一般信息。同时,其他 Actuator 端点提供了对应用程序配置的洞察。让我们看看 Actuator 如何显示应用程序的配置。
2.2. 查看配置详细信息
除了接收关于应用程序的一般信息外,理解应用程序是如何配置的也是很有启发性。应用程序上下文中有哪些 beans?哪些自动配置条件通过了,哪些失败了?应用程序可以使用哪些环境属性?HTTP 请求如何映射到控制器?一个或多个包或类设置的日志级别是什么?
这些问题都可以通过 Actuator 的 /beans、/conditions、/env、/configprops、/mappings 和 /loggers 端点来回答。而且在某些情况下,比如 /env 和 /loggers,你甚至可以在运行时调整应用程序的配置。我们将看看这些端点如何提供对运行应用程序的配置的洞察,从 /beans 端点开始。
2.2.1. 获取 Bean Wiring 报告
探索 Spring 应用上下文最关键的端点是 /beans 端点。这个端点返回一个 JSON 文档,描述了应用上下文中的每一个 Bean,它的 Java 类型,以及它所注入的任何其他 Bean。
我们不去详细介绍来自 /beans 的完整响应,而是考虑以下片段,它关注于一个单一的 Bean 条目:
JSON
{
"contexts": {
"application-1": {
"beans": {
...
"ingredientsController": {
"aliases": [],
"scope": "singleton",
"type": "tacos.ingredients.IngredientsController",
"resource": "file [/Users/habuma/Documents/Workspaces/TacoCloud/ingredient-service/target/classes/tacos/ingredients/IngredientsController.class]",
"dependencies": [
"ingredientRepository"
]
},
...
},
"parentId": null
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在响应的根部是 contexts 元素,它包含了应用中每一个 Spring 应用上下文的子元素。在每一个应用上下文中,有一个 beans 元素,它保存了应用上下文中所有 beans 的详细信息。
在前面的例子中,展示的 bean 是名为 ingredientsController 的那一个。你可以看到,它没有别名,被定义为单例,并且类型为 tacos.ingredients.IngredientsController。此外,resource 属性给出了定义 bean 的类文件的路径。dependencies 属性列出了所有注入给定 bean 的其他 beans。在这个例子中,ingredientsController bean 被注入了一个名为 ingredientRepository 的 bean。
2.2.2. 解释自动配置
如你所见,自动配置是 Spring Boot 提供的最强大的功能之一。然而,有时你可能会想知道为什么某些东西会被自动配置。或者,你可能期望某些东西被自动配置,但却不知道为什么它没有被配置。在这种情况下,你可以向 /conditions 发送 GET 请求,以获取自动配置过程中发生了什么的解释。
从 /conditions 返回的自动配置报告分为三部分:正匹配(通过的条件配置)、负匹配(失败的条件配置)和无条件类。以下是对 /conditions 请求的响应的一个片段,展示了每个部分的示例:
JSON
{
"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",
...
]
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在 positiveMatches 部分下,你可以看到一个 MongoTemplate bean 由于尚未存在而被自动配置。导致这种情况的自动配置包括一个 @ConditionalOnMissingBean 注解,如果 bean 尚未被明确配置,它会将 bean 传递给配置。在这种情况下,没有找到类型为 MongoTemplate 的 beans,所以自动配置介入并配置了一个。
在 negativeMatches 下,Spring Boot 自动配置考虑配置一个 DispatcherServlet。但是 @ConditionalOnClass 条件注解失败了,因为找不到 DispatcherServlet。
最后,一个 ConfigurationPropertiesAutoConfiguration bean 在 unconditionalClasses 部分下被无条件地配置。配置属性是 Spring Boot 操作的基础,所以你应该无条件地自动配置任何与配置属性相关的配置。
2.2.3. 检查环境和配置属性
除了了解你的应用程序 bean 如何连接在一起,你可能还对了解可用的环境属性以及注入到 bean 中的配置属性感兴趣。
当你向 /env 端点发出 GET 请求时,你会收到一个相当长的响应,其中包含了 Spring 应用程序中所有属性源的属性。这包括来自环境变量、JVM 系统属性、application.properties 和 application.yml 文件,甚至还有 Spring Cloud Config Server(如果应用程序是 Config Server 的客户端)的属性。
下方示例显示了一个大大缩短的 /env 端点响应示例:
Bash
$ curl localhost:8080/actuator/env
{
"activeProfiles": [
"development"
],
"propertySources": [
...
{
"name": "systemEnvironment",
"properties": {
"PATH": {
"value": "/usr/bin:/bin:/usr/sbin:/sbin",
"origin": "System Environment Property \"PATH\""
},
...
"HOME": {
"value": "/Users/habuma",
"origin": "System Environment Property \"HOME\""
}
}
},
{
"name": "applicationConfig: [classpath:/application.yml]",
"properties": {
"spring.application.name": {
"value": "ingredient-service",
"origin": "class path resource [application.yml]:3:11"
},
"server.port": {
"value": 8080,
"origin": "class path resource [application.yml]:9:9"
},
...
}
}
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
尽管 /env 的完整响应提供了更多的信息,但在上述示例中显示的内容包含了一些值得注意的元素。首先,注意到响应的顶部附近有一个名为 activeProfiles 的字段。在这个例子中,它表示开发配置文件是活动的。如果有其他活动的配置文件,它们也会被列出。
接下来,propertySources 字段是一个数组,包含了 Spring 应用程序环境中每个属性源的条目。在上述示例中只显示了 systemEnvironment 和一个引用 application.yml 文件的 applicationConfig 属性源。
在每个属性源中,都列出了该源提供的所有属性,以及它们的值。在 application.yml 属性源的情况下,每个属性的 origin 字段准确地告诉了属性设置的位置,包括 application.yml 中的行和列。
/env 端点也可以用来获取特定的属性,当该属性的名称作为路径的第二个元素给出时。例如,要检查 server.port 属性,提交一个针对 /env/server.port 的 GET 请求,如下所示:
Bash
$ curl localhost:8080/actuator/env/server.port
{
"property": {
"source": "systemEnvironment",
"value": "8080"
},
"activeProfiles": [ "development" ],
"propertySources": [
{ "name": "server.ports" },
{ "name": "mongo.ports" },
{ "name": "systemProperties" },
{ "name": "systemEnvironment",
"property": {
"value": "8080",
"origin": "System Environment Property \"SERVER_PORT\""
}
},
{ "name": "random" },
{ "name": "applicationConfig: [classpath:/application.yml]",
"property": {
"value": 0,
"origin": "class path resource [application.yml]:9:9"
}
},
{ "name": "springCloudClientHostInfo" },
{ "name": "refresh" },
{ "name": "defaultProperties" },
{ "name": "Management Server" }
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
如你所见,所有的属性源仍然被表示出来,但只有那些设置了指定属性的属性源才会包含任何额外的信息。在这个例子中,systemEnvironment 属性源和 application.yml 属性源都有 server.port 属性的值。因为 systemEnvironment 属性源优先于它下面列出的任何属性源,所以它的值 8080 胜出。胜出的值在属性字段的顶部反映出来。
/env 端点可以用于不仅仅是读取属性值。通过向 /env 端点提交一个 POST 请求,以及一个带有名称和值字段的 JSON 文档,你也可以在运行的应用程序中设置属性。例如,要将一个名为 tacocloud.discount.code 的属性设置为 TACOS1234,你可以使用 curl 在命令行提交 POST 请求,如下所示:
Bash
$ curl localhost:8080/actuator/env \
-d'{"name":"tacocloud.discount.code","value":"TACOS1234"}' \
-H "Content-type: application/json"
{"tacocloud.discount.code":"TACOS1234"}1
2
3
4
2
3
4
提交属性后,新设置的属性及其值将在响应中返回。稍后,如果你决定不再需要该属性,你可以向 /env 端点提交一个 DELETE 请求,如下所示,以删除通过该端点创建的所有属性:
Bash
$ curl localhost:8080/actuator/env -X DELETE
{"tacocloud.discount.code":"TACOS1234"}1
2
2
尽管通过 Actuator 的 API 设置属性可能很有用,但重要的是要意识到,通过向 /env 端点发送 POST 请求设置的任何属性只适用于接收请求的应用程序实例,这些属性是临时的,并且在应用程序重启时将会丢失。
2.2.4. 浏览 HTTP 请求映射
尽管 Spring MVC(和 Spring WebFlux)的编程模型通过简单地用请求映射注解标注方法,使得处理 HTTP 请求变得容易,但有时候要对一个应用程序可以处理的所有类型的 HTTP 请求以及处理这些请求的组件类型有一个全局的理解可能会有挑战。
Actuator 的 /mappings 端点提供了一个一站式的视图,展示了应用程序中的每一个 HTTP 请求处理器,无论它是来自于 Spring MVC 控制器还是 Actuator 自己的端点。要获取 Spring Boot 应用程序中所有端点的完整列表,向 /mappings 端点发出一个 GET 请求,你可能会收到类似于下面所示的简化响应。
Bash
$ curl localhost:8080/actuator/mappings | jq
{
"contexts": {
"application-1": {
"mappings": {
"dispatcherHandlers": {
"webHandler": [
...
{
"predicate": "{[/ingredients],methods=[GET]}",
"handler": "public reactor.core.publisher.Flux<tacos.ingredients.Ingredient> tacos.ingredients.IngredientsController.allIngredients()",
"details": {
"handlerMethod": {
"className": "tacos.ingredients.IngredientsController",
"name": "allIngredients",
"descriptor": "()Lreactor/core/publisher/Flux;"
},
"handlerFunction": null,
"requestMappingConditions": {
"consumes": [],
"headers": [],
"methods": [
"GET"
],
"params": [],
"patterns": [
"/ingredients"
],
"produces": []
}
}
},
...
]
}
},
"parentId": "application-1"
},
"bootstrap": {
"mappings": {
"dispatcherHandlers": {}
},
"parentId": null
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
在这里,来自 curl 命令行的响应被管道传输到一个叫做 jq 的工具,它可以做很多事情,其中包括以易读的格式漂亮地打印从请求返回的 JSON。为了简洁,这个响应已经被缩短,只显示了一个请求处理器。具体来说,它显示了对 /ingredients 的 GET 请求将由 IngredientsController 的 allIngredients() 方法处理。
2.2.5. 管理日志级别
日志是任何应用程序的重要特性。日志可以提供一种审计的手段,也可以作为一种粗糙的调试手段。
设置日志级别可能是一种平衡行为。如果你将日志级别设置得过于详细,日志中可能会有太多的噪声,找到有用的信息可能会很困难。另一方面,如果你将日志级别设置得过于宽松,日志可能在理解应用程序正在做什么方面没有太大的价值。
日志级别通常是按包逐个应用的。如果你想知道你正在运行的 Spring Boot 应用程序中设置了哪些日志级别,你可以向 /loggers 端点发出 GET 请求。下面的 JSON 代码显示了对 /loggers 的响应的一个摘录:
JSON
{
"levels": [ "OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" ],
"loggers": {
"ROOT": {
"configuredLevel": "INFO",
"effectiveLevel": "INFO"
},
"org.springframework.web": {
"configuredLevel": null,
"effectiveLevel": "INFO"
},
"tacos": {
"configuredLevel": null,
"effectiveLevel": "INFO"
},
"tacos.ingredients": {
"configuredLevel": null,
"effectiveLevel": "INFO"
},
"tacos.ingredients.IngredientServiceApplication": {
"configuredLevel": null,
"effectiveLevel": "INFO"
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
响应首先列出了所有有效的日志级别。之后,loggers 元素列出了应用程序中每个包的日志级别详情。configuredLevel 属性显示了已经明确配置的日志级别(如果没有明确配置,则为 null)。effectiveLevel 属性给出了有效的日志级别,这可能是从父包或根日志记录器继承的。
尽管这个摘录只显示了根日志记录器和四个包的日志级别,但完整的响应将包括应用程序中每个单独包的日志级别条目,包括那些正在使用的库的条目。如果你更愿意将你的请求集中在一个特定的包上,你可以在请求中指定包名作为额外的路径组件。
例如,如果你只想知道 tacos.ingredients 包设置了哪些日志级别,你可以向 /loggers/tacos.ingredients 发出请求,如下所示:
JSON
{
"configuredLevel": null,
"effectiveLevel": "INFO"
}1
2
3
4
2
3
4
除了返回应用程序包的日志级别,/loggers 端点还允许你通过发出 POST 请求来更改配置的日志级别。例如,假设你想将 tacos.ingredients 包的日志级别设置为 DEBUG。下面的 curl 命令将实现这一点:
Bash
$ curl localhost:8080/actuator/loggers/tacos/ingredients \
-d'{"configuredLevel":"DEBUG"}' \
-H"Content-type: application/json"1
2
3
2
3
现在日志级别已经被更改,你可以像这样向 /loggers/tacos/ingredients 发出 GET 请求,以查看它已经被更改:
JSON
{
"configuredLevel": "DEBUG",
"effectiveLevel": "DEBUG"
}1
2
3
4
2
3
4
注意,之前 configuredLevel 是 null,现在它是 DEBUG。这个更改也影响到了 effectiveLevel。
2.3. 查看应用程序活动
在运行的应用程序中,监控活动可能是有用的,包括应用程序正在处理的 HTTP 请求类型和所有线程的活动。为此,Actuator 提供了 /httptrace、/threaddump 和 /heapdump 端点。
/heapdump 端点可能是最难以详细描述的 Actuator 端点。简洁地说,它下载一个 gzip 压缩的 HPROF 堆转储文件,可以用来追踪内存或线程问题。由于空间的限制,以及堆转储的使用是一个相当高级的特性,我将把 /heapdump 端点的介绍限制在这一段落。
2.3.1. 追踪 HTTP 活动
/httptrace 端点报告了应用程序处理的最近 100 个请求的详细信息。包括的详细信息有请求方法和路径,一个时间戳表示何时处理了请求,请求和响应的头部,以及处理请求所花费的时间。
以下的 JSON 代码片段显示了 /httptrace 端点响应的单个条目:
JSON
{
"traces": [
{
"timestamp": "2020-06-03T23:41:24.494Z",
"principal": null,
"session": null,
"request": {
"method": "GET",
"uri": "http://localhost:8080/ingredients",
"headers": {
"Host": [ "localhost:8080" ],
"User-Agent": [ "curl/7.54.0" ],
"Accept": [ "*/*" ]
},
"remoteAddress": null
},
"response": {
"status": 200,
"headers": {
"Content-Type": [
"application/json;charset=UTF-8"
]
}
},
"timeTaken": 4
},
...
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
虽然这些信息可能对于调试目的有用,但是当跟踪数据随着时间的推移而被跟踪时,它们就变得更加有趣了,它们提供了关于应用程序在任何给定时间有多忙,以及根据响应状态的值,成功的请求数量与失败的请求数量相比有多少的洞察。在后续,你将看到 Spring Boot Admin 如何将这些信息捕获到一个运行图中,该图随着时间的推移可视化 HTTP 跟踪信息。
2.3.2. 监控线程
除了 HTTP 请求跟踪,线程活动也可以在确定正在运行的应用程序中发生了什么有所帮助。/threaddump 端点生成了当前线程活动的快照。以下来自 /threaddump 响应的片段给出了这个端点提供的内容的一部分:
JSON
{
"threadName": "reactor-http-nio-8",
"threadId": 338,
"blockedTime": -1,
"blockedCount": 0,
"waitedTime": -1,
"waitedCount": 0,
"lockName": null,
"lockOwnerId": -1,
"lockOwnerName": null,
"inNative": true,
"suspended": false,
"threadState": "RUNNABLE",
"stackTrace": [
{
"methodName": "kevent0",
"fileName": "KQueueArrayWrapper.java",
"lineNumber": -2,
"className": "sun.nio.ch.KQueueArrayWrapper",
"nativeMethod": true
},
{
"methodName": "poll",
"fileName": "KQueueArrayWrapper.java",
"lineNumber": 198,
"className": "sun.nio.ch.KQueueArrayWrapper",
"nativeMethod": false
},
...
],
"lockedMonitors": [
{
"className": "io.netty.channel.nio.SelectedSelectionKeySet",
"identityHashCode": 1039768944,
"lockedStackDepth": 3,
"lockedStackFrame": {
"methodName": "lockAndDoSelect",
"fileName": "SelectorImpl.java",
"lineNumber": 86,
"className": "sun.nio.ch.SelectorImpl",
"nativeMethod": false
}
},
...
],
"lockedSynchronizers": [],
"lockInfo": null
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
完整的线程转储报告包括正在运行的应用程序中的每一个线程。为了节省空间,这里的线程转储显示了一个单线程的缩略条目。如你所见,它包括了关于线程的阻塞和锁定状态的详细信息,以及其他线程的具体情况。还有一个堆栈跟踪,它提供了一些洞察,可以看出线程在代码的哪个区域花费时间。
因为 /threaddump 端点只在请求时提供线程活动的快照,所以要获取线程随时间变化的行为的完整画面可能会有些困难。在后续,你将看到 Spring Boot Admin 如何在实时视图中监控 /threaddump 端点。
2.4. 利用运行时指标
/metrics 端点可以报告由正在运行的应用程序产生的许多指标,包括内存、处理器、垃圾收集和 HTTP 请求。Actuator 提供了超过两打的开箱即用的指标类别,这一点可以通过发出 GET 请求到 /metrics 返回的以下指标类别列表得到证实:
Bash
$ curl localhost:8080/actuator/metrics | jq
{
"names": [
"jvm.memory.max",
"process.files.max",
"jvm.gc.memory.promoted",
"http.server.requests",
"system.load.average.1m",
"jvm.memory.used",
"jvm.gc.max.data.size",
"jvm.memory.committed",
"system.cpu.count",
"logback.events",
"jvm.buffer.memory.used",
"jvm.threads.daemon",
"system.cpu.usage",
"jvm.gc.memory.allocated",
"jvm.threads.live",
"jvm.threads.peak",
"process.uptime",
"process.cpu.usage",
"jvm.classes.loaded",
"jvm.gc.pause",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"process.files.open",
"jvm.buffer.count",
"jvm.buffer.total.capacity",
"process.start.time"
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
涵盖了如此多的指标,以至于在这一章中以任何有意义的方式讨论它们都是不可能的。相反,让我们关注一类指标,http.server.requests,作为如何使用 /metrics 端点的一个例子。
如果你不仅仅是请求 /metrics,而是发出一个针对 /metrics/{metrics name} 的 GET 请求,你将会收到更多关于该类别指标的详细信息。以 http.server.requests 为例,对 /metrics/http.server.requests 发出 GET 请求返回的数据看起来像下面这样:
Bash
$ curl localhost:8080/actuator/metrics/http.server.requests
{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 2103 },
{ "statistic": "TOTAL_TIME", "value": 18.086334315 },
{ "statistic": "MAX", "value": 0.028926313 }
],
"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" ]
}
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
这个响应中最重要的部分是 measurements 部分,它包括了请求类别的所有指标。在这个例子中,它报告了有 2,103 个 HTTP 请求。处理这些请求所花费的总时间是 18.086334315 秒,处理任何请求所花费的最大时间是 0.028926313 秒。
这些通用指标很有趣,但你可以通过使用列在 availableTags 下的标签进一步缩小结果。例如,你知道已经有 2,103 个请求,但未知的是有多少个结果是 HTTP 200,相对于 HTTP 404 或 HTTP 500 的响应状态。使用 status 标签,你可以获取所有结果为 HTTP 404 状态的请求的指标,就像这样:
Bash
$ curl localhost:8080/actuator/metrics/http.server.requests? \
tag=status:404
{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 31 },
{ "statistic": "TOTAL_TIME", "value": 0.522061212 },
{ "statistic": "MAX", "value": 0 }
],
"availableTags": [
{ "tag": "exception", "values": [ "ResponseStatusException", "none" ] },
{ "tag": "method", "values": [ "GET" ] },
{ "tag": "uri", "values": [ "/actuator/metrics/{requiredMetricName}", "/**" ] }
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
通过指定标签名称和值与标签请求属性,你现在可以看到专门针对结果为 HTTP 404 响应的请求的指标。这显示了有 31 个请求结果为 404,服务它们全部花费了 0.522061212 秒。此外,很明显,一些失败的请求是对 /actuator/metrics/{requiredMetricsName} 的 GET 请求(尽管不清楚 {requiredMetricsName} 路径变量解析为什么)。还有一些是对其他路径的请求,由 /** 通配符路径捕获。
如果你想知道那些 HTTP 404 响应中有多少是针对 /** 路径的呢?你需要做的就是在请求中指定 uri 标签,就像这样:
Bash
$ curl "localhost:8080/actuator/metrics/http.server.requests? \
tag=status:404&tag=uri:/**"
{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 30 },
{ "statistic": "TOTAL_TIME", "value": 0.519791548 },
{ "statistic": "MAX", "value": 0 }
],
"availableTags": [
{ "tag": "exception", "values": [ "ResponseStatusException" ] },
{ "tag": "method", "values": [ "GET" ] }
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
现在你可以看到,有 30 个请求是针对匹配 /** 的某个路径的,结果是 HTTP 404 响应,处理这些请求总共花费了 0.519791548 秒。
你还会注意到,随着你对请求的细化,可用的标签更加有限。提供的标签只是与显示的指标捕获的请求匹配的那些。在这种情况下,exception 和 method 标签每个只有一个值。很明显,所有 30 个请求都是 GET 请求,因为 ResponseStatusException 而结果为 404。
导航 /metrics 端点可能是一项棘手的任务,但是通过一些练习,获取你正在寻找的数据并非不可能。在后续,你将看到 Spring Boot Admin 如何使从 /metrics 端点消费数据变得更加容易。
尽管 Actuator 端点提供的信息提供了对正在运行的 Spring Boot 应用程序内部工作的有用洞察,但它并不适合人类消费。因为 Actuator 端点是 REST 端点,它们提供的数据是为了被其他应用程序,也许是一个 UI。
3. 自定义 Actuator
Actuator 的最大特点之一是它可以定制以满足应用程序的特定需求。一些端点本身允许进行定制。同时,Actuator 本身允许你创建自定义端点。
让我们看看 Actuator 可以被定制的几种方式,先从向 /info 端点添加信息的方式开始。
3.1. 向 /info 端点提供信息
正如你在获取重要的应用程序信息小节中看到的,/info 端点开始时是空的,没有信息。但是,你可以通过创建以 info 为前缀的属性来轻松地向其添加数据。
虽然以 info. 为前缀的属性是将自定义数据添加到 /info 端点的一种非常简单的方式,但它并不是唯一的方式。Spring Boot 提供了一个名为 InfoContributor 的接口,允许你以编程方式添加任何你想要的信息到 /info 端点响应。Spring Boot 甚至已经准备好了一些有用的 InfoContributor 的实现,你无疑会发现它们很有用。
让我们看看你如何编写你自己的 InfoContributor 来向 /info 端点添加一些自定义信息。
3.1.1. 创建一个自定义的 InfoContributor
假设你想要向 /info 端点添加一些关于 Taco Cloud 的简单统计信息。例如,你想要包含关于创建了多少个 tacos 的信息。为了做到这一点,你可以编写一个实现 InfoContributor 的类,将 TacoRepository 注入到它,然后将 TacoRepository 给你的任何计数作为信息发布到 /info 端点。下方示例代码显示了你可能如何实现这样一个贡献者:
Java
@Component
public class TacoCountInfoContributor implements InfoContributor {
private final TacoRepository tacoRepo;
public TacoCountInfoContributor(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
@Override
public void contribute(Builder builder) {
long tacoCount = tacoRepo.count().block();
Map<String, Object> tacoMap = new HashMap<String, Object>();
tacoMap.put("count", tacoCount);
builder.withDetail("taco-stats", tacoMap);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过实现 InfoContributor,TacoCountInfoContributor 需要实现 contribute() 方法。这个方法被赋予一个 Builder 对象,contribute() 方法在此对象上调用 withDetail() 来添加信息详情。在你的实现中,你通过调用它的 count() 方法来咨询 TacoRepository,以找出已经创建了多少个 tacos。在这个特定的情况下,你正在使用一个响应式仓库,所以你需要调用 block() 来从返回的 Mono<Long> 中获取计数。然后你将这个计数放入一个 Map 中,然后你将这个 Map 以 taco-stats 的标签给到构建器。/info 端点的结果将包含这个计数,如下所示:
JSON
{
"taco-stats": {
"count": 44
}
}1
2
3
4
5
2
3
4
5
正如你所见,InfoContributor 的实现能够使用任何必要的手段来贡献信息。这与简单地用 info. 作为前缀的属性形成鲜明对比,虽然简单,但它仅限于静态值。
3.1.2. 将构建信息注入到 /info 端点
Spring Boot 自带了一些内置的 InfoContributor 实现,它们会自动将信息添加到 /info 端点的结果中。其中之一是 BuildInfoContributor,它将项目构建文件中的信息添加到 /info 端点结果中。基本信息包括项目版本、构建的时间戳,以及执行构建的主机和用户。
要启用构建信息包含在 /info 端点的结果中,需要将 build-info 目标添加到 Spring Boot Maven 插件的执行中,如下所示:
XML
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果你正在使用 Gradle 来构建你的项目,你可以简单地将以下几行添加到你的 build.gradle 文件中:
Gradle
springBoot {
buildInfo()
}1
2
3
2
3
无论哪种情况,构建都会在可分发的 JAR 或 WAR 文件中生成一个名为 build-info.properties 的文件,BuildInfoContributor 将消费并贡献给 /info 端点。以下来自 /info 端点响应的片段显示了贡献的构建信息:
JSON
{
"build": {
"artifact": "tacocloud",
"name": "taco-cloud",
"time": "2021-08-08T23:55:16.379Z",
"version": "0.0.15-SNAPSHOT",
"group": "sia"
},
...
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
这些信息对于理解正在运行的应用程序的确切版本以及它何时被构建是非常有用的。通过对 /info 端点执行 GET 请求,你将知道你是否正在运行项目的最新和最好的构建。这是一个非常有用的功能,可以帮助开发者和运维人员更好地管理他们的应用程序。如果你有任何其他问题或需要进一步的帮助,随时向我提问。我很乐意帮助你!
3.1.3. 暴露 Git 提交信息
假设你的项目是在 Git 中进行源代码控制,你可能希望在 /info 端点中包含 Git 提交信息。为了做到这一点,你需要在 Maven 项目的 pom.xml 中添加以下插件:
XML
<build>
<plugins>
...
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugins>
</build>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
如果你是一个 Gradle 用户,不用担心。有一个等效的插件可以添加到你的 build.gradle 文件中,如下所示:
Gradle
plugins {
id "com.gorylenko.gradle-git-properties" version "2.3.1"
}1
2
3
2
3
这两个插件基本上做的是同样的事情:它们生成一个名为 git.properties 的构建时工件,其中包含项目的所有 Git 元数据。一个特殊的 InfoContributor 实现在运行时发现该文件,并将其内容作为 /info 端点的一部分暴露出来。
当然,要生成 git.properties 文件,项目需要有 Git 提交元数据。也就是说,它必须是 Git 仓库的克隆,或者是一个至少有一个提交的新初始化的本地 Git 仓库。如果不是,那么这两个插件都会失败。然而,你可以配置它们来忽略缺失的 Git 元数据。对于 Maven 插件,将 failOnNoGitDirectory 属性设置为 false,就像这样:
XML
<build>
<plugins>
...
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<configuration>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
</configuration>
</plugin>
</plugins>
</build>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
同样,你可以通过在 gitProperties 下指定它来设置 Gradle 中的 failOnNoGitDirectory 属性,就像这样:
Gradle
gitProperties {
failOnNoGitDirectory = false
}1
2
3
2
3
在其最简单的形式中,/info 端点中呈现的 Git 信息包括应用程序构建所依赖的 Git 分支、提交哈希和时间戳,如下所示:
JSON
{
"git": {
"branch": "main",
"commit": {
"id": "df45505",
"time": "2021-08-08T21:51:12Z"
}
},
...
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
这个信息对于描述项目构建时代码的状态非常明确。但是,通过将 management.info.git.mode 属性设置为 full,你可以获得关于项目构建时使用的 Git 提交的非常详细的信息,如下所示:
YAML
management:
info:
git:
mode: full1
2
3
4
2
3
4
以下示例显示了完整的 Git 信息可能的样子:
JSON
"git": {
"local": {
"branch": { "ahead": "8", "behind": "0" }
},
"commit": {
"id": {
"describe-short": "df45505-dirty",
"abbrev": "df45505",
"full": "df455055daaf3b1347b0ad1d9dca4ebbc6067810",
"describe": "df45505-dirty"
},
"message": {
"short": "Apply chapter 18 edits",
"full": "Apply chapter 18 edits"
},
"user": {
"name": "Craig Walls",
"email": "craig@habuma.com"
},
"author": {
"time": "2021-08-08T15:51:12-0600"
},
"committer": {
"time": "2021-08-08T15:51:12-0600"
},
"time": "2021-08-08T21:51:12Z"
},
"branch": "master",
"build": {
"time": "2021-08-09T00:13:37Z",
"version": "0.0.15-SNAPSHOT",
"host": "Craigs-MacBook-Pro.local",
"user": {
"name": "Craig Walls",
"email": "craig@habuma.com"
}
},
"tags": "",
"total": {
"commit": { "count": "196" }
},
"closest": {
"tag": {
"commit": { "count": "" },
"name": ""
}
},
"remote": {
"origin": {
"url": "git@github.com:habuma/spring-in-action-6-samples.git"
}
},
"dirty": "true"
},1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
除了时间戳和缩短的 Git 提交哈希值,完整版本还包括提交代码的用户的姓名和电子邮件,以及提交消息和其他信息,使你能够准确地确定用于构建项目的代码。事实上,注意到在上述示例中的 dirty 字段为 true,这表明在构建项目时,构建目录中存在一些未提交的更改。
3.2. 自定义健康指标
Spring Boot 自带了几个开箱即用的健康指标,它们为 Spring 应用可能集成的许多常见外部系统提供健康信息。但在某些时候,你可能会发现你正在与 Spring Boot 既没有预见到也没有为其提供健康指标的外部系统进行交互。
例如,你的应用可能会集成一个遗留的大型机应用,而你的应用的健康可能会受到遗留系统健康状况的影响。要创建一个自定义的健康指标,你需要做的就是创建一个实现 HealthIndicator 接口的 bean。
事实证明,Taco Cloud 服务并不需要一个自定义的健康指标,因为 Spring Boot 提供的那些已经足够了。但为了演示如何开发一个自定义的健康指标,考虑以下示例,它显示了一个简单的 HealthIndicator 实现,其中健康状况是由一天中的时间稍微随机地决定的。
Java
@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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这个疯狂的健康指标首先检查当前的时间,如果已经过了中午,就返回一个 OUT_OF_SERVICE 的健康状态,并附带一些详细信息来解释这个状态的原因。即使在午餐前,健康指标有 10% 的机会报告 DOWN 状态,因为它使用一个随机数来决定是否正常。如果随机数小于 0.1,状态将被报告为 DOWN。否则,状态将为 UP。
显然,上述示例中的健康指标在任何实际应用中都不会很有用。但是,想象一下,如果它不是去查看当前的时间或一个随机数,而是去对某个外部系统进行远程调用,并根据收到的响应来确定状态。在这种情况下,它将是一个非常有用的健康指标。
3.3. 注册自定义指标
在利用运行时指标小节,我们探讨了如何浏览 /metrics 端点以获取 Actuator 发布的各种度量,特别是与 HTTP 请求相关的度量。Actuator 提供的度量非常有用,但 /metrics 端点并不仅限于这些内置度量。
实际上,Actuator 的度量是由 Micrometer 实现的,这是一个无厂商偏见的度量外观,使应用程序能够发布任何他们想要的度量,并在他们选择的第三方监控系统中展示,包括支持 Prometheus、Datadog 和 New Relic 等。
使用 Micrometer 发布度量的最基本方式是通过 Micrometer 的 MeterRegistry。在 Spring Boot 应用程序中,你需要做的就是在可能需要发布计数器、计时器或者度量你的应用程序的度量的地方注入一个 MeterRegistry。
举个例子,假设你想要跟踪用不同配料制作的 tacos 的数量。也就是说,你想要统计用生菜、磨碎的牛肉、面粉玉米饼或者任何可用的配料制作的 tacos 的数量。下方示例中的 TacoMetrics bean 展示了你可能如何使用 MeterRegistry 来收集这些信息。
Java
@Component
public class TacoMetrics extends AbstractRepositoryEventListener<Taco> {
private MeterRegistry meterRegistry;
public TacoMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void onAfterCreate(Taco taco) {
List<Ingredient> ingredients = taco.getIngredients();
for (Ingredient ingredient : ingredients) {
meterRegistry.counter("tacocloud",
"ingredient", ingredient.getId()).increment();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如你所见,TacoMetrics 通过其构造函数注入了一个 MeterRegistry。它还扩展了 AbstractRepositoryEventListener,这是一个 Spring Data 类,使得可以拦截仓库事件,并覆盖了 onAfterCreate() 方法,以便在每次保存新的 Taco 对象时都能得到通知。
在 onAfterCreate() 中,为每个配料声明了一个计数器,其中标签名为 ingredient,标签值等于配料 ID。如果已经存在具有该标签的计数器,它将被重用。计数器增加,表示又为该配料创建了一个 taco。
创建了几个 tacos 之后,你可以开始查询 /metrics 端点以获取配料计数。对 /metrics/tacocloud 发送 GET 请求会产生一些未过滤的度量计数,如下所示:
Bash
$ curl localhost:8080/actuator/metrics/tacocloud
{
"name": "tacocloud",
"measurements": [ { "statistic": "COUNT", "value": 84 } ],
"availableTags": [
{
"tag": "ingredient",
"values": [
"FLTO", "CHED", "LETC", "GRBF",
"COTO", "JACK", "TMTO", "SLSA"
]
}
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
measurements 下的计数值在这里并没有太大意义,因为它是所有配料的所有计数的总和。但假设你想知道有多少 tacos 是用面粉玉米饼(FLTO)制作的。你需要做的就是指定一个值为 FLTO 的 ingredient 标签,如下所示:
Bash
$ curl localhost:8080/actuator/metrics/tacocloud?tag=ingredient:FLTO
{
"name": "tacocloud",
"measurements": [ { "statistic": "COUNT", "value": 39 } ],
"availableTags": []
}1
2
3
4
5
6
2
3
4
5
6
现在很明显,有 39 个 tacos 使用了面粉玉米饼作为其中的一种配料。
3.4. 创建自定义端点
乍一看,你可能会认为 Actuator 的端点只不过是 Spring MVC 控制器的实现。但是,如你将在《Spring In Action 6th:使用 JMX 监控 Spring》中看到,这些端点也被暴露为 JMX MBeans,同 HTTP 请求一样。因此,这些端点必定不仅仅是一个控制器类。
实际上,Actuator 端点与控制器的定义方式截然不同。Actuator 端点是用带有 @Endpoint 注解的类定义的,而不是带有 @Controller 或 @RestController 注解的类。
更进一步,Actuator 端点操作是由带有 @ReadOperation、@WriteOperation 和 @DeleteOperation 注解的方法定义的,而不是使用像 @GetMapping、@PostMapping 或 @DeleteMapping 这样的 HTTP 命名注解。这些注解并不暗示任何特定的通信机制,实际上,它们允许 Actuator 通过任何种类的通信机制进行通信,包括开箱即用的 HTTP 和 JMX。为了演示如何编写一个自定义的 Actuator 端点,考虑下方示例中的 NotesEndpoint:
Java
@Component
@Endpoint(id = "notes", enableByDefault = true)
public class NotesEndpoint {
private List<Note> notes = new ArrayList<>();
@ReadOperation
public List<Note> notes() {
return notes;
}
@WriteOperation
public List<Note> addNote(String text) {
notes.add(new Note(text));
return notes;
}
@DeleteOperation
public List<Note> deleteNote(int index) {
if (index < notes.size()) {
notes.remove(index);
}
return notes;
}
class Note {
private Date time = new Date();
private final String text;
public Note(String text) {
this.text = text;
}
public Date getTime() {
return time;
}
public String getText() {
return text;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
这个端点是一个简单的记事(note-taking)端点,可以通过写操作提交笔记,通过读操作读取笔记列表,通过删除操作删除笔记。诚然,就 Actuator 端点而言,这个端点并不是很有用。但是,当你考虑到开箱即用的 Actuator 端点覆盖了如此广泛的领域,想出一个实用的自定义 Actuator 端点实例是很困难的。
无论如何,NotesEndpoint 类被注解为 @Component,因此它将被 Spring 的组件扫描捕获,并在 Spring 应用程序上下文中实例化为一个 bean。但更相关的是,它还被注解为 @Endpoint,使其成为一个 ID 为 notes 的 Actuator 端点。并且它默认启用,因此你不需要通过将其包含在 management.endpoints.web.exposure.include 配置属性中来显式启用它。
如你所见,NotesEndpoint 提供了每种类型的操作:
notes()方法被注解为@ReadOperation:当调用时,它将返回可用笔记的列表。在 HTTP 术语中,这意味着它将处理对/actuator/notes的 HTTP GET 请求,并以 JSON 笔记列表的形式响应;addNote()方法被注解为@WriteOperation:当调用时,它将从给定的文本创建一个新的笔记并将其添加到列表中。在 HTTP 术语中,它处理 POST 请求,其中请求的主体是一个带有 text 属性的 JSON 对象。它完成后,会以当前笔记列表的状态进行响应;deleteNote()方法被注解为@DeleteOperation:当调用时,它将删除给定索引处的笔记。在 HTTP 术语中,此端点处理 DELETE 请求,其中索引作为请求参数给出;
要查看此操作,你可以使用 curl 来探索这个新的端点。首先,使用两个单独的 POST 请求添加几个笔记,如下所示:
Bash
$ curl localhost:8080/actuator/notes \
-d'{"text":"Bring home milk"}' \
-H"Content-type: application/json"
[{"time":"2020-06-08T13:50:45.085+0000","text":"Bring home milk"}]
$ curl localhost:8080/actuator/notes \
-d'{"text":"Take dry cleaning"}' \
-H"Content-type: application/json"
[{"time":"2021-07-03T12:39:13.058+0000","text":"Bring home milk"},
{"time":"2021-07-03T12:39:16.012+0000","text":"Take dry cleaning"}]1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
如你所见,每次发布新的笔记时,端点都会以添加新笔记后的列表作为响应。但是,如果你稍后想查看笔记列表,你可以像这样发出一个简单的 GET 请求:
Bash
$ curl localhost:8080/actuator/notes
[{"time":"2021-07-03T12:39:13.058+0000","text":"Bring home milk"},
{"time":"2021-07-03T12:39:16.012+0000","text":"Take dry cleaning"}]1
2
3
2
3
如果你决定删除其中一条笔记,一个带有索引请求参数的 DELETE 请求,如下所示,应该可以实现这个目标:
Bash
$ curl localhost:8080/actuator/notes?index=1 -X DELETE
[{"time":"2021-07-03T12:39:13.058+0000","text":"Bring home milk"}]1
2
2
需要注意的是,虽然我只展示了如何使用 HTTP 与端点进行交互,但它也会被暴露为一个 MBean,可以使用你选择的任何 JMX 客户端进行访问。但是,如果你想将其限制为仅暴露 HTTP 端点,你可以使用 @WebEndpoint 注解端点类,而不是 @Endpoint,如下所示:
Java
@Component
@WebEndpoint(id = "notes", enableByDefault = true)
public class NotesEndpoint {
...
}1
2
3
4
5
2
3
4
5
同样,如果你更喜欢只有 MBean 的端点,可以使用 @JmxEndpoint 注解类。
4. 保护 Actuator
Actuator 展示的信息可能不是你希望被窥视者看到的。此外,由于 Actuator 提供了一些让你改变环境属性和日志级别的操作,因此最好对 Actuator 进行安全保护,以便只有具有适当访问权限的客户端才能使用其端点。
尽管保护 Actuator 很重要,但安全性并不在 Actuator 的职责范围内。相反,你需要使用 Spring Security 来保护 Actuator。并且,因为 Actuator 端点就像应用程序中的任何其他路径一样,所以保护 Actuator 与保护任何其他应用程序路径没有什么特别之处。我们在《Spring In Action 6th:Spring 安全》中讨论的所有内容都适用于保护 Actuator 端点。
由于所有 Actuator 端点都聚集在 /actuator(或可能是其他基础路径,如果设置了 management.endpoints.web.base-path 属性)的公共基础路径下,因此很容易对所有 Actuator 端点应用授权规则。例如,要求用户具有 ROLE_ADMIN 权限才能调用 Actuator 端点,你可能需要像这样覆盖 WebSecurityConfigurerAdapter 的 configure() 方法:
Java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/actuator/**").hasRole("ADMIN")
.and()
.httpBasic();
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Warning:
WebSecurityConfigurerAdapter已不再推荐使用,建议的方法是注册一个SecurityFilterChainbean,详细说明可参见 Spring 官方说明文档。
这要求所有请求都来自具有 ROLE_ADMIN 权限的经过身份验证的用户。它还配置了 HTTP 基本认证,以便客户端应用程序可以在其请求的 Authorization 头中提交编码的认证信息。
保护 Actuator 的这种方式的唯一真正问题是,端点的路径被硬编码为 /actuator/**。如果由于更改 management.endpoints.web.base-path 属性而发生变化,它将不再工作。为了帮助解决这个问题,Spring Boot 还提供了 EndpointRequest —— 一个请求匹配器类,使这个过程更加简单,且不依赖于给定的字符串路径。使用 EndpointRequest,你可以在不硬编码 /actuator/** 路径的情况下,应用相同的安全要求于 Actuator 端点,如下所示:
Java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
EndpointRequest.toAnyEndpoint() 方法返回一个请求匹配器,它匹配任何 Actuator 端点。如果你想从请求匹配器中排除一些端点,你可以调用 excluding() 方法,按名称指定它们,如下所示:
Java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(
EndpointRequest.toAnyEndpoint()
.excluding("health", "info"))
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
另一方面,如果你希望只对少数几个 Actuator 端点应用安全性,你可以通过调用 to() 方法而不是 toAnyEndpoint() 来按名称指定这些端点,如下所示:
Java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(EndpointRequest.to(
"beans", "threaddump", "loggers"))
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
这将 Actuator 的安全性限制在 /beans、/threaddump 和 /loggers 端点上。所有其他的 Actuator 端点都是完全开放的。
5. 总结
Spring Boot Actuator 提供了多个端点,既有 HTTP 端点也有 JMX MBeans,让你可以窥探 Spring Boot 应用程序的内部工作;
大多数 Actuator 端点默认是禁用的,但可以通过设置
management.endpoints.web.exposure.include和management.endpoints.web.exposure.exclude来选择性地暴露;一些端点,如
/loggers和/env端点,允许进行写操作,以实时更改正在运行的应用程序的配置;应用程序的构建和 Git 提交的详细信息可以在
/info端点中暴露;应用程序的健康状况可以通过自定义健康指标来影响,跟踪外部集成应用程序的健康状况;
可以通过 Micrometer 注册自定义应用程序度量,这使得 Spring Boot 应用程序可以立即与 Datadog、New Relic 和 Prometheus 等多个流行的度量引擎集成;
Actuator Web 端点可以使用 Spring Security 进行安全保护,就像 Spring 应用程序中的任何其他端点一样;