教程:监控 Java 应用程序
Elastic Stack
在本指南中,您将学习如何使用 Elastic Observability(日志、基础设施指标、APM 和 Uptime)来监控 Java 应用程序。
您将学习如何
- 创建一个示例 Java 应用程序。
- 使用 Filebeat 摄取日志,并在 Kibana 中查看您的日志。
- 使用 Metricbeat Prometheus 模块摄取指标,并在 Kibana 中查看您的指标。
- 使用 Elastic APM Java 代理检测您的应用程序。
- 使用 Heartbeat 监控您的服务,并在 Kibana 中查看您的正常运行时间数据。
创建一个 Elastic Cloud Hosted 部署。该部署包括一个用于存储和搜索数据的 Elasticsearch 集群、一个用于可视化和管理数据的 Kibana 以及一个 APM 服务器。如果您不想按照此处列出的所有步骤操作,并查看最终的 java 代码,请查看 observability-contrib GitHub 仓库中的示例应用程序。
要创建 Java 应用程序,您需要 OpenJDK 14(或更高版本)和 Javalin Web 框架。该应用程序将包括主端点、人为长时间运行的端点以及需要轮询另一个数据源的端点。还将运行一个后台作业。
设置一个 Gradle 项目并创建以下
build.gradle
文件。plugins { id 'java' id 'application' } repositories { jcenter() } dependencies { implementation 'io.javalin:javalin:3.10.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2' } application { mainClassName = 'de.spinscale.javalin.App' } test { useJUnitPlatform() }
运行以下命令。
echo "rootProject.name = 'javalin-app'" >> settings.gradle mkdir -p src/main/java/de/spinscale/javalin mkdir -p src/test/java/de/spinscale/javalin
安装 Gradle 包装器。 安装 Gradle 的一种简单方法是使用 sdkman 并运行
sdk install gradle 6.5.1
。接下来,在当前目录中运行gradle wrapper
以安装 Gradle 包装器。运行
./gradlew clean check
。 您应该看到一个成功的构建,但尚未构建或编译任何内容。要创建 Javalin 服务器及其第一个端点(主端点),请创建
src/main/java/de/spinscale/javalin/App.java
文件。package de.spinscale.javalin; import io.javalin.Javalin; public class App { public static void main(String[] args) { Javalin app = Javalin.create().start(7000); app.get("/", ctx -> ctx.result("Appsolutely perfect")); } }
运行
./gradlew assemble
此命令在
build
目录中编译了App.class
文件。 但是,无法启动服务器。 让我们创建一个包含我们编译的类以及所有必需依赖项的 jar。在
build.gradle
文件中,编辑plugins
,如下所示。plugins { id 'com.github.johnrengelman.shadow' version '6.0.0' id 'application' id 'java' }
运行
./gradlew shadowJar
。 此命令创建一个build/libs/javalin-app-all.jar
文件。shadowJar
插件需要有关其主类的信息。将以下代码段添加到
build.gradle
文件。jar { manifest { attributes 'Main-Class': 'de.spinscale.javalin.App' } }
重新构建项目并启动服务器。
java -jar build/libs/javalin-app-all.jar
打开另一个终端并运行
curl localhost:7000
以显示 HTTP 响应。测试代码。 将所有内容放入
main()
方法中会使测试代码变得困难。 但是,专用的处理程序可以解决此问题。重构
App
类。package de.spinscale.javalin; import io.javalin.Javalin; import io.javalin.http.Handler; public class App { public static void main(String[] args) { Javalin app = Javalin.create().start(7000); app.get("/", mainHandler()); } static Handler mainHandler() { return ctx -> ctx.result("Appsolutely perfect"); } }
将 Mockito 和 Assertj 依赖项添加到
build.gradle
文件。dependencies { implementation 'io.javalin:javalin:3.10.1' testImplementation 'org.mockito:mockito-core:3.5.10' testImplementation 'org.assertj:assertj-core:3.17.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2' }
在
src/test/java/de/spinscale/javalin
中创建一个AppTests.java
类文件。package de.spinscale.javalin; import io.javalin.http.Context; import org.junit.jupiter.api.Test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import static de.spinscale.javalin.App.mainHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; public class AppTests { final HttpServletRequest req = mock(HttpServletRequest.class); final HttpServletResponse res = mock(HttpServletResponse.class); final Context ctx = new Context(req, res, new HashMap<>()); @Test public void testMainHandler() throws Exception { mainHandler().handle(ctx); String response = resultStreamToString(ctx); assertThat(response).isEqualTo("Appsolutely perfect"); } private String resultStreamToString(Context ctx) throws IOException { final byte[] bytes = ctx.resultStream().readAllBytes(); return new String(bytes, StandardCharsets.UTF_8); } }
测试通过后,构建并打包应用程序。
./gradlew clean check shadowJar
日志可以是事件,例如结帐、异常或 HTTP 请求。 对于本教程,让我们使用 log4j2 作为我们的日志记录实现。
将依赖项添加到
build.gradle
文件。dependencies { implementation 'io.javalin:javalin:3.10.1' implementation 'org.apache.logging.log4j:log4j-slf4j18-impl:2.13.3' ... }
要开始记录日志,请编辑
App.java
文件并更改处理程序。注意logger 调用必须在 lambda 中。 否则,日志消息仅在启动期间记录。
package de.spinscale.javalin; import io.javalin.Javalin; import io.javalin.http.Handler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { Javalin app = Javalin.create(); app.get("/", mainHandler()); app.start(7000); } static Handler mainHandler() { return ctx -> { logger.info("This is an informative logging message, user agent [{}]", ctx.userAgent()); ctx.result("Appsolutely perfect"); }; } }
在
src/main/resources/log4j2.xml
文件中创建一个 log4j2 配置。 您可能需要先创建该目录。<?xml version="1.0" encoding="UTF-8"?> <Configuration> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%-5level] %logger{36} %msg%n"/> </Console> </Appenders> <Loggers> <Logger name="de.spinscale.javalin.App" level="INFO"/> <Root level="ERROR"> <AppenderRef ref="Console" /> </Root> </Loggers> </Configuration>
默认情况下,这会在
ERROR
级别上记录。 对于App
类,还有一个额外的配置,以便所有INFO
日志也被记录。 重新打包并重新启动后,日志消息将显示在终端中。17:17:40.019 [INFO ] de.spinscale.javalin.App - This is an informative logging message, user agent [curl/7.64.1]
根据应用程序流量以及它是否发生在应用程序外部,在应用程序级别记录每个请求是有意义的。
在
App.java
文件中,编辑App
类。public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.requestLogger((ctx, executionTimeMs) -> { logger.info("{} {} {} {} \"{}\" {}", ctx.method(), ctx.url(), ctx.req.getRemoteHost(), ctx.res.getStatus(), ctx.userAgent(), executionTimeMs.longValue()); }); }); app.get("/", mainHandler()); app.start(7000); } static Handler mainHandler() { return ctx -> { logger.info("This is an informative logging message, user agent [{}]", ctx.userAgent()); ctx.result("Appsolutely perfect"); }; } }
重新构建并重新启动应用程序。 日志消息将为每个请求记录。
10:43:50.066 [INFO ] de.spinscale.javalin.App - GET / 200 0:0:0:0:0:0:0:1 "curl/7.64.1" 7
在将日志摄取到 Elastic Cloud Hosted 之前,通过编辑 log4j2.xml
文件来创建一个 ISO8601 时间戳。
创建 ISO8601 时间戳消除了在摄取日志时对时间戳进行任何计算的需要,因为这是一个唯一的时间点,包括时区。 一旦您跨数据中心运行并尝试跟踪数据流,拥有时区就变得更加重要。
<PatternLayout pattern="%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n"/>
摄取的日志条目包含如下所示的时间戳。
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
要读取日志记录输出,让我们将数据写入文件和 stdout。 这是一个新的
log4j2.xml
文件。<?xml version="1.0" encoding="UTF-8"?> <Configuration> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%highlight{%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n}"/> </Console> <File name="JavalinAppLog" fileName="/tmp/javalin/app.log"> <PatternLayout pattern="%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n"/> </File> </Appenders> <Loggers> <Logger name="de.spinscale.javalin.App" level="INFO"/> <Root level="ERROR"> <AppenderRef ref="Console" /> <AppenderRef ref="JavalinAppLog" /> </Root> </Loggers> </Configuration>
重新启动应用程序并发送请求。 日志将发送到
/tmp/javalin/app.log
。
要读取日志文件并将其发送到 Elasticsearch,需要 Filebeat。 要下载并安装 Filebeat,请使用适用于您系统的命令
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-9.0.0-amd64.deb
sudo dpkg -i filebeat-9.0.0-amd64.deb
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-9.0.0-x86_64.rpm
sudo rpm -vi filebeat-9.0.0-x86_64.rpm
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-9.0.0-darwin-x86_64.tar.gz
tar xzvf filebeat-9.0.0-darwin-x86_64.tar.gz
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-9.0.0-linux-x86_64.tar.gz
tar xzvf filebeat-9.0.0-linux-x86_64.tar.gz
将 zip 文件的内容提取到
C:\Program Files
中。将
filebeat-[version]-windows-x86_64
目录重命名为Filebeat
。以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择以管理员身份运行)。
从 PowerShell 提示符中,运行以下命令以将 Filebeat 安装为 Windows 服务
PS > cd 'C:\Program Files\Filebeat'
PS C:\Program Files\Filebeat> .\install-service-filebeat.ps1
如果您的系统上禁用了脚本执行,则需要将当前会话的执行策略设置为允许脚本运行。 例如:PowerShell.exe -ExecutionPolicy UnRestricted -File .\install-service-filebeat.ps1
。
使用 Filebeat 密钥库存储 安全设置。 让我们将 Cloud ID 存储在密钥库中。
注意在以下命令中替换部署中的 Cloud ID。 要找到您的 Cloud ID,请单击 https://cloud.elastic.co/deployments 中的部署
./filebeat keystore create echo -n "<Your Cloud ID>" | ./filebeat keystore add CLOUD_ID --stdin
要以最小权限将日志存储在 Elasticsearch 中,请创建一个 API 密钥以将数据从 Filebeat 发送到 Elastic Cloud。
登录 Kibana 用户(您可以从 Cloud Console 执行此操作,而无需键入任何权限),然后选择管理→开发工具。 发送以下请求
POST /_security/api_key
{ "name": "filebeat_javalin-app", "role_descriptors": { "filebeat_writer": { "cluster": ["monitor", "read_ilm"], "index": [ { "names": ["filebeat-*"], "privileges": ["view_index_metadata", "create_doc"] } ] } } }
响应包含一个
api_key
和一个id
字段,可以以下列格式存储在 Filebeat 密钥库中:id:api_key
。echo -n "IhrJJHMB4JmIUAPLuM35:1GbfxhkMT8COBB4JWY3pvQ" | ./filebeat keystore add ES_API_KEY --stdin
注意请确保指定
-n
参数;否则,由于在 API 密钥的末尾添加了换行符,您将遇到痛苦的调试会话。要查看是否已存储这两个设置,请运行
./filebeat keystore list
。要加载 Filebeat 仪表板,请使用
elastic
超级用户。./filebeat setup -e -E 'cloud.id=${CLOUD_ID}' -E 'cloud.auth=elastic:YOUR_SUPER_SECRET_PASS'
提示如果您不想将凭据存储在 shell 的
.history
文件中,请在该行的开头添加一个空格。 根据 shell 配置,这些命令不会添加到历史记录中。配置 Filebeat,使其知道从何处读取数据以及将数据发送到何处。 创建一个
filebeat.yml
文件。name: javalin-app-shipper filebeat.inputs: - type: log paths: - /tmp/javalin/*.log cloud.id: ${CLOUD_ID} output.elasticsearch: api_key: ${ES_API_KEY}
要将数据发送到 Elasticsearch,请启动 Filebeat。
sudo service filebeat start
如果您使用 init.d
脚本启动 Filebeat,则无法指定命令行标志(请参阅 命令参考)。 要指定标志,请在前台启动 Filebeat。
另请参阅 Filebeat 和 systemd。
sudo service filebeat start
如果您使用 init.d
脚本启动 Filebeat,则无法指定命令行标志(请参阅 命令参考)。 要指定标志,请在前台启动 Filebeat。
另请参阅 Filebeat 和 systemd。
./filebeat -e
./filebeat -e
PS C:\Program Files\filebeat> Start-Service filebeat
默认情况下,Windows 日志文件存储在 C:\ProgramData\filebeat\Logs
中。
在日志输出中,您应该看到以下行。
2020-07-03T15:41:56.532+0200 INFO log/harvester.go:297 Harvester started for file: /tmp/javalin/app.log
让我们为应用程序创建一些日志条目。 您可以使用像 wrk 这样的工具并运行以下命令来将请求发送到应用程序。
wrk -t1 -c 100 -d10s http://localhost:7000
此命令导致每秒大约 8,000 个请求,并且还写入了等效数量的日志行。
登录 Kibana 并选择发现应用程序。
顶部有一个文档摘要,但让我们来看一个单独的文档。
您可以看到索引的数据远不止事件本身。这里有关于文件偏移量的信息,关于发送日志的组件的信息,发送方输出中的名称,以及包含日志行内容的
message
字段。您可以看到请求日志记录中存在缺陷。如果 user agent 是
null
,则返回的不是null
的其他内容。阅读我们的日志至关重要;但是,仅仅索引它们对我们没有任何帮助。为了解决这个问题,这里有一个新的请求记录器。Javalin app = Javalin.create(config -> { config.requestLogger((ctx, executionTimeMs) -> { String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-"; logger.info("{} {} {} {} \"{}\" {}", ctx.method(), ctx.req.getPathInfo(), ctx.res.getStatus(), ctx.req.getRemoteHost(), userAgent, executionTimeMs.longValue()); }); });
您可能还想在主处理程序中的日志消息中修复此问题。
static Handler mainHandler() { return ctx -> { String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-"; logger.info("This is an informative logging message, user agent [{}]", userAgent); ctx.result("Appsolutely perfect"); }; }
现在让我们看一下 Kibana 中的 Logs 应用。选择 Observability → Logs。
如果您想查看流式传输功能,请在循环中运行以下 curl 请求,同时休眠。
while $(sleep 0.7) ; do curl localhost:7000 ; done
要查看日志消息的连续流,请单击 Stream live。您还可以突出显示特定术语,如此处所示。
查看正在索引的其中一个文档,您可以看到日志消息包含在单个字段中。通过查看其中一个文档来验证这一点。
GET filebeat-*/_search
{ "size": 1 }
注意事项
- 当您将
@timestamp
字段与日志消息的时间戳进行比较时,您会注意到它们有所不同。这意味着在基于@timestamp
字段进行过滤时,您无法获得预期的结果。当前的@timestamp
字段反映的是 Filebeat 中创建事件的时间戳,而不是应用程序中发生日志事件的时间戳。 - 无法过滤特定字段,例如 HTTP 谓词、HTTP 状态代码、日志级别或生成日志消息的类
- 当您将
要将单个日志行中的更多数据提取到多个字段中,需要对日志进行额外的结构化。
让我们再次看看我们的应用程序生成的日志消息。
2020-07-03T15:45:01,479+02:00 [INFO ] de.spinscale.javalin.App This is an informative logging message
此消息有四个部分:timestamp
、log level
、class
和 message
。拆分规则也很明显,因为它们中的大多数都涉及空格。
好消息是,所有 Beats 都可以通过使用 处理器 在将日志行发送到 Elasticsearch 之前对其进行处理。如果这些处理器的功能不足,您始终可以使用 摄取节点 让 Elasticsearch 完成繁重的工作。这就是 Filebeat 中许多模块所做的事情。Filebeat 中的模块是一种解析特定软件的特定日志文件格式的方法。
让我们尝试使用几个处理器和一个 Filebeat 配置来实现这一点。
processors:
- add_host_metadata: ~
- dissect:
tokenizer: '%{timestamp} [%{log.level}] %{log.logger} %{message_content}'
field: "message"
target_prefix: ""
- timestamp:
field: "timestamp"
layouts:
- '2006-01-02T15:04:05.999Z0700'
test:
- '2020-07-18T04:59:51.123+0200'
- drop_fields:
fields: [ "message", "timestamp" ]
- rename:
fields:
- from: "message_content"
- to: "message"
dissect
处理器将日志消息拆分为四个部分。如果您希望原始消息的最后一部分位于 message
字段中,则需要首先删除旧的 message
字段,然后重命名该字段。剖析过滤器没有就地替换功能。
还有一个专用的时间戳解析,以便 @timestamp
字段包含已解析的值。删除重复的字段,但确保原始消息的一部分仍然在 message
字段中可用。
删除原始消息的部分内容是有争议的。保留原始消息对我来说很有意义。使用上面的示例,如果解析时间戳未按预期工作,则调试可能会出现问题。
时间戳的解析也略有不同,因为 go 时间解析器仅接受点作为秒和毫秒之间的分隔符。 尽管如此,我们的 log4j2 的默认输出使用的是逗号。
可以修复日志输出中的时间戳,使其看起来像 Filebeat 期望的那样。 这会导致以下模式布局。
<PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ} [%-5level] %logger{36} %msg%n"/>
修复时间戳解析是另一种方法,因为您并不总是完全控制您的日志并更改其格式。 想象一下使用某些第三方软件。 目前,这已经足够好了。
更改后重新启动 Filebeat,并通过运行此搜索(并索引另一个日志消息)来查看索引的 JSON 文档中的更改。
GET filebeat-*/_search?filter_path=**._source
{
"size": 1,
"_source": {
"excludes": [
"host.ip",
"host.mac"
]
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
这将返回一个如下所示的文档。
{
"hits" : {
"hits" : [
{
"_source" : {
"input" : {
"type" : "log"
},
"agent" : {
"hostname" : "rhincodon",
"name" : "javalin-app-shipper",
"id" : "95705f0c-b472-4bcc-8b01-2d387c0d309b",
"type" : "filebeat",
"ephemeral_id" : "e4df883f-6073-4a90-a4c4-9e116704f871",
"version" : "7.9.0"
},
"@timestamp" : "2020-07-03T15:11:51.925Z",
"ecs" : {
"version" : "1.5.0"
},
"log" : {
"file" : {
"path" : "/tmp/javalin/app.log"
},
"offset" : 1440,
"level" : "ERROR",
"logger" : "de.spinscale.javalin.App"
},
"host" : {
"hostname" : "rhincodon",
"os" : {
"build" : "19F101",
"kernel" : "19.5.0",
"name" : "Mac OS X",
"family" : "darwin",
"version" : "10.15.5",
"platform" : "darwin"
},
"name" : "javalin-app-shipper",
"id" : "C28736BF-0EB3-5A04-BE85-C27A62C99316",
"architecture" : "x86_64"
},
"message" : "This is an informative logging message, user agent [curl/7.64.1]"
}
}
]
}
}
您可以看到 message
字段仅包含我们日志消息的最后一部分。 此外,还有一个 log.level
和 log.logger
字段。
当日志级别为 INFO
时,它会在末尾记录额外的空格。 您可以使用 脚本处理器 并调用 trim()
。 但是,修复我们的日志记录配置以始终不发出 5 个字符可能更容易,而不管日志级别长度如何。 写入标准输出时,您仍然可以保留此设置。
<File name="JavalinAppLog" fileName="/tmp/javalin/app.log">
<PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ} [%level] %logger{36} %msg%n"/>
</File>
在日志记录的情况下,异常是一种特殊的待遇。 它们跨越多行,因此每个消息一行的旧规则在异常中不存在。
在 App.java
中添加一个首先触发异常的端点,并确保使用异常映射器记录该异常。
app.get("/exception", ctx -> {
throw new IllegalArgumentException("not yet implemented");
});
app.exception(Exception.class, (e, ctx) -> {
logger.error("Exception found", e);
ctx.status(500).result(e.getMessage());
});
调用 /exception
会向客户端返回 HTTP 500 错误,但会在日志中留下如下所示的堆栈跟踪。
2020-07-06T11:27:29,491+02:00 [ERROR] de.spinscale.javalin.App Exception found
java.lang.IllegalArgumentException: not yet implemented
at de.spinscale.javalin.App.lambda$main$2(App.java:24) ~[classes/:?]
at io.javalin.core.security.SecurityUtil.noopAccessManager(SecurityUtil.kt:23) ~[javalin-3.10.1.jar:?]
at io.javalin.http.JavalinServlet$addHandler$protectedHandler$1.handle(JavalinServlet.kt:119) ~[javalin-3.10.1.jar:?]
at io.javalin.http.JavalinServlet$service$2$1.invoke(JavalinServlet.kt:45) ~[javalin-3.10.1.jar:?]
at io.javalin.http.JavalinServlet$service$2$1.invoke(JavalinServlet.kt:24) ~[javalin-3.10.1.jar:?]
... goes on and on and on and own ...
有一个属性有助于解析此堆栈跟踪。 与常规日志消息相比,它似乎有所不同。 每个新行都以空格开头,因此与以日期开头的日志消息不同。 让我们将此逻辑添加到我们的 Beats 配置中。
- type: log
enabled: true
paths:
- /tmp/javalin/*.log
multiline.pattern: ^20
multiline.negate: true
multiline.match: after
因此,上述设置的字面翻译是,将所有内容都视为现有消息的一部分,该消息不是以 20
开头的行。 20
类似于您的时间戳的起始年份。 一些用户更喜欢将日期包装在 []
中,以便更容易理解。
这会将状态引入到您的日志记录中。 您现在无法在多个处理器之间拆分日志文件,因为每个日志行仍然可能属于当前事件。 这不是一件坏事,但同样需要注意。
在重新启动 Filebeat 和您的 Javalin 应用程序后,触发一个异常,您将在日志的 message
字段中看到一个长堆栈跟踪。
为确保日志不会无限增长,让我们向您的日志记录配置添加一些日志轮换。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%highlight{%d{ISO8601_OFFSET_DATE_TIME_HHCMM} [%-5level] %logger{36} %msg%n}"/>
</Console>
<RollingFile name="JavalinAppLogRolling" fileName="/tmp/javalin/app.log" filePattern="/tmp/javalin/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ} [%level] %logger{36} %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="de.spinscale.javalin.App" level="INFO"/>
<Root level="ERROR">
<AppenderRef ref="Console" />
<AppenderRef ref="JavalinAppLogRolling" />
</Root>
</Loggers>
</Configuration>
该示例向我们的配置添加了一个 JavalinAppLogRolling
appender,它使用与以前相同的日志记录模式,但如果新的一天开始或日志文件已达到 50 兆字节,则会进行轮换。
如果创建了一个新的日志文件,则旧的日志文件也会被 gzip 压缩,以减少磁盘空间占用。 50 兆字节的大小指的是未压缩的文件大小,因此磁盘上潜在的二十个文件中的每个文件都会小得多。
内置模块几乎完全使用 Elasticsearch 的 摄取节点 功能,而不是 Beats 处理器。
摄取管道最有用的部分之一是能够使用 模拟管道 API 进行调试。
让我们编写一个类似于我们的 Filebeat 处理器的管道,使用 Kibana 中的 Dev Tools 面板,运行以下命令
# Store the pipeline in Elasticsearch
PUT _ingest/pipeline/javalin_pipeline { "processors": [ { "dissect": { "field": "message", "pattern": "%{@timestamp} [%{log.level}] %{log.logger} %{message}" } }, { "trim": { "field": "log.level" } }, { "date": { "field": "@timestamp", "formats": [ "ISO8601" ] } } ] } # Test the pipeline POST _ingest/pipeline/javalin_pipeline/_simulate { "docs": [ { "_source": { "message": "2020-07-06T13:39:51,737+02:00 [INFO ] de.spinscale.javalin.App This is an informative logging message" } } ] }
您可以在输出中看到管道创建的字段,现在看起来像早期的 Filebeat 处理器。 由于摄取管道在文档级别工作,您仍然需要检查生成日志的位置是否存在异常,并让 Filebeat 从中创建单个消息。 您甚至可以使用单个处理器实现日志级别修剪,并且日期解析也非常容易,因为 Elasticsearch ISO8601 解析器在拆分秒和毫秒时正确识别逗号而不是点。
现在,转到 Filebeat 配置。 首先,让我们删除除 add_host_metadata 处理器 之外的所有处理器,以添加一些主机信息,例如主机名和操作系统。
processors: - add_host_metadata: ~
编辑 Elasticsearch 输出,以确保从 Filebeat 索引文档时将引用该管道。
cloud.id: ${CLOUD_ID} output.elasticsearch: api_key: ${ES_API_KEY} pipeline: javalin_pipeline
重新启动 Filebeat,看看日志是否按预期流入。
您现在已经了解了在 Beats 或 Elasticsearch 中解析日志。 如果我们不需要考虑解析我们的日志并手动提取数据怎么办?
将日志写出为纯文本可以工作,并且易于人类阅读。 但是,首先将它们写出为纯文本,然后使用 dissect
处理器解析它们,然后再次创建 JSON 听起来很乏味并且浪费了不必要的 CPU 周期。
虽然 log4j2 具有 JSONLayout,但您可以更进一步并使用一个名为 ecs-logging-java 的库。 ECS 日志记录的优势在于它使用 Elastic Common Schema。 ECS 定义了一组标准字段,用于在 Elasticsearch 中存储事件数据,例如日志和指标。
与其编写我们的日志记录标准,不如使用现有的标准。 让我们将日志记录依赖项添加到我们的 Javalin 应用程序中。
dependencies { implementation 'io.javalin:javalin:3.10.1' implementation 'org.apache.logging.log4j:log4j-slf4j18-impl:2.13.3' implementation 'co.elastic.logging:log4j2-ecs-layout:0.5.0' testImplementation 'org.mockito:mockito-core:3.5.10' testImplementation 'org.assertj:assertj-core:3.17.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2' } // this is needed to ensure JSON logging works as expected when building // a shadow jar shadowJar { transform(com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer) }
log4j2-ecs-layout
附带一个自定义的<EcsLayout>
,它可以在滚动文件 appender 的日志记录设置中使用<RollingFile name="JavalinAppLogRolling" fileName="/tmp/javalin/app.log" filePattern="/tmp/javalin/%d{yyyy-MM-dd}-%i.log.gz"> <EcsLayout serviceName="my-javalin-app"/> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="50 MB"/> </Policies> <DefaultRolloverStrategy max="20"/> </RollingFile>
当您重新启动应用程序时,您将看到纯 JSON 写入您的日志文件。 当您触发异常时,您将看到堆栈跟踪已在您的单个文档中。 这意味着 Filebeat 配置可以变为无状态,甚至更轻量级。 此外,可以再次删除 Elasticsearch 端的摄取管道。
您可以为
EcsLayout
配置更多参数,但默认值已得到明智的选择。 让我们修复 Filebeat 配置并删除多行设置以及管道filebeat.inputs: - type: log enabled: true paths: - /tmp/javalin/*.log json.keys_under_root: true name: javalin-app-shipper cloud.id: ${CLOUD_ID} output.elasticsearch: api_key: ${ES_API_KEY} # ================================= Processors ================================= processors: - add_host_metadata: ~
正如您所看到的,仅仅通过将日志写出为 JSON,我们的整个日志记录设置就变得容易了很多,因此在可能的情况下,请尝试直接将您的日志写出为 JSON。
指标被认为是随时可能变化的时间点值。 当前请求的数量可能在任何毫秒内发生变化。 您可能会出现 1000 个请求的峰值,然后一切恢复为一个请求。 这也意味着这些类型的指标可能不准确,并且您还需要提取最小值/最大值以获得更多指示。 此外,这意味着您还需要考虑这些指标的持续时间。 您需要每分钟一次还是每 10 秒一次?
为了获得应用程序的不同角度视图,让我们摄取一些指标。 在此示例中,我们将使用 Metricbeat Prometheus 模块 将数据发送到 Elasticsearch。
我们的应用程序中使用的底层库是 micrometer.io,这是一个与供应商无关的应用程序指标外观,结合其 Prometheus 支持 来实现基于拉取的模型。 您可以使用 elastic 支持 来实现基于推送的模型。 这将要求用户在我们的应用程序中存储 Elasticsearch 集群的凭据数据。 此示例将此数据保存在周围的工具中。
将依赖项添加到我们的
build.gradle
文件中。// metrics via micrometer implementation 'io.micrometer:micrometer-core:1.5.4' implementation 'io.micrometer:micrometer-registry-prometheus:1.5.4' implementation 'org.apache.commons:commons-lang3:3.11'
将 micrometer 插件及其相应的导入添加到我们的 Javalin 应用程序中。
... import io.javalin.plugin.metrics.MicrometerPlugin; import io.javalin.core.security.BasicAuthCredentials; ... Javalin app = Javalin.create(config -> { ... config.registerPlugin(new MicrometerPlugin()); );
添加一个新的指标端点,并确保也导入了
BasicAuthCredentials
类。final Micrometer micrometer = new Micrometer(); app.get("/metrics", ctx -> { ctx.status(404); if (ctx.basicAuthCredentialsExist()) { final BasicAuthCredentials credentials = ctx.basicAuthCredentials(); if ("metrics".equals(credentials.getUsername()) && "secret".equals(credentials.getPassword())) { ctx.status(200).result(micrometer.scrape()); } } });
在这里,
MicroMeter
类是一个自定义的类,名为MicroMeter.java
,它设置了一些指标监视器,并为 Prometheus 创建了注册表,Prometheus 提供基于文本的 Prometheus 输出。package de.spinscale.javalin; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics; import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; import io.micrometer.core.instrument.binder.logging.Log4j2Metrics; import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; import io.micrometer.core.instrument.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.binder.system.UptimeMetrics; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; public class Micrometer { final PrometheusMeterRegistry registry = new PrometheusMeterRegistry(new PrometheusConfig() { @Override public String get(String key) { return null; } @Override public String prefix() { return "javalin"; } }); public Micrometer() { Metrics.addRegistry(registry); new JvmGcMetrics().bindTo(Metrics.globalRegistry); new JvmHeapPressureMetrics().bindTo(Metrics.globalRegistry); new JvmThreadMetrics().bindTo(Metrics.globalRegistry); new JvmCompilationMetrics().bindTo(Metrics.globalRegistry); new JvmMemoryMetrics().bindTo(Metrics.globalRegistry); new Log4j2Metrics().bindTo(Metrics.globalRegistry); new UptimeMetrics().bindTo(Metrics.globalRegistry); new FileDescriptorMetrics().bindTo(Metrics.globalRegistry); new ProcessorMetrics().bindTo(Metrics.globalRegistry); } public String scrape() { return registry.scrape(); } }
重新构建你的应用程序并轮询指标端点。
curl localhost:7000/metrics -u metrics:secret
这将返回一个基于行的响应,每行一个指标。这是标准的 Prometheus 格式。
要将指标发送到 Elasticsearch,需要 Metricbeat。要下载和安装 Metricbeat,请使用与你的系统配合使用的命令。
curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-9.0.0-amd64.deb
sudo dpkg -i metricbeat-9.0.0-amd64.deb
curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-9.0.0-x86_64.rpm
sudo rpm -vi metricbeat-9.0.0-x86_64.rpm
curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-9.0.0-darwin-x86_64.tar.gz
tar xzvf metricbeat-9.0.0-darwin-x86_64.tar.gz
curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-9.0.0-linux-x86_64.tar.gz
tar xzvf metricbeat-9.0.0-linux-x86_64.tar.gz
将 zip 文件的内容提取到
C:\Program Files
中。将
metricbeat-[version]-windows-x86_64
目录重命名为Metricbeat
。以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择以管理员身份运行)。
从 PowerShell 提示符下,运行以下命令以将 Metricbeat 安装为 Windows 服务。
PS > cd 'C:\Program Files\Metricbeat'
PS C:\Program Files\Metricbeat> .\install-service-metricbeat.ps1
如果你的系统上禁用了脚本执行,你需要设置当前会话的执行策略以允许脚本运行。例如:PowerShell.exe -ExecutionPolicy UnRestricted -File .\install-service-metricbeat.ps1
。
与 Filebeat 设置类似,使用管理员用户运行所有仪表板的初始设置,然后使用 API 密钥。
POST /_security/api_key
{ "name": "metricbeat_javalin-app", "role_descriptors": { "metricbeat_writer": { "cluster": ["monitor", "read_ilm"], "index": [ { "names": ["metricbeat-*"], "privileges": ["view_index_metadata", "create_doc"] } ] } } }
将
id
和api_key
字段的组合存储在密钥库中。./metricbeat keystore create echo -n "IhrJJHMB4JmIUAPLuM35:1GbfxhkMT8COBB4JWY3pvQ" | ./metricbeat keystore add ES_API_KEY --stdin echo -n "observability-javalin-app:ZXUtY2VudHJhbC0xLmF3cy5jbG91ZC5lcy5pbyQ4NDU5M2I1YmQzYTY0N2NhYjA2MWQ3NTJhZWFhNWEzYyQzYmQwMWE2OTQ2MmQ0N2ExYjdhYTkwMzI0YjJiOTMyYQ==" | ./metricbeat keystore add CLOUD_ID --stdin
不要忘记像这样进行初始设置。
./metricbeat setup -e -E 'cloud.id=${CLOUD_ID}' -E 'cloud.auth=elastic:YOUR_SUPER_SECRET_PASS'
配置 Metricbeat 以读取我们的 Prometheus 指标。从一个基本的
metricbeat.yaml
开始。metricbeat.config.modules: path: ${path.config}/modules.d/*.yml reload.enabled: false name: javalin-metrics-shipper cloud.id: ${CLOUD_ID} output.elasticsearch: api_key: ${ES_API_KEY} processors: - add_host_metadata: ~ - add_cloud_metadata: ~ - add_docker_metadata: ~ - add_kubernetes_metadata: ~
由于 Metricbeat 支持数十个模块,这些模块又是摄取指标的不同方式(Filebeat 也是如此,具有不同类型的日志文件和格式),因此需要启用 Prometheus 模块。
./metricbeat modules enable prometheus
将要轮询的 Prometheus 端点添加到
./modules.d/prometheus.yml
中。- module: prometheus period: 10s hosts: ["localhost:7000"] metrics_path: /metrics username: "metrics" password: "secret" use_types: true rate_counters: true
为了提高安全性,你应该将用户名和密码添加到密钥库中,并在配置中引用它们。
启动 Metricbeat。
sudo service metricbeat start
如果你使用 init.d
脚本来启动 Metricbeat,则无法指定命令行标志(请参阅 命令参考)。要指定标志,请在前台启动 Metricbeat。
另请参阅 Metricbeat 和 systemd。
sudo service metricbeat start
如果你使用 init.d
脚本来启动 Metricbeat,则无法指定命令行标志(请参阅 命令参考)。要指定标志,请在前台启动 Metricbeat。
另请参阅 Metricbeat 和 systemd。
sudo chown root metricbeat.yml
sudo ./metricbeat -e
- 你将以 root 身份运行 Metricbeat,因此你需要更改配置文件的所有权,或者使用指定的
--strict.perms=false
运行 Metricbeat。请参阅 配置文件所有权和权限。
sudo chown root metricbeat.yml
sudo ./metricbeat -e
- 你将以 root 身份运行 Metricbeat,因此你需要更改配置文件的所有权,或者使用指定的
--strict.perms=false
运行 Metricbeat。请参阅 配置文件所有权和权限。
PS C:\Program Files\metricbeat> Start-Service metricbeat
默认情况下,Windows 日志文件存储在 C:\ProgramData\metricbeat\Logs
中。
在 Windows 上,当前未捕获有关系统负载和交换使用情况的统计信息。
验证 Prometheus 事件是否正在流入 Elasticsearch。
GET metricbeat-*/_search?filter_path=**.prometheus,hits.total
{
"query": {
"term": {
"event.module": "prometheus"
}
}
}
由于这是来自我们的 Javalin 应用程序的自定义数据,因此没有用于显示此数据的预定义仪表板。
让我们检查每个日志级别的日志消息数量。
GET metricbeat-*/_search
{
"query": {
"exists": {
"field": "prometheus.log4j2_events_total.counter"
}
}
}
可视化随时间变化的日志消息数量,并按日志级别拆分。自 Elastic Stack 7.7 以来,有一种创建可视化的新方法,称为 Lens
。
登录到 Kibana 并选择 Visualize → Create Visualization。
创建一个折线图,并选择
metricbeat-*
作为来源。基本思想是在
prometheus.log4j2_events_total.rate
字段上的 y 轴上有一个 最大聚合,而 x 轴使用@timestamp
字段上的 date_histogram 聚合按日期拆分。每个日期直方图桶中还有另一个拆分,按日志级别拆分,使用
prometheus.labels.level
上的 术语聚合,其中包含日志级别。此外,将日志级别的大小增加到六个以显示每个日志级别。最终结果如下所示。
第二个可视化是检查我们的应用程序中打开的文件数。
由于没有人能记住所有字段名称,让我们再次查看指标输出。
curl -s localhost:7000/metrics -u metrics:secret | grep ^process
process_files_max_files 10240.0
process_cpu_usage 1.8120711232436825E-4
process_uptime_seconds 72903.726
process_start_time_seconds 1.594048883317E9
process_files_open_files 61.0
让我们看看 process_files_open_files
指标。这应该是一个相当静态的值,很少改变。如果你运行一个在 JVM 中存储数据或打开和关闭网络套接字的应用程序,则此指标会根据负载而增加和减少。对于 Web 应用程序,这是相当静态的。让我们弄清楚为什么我们的小型 Web 应用程序上打开了 60 个文件。
运行
jps
,它将在进程列表中包含你的应用程序。$ jps 14224 Jps 82437 Launcher 82438 App 40895
在该进程上使用
lsof
。$ lsof -p 82438
你将看到比所有正在打开的文件更多的输出,因为文件也是现在正在发生的 TCP 连接。
通过建立长时间运行的 HTTP 连接(每个连接也被视为一个打开的文件,因为它需要一个文件描述符),添加一个端点以增加打开文件的数量,然后针对它运行
wrk
。... import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; ... public static void main(String[] args) { ... final Executor executor = CompletableFuture.delayedExecutor(20, TimeUnit.SECONDS); app.get("/wait", ctx -> { CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "done", executor); ctx.result(future); }); ...
每个 future 延迟 20 秒,这意味着单个 HTTP 请求保持打开状态 20 秒。
让我们运行一个
wrk
工作负载。wrk -c 100 -t 20 -d 5m http://localhost:7000/wait
结果表明只发送了二十个请求,这在给定处理时间的情况下是有道理的。
现在,让我们使用 Kibana 中的 Lens 构建一个可视化。
在
Add filter
下,选择metricbeat-*
索引模式。 这可能会使用filebeat-*
作为默认值。x 轴使用
@timestamp
字段 - 这又将创建一个date_histogram
聚合。 y 轴不应是文档计数,因为它将始终保持稳定,而是存储桶中文档的最大值。 单击 y 轴上字段名称的右侧,然后选择Max
。 这将为你提供与所示类似的视觉效果,在你运行上述wrk
命令的位置处有一个峰值。现在让我们看看 Kibana 中的 Infrastructure 应用程序。 选择 Observability → Infrastructure。
你只会看到来自单个 shipper 的数据。 尽管如此,当你运行多个服务时,按 Kubernetes pod 或主机对此进行分组的能力使你能够发现 CPU 或内存消耗量增加的主机。
单击 Metrics Explorer,你可以开始浏览特定主机的数据或节点上的 CPU 使用率。
这是 Javalin 应用程序发出的总事件计数器的面积图。 它在上升,因为有一个组件轮询一个端点,这又会产生另一个日志消息。 更陡峭的窥视是由于发送了更多请求。 但是突然下降来自哪里? JVM 重新启动。 由于这些指标未持久保存,因此会在 JVM 重新启动时重置它们。 考虑到这一点,通常最好记录
rate
而不是counter
字段。
可观察性的第三个组成部分是应用程序性能管理 (APM)。 APM 设置包括一个 APM 服务器,该服务器接受数据(并且已在我们的 Elastic Cloud 设置中运行),以及一个将数据传递到服务器的代理。
代理有两个任务:检测 Java 应用程序以提取应用程序性能信息,并将该数据发送到 APM 服务器。
APM 的核心思想之一是能够跟踪用户会话在整个堆栈中的流程,无论你有数十个微服务还是一个整体式服务来响应你的用户请求。 这意味着能够标记整个堆栈中的请求。
要完全捕获用户活动,你需要从用户浏览器中使用 Real User Monitoring (RUM) 到你的应用程序开始,该应用程序向你的数据库发送 SQL 查询。
尽管 APM 格局严重分散,但术语通常相似。 两个最重要的术语是 Spans 和 Transactions。
事务封装了一系列 span,其中包含有关代码片段执行的信息。 让我们看一下 Kibana Applications UI 中的此屏幕截图。
这是一个 Spring Boot 应用程序。 调用了 UserProfileController.showProfile()
方法,该方法标记为事务。 其中包含两个 span。 首先,使用 Elasticsearch REST 客户端将请求发送到 Elasticsearch,并在使用 Thymeleaf 呈现响应后。 在这种情况下,对 Elasticsearch 的请求比渲染快。
Java APM 代理可以自动检测特定框架。 Spring 和 Spring Boot 得到了很好的支持,以上数据是通过将代理添加到 Spring Boot 应用程序创建的; 不需要任何配置。
目前有适用于 Go、.NET、Node、Python、Ruby 和浏览器 (RUM) 的代理。 代理不断被添加,因此你可能需要查看 APM 代理文档。
你有两个选项可以将 Java 代理检测添加到你的应用程序。
首先,你可以在调用 java
二进制文件时通过参数添加代理。 这样,它不会干扰应用程序的打包。 此机制在启动时检测应用程序。
首先,下载代理,你可以查看 最新版本。
wget https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/1.17.0/elastic-apm-agent-1.17.0.jar
在启动时指定代理以及将 APM 数据发送到何处的配置参数。 在启动 Java 应用程序之前,让我们为在 Elastic Cloud 中运行的 APM 服务器获取一个 API 密钥。
当你在 Elastic Cloud 中检查你的部署并在左侧单击 APM
时,你将看到 APM Server Secret Token
,你可以使用它。 你也可以从那里复制 APM 端点 URL。
java -javaagent:/path/to/elastic-apm-agent-1.17.0.jar\
-Delastic.apm.service_name=javalin-app \
-Delastic.apm.application_packages=de.spinscale.javalin \
-Delastic.apm.server_urls=$APM_ENDPOINT_URL \
-Delastic.apm.secret_token=PqWTHGtHZS2i0ZuBol \
-jar build/libs/javalin-app-all.jar
你现在可以继续打开 Applications UI,你应该会看到数据流入。
如果你不想更改应用程序的启动选项,则独立代理允许你连接到主机上运行的 JVM。
这需要你下载独立的 jar。 你可以在 官方文档上找到链接。
要列出你本地运行的 Java 应用程序,你可以运行
java -jar /path/to/apm-agent-attach-1.17.0-standalone.jar --list
由于我通常在我的系统上运行多个 Java 应用程序,因此我指定要连接的应用程序。 此外,请确保你已停止已附加代理的 Javalin 应用程序,并启动一个未配置要附加代理的常规 Javalin 应用程序。
java -jar /tmp/apm-agent-attach-1.17.0-standalone.jar --pid 30730 \
--config service_name=javalin-app \
--config application_packages=de.spinscale.javalin \
--config server_urls=$APM_ENDPOINT_URL \
--config secret_token=PqWTHGtHZS2i0ZuBol
以上消息将返回类似以下内容
2020-07-10 15:04:48.144 INFO Attaching the Elastic APM agent to 30730
2020-07-10 15:04:49.649 INFO Done
因此,现在代理已附加到具有特殊配置的正在运行的应用程序。
虽然前两种方式都可以,但你也可以使用第三种:将 APM Agent 作为直接依赖项。这允许你在应用程序中编写自定义 Span 和 Transaction。
通过编程设置,你可以通过源代码中的一行 Java 代码附加 Agent。
添加 Java Agent 依赖项。
dependencies { ... implementation 'co.elastic.apm:apm-agent-attach:1.17.0' ... }
在我们的
main()
方法中,从一开始就检测应用程序。import co.elastic.apm.attach.ElasticApmAttacher; ... public static void main(String[] args) { ElasticApmAttacher.attach(); ... }
我们尚未配置任何 Endpoint 或 API Token。虽然 文档 建议使用
src/main/resources/elasticapm.properties
文件,但我更喜欢使用环境变量,因为这可以防止将 API Token 提交到源代码或合并另一个存储库。诸如 Vault 之类的机制允许你以这种方式管理你的密钥。对于我们的本地部署,我通常使用类似 direnv 的工具进行本地设置。
direnv
是你本地 Shell 的一个扩展,当你进入目录时,它会加载/卸载环境变量,就像你的应用程序一样。direnv
可以做更多的事情,例如加载正确的 node/ruby 版本或将目录添加到你的 $PATH 变量。要启用
direnv
,你需要创建一个包含以下内容的.envrc
文件。dotenv
这告诉
direnv
将.env
文件的内容作为环境变量加载。.env
文件应如下所示:ELASTIC_APM_SERVICE_NAME=javalin-app ELASTIC_APM_SERVER_URLS=https://APM_ENDPOINT_URL ELASTIC_APM_SECRET_TOKEN=PqWTHGtHZS2i0ZuBol
如果你不希望将敏感数据放入
.env
文件中,你可以使用像 envchain 这样的工具,或者在.envrc
文件中调用任意命令,例如访问 Vault。你现在可以像以前一样运行 Java 应用程序。
java -jar build/libs/javalin-app-all.jar
如果你想在你的 IDE 中运行它,你可以手动设置环境变量,或者搜索一个支持
.env
文件的插件。等待几分钟,让我们最后看一下 Applications UI。
正如你所看到的,这与之前显示的 Spring Boot 应用程序有很大的不同。不同的 Endpoint 没有列出;我们可以看到每分钟的请求数,包括错误。
唯一的 Transaction 来自于单个 Servlet,这没什么帮助。让我们尝试通过引入自定义编程 Transaction 来解决这个问题。
添加另一个依赖项。
dependencies { ... implementation 'co.elastic.apm:apm-agent-attach:1.17.0' implementation 'co.elastic.apm:apm-agent-api:1.17.0' ... }
修复 Transaction 的名称,使其包含 HTTP 方法和请求路径。
app.before(ctx -> ElasticApm.currentTransaction() .setName(ctx.method() + " " + ctx.path()));
重新启动你的应用程序,看看数据流入。测试一些不同的 Endpoint,尤其是那些抛出异常和触发 404 的 Endpoint。
这看起来好多了,Endpoint 之间存在差异。
添加另一个 Endpoint 以查看 Transaction 的强大功能,该 Endpoint 轮询另一个 HTTP 服务。你可能听说过 wttr.in,一个用于轮询天气信息的服务。让我们实现一个代理 HTTP 方法,将请求转发到该 Endpoint。让我们使用 Apache HTTP client,这是最典型的 HTTP 客户端之一。
implementation 'org.apache.httpcomponents:fluent-hc:4.5.12'
这是我们的新 Endpoint。
import org.apache.http.client.fluent.Request; ... public static void main(String[] args) { ... app.get("/weather/:city", ctx -> { String city = ctx.pathParam("city"); ctx.result(Request.Get("https://wttr.in/" + city + "?format=3").execute() .returnContent().asBytes()) .contentType("text/plain; charset=utf-8"); }); ...
Curl
http://localhost:7000/weather/Munich
,查看关于当前天气的单行响应。让我们检查 APM UI。在概览中,你可以看到大部分时间都花在了 HTTP 客户端上,这并不奇怪。
我们对
/weather/Munich
的 Transaction 现在包含一个 Span,显示了检索天气数据花费的时间。因为 HTTP 客户端是自动检测的,所以不需要做任何事情。如果该 URL 中的
city
参数具有很高的基数,这将导致提到大量的 URL,而不是通用的 Endpoint。 如果你想避免这种情况,一种可能性是使用ctx.matchedPath()
将对 Weather API 的每次调用记录为GET /weather/:city
。 但是,这需要通过删除app.before()
处理程序并将其替换为app.after()
处理程序来进行一些重构。app.after(ctx -> ElasticApm.currentTransaction().setName(ctx.method() + " " + ctx.endpointHandlerPath()));
除了编写代码来跟踪方法,你也可以配置 Agent 来做到这一点。 让我们试着弄清楚日志记录是否是我们应用程序的瓶颈,并跟踪我们之前添加的请求记录器语句。
Agent 可以根据签名 跟踪方法。
要监控的接口是 io.javalin.http.RequestLogger
接口及其 handle
方法。 所以,让我们尝试 io.javalin.http.RequestLogger#handle
来识别要记录的方法,并将其放入你的 .env
中。
ELASTIC_APM_TRACE_METHODS="de.spinscale.javalin.Log4j2RequestLogger#handle"
创建一个专用的 Logger 类,以匹配上述跟踪方法。
package de.spinscale.javalin; import io.javalin.http.Context; import io.javalin.http.RequestLogger; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Log4j2RequestLogger implements RequestLogger { private final Logger logger = LoggerFactory.getLogger(Log4j2RequestLogger.class); @Override public void handle(@NotNull Context ctx, @NotNull Float executionTimeMs) throws Exception { String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-"; logger.info("{} {} {} {} \"{}\" {}", ctx.method(), ctx.req.getPathInfo(), ctx.res.getStatus(), ctx.req.getRemoteHost(), userAgent, executionTimeMs.longValue()); } }
修复
App
类中的调用。config.requestLogger(new Log4j2RequestLogger());
重新启动你的应用程序,看看你的日志记录花费了多少时间。
请求记录器大约需要 400 微秒。 整个请求大约需要 1.3 毫秒。 我们请求处理的大约三分之一用于日志记录。
如果你正在寻求更快的服务,你可能需要重新考虑日志记录。 但是,此日志记录发生在结果写入客户端之后,因此虽然总处理时间随着日志记录而增加,但响应客户端不会(关闭连接可能会)。 另请注意,这些测试是在没有适当预热的情况下进行的。 我假设在适当的 JVM 预热之后,你将更快地处理请求。
一旦你有一个比我们的示例应用程序更大的应用程序,并且有更多的代码路径,你可以尝试通过设置以下内容来启用 推断 Span 的自动分析。
ELASTIC_APM_PROFILING_INFERRED_SPANS_ENABLED=true
这种机制使用 异步分析器 来创建 Span,而无需你检测任何东西,从而使你能够更快地找到瓶颈。
Transaction ID 会自动添加到日志中。 你可以检查发送到 Elasticsearch 的 Filebeat 生成的日志文件。 一个条目看起来像这样。
{
"@timestamp": "2020-07-13T12:03:22.491Z",
"log.level": "INFO",
"message": "GET / 200 0:0:0:0:0:0:0:1 \"curl/7.64.1\" 0",
"service.name": "my-javalin-app",
"event.dataset": "my-javalin-app.log",
"process.thread.name": "qtp34871826-36",
"log.logger": "de.spinscale.javalin.Log4j2RequestLogger",
"trace.id": "ed735860ec0cd3ee3bdf80ed7ea47afb",
"transaction.id": "8af7dff698937dc5"
}
添加了 trace.id
和 transaction.id
,在发生错误的情况下,你将获得一个 error.id
字段。
我们没有涵盖 Elastic APM OpenTracing 桥接器,也没有研究 Agent 提供的 其他指标,这使我们可以查看诸如垃圾收集或应用程序的内存占用之类的事情。
Elastic Stack
到目前为止,我们的应用程序中存在一些基本的监控功能。 我们索引日志(带有跟踪),我们索引指标,甚至可以查看我们的应用程序以找出单个性能瓶颈,这要归功于 APM。 但是,仍然存在一个薄弱点。 到目前为止,所做的一切都在应用程序内部,但是所有用户都是从 Internet 访问该应用程序的。
检查我们的用户是否具有与我们的 APM 数据建议相同的体验如何? 想象一下,你的应用程序前面有一个滞后的负载均衡器,每个请求额外花费你 50 毫秒。 那将是毁灭性的。 或 TLS 协商成本很高。 即使这些外部事件都不是你的错,你仍然会受到影响,并且应该尝试减轻这些影响。 这意味着你需要首先了解它们。
Uptime 不仅使你能够监视服务的可用性,还可以绘制随时间变化的延迟图,并获得有关 TLS 证书过期的通知。
要将 Uptime 数据发送到 Elasticsearch,需要 Heartbeat(轮询组件)。 要下载并安装 Heartbeat,请使用适用于你系统的命令。
curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-9.0.0-amd64.deb
sudo dpkg -i heartbeat-9.0.0-amd64.deb
curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-9.0.0-x86_64.rpm
sudo rpm -vi heartbeat-9.0.0-x86_64.rpm
curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-9.0.0-darwin-x86_64.tar.gz
tar xzvf heartbeat-9.0.0-darwin-x86_64.tar.gz
curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-9.0.0-linux-x86_64.tar.gz
tar xzvf heartbeat-9.0.0-linux-x86_64.tar.gz
将 zip 文件的内容提取到
C:\Program Files
中。将
heartbeat-[version]-windows-x86_64
目录重命名为Heartbeat
。以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择以管理员身份运行)。
从 PowerShell 提示符下,运行以下命令以将 Heartbeat 安装为 Windows 服务。
PS > cd 'C:\Program Files\Heartbeat'
PS C:\Program Files\Heartbeat> .\install-service-heartbeat.ps1
如果你的系统上禁用了脚本执行,则需要为当前会话设置执行策略,以允许脚本运行。 例如: PowerShell.exe -ExecutionPolicy UnRestricted -File .\install-service-heartbeat.ps1
。
下载并解压缩后,我们需要再次设置云 ID 和密码。
我们需要在 Kibana 中以 elastic 管理员用户身份创建另一个
API_KEY
。POST /_security/api_key
{ "name": "heartbeat_javalin-app", "role_descriptors": { "heartbeat_writer": { "cluster": ["monitor", "read_ilm"], "index": [ { "names": ["heartbeat-*"], "privileges": ["view_index_metadata", "create_doc"] } ] } } }
让我们设置 Heartbeat 密钥库并运行设置。
./heartbeat keystore create echo -n "observability-javalin-app:ZXUtY2VudHJhbC0xLmF3cy5jbG91ZC5lcy5pbyQ4NDU5M2I1YmQzYTY0N2NhYjA2MWQ3NTJhZWFhNWEzYyQzYmQwMWE2OTQ2MmQ0N2ExYjdhYTkwMzI0YjJiOTMyYQ==" | ./heartbeat keystore add CLOUD_ID --stdin echo -n "SCdUSHMB1JmLUFPLgWAY:R3PQzBWW3faJT01wxXD6uw" | ./heartbeat keystore add ES_API_KEY --stdin ./heartbeat setup -e -E 'cloud.id=${CLOUD_ID}' -E 'cloud.auth=elastic:YOUR_SUPER_SECRET_PASS'
添加一些要监控的服务。
name: heartbeat-shipper cloud.id: ${CLOUD_ID} output.elasticsearch: api_key: ${ES_API_KEY} heartbeat.monitors: - type: http id: javalin-http-app name: "Javalin Web Application" urls: ["http://localhost:7000"] check.response.status: [200] schedule: '@every 15s' - type: http id: httpbin-get name: "httpbin GET" urls: ["https://httpbin.org/get"] check.response.status: [200] schedule: '@every 15s' - type: tcp id: javalin-tcp name: "TCP Port 7000" hosts: ["localhost:7000"] schedule: '@every 15s' processors: - add_observer_metadata: geo: name: europe-munich location: "48.138791, 11.583030"
现在启动 Heartbeat 并等待几分钟以获取一些数据。
sudo service heartbeat start
如果你使用 init.d
脚本启动 Heartbeat,则无法指定命令行标志(请参阅 命令参考)。 要指定标志,请在前台启动 Heartbeat。
另请参阅 Heartbeat 和 systemd。
sudo service heartbeat start
如果你使用 init.d
脚本启动 Heartbeat,则无法指定命令行标志(请参阅 命令参考)。 要指定标志,请在前台启动 Heartbeat。
另请参阅 Heartbeat 和 systemd。
sudo chown root heartbeat.yml
sudo ./heartbeat -e
- 你将以 root 身份运行 Heartbeat,因此你需要更改配置文件的所有权,或使用
--strict.perms=false
指定运行 Heartbeat。 请参阅 配置文件所有权和权限。
sudo chown root heartbeat.yml
sudo ./heartbeat -e
- 你将以 root 身份运行 Heartbeat,因此你需要更改配置文件的所有权,或使用
--strict.perms=false
指定运行 Heartbeat。 请参阅 配置文件所有权和权限。
PS C:\Program Files\heartbeat> Start-Service heartbeat
默认情况下,Windows 日志文件存储在 C:\ProgramData\heartbeat\Logs
中。
要查看 Uptime 应用程序,请选择 Observability → Uptime。 概览如下所示。
你可以看到监视器列表和一个全局概览。 让我们看看其中一个警报的详细信息。 点击 Javalin Web Application。
你可以看到上次计划检查的执行情况,但每次检查的持续时间可能更有趣。 你可以看到你的某个检查的延迟是否在上升。
有趣的部分是顶部的世界地图。 你可以在配置中指定检查的来源,在本例中是欧洲的慕尼黑。 通过配置在全球运行的多个 Heartbeat,你可以比较延迟并找出你需要运行应用程序的数据中心,以便靠近你的用户。
监视器的持续时间很短,因为它真的很快。 检查 httpbin.org
Endpoint 的监视器,你将看到更高的持续时间。 在这种情况下,每个请求大约需要 400 毫秒。 这并不令人意外,因为该 Endpoint 并不在附近,并且你需要为每个请求启动一个 TLS 连接,这成本很高。
不要低估这种监控的重要性。 此外,请考虑这仅仅是一个开始,因为下一步是进行综合监控,以监控你的应用程序的正确行为,例如,确保你的结帐流程始终有效。
有关使用 Elastic Observability 的更多信息,请参阅 Observability 文档。