Skip to content

Configuration ​

An appender is opened with one config, and every platform carries the same options under its own spelling. logdir / logDir / logDirectory is the only one with no default — the appender is not opened without somewhere to write.

what it doesRustSwiftAndroidKotlin MultiplatformFlutter / React NativeCC++HarmonyOSdefault
where the .xlog files go; created when it is not therelogdirlogDirectorylogDirlogDirlogDirlog_dirlogDirlogDirrequired (./log in Rust)
what every file starts with, and the name the appender is known bynameprefixnamePrefixnamePrefixnamePrefixnamePrefixname_prefixnamePrefixnamePrefixxlog (C++ gives xlog too; C gives none — name_prefix is used verbatim, and an empty one stays empty)
the level a record has to reachsee levelslevellevellevellevelmars_xlog_new_instance(&config, level)levellevelinfo
whether a write waits for the filemodemodemodemodemodemodemodemodeasync
where the async cache file goescachedircacheDirectorycacheDircacheDircacheDircache_dircacheDircacheDirnext to the log files
how many days a file staged in the cache directory waits before it is moved into the log directorycache_dayscacheDayscacheDayscacheDayscacheDayscache_dayscacheDayscacheDays0 — nothing is staged: the day's file is written in the log directory
what a closed file is compressed withcompress_modecompressioncompressModecompressModecompressModecompress_modecompressModecompressModezlib
how hard the compressor triescompress_levelcompressionLevelcompressLevelcompressLevelcompressLevelcompress_levelcompressLevelcompressLevel0 — the appender's own level, 6, as mars_xlog.h spells it; zlib ignores the level and always compresses at 9; 6 in Rust
the public key a record is encrypted withpub_keypublicKeypubKeypubKeypubKeypub_keypubKeypubKeyempty — no encryption

A config the appender cannot honour is refused where you build it and not silently by the library: Swift and C++ throw an XlogError, Kotlin raises IllegalArgumentException, Rust returns an Err, C answers a negative MARS_XLOG_ERR_*, ArkTS throws an Error, and on the two bridging platforms the call that opened it fails — a PlatformException in Dart, a thrown Error in TypeScript.

Levels ​

A record is written when the level of the appender is at most the record's own: an appender opened at info keeps warning and drops debug.

levelRustSwiftAndroidKMPFlutter / React NativeCC++HarmonyOS
0LogLevel::Verbose.verboseLogLevel.VERBOSELogLevel.VERBOSELogLevel.verboseMarsLevelVerboseLogLevel::VerboseLogLevel.Verbose
1LogLevel::Debug.debugLogLevel.DEBUGLogLevel.DEBUGLogLevel.debugMarsLevelDebugLogLevel::DebugLogLevel.Debug
2LogLevel::Info.infoLogLevel.INFOLogLevel.INFOLogLevel.infoMarsLevelInfoLogLevel::InfoLogLevel.Info
3LogLevel::Warn.warningLogLevel.WARNINGLogLevel.WARNINGLogLevel.warningMarsLevelWarnLogLevel::WarningLogLevel.Warning
4LogLevel::Error.errorLogLevel.ERRORLogLevel.ERRORLogLevel.errorMarsLevelErrorLogLevel::ErrorLogLevel.Error
5LogLevel::Fatal.fatalLogLevel.FATALLogLevel.FATALLogLevel.fatalMarsLevelFatalLogLevel::FatalLogLevel.Fatal
6LogLevel::None.noneLogLevel.NONELogLevel.NONELogLevel.noneMARS_LEVEL_NONELogLevel::NoneLogLevel.None

The two Kotlin columns are the same on purpose: xlog-kmp and xlog publish one API, so a shared module that moves between them renames nothing. So are the two columns that follow them — marsrs_xlog and marsrs-react-native-xlog carry the same spelling of the same options, and it is the Kotlin one.

HarmonyOS is the Kotlin spelling in a different case: LogLevel.Verbose and not LogLevel.VERBOSE, because an ArkTS enum sits in an app full of HarmonyOS enums.

none writes nothing, not even fatal — it is how an appender is quieted without being closed.

There is no assert in the port: mars_xlog_assert was the process-wide appender's, and it went with it. What an app that wants one writes is the record itself — xlog.f(tag, message) — and stops the process after it, std::process::abort() or a platform trap; upstream raises SIGTRAP on Android and calls __assert_rtn on Apple once the record is written, and nothing here does either.

A message that is expensive to build is worth asking about first, because a record the level drops still costs the caller the string:

swift
if log.isEnabled(for: .debug) {
    log.debug(message: "\(expensiveDescription())", tag: "net")
}
kotlin
if (xlog.isLoggable(LogLevel.DEBUG)) {
    xlog.d("net", expensiveDescription())
}
dart
if (await xlog.isLoggable(LogLevel.debug)) {
  xlog.d("net", expensiveDescription());
}
ts
if (xlog.isLoggable(LogLevel.debug)) {
  xlog.d("net", expensiveDescription());
}
typescript
if (xlog.isLoggable(LogLevel.Debug)) {
  xlog.d('net', expensiveDescription());
}

Async or sync ​

async (the default)sync
what a write doescompresses the record into a memory-mapped cache and hands it to a writer threadwrites it through to the file
what it coststhe write syscall is not on the logging threadthe thread that logs waits for the file
what you must doflushNow() before the file is read or uploaded — and nothing at all when the app goes away (log files)close() or flushNow() before the process can be killed: what is still buffered goes with it

This page describes the tree at main. The newest release, v0.1.0-alpha.3, is older than it, and it spells the drain one way: flush(sync = true) in Kotlin and Swift, mars_xlog_flush_instance(h, 1) in C, and appender_flush_sync() in Rust. The flushNow() above is main's: an app that takes a tagged release gets the older spelling until the next tag, and a Cargo dependency on the repository gets this one by leaving --tag off.

Async is the default because the file is slower than the record; sync is what you want when a record cannot wait for a writer thread — a crash log, or the last lines before an exit.

What neither mode does is reach the kernel once per record: sync holds what it has until roughly 4 KiB of it has piled up, and async until its cache block is a third full — 50 KiB of the 150 KiB mmap — or a fatal record arrives, or until close() or flushNow() runs. Async loses nothing by waiting — the record is in the cache file the kernel holds as well — but sync has nothing behind it, so a process that is killed loses the tail it was still holding. That tail is the one thing an exit needs a call for.

Compression and encryption ​

zlib is the default and zstd compresses harder; both are per file, applied when the file is closed. 22 is the ceiling of the level, but 0 is not zstd's own default (3): it is the appender's, 6, which is what mars_xlog.h asks a 0 to mean, and every platform but Rust answers a 0 with the appender's own level, before the compressor sees it. zlib ignores the level and always compresses at 9, the way the C++ does. Only a Rust caller who writes 0 into the config by hand reaches zstd's own default, because nothing between the config and the encoder rewrites it — Rust's own default is 6.

A pubKey encrypts the body of each async record with ECDH + TEA — the cipher the C++ implementation uses, over a key the writer and the reader agree on. A sync record's body is stored in the clear whatever the key says, as it is in the C++: what a pubKey encrypts is an async record, so a file written in sync mode is readable with no key at all. What you put there is the public key of the pair whose private key reads the file back; leaving it empty writes a file any mars log reader can open.

After it is open ​

These are not in the config — they are setters or properties on the appender, and every one of them takes effect from the next record:

what it doesRustSwiftAndroidKotlin MultiplatformFlutterReact NativeCC++HarmonyOS
move the levelxlog.set_levellog.levelxlog.levelxlog.levelxlog.levelxlog.levelmars_xlog_set_level_instance(handle, level)log.setLevelxlog.level
switch async / syncxlog.set_modelog.modexlog.modexlog.modexlog.modexlog.modemars_xlog_set_mode_instance(handle, mode)log.setModexlog.mode
mirror records to the consolexlog.set_console_log_enabledlog.isConsoleLogEnabledxlog.consoleLogEnabledxlog.consoleLogEnabledxlog.consoleLogEnabledxlog.consoleLogEnabledmars_xlog_set_console_log_instance(handle, on)log.setConsoleLogEnabledxlog.consoleLogEnabled
close a file after N bytesxlog.set_max_file_size_byteslog.maxFileSizeBytesxlog.maxFileSizeBytesxlog.maxFileSizeBytesxlog.maxFileSizeBytesxlog.maxFileSizeBytesmars_xlog_set_max_file_size_instance(handle, bytes)log.setMaxFileSizeBytesxlog.maxFileSizeBytes
drop a file older than N secondsxlog.set_max_alive_time_secondslog.maxAliveTimeSecondsxlog.maxAliveTimeSecondsxlog.maxAliveTimeSecondsxlog.maxAliveTimeSecondsxlog.maxAliveTimeSecondsmars_xlog_set_max_alive_duration_instance(handle, secs)log.setMaxAliveTimeSecondsxlog.maxAliveTimeSeconds
where it writesxlog.current_log_path()log.currentLogPathxlog.currentLogPathxlog.currentLogPathawait xlog.currentLogPath()xlog.currentLogPathmars_xlog_current_log_path_instancelog.currentLogPath()xlog.currentLogPath
a day of filesxlog.log_files(1)log.logFiles(daysAgo: 1)xlog.logFiles(1L)xlog.logFiles(1L)await xlog.logFiles(1)xlog.logFiles(1)mars_xlog_getfilepath_from_timespan_instancelog.logFiles(1)xlog.logFiles(1)

It answers a directory and not a file, which is what the C++'s GetCurrentLogPath hands back; the day's file is the row under it.

0 is "no limit" for the size: a file is never split. It is not "no limit" for the age: a value under a day is ignored, 0 included, so the appender keeps the age it already has — the ten days it was opened with, or whatever day or more it was last given. The C++ default is the same ten days.

Where the console copy goes ​

xlog.set_console_log_enabled(true) — xlog.consoleLogEnabled = true on the platforms that spell it that way — mirrors every record to the console as well as to the file. A record written through a Rust Xlog carries an XLoggerInfo — the level and the tag the call named — so what the console gets is the whole record and not only the message.

The built-in sink is standard error, on every platform and in every package — nothing here writes to os_log or to logcat, and there is no sink to set: the one the port had sat on the process-wide appender, and that appender is gone. On Apple the system log is a call the app makes for itself — os_log is a macro a library cannot reach.

An app that wants its records there writes them twice, or reads them back out of the file: the console copy is standard error's, and it is not the port's to hand anywhere else.

MIT, like the upstream project.