需求
- 日志打印记录时间
- 日志打印包含 TAG
- 最多保存 n 个日志文件,每个日志文件限制最大占用空间
- 当日志文件满后,新建日志文件时删除最早的日志文件(文件滚动更新)
- 先将日志写入缓存中,每隔固定时间间隔,将缓存中的日志写入到文件中(避免频繁文件读写)
- 同时打印原生日志和 JavaScript 日志
介绍
react-native-turbo-log
安装
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 将会把日志存储到文件系统中,如果未指定,你的
console的debug、log、info、warn和error都会被TurboLogger捕获,并且他们都会被写入到日志文件中。你也可以配置 TurboLogger 来自定义
dailyRolling、调用console拦截或启用/禁用文件系统的日志记录。
API
configure
TurboLogger.configure(options?: ConfigureOptions) => Promise
从0.5开始,必须在原生代码中进行配置,并且可以在 JavaScript 代码进行额外的配置。
使用指定的选项初始化TurboLogger,一旦配置的 Promise 解决,所有
console调用都会被捕获并写入日志文件。为了确保没有日志丢失,最好在应用程序启动时等待此调用。
| 选项 | 描述 | 默认值 | 设置位置 |
|---|---|---|---|
logLevel | 文件输出的最低日志级别(不会影响控制台输出 | LogLevel.Debug | Native |
captureConsole | 如果为true,所有console调用都会自动捕获并写入日志文件 | true | JavaScript |
dailyRolling | 如果为true,则每天都会创建一个新的日志文件 | false | Native |
maximumFileSize | 当前日志文件超过给定的字节大小时,将创建一个新的日志文件。将其设置为0以禁用 | 1024 * 1024 (1MB) | Native |
maximumNumberOfFiles | 要保留的最大日志文件数。创建新日志文件时,如果文件总数超过此限制,则删除最旧的文件。设置0以禁用 | 5 | Native |
logsDirectory | 存储日志文件的目录的绝对路径。如果未定义,日志文件将存储在应用程序的缓存目录中 | undefined | Native |
logToFile | 如果为true,则创建日志文件并将其写入文件系统。也可以通过调用setLogToConsole方法来更改它 | true | Native |
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.ts
import 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.kt
class 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.kt
class 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;
}
}
总结
这个库使用了RollingFileAppender和DDFileLogger来保存日志,优点是实现了最大文件个数和文件大小限制,并且支持文件滚动更新,支持原生日志打印,支持拦截console。缺点是在每次打印日志,都会进行一次文件写入操作,可能有性能问题。
react-native-logs
安装
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");
配置
| 参数 | 类型 | 描述 | 默认值 |
|---|---|---|---|
severity | string | 初始化日志严重性(您想要看到的最不重要的级别) | debug(或第一个自定义级别) |
transport | function or [function] | 日志的传输方法(请参阅下面的预设) | The preset transport consoleTransport |
transportOptions | Object | 为传输设置的自定义选项 | null |
levels | Object | 设置自定义日志级别:{name:power} | / |
async | boolean | 将异步日志设置为 true(以提高应用程序性能) | true |
asyncFunc | function | 设置自定义异步函数(cb: Function) => {return cb()} | setTimeout |
stringifyFunc | function | 设置自定义字符串化函数(msg: any) => string | 自定义JSON.stringify |
formatFunc | function | 设置自定义格式函数(level: string, extension?: string, msg: any) => string | 默认字符串格式化函数 |
dateFormat | string or function | time,local,utc,iso或者(date: Date) => string | time |
printLevel | boolean | 选择是否打印日志级别 | true |
printDate | boolean | 选择是否打印日志日期/时间 | true |
fixedExtLvlLength | boolean | 打印扩展和级别时确保字符数一致 | false |
enabled | boolean | 启用或禁用日志记录 | true |
| enabledExtensions | string[] | 仅启用某些命名空间 | 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使用自定义控制台设置选项。
| 名称 | 类型 | 描述 | 默认值 |
|---|---|---|---|
colors | object | 如果设置的话,您可以选择日志颜色,由级别定义:{level:color} | null |
extensionColors | object | 如果设置的话您可以选择扩展标签颜色:{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-fs或expo-file-system,允许您将日志保存在<filePath>/<fileName>.txt文件中
如果您希望每天创建一个新文件,您可以{date-today}在 fileName: app_logs_{date-today}.log->中使用app_logs_D-M-YYYY.log。
| 名称 | 类型 | 描述 | 默认值 |
|---|---|---|---|
FS | object | 文件系统实例(RNFS 或 expo FileSystem) | null |
fileName | string | 设置日志文件名(插入{date-today}当前日期) | / |
fileNameDateType | eu,us,iso | {date-today}日期类型eu“DD-MM-YYYY”、us“MM-DD-YYYY”、iso“YYYY-MM-DD” | eu |
filePath | string | 日志文件路径 | 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-log | react-native-logs |
|---|---|---|
| 生态 | 7 star,周 162 下载量 | 473 star,周 1.4 万下载量 |
| 包体积 | 281kb | 168kb |
| 打印原生日志 | 支持 | 不支持 |
| 输出文件 | 支持 | 需要 react-native-fs 支持 |
| 自定义级别 | 不支持 | 支持 |
| 自定义日期格式 | 不支持 | 支持 |
| 重写 console | 支持 | 不支持 |
| 原生代码 | 有 | 无 |
| 支持文件滚动 | 支持 | 不支持 |
| 可扩展性 | 基本没有 | 可自定义 transport 方法,自定义控制台日志样式 |
总结
生态上来看,react-native-logs 更胜一筹,功能上看,react-native-logs 不支持打印原生代码,react-native-turbo-log 更贴合需求,支持文件滚动。但两个库都有个缺点,没法先将日志保存在缓存中,定时、或定量保存至文件中,而是每打印一句日志都会触发一次文件的读写操作。