libnslog
|
The purpose of the NSLog library is to provide a flexible logging macro which permits logging with categories, and at levels. Then the library can queue log entries and permit filtering for log readers.
Since logging may have to happen during static initialisation, or before the logging library is fully initialised, categories are progressively added to the logging system as they are encountered. Since, at their core, categories are essentially just strings, we can write filters which will be resolved to categories later as and when they are encountered.
Libraries which use NSLog should NOT act as clients of the library excepting in tools, and test drivers. By implementing a client in the test drivers, libraries can verify the log messages sent. If a library might produce a lot of logging then it must implement a client in its test drivers or it might run out of RAM while running tests.
If you're not into prose, here is a commented example.
In order to log messages, you need to have a category to log under. We recommend that you have one category per library or application, and that you create sub-categories beneath that to divide the library (or app) into logical units.
Categories should be declared in headers, and defined in C files. You declare a category thusly:
NSLOG_DECLARE_CATEGORY(catname);
In that catname
is a valid suffix for a C identifier, and will be used as the logical name of the category for the purpose of NSLOG statements.
You define a category thusly:
NSLOG_DEFINE_CATEGORY(catname, description);
In that catname
matches the above, and description
is a C string (or identifier to a C string) which describes the category.
You define a subcategory thusly:
NSLOG_DEFINE_SUBCATEGORY(parentcatname, catname, description);
In that parentcategory
is the catname
of another category which will be considered the parent of this subcategory. catname
and description
as above. Subcategories are rendered with slashes between their names when shown as text, so a category of foo
with a subcategory of bar
would result in two categories, one foo
and one foo/bar
.
Once you have categories, you can log stuff. Your categories don't have to be entirely public, but you will need access to the symbol to be able to log things with the given category. To log something, you use:
NSLOG(catname, level, logmsg, args...);
In that catname
is a catname
from the above, level
is a bareword logging level which will be prefixed with NSLOG_LEVEL_
for use (e.g. you can use WARNING
directly). logmsg
and args...
are a printf()
style log message which will be rendered by the time NSLOG()
returns, so you don't have to worry about lifetimes.
Lib NSLOG lets you define a minimum compiled-in logging level which can be used to elide deep debugging in release builds. The NSLOG()
macro expands to a constant comparison which will be resolved at compile time (assuming optimisations are turned on) to result in logging which doesn't need to be compiled in, being compiled out (and thus is is basically zero-cost to sprinkle deep debugging in your code).
In order for code which uses NSLOG()
to be hosted effectively, the client must do two things. Firstly a callback must be provided. The simplest callback would be:
static nslog__client_callback(void *context, nslog_entry_context_t *ctx, const char *fmt, va_list args) { (void)context; (void)ctx; (void)fmt; va_start(args, fmt); va_end(args); }
In that context
is a void*
which is passed through to the client code directly, allowing a client to provide some struct or other through to the logging client callback. ctx
is an NSLOG context which you can find out more about by reading the docs. It contains the information about the logging site such as the filename, line number, function name, level, and category used to make the log entry. fmt
is the logmsg
from above, and args
is the variadic argument set used to format the arguments.
This callback is registered into Lib NSLOG with something like:
nslog_set_render_callback(nslog__client_callback, NULL);
If a callback is not set, then bad things will happen when the next thing is run. The final thing which must happen before the client will receive log statements is that the client must uncork the logging library. Since logging can occur before the client is ready, the library will render and store the log messages in a list, awaiting the call to uncork. Once uncorked, log messages will be passed through as flatly as possible to the client. To uncork the library, call:
nslog_uncork();
All corked messages will be passed to the client callback, in the order they were logged, before the uncork call returns.
The primary way to filter logs is to simply compile out the logging levels you do not wish to exist. Sadly this is a very coarse filter and is best used to exclude deep debug (and possibly debug) from release builds. If you wish to filter your logs more flexibly at runtime, then you need to set up a log filter. You can build the filters "by hand" but much easier is to use the text filter parser as such:
nslog_filter_t *filter = NULL; if (nslog_filter_from_text(my_filter_text, &output) == NSLOG_NO_ERROR) { nslog_filter_set_active(filter, NULL); nslog_filter_unref(filter); }
The filter syntax is fairly simple:
filter
is one of a NOT
an AND
, OR
, or XOR
, or a simple
filter.NOT
filter is of the form !filter
AND
filter is of the form (filter && filter)
OR
filter is of the form (filter || filter)
XOR
filter is of the form (filter ^ filter)
simple
filter is one of a level
, category
, filename
, dirname
, or funcname
filter.level
filter is of the form level: FOOLEVEL
where FOOLEVEL
is one of DEEPDEBUG
, DEBUG
, VERBOSE
....category
filter is of the form cat: catname
where catname
is a category name. If it is of the form foo
then it will match all categories foo
or foo/*
but not foobar
filename
filter is of the form file: filename
and is essentially a suffix match on the filename. foo.c
will match foo.c
and bar/foo.c
but not barfoo.c
dirname
filter is of the form dir: dirname
and is essentially a prefix match on the filename. foo
will match foo/bar.c
and foo/bar/baz.c
but not foobar.c
Between tokens, any amount of whitespace is valid, but canonically only a small amount will be used. You can take any filter you have and re-render it in its canonical text form by using nslog_filter_sprintf()
which is documented.
Filters are evaluated in standard order, with parentheses doing the obvious thing. In addition, the AND
and OR
filters will short-circuit as you'd expect, so you can use that to your advantage to make your filters efficient. Internally, Lib NSLOG will cache filtering intermediates to make things as fast as possible, but a badly designed filter will end up slowing things down dramatically.