Skip to content

Log through CoreLogger ​

Use CoreLogger to write fast, structured, and styled log messages. This guide demonstrates the default DeltaTimeLogger implementation and how to log using the styled Interpolated Expression Sequences (IES).

Using the DeltaTimeLogger ​

By default, calling initLogger installs DeltaTimeLogger as both the Phobos std.logger.sharedLog and sparkles.base.logger.sharedCoreLog. DeltaTimeLogger formats logs in the following layout:

[ Time | Δt (since start) | Δtᵢ (since last log) | Level | File:Line ]: Message

Here is an example showing how to initialize the logger and output logs of various levels, using both static styled messages and dynamic interpolated values.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "base_core_logger"
    dependency "sparkles:base" version="*"
+/
import sparkles.base.logger : initLogger, info, warning, error, critical, log, trace, LogLevel;

void main()
{
    // Initialize the logger to output logs of TRACE level or higher
    initLogger(LogLevel.trace);

    // Standard log levels with static styled messages
    trace(i"Application starting up");
    info(i"Listening on port {green 8080}");
    warning(i"Disk usage above {yellow 80%}");
    error(i"Connection to {red database} lost");
    critical(i"{bold.red Out of memory}");

    // Styled IES with interpolated values
    immutable host = "db-01.prod";
    immutable port = 5432;
    info(i"Reconnected to {green $(host)}:{cyan $(port)}");

    // Explicit log level with styled IES
    log(LogLevel.warning, i"Latency spike: {yellow.bold 230ms} on {dim $(host)}");
}
[ 15:22:40 | Δt 46.7µs | Δtᵢ 46.7µs | TRC | base_core_logger.d:14 ]: Application starting up
[ 15:22:40 | Δt 119.7µs | Δtᵢ 72.9µs | INF | base_core_logger.d:15 ]: Listening on port 8080
[ 15:22:40 | Δt 167.3µs | Δtᵢ 47.5µs | WRN | base_core_logger.d:16 ]: Disk usage above 80%
[ 15:22:40 | Δt 212.2µs | Δtᵢ 44.9µs | ERR | base_core_logger.d:17 ]: Connection to database lost
[ 15:22:40 | Δt 257.9µs | Δtᵢ 45.7µs | CRT | base_core_logger.d:18 ]: Out of memory
[ 15:22:40 | Δt 315.2µs | Δtᵢ 57.2µs | INF | base_core_logger.d:23 ]: Reconnected to db-01.prod:5432
[ 15:22:40 | Δt 361.1µs | Δtᵢ 45.9µs | WRN | base_core_logger.d:26 ]: Latency spike: 230ms on db-01.prod

Log from @nogc code ​

The level functions are templates and take their attributes from what you pass. A message whose values render without allocating (numbers, strings, enums, prettyPrint(x), a type with a @nogc toString(Writer)) makes the call @safe nothrow @nogc, so it works inside @nogc code. A value whose rendering allocates, such as a toString returning a new string, is still accepted, but only outside @nogc code, and the compiler reports the problem at your call.

Tracing (--log-level=trace) ​

trace(), traceCall, and traceSpan are no-ops when the process is above TRACE — they return before formatting. Enable them with --log-level=trace (apps that go through runCli and declare a LogLevel logLevel field get initLogger once after parse).

Wrap a function:

traceCall!InitWindow(width, height, title.ptr);

Wrap a block; the name is a compile-time string (no runtime lookup):

auto span = traceSpan!"document"();
doc = pipeline.load(path);

The closing line is << name <duration> on top of the usual Δt / Δtᵢ prefix.

Keep a copy: the ring and the rotating file ​

sparkles.base.log_sinks adds two sinks that install in front of the one already there, record each entry, and forward it — so stderr (or Android's logcat) sees exactly what it saw before:

initLogger(LogLevel.info);                          // stderr
installFileLog(buildPath(stateDir, "app.log"));     // file → stderr
auto ring = installRingLog();                       // ring → file → stderr
  • RingCoreLogger keeps the last 10 000 entries (messages over 4 KiB are cut at a UTF-8 boundary) in storage allocated once; a full ring overwrites the oldest entry and counts it in dropped. ring.each(fun, fromSeq) reads oldest-first; pass the next seq you have not seen to tail it.
  • RotatingFileCoreLogger appends one flushed line per entry, rotating to <path>.1 at 1 MiB, and moves the previous run's file there when it opens. A failed write turns it off with one warning; it never throws.

initLogger sets the level on every link of the chain. Writing to either sink is @safe nothrow @nogc.

Report the build: buildStampOf ​

buildStampOf!() (sparkles.base.build_stamp) is the version and commit a packaging build wrote to sparkles-build-stamp on the compiler's -J path — dev for a plain dub build. It is a template, so the application's compilation resolves it:

enum stamp = buildStampOf!();
info(i"myapp $(stamp.version_) ($(stamp.commitLabel))");

The Nix side is legacyPackages.mkBuildStamp { version = …; }, which stamps a dirty tree as <rev>-dirty (commitLabel: <rev> + uncommitted changes).

Advanced Customization: Fatal Handlers ​

fatal calls follow the same rule. By default, the fatal handler throws a thread-local, recycled FatalLogError to avoid GC allocation.

If you want the process to exit immediately or panic instead of throwing, you can customize the handler by setting coreFatalHandler to assertingFatalHandler or abortingFatalHandler:

d
import sparkles.base.logger : coreFatalHandler, assertingFatalHandler;

// Change the behavior of `fatal` logs to assert(0)
coreFatalHandler = &assertingFatalHandler;