CIS Home http://cisweb.bristol.mass.edu/~ik/

Unilog Architecture

Unilog (Universal Logging Facility) server provides named shared memory to the entire operating system. Unilog client opens the shared memory by name and starts writing into it. The writing is organized into 32-bit -sized blocks of memory. The unilog server creates two threads. The reader thread monitors shared memory and moves used blocks into internal server queue. The writer thread takes blocks from the queue and stores them into the log file on the hard drive.

Unilog client is any thread that wants to write into the log. Unilog library provides API functions to initialize, write, and release shared memory. Unilog API is thread safe. The client is not required to implement any synchronization mechanisms and may call log API on any thread.

For smaller log files, all log information is stored in binary form. Shared memory-based logging simplifies the task of locating, opening, and closing log files by the application threads.

Logging

First, UnilogServer.exe server should be started. The server initializes and uninitializes the shared memory.

The client should initialize and uninitialize logging on the same thread. In MFC projects, Application::InitInstance() and ExitInstance() are good candidates for unilog initialization/uninitialization. In ATL projects, FinalConstruct() and FinalRelease() are the candidates.

If logging is used inside a DLL that is loaded into memory by executable that has already completed unilog initialization, DLL may proceed directly to using logging APIs; no initialization is needed.

Unilog Client API

Function

Description

DWORD unilog_final_construct();

Global function: performs unilog initialization and opens shared memory

Return values: ERROR_SUCCESS or ERROR_NOT_READY indicating that shared memory has failed to properly initialize.

DWORD unilog_final_release();

Global function: closes shared memory

Return values: ERROR_SUCCESS or ERROR_LOCK_VIOLATION - Unilog internal error. This will assert in debug build.

DECLARE_UNILOG_ID

Macro: creates an integer variable, named UNILOG_ID, which contains the actual address of the execution. The address is than used as unique identifier of the client log records. Usage example:

void MyFunction() 
{
    DECLARE_UNILOG_ID;
    CUnilog unilog( UNILOG_ID, __LINE__ );
    // The rest of the MyFunction
}

CUnilog C++ class

C++ helper class for accessing unilog logging functions

In C++ source files, it is necessary to instantiate an object of this helper class before you can access unilog logging functions. Constructor of this class will guarantee that the shared memory is properly initialized, and if it isn?t, the logging will be silently turned off.

CUnilog::CUnilog(
    DWORD         LocationID
    unsigned short LineNumber = 0,
    bool          AutoLog = true,
    DWORD         WriterTimeout
    = UNILOG_SHARED_MEMORY_WRITER_TIMEOUT
    );

Helper class constructor. Has a list of parameters to control logging behavior of the helper object.

Parameters:

LocationID: Required parameter; normally UNILOG_ID created by the DECLARE_UNILOG_ID macro.

LineNumber: The line number of the source file; usually __LINE__

AutoLog: If true, constructor of the object will automatically log event UNILOG_EVENT_TYPE_ENTER; the destructor of the objects will automatically log event UNILOG_EVENT_TYPE_EXIT. The default value for this parameter is true.

WriterTimeout: Timeout value for all logging functions in milliseconds. The default value is 500 milliseconds.

Usage example:

void MyFunction() 
{
    DECLARE_UNILOG_ID;
    CUnilog unilog( UNILOG_ID, __LINE__, false );
    unilog.LogText( __LINE__, "Inside MyFunction" );
    // The rest of the MyFunction
}
DWORD CUnilog::LogFormat(
    unsigned short LineNumber,
    const _TCHAR* text_format,
    ...
);

CUnilog class method to log text message with variable list of parameters. Supports common printf() formats.

DWORD CUnilog::LogEvent(
    int unilog_event
);

CUnilog class method to log an event; for example, entry into a function, exit from function, or issue command to the unilog server to create new log file.

Parameter can be one of the following:

UNILOG_EVENT_TYPE_ENTER - Indicates entry into a function

UNILOG_EVENT_TYPE_EXIT - Indicates exit from the function

UNILOG_EVENT_TYPE_NEW_LOG - Posts a message to the unilog server to generate new file name.

UNILOG_BLOCK_TYPE_CHECKPOINT - special type of log for manual debugging.

UNILOG_BLOCK_TYPE_ALERT - special type of log for manual debugging.

Note: if automatic logging is enabled, LogEvent( ) is called implicitly by the constructor/destructor of the CUnilog object, For example,

void MyFunction( int param ) 
{
    DECLARE_UNILOG_ID;
    CUnilog unilog( UNILOG_ID, __LINE__ );
    // The rest of the MyFunction
}
DWORD LogText(
    unsigned short LineNumber,
    const _TCHAR* text_message
);
                        

CUnilog class method to log simple text message. For example:

void MyFunction() 
{
    DECLARE_UNILOG_ID;
    CUnilog unilog( UNILOG_ID, __LINE__ );
    unilog.LogText( __LINE__, "Inside MyFunction" );
    // The rest of the MyFunction
}

                        
UNILOG_TRACING_ON
                        

Macro to activate traces via UNILOG_AUTO_TRACE and UNILOG_TRACE. UNILOG_TRACING_ON should be defined before UnilogClient.h is included.

If UNILOG_TRACING_ON is not defined, UNILOG_AUTO_TRACE and UNILOG_TRACE have no effect.

UNILOG_AUTO_TRACE( __LINE__ )
                        

Macro equivalent of automatic tracing via

    DECLARE_UNILOG_ID;
    CUnilog unilog( UNILOG_ID, __LINE__ );
                        
UNILOG_TRACE( __LINE__, MSG )
                        

Macro equivalent of

    unilog.LogText( __LINE__, MSG )
                        
UNILOG_DISABLE_AUTO_TRACE
                        

Macro to prevent automatic traces inserted by unilog trace inserter utility. Put this in your function as a first line of code to hint trace inserter that this function should not be modified.

Programmers, on the other hand, can manually instrument the function.

UNILOG_DISABLE_AUTO_TRACE is an empty macro and has no effect on the program execution.

UNILOG_INIT( __LINE__ )
                        

Macro equivalent of

    if ( unilog_final_construct() != ERROR_SUCCESS ) {
        unilog_final_release();
    } else {
    	CUnilog( UNILOG_ID, LINE, false ).LogEvent(
    	    UNILOG_BLOCK_TYPE_CHECKPOINT );
    }
                        

Note that this macro requires UNILOG_ID. Typical usage:

int main()
{
    UNILOG_DISABLE_AUTO_TRACE;
    DECLARE_UNILOG_ID;
    UNILOG_INIT( __LINE__ );
    //...
    UNILOG_RELEASE;
	return 0;
}
                        
UNILOG_RELEASE
                        

Macro equivalent of

    unilog_final_release();
                        

IMPLEMENTATION DETAILS

How shared memory is accessed by clients and server

The server uses CreateFileMapping( ) and MapViewOfFile( ) Win32 APIs to create a handle to the shared memory and map that memory into the process. The clients use the same set of functions to open the shared memory created by the server.

Shared memory is organized into 32-byte sized blocks. Each block includes the following data fields:

Unique trace number

Type of logged event (e.g. entering/exiting functions, logging text messages)

Trace location ID (relevant virtual addresses inside caller binary image)

Process ID

Thread ID

UNC Date/Time of the trace

Source file line number

Long text messages are split into smaller blocks of text if necessary. Each text block is assigned its sequence number and the number of the text header block. The viewer application reassembles these blocks into the original text strings.

The blocks of shared memory have their own headers containing lock counters and ?in use? flags. In addition, there is shared memory header containing field for generating unique trace numbers. The entire shared memory occupies 8K of memory.

Unilog clients and server access memory and block headers using Intel interlocked exchange facilities, wrapped by Interlocked Win32 family of functions. MSDN refers to these APIs as a reliable and efficient way of synchronizing access to the memory shared between multiple processes. Another advantage is that interlocked exchange functions do not require any initialization, when compared to windows synchronization objects, like mutexes or events.