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