Dtext consist of a format package, offering similar capabilities to std.format,
and a log package, offering a powerful Logger class.
Both packages have been extracted from ocean, and have been used in real-time bidding applications for the better part of a decade.
The building block of Dtext is dtext.format.Formatter.
It is an implementation as a formatter that is guaranteed to minimally allocate,
and never under some circumstances.
It consists of a few overloads:
/// Pedestrian `format`: Returns a new, GC-allocated string
public string format (Args...) (in char[] fmt, Args args);
/// Building block: Takes a delegate, allow to implement any allocation strategy,
/// including using `malloc` or one of Phobos' allocators
public bool sformat (Args...) (scope FormatterSink sink, in char[] fmt, Args args);
/// Similarly to `sprintf`, will write to `buffer` up to its available length
/// Does not allocate on its own, but might lead to GC allocations if `args`
/// has allocating `toString`.
public char[] snformat (Args...) (char[] buffer, in char[] fmt, Args args)
/// Will append (using `~=`) to `buffer`. Intended to be used with `assumeSafeAppend`.
public char[] sformat (Args...) (ref char[] buffer, in char[] fmt, Args args)If you just intend to replace std.format, the basic format overload will work well.
dtext's Formatter main utility however comes from its sformat overload,
which similarly to formattedWrite will output to a sink.
The Formatter uses a different format string than std.format:
Instead of following the printf convention, which makes little sense in the presence
of compiler-provided type information (as [{s,sn}]format use templates),
the simplest way to format an argument is to use {}, equivalent to std.format's %s.
Double brace ("{{") is formatted as a single brace ("{"), positional arguments
(assert(format("{2} {1} {0}", 1, 2, 3) == "3 2 1")), width, and other options are available.
For more details, read the module's extensive documentation.
Like its Formatter, Dtext's Logger was built for real-time application.
As a result, message formatting takes place in a buffer (1024 chars by default, configurable)
using snformat and does not cause per-call invocation,
unless the arguments or Appender allocate.
Logger is a class, and each instance must have a name and belong to a Hierarchy.
A Hierarchy is built the same way as a module hierarchy is, using dot (.) as delimiter.
The common idiom that was used with Loggers was:
module some.awesome.project;
import dtext.log.Logger;
private Logger log;
static this ()
{
log = Log.lookup(__MODULE__);
}
void main ()
{
log.info("The answer is: {}", 42);
}In the above example, the first call to Log.lookup in the thread will allocate a new Logger,
subsequent calls will return the alread-instantiated Logger. Hierarchies are thread-local.
Looking up a parent is possible (e.g. Log.lookup("some.awesome")), and some configurations / operations
can be set to propagate to children (e.g. e.g. adding an Appender or setting a log level).
The root logger of the hierarchy is accessible via Log.root.
Loggers work in combination with two other classes: Appender and Layout.
An Appender defines where an event will go: this can be a file, the console,
syslog, or any custom logic (e.g. the AppendSterrStdout appender
will append to stdout below a certain level, and to stderr afterwards).
A Logger can have multiple Appender (e.g. a ConsoleAppender and FileAppender are common),
and Appender can be set to propagate when added to parents.
Layout define how the messages will be printed. The most basic layout, LayoutSimple,
will just print the event's message, but log calls also include the Level at which
the message was emitted, the time, logger's name, etc...
Loggers have 7 normal log levels: Debug, Trace, Verbose, Info, Warn, Error, Fatal,
in that order of importance. A special None value exists in ILogger.Level to disable any logging.
The dtext.log.ILogger : ILogger.Level is aliased as dtext.log.Logger : Level.
Each log level has a corresponding lowercase function: Logger.info, Logger.fatal, etc...
Due to Debug being a keyword, the matching function is Logger.dbg.
Providing a log level at runtime can be done via Logger.format(loglevel, format, args).
If a Logger is enabled for a certain level, messages of a higher levels will be emitted,
but messages for a lower level will be discarded without being formatted.
For example, for a Logger that is enabled for Verbose level,
calling log.trace will be a no-op.
The default Level is Level.Info.
The Formatter is currently not CTFE-able, not does it support passing a FormatSpec-like
struct to a toString method.
The Logger and Formatter alike are not attributes-friendly, and currently will not
play along well with them. As a compromise, @safe currently works,
although it wrongly apply @trusted to the user-provided arguments.