在本指南中,你将学习如何使用 Elastic Observability 监控 Java 应用:日志、基础设施指标、APM 和 Uptime。
你将学到什么
你将学习如何:
-
创建一个示例 Java 应用。
-
使用 Filebeat 摄取日志,并在 Kibana 中查看日志。
-
使用 Metricbeat Prometheus Module 摄取指标,并在 Kibana 中查看指标。
-
使用 Elastic APM Java agent 为应用添加 instrumentation。
-
使用 Heartbeat 监控你的服务,并在 Kibana 中查看 Uptime 数据。
提示
我们建议使用 Elastic OpenTelemetry Java SDK,作为为 Java 应用添加 instrumentation 的首选方式。EDOT Java SDK 提供基于 OpenTelemetry 的 instrumentation,并且可以将遥测数据发送到 Elastic。
开始之前
创建一个 Elastic Cloud Hosted 部署。该部署包含一个用于存储和搜索数据的 Elasticsearch 集群、一个用于可视化和管理数据的 Kibana,以及一个 APM Server。你也可以参考文章 "如何在 Linux,MacOS 及 Windows 上进行安装 Elasticsearch" 创建自己的 Elasticsearch 集群。
如果你不想按照本教程中的所有步骤操作,而只是希望查看最终的 Java 代码,可以查看 observability-contrib GitHub 仓库中的示例应用。
第 1 步:创建 Java 应用
要创建 Java 应用,你需要 OpenJDK 14 或更高版本,以及 Javalin Web 框架。
该应用将包含主端点、一个人为设置为长时间运行的端点,以及一个需要轮询另一个数据源的端点。此外,还会运行一个后台任务。
1)设置 Gradle 项目,并创建 build.gradle 文件。
代码保持原样:
markdown
`
1. plugins {
2. id 'java'
3. id 'application'
4. }
6. repositories {
7. jcenter()
8. }
10. dependencies {
11. implementation 'io.javalin:javalin:3.10.1'
13. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
14. testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2'
15. }
17. application {
18. mainClassName = 'de.spinscale.javalin.App'
19. }
21. test {
22. useJUnitPlatform()
23. }
`AI代码解释
2)运行以下命令:
bash
`
1. echo "rootProject.name = 'javalin-app'" >> settings.gradle
3. mkdir -p src/main/java/de/spinscale/javalin
4. mkdir -p src/test/java/de/spinscale/javalin
`AI代码解释
3)安装 Gradle Wrapper。
一种简单的安装 Gradle 的方法是使用 sdkman,然后运行:
go
`sdk install gradle 6.5.1`AI代码解释
接下来,在当前目录运行:
go
`gradle wrapper`AI代码解释
以安装 Gradle Wrapper。
4)运行:
bash
`./gradlew clean check`AI代码解释
你应该看到构建成功,但目前还没有任何内容被构建或编译。
5)要创建 Javalin Server 及其第一个端点,也就是主端点,创建 src/main/java/de/spinscale/javalin/App.java 文件。
java
`
1. package de.spinscale.javalin;
3. import io.javalin.Javalin;
5. public class App {
6. public static void main(String[] args) {
7. Javalin app = Javalin.create().start(7000);
8. app.get("/", ctx -> ctx.result("Appsolutely perfect"));
9. }
10. }
`AI代码解释
6)运行:
bash
`./gradlew assemble`AI代码解释
该命令会在 build 目录中编译出 App.class 文件。不过,现在还没有办法启动 Server。
接下来创建一个 jar,其中包含已经编译的类以及所有必需的依赖。
7)在 build.gradle 文件中修改 plugins:
bash
`
1. plugins {
2. id 'com.github.johnrengelman.shadow' version '6.0.0'
3. id 'application'
4. id 'java'
5. }
`AI代码解释
8)运行:
bash
`./gradlew shadowJar`AI代码解释
该命令会创建 build/libs/javalin-app-all.jar 文件。
shadowJar 插件需要知道主类的信息。
9)将以下代码添加到 build.gradle:
markdown
`
1. jar {
2. manifest {
3. attributes 'Main-Class': 'de.spinscale.javalin.App'
4. }
5. }
`AI代码解释
10)重新构建项目并启动 Server:
bash
`java -jar build/libs/javalin-app-all.jar`AI代码解释
打开另一个终端,运行:
go
`curl localhost:7000`AI代码解释
即可看到 HTTP 响应。
11)测试代码。
将所有内容都放在 main() 方法中会使代码难以测试。不过,通过使用专门的 Handler 可以解决这个问题。
重构 App 类:
java
`
1. package de.spinscale.javalin;
3. import io.javalin.Javalin;
4. import io.javalin.http.Handler;
6. public class App {
7. public static void main(String[] args) {
8. Javalin app = Javalin.create().start(7000);
9. app.get("/", mainHandler());
10. }
12. static Handler mainHandler() {
13. return ctx -> ctx.result("Appsolutely perfect");
14. }
15. }
`AI代码解释
在 build.gradle 文件中添加 Mockito 和 AssertJ 依赖。
然后在 src/test/java/de/spinscale/javalin 中创建 AppTests.java。
12)测试通过后,构建并打包应用:
bash
`./gradlew clean check shadowJar`AI代码解释
第 2 步:摄取日志
日志可以表示各种事件,例如结账、异常或者 HTTP 请求。
在本教程中,我们使用 log4j2 作为日志实现。
添加日志实现
1)将依赖添加到 build.gradle 文件。
markdown
`
1. dependencies {
2. implementation 'io.javalin:javalin:3.10.1'
3. implementation 'org.apache.logging.log4j:log4j-slf4j18-impl:2.13.3'
5. ...
6. }
`AI代码解释
2)要开始记录日志,编辑 App.java 文件并修改一个 Handler。
注意
logger 调用必须位于 lambda 中。否则,日志消息只会在启动过程中记录一次。
java
`
1. package de.spinscale.javalin;
3. import io.javalin.Javalin;
4. import io.javalin.http.Handler;
5. import org.slf4j.Logger;
6. import org.slf4j.LoggerFactory;
8. public class App {
10. private static final Logger logger = LoggerFactory.getLogger(App.class);
12. public static void main(String[] args) {
13. Javalin app = Javalin.create();
14. app.get("/", mainHandler());
15. app.start(7000);
16. }
18. static Handler mainHandler() {
19. return ctx -> {
20. logger.info("This is an informative logging message, user agent [{}]", ctx.userAgent());
21. ctx.result("Appsolutely perfect");
22. };
23. }
24. }
`AI代码解释
3)在 src/main/resources/log4j2.xml 中创建 log4j2 配置。你可能需要先创建该目录。
xml
`
1. <?xml version="1.0" encoding="UTF-8"?>
2. <Configuration>
3. <Appenders>
4. <Console >
5. <PatternLayout pattern="%d{HH:mm:ss.SSS} [%-5level] %logger{36} %msg%n"/>
6. </Console>
7. </Appenders>
8. <Loggers>
9. <Logger />
10. <Root level="ERROR">
11. <AppenderRef ref="Console" />
12. </Root>
13. </Loggers>
14. </Configuration>
`AI代码解释
默认情况下,这里只记录 ERROR 级别的日志。对于 App 类,还额外配置了 INFO 级别,因此所有 INFO 日志也会被记录。
重新打包并重新启动后,日志消息会显示在终端中。
记录请求
根据应用流量以及流量是否发生在应用之外,在应用层面记录每一个请求可能非常有意义。
1)在 App.java 中编辑 App 类,为请求添加日志记录。
arduino
`
1. public class App {
3. private static final Logger logger = LoggerFactory.getLogger(App.class);
5. public static void main(String[] args) {
6. Javalin app = Javalin.create(config -> {
7. config.requestLogger((ctx, executionTimeMs) -> {
8. logger.info("{} {} {} {} \"{}\" {}",
9. ctx.method(), ctx.url(), ctx.req.getRemoteHost(),
10. ctx.res.getStatus(), ctx.userAgent(), executionTimeMs.longValue());
11. });
12. });
13. app.get("/", mainHandler());
14. app.start(7000);
15. }
17. static Handler mainHandler() {
18. return ctx -> {
19. logger.info("This is an informative logging message, user agent [{}]", ctx.userAgent());
20. ctx.result("Appsolutely perfect");
21. };
22. }
23. }
`AI代码解释
2)重新构建并重新启动应用。现在,每一个请求都会被记录。
sql
`10:43:50.066 [INFO ] de.spinscale.javalin.App - GET / 200 0:0:0:0:0:0:0:1 "curl/7.64.1" 7`AI代码解释
创建 ISO8601 时间戳
在将日志摄取到 Elastic Cloud Hosted 之前,需要通过编辑 log4j2.xml 创建 ISO8601 时间戳。
注意
创建 ISO8601 时间戳后,在摄取日志时不需要再计算时间戳,因为 ISO8601 时间戳代表一个唯一的时间点,并且包含时区。
当你在多个数据中心之间运行系统并需要跟踪数据流时,时区信息会变得更加重要。
ini
`<PatternLayout pattern="%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n"/>`AI代码解释
日志条目最终会包含类似下面的时间戳:
sql
`2020-07-03T14:25:40,378+02:00 [INFO ] de.spinscale.javalin.App GET / 200 0:0:0:0:0:0:0:1 "curl/7.64.1" 0`AI代码解释
将日志写入文件和 stdout
1)为了读取日志输出,我们同时将数据写入文件和 stdout。
xml
`
1. <?xml version="1.0" encoding="UTF-8"?>
2. <Configuration>
3. <Appenders>
4. <Console >
5. <PatternLayout pattern="%highlight{%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n}"/>
6. </Console>
7. <File >
8. <PatternLayout pattern="%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n"/>
9. </File>
10. </Appenders>
11. <Loggers>
12. <Logger />
13. <Root level="ERROR">
14. <AppenderRef ref="Console" />
15. <AppenderRef ref="JavalinAppLog" />
16. </Root>
17. </Loggers>
18. </Configuration>
`AI代码解释
重新启动应用并发送请求后,日志会写入:
bash
`/tmp/javalin/app.log`AI代码解释
安装和配置 Filebeat
为了读取日志文件并将其发送到 Elasticsearch,需要使用 Filebeat。
根据你的操作系统下载并安装 Filebeat。
Windows
-
下载 Filebeat Windows zip 文件。
-
将 zip 文件内容解压到
C:\Program Files。 -
将
filebeat-[version]-windows-x86_64目录重命名为Filebeat。 -
以管理员身份打开 PowerShell。
-
运行安装 Filebeat Windows 服务的命令。
如果系统禁用了脚本执行,需要为当前会话设置执行策略,以允许脚本运行。例如:
r
`PowerShell.exe -ExecutionPolicy UnRestricted -File .\install-service-filebeat.ps1`AI代码解释
使用 Filebeat keystore
1)可以使用 Filebeat keystore 保存安全设置。
将 Cloud ID 保存到 keystore:
bash
`
1. ./filebeat keystore create
2. echo -n "<Your Cloud ID>" | ./filebeat keystore add CLOUD_ID --stdin
`AI代码解释
注意
将上面的 Cloud ID 替换成你的部署对应的 Cloud ID。要查找你的 Cloud ID,请在 cloud.elastic.co/deployments 中点击你的部署。
为了以最小权限将日志存储到 Elasticsearch,可以创建 API key,让 Filebeat 将数据发送到 Elastic Cloud。
2)在 Kibana 中进入 Management → Dev Tools,然后发送创建 API key 的请求。
bash
`
1. POST /_security/api_key
2. {
3. "name": "filebeat_javalin-app",
4. "role_descriptors": {
5. "filebeat_writer": {
6. "cluster": ["monitor", "read_ilm"],
7. "index": [
8. {
9. "names": ["filebeat-*"],
10. "privileges": ["view_index_metadata", "create_doc"]
11. }
12. ]
13. }
14. }
15. }
`AI代码解释
响应包含 api_key 和 id 字段,可以按照以下格式存储到 Filebeat keystore:id:api_key.
bash
`echo -n "IhrJJHMB4JmIUAPLuM35:1GbfxhkMT8COBB4JWY3pvQ" | ./filebeat keystore add ES_API_KEY --stdin`AI代码解释
注意
一定要指定
-n参数。否则,API key 末尾会添加换行符,这可能导致非常难以排查的问题。
运行:
bash
`./filebeat keystore list`AI代码解释
arduino
`./filebeat setup -e -E 'cloud.id=${CLOUD_ID}' -E 'cloud.auth=elastic:YOUR_SUPER_SECRET_PASS'`AI代码解释
即可查看两个设置是否已经保存。
3)加载 Filebeat Dashboard
使用 elastic 超级用户加载 Filebeat Dashboard:
arduino
`./filebeat setup -e -E 'cloud.id=${CLOUD_ID}' -E 'cloud.auth=elastic:YOUR_SUPER_SECRET_PASS'`AI代码解释
提示
如果你不希望凭据出现在 Shell 的
.history文件中,可以在命令行开头添加一个空格。根据 Shell 配置,这些命令不会被添加到历史记录中。
4)配置 Filebeat
创建 filebeat.yml,告诉 Filebeat 从哪里读取数据以及将数据发送到哪里:
bash
`
1. name: javalin-app-shipper
3. filebeat.inputs:
4. - type: log
5. paths:
6. - /tmp/javalin/*.log
8. cloud.id: ${CLOUD_ID}
9. output.elasticsearch:
10. api_key: ${ES_API_KEY}
`AI代码解释
将数据发送到 Elasticsearch
启动 Filebeat。
在 MacOS 或 Linux 上:
bash
`./filebeat -e`AI代码解释
在 Windows 上:
r
`PS C:\Program Files\filebeat> Start-Service filebeat`AI代码解释
Windows 日志文件默认存储在:
r
`C:\ProgramData\filebeat\Logs`AI代码解释
日志输出中应该能够看到类似:
bash
`2020-07-03T15:41:56.532+0200 INFO log/harvester.go:297 Harvester started for file: /tmp/javalin/app.log`AI代码解释
接下来为应用创建一些日志条目。
可以使用 wrk 等工具向应用发送请求:
arduino
`wrk -t1 -c 100 -d10s http://localhost:7000`AI代码解释
该命令大约可以产生每秒 8,000 个请求,同时也会写入大约相同数量的日志行。
第 3 步:在 Kibana 中查看日志
1)登录 Kibana 并选择 Discover 应用。
顶部会显示文档摘要。现在查看其中一个文档。

你会发现,被索引的数据远远不只是事件本身。
其中还包含文件中的偏移量、负责发送日志的组件信息、输出中的 shipper 名称,以及一个包含日志行内容的 message 字段。
你还会发现请求日志记录存在一个问题:如果 user agent 为 null,返回的内容并不是你期望的 null。
读取日志非常重要,但仅仅将日志进行索引并不能解决问题。
ini
`
1. Javalin app = Javalin.create(config -> {
2. config.requestLogger((ctx, executionTimeMs) -> {
3. String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-";
4. logger.info("{} {} {} {} \"{}\" {}",
5. ctx.method(), ctx.req.getPathInfo(), ctx.res.getStatus(),
6. ctx.req.getRemoteHost(), userAgent, executionTimeMs.longValue());
7. });
8. });
`AI代码解释
可以通过新的 request logger 修复这个问题。
csharp
`
1. static Handler mainHandler() {
2. return ctx -> {
3. String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-";
4. logger.info("This is an informative logging message, user agent [{}]", userAgent);
5. ctx.result("Appsolutely perfect");
6. };
7. }
`AI代码解释
2)接下来可以查看 Kibana 中的 Logs 应用:
Observability → Logs
如果希望观察日志流功能,可以循环运行以下 curl 请求:
bash
`while $(sleep 0.7) ; do curl localhost:7000 ; done`AI代码解释
3)点击 Stream live,即可查看连续的日志消息流,也可以高亮特定术语。
csharp
`
1. static Handler mainHandler() {
2. return ctx -> {
3. String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-";
4. logger.info("This is an informative logging message, user agent [{}]", userAgent);
5. ctx.result("Appsolutely perfect");
6. };
7. }
`AI代码解释
查看某个已经被索引的文档,可以发现整个日志消息都包含在一个字段中。
运行:
bash
`
1. GET filebeat-*/_search
2. {
3. "size": 1
4. }
`AI代码解释
需要注意:
-
比较
@timestamp字段和日志消息中的时间戳时,你会发现二者不同。这意味着,如果根据@timestamp字段进行过滤,你得到的结果可能并不是预期结果。当前的@timestamp表示事件在 Filebeat 中创建的时间,而不是应用中实际发生日志事件的时间。 -
目前无法按照 HTTP 动词、HTTP 状态码、日志级别或者生成日志消息的类等具体字段进行过滤。
第 4 步:处理日志
结构化日志
如果希望从一行日志中提取更多数据,并将它们放入多个字段,就需要进一步对日志进行结构化。
例如:
csharp
`2020-07-03T15:45:01,479+02:00 [INFO ] de.spinscale.javalin.App This is an informative logging message`AI代码解释
该消息包含四个部分:
-
timestamp -
log level -
class -
message
拆分规则也非常明显,因为其中大部分部分都通过空白字符进行分隔。
好消息是,所有 Beats 都可以在将日志行发送到 Elasticsearch 之前,使用 processors 对日志进行处理。
如果这些 processors 的能力不足,还可以让 Elasticsearch 通过 ingest node 完成更复杂的处理。
Filebeat 中的很多 module 都采用这种方式。Filebeat module 是一种针对特定软件的特定日志文件格式进行解析的方法。
使用几个 processors 和 Filebeat 配置即可完成拆分。
yaml
`
1. processors:
2. - add_host_metadata: ~
3. - dissect:
4. tokenizer: '%{timestamp} [%{log.level}] %{log.logger} %{message_content}'
5. field: "message"
6. target_prefix: ""
7. - timestamp:
8. field: "timestamp"
9. layouts:
10. - '2006-01-02T15:04:05.999Z0700'
11. test:
12. - '2020-07-18T04:59:51.123+0200'
13. - drop_fields:
14. fields: [ "message", "timestamp" ]
15. - rename:
16. fields:
17. - from: "message_content"
18. - to: "message"
`AI代码解释
dissect processor 会将日志消息拆分成四个部分。
如果希望原始消息最后一部分仍然存在于 message 字段中,需要先删除旧的 message 字段,然后再重命名字段。
dissect filter 不支持原地替换。
此外,还需要专门解析时间戳,使 @timestamp 字段包含解析后的值。
可以删除重复字段,但需要确保原始消息的一部分仍然保留在 message 字段中。
重要
是否应该删除原始消息中的部分内容,是一个值得讨论的问题。
在很多情况下,保留原始消息非常有意义。例如,如果时间戳解析出现问题,保留原始消息会让调试更加容易。
时间戳解析还有一个细微区别:Go 时间解析器只接受点作为秒和毫秒之间的分隔符,而 log4j2 默认输出使用逗号。
你可以修改日志输出,使其使用 Filebeat 期望的格式;也可以修改时间戳解析逻辑,因为你并不总能完全控制日志格式,例如日志来自第三方软件。
css
`<PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ} [%-5level] %logger{36} %msg%n"/>`AI代码解释
修改之后重新启动 Filebeat,并通过搜索查看被索引的 JSON 文档发生了什么变化。
bash
`
1. GET filebeat-*/_search?filter_path=**._source
2. {
3. "size": 1,
4. "_source": {
5. "excludes": [
6. "host.ip",
7. "host.mac"
8. ]
9. },
10. "sort": [
11. {
12. "@timestamp": {
13. "order": "desc"
14. }
15. }
16. ]
17. }
`AI代码解释
这会返回一个类似这样的文档。
bash
`
1. {
2. "hits" : {
3. "hits" : [
4. {
5. "_source" : {
6. "input" : {
7. "type" : "log"
8. },
9. "agent" : {
10. "hostname" : "rhincodon",
11. "name" : "javalin-app-shipper",
12. "id" : "95705f0c-b472-4bcc-8b01-2d387c0d309b",
13. "type" : "filebeat",
14. "ephemeral_id" : "e4df883f-6073-4a90-a4c4-9e116704f871",
15. "version" : "7.9.0"
16. },
17. "@timestamp" : "2020-07-03T15:11:51.925Z",
18. "ecs" : {
19. "version" : "1.5.0"
20. },
21. "log" : {
22. "file" : {
23. "path" : "/tmp/javalin/app.log"
24. },
25. "offset" : 1440,
26. "level" : "ERROR",
27. "logger" : "de.spinscale.javalin.App"
28. },
29. "host" : {
30. "hostname" : "rhincodon",
31. "os" : {
32. "build" : "19F101",
33. "kernel" : "19.5.0",
34. "name" : "Mac OS X",
35. "family" : "darwin",
36. "version" : "10.15.5",
37. "platform" : "darwin"
38. },
39. "name" : "javalin-app-shipper",
40. "id" : "C28736BF-0EB3-5A04-BE85-C27A62C99316",
41. "architecture" : "x86_64"
42. },
43. "message" : "This is an informative logging message, user agent [curl/7.64.1]"
44. }
45. }
46. ]
47. }
48. }
`AI代码解释
解析异常
日志中的异常比较特殊。
异常通常跨越多行,因此"一行对应一条消息"的规则不再适用。
首先在 App.java 中添加一个能够触发异常的端点,并通过 exception mapper 确保异常被记录。
vbnet
`
1. app.get("/exception", ctx -> {
2. throw new IllegalArgumentException("not yet implemented");
3. });
5. app.exception(Exception.class, (e, ctx) -> {
6. logger.error("Exception found", e);
7. ctx.status(500).result(e.getMessage());
8. });
`AI代码解释
调用 /exception 会向客户端返回 HTTP 500,同时日志中会留下完整的 stack trace。
less
`
1. 2020-07-06T11:27:29,491+02:00 [ERROR] de.spinscale.javalin.App Exception found
2. java.lang.IllegalArgumentException: not yet implemented
3. at de.spinscale.javalin.App.lambda$main$2(App.java:24) ~[classes/:?]
4. at io.javalin.core.security.SecurityUtil.noopAccessManager(SecurityUtil.kt:23) ~[javalin-3.10.1.jar:?]
5. at io.javalin.http.JavalinServlet$addHandler$protectedHandler$1.handle(JavalinServlet.kt:119) ~[javalin-3.10.1.jar:?]
6. at io.javalin.http.JavalinServlet$service$2$1.invoke(JavalinServlet.kt:45) ~[javalin-3.10.1.jar:?]
7. at io.javalin.http.JavalinServlet$service$2$1.invoke(JavalinServlet.kt:24) ~[javalin-3.10.1.jar:?]
9. ... goes on and on and on and own ...
`AI代码解释
Stack trace 的一个重要特征是:每个新行都以空白字符开头,而普通日志消息通常从日期开始。
因此可以在 Beats 配置中加入 multiline 设置:
yaml
`
1. - type: log
2. enabled: true
3. paths:
4. - /tmp/javalin/*.log
5. multiline.pattern: ^20
6. multiline.negate: true
7. multiline.match: after
`AI代码解释
这意味着:凡是不以 20 开头的行,都被视为当前消息的一部分。
这里的 20 对应时间戳中的年份开头。
注意
这种配置会让日志处理产生状态。
现在不能简单地将一个日志文件拆分给多个 processor,因为每一行都可能属于当前事件。
这并不是坏事,但需要意识到这一点。
重新启动 Filebeat 和 Javalin 应用后,再触发一次异常,你就会看到完整的 stack trace 出现在日志的 message 字段中。
配置日志轮转
为了防止日志无限增长,可以在日志配置中加入日志轮转。
示例配置使用 JavalinAppLogRolling appender:
-
每天开始新日志文件时进行轮转。
-
日志文件达到 50 MB 时进行轮转。
-
较旧的日志文件会被 gzip 压缩,以减少磁盘空间占用。
-
50 MB 指的是未压缩文件大小,因此磁盘上最多 20 个文件实际占用的空间会小得多。
Ingest node
内置 module 几乎全部使用 Elasticsearch 的 ingest node 功能,而不是 Beats processors。
ingest pipeline 最有用的功能之一,是可以使用 Simulate Pipeline API 进行调试。
可以在 Kibana 的 Dev Tools 中创建一个与之前 Filebeat processors 类似的 pipeline。
该 pipeline 会:
-
使用
dissect拆分message。 -
使用
trim清理log.level。 -
使用
date解析@timestamp。
然后使用 Simulate Pipeline API 对 pipeline 进行测试。
markdown
`2. # Store the pipeline in Elasticsearch
3. PUT _ingest/pipeline/javalin_pipeline
4. {
5. "processors": [
6. {
7. "dissect": {
8. "field": "message",
9. "pattern": "%{@timestamp} [%{log.level}] %{log.logger} %{message}"
10. }
11. },
12. {
13. "trim": {
14. "field": "log.level"
15. }
16. },
17. {
18. "date": {
19. "field": "@timestamp",
20. "formats": [
21. "ISO8601"
22. ]
23. }
24. }
25. ]
26. }
27. # Test the pipeline
28. POST _ingest/pipeline/javalin_pipeline/_simulate
29. {
30. "docs": [
31. {
32. "_source": {
33. "message": "2020-07-06T13:39:51,737+02:00 [INFO ] de.spinscale.javalin.App This is an informative logging message"
34. }
35. }
36. ]
37. }`AI代码解释
你可以在输出中看到 pipeline 创建的字段,它们与之前通过 Filebeat processors 创建的字段类似。
由于 ingest pipeline 按文档进行处理,因此对于异常仍然需要在日志生成的位置进行处理,并让 Filebeat 将多行异常组合成一条消息。
此外,还可以使用单个 processor 完成日志级别的空白字符清理。
日期解析也非常简单,因为 Elasticsearch 的 ISO8601 解析器可以正确识别秒和毫秒之间的逗号,而不是只能识别点。
接下来修改 Filebeat 配置,只保留 add_host_metadata processor,以便添加主机名称和操作系统等主机信息。
markdown
`
1. processors:
2. - add_host_metadata: ~
`AI代码解释
然后修改 Elasticsearch 输出,使 Filebeat 索引文档时引用这个 pipeline。
yaml
`
1. cloud.id: ${CLOUD_ID}
2. output.elasticsearch:
3. api_key: ${ES_API_KEY}
4. pipeline: javalin_pipeline
`AI代码解释
将日志直接写成 JSON
如果应用直接输出 JSON 日志,整个日志处理流程会简单很多。
重启应用后,你会看到日志文件中直接写入纯 JSON。
当触发异常时,stack trace 也已经位于同一个文档中。
这样 Filebeat 配置就可以变成无状态且更加轻量,并且 Elasticsearch 端的 ingest pipeline 也可以删除。
Filebeat 配置可以简化为直接解析 JSON:
yaml
`
1. filebeat.inputs:
2. - type: log
3. enabled: true
4. paths:
5. - /tmp/javalin/*.log
6. json.keys_under_root: true
8. name: javalin-app-shipper
10. cloud.id: ${CLOUD_ID}
11. output.elasticsearch:
12. api_key: ${ES_API_KEY}
14. processors:
15. - add_host_metadata: ~
`AI代码解释
正如你所看到的,只要直接将日志写成 JSON,整个日志记录和摄取配置就会简单很多。
因此,只要条件允许,尽量直接将日志写成 JSON。
第 5 步:摄取指标
指标可以被理解为某个时间点上的值,并且这个值随时可能发生变化。
当前请求数可能每毫秒发生变化。例如,你可能突然出现 1,000 个请求,然后马上又回到 1 个请求。
这意味着这类指标可能并不总是准确地反映情况,因此还需要获取最小值和最大值。
同时,这也意味着你需要考虑指标的采集周期。
你需要每分钟采集一次,还是每 10 秒采集一次?
为了从另一个角度观察应用,我们接下来摄取一些指标。
本例使用 Metricbeat Prometheus Module 将数据发送到 Elasticsearch。
应用使用的底层库是 Micrometer,这是一个与厂商无关的应用指标 facade,并结合其 Prometheus 支持实现基于 pull 的模型。
也可以使用 Elastic 支持实现基于 push 的模型,不过这样就需要应用保存 Elasticsearch 集群的凭据。
本例将这些凭据保留在外围工具中。
向应用添加指标
-
将 Micrometer 相关依赖添加到
build.gradle。 -
向 Javalin 应用添加 Micrometer plugin 以及相应的 import。
-
添加新的
/metrics端点,并使用 Basic Authentication 对其进行保护。
这里的 MicroMeter 类是自己编写的 MicroMeter.java,它配置了一些指标监控,并创建 Prometheus registry,用于生成基于文本的 Prometheus 输出。
这些指标包括:
-
JVM 编译指标。
-
JVM GC 指标。
-
JVM Heap 压力指标。
-
JVM 内存指标。
-
JVM 线程指标。
-
Log4j2 指标。
-
文件描述符指标。
-
Processor 指标。
-
Uptime 指标。
安装和配置 Metricbeat
根据你的操作系统下载并安装 Metricbeat。
Windows 上:
-
下载 Metricbeat Windows zip 文件。
-
将内容解压到
C:\Program Files。 -
将
metricbeat-[version]-windows-x86_64重命名为Metricbeat。 -
以管理员身份打开 PowerShell。
-
运行安装 Metricbeat 服务的命令。
如果系统禁止脚本执行,需要调整当前会话的执行策略。
与 Filebeat 类似,首先使用管理员用户执行所有 Dashboard 的初始化设置,然后使用 API key。
创建 Metricbeat 对应的 API key,并将 id 和 api_key 保存到 keystore。
接下来配置 Metricbeat 读取 Prometheus 指标。
启用 Prometheus module:
bash
`./metricbeat modules enable prometheus`AI代码解释
然后在 ./modules.d/prometheus.yml 中配置需要轮询的 Prometheus 端点:
yaml
`
1. - module: prometheus
2. period: 10s
3. hosts: ["localhost:7000"]
4. metrics_path: /metrics
5. username: "metrics"
6. password: "secret"
7. use_types: true
8. rate_counters: true
`AI代码解释
为了提高安全性,应该将用户名和密码保存到 keystore,并在配置中引用它们。
最后启动 Metricbeat。
启动后,可以通过以下查询确认 Prometheus 事件是否已经流入 Elasticsearch:
bash
`
1. GET metricbeat-*/_search?filter_path=**.prometheus,hits.total
2. {
3. "query": {
4. "term": {
5. "event.module": "prometheus"
6. }
7. }
8. }
`AI代码解释
第 6 步:在 Kibana 中查看指标
由于这些是来自 Javalin 应用的自定义数据,因此没有预定义的 Dashboard 来显示这些数据。
首先查看每个日志级别产生了多少条日志消息。
可以通过以下查询进行检查:
bash
`
1. GET metricbeat-*/_search
2. {
3. "query": {
4. "exists": {
5. "field": "prometheus.log4j2_events_total.counter"
6. }
7. }
8. }
`AI代码解释
接下来将一段时间内的日志消息数量进行可视化,并按照日志级别进行拆分。
从 Elastic Stack 7.7 开始,可以使用一种新的可视化创建方式:Lens。
-
登录 Kibana,选择 Visualize → Create Visualization。
-
创建折线图,并选择
metricbeat-*作为数据源。
基本思路是:
-
Y 轴使用
prometheus.log4j2_events_total.rate字段的 max 聚合。 -
X 轴使用
@timestamp字段,通过 date_histogram 聚合按照日期进行划分。 -
在每个 date_histogram 分桶内部,再根据日志级别进行拆分。
-
使用
prometheus.labels.level上的 terms 聚合完成日志级别拆分。 -
将日志级别的 size 增加到 6,以显示所有日志级别。
随时间可视化打开的文件
第二个可视化用于检查应用打开的文件数量。
由于没人能记住所有字段名,先重新查看指标输出:
arduino
`curl -s localhost:7000/metrics -u metrics:secret | grep ^process`AI代码解释
其中包括:
markdown
`
1. process_files_max_files 10240.0
2. process_cpu_usage 1.8120711232436825E-4
3. process_uptime_seconds 72903.726
4. process_start_time_seconds 1.594048883317E9
5. process_files_open_files 61.0
`AI代码解释
重点查看 process_files_open_files 指标。
这个值通常比较稳定,很少发生变化。
如果应用在 JVM 中存储数据,或者不断打开和关闭网络 Socket,这个指标会根据负载增加和减少。
对于 Web 应用来说,它通常比较稳定。
接下来可以使用 jps 找到应用进程:
markdown
`
1. $ jps
2. 14224 Jps
3. 82437 Launcher
4. 82438 App
5. 40895
`AI代码解释
然后使用 lsof 查看该进程:
css
`$ lsof -p 82438`AI代码解释
你会看到的不仅仅是打开的文件,因为当前发生的 TCP 连接也会作为文件出现。
接下来添加一个端点,通过创建长时间运行的 HTTP 连接增加打开文件数量。每个连接也会被视为一个打开的文件,因为它需要文件描述符。
然后可以使用 wrk 对该端点产生流量。
Javalin 应用发出的事件计数会随着时间增长。
这是因为有一个组件不断轮询某个端点,而该端点又会产生另一条日志消息。
更陡峭的增长是由于发送了更多请求。
突然出现的下降则来自 JVM 重启。
因为这些指标没有持久化,所以 JVM 重启后会重置。
因此,在这种情况下,通常记录 rate 字段比记录 counter 字段更好。
第 7 步:为应用添加 instrumentation
Observability 的第三个组成部分是 Application Performance Management,也就是 APM。
一个 APM 设置由 APM Server 和负责发送数据的 agent 组成。
APM Server 接收数据,并且已经运行在我们的 Elastic Cloud 环境中。
agent 有两个任务:
-
为 Java 应用添加 instrumentation,以提取应用性能信息。
-
将这些数据发送到 APM Server。
注意
本教程使用 Elastic APM Java agent。
我们建议使用 Elastic OpenTelemetry Java SDK,作为基于 OpenTelemetry 的 Java 应用 instrumentation 的首选方式。
APM 的核心思想之一,是能够跟踪一个用户会话在整个技术栈中的流转。
无论你的系统包含几十个微服务,还是一个处理用户请求的单体应用,都应该能够在整个技术栈中标记同一个请求。
为了完整捕获用户活动,需要从用户浏览器中的 Real User Monitoring(RUM)开始,一直跟踪到应用,再跟踪到应用向数据库发送的 SQL 查询。
数据模型
尽管 APM 生态比较分散,但其中使用的术语通常非常相似。
两个最重要的术语是:
-
Spans
-
Transactions
Transaction 包含一系列 Span,而 Span 包含某段代码执行过程的信息。
例如,在 Kibana Applications UI 中,一个 Spring Boot 应用可能会显示:
UserProfileController.showProfile() 方法被调用,并被标记为 Transaction。
其中包含两个 Span:
-
第一个 Span 是通过 Elasticsearch REST client 向 Elasticsearch 发送请求。
-
第二个 Span 是使用 Thymeleaf 渲染响应。
在这个例子中,对 Elasticsearch 的请求比渲染过程更快。
Java APM agent 可以自动为特定框架添加 instrumentation。
Spring 和 Spring Boot 都得到了很好的支持。
上面的数据只需要将 agent 添加到 Spring Boot 应用即可,不需要额外配置。
目前还存在 Go、.NET、Node、Python、Ruby 以及浏览器(RUM)的 agent,并且新的 agent 还在不断增加。
将 APM agent 添加到代码
有两种方式可以为 Java 应用添加 agent instrumentation。
第一种方式是在调用 java 二进制文件时通过参数添加 agent。
这种方式不会干扰应用打包,并且会在应用启动时完成 instrumentation。
首先下载 agent。
然后在启动应用时指定 agent,以及 APM 数据发送位置等配置。
在 Elastic Cloud 中打开你的部署,点击左侧的 APM,即可看到 APM Server Secret Token。
同时也可以在那里复制 APM endpoint URL。
启动 Java 应用后,就可以打开 Applications UI 查看流入的数据。
自动附加
如果你不希望修改应用的启动参数,可以使用 standalone agent 将 agent 附加到主机上正在运行的 JVM。
首先下载 standalone jar。
列出本地运行的 Java 应用:
css
`java -jar /path/to/apm-agent-attach-1.17.0-standalone.jar --list`AI代码解释
然后指定需要附加的应用:
ini
`
1. java -jar /tmp/apm-agent-attach-1.17.0-standalone.jar --pid 30730 \
2. --config service_name=javalin-app \
3. --config application_packages=de.spinscale.javalin \
4. --config server_urls=$APM_ENDPOINT_URL \
5. --config secret_token=PqWTHGtHZS2i0ZuBol
`AI代码解释
成功后会看到类似:
markdown
`
1. 2020-07-10 15:04:48.144 INFO Attaching the Elastic APM agent to 30730
2. 2020-07-10 15:04:49.649 INFO Done
`AI代码解释
这样 agent 就已经使用指定配置附加到正在运行的应用。
程序化设置
第三种方式是直接将 APM agent 作为依赖使用。
这种方式允许你在应用中编写自定义 Span 和 Transaction。
程序化设置允许你通过源代码中的一行 Java 代码附加 agent。
添加:
arduino
`implementation 'co.elastic.apm:apm-agent-attach:1.17.0'`AI代码解释
然后在 main() 方法最开始调用:
go
`ElasticApmAttacher.attach();`AI代码解释
对于 endpoint 和 API token,建议使用环境变量,而不是直接将 token 写入源代码。
这样可以避免将 API token 提交到源代码仓库。
也可以使用 Vault 等工具管理 Secret。
本地环境中可以使用 direnv。
direnv 是本地 Shell 的一个扩展,当你进入应用所在目录时,它可以自动加载环境变量。
创建 .envrc:
go
`dotenv`AI代码解释
然后创建 .env:
ini
`
1. ELASTIC_APM_SERVICE_NAME=javalin-app
2. ELASTIC_APM_SERVER_URLS=https://APM_ENDPOINT_URL
3. ELASTIC_APM_SECRET_TOKEN=PqWTHGtHZS2i0ZuBol
`AI代码解释
如果你不希望敏感数据保存在 .env 文件中,可以使用 envchain,或者在 .envrc 中执行访问 Vault 等命令。
之后可以像之前一样运行 Java 应用:
bash
`java -jar build/libs/javalin-app-all.jar`AI代码解释
等待几分钟后打开 Applications UI。
最初的 Javalin 应用可能只显示一个 Servlet Transaction,而不会区分不同的 endpoint。
自定义 Transaction
可以通过程序化方式为不同的 HTTP 请求设置 Transaction 名称。
添加相应依赖,然后将 Transaction 名称设置为 HTTP 方法和请求路径:
less
`
1. app.before(ctx -> ElasticApm.currentTransaction()
2. .setName(ctx.method() + " " + ctx.path()));
`AI代码解释
重新启动应用,然后测试几个不同的 endpoint,尤其是会抛出异常以及返回 404 的 endpoint。
现在 Applications UI 中会显示不同的 endpoint。
让 Transaction 监控 HTTP 调用
再添加一个 endpoint,用于轮询另一个 HTTP 服务。
这里使用 wttr.in 获取天气信息,并通过 Apache HTTP client 实现代理 HTTP 方法。
新 endpoint 为:
bash
`/weather/:city`AI代码解释
然后运行:
bash
`curl http://localhost:7000/weather/Munich`AI代码解释
可以看到一行当前天气信息。
在 APM UI 中,可以看到大部分时间都花费在 HTTP client 上,这并不令人意外。
/weather/Munich 的 Transaction 现在包含一个 Span,显示获取天气数据花费了多少时间。
由于 HTTP client 会被自动添加 instrumentation,因此不需要额外操作。
如果 URL 中的 city 参数具有很高的基数,那么会产生大量不同的 URL,而不是一个通用 endpoint。
如果希望避免这种情况,可以使用 ctx.matchedPath(),让天气 API 的每次调用都记录成:
bash
`GET /weather/:city`AI代码解释
不过这需要进行一些重构,将 app.before() Handler 替换为 app.after() Handler。
通过 agent 配置进行方法追踪
除了编写代码追踪方法之外,也可以配置 agent 自动完成。
例如,可以调查日志记录是否是应用中的性能瓶颈,并追踪之前添加的 request logger。
agent 可以根据方法签名追踪方法。
需要监控的接口是:
go
`io.javalin.http.RequestLogger`AI代码解释
其中的方法是:
go
`handle`AI代码解释
因此可以使用:
arduino
`io.javalin.http.RequestLogger#handle`AI代码解释
并将它添加到 .env:
ini
`ELASTIC_APM_TRACE_METHODS="de.spinscale.javalin.Log4j2RequestLogger#handle"`AI代码解释
然后创建对应的 logger 类。
日志关联
APM agent 可以自动将 Transaction ID 添加到日志中。
通过 Filebeat 发送到 Elasticsearch 的日志文件会包含类似这样的内容:
bash
`
1. {
2. "@timestamp": "2020-07-13T12:03:22.491Z",
3. "log.level": "INFO",
4. "message": "GET / 200 0:0:0:0:0:0:0:1 \"curl/7.64.1\" 0",
5. "service.name": "my-javalin-app",
6. "event.dataset": "my-javalin-app.log",
7. "process.thread.name": "qtp34871826-36",
8. "log.logger": "de.spinscale.javalin.Log4j2RequestLogger",
9. "trace.id": "ed735860ec0cd3ee3bdf80ed7ea47afb",
10. "transaction.id": "8af7dff698937dc5"
11. }
`AI代码解释
添加 trace.id 和 transaction.id 后,如果发生错误,还会得到 error.id 字段。
重要
本教程没有介绍 Elastic APM OpenTracing bridge,也没有介绍 agent 提供的其他指标,例如垃圾回收情况或者应用的内存占用情况。
第 8 步:摄取 Uptime 数据
到目前为止,应用已经具备一些基本的监控能力:
-
索引日志,包括 trace。
-
索引指标。
-
使用 APM 查看应用中的单个性能瓶颈。
但是仍然存在一个薄弱点。
之前完成的所有事情都发生在应用内部,而所有用户都是通过互联网访问应用的。
那么,用户的实际体验是否真的和 APM 数据所显示的一样?
例如,假设应用前面存在一个速度缓慢的负载均衡器,每个请求因此额外增加 50 毫秒延迟。
这可能造成非常严重的影响。
或者 TLS 协商本身非常耗时。
虽然这些外部事件并不是你的应用本身造成的,但它们依然会影响用户体验,因此你需要首先知道这些问题的存在,然后才能采取措施进行缓解。
Uptime 不仅可以监控服务是否可用,还可以:
-
随时间绘制延迟。
-
在 TLS 证书即将过期时发送通知。
设置
要将 Uptime 数据发送到 Elasticsearch,需要 Heartbeat,也就是负责轮询的组件。
根据操作系统下载并安装 Heartbeat。
Windows 上:
-
下载 Heartbeat Windows zip 文件。
-
将内容解压到
C:\Program Files。 -
将
heartbeat-[version]-windows-x86_64重命名为Heartbeat。 -
以管理员身份打开 PowerShell。
-
安装 Heartbeat Windows 服务。
如果脚本执行被禁用,需要调整当前会话的执行策略。
下载并解压之后,需要再次设置 Cloud ID 和密码。
首先,以 Elasticsearch 管理员身份在 Kibana 中创建 API key:
bash
`POST /_security/api_key`AI代码解释
API key 应该具有向 heartbeat-* 写入数据所需的最小权限。
然后创建 Heartbeat 配置。
例如:
yaml
`
1. name: heartbeat-shipper
3. cloud.id: ${CLOUD_ID}
4. output.elasticsearch:
5. api_key: ${ES_API_KEY}
7. heartbeat.monitors:
8. - type: http
9. id: javalin-http-app
10. name: "Javalin Web Application"
11. urls: ["http://localhost:7000"]
12. check.response.status: [200]
13. schedule: '@every 15s'
15. - type: http
16. id: httpbin-get
17. name: "httpbin GET"
18. urls: ["https://httpbin.org/get"]
19. check.response.status: [200]
20. schedule: '@every 15s'
22. - type: tcp
23. id: javalin-tcp
24. name: "TCP Port 7000"
25. hosts: ["localhost:7000"]
26. schedule: '@every 15s'
28. processors:
29. - add_observer_metadata:
30. geo:
31. name: europe-munich
32. location: "48.138791, 11.583030"
`AI代码解释
这里配置了三个监控:
-
Javalin Web Application 的 HTTP 检查。
-
httpbin.org的 HTTP 检查。 -
本地 7000 端口的 TCP 检查。
所有检查每 15 秒执行一次。
启动 Heartbeat
启动 Heartbeat,然后等待几分钟,让系统产生一些数据。
MacOS 或 Linux:
bash
`
1. sudo chown root heartbeat.yml
2. sudo ./heartbeat -e
`AI代码解释
Windows:
r
`PS C:\Program Files\heartbeat> Start-Service heartbeat`AI代码解释
Windows 日志默认存储在:
r
`C:\ProgramData\heartbeat\Logs`AI代码解释
注意
如果使用 init.d 脚本启动 Heartbeat,就不能指定命令行参数。
如果需要指定参数,请在前台运行 Heartbeat。
在 Uptime 中查看结果
选择:
Observability → Uptime
即可打开 Uptime 应用。
你可以看到监控列表以及整体概览。

点击 Javalin Web Application,可以查看某个监控的详细信息。
你可以看到最近几次计划检查的执行情况,但每次检查的持续时间可能更加有趣。
例如,你可以发现某个检查的延迟正在不断增加。
顶部的世界地图尤其有用。
你可以在配置中指定检查从哪里发起。
本例中,检查位置被设置为欧洲的 Munich。
如果在世界各地运行多个 Heartbeat,就可以比较不同地区的延迟,并确定应该将应用部署在哪个数据中心,以便更加接近用户。
Javalin Web Application 的监控耗时只有几毫秒,因为它非常快。
再查看 httpbin.org endpoint,你会看到明显更高的耗时,大约每次请求 400 毫秒。
这并不奇怪,因为该 endpoint 距离当前监控位置较远,而且每次请求都需要建立 TLS 连接,这本身就会产生较高成本。
不要低估这种监控的重要性。
同时,这也只是一个开始。
下一步可以使用 synthetics 来监控应用是否真正按照预期工作,例如确保结账流程始终能够正常运行。
接下来做什么?
有关 Elastic Observability 的更多信息,请参考 Observability 文档。
你现在已经完成了一个完整的 Java 应用可观测性示例:
-
使用 Filebeat 摄取日志。
-
在 Kibana 中查看和处理日志。
-
使用 processors 和 ingest pipeline 结构化日志。
-
处理多行异常和日志轮转。
-
使用 JSON 日志简化日志摄取。
-
使用 Metricbeat Prometheus Module 摄取应用指标。
-
在 Kibana 中可视化指标。
-
使用 APM 监控 Java 应用性能。
-
使用 Transaction 和 Span 分析请求。
-
将 trace 与日志关联。
-
使用 Heartbeat 和 Uptime 监控从真实用户角度看到的服务可用性和延迟。
免费开始使用:
-
14 天免费试用。
-
包含所有功能。
-
无需设置。