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.
Readers may want to check out the previous Pantheios tutorial article on setting up a project and selecting back-ends before reading on.
The Back-end API
The Pantheios architecture is based on four components:
- Application Layer - The classes and functions used in application code, responsible for presenting a log statement's elements to the core in a unified form
- Core - Handles initialisation of all components, and handles processing of logging statements submitted by the Application Layer
- Front-end - Defines the process identity, and arbitrates whether a log statement is to be prepared by the core and emitted to the back-end
- Back-end - Emits the prepared logging statement to the transport.
The back-end API consists of three functions:
int pantheios_be_init(
char const* processIdentity
, void* reserved
, void** ptoken
);
void pantheios_be_uninit(
void* token
);
int pantheios_be_logEntry(
void* feToken
, void* beToken
, int severity
, char const* entry
, size_t cchEntry
);
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():
#include <pantheios>
#include <pantheios>
#include <stdio.h>
#include <string.h>
int pantheios_be_init(
char const* processIdentity
, void* reserved
, void** ptoken
)
{
*ptoken = strdup(processIdentity);
return (NULL == *ptoken)
? PANTHEIOS_INIT_RC_OUT_OF_MEMORY
: PANTHEIOS_INIT_RC_SUCCESS;
}
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 string cannot 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()).
That just leaves the logging function, pantheios_be_logEntry():
int pantheios_be_logEntry(
void* feToken
, void* beToken
, int severity
, char const* entry
, size_t cchEntry
)
{
char const* processIdentity = (char const*)beToken;
fprintf(stdout, "%s[%d]: %.*s\n", processIdentity, severity, (int)cchEntry, entry);
return 0;
}
The function takes five parameters:
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 string containing the statement text.
cchEntry is the length of the C-style string pointed to by entry.
Providing the string length as well as guaranteeing that the statement string is 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.fail - Always fails initialisation. Used in the automated unit/component testing
be.file - Outputs to a file
be.fprintf - Outputs to stdout/stderr. This is a full-featured, portable version of the trivial example above
be.null - Outputs to the "bit bucket". This is used in performance testing.
be.speech (currently Windows-only) - Outputs in the form of speech.
be.syslog (UNIX-only) - Outputs by emitting Syslog packets.
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.
be.WindowsEventLog (Windows-only) - Outputs to the Windows Event Log. Severity levels are translated to Event Log categories (EVENTLOG_ERROR_TYPE, EVENTLOG_WARNING_TYPE, EVENTLOG_INFORMATION_TYPE).
be.WindowsMessageBox (Windows-only) - Outputs to a Windows message box. Severity levels are translated into message box types (MB_ICONERROR, MB_ICONWARNING, MB_ICONINFORMATION).
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.
Please feel free to post questions on the forums on the Pantheios project site on SourceForge.
History
- 9th September, 2008: Initial version