appcore-log 1.0.0-rc.2

Structured, bounded, security-first operational logging for AppCore
Documentation

appcore-log

Português | Français

appcore-log provides structured, bounded, and security-first operational logging for AppCore. It has no global logger or hidden queue and creates no background worker unless the caller explicitly selects AsyncSink. The caller chooses filtering, destinations, retention, and durability.

Quick start

use appcore_log::{LogOutputMode, LoggerConfig};

fn main() -> Result<(), appcore_log::LogConfigError> {
    let logger = LoggerConfig {
        output: LogOutputMode::Terminal,
        ..LoggerConfig::default()
    }
    .build()?;

    let log = logger.dispatcher().event(0, "application");

    log.info("application started");
    log.warn("connection is slow");

    Ok(())
}

Applications using appcore-sdk configure the same logger with App::logging and emit through app.log(...) or app.logger().

Output modes

Mode Normal execution File required
Terminal Human-readable terminal output No
File Structured bounded JSONL Yes
TerminalAndFile Terminal and JSONL Yes
Disabled No sink work or message conversion No
CrashOnly Bounded sanitized memory ring Yes, created only by dump_crash

CrashOnly does not install a panic handler. Call dump_crash once from the application's controlled crash boundary.

Bounded file output

use appcore_log::{
    FileArchiveConfig, FileSinkConfig, LogOutputMode, LoggerConfig,
    LOG_SIZE_8_MIB,
};

fn main() -> Result<(), appcore_log::LogConfigError> {
    let logger = LoggerConfig {
        output: LogOutputMode::TerminalAndFile,
        file: Some(FileSinkConfig {
            path: "logs/runtime.jsonl".into(),
            max_bytes: LOG_SIZE_8_MIB,
            sync_each_write: false,
            retention: 2,
            archive: Some(FileArchiveConfig {
                directory: "logs/archive".into(),
                max_files: 120,
            }),
        }),
        ..LoggerConfig::default()
    }
    .build()?;

    logger.dispatcher().event(0, "application").info("ready");

    Ok(())
}

The active directory contains runtime.jsonl, runtime.jsonl.1, and runtime.jsonl.2. Older rotations move to archive/YYYY/MM until the complete archive reaches max_files. The file name is application-defined. Create the active file's parent directory during application setup; archive year/month directories are created during rotation. File and archive destinations reject symbolic links instead of following them.

Set sync_each_write: true when every event must be flushed to storage before returning. false avoids that syscall and favors throughput. Common limits are available from LOG_SIZE_1_MIB through LOG_SIZE_64_MIB; custom nonzero u64 values remain valid.

Severity, verbosity, and components

Severity describes impact, from Trace to Critical. Verbosity describes detail, from V1 to V9. A V4 policy accepts V1 through V4; it does not mean "severity four". Component overrides are hierarchical, so an override for sync also applies to sync.transport unless a more specific one exists.

Security boundary

Safe and Diagnostic policies redact typed secrets and alias typed paths before ordinary sinks receive an event. Use LogEvent::secret and LogEvent::path; never put credentials in free-form messages. Full paths require a separate explicit policy decision.

Sensitive diagnostics require Sensitivity::Sensitive and an explicitly wired SensitiveDntSink. They are authenticated and encrypted as DNT and never fall back to terminal, JSONL, or ordinary memory sinks. Secret fields remain redacted even there.

Limits and failure behavior

  • Event text: 4 KiB per text or identity field.
  • Structured fields: 32 per event.
  • Field keys: 128 bytes; field values: 4 KiB.
  • Active rotations: at most 32.
  • Archived files: from 1 to 10,000.
  • Rings: always bounded by both event count and estimated retained bytes.
  • Sink failures: counted by the dispatcher and never logged recursively.

Use stats() for aggregate filtering/failure counters and sink_stats() for per-destination failures. Use FixedLogClock for deterministic tests.

See the English guide and the executable examples in examples/.

Before allocating a JSONL payload, the file sink counts serialized bytes, including escaping and the newline, against max_bytes. Oversized records return Capacity before rotation or writing. Accepted records use an exact-size reservation and a second serialization pass; allocator overhead, caller-owned events and filesystem memory are outside this payload budget.

Each FileSink keeps one append handle open and tracks the active file size under its existing mutex. It closes the handle before rotation and reopens it if the path is removed, replaced, truncated, or appended by another writer. There is still no hidden queue or background flush worker.

For an explicitly asynchronous boundary, wrap one sink in AsyncSink. Its non-blocking emit is bounded by both AsyncSinkConfig::max_events and max_bytes; saturation returns Capacity and is counted. flush waits for previously admitted events and shutdown drains and joins the single 256 KiB worker. The wrapped sink must itself complete: arbitrary blocking I/O cannot be forcibly terminated safely. Dropping without explicit shutdown never waits for that I/O. The queue is opt-in and never changes LoggerConfig defaults.

Benchmark

Run the ten hardware-aware workloads with:

cargo run -p appcore-dev -- bench --name appcore-log \
  --output target/appcore-log-benchmark.json

The report records timing distributions, CPU time, peak/retained RSS, and host CPU, RAM, GPU, OS, and filesystem context. Compare compatible reports with appcore-dev bench compare.

The concurrent JSONL cases measure 64 events per batch from four producers, with and without per-event sync. The slow-sink case serializes four events with a simulated 1 ms wait each; it is not a disk measurement. See the guide for timing boundaries.

Stable documentation

Stable ID: ACR-027. See the supplemental architecture and integration guide. This permanent ID remains valid if the wiki page moves.