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:
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:
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.
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:
Most of the macros that we are using here have the form "Log_Xyz()". What these macros are doing is basically two things:
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 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.
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 .
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:
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.
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:
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.
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:
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 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).
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:
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:
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'.
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:
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:
Our sample now looks like this:
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:
Then ALox matches both attributes to decide whether a Log Statement is executed with a Logger or not.
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!
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:
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:
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!
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:
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:
If we were interested how mouse events are processed in our application, we might do invoke:
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".
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.
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
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:
and its output:
ut_dox_tutorial.cpp:613:Extract [0.000 +492 µs][PROCESS] [/ZIP/EXTRACT]#001: Extracting "myfile.zip" 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:
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 "myfile.zip" 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 "myfile.zip" 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:
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 "myfile.zip" 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 "myfile.zip" 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:
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 :
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:
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:
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:
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:
which is Python compatible syntax, or like this:
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:
Of-course it is even allowed to mix Python style and Java style format strings in one log statement:
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:
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 :
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 .
Often log lines should only be displayed if a certain condition is met. Here is a sample:
The last two lines can be replaced by one using method lox::Lox::Assert as follows:
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).
false
to have ALox perform the log.A similar method to Lox::Assert this is Lox::If . The differences to Lox::Assert are:
true
, the Log Statement is executed.Hence, the very same effect as given in the previous sample can be achieved with:
Another useful Log Statement provided by ALox is Lox::Once . As the method name suggests, the statement
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:
Imagine, all log statements of a of a certain kind should be very visible and distinguishable from other output. You could do this by
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:
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:
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:
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!
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:
// 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: {java.security.AccessControlContext} >> <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:
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:
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.
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 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.
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:
Now, whenever it might be interesting, this file version can be accessed:
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:
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:
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 >> FEAT_BOXING_BIJECTIVE_INTEGRALS : Off >> FEAT_BOXING_BIJECTIVE_CHARACTERS : Off >> FEAT_BOXING_BIJECTIVE_FLOATS : Off >> 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: >> DEBUG_LOGGER (ANSI_CONSOLE) >> 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) >> MEMORY >> 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: >> DEBUG_LOGGER (ANSI_CONSOLE) >> 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:
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 :
/
, 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 .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:
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!