Spring Boot 实战 —— 通过 Sentry 收集 Log4j2 异常日志记录

Sentry-Robot

背景

在日常线上运行的应用中如果报了异常,怎么在第一时间收到报错告警并且查看报错日志呢?周五就因为缺乏报错日志,导致构建服务受影响超过了 2 小时,虽然不至于「死人」,但是也确实让我意识到日志的重要性!后来临时在相关的地方加上日志,通过错误信息很快定位了到了原因,原来是数据库被运维人员做了新特性的改动,而我们服务还未适配好。想想当时的场景,还真是有点紧张~囧

今天就学习一下利用 Sentry 作为日志收集系统,结合之前学习的 log4j2 日志输出,主动收集异常日志。

Sentry

我们先来 Sentry 是怎么介绍自己的:

Open-source error tracking that helps developers monitor and fix crashes in real time. Iterate continuously. Boost efficiency. Improve user experience

大体意思就是:Sentry 是一款开源的错误跟踪系统,可帮助开发人员实时监控和修复崩溃。它是不断迭代的,提高效率,改善用户体验。Sentry 本身的文档也记载的比较全面,强烈安利,一般问题可以通过阅读 Doc 学习或者论坛咨询

OK,下面我们就开始通过 Docker 安装 Sentry。

PS:如果你的 Docker 环境还未配置好,可以阅读我之前的总结:Linux——CentOS 安装 Docker 教程

前提

Sentry Installation 页面,官方做了总结,Sentry 需要和几个服务有交互:

  • PostgreSQL:Docker image postgres:9.5,这里明确版本是 9.5,我觉得非常棒!这保证了在未来部署的可重现,避免了不同版本差异造成部署失败的问题。
  • Redis
    • 官方说如果你使用的 Ubuntu < 15.04,推荐安装 chirs-lea/redis-server
    • 文档推荐的是 Docker image redis:3.2-alpine.

硬件要求,具体的可以阅读官方文档,我这里仅仅是家里测试使用,因此无所谓。

下面的步骤其实是官方文档的翻译,但是,跟着走了一遍之后发现,服务并不能正常运行起来,但是也没啥坏处,主要步骤其实就和这个差不多,可能是某些细节漏了,因此,你可以大体浏览一下步骤。如果不想浪费时间,可以直接跳到下面的 「脚本一键安装 Sentry」小节。

创建容器

需要 Docker 版本 1.10+

克隆仓库 getsentry/onpremise。这个仓库是定制化构建你自己 Sentry 镜像的基础:

1
2
3
cd /data
git clone https://github.com/getsentry/onpremise.git
cd onpremise

仓库里的 sentry.conf.py and config.yml 可供你 配置 Sentry 所用,建议先浏览一下这个配置文档。

现在开始构建我们定制好的镜像。如果你构建的镜像需要推送到你本地的镜像仓中,那么可以用如下方式先定义好镜像名再构建:

1
REPOSITORY=registry.michael.com/sentry make build push

我本地没有镜像仓,只需要用如下方式构建即可:

1
make build

运行依赖的服务

Redis:

1
2
3
4
docker run \
--detach \
--name sentry-redis \
redis:3.2-alpine

PostgreSQL:

1
2
3
4
5
6
docker run \
--detach \
--name sentry-postgres \
--env POSTGRES_PASSWORD=secret \
--env POSTGRES_USER=sentry \
postgres:9.5

Outbound Email:

1
2
3
4
docker run \
--detach \
--name sentry-smtp \
tianon/exim4

运行 Sentry 服务

${REPOSITORY} 只的是你刚刚定义的镜像名,如果没有,默认是 sentry-onpremise

REPOSITORY

为了测试镜像是 OK 的,可以运行如下命令:

1
2
3
docker run \
--rm ${REPOSITORY} \
--help

现在可以生成一个 secret-key 值:

1
2
3
docker run \
--rm ${REPOSITORY} \
config generate-secret-key

生成的值可以在 config.yml 中设置给 system.secret-key,或者通过环境变量。如果是设置在 config.yml 文件中,这时候你必须重新构建你的镜像。

运行的基本命令举例如下:

1
2
3
4
5
6
7
8
docker run \
--detach \
--link sentry-redis:redis \
--link sentry-postgres:postgres \
--link sentry-smtp:smtp \
--env SENTRY_SECRET_KEY='<secret-key>' \
${REPOSITORY} \
<command>

官方文档里有一行小字,说明的是后面的文档,不会特地的写上 –link 去表示链接容器,但是这些都是必须的!同时,${REPOSITORY} 会被引用为 sentry-onpremise。

运行 Web 服务

暴露的是 9000 的端口,这样部署之后,可以通过 http://localhost:9000/ 访问。

1
2
3
4
5
6
7
8
9
10
docker run \
--detach \
--link sentry-redis:redis \
--link sentry-postgres:postgres \
--link sentry-smtp:smtp \
--name sentry-web \
--publish 9000:9000 \
--env SENTRY_SECRET_KEY=${SENTRY_SECRET_KEY} \
sentry-onpremise \
run web

脚本一键安装 Sentry

发现了 getsentry/onpremise/install.sh 这个脚本,看懂这个脚本里的内容基本上就差不多知道安装步骤了。

读一下脚本:

  • LATEST_STABLE_SENTRY_IMAGE='sentry:9.1.2' 安装的是 sentry:9.1.2
  • docker-compose.yml 中会去创建两个卷,分别是 sentry-datasentry-postgres,这和上面步骤是不是发现区别了,上面创建
    postgres 时,可没有创建卷;
  • cp -n .env.example "$ENV_FILE" 会将仓库下的 .env.example 文件重命名为 .env 文件,其实里面就是为了设置一个环境变量 SENTRY_SECRET_KEY
  • export SENTRY_IMAGE=$LATEST_STABLE_SENTRY_IMAGE 命名了一个环境变量,记录了镜像名
  • docker-compose build 开始构建镜像啦,其实就是类似这个命令 docker build .
  • SECRET_KEY=$(docker-compose run --rm web config generate-secret-key 2> /dev/null | tail -n1 | sed -e 's/[\/&]/\\&/g') 运行命令获得 secret-key 值,赋值给了变量 SECRET_KEY
  • sed -i -e 's/^SENTRY_SECRET_KEY=.*$/SENTRY_SECRET_KEY='"$SECRET_KEY"'/' $ENV_FILESECRET_KEY 变量值填写到环境变量文件 .env
  • 接着就是设置数据库的步骤,这其实是就是为了在数据库中添加账号数据和其他一些数据表生成
  • 最后就是执行 cleanup 函数

执行脚本之前,我们先把之前创建的几个服务删除掉吧:

1
2
docker stop sentry-smtp sentry-postgres sentry-redis
docker rm $(docker ps -aq)

配置邮箱

通过上面的阅读,可以发现,运行脚本时,会去执行 docker build .,这个会将配置文件 config.yml 和环境变量 .env 放入镜像的。因此,我们启用发生异常时获取邮箱告警的功能,需要现在配置文件中设置好邮箱配置。

这里我们选择在 .env 中配置好环境变量,实现邮箱的配置:
配置 config.yml:

1
2
3
4
5
6
7
8
9
SENTRY_SECRET_KEY=)g=lkl1)uugx236%#)mq2o34^@a&g3q85q**co*hbapm5y1*bs
# 邮箱设置
SENTRY_EMAIL_HOST=smtp.qq.com
SENTRY_EMAIL_USER=649168982@qq.com
SENTRY_SERVER_EMAIL=649168982@qq.com
# 这里指的是邮箱的授权码,而非密码
SENTRY_EMAIL_PASSWORD=xxxx
SENTRY_EMAIL_USE_TLS=true
SENTRY_EMAIL_PORT=587
  • 网上的教程,大多数是 SENTRY_EMAIL_HOST: 'smtp.exmail.qq.com',这个是企业邮箱,我们个人的不这么设置。
  • SENTRY_EMAIL_USERSENTRY_SERVER_EMAIL 要保持一致;

这里我设置 1 分钟,就能及时收到邮箱,但是设置 5 分钟,等了 5 分钟,也没收到,不知道是何原因:

Project-Alerts

Alerts

经过上面的设置,可以试试邮箱发送功能是否 OK:点击左上角头像,选择 Admin-》Mail-》测试设置

邮箱测试

部署 sentry

下面开始安装步骤:

  • sh install.sh 执行安装脚本,脚本执行完需要一点时间,运行完成之后,会退出。中间过程会让你选择是否创建账号:

添加账号

  • 接着运行 docker-compose up -d 即可;

容器列表

PS:为何容器的名称是都是 onpremise 开头的呢?因为不指定名称时,会默认取目录名的

这时候输入刚刚创建的账号登录:

login

配置 Sentry

创建 project

那我之前的 log4j2 的 demo 作为演示,这里选择一个 Java 项目,并且,我还创建了一个叫 spring-boot 的 Team。

project

spring-boot 项目适配

官方文档-Java 给出了适用于 Java 项目的全面的适配指南,咱们使用的是 log4j2

引入依赖

1
2
3
4
5
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-log4j2</artifactId>
<version>1.7.26</version>
</dependency>

log4j2.xml 配置修改

  • configuration 中加上 packages="org.apache.logging.log4j.core,io.sentry.log4j2"

下面示例中的 SentryAppender 表示发送 warn 级别的日志到 Sentry Server。ConsoleAppender 仅仅表示是一个示例,表示你项目中之前使用的非 sentryappener 的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="warn" packages="org.apache.logging.log4j.core,io.sentry.log4j2">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>

<Sentry name="Sentry" />
</appenders>

<loggers>
<root level="INFO">
<appender-ref ref="Console" />
<!-- Note that the Sentry logging threshold is overridden to the WARN level -->
<appender-ref ref="Sentry" level="WARN" />
</root>
</loggers>
</configuration>

经过测试,,因为原有项目中的 apppender 都是为了之前的作用设置的,比如控制台打印、比如输出到文件。要想将异常信息发送到 Sentry,这里的SentryAppender 是必不可少的。别忘了 appender-ref 也要设置!

配置 DSN

配置页面 介绍了如何设置 DSN(Data Source Name)。

进入 Sentry,项目的 DSN 在项目页面-》setings-》Clinet Keys(DSN) 中可以发现:

DSN

配置 DSN 有好几种方式,具体的可以在页面查看,这里介绍我采用的:

resources 文件夹下,新建 sentry.properties

1
dsn=http://8d53042c89774e5dba599ee67c5c8804@192.168.3.43:9000/3

默认的就是 sentry.properties,一开始我直接写在了 application.properties 中,Sentry 怎么也收不到异常日志。

代码中

代码示例:

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
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;

public class MyClass {
private static final Logger logger = LogManager.getLogger(MyClass.class);
private static final Marker MARKER = MarkerManager.getMarker("myMarker");

void logSimpleMessage() {
// This sends a simple event to Sentry
logger.error("This is a test");
}

void logWithBreadcrumbs() {
// Record a breadcrumb that will be sent with the next event(s),
// by default the last 100 breadcrumbs are kept.
Sentry.record(
new BreadcrumbBuilder().setMessage("User made an action").build()
);

// This sends a simple event to Sentry
logger.error("This is a test");
}

void logWithTag() {
// This sends an event with a tag named 'log4j2-Marker' to Sentry
logger.error(MARKER, "This is a test");
}

void logWithExtras() {
// MDC extras
ThreadContext.put("extra_key", "extra_value");
// NDC extras are sent under 'log4j2-NDC'
ThreadContext.push("Extra_details");
// This sends an event with extra data to Sentry
logger.error("This is a test");
}

void logException() {
try {
unsafeMethod();
} catch (Exception e) {
// This sends an exception event to Sentry
logger.error("Exception caught", e);
}
}

void unsafeMethod() {
throw new UnsupportedOperationException("You shouldn't call this!");
}
}

可以发现,Sentry 使用的接口和之前 log4j2 是有区别的:

1
2
3
4
5
6
7
8
// 原来
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private final Logger log = LoggerFactory.getLogger(this.getClass());
// sentry
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
private final Logger log = LogManager.getLogger(this.getClass());

实际过程中,这里是很多人忽视的,一定要仔细一点!

这时候,使用我们之前的错误接口故意打印错误日志,看看 Sentry 的捕获效果吧:

error

总结

今天在之前加入的一个技术微信群里,一位同学发了一篇怎么搭建 Zabbix 的文章,被群主踢了。后来大家讨论,这种搭建的文章,普遍质量比较低,没有太多分享的意义,应该要更多的关注一些前言的知识、或者底层的基础知识。这种观点我是比较认同的,但是谁不是一步一步慢慢来的呢?

今天搭建 Sentry 的过程,对之前 docker-compose 的用法又有了进一步的认识,学习到了使用 .env 的方式设置容器内环境变量的方式,同时,也学习到了可以公用一种配置,让 docker-compose 文件内多个服务公用的方式。其实,自己的每一点的折腾,都会是后面的基石。只要持续积累,才会越走越顺!

示例代码

  • awesome-spring-boot-examples/log4j2

参考

  • 神奇的展示-用 Docker 部署 Sentry Bug 日志收集服务
  • 掘金-搭建私有的前端监控服务: sentry
  • sentry配置邮件
  • 自建sentry后,配置了邮件服务,但是还是收不到验证邮件? 这个问题有参考意义,还是推荐通过环境变量的方式而不是 config.yml 配置邮箱
Michael翔 wechat
ヾノ≧∀≦)o 欢迎订阅公众号「Coder魔法院」☑工具控 ☑编程 ☑读书☑电影
「🤓 码字不易,来杯Coffee👇」