Sentry 后端监控SDK分析

本章从源码出发探讨后端监控SDK的原理,主要集中在Java SDK和Spring Boot SDK.

并回答以下问题:

  1. 如何收集错误?
  2. 如何收集性能数据?
  3. 如何追踪路径?
  4. 什么时候发送数据?
  5. 怎么发送数据?
  6. 如何扩展Sentry SDK?

代码架构

1
2
3
4
|- sentry-spring
|- sentry-spring-boot-starter
|- sentry-logback
|- sentry

Sentry SDK的代码分为多个模块,放在Github仓库的根目录下。

  • sentry-spring-boot-starter在sentry-spring的基础上提供了跟mvc框架相关的插装项。
  • sentry-spring提供对spring框架的插装(Instumentation)。
  • sentry-logback提供了对日志系统logback的插装。
  • sentry提供接口定义,基本的类和方法。

Java SDK

有了之前分析前端SDK的经验,很多术语已经比较熟悉了。比如Hub, Scope, Event, Breadcrumbs, Transaction, Span等。

  • Event:收集的异常。
  • Hub: 将异常发送到Sentry的地方。
  • Scope: Event相关的上下文和Breadcrumbs。
  • Breadcrumbs: 异常发生前的事情。
  • Tansaction: 用于性能追踪的操作记录。
  • Span: transaction树的节点。

Java SDK实现了上述的数据结构。在任何Java项目里,都可以导入Sentry Java SDK。手动上传Event和Transaction。

Spring Boot SDK

有了框架,就可以利用框架提供的接口自动上传异常和性能数据。

在初始化阶段,只需在application.propertiesapplication.yml里添加配置信息即可。这些配置信息会自动注入到相应的对象中。

它的实现原理是利用了Spring Boot Starter自动装配的特性。

SpringBoot 定义了一套接口规范,SpringBoot 在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器。

查看META-INF/spring.factories.两个自动装配入口。

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.sentry.spring.boot.SentryAutoConfiguration,\
io.sentry.spring.boot.SentryLogbackAppenderAutoConfiguration

SentryLogbackAppenderAutoConfiguration

查看SentryLogbackAppenderAutoConfiguration,创建了SentryLogbackInitializer对象。

1
2
3
4
5
@Bean
public @NotNull SentryLogbackInitializer sentryLogbackInitializer(
final @NotNull SentryProperties sentryProperties) {
return new SentryLogbackInitializer(sentryProperties);
}

SentryLogbackInitializer实现了GenericApplicationListener接口。实际上是监听Spring的ContextRefreshedEvent事件。

1
2
3
4
5
@Override
public boolean supportsEventType(final @NotNull ResolvableType eventType) {
return eventType.getRawClass() != null
&& ContextRefreshedEvent.class.isAssignableFrom(eventType.getRawClass());
}

该事件在Spring容器里所有对象都实例化后触发。事件被捕捉后调用SentryLogbackInitializer的onApplicationEvent(). 在该方法里首先创建了SentryAppender。Appender在LogBack概念里是将日志事件发送到目的地的,sentryAppender就是将日志发送到Sentry。然后调用sentryAppender.start()对Sentry进行了初始化,最后将sentryAppender加入到logger。

1
2
3
4
5
6
7
8
9
@Override
public void onApplicationEvent(final @NotNull ApplicationEvent event) {
final Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
...
final SentryAppender sentryAppender = new SentryAppender();
...
sentryAppender.start();
rootLogger.addAppender(sentryAppender);
}

当logger接收到事件时,会触发appender里的append方法。根据日志的级别,会发送日志或者将日志放入到BreadCrumbs。

1
2
3
4
5
6
7
8
9
@Override
protected void append(@NotNull ILoggingEvent eventObject) {
if (eventObject.getLevel().isGreaterOrEqual(minimumEventLevel)) {
Sentry.captureEvent(createEvent(eventObject));
}
if (eventObject.getLevel().isGreaterOrEqual(minimumBreadcrumbLevel)) {
Sentry.addBreadcrumb(createBreadcrumb(eventObject));
}
}

SentryAutoConfiguration

回到另一个配置入口SentryAutoConfiguration。它创建了Hub对象,Spring MVC相关对象, performance相关对象,传输对象工厂等。Hub对象用于捕捉和发送异常,之前Hub在前端SDK中已经分析过,基本逻辑是一致的。Spring MVC和performance相关对象是收集数据用的。传输对象工厂用于创建发送到Sentry的客户端。

Spring MVC相关对象

Spring MVC相关对象主要有SentryRequestResolver, SentrySpringRequestListener, SentryExceptionResolver, SentryTracingFilter.

SentryRequestResolver主要是获取Http request相关信息并记录下来。

1
2
3
4
5
6
7
8
9
10
11
12
public @NotNull Request resolveSentryRequest(final @NotNull HttpServletRequest httpRequest) {
final Request sentryRequest = new Request();
sentryRequest.setMethod(httpRequest.getMethod());
sentryRequest.setQueryString(httpRequest.getQueryString());
sentryRequest.setUrl(httpRequest.getRequestURL().toString());
sentryRequest.setHeaders(resolveHeadersMap(httpRequest));

if (hub.getOptions().isSendDefaultPii()) {
sentryRequest.setCookies(toString(httpRequest.getHeaders("Cookie")));
}
return sentryRequest;
}

SentrySpringRequestListener实现了ServletRequestListener用于监听http请求。它主要用于初始化scope,添加breadscrumb并将前面的SentryRequestResolver加到scope的事件处理器中。事件处理器会在捕捉到错误的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void requestInitialized(ServletRequestEvent sre) {
hub.pushScope();

final ServletRequest servletRequest = sre.getServletRequest();
if (servletRequest instanceof HttpServletRequest) {
final HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
hub.addBreadcrumb(Breadcrumb.http(request.getRequestURI(), request.getMethod()));

hub.configureScope(
scope -> {
scope.addEventProcessor(
new SentryRequestHttpServletRequestProcessor(request, requestResolver));
});
}
}

SentryExceptionResolver实现了HandlerExceptionResolver从而捕捉Spring Boot全局异常,最后通过hub.captureEvent(event)发送给Sentry.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public @Nullable ModelAndView resolveException(
final @NotNull HttpServletRequest request,
final @NotNull HttpServletResponse response,
final @Nullable Object handler,
final @NotNull Exception ex) {

final Mechanism mechanism = new Mechanism();
mechanism.setHandled(false);
final Throwable throwable =
new ExceptionMechanismException(mechanism, ex, Thread.currentThread());
final SentryEvent event = new SentryEvent(throwable);
event.setLevel(SentryLevel.FATAL);
event.setTransaction(transactionNameProvider.provideTransactionName(request));
hub.captureEvent(event);

// null = run other HandlerExceptionResolvers to actually handle the exception
return null;
}

SentryTracingFilter通过FilterRegistrationBean注册了一个Servelt的过滤器。并且拥有最高优先级。

1
2
3
4
5
6
7
8
9
10
@Bean
@ConditionalOnProperty(name = "sentry.enable-tracing", havingValue = "true")
@ConditionalOnMissingBean(name = "sentryTracingFilter")
public FilterRegistrationBean<SentryTracingFilter> sentryTracingFilter(
final @NotNull IHub hub, final @NotNull SentryRequestResolver sentryRequestResolver) {
FilterRegistrationBean<SentryTracingFilter> filter =
new FilterRegistrationBean<>(new SentryTracingFilter(hub, sentryRequestResolver));
filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filter;
}

从@ConditionalOnProperty注解可知,SentryTracingFilter只有在sentry.enable-tracing=true的时候才创建。

下面代码里的doFilterInternal是SentryTracingFilter的过滤器逻辑。它的作用是开启一个transaction,并在执行完所有filter后(包括业务代码)关闭transaction。finish()里面调用了hub.captureTransaction(transaction); 将transation发送给Sentry.

另外,注意到代码里从头文件中取出了sentryTraceHeader。这是前端传过来的trace Id, 如果前端也配有Sentry,那么前后端的性能监控就能连结起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void doFilterInternal(){
...
final String sentryTraceHeader = httpRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER);
...
final ITransaction transaction = startTransaction(httpRequest, sentryTraceHeader);
try {
filterChain.doFilter(httpRequest, httpResponse);
} finally {
// after all filters run, templated path pattern is available in request attribute
final String transactionName = transactionNameProvider.provideTransactionName(httpRequest);
// if transaction name is not resolved, the request has not been processed by a controller
// and
// we should not report it to Sentry
if (transactionName != null) {
transaction.setName(transactionName);
transaction.setOperation(TRANSACTION_OP);
transaction.setStatus(SpanStatus.fromHttpStatusCode(httpResponse.getStatus()));
transaction.finish();
}
}
...
}

performance相关对象

此处引入了Spring 配置类SentryTransactionPointcutConfigurationSentrySpanPointcutConfiguration。它们分别定义@SentryTransaction和@SentrySpan注解的切点。同时还导入了SentryAdviceConfiguration配置类,定义了处理切点行为的类sentryTransactionAdvice和SentrySpanAdvice。

SentrySpanPointcutConfiguration为例。它定义了两类切点,一类是Class上带有@SentrySpan的方法。另一类是直接带有@SentrySpan的方法。

1
2
3
4
5
@Bean
public @NotNull Pointcut sentrySpanPointcut() {
return new ComposablePointcut(new AnnotationClassFilter(SentrySpan.class, true))
.union(new AnnotationMatchingPointcut(null, SentrySpan.class));
}

处理@SentrySpan的行为定义在SentrySpanAdvice。他的主要作用是将该函数的调用以span方式记录到transaction里。

1
2
final String operation = resolveSpanOperation(targetClass, mostSpecificMethod, sentrySpan);
final ISpan span = activeSpan.startChild(operation);

另外,对于@SentryTransaction. 如果已经有活跃的transaction, @SentryTransaction不会启动一个新的transaction.

1
2
3
4
if (isTransactionActive) {
// transaction is already active, we do not start new transaction
return invocation.proceed();
}

传输对象工厂

传输对象工厂会创建ApacheHttpClientTransport,该对象利用CloseableHttpAsyncClient发送请求。异步请求不会阻塞原来的业务逻辑。

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
@Override
public ITransport create(
final @NotNull SentryOptions options, final @NotNull RequestDetails requestDetails) {
Objects.requireNonNull(options, "options is required");
Objects.requireNonNull(requestDetails, "requestDetails is required");

final PoolingAsyncClientConnectionManager connectionManager =
PoolingAsyncClientConnectionManagerBuilder.create()
.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX)
.setConnectionTimeToLive(connectionTimeToLive)
.setMaxConnTotal(options.getMaxQueueSize())
.setMaxConnPerRoute(options.getMaxQueueSize())
.build();

final CloseableHttpAsyncClient httpclient =
HttpAsyncClients.custom()
.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
.setConnectionManager(connectionManager)
.setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE)
.build();

final RateLimiter rateLimiter = new RateLimiter(options.getLogger());

return new ApacheHttpClientTransport(options, requestDetails, httpclient, rateLimiter);
}

回答问题

现在基本摸清了Sentry后端SDK的代码。回到我们前面提到的问题。

如何收集错误?

对于Java, 利用sentry提供的方法手动收集。

对于Spring Boot, 一是利用它提供的HandlerExceptionResolver捕捉全局异常。 二是利用日志系统的收集。

如何收集性能数据?

对于Java, 手动收集。

对于Spring Boot, 一是利用了Servlet的filter, 开启和完成transaction。二是通过手动标记@SentryTransaction和@SentrySpan注解。

如何追踪路径?

在日志系统里添加Breadcrumb.

利用Spring的AOP机制,跟据@SentryTransaction和@SentrySpan往Transaction里面添加Span。

什么时候发送数据?

对于异常,一旦捕捉到就会发送。

对于性能数据,请求结束就会发送。

怎么发送数据?

Spring boot框架下默认使用CloseableHttpAsyncClient。

如何扩展Sentry SDK?

在 Java 相关的SDK中也有Intregration的概念,但大多数用在了安卓端。在后端SDK中相对较少。如果在Spring boot中,利用Spring Boot Starter扩展就够了。

Author: 方镇澎
Link: http://fzp.github.io/2021/03/16/how-sentry-work-1/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.