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
.
Logger
s 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.