Introduction

If your logging needs are simple, then the stock transports, known-as back-ends, that come with the Pantheios logging API library may serve your needs. But if you're developing programs with high-uptimes, remote control, and other aspects of non-trivial systems that usually require logging, then you will probably need to write custom back-ends. In that case, a detailed understanding of the Pantheios back-end architecture will be essential. This article, the first of a series on Pantheios back-ends, introduces the API, illustrates how to write a very simple custom back-end, and discusses the features of several of the stock back-ends.

pantheios_be_init() and pantheios_be_uninit() are invoked by the core during initialisation/uninitialisation. They are invoked at most once per process, and always (unless someone tries incredibly hard to do something weird) in the main thread. pantheios_be_logEntry() is invoked by the core each time a prepared log statement is to be emitted, on any thread in the process.

A Trivial Back-end

The following hypothetical code shows how these functions might be implemented to log to stdout. First, pantheios_be_init():

This code is invoked by the Pantheios Core during initialisation. It is given the process identity (which is defined by the Front-end; we'll cover this in a future article), which it should copy if it needs it, along with a pointer to a void* within which it may store any value representing its state. This is held on behalf of the back-end by the core, and is passed back into other back-end API functions, as we'll see.

In this case, we'll just copy the process identity, and store that back in *ptoken. If the initialisation is successful, we must return PANTHEIOS_INIT_RC_SUCCESS. The only alternative in this case is to fail if the stringcannot be duplicated, returning the indicative error code PANTHEIOS_INIT_RC_OUT_OF_MEMORY. Both error codes are defined in pantheios/error_codes.h, along with a number of other codes representing common (and some uncommon) initialisation failure conditions.

(Note: This implementation uses the non-standard, and therefore non-portable, C function strdup() as a convenience in this case; it's a trivial example, remember.)

In Pantheios, all initialisation is done in pairs, according to the following rule:

Pantheios Initialisation Rule: Any successful call to an initialisation function will always be matched by a call to the corresponding uninitialisation function. An uninitialisation function will never be called if the corresponding initialisation function is unsuccessful.

Bearing this in mind, we can implement pantheios_be_uninit() very simply, as:

void pantheios_be_uninit(
void* token
)
{
free(token);
}

The token passed in is the same thing that we wrote to *ptoken in pantheios_be_init(), and we can just free it (on the assumption that our non-standard strdup() allocates using malloc()).

feToken is the front-end initialisation state. This enables custom front and back-ends to talk to each other. This will be discussed in a future article, and is not considered further here.

beToken is the token we created in pantheios_be_init().

severity is the severity passed in to the logging statement in the application code.

entry is a non-NULL pointer to a nul-terminated C-style stringcontaining the statement text.

cchEntry is the length of the C-style string pointed to by entry.

Providing the stringlength as well as guaranteeing that the statement stringis nul-terminated is somewhat redundant. However, doing so facilitates the easy implementation of back-ends that prefer one form or the other. For example, the be.WindowsDebugger back-end uses the Windows API function OutputDebugStringA(), which takes a pointer to a nul-terminated C-style string. If that back-end had to allocate a buffer of cchEntry + 1, then memcpy() entry into it, and append a nul-terminator before passing to OutputDebugStringA(), that would eat into Pantheios' considerable performance advantages. The converse applies for an output API that requires an explicit length: having to do a strlen() on entry would similarly be a cost we don't want to pay.

In our case, we just pass processIdentity, severity and the statement to fprintf(), which outputs them to standard out in the form: "<processIdentity>[<severity-code>]: <message>"

Because the core maintains the state on behalf of the back-end, the back-end implementation can be very simple, and can, as in this case, be written in C, rather than C++. (Several stock back-ends and front-ends are written in C, partly for the decreased compilation times.) Furthermore, the state can be a lot more complex than a pointer to an allocated block of memory: in a number of stock back-ends it is a pointer to a C++ object, which handles the relative sophistication of the given back-end functionality.

Pantheios' Stock Back-ends

The stock back-ends provided with the current Pantheios distribution are in two groups:

Concrete back-ends

Multiplexing back-ends

The multiplexing back-ends allow for combining two or more concrete back-ends to send logging output to multiple destinations, e.g. console, Syslog and file. These will be discussed in detail in subsequent articles in this series.

The concrete back-ends available are:

be.ACE - Outputs using the logging facilities from the Adaptive Communications Environment (ACE) library. This is an example of how Pantheios' superior type-safety and performance can be married to logging libraries with much richer logging facilities.

be.COMErrorObject (Windows-only) - Outputs to the COM Error Object. Useful when implementing COM servers, as you can log to file/debugger and update the COM error object, used by automation/scripting clients, in a single statement. A future article will discuss how to do this.

be.WindowsConsole (Windows-only) - Outputs to the Windows console, with severity-specific colour-coding of statements.

be.WindowsDebugger (Windows-only) - Outputs to the Windows debugger using OutputDebugStringA(). This is useful in combination with your other back-ends, as it allows the logging output to be followed from within your IDE.

Summary

We've discussed the Pantheios back-end API, and had a look at a trivial implementation of the API, covering how to maintain state and the details of the output function. We've also briefly discussed the stock back-ends and their use.

Building on this base, subsequent articles in this series will cover back-end multiplexing, interaction with custom front-ends, and a deeper look into some of the stock back-ends as a guide to what to do (and what not to do) when implementing your custom back-ends.

There's a whole lot more to the world of Pantheios, and in future articles I will explain more features, as well as cover best-practice and discuss why Pantheios offers 100% type-safety with unbeatable performance.