27
2017
09

Log搜集工具软件文档梳理

JrdLoger软件架构

1. 软件目标

目前公司软件开发及验证的基本流程是软件开发部门负责软件功能开发调试并发布正式版本后,测试部门及End User测试人员获取最新版本进行测试及使用体验。若发现问题则提交Defect到公司ALM系统中由软件开发人员处理。但有些问题并不一定能够稳定复现,同时系统在运行期间发生的异常用户不一定会留意到,因此部门希望能够新增软件自动反馈渠道。在系统中预置一个自动测试异常的应用,它能够在系统发生异常时自动抓取LOG信息及截图,自动将异常上报给服务端。在服务端模块负责人能够获取异常信息并能够关联公司ALM系统中的defect,保证有效异常能够得到跟踪处理。

jrdLogger是公司设计团队为公司手机设计的Log搜集工具。此工具运行在End User之前的版本,可以帮助公司搜集手机中Crash Log,完善手机的系统,在公司手机产品正式发布之后,会移除这个工具。

设计的原则:

  • 程序要易于修改和维护
  • 在编程时要考虑到测试的需求,编制出易于测试的代码
  • 编程与编制文档工作要同步进行
  • 编程时要采用统一的标准和约定,注意命名规则、格式和多加注释,降低程序的复杂性
  • 编程命名能够反映变量的数据类型和含义、函数方法的使用范围和含义
  • 尽可能多地重用代码

2. 需求说明

jrdLoger整个生态包含2个部分:客户端和服务端。客户端完成log记录创建,附件上传等功能;服务段对上传log记录进行保存和管理,同时提供给工程师进行页面操作。

这里写图片描述

2.1 jrdLogger服务端

jrdLogger服务端应该具有以下的功能:

  • 接收客户端新建log记录请求
  • 接收客户端上传log附件请求
  • 接收追加comment请求
  • 对本地存储的log进行整合和过滤
  • 提供页面进行查看,下载等操作
  • 可自动同步ALM上的task状态

其整体架构如下图所示,左边为Mobile端,右边为Web端,中间为服务器。
这里写图片描述

  • Mobile端:主要工作是捕捉Mobile中的Log然后结合DeviceInfo,打包为一个Zip文件上传到服务器。

  • Web端:用户包括Developer和Validator可以从Server下载相关的Zip文件。

  • Server端:除了可以提供上传和下载服务之外,还可以支持与Mobile端的通信,比如添加comment。

2.2 jrdLogger

jrdLogger即客户端应该具有以下的功能:

  • 自动捕获系统发生的重要异常
  • 自动处理异常,打包重要的Log信息,异常截图
  • 支持用户手动复现异常
  • 上传本地处理完成的压缩包到服务器
  • 追加额外的评论
  • 可以对应用进行设置

客户端的架构主要分为两个部分,第一个为主要业务流程,包括了从异常检测到最后追加评论的整个逻辑;第二个为辅助功能,这个辅助功能的模块为第一个流程服务,包括应用设置,使用帮助,还有UI设计。
这里写图片描述

3. 功能设计

针对客户端,主要设计如下几个功能模块:

  • 异常检测功能
  • 异常处理功能
  • 异常识别功能
  • 手动复现
  • 日志上传
  • 应用设置功能
  • 异常记录管理功能
  • 追加评论功能

3.1 异常检测功能

功能介绍:

异常检测功能就是在能在手机出现Crash异常的时候,能精确的捕获到这个异常。异常检测功能是整个JrdLogger工作流程开始的地方

实现原理:

首先,使用Android系统自带的DropBoxManager功能,监听异常的产生,并从DropBoxManager中得到比较粗糙的log信息;然后通过DropBoxManager,获取当前异常的类型,根据异常类型到MtkLogger(这里不只是MtkLog,还包括TCTlog和android 自带的log信息中)相应的目录下获取更为详细的log,最后将DropBoxManager中得到的异常信息和对应的MtkLogger打包上传到服务器,为工程师分析问题提供依据。

  • DropBoxManager介绍:
    DropBoxManager是Android系统自带的,用于监视系统运行稳定性的机制,DropBoxManager会搜集手机中的漏洞,以便于工程师对Android系统的维护。利用DropBoxManager可以搜集手机Crash异常的这个特性,把DropBoxManager作为异常检测功能的核心。DropBoxManager使用的原理为,当Android系统发生Crash异常的时候,DropBoxManager会捕获到这个异常,把这个异常添加到DROPBOX_ENTRY里面,同时发出广播android.intent.action.DROPBOX_ENTRY_ADDED。所以,只要在注册监听这个广播的监听器,然后从广播的Intent里面获取异常信息,这就完成了异常检测功能。

  • MtkLogger介绍:
    mtklog是由log生成工具MTKLogger生成的一系列问题追踪文件,其主要作用就是对系统或者应用产生的异常进行快速定位,从而解决问题。通过实践发现,MtkLogger在Crash异常发生之后,会在/sdcard/mtklog/aee_exp(有些手机是aee_exp_bak)目录生成db目录,然后根据不同的异常类型,获取不同目录下的db文件(对于ANR类型或者NE异常,会在相应的/tctpersist/minilog目录和/data/目录下获取对应的db文件)。最后将DropBoxManager中获得的异常信息及对应的db文件一起打包上传到服务器中。

整个log收集的开始逻辑,都是从监听到android.intent.action.DROPBOX_ENTRY_ADDED异常广播开始的,具体广播接收器注册如下:

<receiver android:name=".receiver.StartupIntentReceiver">
<intent-filter android:priority="10">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.DROPBOX_ENTRY_ADDED" />
<action android:name="com.jrd.logger.UPTIME_REPORT" />
<action android:name="com.jrd.logger.action.manual.ignore" />
<action android:name="com.jrd.logger.action.upload.check" />
<action android:name="com.jrd.logger.action.logger.trunon" />
</intent-filter>
</receiver>

然后在监听器中对异常信息进行分类判断

String tag = intent.getStringExtra(DropBoxManager.EXTRA_TAG);
long time = intent.getLongExtra(DropBoxManager.EXTRA_TIME, 0);
boolean ignore = true;
 if (tag.endsWith("native_crash")) {
            ignore = false;
            report = "reportNE";
            type = "Native (NE)";
 Exception_Type = LoggerStore.LoggerRecord.EXCEPTION_TYPE_NE;
        } else if (tag.endsWith("crash")) {
            ignore = false;
            report = "reportJE";
            type = "Java (JE)";
            Exception_Type = LoggerStore.LoggerRecord.EXCEPTION_TYPE_JE;
        } else if (tag.endsWith("anr")) {
            ignore = false;
            report = "reportANR";
            type = "ANR";
            Exception_Type = LoggerStore.LoggerRecord.EXCEPTION_TYPE_ANR;
        } else if (tag.endsWith("wtf")) {
        } else if (tag.endsWith("watchdog")) {
        } else if (tag.equals("SYSTEM_BOOT")) {
        } else if (tag.equals("SYSTEM_RESTART")) {
        } else if (tag.endsWith("lowmem")) {
        } else if (tag.endsWith("strictmode")) {
        } else {
            LogUtils.getInstance().logw(TAG, "getInfoFromDropBox(): tag.endsWith:unknown");
        }
        //从DropBoxManager获取异常详细信息
        DropBoxManager dropbox = (DropBoxManager) context.getSystemService(Context.DROPBOX_SERVICE);
        DropBoxManager.Entry entry = dropbox.getNextEntry(tag, time - 1);
        //如果没有获取到entry信息,返回null
        if (entry == null) {
            return null;
        }
        /*异常的详细信息*/
        String data = entry.getText(8192);
        String[] fields = data.split("\n");
        /*从详细信息中获取包名*/
        String packageName = "Unknow";
        for (String tmp : fields) {
            if (tmp.startsWith("Process: ")) {
                packageName = tmp.substring(9);
            }
        }
         /*从详细信息中获取pid*/
        String pid = "0000";
        for (String tmp : fields) {
            if (tmp.startsWith("Pid: ")) {
                pid = tmp.substring(5);
            }
        }
        //自定义拼凑的crashcontent
String crashContent = "Type:" + type + ",Time:" + time_format + ",PackageName:" + packageName+ ",Tag:" + tag + ",Pid:" + pid + "," + outFieldSeparator + data;

工作流程:

异常检测功能的工作流程图具体如下:
这里写图片描述

3.2 异常处理功能

功能介绍:

异常处理功能的作用是从检测到的异常中提取到有用的信息,然后把整理后的信息提交给日志上传模块。在整个过程中有很多细节需要处理,比如要在检测到异常之后,调用异常识别功能把异常的日志和本地数据库的异常记录进行对比,只有新发现的异常才会处理,已经存在的异常不处理。还有在异常处理过程还要判断应用配置是否打开手动复现功能,如果开启了,会进入手动复现流程,如果没有开启,则走默认流程把信息提交给日志上传模块。

工作流程:

  1. 检测到异常之后,第一时间就是截图,这张截图会作为异常日志的一部分,保存到/sdcard/jrdlog/picture/目录
  2. 调用异常识别功能,将新检测到的异常和本地数据库的异常进行对比
  3. 如果数据库已经存在这个异常,则把截图删除,结束流程。如果不存在,则继续下面的流程
  4. 判断用户是否设置了开启手动复现功能,如果没有开启则进行第5步,如果开启则弹出提示,询问用户是否手动复现这个异常,如果用户拒绝或者在响应时间内没有操作,则进行第5步默认流程;如果用户选择手动复现,则进入手动复现流程,手动复现流程在后面的章节介绍
  5. 把新记录插入数据库
  6. 把异常信息、Log、截图压缩保存到/sdcard/jrdlog/ziplog/目录
  7. 调用日志上传功能
  8. 结束流程

整个工作流程的流程图具体如下所示:
这里写图片描述

在接收到DropBoxManager类发出的异常广播后,会触发截屏功能,该功能的实现是通过工具类ScreenShotUtil实现的,其实现原理是通过调用系统服务来进行截屏处理,然后从截屏的存储位置获得最新的一张图片,并以时间作为该截图的名字(如:20170101083518176.png为2017/01/01/08:35的截图)将图片存储在/sdcard/jrdlog/picture目录下。
实现代码如下:

 /** * 把最新的截图复制一份到指定目录中 * @param context * @param destPath 截图保存目录 * @param time 以时间作为截图的命名 */
    private void savePicture(Context context, String destPath, long time) {
        PictureInfo info = getTheNewestPicture();
        /*将图片以时间为名,并拷贝到/sdcard/jrdlog/picture目录下*/
        FileUtils.copyFileToDir(info.filePath, destPath);
        //以时间重命名图片,方便后面的程序比较
        String oldName = destPath + File.separator + info.fileName;
        String newName = destPath + File.separator + TimeUtils.getTimeString2(time) + ".png";
        FileUtils.reNameTo(oldName, newName);
        //把截图信息更新进数据库
        //根据时间为条件,把截图信息保存进数据库
        ContentResolver contentResolver = context.getContentResolver();
        String selection = LoggerStore.LoggerRecord.EXCEPTION_TIME + " = ? ";
        String[] selectionArgs = {String.valueOf(time)};
        ContentValues values = new ContentValues();
        values.put(LoggerStore.LoggerRecord.SCREEN_SHOT_PATH, newName);
        int updateResult = contentResolver.update(LoggerApplication.LOGGER_RECORDER_URI,values,selection,selectionArgs);
        if (updateResult > 0) {
            LogUtils.getInstance().logd(TAG, "savePicture update ScreenShot successful");
        } else {
            LogUtils.getInstance().loge(TAG, "savePicture update ScreenShot fail");
        }
    }
    /** * 调用系统服务进行 截屏 */
public void takeScreenShot() {
        synchronized (mScreenShotLock) {
            Intent intent = new Intent();
            intent.sejrdassName("com.android.systemui", "com.android.systemui.screenshot.TakeScreenshotService");
            boolean ret = mContext.bindService(intent, mServiceConnection, mContext.BIND_AUTO_CREATE);
        }
    }

3.3 异常识别功能

功能介绍:

有时候手机系统会重复发生同一个异常,为了避免处理相同的异常,JrdLogger需要实现异常识别功能,通过异常识别功能,程序可以将新发生的异常与本地数据库的异常对比,根据对比结果进行不同处理。

实现原理:

不管是从DropBoxManager还是从MtkLogger的db目录获得的Log日志都是比较粗糙的,无法直接作为异常识别的“快照”,需要进行一些加工。通过实践发现,当同一个异常多次发生的时候,Log日志堆栈信息的关键部分都是一致的,根据这个关键部分再加上软件版本和应用包名就能组成一个异常独一无二的“快照”。所以异常识别功能的工作重点就在于怎么从Log日志中提取出堆栈信息中的关键部分,并在取出关键部分之后,通过与软件版本和应用包名以指定格式(软件版本信息—异常包名—关键Log)组成“快照”。

工作流程:

  1. 当DropBoxManager捕获到异常的时候,在程序中处理过后,可以得到一串关于异常的信息。这串信息的格式固定如下:

    Type:Java (JE),Time:20161008091222866,PackageName:com.dropboxtest2.testerror,Tag:data_app_crash,Pid:0000,
    Process: com.dropboxtest2.testerror
    Flags: 0x38e8be46
    Package: com.dropboxtest2.testerror v1 (1.0)
    Build: jrd/5080X/shine_lite:6.0/MRA58K/v2CA6-0:user/release-keys
    java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
    at com.dropboxtest2.testerror.MainActivity.onClick(MainActivity.java:71)
    at android.view.View.performClick(View.java:5265)
    at android.view.View$PerformClick.run(View.java:21534)
    at android.os.Handler.handleCallback(Handler.java:815)
    at android.os.Handler.dispatchMessage(Handler.java:104)
    at android.os.Looper.loop(Looper.java:207)
    at android.app.ActivityThread.main(ActivityThread.java:5774)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
  2. 截取第五行的Build版本信息jrd/5080X/shine_lite:6.0/MRA58K/v2CA6-0:user/release-keys
  3. 截取第四行的Package信息com.dropboxtest2.testerror v1 (1.0)
  4. 截取关键Log,取第六七八九行信息

    java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
    at com.dropboxtest2.testerror.MainActivity.onClick(MainActivity.java:71)
    at android.view.View.performClick(View.java:5265)
    at android.view.View$PerformClick.run(View.java:21534)
  5. 把版本信息、包名信息、关键Log拼接起来,中间用“—”分开,再去掉中间所有空格得到最终的快照信息,这条快照信息可以唯一确定一条异常记录。

    jrd/5080X/shine_lite:6.0/MRA58K/v2CA6-0:user/release-keys---com.dropboxtest2.testerror---java.lang.NullPointerException:Attempttoinvokevirtualmethod'intjava.lang.String.length()'onanullobjectreferenceatcom.dropboxtest2.testerror.MainActivity.onClick(MainActivity.java:71)atandroid.view.View.performClick(View.java:5265)atandroid.view.View$PerformClick.run(View.java:21534)

    具体方法实现:
    java
    public static String getSnapShot(Context context, String content) {
    String result = "";
    String[] arrays = content.split("\\n");
    if (arrays == null) {
    return null;
    }
    //提取异常的Tag,根据Tag区分是哪种异常
    String Tag = getTag(arrays);
    //提取版本信息,包名
    String version = getVersion(arrays);
    String packageName = getPackageName(arrays);
    //String exception = getExceptionType(arrays);
    //提取快照主体部分,根据异常的类型Tag,有不同的提取方式
    String logBody = getLogBody(arrays, packageName, Tag);
    //把version、packageName、exception、logBody拼凑成快照
    StringBuilder SnapShot = new StringBuilder();
    SnapShot = SnapShot.append(version).append("===")
    .append(packageName).append("===")
    .append(logBody);
    //去除结果中所有的空格
    result = SnapShot.toString().replaceAll(" ", "");
    result = result.replaceAll("\\t", "");
    return result;
    }

3.4 异常附件整合功能

功能介绍:

前面异常检测功能只能从DropBoxManager提取简单的异常信息,没有完整的Log日志。为了提高JrdLogger的实用性,为发生的异常提供更加丰富的Log日志附件,更加有利于工程师分析问题,所以增加了异常附件整合功能。

实现原理:

异常附件整合功能即是当异常发生之后,从手机系统原有的Log系统中,提取与异常相关的日志。这个功能依赖于MTK平台的MTKLog,公司手机平台的MiniLog还有Android系统的Bug定位。实现的原理为,当异常捕获的时候,根据异常发生的类型,到对应的Log系统中提取Log到JrdLogger的/sdcard/jrdlog/explog/xxxxxxxxxx/下面,其中的xxxxxxxxxx目录为异常发生时间,以这个时间为目录名,这个异常创建一个存放Log信息的目录,最后搜集Log完成之后,把这个路径下的所有文件压缩打包到/sdcard/jrdlog/ xxxxxxxxxx_jrdlog.zip文件中,这个压缩文件就是这个异常的日志附件了。

异常类型 提取Log的路径 存放Log的路径
reportJE /tctpersist/minilog/app_crash; /sdcard/mtklog/aee_exp /sdcard/jrdlog/explog/mini_log/app_crash; /sdcard/jrdlog/explog/mtk_log/aee_exp
reportNE /data/tombstones /sdcard/jrdlog/explog/android_log/ tombstones
reportKE /tctpersist/minilog/kernel_crash /sdcard/jrdlog/explog/mini_log/ kernel_crash
reportANR /data/anr; /tctpersist/minilog/app_anr /sdcard/jrdlog/explog/android_log/anr; /sdcard/jrdlog/explog/mini_log/ app_anr

工作流程:

  1. 异常发生之后,被异常检测功能检测到
  2. 异常识别功能判定该异常为新发现的异常
  3. 异常处理功能从异常中提取异常的基本信息存进数据库
  4. 启动线程休眠20s等待手机系统生成Log
  5. 20s之后,根据Log类型,搜集手机中的Log到/sdcard/jrdlog/ explog下面的指定路径中,同时清除手机系统中的旧Log
  6. 对/sdcard/jrdlog/explog下面的Log进行压缩,压缩完成后移动到/sdcard/jrdlog/ziplog路径下面,并且更新数据库,写入压缩包名和路径信息

log附件的截取和打包压缩主要有工具类LoggerCollectManager执行的。首先,会根据从DropBoxManager中得到的异常类型,到指定的路径下获取详细的logger信息。

   /** * 搜集与此异常相关的Log,并且压缩成zip文件,作为这个异常的附件。 * * @param logSaveRootPath log保存的根目录 * @param zipSavePath 压缩包保存的路径 * @param time 异常发生时间,用这个时间作为文件名 * @param exceptionType 异常类型 * @param recordUri 异常记录的 */
public void loggerCollect(String logSaveRootPath, String zipSavePath, String time,String exceptionType, Uri recordUri) {
        LogUtils.getInstance().logd(TAG, "loggerCollect(): recordUri=" + recordUri);
        // log文件保存路径
        // eg:/sdcard/jrdlog/explog/time
        String logSavePath = logSaveRootPath + File.separator + time;
        // 压缩文件路径
        // eg:/sdcard/jrdlog/ziplog/time_jrdlog.zip
        String zipFilePath = zipSavePath + File.separator + time + "_jrdlog.zip";
        //新建Log搜集任务
        CollectLogTask task = new CollectLogTask();
        task.setTime(time);
        task.setLogSavePath(logSavePath);
        task.setRecordUri(recordUri);
        task.setExceptionType(exceptionType);
        task.setZipFilePath(zipFilePath);
        //把新任务添加进工作队列
        addToQueue(task);
    }

然后将异常信息通过CollectLogTask类包装成一个个任务,以队列的形式,线性处理处理工作队列中的异常记录,其代码实现如下所示。

/** * 把搜集Log的任务加入工作队列 * * @param collectLogTask */
private void addToQueue(CollectLogTask collectLogTask) {
        //把task加入队列尾部
        mCollectLogTaskQueue.offer(collectLogTask);
        if (mIsCollecting) {
            LogUtils.getInstance().logd(TAG, "addToQueue(): LoggerCollectManager is collecting Log now.It will handle new task after.");
            return;
        } else {
            LogUtils.getInstance().logd(TAG, "addToQueue(): LoggerCollectManager is free now, so handle this task.");
        }
        while (true) {
            mIsCollecting = true;
            //从队列头部取出task
            CollectLogTask task = mCollectLogTaskQueue.poll();
            LogUtils.getInstance().logd(TAG, "Queue poll a new task:" + task);
            if (task != null) {
                /*搜集详细的Log,用于生成附件*/
                collectDetailLog(task);
            } else {
                mIsCollecting = false;
                LogUtils.getInstance().logd(TAG, "Queue is null now. break the cycle.");
                break;
            }
        }
    }
    /** * 搜集详细的Log,用于生成附件 * * @param collectLogTask 工作任务 */
private void collectDetailLog(CollectLogTask collectLogTask) {
        LogUtils.getInstance().logd(TAG, "collectDetailLog()");
        // 休眠,等待miniLog 、mtk db 生成log
        try {
            Thread.sleep(LoggerSystemProp.LOG_COLLECT_DELAY);//休眠,等待Log生成
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 特定的某一个 bug在本地的 uri信息
        Uri recordUri = collectLogTask.getRecordUri();
        String fileName = collectLogTask.getTime();
        String logType = collectLogTask.getExceptionType();
        String logSavePath = collectLogTask.getLogSavePath();
        String zipFileName = collectLogTask.getZipFilePath();
        ContentResolver contentResolver = mContext.getContentResolver();
        Cursor cursor = contentResolver.query(recordUri, null, null, null, null);
        // 因为 recordUri 是针对某一个 bug的uri, 所以从cursor中获取出来的记录最多只有一条
        cursor.moveToNext();
        //判断服务器是否存在这个异常,如果存在,则不继续往下处理
        if (DebugConst.DEBUG_CHECK_REPEAT) {
            /*除重操作*/
            int isRepeat = cursor.getInt(cursor.getColumnIndex(LoggerStore.LoggerRecord.IS_REPEAT));
            if (LoggerStore.LoggerRecord.REPEAT == isRepeat) {
                LogUtils.getInstance().logd(TAG, "Service has been exist this exception, so donot compress it.");
                cursor.close();
                return;
            }
        }
        // eg: logSavePath=/sdcard/jrdlog/explog/time ======>/sdcard/jrdlog/log/log.txt
        copyWorkLogToDir(logSavePath + File.separator + "jrdloger_Work_Log");

        String screenShotPath = cursor.getString(cursor.getColumnIndex(LoggerStore.LoggerRecord.SCREEN_SHOT_PATH));
        cursor.close();
        //拷贝截图
        if (screenShotPath == null || "".equals(screenShotPath)) {
            LogUtils.getInstance().logd(TAG, "screenShotPath is null, so ignore the screen shot !");
        } else {
            if (FileUtils.copyFileToDir(screenShotPath, logSavePath)) {

                LogUtils.getInstance().logd(TAG, "Copy screenShotPath success !");
            } else {
                LogUtils.getInstance().loge(TAG, "Copy screenShotPath fail !");
            }
        }
        //搜集log
        for (int i = 0; i < TCT_LOG_PATH.length; i++) {
            if (TCT_LOG_PATH[i][0].equals(logType)) {
                for (int j = 1; j < TCT_LOG_PATH[i].length; j++) {
                    String logSavePathSub = logSavePath;
                    String logPath = TCT_LOG_PATH[i][j];
                    //根据不同的Log,创建不同的子目录
                    if (MTK_AEE_PATH.equals(logPath)) {
                        logSavePathSub = logSavePathSub + File.separator + "mtk_log";
                    } else if (MINI_JE_PATH.equals(logPath)
                            || MINI_KE_PATH.equals(logPath)
                            || MINI_ANR_PATH.equals(logPath)) {
                        logSavePathSub = logSavePathSub + File.separator + "mini_log";
                    } else if (ANDROID_ANR_PATH.equals(logPath)) {
                        logSavePathSub = logSavePathSub + File.separator + "android_log" + File.separator + "anr";
                    } else if (ANDROID_NE_PATH.equals(logPath)
                            || ANDROID_NE_PATH2.equals(logPath)) {
                        logSavePathSub = logSavePathSub + File.separator + "android_log" + File.separator + "tombstones";
                    }
                    // 如果工作队列为空,则拷贝完Log之后,把源Log删除。
                    // 如果工作队列不为空,为了避免下一个异常没有Log可以拷贝,则拷贝完成之后,不把旧源Log删除。
                    if (mCollectLogTaskQueue.isEmpty()) {
                        LogUtils.getInstance().logd(TAG, "Queue is Empty now, so Move the log to dest file.");
                        if (FileUtils.moveDirTo(logPath, logSavePathSub)) {
                            LogUtils.getInstance().logd(TAG, "Move Successfully: from " + logPath + " => " + logSavePath);
                        } else {
                            LogUtils.getInstance().loge(TAG, "Move Fail : from " + logPath + " => " + logSavePath);
                        }
                    } else {
                        LogUtils.getInstance().logd(TAG, "Queue is Not empty now, so Copy the log to dest file.");
                        if (FileUtils.copyDirTo(logPath, logSavePathSub)) {
                            LogUtils.getInstance().logd(TAG, "Copy Successfully: from " + logPath + " => " + logSavePath);
                        } else {
                            LogUtils.getInstance().loge(TAG, "Copy Fail : from " + logPath + " => " + logSavePath);
                        }
                    }
                }
            }
        }
        // 更新数据库中 zipName 和 zip_path字段
        ContentValues contentValues = new ContentValues();
        contentValues.put(LoggerStore.LoggerRecord.ZIP_NAME, fileName + "_jrdlog.zip");
        contentValues.put(LoggerStore.LoggerRecord.ZIP_PATH, zipFileName);
        int result = contentResolver.update(recordUri, contentValues, null, null);
        if (result > 0) {
            LogUtils.getInstance().logd(TAG, "Database update Success ! zipName:" + fileName + "_jrdlog.zip zipPath:" + zipFileName);
        } else {
            LogUtils.getInstance().loge(TAG, "Database update Fail.zipName and zipPath Not update!");
        }
        // 判断目标压缩文件是否存在,如果存在则删除,避免重复创建压缩文件。
        File zipFile = new File(zipFileName);
        if (zipFile.exists()) {
            zipFile.delete();
        }
        // 压缩Log文件,生成zip压缩包,并且放在指定目录。
        try {
            if (ZipUtil.compressZip(zipFileName, logSavePath)) {
                LogUtils.getInstance().logd(TAG, "Compress Zip Success! Form:" + logSavePath + " to " + zipFileName);
            } else {
                LogUtils.getInstance().logd(TAG, "Compress Zip Fail! Form:" + logSavePath + " to " + zipFileName);
            }
            FileUtils.delDir(new File(logSavePath));   //压缩完成之后,删除Log
            uploadZipToServier();                      //上传异常附件到服务器
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

3.5 手动复现

功能介绍:

JrdLogger工具在运行在系统后台,在异常出现的时候虽然可以自动抓取Log,然后打包上传到服务器,对工程是分析手机系统的漏洞帮助非常大,但是也有弊端,那就是上传的Log比较简单,没有具体的复现流程,工程师无法从Log中快速定位到问题的所在,所以新增加手动复现流程作为补充。手动复现指的是,在用户选择开始手动复现之后,JrdLogger会同时进行Log抓取和屏幕录制的工作,把用户整个操作录制下来,最后把Log和视频打包上传到服务器的过程。用户可以通过两种方式来手动复现操作,第一种,进入Jrdlogger应用,点击Record界面的录制按钮即可启动手动复现功能;第二种,在JrdLogger捕获到系统异常的时候会弹出提示询问是否进行手动录制,选择手动录制即可启动手动复现流程。

实现原理:

手动复现功能使用到了录屏功能,这个功能调用了shine plus项目中的录屏方案。JrdLogger直接使用了shine plus中的libscreenrecord.so库,然后在代码中调用

工作流程:

手动复现流程有两种启动方式,以下流程图分别介绍两种启动方式的工作流程.

  • 方式一:用户打开JrdLogger应用,点击Record按钮来复现自己的问题,并且上传到服务器,录制流程如下图

这里写图片描述

  • 方式二:JrdLogger工具检测到异常,弹出提示让用户进行手动复现,录制流程如下图。

这里写图片描述

进行手动复现主要是将整个操作通过录屏方式记录下来,然后再将录屏文件上传到到服务器中,其实现由服务类ScreenRecordService来处理。当点击录屏操作时,会首先将异常监听服务暂停掉,然后清空mtklog目录,之后再开启mtklog以便可以在录屏时得到对应的log信息。

 /** * 启动录屏 * * @param fileName * @throws IOException */
    public void startRecording(String fileName) throws IOException {
        Log.d(TAG, "startRecording()");
        DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
        Display defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
        if (defaultDisplay == null) {
            Log.e(TAG, "startRecording : defaultDisplay == null");
            throw new RuntimeException("No display found.");
        }
        //准备
        prepareVideoEncoder();
        File file = new File(fileName);
        if (file.exists()) {
            file.delete();
        }
        try {
            mMuxer = new MediaMuxer(fileName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        } catch (IOException ioe) {
            Log.e(TAG, "startRecording : new MediaMuxer happen IOException.");
            throw new RuntimeException("MediaMuxer creation failed", ioe);
        }
        mMediaProjection.createVirtualDisplay("Recording Display", mWidth,
                mHeight, mDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mInputSurface, null, null);
        // Start the encoders
        drainEncoder();
    }

3.6 日志上传

功能介绍:

JrdLogger检测到异常之后,需要把处理之后的日志上传到服务器,日志上传涉及到客户端与服务器的交互还有客户端本地流程的设计,为了提高日志上传的效率,减少服务器重复日志的数量,所以设计了日志上传功能的方案。

工作流程:

  • 上传日志过程中客户端与服务器交互过程的流程如下图所示:

    1. 客户端检测到本地数据库改变,向服务器发送字段信息
    2. 服务端根据发过来的快照信息判断该日志是否为重复
    3. 如果信息重复,从数据库获取重复的ID并将其count+1;
    4. 如果不重复新建一条数据,并返回ID
    5. 根据服务返回的信息判定是否重复,如果重复根据快照信息count+1,结束
    6. 如果不重复,上传附件
    7. 服务器返回是否成功信息
    8. 不成功返回继续心跳包上传
    9. 成功,返回附件的路径,结束

这里写图片描述

  • 上传日志过程中客户端本地流程如下图所示:

    1. 本地侦测到数据有变化,获取到新增的那条数据信息
    2. 网络判断
    3. 将信息上传到服务器,等待服务器返回数据
    4. 服务器返回重复新信息,count+1,删除本地文件,结束
    5. 服务器返回快照信息不存在,获取返回的ID
    6. 根据返回的ID信息上传附件
    7. 等待服务器返回上传结果,不成功采用心跳包的形式继续上传
    8. 上传成功,删除本地文件,结束

这里写图片描述

3.7 应用设置功能

功能介绍:

JrdLogger虽然是一个自动运行的应用,但是如果可以按照用户的需求进行配置,将会更加实用,以下为应用设置功能。

  • 是否开启JrdLogger功能开关(默认开启)
  • 是否只在WIFI模式下上传Log到服务器(默认开启)
  • 当异常发生时,是否提示用户是进行手动录屏(默认开启)
  • 用户设置联系方式(一般为邮箱,允许用户不设置)
  • 使用帮助,用户可以通过这一项来了解JrdLogger使用的方法。
  • 录屏时候是否开启屏幕触摸点位置开关
  • 用户可以在本地查看发生异常的列表信息

3.8 异常记录管理功能

功能介绍:

JrdLogger工具在系统运行出现异常的时候虽然可以自动抓去Log然后上传到服务器,但是Log上传之后,用户就无法继续跟踪问题了,所以为了进一步提高工具的实用性,增加了异常记录管理功能,包括异常记录列表异常记录详情界面,通过这两个界面,用户可以针对其中一条异常记录进行操作。

  1. 异常记录列表

    • 异常记录列表通过从数据库查询JrdLogger处理过的异常记录信息,然后一条条显示出来, 异常记录列表只是显示简单的异常信息,比如说异常发生时间、异常的类型、Log是否上传到服务器等等,此外异常记录列表可以提供排序和筛选功能,方便用户定位异常信息。通过这个列表入口,用户可以方便的定位到自己想要的异常信息,然后单击条目,进入异常记录详情界面,进行后续操作。
    • 异常记录列表的实现方式为安卓典型的MVC设计模式,数据库为modle,显示界面为view,适配器为control.这样实现可以使数据和界面分离,方便后期修改和维护
      这里写图片描述
    • 在异常记录列表中需要显示的信息有:异常应用名,异常发生时间,异常类型,异常附件是否上传。
  2. 异常记录详情界面

    • 异常记录列表只是负责显示简单的异常记录信息,无法把所有的信息都显示完整,也无法把所有操作集成在一条列表上面,所以需要设计一个异常记录的详细界面来显示这些异常记录的当前的详细信息,比如说可以把异常的截图,异常发生时间,异常发生的包名和异常生成的打包文件名。而且在这个界面可以为这条异常记录添加Comment,比如异常的复现概率,异常的复现步骤,进一步帮助工程师分析问题解决问题。
    • 在异常记录详情界面中需要显示的信息有:异常应用名、异常发生时间、异常类型、异常附件是否上传、是否追加评论、异常压缩包的包名、异常压缩包时间、发生异常的版本号。

3.9 追加评论功能

功能介绍:

由于Logger捕获到异常是自动提交到服务器的,用户无法把发生问题的想法也反馈上去,于是设计了追加异常信息的功能。这个功能允许用户在异常发生之后,对这个异常发表自己的看法,或者向服务器提供具体的复现步骤,给解决问题的工程师提供帮助。

工作流程:

  1. 进入JrdLogger应用,打开Record界面,在异常记录列表中找到想要添加记录。
  2. 点击列表的条目,进入异常记录详情界面。
  3. 点击Add Comment按钮,然后输入comment内容,点击提交。
  4. 调用追加评论接口,把服务器返回的id 和comment的内容提交上去。
  5. 判断网络环境,等待WiFi环境上传到服务器。

这里写图片描述

上一篇:springmvc 异步请求(json + ajax + jquery) 下一篇:Android发送短信