如何写一个日志框架

整体功能设计

  • 1、支持控制台打印
  • 2、支持缓存到文件,支持日志文件上传
  • 3、支持接入APM性能监控

需要考虑的细节有哪些?

  • 1、日志文件 和 APM 的区别是什么?APM 是通过上传部分关键日志,使得开发人员可以查询用户APP 的上传每条日志,进行问题排查;而日志文件是存在用户设备上的,一般情况下不会上传,只有需要的时候,通过开关设置,来触发指定名单用户的日志上传,用来获取更多信息来排查问题。
  • 2、写入日志文件的策略是什么?内存中日志最大数目是多少?什么时候触发写入文件?日志文件多大?线程怎么管理?最多保存多少个日志文件?什么时候触发日志上传?
  • 3、APM性能监控怎么处理每条日志?

二、开始开发

1.提供外部调用接口

对于 Verbose 日志,一般没什么有用信息,可以只进行控制台输出。(Debug 日志也可以一样处理)

代码如下:

public class LogUtil {
    private static String TAG = "LogUtil";
	
	/**
     * 对外 v 日志接口
     * 只进行控制台输出
     */
    public static void v(String tag, String msg) {
        printLog(Log.VERBOSE, tag, msg);
    }

    private static void printLog(int logLevel, String TAG, String msg) {
        printLog(logLevel, TAG, msg);
    }

    /**
     * 将日志输入到控制台
     * 可以在这里对控制台的日志输出进行通用性的控制
     */
    private static void printLog(int logLevel, String tag, String msg, Throwable e) {
        if (TextUtils.isEmpty(msg)) {
            return;
        }
        if (tag == null) {
            tag = "";
        }
        // 只有在测试环境下才会将日志输出到控制台
        if (isDebug()) {
            StringBuffer stringBuffer = new StringBuffer();
            // 添加主线程标志
            if (Looper.myLooper() == Looper.getMainLooper() && TextUtils.equals(Thread.currentThread().getName(), "main")) {
                stringBuffer.append("[main]");
            }
            if (!TextUtils.isEmpty(tag)) {
                stringBuffer.append(tag).append(" ");
            }
            stringBuffer.append(msg);
            if (logLevel == Log.VERBOSE) {
                stringBuffer.insert(0, "[V]");
                String msgInfo = stringBuffer.toString();
                Log.i(TAG, msgInfo);
            } else if (logLevel == Log.DEBUG) {
                stringBuffer.insert(0, "[D]");
                String msgInfo = stringBuffer.toString();
                Log.d(TAG, msgInfo);
            } else if (logLevel == Log.INFO) {
                stringBuffer.insert(0, "[I]");
                String msgInfo = stringBuffer.toString();
                Log.w(TAG, msgInfo);
            } else if (logLevel == Log.WARN) {
                stringBuffer.insert(0, "[W]");
                String msgInfo = stringBuffer.toString();
                Log.w(TAG, msgInfo);
            } else if (logLevel == Log.ERROR) {
                stringBuffer.insert(0, "[E]");
                String msgInfo = stringBuffer.toString();
                Log.e(TAG, msgInfo);
            } else {
                stringBuffer.insert(0, "[I]");
                String msgInfo = stringBuffer.toString();
                Log.i(TAG, msgInfo);
            }
        }
    }

    private static boolean isDebug() {
        return false;
    }

}

对于 INFO 及以上等级的日志,我们需要考虑保存到日志文件中

public class LogUtil {
    private static String TAG = "LogUtil";
    
    public static void i(String tag, String msg) {
        printAndSaveLog(Log.INFO, tag, msg);
    }

    private static void printAndSaveLog(int logLevel, String tag, String msg) {
        String messageInfo = "";
        printLog(logLevel, tag, msg);
        saveLog(tag, logLevel, msg, false);
    }

    /**
     * 保存日志到文件
     * @param isNowSave 是否马上保存
     */
    private static void saveLog(String tag, int logLevel, String msgInfo, boolean isNowSave) {
        if (TextUtils.isEmpty(msgInfo)) {
            return;
        }
        msgInfo = "[" + tag + "]" + msgInfo;
        if (isNowSave) {
            LogWriterProxy.writeLogNow(logLevel, msgInfo);
        } else {
            LogWriterProxy.writeLog(logLevel, msgInfo);
        }
    }

}

写日志到文件

LogWriterProxy 是一个代理类,实际真正的操作交给实现了 ILogWriter 接口的对象去做。

public class LogWriterProxy {
    private static ILogWriter mLogWriter = null;

    public static void setLogWriter(ILogWriter logWriter) {
        mLogWriter = logWriter;
    }

    /**
     * 立即保存所有日志
     * 一般用于 Crash 日志保存
     */
    public static void writeLogNow(int logLevel, String msgInfo) {
        if (mLogWriter != null) {
            mLogWriter.writeLogNow(logLevel, msgInfo);
        }
    }

	/**
     * 一般普通日志保存都不是即时的
     */	
    public static void writeLog(int logLevel, String msgInfo) {
        if (mLogWriter != null) {
            mLogWriter.writeLog(logLevel, msgInfo);
        }
    }
	
	public static void release() {
        if (mLogWriter != null) {
            mLogWriter.release();
        }
    }
}

public interface ILogWriter {
    void writeLogNow(int logLevel, String msgInfo);

    void writeLog(int logLevel, String msgInfo);

    void release();
}

定义一个 LogWriter 类实现 ILogWriter

public class LogWriter implements  ILogWriter{
    private static volatile LogWriter mInstance = null;
    
    private IWriteStrategy mWriteStrategy;
    
    public static LogWriter getInstance() {
        if (mInstance == null) {
            synchronized (LogWriter.class) {
                if (mInstance == null) {
                    mInstance = new LogWriter();
                }
            }
        }
        return mInstance;
    }
    
    private LogWriter() {
        mWriteStrategy = new JavaWriteStrategy();    
        mWriteStrategy.initFilePath(LogPath.CACHE_LOG_FILE_PATH);
    }
    
    
    @Override
    public void writeLogNow(int logLevel, String msgInfo) {
        writeLog(logLevel, msgInfo, true);
    }

    @Override
    public void writeLog(int logLevel, String msgInfo) {
        writeLog(logLevel, msgInfo, false);
    }
    
    private void writeLog(int logLevel, String msgInfo, boolean isNow) {
        if (TextUtils.isEmpty(msgInfo) || msgInfo.length() > 5000) {
            return;
        }
        
        LogLineInfo logLineInfo = new LogLineInfo.Builder()
                .setLevel(logLevel)
                .setTime(System.currentTimeMillis())
                .setInfo(msgInfo)
                .build();
        write(isNow, logLineInfo);
    }

    private void write(boolean isNow, LogLineInfo logLineInfo) {
        mWriteStrategy.write(isNow, logLineInfo);
    }

    @Override
    public void release() {
        mWriteStrategy.release();
    }
}

定义一个 JavaWriteStrategy 实现 IWritesStrategy。

public class JavaWriteStrategy implements IWriteStrategy {

    private HandlerThread mHandlerThread = null;
    private Handler mHandler = null;
    private String mLogFileDirPath = ""; // 日志文件目录
    private ConcurrentLinkedQueue<LogLineInfo> mLogList = null;
    private Object mWriteLockObj = new Object();

    /**
     * 内存中日志最大数目
     */
    private static final int MAX_RAM_LOG_COUNT = 20;

    /**
     * 内存中日志检查时间间隔
     */
    private static final int MAX_CHECK_GAP_TIME = 5 * 1000;
    private static final int MSG_CHECK_WRITE_LOG_ACTION = 0x1001;
    private static final int MSG_NO_CHECK_WRITE_LOG_ACTION = 0x1002;

    public JavaWriteStrategy() {
        // 初始化工作线程和 handler
        mHandlerThread = new HandlerThread("LogWriterThread");
        mHandlerThread.start();
        mHandler = new WriteLogHandler(mHandlerThread.getLooper());
    }

    @Override
    public void initFilePath(String logFileDirPath) {
        this.mLogFileDirPath = logFileDirPath;
    }

    @Override
    public void write(boolean isNow, LogLineInfo logLineInfo) {
        if (isNow) {
            // 直接在主线程触发写入磁盘操作
            synchronized (mWriteLockObj) {
                ConcurrentLinkedQueue<LogLineInfo> logList = null;
                if (mLogList != null && mLogList.size() > 0) {
                    logList = new ConcurrentLinkedQueue<>(mLogList);
                    mLogList.clear();
                } else {
                    logList = new ConcurrentLinkedQueue<>();
                }
                logList.add(logLineInfo);
                writeLog(logList);
            }
        } else {
            // 检查内存日志数量后,满足条件再触发写入磁盘操作
            if (mHandler == null || !mHandlerThread.isAlive()) {
                return;
            }
            Message msg = mHandler.obtainMessage();
            msg.what = MSG_CHECK_WRITE_LOG_ACTION;
            msg.obj = logLineInfo;
            mHandler.sendMessage(msg);
        }
    }


    @Override
    public void release() {

    }

    private class WriteLogHandler extends Handler {
        public WriteLogHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            int what = msg.what;
            Object object = msg.obj;
            mHandler.removeMessages(MSG_NO_CHECK_WRITE_LOG_ACTION);
            // 检查内存日志条数,满足条件就写入磁盘
            if (what == MSG_CHECK_WRITE_LOG_ACTION) {
                synchronized (mWriteLockObj) {
                    if (mLogList == null) {
                        mLogList = new ConcurrentLinkedQueue<>();
                    }
                    if (object instanceof LogLineInfo) {
                        mLogList.add((LogLineInfo) object);
                    }
                    int curLogSize = mLogList.size();
                    if (curLogSize >= MAX_RAM_LOG_COUNT) {
                        ConcurrentLinkedQueue<LogLineInfo> logList = new ConcurrentLinkedQueue<>(mLogList);
                        mLogList.clear();
                        writeLog(logList);
                    } else if (curLogSize > 0) {
                        // 延时5秒后,不检查内存日志条数,直接写入磁盘
                        mHandler.sendEmptyMessageDelayed(MSG_NO_CHECK_WRITE_LOG_ACTION, MAX_CHECK_GAP_TIME);
                    }
                }
            } else if (what == MSG_NO_CHECK_WRITE_LOG_ACTION) {
                // 不检查内存日志条数,直接写入磁盘
                synchronized (mWriteLockObj) {
                    if (mLogList != null && mLogList.size() > 0) {
                        ConcurrentLinkedQueue<LogLineInfo> logList = new ConcurrentLinkedQueue<>(mLogList);
                        mLogList.clear();
                        writeLog(logList);
                    }
                }
            }
        }
    }

    private void writeLog(ConcurrentLinkedQueue<LogLineInfo> logList) {
        FileWriter fileWriter = null;
        try {
            File writePathFile = getLogDirFile();
            if (writePathFile != null && !writePathFile.exists()) {
                writePathFile.mkdirs();
            }
            File writeLogFile = new File(writePathFile, LogPath.LOG_NORMAL_FILE_NAME);
            if (writeLogFile != null && !writeLogFile.exists()) {
                writeLogFile.createNewFile();
            }
            boolean reNameSuccess = true;
            long fileSize = writeLogFile.length();
            // 如果文件大于 5兆,则需要重新起一个新的文件来写
            if (fileSize >= 5 * 1024 * 1024) {
                // 根据系统_系统版本号_应用版本号_设备型号_设备id_uid_上报时间 来生产新的文件名
                String logFileName = generateNewLogFileName();
                if (!TextUtils.isEmpty(logFileName)) {
                    reNameSuccess = writeLogFile.renameTo(new File(writePathFile, logFileName));
                }
            }
            if (reNameSuccess) {
                // 只保留最近修改的两个日志文件,其他删除
                deleteRedundantFile();
                writeLogFile = new File(writePathFile, LogPath.LOG_NORMAL_FILE_NAME);
                if (writeLogFile != null && !writeLogFile.exists()) {
                    writeLogFile.createNewFile();
                }
            } else {
                return;
            }
            if (fileWriter == null) {
                fileWriter = new FileWriter(writeLogFile, true);
            }
        } catch (FileNotFoundException e) {

        } catch (IOException e) {
            e.printStackTrace();
        }
        if (fileWriter != null) {
            try {
                for (LogLineInfo logLineInfo: logList) {
                    fileWriter.append(logLineInfo.getMessage()).append("\n");
                }
                fileWriter.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private File getLogDirFile() {
        File logDirFile = new File(mLogFileDirPath);
        return logDirFile;
    }
}

日志上传

可以通过开关设置,把保存的最多两个日志文件压缩后上传

接入 APM 性能监控

我们可以修改 LogUtil 的 saveLog 方法,通过观察者模式,将日志信息上传到 APM。
触发时机:启动上传、每次新增日志时检查数据库日志数量
日志上传后成功后,根据 id 删除本地数据库冗余数据。

你可能感兴趣的:(android,android,日志框架)