ALib C++ Library
Library Version: 2402 R1
Documentation generated by doxygen
No Matches
ALox - Tutorial

1. Hello ALox

Welcome to the ALox for C++ Tutorial! This tutorial aims to introduce you to ALox as quickly as possible and put you into a position to decide whether ALox is the right logging library for you.

Let us start with the very quick "hello world" sample:

#include "alib/alox.hpp"
int main( int, const char** )
// bootstrap ALib
// the main program
Log_Info ( "Hello ALox!" )
// alib termination
return 0;

When this program is run, the output should look similar to this

    myproject/mysourcefile.cpp:7:main [0.001 +003 µs][PROCESS][/] #001: Hello ALox!

So, with one line of code we get a Log Statement that incorporates:

  • The source file, line number and method, which - depending on the IDE you use - is 'clickable' which means a click in the output window opens the source file and highlights the line where the Log Statement is placed.
  • The time that passed since the process started (here 0.001 which means a millisecond after start)
  • The time that passed since the last log line: 3 microseconds. This information is handy for a first impression on the execution speed of your methods.
  • The name of the thread executing the log (automatically identified as the main thread named PROCESS in this case).
  • The log domain "/", which is to be explained later.
  • The log counter #001 , which e.g. is useful to quickly find the right output lines in your next debug-run.
  • Finally the log message itself.

In the release build of your software, this line of code is completely pruned away. But, as we will see later, also in debug compilations you can selectively disable log output. And this is not just done using "log levels" like "Info", "Warning", "Error", etc. Instead you assign your log statements a "domain" and disable or enable these domains.
This means: with ALox you can keep your debug Log Statements in your code forever!

Let us quickly start a new chapter of this tutorial and go into more details.

2. Class Lox and ALox C++ Macros

The previous chapter showed the shortest possible sample program which just bootstrapped everything to reasonable default options.

ALox has some concepts that need a little more time to understand. Before we see more code, we have to take some prerequisites.

ALox is designed to support debug logging (log output that should appear only in the debug version of your software) as well as release logging. In C++ such pruning is (still ) done best by using preprocessor macros. Therefore, you will see a lot of macros in the code samples given here. We want you to understand a little of the class design of ALox , while you are reading yourself through this tutorial. This is why a very brief explanation of the macros is given here:

  • The most important interface into ALox is class Lox , which is a container to hold one or more instances of abstract class detail::Logger . Through the interface of class Lox, the log statements are invoked. Log statements are forwarded to the Loggers attached.
  • The most simple case is that you have just one Lox instance, holding exactly one Logger which logs its output to the standard output stream of your process (e.g. AnsiConsoleLogger ). ALox supports much more complex scenarios and the preprocessor macros must support this as well.
    In this tutorial we stick to the simple case, maybe adding a second Logger here and there.
  • Most of the macros that we are using here have the form "Log_Xyz()". What these macros are doing is basically two things:

    • Invoke the method Xyz() on a predefined singleton of class Lox. This predefined singleton is the default (configurable) container for all debug logging output. Macros of this type will be pruned in release compilations of your software.
    • Pass scope information to the ALox , so that the file name, line number and method name of the occurrence of a log statement can be included in the log output.

    So, when you see the line

      Log_Info( "Hello ALox" )

    This means that method Info is invoked on the predefined singleton of class Lox which is by default used for debug logging.

  • In addition to the macros that actually perform a Log Statement or otherwise interface with class Lox, macro Log_Prune exists, which in the release version of your software, removes any code it gets passed. This macro is needed for things that do not represent single invocation of methods of class Lox, e.g.:
    • creation and manipulation of Loggers
    • changing log output format
    • code to assemble more complex log messages prior to logging them

In the tutorial language we do not further explain these macros. So do not be surprised when the presented sample code shows a macro, while the explaining text refers to the method invocations that are represented by these macros. For a information on ALox macros for logging, please refer to Programmer's Manual appendix chapter C.2 ALox Macros.

3. Create a tutorial project or equip your current project with ALox

If you want to follow the tutorial samples and play around with it a little, you should either create a tutorial project or right away equip your current project with ALox . For information about how you do that see chapter 5. Building The Library of the Programmer's Manual of ALib C++ Library .

Of-course, you can easily skip this step and just continue reading the tutorial without setting up ALox now. For some IDEs, there might be a ready to use solution available that minimizes efforts when you want to follow the samples and play around a little.

To include all headers necessary for this tutorial, add the following statements to your source file:

And add a statement to use namespaces std and alib to your source file:

using namespace std;
using namespace alib;
Although ALib and ALox are organized in several sub-namespaces (below alib respectively alib::lox), it is enough in most cases to just 'use' namespace alib. The mechanics and design goal are explained in the documentation of namespace alib.

4. Class Logger

While class Lox is the main interface to ALox logging, abstract class Logger is performing the log. 'Performing' here means that derived instances of this abstract class are responsible for writing the 'Logables' (the message or data that is logged) to a 'drain', for example the console or a text file.

Before we can start logging, we need to create one or more Loggers and attach them to our instance of class Lox.

In the shortest sample shown above (Hello ALox) we have not done this. For debug logging ALox notices this and automatically creates a Logger.

You need to identify the right place in your application where to do this. A good place is the "main()" method or any other place of your project where some "bootstrapping" of your application takes place. Add the following line:

This tries to identify the best Logger type that fits to your console type. E.g. If the process runs on an ANSI enabled console or a console window on Windows OS, colorful log output is enabled. Sometimes, this method even creates two Loggers: one for the console and one for your IDE output, when ALox is able to detect and support such IDE.
For information about what is currently supported and how (with environment variables, configuration files or command line parameters) this behavior can be changed, see methods Log::AddDebugLogger and Lox::CreateConsoleLogger which this macro invokes.

We never interface with a Logger directly. After it is created and added to the Lox, we can mostly forget about it. For almost all the rest of this introductory tutorial, all you need to know is that Loggers created with this method, write log messages in textual format e.g. to standard output stream (and/or to the application console). As said above, other Loggers could be added in parallel that use other "drains" to write the output to, e.g. files, a metrics server in the cloud, etc.

Together with bootstrapping the library and the first Log Statement of the introductory sample, your projects code (e.g. in method main) should look like this:

// Dont forget to bootstrap on top of main()! It is removed here, because this code runs
// in the unit tests, where bootstrapping was already performed.
// alib::Bootstrap();
Log_Info ( "Hello ALox" )

5. Run the Application

When you run your project, output similar to this:

ut_dox_tutorial.cpp:734:Hello_ALox [0.000 +753 µs][PROCESS]     [/]#001: Hello ALox

should appear in your IDE or your console.

From the meta information of the log output above you can see, that this tutorial has its sample code embedded in unit tests and the output is captured when running them and inserted here.

5.1. Run your application within your IDE

If you are using an IDE supported by ALox (e.g. Visual Studio in Windows), you can double click the log line in the output window. The IDE opens the source and selects the line of code where the log call was invoked. This means, each Log Statement is "clickable" in your IDE and links to its source code line in the editor. This is a tremendous help for developers when debugging their code.

Besides the fact that you can click it, users of ALox are saving even more time. Developers, when using standard "debug print lines", often phrase textual info like:

    "attn: dbman.savedata(): oops could not store data....ERROR!"

hence, putting some info about the place in the code, where they have put the debug statements. As you see from the ALox output above, information about the caller of the Log Statement is all included automatically. Advantages are:

  • lines are clickable and link to the editor (as mentioned above)
  • less to type when adding debug print lines
  • no need to change the Log Statement when you copy/paste a debug print line to a different class/method or when you rename your class/method

Furthermore, as we will see later, the log message type (which seems to be "ERROR!" in the phrase above) is standardized in ALox and does not need to be phrased in your message text.

Besides scope information and message type, you automatically get:

  • the relative time of the log execution (time since application started)
  • the time elapsed since the last log
  • the ID of the thread that executed the method

The output format and detail of the log line is configurable in ALox . The line above just shows the default. Details you see can be removed or other details can be added (e.g. the absolute date / time instead of only the relative).

Should your IDE not be supported by ALox in respect to generate "clickable log output lines" (aka lines that link automatically to the right point in your source code) it might be the case that only by adjusting the log output format a little, your IDE learns how to read and interpret ALox logging!
You should adjust your IDE in a way that the output window spans over the complete width of the main window. Also, depending on your screen size, you might decrease the font size of the output window a little. The output gets wide, because a lot of valuable information is logged besides the log message. In addition, the output is organized in columns that auto adjust their size. This takes a wider output into account for the benefit of largely improved readability.

5.2. Build a release version

Switch your project configuration to "Release" and run the application. The output should not appear!

Even better: The ALox code is not even compiled into the release target. So, whatever you are logging out during developing and debugging your software, it is all automatically gone in the release version.

This is achieved by providing the right set of Compilation Symbols when setting up the project. Only project configurations that have the symbol ALOX_DBG_LOG set to 1 will contain ALox debug logging statements.

This has a lot of benefits:

  • your release code executes faster,
  • your release executable gets a smaller footprint and
  • you are not delivering your debug language to the end user (not even if your executable is reverse engineered).

6. Controlling the 'Verbosity'

The code above uses the method Lox::Info (embedded in a preprocessor macro) to create the log output. There are three other Versions of that method, together constituting the four 'Verbosities':

Let us see what happens when extending our sample as follows:

Log_Error ( "A severe error happened :-(" )
Log_Warning( "This is a warning :-/ Maybe an error follows?" )
Log_Info ( "Just for your further information!" )
Log_Verbose( "Today, I am in the mood to talk..." )

If you run your application now (in "Debug" mode), the following output should appear:

ut_dox_tutorial.cpp:761:ALoxTut_Verbosity [0.000 +382 µs][PROCESS][ERR][/]#001: A severe error happened :-(
ut_dox_tutorial.cpp:762:ALoxTut_Verbosity [0.000 +020 µs][PROCESS][WRN][/]#002: This is a warning :-/ Maybe an error follows?
ut_dox_tutorial.cpp:763:ALoxTut_Verbosity [0.000 +011 µs][PROCESS]     [/]#003: Just for your further information!
ut_dox_tutorial.cpp:764:ALoxTut_Verbosity [0.000 +009 µs][PROCESS][***][/]#004: Today, I am in the mood to talk...

The little space in the meta information, right after the thread name [main] fills with text-markers [ERR], [WRN] and [***], giving info about which verbosity a Log Statement defines.
In this sample, we are using just one source file, thread name, etc. When more variety appears, ALox will automatically set tabulators in the meta information of the log output, so that this information is almost always at the same column. This makes it very easy for example to identify those log messages of Verbosity 'Error'.

If on your machine, ALox detects a console that supports colorizing the log output, method Log.AddDebugLogger will choose a colorful implementation of class Logger and you will see the different output lines in different colors instead of these little text-markers. This helps 'visually filtering' log lines even better.

Now, we want to control the Verbosity of the log output. Let's say we just want to see 'Error' and 'Warning' messages, and suppress those of Verbosity 'Info' and 'Verbose'. ALox allows to control this on a 'per Logger' basis using method Lox::SetVerbosity. We add the line:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Warning )

Here, we are explicitly passing the Logger object that ALox creates as a static singleton for debug-logging purposes, with method AddDebugLogger(). Instead of passing the reference to the object, we could as well use a Loggers' name. The Logger that method AddDebugLogger() creates is named "DEBUG_LOGGER". Therefore, the line of code shown above can alternatively stated as:

Log_SetVerbosity( "DEBUG_LOGGER", Verbosity::Warning )

Our sample now looks like this:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Warning )
Log_Error ( "A severe error happened :-(" )
Log_Warning( "This is a warning :-/ Maybe an error follows?" )
Log_Info ( "Just for your further information!" )
Log_Verbose( "Today, I am in the mood to talk..." )

and produces the following output:

ut_dox_tutorial.cpp:790:ALoxTut_Verbosity [0.000 +516 µs][PROCESS][ERR][/]#001: A severe error happened :-(
ut_dox_tutorial.cpp:791:ALoxTut_Verbosity [0.001 +018 µs][PROCESS][WRN][/]#002: This is a warning :-/ Maybe an error follows?

As you see, only the Log Statements with Verbosity 'Error' and 'Warning' survived.

Obviously ALox uses the attribute Verbosity, defined in enum class Verbosity two times:

  • every Log Statement has a Verbosity assigned.
  • A Logger attached to a Lox has a Verbosity assigned.

Then ALox matches both attributes to decide whether a Log Statement is executed with a Logger or not.

Some notes on method SetVerbosity:
  • The first time this method is called for a Logger, this method internally 'registers' the given Logger with the Lox object. In other words, there is no other method (and no need!) to register a Logger with a Lox.
  • To switch off all log output, use Verbosity.Off.
  • To remove (for any reason) a Logger from a Lox, use method Lox::RemoveLogger.
  • Method AddDebugLogger, by default sets Verbosity.Verbose for the Logger created.

Now, we are able to control the Verbosity of the 'overall' log output of our application. But probably, in bigger projects, we need more 'granularity' for such control. The next section tells how!

7. Log Domains

Controlling the Verbosity of the log output, including switching it completely off, is of-course a core feature of any logging library. ALox allows to control such Verbosity for different Log Domains. A Log Domain can be seen like a key-word associated with just every Log Statement. This way, Log Domains are sorting each and every Log Statement used in an application into a specific subset.

Until now, we have not used and set any Log Domain in our samples. We have just omitted the parameter in our Log Statements and this way the parameter defaulted to an empty string. Look at the following two Log Statements:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Verbose ) // the default anyhow
Log_Verbose( "HTTP", "Connected" )
Log_Verbose( "UI", "Somebody moved the mouse!" )

As you see, two parameters are given now. The first denotes the Log Domain. The string provided can be arbitrarily chosen. Let us quickly look at the log output:

ut_dox_tutorial.cpp:818:ALoxTut_Domains [0.000 +381 µs][PROCESS][***][/HTTP]#001: Connected
ut_dox_tutorial.cpp:822:ALoxTut_Domains [0.000 +018 µs][PROCESS][***][/UI  ]#002: Somebody moved the mouse!

In the meta information of the log output, just before the line number, formerly appeared [/] (what we did not explain, yet). Now it says [/HTTP], respectively [/UI]. Obviously this field of the meta information denotes the Log Domain associated with the Log Statement.

The Log Domains chosen in this sample obviously group the Log Statements of the application into two sets, one for Log Statement concerning information about the 'user interface', the other about HTTP communication. Now, we can control the Verbosity of the two sets independently. We are using another default parameter that we previously omitted. Imagine, the UI of your application works well, but you have some problems with HTTP connections:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Verbose, "HTTP" ) // our interest
Log_SetVerbosity( Log::DebugLogger, Verbosity::Error, "UI" ) // only if ouch!
Log_Verbose( "HTTP", "Connected" )
Log_Verbose( "UI", "Somebody moved the mouse!" )

Now the output is:

ut_dox_tutorial.cpp:840:ALoxTut_Domains [0.001 +673 µs][PROCESS][***][/HTTP]#001: Connected

Although both Log Statement share the same Verbosity, only the one that belongs to Log Domain "HTTP" is shown.

The next section tells us more about Log Domains. You might already guess what this is when looking at the meta information of the log output, showing [/HTTP], [/UI] and [/] for the Log Domains!

8. Hierarchical Log Domains

ALox organizes Log Domains hierarchically. A first advantage of this is that it becomes easy to switch Verbosity of a whole set of Log Domains by controlling the parent.

Look at the following sample:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Verbose ) // the default anyhow
Log_Info ( "UI/MOUSE", "A mouse click" )
Log_Verbose( "UI/MOUSE", "Somebody moved the mouse!" )
Log_Info ( "UI/DLG", "About dialog opend" )
Log_Verbose( "UI/DLG", "About dialog, link to product page pressed." )

and its output:

ut_dox_tutorial.cpp:873:Tut_HierDom [0.000 +380 µs][PROCESS]     [/UI/MOUSE]#001: A mouse click
ut_dox_tutorial.cpp:875:Tut_HierDom [0.000 +016 µs][PROCESS][***][/UI/MOUSE]#002: Somebody moved the mouse!
ut_dox_tutorial.cpp:878:Tut_HierDom [0.000 +013 µs][PROCESS]     [/UI/DLG  ]#003: About dialog opend
ut_dox_tutorial.cpp:880:Tut_HierDom [0.000 +011 µs][PROCESS][***][/UI/DLG  ]#004: About dialog, link to product page pressed.

We can use a slash ( '/') to separate Log Domains and organize them in a tree structure, just as we do with directories in a file system. In the sample above, Log Domains "DLG" and "MOUSE" are Sub-Log Domains of Log Domain "UI".

With this information, it is important to understand that method Lox::SetVerbosity always sets the given Log Domain and all its sub-domains to the Verbosity value provided. Consequently, the following statement switches Log Domains "UI", "UI/MOUSE" and "UI/DLG" to the same Verbosity.Warning:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Warning, "UI" ) // Always sets all sub-domains!

If we were interested how mouse events are processed in our application, we might do invoke:

Log_SetVerbosity( Log::DebugLogger, Verbosity::Warning, "UI" ) // First set parent...
Log_SetVerbosity( Log::DebugLogger, Verbosity::Verbose, "UI/MOUSE" ) // ...then children!

The order of these two statements is important: If they had been written the other way round, then the setting of Log Domain "UI" had overwritten that of Log Domain "UI/MOUSE".

You might wonder why there is no second version of the method available (or an optional parameter) that allows to manipulate only the Log Domain given, without touching its Sub-Log Domains. There are good reasons for this and these are explained in the Programmer's Manual. It is also explained there, that there is a way to stop recursion and in which situations this might be useful. But for now and in standard situations: The setting of Verbosity is recursive!

Almost everywhere ALox accepts a Log Domain name as a parameter, a domain path can be given. This even works with the dot and dot-dot syntax that we know from file paths. Also, relative and absolute path names can be given. Here are some samples:

    "DOM"          // a relative path name
    "./DOM"        // same as "DOM"
    "/DOM"         // an absolute path name
    "/DOM/XYZ/.."  // same as "/DOM"
    "/DOM/.."      // same as "/"
    "DOM/XYZ/.."   // same as "DOM"
    "DOM/./XYZ"    // same as "DOM/XYZ"

Now, we are able control the Verbosity of a (sub-)tree of Log Domains. This enables us to write very complex software and still manage log output far beyond the classical "log level" way, that other logging libraries offer. Remember: The primary goal of ALox is that you never again remove any log statement. You keep them forever, and should your code be perfect, well, then you disable them forever. In case something goes wrong, even years after the initial creation of the code, the log statements in the concrete area of concern can be simply activated again.

But the concept of hierarchical organization of Log Domains is even more powerful, as you will learn in the next section.

9. Scope Domains

As we saw, optional parameter domain of Log Statements allows us to group the log output into different areas of interest and control the Verbosity per group. This is nice, but

  • We have to type more
  • We have to recall the 'actual' Log Domains' name when we insert a Log Statement
  • The Log Statement become less easier to read (remember: Log Statement should fit to your code like comment lines)
  • When copy/pasting code, all Log Domain might have to be changed to the destinations' domain name
  • When we want to change a Log Domain name, we have to change it everywhere we use it.

To avoid all this (and gain even more cool features) ALox provides a feature called Scope Domains.

The term Scope is known from your programming language. For example, a variable declared within a class method, 'automatically' carries the scope of that method. This means, it is not visible outside of that method. ALox uses a similar approach: All Log Statements within a method might carry the same Log Domain. In other words, we can set a 'default value' for a Log Domain to be used for all Log Statements of a method.

The interface in ALox which provides this feature is found with the set of overloaded versions of Lox::SetDomain. Here is a sample:

void* Extract( const alib::NString& fileName, void* buffer )
Log_SetDomain( "ZIP/EXTRACT", Scope::Method ) // set Scope Domain path for this method
Log_Info( "Extracting {!Q}", fileName )
Log_Info( "Success" ) // a nice, clear, local, copyable log statement!
return buffer;

and its output:

ut_dox_tutorial.cpp:613:Extract [0.000 +492 µs][PROCESS]     [/ZIP/EXTRACT]#001: Extracting ""
ut_dox_tutorial.cpp:616:Extract [0.000 +021 µs][PROCESS]     [/ZIP/EXTRACT]#002: Success

You see, all disadvantages we just listed are blown away using Scope Domains.

In the sample above, we have used Scope::Method. Another type is Scope::Filename which "binds" a given Log Domain path to all Log Statements within a source file. As with scopes in your programming language, Scopes in ALox are nested and of-course Scope::Filename is an 'outer Scope' of Scope::Method. For a single Log Statement, both Scope Domains might apply at the same time. With having hierarchically organized Log Domains, ALox concatenates all Scope Domains applicable. Look at the following sample:

class Zipper
Log_SetDomain( "ZIP", Scope::Filename ) // set Scope Domain path for this class (filename)
Log_Info( "Zipper created" ) // domain "ZIP"
void* Compress( const alib::NString& fileName, void* buffer )
Log_SetDomain( "COMPRESS", Scope::Method ) // set Scope Domain path for this method
Log_Info( "Compressing {!Q}", fileName )
Log_Info( "Success" ) // domain "ZIP/COMPRESS"
return buffer;
void* Extract( const alib::NString& fileName, void* buffer )
Log_SetDomain( "EXTRACT", Scope::Method ) // set Scope Domain path for this method
Log_Info( "Extracting {!Q}", fileName )
Log_Info( "Success" ) // domain "ZIP/EXTRACT"
return buffer;

and its output when both sample methods are executed:

ut_dox_tutorial.cpp:631:Zipper      [0.000 +016 µs][PROCESS]     [/ZIP         ] #001: Zipper created
ut_dox_tutorial.cpp:639:Compress    [0.001 +016 µs][PROCESS]     [/ZIP/COMPRESS] #002: Compressing ""
ut_dox_tutorial.cpp:642:Compress    [0.001 +013 µs][PROCESS]     [/ZIP/COMPRESS] #003: Success
ut_dox_tutorial.cpp:651:Extract     [0.001 +015 µs][PROCESS]     [/ZIP/EXTRACT ] #004: Extracting ""
ut_dox_tutorial.cpp:654:Extract     [0.001 +012 µs][PROCESS]     [/ZIP/EXTRACT ] #005: Success

Imagine, the source file of class Zipper resides in a source directory, together with other 'utility classes'. Somewhere in these classes, probably at the place where this 'utility library' is initialized (once, when bootstrapping a process), the following statement might be added:

Log_SetDomain( "UTIL", Scope::Path )

This sets Log Domain "UTIL" for Scope.Path, which is again an outer scope of Scope.Filename. Without changing the code in class Zipper, invoking its methods then leads to the output:

ut_dox_tutorial.cpp:631:Zipper      [0.001 +016 µs][PROCESS]     [/UTIL/ZIP         ] #001: Zipper created
ut_dox_tutorial.cpp:639:Compress    [0.001 +015 µs][PROCESS]     [/UTIL/ZIP/COMPRESS] #002: Compressing ""
ut_dox_tutorial.cpp:642:Compress    [0.001 +012 µs][PROCESS]     [/UTIL/ZIP/COMPRESS] #003: Success
ut_dox_tutorial.cpp:651:Extract     [0.001 +014 µs][PROCESS]     [/UTIL/ZIP/EXTRACT ] #004: Extracting ""
ut_dox_tutorial.cpp:654:Extract     [0.001 +013 µs][PROCESS]     [/UTIL/ZIP/EXTRACT ] #005: Success

What happens when Scope Domains are set and we still use optional parameter domain with a Log Statement? Let us just try out:

Log_SetDomain( "METHOD", Scope::Method )
Log_Info( "No domain parameter given" )
Log_Info( "PARAM", "Domain parameter \"PARAM\" given" )
ut_dox_tutorial.cpp:981:ALoxTut_ScopeDomains [0.001 +014 µs][PROCESS]     [/METHOD      ] #001: No domain parameter given
ut_dox_tutorial.cpp:982:ALoxTut_ScopeDomains [0.001 +011 µs][PROCESS]     [/METHOD/PARAM] #002: Domain parameter "PARAM" given

As you see, the path provided with parameter domain gets appended to the path of Scope Domains evaluated. You can consider this parameter being a 'local' Scope, or an inner scope of Scope::Method!

Finally, imagine you want to 'overwrite' current applicable Scope Domains and direct a certain Log Statement to a different Log Domain. All you need to do is to use an absolute domain path with parameter domain :

Log_SetDomain( "READ", Scope::Method )
Log_Info( "Reading file" )
Log_Info( "/CONFIG", "Path not found." )
ut_dox_tutorial.cpp:990:ALoxTut_ScopeDomains [0.001 +095 µs][PROCESS]     [/READ        ] #001: Reading file
ut_dox_tutorial.cpp:993:ALoxTut_ScopeDomains [0.002 +017 µs][PROCESS]     [/CONFIG      ] #002: Path not found.

In this sample, the second Log Statement uses an absolute path. The reason is, that the error dectected in the code, obviously belongs to a different topic. The sample suggests, that it is not related to reading a file, but it is related to a wrong configuration. So, the log output goes directly to the corresponding domain.

This is enough about Scope Domains in the context of this tutorial. All details about Log Domain and Scope Domains are found in the Programmer's Manual. Among other things, you will find:

  • Information on a Scope.Global
  • How to use "outer \e Scopes" of Scope.Path, which allows to set Log Domains for a complete subtree of your source files at once
  • Information on "thread-related" Scopes, which allow to change Log Domain on a per thread basis in different ways. For-example, this allows to create log output only if a certain thread runs a code.
  • More samples and use cases, that make ALox the most powerful and unique logging library.

10. Formatting

While talking about rather complicated things like Log Domains and Scopes, we must not forget to talk about the log output itself. ALox is designed to be able to pass any type of data to one or more Loggers. In the default case, a textual output of these "Logables" is wanted. We have seen in previous samples already, that if two objects are passed, their textual representation is just concatenated:

Log_Info( "Value=", 5 )
ut_dox_tutorial.cpp:1238:TestBody [0.000 +217 µs][PROCESS]     [/]#001: Value=5

But in fact, this concatenation is just the "fallback" strategy of a quite powerful formatting system coming with ALox . In short, ALox here relies on underlying library ALib , which provides abstract class Formatter allowing to format a set of arguments in accordance with a given format string that contains placeholder symbols.

Now there are two tricks implemented: First, if no format string is recognized, a simple concatenation is performed. This is what we already saw. But secondly, if one formatter does not recognize a format string, a next formatter can be asked.

Two formatters are provided by ALib and ALox (by default) installs both in parallel:

  1. FormatterPythonStyle
    Implements an extended version of Python String Fromat Syntax, which is also similar to what C# offers.
  2. FormatterJavaStyle
    Implements the formatting standards of the Java language, which in turn are a little like good old printf, but of-course much more powerful and also type-safe.

With this background information, it is no surprise that the line above can alternatively be written like this:

Log_Info( "Value={}", 5 )

which is Python compatible syntax, or like this:

Log_Info( "Value=%s", 5 )

which is how Java formatters declare format strings!

If more arguments exists than a format string "consumes", the next remaining argument is treated like the first one: if a format string is detected it is processed, otherwise the textual representation of the argument is just appended to the log output. Because of this, the following statements all produce the same log output:

Log_Info( "One-", "Two-", "Three" )
Log_Info( "{}-{}-{}", "One", "Two", "Three" )
Log_Info( "{}-{}-" , "One", "Two", "Three" )
Log_Info( "{}-" , "One", "{}-", "Two", "{}", "Three" )

Of-course it is even allowed to mix Python style and Java style format strings in one log statement:

Log_Info( "Python Style: {!s}","PS", " - ", "Java Style: \"%s\"", "JS" )

The output is:

ut_dox_tutorial.cpp:1266:TestBody [0.000 +001 µs][PROCESS]     [/]#008: Python Style: "PS" - Java Style: "JS"

However, please note that it is not allowed to mix Python and Java styles within one format string!

In general, we are recommending to use Python style syntax with ALox , because it is more powerful and probably also better readable. The full documentation of the formats and how this is adopted within the C++ version of ALib/ALox is found with documentation of classes FormatterPythonStyle and FormatterJavaStyle.

Here are a few more samples:

Log_Info( ">{:<10}<" , "left" )
Log_Info( ">{:>10}<" , "right" )
Log_Info( ">{:^10}<" , "center" )
Log_Info( ">{:10.3}<", 12.3456789 )
Log_Info( "Tab:{!Tab12}", "Stop" )
Log_Info( "Auto Tab:{!ATab}", "Stop" )
Log_Info( "Auto Tab XXX:{!ATab}", "Stop" )
Log_Info( "Auto Tab:{!ATab}", "Stop" )
Log_Info( "A quoted {!Q} string", "Placeholder" )
Log_Info( "A quoted {!Q} number", 395 )
Log_Info( "Upper {0!Q!up} and lower {0!Q!lo} conversion", "CaSe" )
Log_Info( "Hex: {:#x}. With group chars: {0:x,}", 0x11FF22EE )
Log_Info( "Oct: {:#o}. With group chars: {0:o,}", 012345670 )
Log_Info( "Bin: {:#b}. With group chars: {0:b,}", 145 )
ut_dox_tutorial.cpp:1273:TestBody [0.000 +044 µs][PROCESS]     [/]#009: >left      <
ut_dox_tutorial.cpp:1274:TestBody [0.000 +001 µs][PROCESS]     [/]#010: >     right<
ut_dox_tutorial.cpp:1275:TestBody [0.000 +001 µs][PROCESS]     [/]#011: >  center  <
ut_dox_tutorial.cpp:1276:TestBody [0.000 +001 µs][PROCESS]     [/]#012: >    12.346<
ut_dox_tutorial.cpp:1278:TestBody [0.000 +001 µs][PROCESS]     [/]#013: Tab:        Stop
ut_dox_tutorial.cpp:1280:TestBody [0.000 +001 µs][PROCESS]     [/]#014: Auto Tab:Stop
ut_dox_tutorial.cpp:1281:TestBody [0.000 +001 µs][PROCESS]     [/]#015: Auto Tab XXX:   Stop
ut_dox_tutorial.cpp:1282:TestBody [0.000 +001 µs][PROCESS]     [/]#016: Auto Tab:       Stop
ut_dox_tutorial.cpp:1284:TestBody [0.000 +001 µs][PROCESS]     [/]#017: A quoted "Placeholder" string
ut_dox_tutorial.cpp:1285:TestBody [0.000 +001 µs][PROCESS]     [/]#018: A quoted "395" number
ut_dox_tutorial.cpp:1287:TestBody [0.000 +001 µs][PROCESS]     [/]#019: Upper "CASE" and lower "case" conversion
ut_dox_tutorial.cpp:1289:TestBody [0.000 +001 µs][PROCESS]     [/]#020: Hex: 0x11ff22ee. With group chars: 11ff'22ee
ut_dox_tutorial.cpp:1290:TestBody [0.001 +001 µs][PROCESS]     [/]#021: Oct: 0o12345670. With group chars: 12'345'670
ut_dox_tutorial.cpp:1291:TestBody [0.001 +001 µs][PROCESS]     [/]#022: Bin: 0b10010001. With group chars: 1001'0001

The formatting system found with ALib module ALib BaseCamp provide a concept which is similar to method ToString() in the Java language or similar concepts in other languages: It allows to define a textual representation for just any C++ Type. It needs a little effort, but once in place you can drop just any of your objects into a log statement. This relieves you of getting the single attributes out of an object and write complicated long format strings, again and again.

Next, it is even possible to define new placeholder "languages" for the formatters. As a sample, for class DateTime such custom extension exists with the inclusion of alib/lang/system/calendar.hpp :

Log_Info ( "Custom Date Format: {:yyyy * MM * dd}", DateTime() )

The result is:

ut_dox_tutorial.cpp:1298:TestBody [0.001 +044 µs][PROCESS]     [/]#023: Custom Date Format: 2024 * 03 * 20

Details on how to do this are given in chapter 4.3. Formatting Custom Types of the Programmer's Manual of module ALib BaseCamp .

11. Conditional logging

11.1. Lox::Assert

Often log lines should only be displayed if a certain condition is met. Here is a sample:

int i= 0;
while( i < len )
if ( array[i] == search )
process( i );
if ( i == len )
Log_Error( "Nothing found :-(" )

The last two lines can be replaced by one using method lox::Lox::Assert as follows:

Log_Assert( i != len, "Nothing found :-(" )

Advantages are again less typing and better readability of the code. Furthermore, the C++ compiler would probably not prune the if statement if it was a more complex evaluation. As with using Assert() the evaluation of the expression is definitely pruned from your code. (Be sure you do not put side effects into expressions of Assert() invocations).

  • In accordance with the concept of assertions, the condition has to be false to have ALox perform the log.
  • The Verbosity of Assert() is Verbosity::ERROR.

11.2. Lox::If

A similar method to Lox::Assert this is Lox::If . The differences to Lox::Assert are:

  • The given condition is interpreted the other way round: if true, the Log Statement is executed.
  • The Verbosity of the Log Statement is not fixed but can (has to) be specified with parameter verbosity .

Hence, the very same effect as given in the previous sample can be achieved with:

Log_If( i == len, Verbosity::Error, "Nothing found :-(" )

11.3. Log.Once

Another useful Log Statement provided by ALox is Lox::Once . As the method name suggests, the statement

Log_Once( "I tell you this now only once!" )

will lead to a log output only the very first time that it is executed. This seems simple, and well it is - as long as you omit all optional parameters. There are quite a bit, which makes this statement extremely flexible.

All details are given in a dedicated chapter of the Programmer's Manual. Let us therefore just summarize some facts:

  • A Verbosity and Log Domain can be given (of-course)
  • A counter to increase the 'once' to 'n-times' can be given.
  • The method can be used to log a message every n-th time.
  • A key-word can be given, which combines different of such Log Statement to a set. Then the counter applies to this set.
  • Instead of a keyword, a Scope can be given. This implements "Log once per this \e Scope" or "Log n-times per this Scope"
  • A combination of a keyword and a Scope can be given. This implements "Log those statements of this group in this \e Scope n-times"

12. Prefixes

Imagine, all log statements of a of a certain kind should be very visible and distinguishable from other output. You could do this by

  • giving them a separated color
  • Indent them with spaces
  • Start each log line with a clearly visible 'marker text'.

These things can be achieved with ALox using method Lox::SetPrefix . With this method, additional Logables are passed to Loggers attached to a Lox. When logging text messages (we have not even talked in this tutorial about logging arbitrary objects) these objects are simply prepended to the log message itself.

Of-course, Prefix Logables can be defined according to ALox Scopes, language-related or thread-related ones. This means, you can do things like this:

  • Add a prefix to all statements of method, source file or source directory
  • Add a prefix to all statements executed by a certain tread.

Often, it is simpler to assign a Prefix Logable to a Log Domain. Also this is possible with overloaded versions of the method.

The real simple sample looks like this:

Log_SetPrefix( "ALOX TUTORIAL: ", Scope::Method )
Log_Info( "Well, just a sample" )
ut_dox_tutorial.cpp:1019:ALoxTut_Prefix [0.000 +452 µs][PROCESS]     [/]#001: ALOX TUTORIAL: Well, just a sample

For colorizing (depending on the availability of a console that supports colors) the following prefix would be suitable:

Log_SetPrefix( ESC::BG_MAGENTA, Scope::Filename )

We can not easily show colorful sample output in this tutorial. Try this yourself. ALox supports colorized output on ANSI consoles (GNU/Linux, etc.) and Windows OS command windows.

More and more complex use cases are elaborated in the Programmer's Manual, for example it is explained how log output can be 'recursively indented' with very simple statements. Recursive indentation is very helpful when logging in recursive algorithms or when structured, composed data objects are logged.

The next chapter shows that ALox has already built-in mechanics for logging out structured data!

13. LogTools: Log Complex Data

Ooopps, unfortunately, we have to (almost) skip this chapter in the C++ Version of ALox . While C# and Java provide runtime type information and class reflection (introspection), C++ does not. Therefore, we are missing the extremely handy LogTools class in C++, which in the other ALox incarnations recursively log complex objects automatically.

Therefore, if you want to dump more complex objects you have to extend your classes (and probably their subclasses) with corresponding helper functions that assemble a log string.

What helps to have a clear presentation of objects dumps within your log files are the concepts of Prefix Logable providing recursive indentation and multi-line logging that ALox provides.

Here is a sample output from the Java LogTools class that gives you and idea how nicely a dumped object might look, if recursive indentation and tabulators are used:

This is Java code, taken from the ALox for Java tutorial!
// Log multi-lines without meta info
((TextLogger) Log.getLogger( "Console" )).multiLineMsgMode= 4;

// Log current thread instance
LogTools.instance( "MYDOM", Verbosity.INFO, Thread.currentThread(), 4, "Actual Thread: " );

As you can see, we are just passing a quite complex object, namely the current thread, to the method. The parameter that follows the object which is to be logged determines the level of recursively logging composite objects. Here are the first lines of output of the above sample:

>> Actual Thread:                                                    {Thread}
>> <01>  name:                         "main"                        {[C}
>> <02>  priority:                     5                             {Integer}
>> <03>  threadQ:                      <null>
>> <04>  eetop:                        140565756819456               {Long}
>> <05>  single_step:                  false                         {Boolean}
>> <06>  daemon:                       false                         {Boolean}
>> <07>  stillborn:                    false                         {Boolean}
>> <08>  target:                       <null>
>> <09>  group:                                                      {ThreadGroup}
>> <10>    name:                       "main"                        {String}
>> <11>    maxPriority:                10                            {Integer}
>> <12>    destroyed:                  false                         {Boolean}
>> <13>    daemon:                     false                         {Boolean}
>> <14>    vmAllowSuspension:          false                         {Boolean}
>> <15>    nUnstartedThreads:          0                             {Integer}
>> <16>    nthreads:                   2                             {Integer}
>> <17>    threads:                    array[8]                      {[Ljava.lang.Thread;}
>> <18>      0: (Cyclic ref., see line <00>)                         {Thread}
>> <19>      1:                                                      {com.intellij.rt.execution.application.AppMainV2$1}
>> <20>      2: <null>
>> <21>      3: <null>
>> <22>      4: <null>
>> <23>      5: <null>
>> <24>      6: <null>
>> <25>      7: <null>
>> <26>    ngroups:                    0                             {Integer}
>> <27>    groups:                     <null>
>> <28>  contextClassLoader:                                         {sun.misc.Launcher$AppClassLoader}
>> <29>  inheritedAccessControlContext:                              {}
>> <30>    context:                    <null>
>> <31>    isPrivileged:               false                         {Boolean}

When implementing an object dump, you should not place a Log Statement for every attribute. In contrast, you should collect the whole dump within an AString (or any string type you like) and then log this String as a whole.

So, currently class LogTools just provides one static method for logging an Exception .

For future versions of ALox for C++, it is planned to provide:

  • Working with debug information to implement log tools for user defined classes
  • Preprocessor macros that standardize the customized logging of complex objects
  • Tools to log certain standard classes from STL, BOOST, QT or what have you. You are invited to send us your proposals of log tool code for common libraries to be included in our codebase.

14. Name Your Threads

In multi-threaded applications, ALox by default logs information about the thread id or name that executed the code containing your log statements. (This can be configured for ALox textual Loggers with class MetaInfo.)

Often threads do not have a name, but just an ID. It would be much nicer to see a name instead. ALox provides a simple mechanism to overwrite thread names with method Lox::MapThreadName .

Let us go back to the Hello ALox sample and add one line to rename the currently executed thread:

Log_Info ( "Hello ALox" )

The generated output is:

ut_dox_tutorial.cpp:1050:ALoxTut_ThreadName [0.000 +179 µs][BKGRND]     [/]#001: Hello ALox

As you see, the currently executed thread is now named BKGRND. All Log Statements that will be executed by this thread after the mapping takes place, will log this name.

Renaming threads in ALox will not rename the thread in your operating system. It is a purely ALox defined mapping of threads to ALox thread names.

The mechanism is simple, but it might not be simple to identify the right place in the code to put it! Some code can be executed from different threads, and sometimes (for example when using an UI framework) you might not even know exactly which thread will invoke your code. However, the good news is that each thread ID has to be mapped only once during the lifecycle of the program, but may be mapped multiple times. Therefore, the guideline is:

  • For threads that your own code starts, the name mapping should occur in the start code of-course.
  • Otherwise: Identify a piece of code from which you are sure that only one certain thread invokes that code.
  • Make sure that this code is not executed frequently (to avoid the overhead caused by the duplicated mapping ).
  • Make sure that this code is executed early in the lifecycle of your application, so that all log lines are equipped with the right thread name mapping.

For the sample of UI Frameworks, a good place to invoke Lox::MapThreadName is the initial creation callback of your main application UI component. Normally, it is enough to put this statement only in one component (the main one), because all other components will be initialized by the same Framework thread.

But as this is a convenience concept anyhow, it is good practice to not care too much about it at first. Later, when your application grows, you can check your log files periodically for new, unmapped thread IDs. When you see one, scroll up in the log file and try to identify the very first appearance of a this new ID. A double click on the log line will open the code that invoked the log. If you have populated your code with a reasonable amount of log entries, you will end up at the right place automatically! It also might be a good idea to restart your app with all domains set to Verbosity.Verbose and then look for the first appearances of unknown threads.

15. ALox Log Data

We hope that you are fully aware when reading through this tutorial, that all debug-log statements that are sampled here, are pruned from the release executables. One of the great benefits of ALox is that this removal of the statements is done quite automatically (depending on the language version you are using). Once, you have set-up your projects rightfully, your next release built is silent and does not contain your ALox Log Statements.

The concept of Log Data leverages this feature of ALox to allow to create variables that are existing exclusively with debug builds of your software. You can consider this feature as a "low hanging fruit" - not necessarily related with logging - but for ALox easy to implement and for the user easy to use. As the access to Log Data is exclusively done through the ALox API, the data is nicely 'encapsulated' and the probability of creating side effects is reduced in comparison to other ways to introduce temporary variables used for debugging.

The methods to store and retrieve Log Data with ALox are Lox::Store and Lox::Retrieve. The objects stored are of type Box. In the C++ implementation of ALox it is notable, that in the case that the data provided is not a value type (in respect to ALib Boxing , the data has to be kept in memory as long it is stored and potentially retrieved.

Here is a sample code which sets a debug variable in ALox providing a version of a file that was read by an application:

class FileIO
void Read( const alib::NString& fileName )
Log_SetDomain( "READ", Scope::Method )
Log_Info( "Reading {!Q}", fileName )
String fileVersion= nullptr;
// Identified file version
fileVersion= A_CHAR("3.1");
Log_Store( fileVersion, "FILE_VERSION" )
Log_Info( "Success" )

Now, whenever it might be interesting, this file version can be accessed:

Log_Retrieve( dbgFileVersion, "FILE_VERSION" )
Log_Info( "Working on file version {!Q}", dbgFileVersion.Unbox<String>() )

The output will be:

ut_dox_tutorial.cpp:672: Read     [0.000 +068 µs][PROCESS]     [/READ] #002: Reading "myfile.dat"
ut_dox_tutorial.cpp:684: Read     [0.000 +052 µs][PROCESS]     [/READ] #003: Success
ut_dox_tutorial.cpp:1209:TestBody [0.000 +027 µs][PROCESS]     [/    ] #004: Working on file version "3.1"

If it was not sure, that Log Data was set, then using macro Log_Retrieve as shown above, allows to do checks on the object returned, because it is declared as a local variable. When you are sure, that the retrieval returns an object of the type you expect, then the retrieval of the data can also be inlined. In this case, macro LOG_LOX should be used to name the right debug Lox:

Log_Info( "Working on file version {!Q}", LOG_LOX.Retrieve("FILE_VERSION").Unbox<String>() )

16. ALox Configuration Information and Internal Log Messages

In complex projects it might get a little confusing to keep track about the Loggers and their Verbosity, Scope Domains, Prefix Logables and Log Data set.

More importantly, when other software components of your project are using ALox as well (defining their own domains) then you might not know exactly which Log Domains get registered and used by those. The same applies when working in a team.

Let us create a sample, do some ALox configuration and then invoke method Lox::State:

// create two different loggers
Log_Prune( MemoryLogger memLogger; )
// reduce meta information to limit tutorial output width
Log_Prune( Log::DebugLogger->MetaInfo->Format.Reset( A_CHAR("[%tN]%V[%D](%#): " ) ); )
Log_Prune( memLogger.MetaInfo->Format.Reset( A_CHAR("[%tN]%V[%D](%#): " ) ); )
Log_Prune( memLogger.MultiLineMsgMode= 3; )
Log_SetVerbosity( &memLogger, Verbosity::Verbose )
// OK, let's use ALox
Log_SetDomain( "PNS" , Scope::Path + 1 )
Log_SetDomain( "PATH", Scope::Path )
Log_SetDomain( "FN", Scope::Filename )
Log_SetDomain( "THREAD", Scope::ThreadOuter )
Log_SetVerbosity( "MEMORY", Verbosity::Off , "/CON" )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Verbose )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Off , "/MEM" )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Error , "/UI" )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Info , "/UI/DLG" )
Log_Info( "This goes to both loggers" )
Log_Info( "/MEM", "This goes only to the memory logger" )
Log_Info( "/CON", "This goes only to the console logger" )
Log_Once( "Will we see this in the config?" )
Log_Once( "Will we see this in the config?", A_CHAR("ONCEKEY"), Scope::Filename )
Log_Store( "MyData 1" , Scope::Method )
Log_Store( "MyData 2" , "DataKey", Scope::Method )
Log_Store( 3 , "DataKey", Scope::Filename )
Log_Store( 4 , "DataKey", Scope::ThreadOuter )
Log_SetPrefix( "TPre: " , Scope::ThreadOuter )
Log_SetPrefix( "MPre: " , Scope::Method )
Log_SetPrefix( "DomPre: " )
Log_SetPrefix( "Mouse: ", "/UI/MOUSE" )
Log_SetPrefix( ESC::RED, "/ERRORS", lang::Inclusion::Exclude )
Log_MapThreadName( A_CHAR("TUTORIAL") )
// now, log the current config
Log_LogState( nullptr, Verbosity::Info, A_CHAR("The current configuration of this Lox is:") )

Lox::State gathers internal configuration information and logs it out. The output is quite self explanatory:

[PROCESS]     [/THREAD/PNS/PATH/FN](001): This goes to both loggers
[PROCESS]     [/MEM               ](002): This goes only to the memory logger
[PROCESS]     [/THREAD/PNS/PATH/FN](003): Will we see this in the config?
[PROCESS]     [/THREAD/PNS/PATH/FN](004): Will we see this in the config?
[TUTORIAL]     [/THREAD/PNS/PATH/FN](005): ALox: Multi line message follows: 
>> TPre: MPre: DomPre: The current configuration of this Lox is:
>> ALib Version:      2402 (Rev. 1)
>> ALib Compiler Symbols:
>>   DEBUG                                    : On
>>   MONOMEM                                  : On
>>   SINGLETONS                               : On
>>   CHARACTERS                               : On
>>   ENUMS                                    : On
>>   TIME                                     : On
>>   BOXING                                   : On
>>   STRINGS                                  : On
>>   BITBUFFER                                : On
>>   THREADS                                  : On
>>   CAMP                                     : On
>>   ALOX                                     : On
>>   CLI                                      : On
>>   CONFIGURATION                            : On
>>   EXPRESSIONS                              : On
>>   FILES                                    : On
>>   FEAT_SINGLETON_MAPPED                    : Off
>>   CHARACTERS_WIDE                          : Off
>>   CHARACTERS_WCHAR_IS_4                    : On
>>   DEBUG_BOXING                             : On
>>   DEBUG_STRINGS                            : Off
>>   DEBUG_MONOMEM                            : Off
>>   DEBUG_RESOURCES                          : Off
>>   DBG_LOG                                  : On
>>   DBG_LOG_CI                               : On
>>   REL_LOG                                  : On
>>   REL_LOG_CI                               : Off
>> Name:            "LOG"
>> Version:         2402 (Rev. 1)
>> Thread Safeness: Safe
>> #Log Calls:      5
>> Source Path Trimming Rules: 
>>   Global: "*/src/", Include, Priority: DefaultValues
>> Domain Substitution Rules: 
>>   <no rules set>
>> Once() Counters: 
>>   Scope::FileName    [/unittests/alox/ut_dox_tutorial.*]
>>     "#1107"   =1
>>     "ONCEKEY" =1
>> Log Data: 
>>   Scope::FileName    [/unittests/alox/ut_dox_tutorial.*]
>>     "DataKey" =3
>>   Scope::Method      [/unittests/alox/ut_dox_tutorial.* @TestBody()]
>>     <global>  =MyData 1
>>     "DataKey" =MyData 2
>>   Scope::ThreadInner [Thread="TUTORIAL"]:
>>     "DataKey" =4
>> Prefix Logables: 
>>   TPre:                  Scope::ThreadOuter [Thread="TUTORIAL"]
>>   MPre:                  Scope::Method      [/unittests/alox/ut_dox_tutorial.* @TestBody()]
>>   "{ESC::RED}" (Excl.)   <domain>           [/ERRORS]
>>   "DomPre: "             <domain>           [/THREAD/PNS/PATH/FN]
>>   "Mouse: "              <domain>           [/UI/MOUSE]
>> Named Threads:   
>>   (-1):  "TUTORIAL"
>> Scope Domains: 
>>   THREAD                 Scope::ThreadOuter [Thread="TUTORIAL"]
>>   PNS                    Scope::Path        [//unittests/]
>>   PATH                   Scope::Path        [/unittests/alox/]
>>   FN                     Scope::FileName    [/unittests/alox/ut_dox_tutorial.*]
>> Loggers:
>>     Lines logged:  4
>>     Creation time: 2024-03-20 11:59:40
>>     Last log time: 2024-03-20 11:59:40
>>     Verbosities:   /                   = Verbose(DefaultValues)
>>                    /MEM                = Off    (DefaultValues)
>>                    /UI                 = Error  (DefaultValues)
>>                    /UI/DLG             = Info   (DefaultValues)
>>     Lines logged:  4
>>     Creation time: 2024-03-20 11:59:40
>>     Last log time: 2024-03-20 11:59:40
>>     Verbosities:   /                   = Verbose(DefaultValues)
>>                    /CON                = Off    (DefaultValues)
>> Loggers on Internal Domains:
>>     Lines logged:  4
>>     Creation time: 2024-03-20 11:59:40
>>     Last log time: 2024-03-20 11:59:40
>>     Verbosities:   $/                  = Warning(DefaultValues)
>> Internal Domains:
>>   $/                    [002]  { ([000], Warning(DefaultValues)) }
>>   $/DMN                 [034]  { ([000], Warning(DefaultValues)) }
>>   $/LGD                 [004]  { ([000], Warning(DefaultValues)) }
>>   $/LGR                 [011]  { ([000], Warning(DefaultValues)) }
>>   $/PFX                 [005]  { ([000], Warning(DefaultValues)) }
>>   $/THR                 [001]  { ([000], Warning(DefaultValues)) }
>>   $/VAR                 [000]  { ([000], Warning(DefaultValues)) }
>> Domains:
>>   /                     [000]  { ([000], Verbose(DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /CON                  [001]  { ([001], Verbose(DefaultValues)), ([000], Off    (DefaultValues)) }
>>   /ERRORS               [000]  { ([000], Verbose(DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /MEM                  [001]  { ([000], Off    (DefaultValues)), ([001], Verbose(DefaultValues)) }
>>   /THREAD               [000]  { ([000], Verbose(DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /THREAD/PNS           [000]  { ([000], Verbose(DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /THREAD/PNS/PATH      [000]  { ([000], Verbose(DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /THREAD/PNS/PATH/FN   [003]  { ([003], Verbose(DefaultValues)), ([003], Verbose(DefaultValues)) }
>>   /UI                   [000]  { ([000], Error  (DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /UI/DLG               [000]  { ([000], Info   (DefaultValues)), ([000], Verbose(DefaultValues)) }
>>   /UI/MOUSE             [000]  { ([000], Error  (DefaultValues)), ([000], Verbose(DefaultValues)) }

Besides this, there is a second option to inspect what happens internally in class Lox. ALox is equipped with an internal Log Domain. This domain is used by ALox to log messages about itself! All we have to do, is setting the Verbosity of the internal domain to verbose for our debug Logger:

// This is the very same code as above...
Log_Prune( MemoryLogger memLogger; )
Log_Prune( Log::DebugLogger->MetaInfo->Format.Reset("[%tN]%V[%D](%#): "); )
Log_Prune( memLogger.MetaInfo->Format.Reset("[%tN]%V[%D](%#): "); )
Log_SetVerbosity( &memLogger, Verbosity::Verbose )
// ... with one difference: we are activating the internal domain
Log_SetVerbosity( &memLogger, Verbosity::Verbose, Lox::InternalDomains )
Log_SetVerbosity( Log::DebugLogger, Verbosity::Verbose, Lox::InternalDomains )
Log_SetDomain( "PNS" , Scope::Path + 1 )
Log_SetDomain( "PATH", Scope::Path )
Log_SetDomain( "FN", Scope::Filename )
Log_SetDomain( "THREAD", Scope::ThreadOuter )
Log_SetVerbosity( "MEMORY", Verbosity::Off , "/CON" )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Verbose )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Off , "/MEM" )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Error , "/UI" )
Log_SetVerbosity( "DEBUG_LOGGER" , Verbosity::Info , "/UI/DLG" )
Log_Once( "Will we see this in the config?" )
Log_Once( "Will we see this in the config?", A_CHAR("ONCEKEY"), Scope::Filename )
Log_Store( "MyData 1" , Scope::Method )
Log_Store( "MyData 2" , "DataKey", Scope::Method )
Log_Store( 3 , "DataKey", Scope::Filename )
Log_Store( 4 , "DataKey", Scope::ThreadOuter )
Log_SetPrefix( "TPre: " , Scope::ThreadOuter )
Log_SetPrefix( "MPre: " , Scope::Method )
Log_SetPrefix( "DomPre: " )
Log_SetPrefix( "Mouse: ", "/UI/MOUSE" )
Log_SetPrefix( ESC::RED, "/ERRORS", lang::Inclusion::Exclude )
Log_MapThreadName( A_CHAR("TUTORIAL") )

The output will be:

[PROCESS]     [$/LGR](001): Logger "MEMORY":       '$/'    = Verbosity::Verbose(DefaultValues).
[PROCESS]     [$/LGR](002): Logger "DEBUG_LOGGER": '$/'    = Verbosity::Verbose(DefaultValues).
[PROCESS]     [$/DMN](003): 'PNS' set as default for Scope::Path+1.
[PROCESS]     [$/DMN](004): 'PATH' set as default for Scope::Path.
[PROCESS]     [$/DMN](005): 'FN' set as default for Scope::Filename.
[PROCESS]     [$/DMN](006): 'THREAD' set as default for Scope::ThreadOuter.
[PROCESS]     [$/DMN](007): "/CON" registered.
[PROCESS][***][$/DMN](008):   "DEBUG_LOGGER": /CON = Verbose(DefaultValues)
[PROCESS][***][$/DMN](009):   "MEMORY":       /CON = Verbose(DefaultValues)
[PROCESS]     [$/LGR](010): Logger "MEMORY":       '/CON'  = Verbosity::Off    (DefaultValues).
[PROCESS]     [$/LGR](011): Logger "DEBUG_LOGGER": '/'     = Verbosity::Verbose(DefaultValues).
[PROCESS]     [$/DMN](012): "/MEM" registered.
[PROCESS][***][$/DMN](013):   "DEBUG_LOGGER": /MEM = Verbose(DefaultValues)
[PROCESS][***][$/DMN](014):   "MEMORY":       /MEM = Verbose(DefaultValues)
[PROCESS]     [$/LGR](015): Logger "DEBUG_LOGGER": '/MEM'  = Verbosity::Off    (DefaultValues).
[PROCESS]     [$/DMN](016): "/UI" registered.
[PROCESS][***][$/DMN](017):   "DEBUG_LOGGER": /UI = Verbose(DefaultValues)
[PROCESS][***][$/DMN](018):   "MEMORY":       /UI = Verbose(DefaultValues)
[PROCESS]     [$/LGR](019): Logger "DEBUG_LOGGER": '/UI'   = Verbosity::Error  (DefaultValues).
[PROCESS]     [$/DMN](020): "/UI/DLG" registered.
[PROCESS][***][$/DMN](021):   "DEBUG_LOGGER": /UI/DLG = Error  (DefaultValues)
[PROCESS][***][$/DMN](022):   "MEMORY":       /UI/DLG = Verbose(DefaultValues)
[PROCESS]     [$/LGR](023): Logger "DEBUG_LOGGER": '/UI/DLG' = Verbosity::Info   (DefaultValues).
[PROCESS]     [$/DMN](024): "/THREAD" registered.
[PROCESS][***][$/DMN](025):   "DEBUG_LOGGER": /THREAD = Verbose(DefaultValues)
[PROCESS][***][$/DMN](026):   "MEMORY":       /THREAD = Verbose(DefaultValues)
[PROCESS]     [$/DMN](027): "/THREAD/PNS" registered.
[PROCESS][***][$/DMN](028):   "DEBUG_LOGGER": /THREAD/PNS = Verbose(DefaultValues)
[PROCESS][***][$/DMN](029):   "MEMORY":       /THREAD/PNS = Verbose(DefaultValues)
[PROCESS]     [$/DMN](030): "/THREAD/PNS/PATH" registered.
[PROCESS][***][$/DMN](031):   "DEBUG_LOGGER": /THREAD/PNS/PATH = Verbose(DefaultValues)
[PROCESS][***][$/DMN](032):   "MEMORY":       /THREAD/PNS/PATH = Verbose(DefaultValues)
[PROCESS]     [$/DMN](033): "/THREAD/PNS/PATH/FN" registered.
[PROCESS][***][$/DMN](034):   "DEBUG_LOGGER": /THREAD/PNS/PATH/FN = Verbose(DefaultValues)
[PROCESS][***][$/DMN](035):   "MEMORY":       /THREAD/PNS/PATH/FN = Verbose(DefaultValues)
[PROCESS]     [/THREAD/PNS/PATH/FN](036): Will we see this in the config?
[PROCESS]     [$/                 ](037): Once() reached limit of 1 logs. No further logs for Scope::Filename.
[PROCESS]     [/THREAD/PNS/PATH/FN](038): Will we see this in the config?
[PROCESS]     [$/                 ](039): Once() reached limit of 1 logs. No further logs for group "ONCEKEY" in Scope::Filename.
[PROCESS]     [$/LGD              ](040): Stored data in Scope::Method.
[PROCESS]     [$/LGD              ](041): Stored data  with key "DataKey" in Scope::Method.
[PROCESS]     [$/LGD              ](042): Stored data  with key "DataKey" in Scope::Filename.
[PROCESS]     [$/LGD              ](043): Stored data  with key "DataKey" in Scope::ThreadOuter.
[PROCESS]     [$/PFX              ](044): Object TPre:  added as prefix logable for Scope::ThreadOuter.
[PROCESS]     [$/PFX              ](045): Object MPre:  added as prefix logable for Scope::Method.
[PROCESS]     [$/PFX              ](046): Object DomPre:  added as prefix logable for  domain '/THREAD/PNS/PATH/FN'.
[PROCESS]     [$/DMN              ](047): "/UI/MOUSE" registered.
[PROCESS][***][$/DMN              ](048):   "DEBUG_LOGGER": /UI/MOUSE = Error  (DefaultValues)
[PROCESS][***][$/DMN              ](049):   "MEMORY":       /UI/MOUSE = Verbose(DefaultValues)
[PROCESS]     [$/PFX              ](050): Object Mouse:  added as prefix logable for  domain '/UI/MOUSE'.
[PROCESS]     [$/DMN              ](051): "/ERRORS" registered.
[PROCESS][***][$/DMN              ](052):   "DEBUG_LOGGER": /ERRORS = Verbose(DefaultValues)
[PROCESS][***][$/DMN              ](053):   "MEMORY":       /ERRORS = Verbose(DefaultValues)
[PROCESS]     [$/PFX              ](054): Object  added as prefix logable for  domain '/ERRORS'.
[TUTORIAL]     [$/THR              ](055): Mapped thread ID -1 to "TUTORIAL". Original thread name: "MAIN_THREAD".

This way, you can exactly observe what is going on inside ALox .

To summarize: We have to ways to look into ALox :

  1. Method Lox::State logs a "snapshot" of the current states. The advantage of this is, that it is all logged sequentially in one place and does not clutter your log output.
  2. By setting the Verbosity of the internal Log Domain Lox::InternalDomains to a more verbose level. While this clutters your log output and you might have to search the pieces in your overall log stream, the advantage here is that you see the scope information and therefore you see "where" a certain manipulation of the settings took place...and you can click on it!
The internal Log Domain found in static, constant field Lox::InternalDomains is not just a domain name string. In fact it specifies a completely different domain tree which is not related to those domains created when using ALox . Even when changing the Verbosity for a Logger on the root domain /, this domain is not affected, because it is just not a part of the general domain system. The advantage is that you can not 'accidentally' switch this domain on or off. In other words, the general domain tree is completely kept free for the users of ALox .
See also
ALox configuration variable ALOX_LOXNAME_DUMP_STATE_ON_EXIT provides a way to automatically log state information when a Lox gets deleted.

Further Reading

This is the end of the tutorial of ALox for C++. You should be able to use the basic features of the ALox logging ecosystem and have nice, formatted, configurable log output in your software projects. ALox puts you in the comfortable position to keep your debug Log Statements in your code, because you can easily configure not only the verbosity as a global parameter, but for different Log Domains separately. When you start editing code that you have not touched for a while, you just can enable the corresponding Log Domain and restrict others to warnings and errors for a while. Because of the efficient ALox implementation, disabled Log Domains do not have a relevant impact on the execution of your debug code. As soon as you release your code, debug logging is pruned completely from your binaries.

In the Programmer's Manual you will find all details of the ALox features along with information on use cases and often explanations why a feature is designed as it is. Some important things not covered in the tutorial are:

  • Release Logging
    Release logging is very similar to debug logging. The differences and how to use release-logging is described in a dedicated chapter of the Programmer's Manual.
  • External Configuration
    Instead of setting Verbosities and other changeable configuration "from within the source" (by adding ALox API invocations to your software), such data can also come from INI files, environment variables, command-line parameters or a any custom source that your application uses. This is especially important on larger projects, when working in teams.
    A summary of all ALox configuration variables is found in reference documentation ALox Configuration Variables.
  • Trimming Source File Paths
    For C++ and C# users: ALox automatically trims paths of source code files. This can be optimized, as described in chapter 14 Trimming Source File Paths and Clickable IDE Output of the Programmer's Manual.
  • Log Domain Substitution
    What this is, is described chapter 15 Log Domain Substitution of the Programmer's Manual

If however, you just want to start using ALox and get to know more details over time, you should only read Appendix D: IDE Setup for ALox for C++ and probably section "ALox Macros For Debug Logging" in ALib Preprocessor Macros.

Finally, for the daily work, the ALox class reference gives a lot of help!