react-native 日志工具研究

438 阅读11分钟

需求

  • 日志打印记录时间
  • 日志打印包含 TAG
  • 最多保存 n 个日志文件,每个日志文件限制最大占用空间
  • 当日志文件满后,新建日志文件时删除最早的日志文件(文件滚动更新)
  • 先将日志写入缓存中,每隔固定时间间隔,将缓存中的日志写入到文件中(避免频繁文件读写)
  • 同时打印原生日志和 JavaScript 日志

介绍

react-native-turbo-log

image-20250109095929385

安装

yarn add @mattermost/react-native-turbo-log
npx react-native link @mattermost/react-native-turbo-log
npx pod-install

配置

从 0.5 版本有重大变更,原生的日志同样会添加到文件中,这意味着需要在原生代码中完成配置。

import com.mattermost.turbolog.TurboLog
import com.mattermost.turbolog.ConfigureOptions
​
class MainApplication : NavigationApplication(), INotificationsApplication {
    // ...
    override fun onCreate() {
        // ...
        TurboLog.configure(options = ConfigureOptions())
        TurboLog.i("Label", "Text")
    }
}

在应用程序创建时配置应用程序。在本机端使用TurboLog.X而不是Log.X来让日志显示在文件中。

import TurboLogger from "@mattermost/react-native-turbo-log";
​
await TurboLogger.configure();
​
// ...

在完成 TurboLogger 配置后,你的 app 将会把日志存储到文件系统中,如果未指定,你的 consoledebugloginfowarnerror都会被TurboLogger捕获,并且他们都会被写入到日志文件中。

你也可以配置 TurboLogger 来自定义dailyRolling、调用console拦截或启用/禁用文件系统的日志记录。

API

configure

TurboLogger.configure(options?: ConfigureOptions) => Promise

从0.5开始,必须在原生代码中进行配置,并且可以在 JavaScript 代码进行额外的配置。

使用指定的选项初始化TurboLogger,一旦配置的 Promise 解决,所有console调用都会被捕获并写入日志文件。为了确保没有日志丢失,最好在应用程序启动时等待此调用。

选项描述默认值设置位置
logLevel文件输出的最低日志级别(不会影响控制台输出LogLevel.DebugNative
captureConsole如果为true,所有console调用都会自动捕获并写入日志文件trueJavaScript
dailyRolling如果为true,则每天都会创建一个新的日志文件falseNative
maximumFileSize当前日志文件超过给定的字节大小时,将创建一个新的日志文件。将其设置为0以禁用1024 * 1024 (1MB)Native
maximumNumberOfFiles要保留的最大日志文件数。创建新日志文件时,如果文件总数超过此限制,则删除最旧的文件。设置0以禁用5Native
logsDirectory存储日志文件的目录的绝对路径。如果未定义,日志文件将存储在应用程序的缓存目录中undefinedNative
logToFile如果为true,则创建日志文件并将其写入文件系统。也可以通过调用setLogToConsole方法来更改它trueNative
deleteLogs

TurboLogger.deleteLogs(): Promise

从文件系统中删除所有应用程序日志文件。

getLogPaths

TurboLogger.getLogPaths(): Promise<string[]>

返回所有日志文件的绝对路径。

setLogToFile

TurboLogger.setLogToFile(enabled: boolean)

启用或禁用将消息记录到文件

debug

TurboLogger.log(LogLevel.Debug, ...args)的快捷方式

info

TurboLogger.log(LogLevel.Info, ...args)的快捷方式

warn

TurboLogger.log(LogLevel.Warning, ...args)的快捷方式

error

TurboLogger.log(LogLevel.Error, ...args)的快捷方式

log

使用与console.log相同的格式将日志消息附加到指定级别。 重要提示:日志格式不支持字符串替换.

console.log("User %s %s is %d years old.","Iain","Freestone",39);       // 不支持

源码分析

import {
  configure as configureNative,
  deleteLogFiles,
  getLogFilePaths,
  write,
} from './native';
​
class TurboLoggerStatic {private logToFile = false;
​
  async configure(options: ConfigureOptions = {}): Promise<void> {
    // 是否将日志保存到文件中
    private logToFile = false;
      
    const { 
      captureConsole = true, 
      logToFile = true 
    } = options;
​
    await configureNative(options);
​
    this.logToFile = logToFile;
​
    // 如果捕获 console,在非调试环境下,使用 this.xxx 替代 console.xxx
    if (captureConsole) {
      const c = {
        ...global.console,
      };
​
      global.console.debug = (...args) => {
        this.log(LogLevel.Debug, ...args);
        if (__DEV__) {
          c.debug(...args);
        }
      };
​
      global.console.log = (...args) => {
        this.log(LogLevel.Info, ...args);
        if (__DEV__) {
          c.log(...args);
        }
      };
​
      global.console.info = (...args) => {
        this.log(LogLevel.Info, ...args);
        if (__DEV__) {
          c.info(...args);
        }
      };
​
      global.console.warn = (...args) => {
        this.log(LogLevel.Warning, ...args);
        if (__DEV__) {
          c.warn(...args);
        }
      };
​
      global.console.error = (...args) => {
        this.log(LogLevel.Error, ...args);
        if (__DEV__) {
          c.error(...args);
        }
      };
    }
  }
​
  async deleteLogs(): Promise<boolean> {
    return deleteLogFiles();
  }
​
  async getLogPaths(): Promise<string[]> {
    return getLogFilePaths();
  }
​
  setLogToFile(enabled: boolean) {
    this.logToFile = enabled;
  }
​
  // 调用 this.log 方法 
  debug(...args: any) {
    this.log(LogLevel.Debug, ...args);
  }
​
  info(...args: any) {
    this.log(LogLevel.Info, ...args);
  }
​
  warn(...args: any) {
    this.log(LogLevel.Warning, ...args);
  }
​
  error(...args: any) {
    this.log(LogLevel.Error, ...args);
  }
​
  log(level: LogLevel, ...args: any) {
    // 如果日志保存到文件,调用 write 方法
    if (this.logToFile) {
      write(level, args);
    }
  }
}
// native.tsimport type { ConfigureOptions } from './types';
​
const RNTurboLog = require('./NativeRNTurboLog').default;
​
export function configure(options: ConfigureOptions = {}): Promise<void> {
  const opts = {
    dailyRolling: options.dailyRolling ?? false,
    // 最大支持日志文件尺寸,默认值是 1MB
    maximumFileSize: options.maximumFileSize ?? 1024 * 1024,
    // 最大支持日志文件个数,默认值是 5 个
    maximumNumberOfFiles: options.maximumNumberOfFiles ?? 5,
    logsDirectory: options.logsDirectory,
  };
​
  return RNTurboLog.configure(opts);
}
​
export function deleteLogFiles(): Promise<boolean> {
  return RNTurboLog.deleteLogFiles();
}
​
export function getLogFilePaths(): Promise<string[]> {
  return RNTurboLog.getLogFilePaths();
}
​
// 调用 RNTurboLog 的 write 方法
export function write(logLevel: number, message: Array<any>) {
  RNTurboLog.write(logLevel, message);
}
​
// RNTurboLogModuleImpl.ktclass RNTurboLogModuleImpl {
  
  // 调用 TurboLog 的 d、i、w、e 等方法
  fun write(level: Int, messages: ReadableArray?) {
    val str: String = format(messages)
    when (level) {
      LOG_LEVEL_DEBUG -> {
        TurboLog.d(TAG, str)
      }
​
      LOG_LEVEL_INFO -> {
        TurboLog.i(TAG, str)
      }
​
      LOG_LEVEL_WARNING -> {
        TurboLog.w(TAG, str)
      }
​
      LOG_LEVEL_ERROR -> {
        TurboLog.e(TAG, str)
      }
    }
  }
​
  // 删除日志方法,**ps:这个方法有问题,删除成功后,没有解决期约**
  fun deleteLogFiles(promise: Promise?) {
    try {
      for (file in getLogFiles()) {
        file.delete()
      }
      TurboLog.reconfigure()
    } catch (e: java.lang.Exception) {
      promise?.resolve(false)
    }
  }
}
// TurboLog.ktclass TurboLog {
    
    companion object {
      fun configure(options: ConfigureOptions) {
        // ...
          
        val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
        // 使用 RollingFileAppender,RollingFileAppender 具有文件滚动更新机制,当文件达到了指定的大小,就重命名原日志文件进行归档,并生成新的日志文件用于log写入。如果还设置了一定时间内允许归档的日志文件的最大数量,将对过旧的日志文件进行删除操作。
        val rollingFileAppender = RollingFileAppender<ILoggingEvent>()
        rollingFileAppender.context = loggerContext
        rollingFileAppender.file = "$logsDirectory/$logPrefix-latest.log"
      }
    }
    
    fun d(tag: String, message: String) {
      if (configureOptions != null) {
        logger.debug("$tag: $message")
      } else {
        Log.w("TurboLog", "Used before configured")
      }
      Log.d(tag, message)
    }
​
    fun e(tag: String, message: String) {
      if (configureOptions != null) {
        logger.error("$tag: $message")
      } else {
        Log.w("TurboLog", "Used before configured")
      }
      Log.e(tag, message)
    }
​
    fun i(tag: String, message: String) {
      if (configureOptions != null) {
        logger.info("$tag: $message")
      } else {
        Log.w("TurboLog", "Used before configured")
      }
      Log.i(tag, message)
    }
​
    fun w(tag: String, message: String) {
      if (configureOptions != null) {
        logger.warn("$tag: $message")
      } else {
        Log.w("TurboLog", "Used before configured")
      }
      Log.w(tag, message)
    }
}
// RNTurboLog.mm

-(void)setConfig:(NSDictionary *)options withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject {
   	// ...
    fileManager.maximumNumberOfLogFiles = [maximumNumberOfFiles unsignedIntegerValue];
    
    DDFileLogger* fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];
    fileLogger.logFormatter = [[TurboLoggerFormatter alloc] init];
    // 同样具有文件滚动更新机制
    fileLogger.rollingFrequency = [dailyRolling boolValue] ? 24 * 60 * 60 : 0;
    fileLogger.maximumFileSize = [maximumFileSize unsignedIntegerValue];
    [DDLog removeAllLoggers];
    [DDLog addLogger:fileLogger];
    NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
    [DDLog addLogger:[[DDOSLogger alloc] initWithSubsystem:bundleIdentifier category:@"TurboLogger"]];
    self.fileLogger = fileLogger;
}

- (void)write:(NSInteger)logLevel message:(NSArray *)message { 
    NSString *str =  [self format:message];
    switch (logLevel) {
        case LOG_LEVEL_DEBUG:
            DDLogDebug(@"%@", str);
            break;
        case LOG_LEVEL_INFO:
            DDLogInfo(@"%@", str);
            break;
        case LOG_LEVEL_WARNING:
            DDLogWarn(@"%@", str);
            break;
        case LOG_LEVEL_ERROR:
            DDLogError(@"%@", str);
            break;
    }
}

总结

这个库使用了RollingFileAppenderDDFileLogger来保存日志,优点是实现了最大文件个数文件大小限制,并且支持文件滚动更新支持原生日志打印,支持拦截console。缺点是在每次打印日志,都会进行一次文件写入操作,可能有性能问题。

react-native-logs

image-20250109100005012

安装

yarn add react-native-logs

快速开始

import { logger } from "react-native-logs";

const log = logger.createLogger();

log.debug("This is a Debug log");
log.info("This is an Info log");
log.warn("This is a Warning log");
log.error("This is an Error log");

配置

参数类型描述默认值
severitystring初始化日志严重性(您想要看到的最不重要的级别)debug(或第一个自定义级别)
transportfunction or [function]日志的传输方法(请参阅下面的预设)The preset transport consoleTransport
transportOptionsObject为传输设置的自定义选项null
levelsObject设置自定义日志级别:{name:power}/
asyncboolean将异步日志设置为 true(以提高应用程序性能)true
asyncFuncfunction设置自定义异步函数(cb: Function) => {return cb()}setTimeout
stringifyFuncfunction设置自定义字符串化函数(msg: any) => string自定义JSON.stringify
formatFuncfunction设置自定义格式函数(level: string, extension?: string, msg: any) => string默认字符串格式化函数
dateFormatstring or functiontimelocalutciso或者(date: Date) => stringtime
printLevelboolean选择是否打印日志级别true
printDateboolean选择是否打印日志日期/时间true
fixedExtLvlLengthboolean打印扩展和级别时确保字符数一致false
enabledboolean启用或禁用日志记录true
enabledExtensionsstring[]仅启用某些命名空间null

自定义级别

日志级别具有以下格式:{ name : severity }并且您可以创建个性化列表,例如:

import { logger } from "react-native-logs";

const log = logger.createLogger({
  levels: {
    trace: 0,
    info: 1,
    silly: 2,
    error: 3,
    mad: 4,
  },
});

log.silly("Silly message");

级别输入

(仅当你使用 TypeScript 时才可用)

该包将直接从配置中获取日志级别的类型,如果未指定,则采用默认日志级别。

import { logger } from "react-native-logs";

const log = logger.createLogger({
  levels: {
    trace: 0,
    info: 1,
    error: 2,
  },
});

log.trace("message"); // correct log call

log.silly("message"); // typescript error, "silly" method does not exist

自定义传输方法

您可以编写自己的传输程序,将日志发送到云服务、将其保存到数据库或执行任何您想做的事情。该函数接收以下参数:

  • msg: any:记录器格式化的消息“[time] | [namespace] | [level] | [msg]”
  • rawMsg: any:原始形式的消息(或消息数组)
  • level: { severity: number; text: string }:日志级别
  • extension?: string | null:如果是扩展日志,则为它的命名空间
  • options?: any:transportOptions 对象
import { logger, transportFunctionType } from "react-native-logs";

const customTransport: transportFunctionType<{ myCustomOption: string }> = (
  props
) => {
  // Do here whatever you want with the log message
  // You can use any options setted in config.transportOptions
  // Eg. a console log: console.log(props.level.text, props.msg)
};

const log = logger.createLogger({
  transport: customTransport,
  transportOptions: {
    myCustomOption: "option",
  },
});

log.debug("Debug message");

传输选项

通过设置transportOptions参数,您可以插入将传递给传输的选项。对于某些传输,这些选项可能是强制性的,例如(有关详细信息,请参阅预设传输FS列表fileAsyncTransport

import { logger, fileAsyncTransport } from "react-native-logs";
import RNFS from "react-native-fs";

const log = logger.createLogger({
  transport: fileAsyncTransport,
  transportOptions: {
    FS: RNFS,
    fileName: `log.txt`,
  },
});

log.debug("Debug message");

多个参数

可以通过向日志函数添加多个参数来连接日志消息:

const errorObject = {
  staus: 404,
  message: "Undefined Error",
};
log.error("New error occured", errorObject);

预设传输方法

react-native-logs 包含一些预设的传输方式。你可以导入你选择的一种: import { logger, <transportName> } from 'react-native-logs';

import { logger, mapConsoleTransport } from "react-native-logs";

const log = logger.createLogger({
  transport: mapConsoleTransport,
});

log.debug("Debug message");

预设传输方法列表

consoleTransport

以格式化的输出打印日志console.log

如果您需要使用不同的控制台或方法,console.log则可以consoleFunc使用自定义控制台设置选项。

名称类型描述默认值
colorsobject如果设置的话,您可以选择日志颜色,由级别定义:{level:color}null
extensionColorsobject如果设置的话您可以选择扩展标签颜色:{extension:color}null
consoleFunc(msg:any) => any如果设置的话,你可以选择控制台对象null
import { logger, consoleTransport } from "react-native-logs";

const log = logger.createLogger({
  levels: {
    debug: 0,
    info: 1,
    warn: 2,
    error: 3,
  },
  transport: consoleTransport,
  transportOptions: {
    colors: {
      info: "blueBright",
      warn: "yellowBright",
      error: "redBright",
    },
    extensionColors: {
      root: "magenta",
      home: "green",
    },
  },
});

const rootLog = log.extend("root");
const homeLog = log.extend("home");

rootLog.info("Magenta Extension and bright blue message");
homeLog.error("Green Extension and bright red message");
fileAsyncTransport

需要安装react-native-fsexpo-file-system,允许您将日志保存在<filePath>/<fileName>.txt文件中

如果您希望每天创建一个新文件,您可以{date-today}在 fileName: app_logs_{date-today}.log->中使用app_logs_D-M-YYYY.log

名称类型描述默认值
FSobject文件系统实例(RNFS 或 expo FileSystem)null
fileNamestring设置日志文件名(插入{date-today}当前日期)/
fileNameDateTypeeu,us,iso{date-today}日期类型eu“DD-MM-YYYY”、us“MM-DD-YYYY”、iso“YYYY-MM-DD”eu
filePathstring日志文件路径RNFS.DocumentDirectoryPath or expo FileSystem.documentDirectory
const log = logger.createLogger({
  transport: fileAsyncTransport,
  transportOptions: {
    FS: RNFS as any,
    fileName: 'log.txt',
  },
});

export default function ReactNativeLogsScreen() {
  const readLog = () => {
    const path = RNFS.DocumentDirectoryPath + '/wclog.txt';
    return RNFS.readFile(path)
      .then(result => {
        console.log('读取成功\n', result);
      })
      .catch(err => {
        console.log('读取失败\n', err);
      });
  };

  const clearLog = () => {
    const path = RNFS.DocumentDirectoryPath + '/wclog.txt';
    RNFS.unlink(path)
      .then(result => {
        console.log('删除成功\n', result);
      })
      .catch(err => {
        console.log('删除失败\n', err);
      });
  };
}

扩展(命名空间)

通过log.extend方法新建命名空间,命名空间打印的日志会带上该命名空间的 TAG,如 “[time] | [namespace] | [level] | [msg]”,并且命名空间可以通过 log.disable关闭

import { logger, consoleTransport } from "react-native-logs";

const log = logger.createLogger({
  transport: consoleTransport,
  enabledExtensions: ["ROOT", "HOME"],
});

const rootLog = log.extend("ROOT");
const homeLog = log.extend("HOME");

log.info("print this"); // this will print "<time> | ROOT | INFO | print this"
homeLog.info("print this"); // extension is enabled

log.disable("HOME");

homeLog.info("not print this"); // extension is not enabled
rootLog.info("print this"); // extension is enabled

log.disable();

homeLog.info("not print this"); // logger is not enabled
rootLog.info("not print this"); // logger is not enabled
log.info("not print this"); // logger is not enabled

源码分析

// 默认配置
const defaultLogger = {
  severity: "debug",
  transport: consoleTransport,
  transportOptions: {},
  levels: {
    debug: 0,
    info: 1,
    warn: 2,
    error: 3,
  },
  async: false,
  asyncFunc: asyncFunc,
  stringifyFunc: stringifyFunc,
  formatFunc: null,
  printLevel: true,
  printDate: true,
  dateFormat: "time",
  fixedExtLvlLength: false,
  enabled: true,
  enabledExtensions: null,
  printFileLine: false,
  fileLineOffset: 0,
} as const;


const createLogger = <
  K extends
    | transportFunctionType<any>
    | transportFunctionType<any>[] = transportFunctionType<{ _def: string }>,
  Y extends string = keyof typeof defaultLogger.levels
>(
  config?: configLoggerType<K, Y>
) => {
  let mergeConfig = config ? { ...config } : ({} as object);

  // 合并默认配置
  const mergedConfig = {
    ...defaultLogger,
    ...mergeConfig,
  };

  return new logs(mergedConfig);
};

class logs {
    constructor() {
        this._levels = config.levels;
        this._level = config.severity ?? Object.keys(this._levels)[0];

        this._transport = config.transport;
        this._transportOptions = config.transportOptions;
		
        this._asyncFunc = config.asyncFunc;
        this._async = config.async;

        this._stringifyFunc = config.stringifyFunc;
        this._formatFunc = config.formatFunc;
        this._dateFormat = config.dateFormat;
        this._printLevel = config.printLevel;
        this._printDate = config.printDate;
        this._fixedExtLvlLength = config.fixedExtLvlLength;

        this._enabled = config.enabled;
        
        Object.keys(this._levels).forEach((level: string) => {
            // ...
            if (typeof this._levels[level] === "number") {
              // 新增级别打印日志方法
              _this[level] = this._log.bind(this, level, null);
            } else {
              throw Error(`[react-native-logs] ERROR: [${level}] wrong level config`);
            }
        }, this);
    }
    
      // 私有的打印方法
      private _log: logMethodType = (level, extension, ...msgs) => {
          // 异步打印日志
          if (this._async) {
              return this._asyncFunc(() => {
                this._sendToTransport(level, extension, msgs);
              });
          } else {
              return this._sendToTransport(level, extension, msgs);
          }
      };

	  // 调用传输方法
	  private _sendToTransport = (
        level: string,
        extension: string | null,
        msgs: any
      ) => {
        if (!this._enabled) return false;
        if (!this._isLevelEnabled(level)) {
          return false;
        }
        if (extension && !this._isExtensionEnabled(extension)) {
          return false;
        }
        let msg = this._formatMsg(level, extension, msgs);
        // 传递给传输函数的参数
        let transportProps = {
          msg: msg,
          rawMsg: msgs,
          level: { severity: this._levels[level], text: level },
          extension: extension,
          // 配置中的传输选项
          options: this._transportOptions,
        };
        // 如果传输方法是一个数组,每个传输方法都会调用一遍
        if (Array.isArray(this._transport)) {
          for (let i = 0; i < this._transport.length; i++) {
            if (typeof this._transport[i] !== "function") {
              throw Error(`[react-native-logs] ERROR: transport is not a function`);
            } else {
              this._transport[i](transportProps);
            }
          }
        } else {
          if (typeof this._transport !== "function") {
            throw Error(`[react-native-logs] ERROR: transport is not a function`);
          } else {
            this._transport(transportProps);
          }
        }
        return true;
      };
}

总结

该库的优势在于:可自定义transport传输方法,可扩展性强,并且是纯javascript库,无原生代码。并且支持自定义级别level,用来对日志进行分级。缺点在于,库本身不支持删除日志,需要自己实现。并且不支持打印原生日志使用内置的fileAsyncTransport同样有每打印一条日志,就会进行一次读写的问题。此外,该库本身不支持文件滚动更新机制,需要开发自己实现。

对比

功能react-native-turbo-logreact-native-logs
生态7 star,周 162 下载量473 star,周 1.4 万下载量
包体积281kb168kb
打印原生日志支持不支持
输出文件支持需要 react-native-fs 支持
自定义级别不支持支持
自定义日期格式不支持支持
重写 console支持不支持
原生代码
支持文件滚动支持不支持
可扩展性基本没有可自定义 transport 方法,自定义控制台日志样式

总结

生态上来看,react-native-logs 更胜一筹,功能上看,react-native-logs 不支持打印原生代码,react-native-turbo-log 更贴合需求,支持文件滚动。但两个库都有个缺点,没法先将日志保存在缓存中,定时、或定量保存至文件中,而是每打印一句日志都会触发一次文件的读写操作。