Sentry初始化流程源码解析

2,192 阅读7分钟

##Sentry是什么 Sentry 是一个实时事件日志记录和汇集的平台。其专注于错误监控以及提取一切事后处理所需信息而不依赖于麻烦的用户反馈。 ##Sentry介绍 无论测试如何完善的程序,bug总是免不了会存在的,有些bug不是每次都会出现,测试时运行好好的代码可能在某个用户使用时就歇菜了,可是当程序在用户面前崩溃时,你是看不到错误的,当然你会说:”Hey, 我有记日志呢”。 但是说实话,程序每天每时都在产生大量的日志,而且分布在各个服务器上,并且如果你有多个服务在维护的话,日志的数量之多你是看不过来的吧。等到某天某个用户实在受不了了,打电话来咆哮的时候,你再去找日志你又会发现日志其实没什么用:缺少上下文,不知道用户什么操作导致的异常,异常太多(从不看日志的缘故)不知如何下手 等等。

Sentry就是来帮我们解决这个问题的,它是一款精致的Django应用,目的在于帮助开发人员从散落在多个不同服务器上毫无头绪的日志文件里发掘活跃的异常,继而找到潜在的臭虫。

Sentry是一个日志平台, 它分为客户端和服务端,客户端(目前客户端有Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web节目方便查看。Sentry由python编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest等。 ##Sentry 和 ELK 的区别 相对来说Sentry适合做重要异常的记录与提醒把每一种类型的日志进行聚合分类,定期分析事件发生评率,而且Sentry提供了可扩展的操作灵活性很高,可以做到及时提醒等等,但是不便于上下文追踪。ELK比较适合做相对较多日志的存储与分析和查找便于上下文追踪。 两者配合使用,在收到Sentry提醒的时候可以到ELK上寻找详细的上下文

设置DSN(数据源名称)

DSN是第一个也是最重要的配置,因为它告诉SDK在哪里发送事件。您可以在Sentry的“项目设置”的“客户端密钥”部分中找到项目的DSN。它可以以多种方式配置。的说明的配置方法在下面详述

在文件系统或类路径上的属性文件中(默认为sentry.properties):

dsn=https://public:private@host:port/1

通过Java启动入参:

java -Dsentry.dsn=https://public:private@host:port/1 -jar app.jar

通过系统环境变量:

SENTRY_DSN=https://public:private@host:port/1 java -jar app.jar

在代码中:

import io.sentry.Sentry;
Sentry.init("https://public:private@host:port/1");

##Sentry初始化 我们现在来看下到底怎么初始化的 以Sentry.init("https://public:private@host:port/1")为例 静态方法Sentry.init(String str)实际上调用

public static SentryClient init(String dsn) {
        return init(dsn, null);
    }
public static SentryClient init(String dsn, SentryClientFactory sentryClientFactory) {
        SentryClient sentryClient = SentryClientFactory.sentryClient(dsn, sentryClientFactory);
        setStoredClient(sentryClient);
        return sentryClient;
    }

先到方法SentryClientFactory.sentryClient(dsn, sentryClientFactory);看看干了什么简化代码如下:

public static SentryClient sentryClient(String dsn, SentryClientFactory sentryClientFactory) {
        Dsn realDsn = resolveDsn(dsn);
        if (sentryClientFactory == null) {
            String sentryClientFactoryName = Lookup.lookup("factory", realDsn);
            if (Util.isNullOrEmpty(sentryClientFactoryName)) {
                sentryClientFactory = new DefaultSentryClientFactory();
            } else {
                Class<? extends SentryClientFactory> factoryClass = null;
                factoryClass = (Class<? extends SentryClientFactory>) Class.forName(sentryClientFactoryName);
                sentryClientFactory = factoryClass.newInstance();
                
            }
        }
        return sentryClientFactory.createSentryClient(realDsn);
    }

Dsn realDsn = resolveDsn(dsn);先看看里面比较关键的一个查找方法lookup()简化代码如下:

public static String lookup(String key, Dsn dsn){
        String value=null;
//jndiLookup()
      Class.forName("javax.naming.InitialContext",false,Dsn.class.getClassLoader());
        value=JndiLookup.jndiLookup(key);
//启动jar包时入参-Dsentry.dsn=xxx格式
        if(value==null){
     value=System.getProperty("sentry."+key.toLowerCase());
        }

//通过系统环境变量SENTRY_DSN=xxxx
        if(value==null){
        value=System.getenv("SENTRY_"+key.replace(".","_").toUpperCase());
        }

////用java代码进行配置Sentry.init("xxxxxxx")
        if(value==null&&dsn!=null){
        value=dsn.getOptions().get(key);
        }
//sentry.properties.file或者sentry.properties文件中查找或者环境变量
        if(value==null&&configProps!=null){
        value=configProps.getProperty(key);
        }
}

可以看因为后面value不为空时就不执行其他获取配置,所以sentry启动参数的优先级为 启动jar入参> 从配置文件获取参数> 通过系统环境变量获取参数> 用java代码配置> 从jvm获取配置>

这里找到了字符串配置把字符串路径转为URL对象在进行参数切割成Dsn数据源。后面通过集成抽象类SentryClientFactory实现createSentryClient()方法就初始化一个DefaultSentryClientFactory简化代码如下:

public SentryClient createSentryClient(Dsn dsn) {
//SentryClient添加一个连接和管理上下文的Manager
        SentryClient sentryClient = new SentryClient(createConnection(dsn), getContextManager(dsn));
        try {
	   Class.forName("javax.servlet.ServletRequestListener", false, this.getClass().getClassLoader());
		//对事件进行增加	
            sentryClient.addBuilderHelper(new HttpEventBuilderHelper());
        } 
		//添加面包屑
        sentryClient.addBuilderHelper(new ContextBuilderHelper(sentryClient));
//这一步会去查找sentry其他配置release、dist、environment、servername、tags、extratags。配置方式与dsn一样
        return configureSentryClient(sentryClient, dsn);
    }

既然上面已经说到了其他配置,那么就一同整理出来吧 getContextManager(dsn)获得context。默认情况下,Sentry使用一个ThreadLocalContextManager维护Context每个线程的单个实例。这对于每个用户请求使用一个线程的框架(例如基于同步servlet API的框架)非常有用。Sentry还会安装一个ServletRequestListener在每个servlet请求完成后清除线程上下文的文件。 HttpEventBuilderHelper重写了helpBuildingEvent()方法对EventBuilder进行了增强

public void helpBuildingEvent(EventBuilder eventBuilder) {
        HttpServletRequest servletRequest = SentryServletRequestListener.getServletRequest();
        if (servletRequest == null) {
            return;
        }
        //对eventBuilder添加了请求ip地址
        addHttpInterface(eventBuilder, servletRequest);
      //添加了请求用户名和ip
        addUserInterface(eventBuilder, servletRequest);
    }

因为return configureSentryClient(sentryClient, dsn);方法会去按加载dsn的方法去查找其他配置添加到SentryClient顺便再看看其他附加属性分别什么含义

    public static final String RELEASE_OPTION = "release";
    /**
     * Option to set the distribution of the application.
     */
    public static final String DIST_OPTION = "dist";
    /**
     * Option to set the environment of the application.
     */
    public static final String ENVIRONMENT_OPTION = "environment";
    /**
     * Option to set the server name.
     */
    public static final String SERVERNAME_OPTION = "servername";
    /**
     * Option to set additional tags to be sent to Sentry.
     */
    public static final String TAGS_OPTION = "tags";
    /**
     * Option to set extras to extract and send as tags, where applicable.
     */
    public static final String EXTRATAGS_OPTION = "extratags";

release

要设置将与每个事件一起发送的应用程序版本,请使用以下release选项:

release=1.0.0

###distribution 要设置将随每个事件一起发送的应用程序分发,请使用以下dist选项:

release=1.0.0
dist=x86

请注意,只有和release一起设置才有用。 ###environment 要设置将随每个事件一起发送的应用程序环境,请使用以下environment选项:

environment=staging

###server Name 要设置将随每个事件一起发送的服务器名称,请使用以下servername选项:

servername=host1

###tags 要设置将随每个事件一起发送的标记,请使用tags逗号分隔的键对和由冒号连接的值的选项:

tags=tag1:value1,tag2:value2

###extratags 要设置将随每个事件(但不作为标记)一起发送的额外数据,请使用extra逗号分隔的键对和由冒号连接的值的选项:

extra=key1:value1,key2:value2

##整合日志 因为公司用的是logback框架写的日志那么看一下整合方式: 以下示例ConsoleAppender将该日志配置为在该INFO级别记录到标准输出,并将SentryAppender该日志记录到该WARN级别的Sentry服务器。将ConsoleAppender仅作为被设置为一个不同的日志记录阈值的非哨兵附加器。 使用logback.xml格式的示例配置:

<configuration>
    <!-- Configure the Console appender -->
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Configure the Sentry appender, overriding the logging threshold to the WARN level -->
    <appender name="Sentry" class="io.sentry.logback.SentryAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
    </appender>

    <!-- Enable the Console and Sentry appenders, Console is provided as an example
 of a non-Sentry logger that is set to a different logging threshold -->
    <root level="INFO">
        <appender-ref ref="Console" />
        <appender-ref ref="Sentry" />
    </root>
</configuration>

再来看看io.sentry.logback.SentryAppender看它是怎么写入sentry服务器的,简化代码如下:

@Override
    protected void append(ILoggingEvent iLoggingEvent) {
        // 如果当前线程由Sentry管理,则不要记录该事件
        if (isNotLoggable(iLoggingEvent) || SentryEnvironment.isManagingThread()) {
            return;
        }
        //记录当前线程写入次数
        SentryEnvironment.startManagingThread();
        //创建eventBuilder
        EventBuilder eventBuilder = createEventBuilder(iLoggingEvent);
        //调用静态方法写入sentry服务器,前面初始化Sentry的时候已经创建了链接
        Sentry.capture(eventBuilder);
    }

SentryEnvironment内部使用的TheadLocal为一个线程统计计数,并且还是AtomicInteger进行原子加代码简化如下:

public final class SentryEnvironment {

    protected static final ThreadLocal<AtomicInteger> SENTRY_THREAD = new ThreadLocal<AtomicInteger>() {
        @Override
        protected AtomicInteger initialValue() {
            return new AtomicInteger();
        }
    };
    
    public static boolean isManagingThread() {
        return SENTRY_THREAD.get().get() > 0;
    }
    
    public static void startManagingThread() {
            SENTRY_THREAD.get().incrementAndGet();
    }
}

上面初始化SentryClient时后调用的setStoredClient(sentryClient)方法现在就可以拿来用了

public static void capture(EventBuilder eventBuilder) {
        //获取client准备发送
        getStoredClient().sendEvent(eventBuilder);
 }
public void sendEvent(EventBuilder eventBuilder) {
        //其他附加属性添加进eventBuilder
        if (!Util.isNullOrEmpty(release)) {
            eventBuilder.withRelease(release.trim());
            if (!Util.isNullOrEmpty(dist)) {
                eventBuilder.withDist(dist.trim());
            }
        }
        if (!Util.isNullOrEmpty(environment)) {
            eventBuilder.withEnvironment(environment.trim());
        }
        if (!Util.isNullOrEmpty(serverName)) {
            eventBuilder.withServerName(serverName.trim());
        }
        for (Map.Entry<String, String> tagEntry : tags.entrySet()) {
            eventBuilder.withTag(tagEntry.getKey(), tagEntry.getValue());
        }
        //对helpers进行遍历对eventBuilder进行增强其实就只有ContextBuilderHelper、HttpEventBuilderHelper这两个东西
        runBuilderHelpers(eventBuilder);
        //构建事件
        Event event = eventBuilder.build();
        //拿到connection.send()发送到sentry服务器
        sendEvent(event);
 }

没了