ALox  V. 2402 R. 0
Home ALox for C++ ALox for C# ALox for Java Download
ALox for Java Tutorial

1. Hello ALox

Welcome to the ALox for Java 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:

import com.aworx.lox.Log;
class HelloALox
{
public static void sayHello()
{
Log.info ( "Hello ALox!" );
}
}

When this method gets invoked, the output should look similar to this

   (HelloALox.java:7) sayHello():[00:00.000 +167 µs] [main]       [/] (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 sits.
  • The time that passed since the process started (here 00:00.000 which means below a millisecond)
  • The time that passed since the last log line: 167 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 in this case)
  • [/]: 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.

And: In the release build of your software, this line of code is completely pruned away. Because, as we see later, also in debug versions you can disable log output selectively, 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. Classes Log and Lox

The previous chapter was a teaser. We showed the shortest possible sample program which does not 'bootstrap' ALox (aka initialize and configure). This is detected and hence ALox tries to setup itself with 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 Java such pruning is done using an 'obfuscator' tool and ALox provides ready to use configuration files and a tutorial on how to use one of the best of its kind.

We want you to understand a little of the class design of ALox, while you are reading yourself through this tutorial. Some first general notes:

  • The most important interface into ALox is class Lox which is is a container to hold one or more instances of abstract class 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). In this tutorial we stick to the simpler cases, maybe adding a second Logger here and there.
  • Most of the methods that we are using here are invoked on class Log instead of class Lox. This class is a 'mirror' class of Lox, a kind of fake class that mimics the interface but internally just calls the original methods on a dedicated Lox singleton which is used for debug logging. Therefore,

      Log.info( "Hello ALox" )
    

    means that method info is invoked on the predefined singleton of class Lox which is by default used for debug logging. (Class Log and any reference on it gets pruned with release versions.)

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

If you want to follow the tutorial samples and play around with them 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: IDE Setup for ALox for Java.

Note
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 have all packages for this tutorial set up, add the following lines to the start of your source file:

import com.aworx.lib.strings.*;
import com.aworx.lox.Verbosity;
import com.aworx.lox.Scope;
import com.aworx.lox.Log;
import com.aworx.lox.ESC;
import com.aworx.lox.ALox;
import com.aworx.lox.detail.textlogger.*;
import com.aworx.lox.loggers.*;
import com.aworx.lox.tools.*;

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.

Note
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:

Log.addDebugLogger();

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.

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.

Note
For Android developers: To stream ALox logging into LogCat create a Logger of type AndroidLogCatLogger instead of type ConsoleLogger as follows:
Log.setVerbosity( new AndroidLogCatLogger("LogCat"), Verbosity.VERBOSE );
The normal ConsoleLogger would not produce any visible output using the Android Emulator or a debugging device.

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

Log.addDebugLogger();

5. Run the Application

When you run your project, output similar to this:

(UT_dox_tutorial.java:178) Tut_Hello_ALox():[0.000 +379 µs][UTThread]     [/](001): Hello ALox

should appear in your IDE or your console.

Note
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.2. Run your application within your IDE

If you are using an IDE supported by ALox (e.g. Eclipse), 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 (because ALox formats the scope information accordingly)
  • 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).

Note
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!
Note
You should adjust your Java IDE in a way that the console 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. A quick note on pruning

At this point, the tutorial on ALox for C# or ALox for C++ include a section about a "release build" of the current tutorial project. In these languages, such release builds remove all Log Statements from the code. Java does not know release builds. But pruning is still supported and automated in ALox for Java as well (not covered in this tutorial, but documented here: Pruning ALox). The advantages are:

  • your release code executes faster
  • your release executable gets a smaller footprint
  • 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.addDebugLogger();
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.java:194) Tut_Verbosity():[0.000 +536 µs][UTThread][ERR][/](001): A severe error happened :-(
(UT_dox_tutorial.java:195) Tut_Verbosity():[0.000 +123 µs][UTThread][WRN][/](002): This is a warning :-/ Maybe an error follows?
(UT_dox_tutorial.java:196) Tut_Verbosity():[0.000 +139 µs][UTThread]     [/](003): Just for your further information!
(UT_dox_tutorial.java:197) Tut_Verbosity():[0.000 +123 µs][UTThread][***][/](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'.

Note
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.addDebugLogger();
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.java:220) Tut_Verbosity():[0.001 +558 µs][UTThread][ERR][/](001): A severe error happened :-(
(UT_dox_tutorial.java:221) Tut_Verbosity():[0.001 +119 µs][UTThread][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.

Note
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.addDebugLogger();
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.java:241) Tut_Domains():[0.000 +446 µs][UTThread][***][/HTTP](001): Connected
(UT_dox_tutorial.java:245) Tut_Domains():[0.000 +219 µs][UTThread][***][/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.addDebugLogger();
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.java:263) Tut_Domains():[0.001 +675 µs][UTThread][***][/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.addDebugLogger();
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.java:286) Tut_HierDom():[0.000 +688 µs][UTThread]     [/UI/MOUSE](001): A mouse click
(UT_dox_tutorial.java:288) Tut_HierDom():[0.000 +183 µs][UTThread][***][/UI/MOUSE](002): Somebody moved the mouse!
(UT_dox_tutorial.java:291) Tut_HierDom():[0.001 +208 µs][UTThread]     [/UI/DLG  ](003): About dialog opend
(UT_dox_tutorial.java:293) Tut_HierDom():[0.001 +155 µs][UTThread][***][/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".

Note
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 ALox User 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. The next section tells us more about why the hierarchical organization of Log Domains is extremely useful.

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) 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:

public char[] Extract( String fileName, char[] buffer )
{
Log.setDomain( "ZIP/EXTRACT", Scope.METHOD ); // set Scope Domain path for this method
//...
Log.info( "Extracting " + fileName );
//...
//...
Log.info( "Success" ); // a nice, clear, local, copyable log statement!
//...
return buffer;
}

and its output:

(UT_dox_tutorial.java:44) Extract():[0.000 +650 µs][UTThread]     [/ZIP/EXTRACT](001): Extracting myfile.zip
(UT_dox_tutorial.java:47) Extract():[0.000 +156 µs][UTThread]     [/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.CLASS which "binds" a given Log Domain path to all Log Statements within a Java class. As with scopes in your programming language, Scopes in ALox are nested and of-course Scope.CLASS 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
{
String fileName;
public Zipper( String fileName )
{
Log.setDomain( "ZIP", Scope.CLASS ); // set Scope Domain path for this class
//...
this.fileName= fileName;
Log.info( "Zipper created" ); // domain "ZIP"
//...
}
public char[] Compress( char[] buffer )
{
Log.setDomain( "COMPRESS", Scope.METHOD ); // set Scope Domain path for this method
//...
Log.info( "Compressing " + fileName );
//...
//...
Log.info( "Success" ); // domain "ZIP/COMPRESS"
//...
return buffer;
}
public char[] Extract( char[] buffer )
{
Log.setDomain( "EXTRACT", Scope.METHOD ); // set Scope Domain path for this method
//...
Log.info( "Extracting " + fileName );
//...
//...
Log.info( "Success" ); // domain "ZIP/EXTRACT"
//...
return buffer;
}
}

and its output when both sample methods are executed:

(UT_dox_tutorial.java:64) <init>():       [0.002 +159 µs][UTThread]     [/ZIP         ] (001): Zipper created
(UT_dox_tutorial.java:72) Compress():     [0.002 +169 µs][UTThread]     [/ZIP/COMPRESS] (002): Compressing myfile.zip
(UT_dox_tutorial.java:75) Compress():     [0.002 +167 µs][UTThread]     [/ZIP/COMPRESS] (003): Success
(UT_dox_tutorial.java:84) Extract():      [0.002 +174 µs][UTThread]     [/ZIP/EXTRACT ] (004): Extracting myfile.zip
(UT_dox_tutorial.java:87) Extract():      [0.002 +168 µs][UTThread]     [/ZIP/EXTRACT ] (005): Success

Imagine, class Zipper resides in a package, 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.PACKAGE );

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

(UT_dox_tutorial.java:64) <init>():       [0.004 +174 µs][UTThread]     [/UTIL/ZIP         ] (001): Zipper created
(UT_dox_tutorial.java:72) Compress():     [0.004 +175 µs][UTThread]     [/UTIL/ZIP/COMPRESS] (002): Compressing myfile.zip
(UT_dox_tutorial.java:75) Compress():     [0.004 +178 µs][UTThread]     [/UTIL/ZIP/COMPRESS] (003): Success
(UT_dox_tutorial.java:84) Extract():      [0.004 +168 µs][UTThread]     [/UTIL/ZIP/EXTRACT ] (004): Extracting myfile.zip
(UT_dox_tutorial.java:87) Extract():      [0.005 +169 µs][UTThread]     [/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.java:382) Tut_ScopeDomains():[0.006 +191 µs][UTThread]     [/METHOD      ] (001): No domain parameter given
(UT_dox_tutorial.java:383) Tut_ScopeDomains():[0.006 +188 µs][UTThread]     [/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.java:391) Tut_ScopeDomains():[0.007 +554 µs][UTThread]     [/READ        ] (001): Reading file
(UT_dox_tutorial.java:394) Tut_ScopeDomains():[0.007 +173 µs][UTThread]     [/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 ALox User Manual. Among other things, you will find:

  • Information on a Scope.Global
  • How to use outer Scopes of Scope.PACKAGE, addressing outer packages
  • Information on 'thread-related' Scopes, which allow to change Log Domain on a per thread basis in different ways
  • More samples and use cases, e.g. when to use absolute paths in Scope Domains.

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.java:707) Tut_Format():[0.000 +337 µs][UTThread]     [/](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.java:730) Tut_Format():[0.001 +004 µs][UTThread]     [/](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. Let us conclude this chapter with some 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.java:736) Tut_Format():[0.001 +088 µs][UTThread]     [/](009): >left      <
(UT_dox_tutorial.java:737) Tut_Format():[0.002 +003 µs][UTThread]     [/](010): >     right<
(UT_dox_tutorial.java:738) Tut_Format():[0.002 +003 µs][UTThread]     [/](011): >  center  <
(UT_dox_tutorial.java:739) Tut_Format():[0.002 +008 µs][UTThread]     [/](012): >    12.346<
(UT_dox_tutorial.java:741) Tut_Format():[0.002 +002 µs][UTThread]     [/](013): Tab:        Stop
(UT_dox_tutorial.java:743) Tut_Format():[0.002 +001 µs][UTThread]     [/](014): Auto Tab: Stop
(UT_dox_tutorial.java:744) Tut_Format():[0.003 +001 µs][UTThread]     [/](015): Auto Tab XXX:    Stop
(UT_dox_tutorial.java:745) Tut_Format():[0.003 +001 µs][UTThread]     [/](016): Auto Tab:        Stop
(UT_dox_tutorial.java:747) Tut_Format():[0.003 +002 µs][UTThread]     [/](017): A quoted "Placeholder" string
(UT_dox_tutorial.java:748) Tut_Format():[0.003 +001 µs][UTThread]     [/](018): A quoted "395" number
(UT_dox_tutorial.java:750) Tut_Format():[0.003 +002 µs][UTThread]     [/](019): Upper "CASE" and lower "case" conversion
(UT_dox_tutorial.java:752) Tut_Format():[0.004 +002 µs][UTThread]     [/](020): Hex: 0x11ff22ee. With group chars: 11ff'22ee
(UT_dox_tutorial.java:753) Tut_Format():[0.004 +002 µs][UTThread]     [/](021): Oct: 0o12345670. With group chars: 12'345'670
(UT_dox_tutorial.java:754) Tut_Format():[0.004 +003 µs][UTThread]     [/](022): Bin: 0b10010001. With group chars: 1001'0001

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 );
break;
}
i++;
}
if ( i == len )
Log.error( "Nothing found :-(" );

The last two lines can be replaced by one using method Log.Assert() as follows:

Log.Assert( i < len, "Nothing found :-(" );

Advantages are again less typing and better readability of the code. Furthermore, the Java 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).

Note
  • The method Assert() starts with capitalized "A" because "assert" is a reserved key word in Java.
  • 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 :-(" );
Note
Same as with method Assert, this methods' name is starting with a capital letter, because if is a reserved keyword in Java.

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 ALox User 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 \e 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 Logable 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.java:416) Tut_Prefix():[0.000 +381 µs][UTThread]     [/](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.CLASS );

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 ALox User 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

ALox also provides some log functions that log more complex stuff. The design decision was made to not put this into the classes Lox / Log but into a separated class LogTools. However, LogTools is used in the same simple fashion as classes Log / Lox itself.

On the Java language platform two methods are currently available which are briefly covered in the following:

13.1 Instance(): Log any object recursively

The method LogTools.instance() allows you to log just any object recursively in a well formatted way. You can try this out quickly by adding the following code to your sample:

// 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, the current thread, to the method. The parameter that follows the object which is to be logged (the current thread) determines the level of objects which are recursively logged, starting from the given one (composite objects). You can play around with it, and increase it from 2 to lets say 5. But be careful: this can lead to a quite long log message!

The given parameter "Actual Thread: " provides a headline for the log output. This allows you to save another line of code, which would be logging the headline explicitly.

Here are the first lines of output of the above sample:

>> Actual Thread:                                                    {Thread}
>> <01>  eetop:                        "<no access>"                 {String}
>> <02>  name:                         "<no access>"                 {String}
>> <03>  interrupted:                  "<no access>"                 {String}
>> <04>  contextClassLoader:           "<no access>"                 {String}
>> <05>  inheritedAccessControlContext: "<no access>"                {String}
>> <06>  threadLocals:                 "<no access>"                 {String}
>> <07>  inheritableThreadLocals:      "<no access>"                 {String}
>> <08>  scopedValueBindings:          "<no access>"                 {String}
>> <09>  parkBlocker:                  "<no access>"                 {String}
>> <10>  nioBlocker:                   "<no access>"                 {String}
>> <11>  cont:                         "<no access>"                 {String}
>> <12>  uncaughtExceptionHandler:     "<no access>"                 {String}
>> <13>  threadLocalRandomSeed:        "<no access>"                 {String}
>> <14>  threadLocalRandomProbe:       "<no access>"                 {String}
>> <15>  threadLocalRandomSecondarySeed: "<no access>"               {String}
>> <16>  container:                    "<no access>"                 {String}
>> <17>  headStackableScopes:          "<no access>"                 {String}...
...
...

13.2 Exception(): Log an exception recursively (with inner exceptions)

Just like Instance(), the method LogTools.exception() uses the multi line feature of ALox to log out the contents of a Java Exception well formatted and recursively (following inner exceptions).

Of-course a perfect place in your code to add LogTools.exception() is the first line within a catch() block.

To try it out, add the following code into your project:

Exception testException= new Exception( "TestException Message",
new Exception ( "InnerException Message",
new Exception("Inner, inner Exception") ) );
LogTools.exception( "MYDOM", Verbosity.WARNING, testException, "Logging an exception: " );

Like the method LogTools.instance(), a headline for the Exception log output can be specified. This allows you to save another line of code, which would be logging the headline explicitly.

The following log output will be generated:

>> Logging an exception: 
>>   Type:       java.lang.Exception
>>   Message:    TestException Message
>>   Cause:   
>>     Type:       java.lang.Exception
>>     Message:    InnerException Message
>>     Cause:   
>>       Type:       java.lang.Exception
>>       Message:    Inner, inner Exception
>>   Exception stack trace:
>>     ut_alox.UT_dox_tutorial.Tut_Exception(UT_dox_tutorial.java:515)
>>     java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
>>     java.base/java.lang.reflect.Method.invoke(Method.java:580)
>>     org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
>>     org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
>>     org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
>>     org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
>>     org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
>>     org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
>>     org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
>>     org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
>>     org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
>>     org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
>>     org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
>>     org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
>>     org.junit.runners.ParentRunner.run(ParentRunner.java:300)
>>     org.junit.runners.Suite.runChild(Suite.java:128)
>>     org.junit.runners.Suite.runChild(Suite.java:24)
>>     org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
>>     org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
>>     org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
>>     org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
>>     org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
>>     org.junit.runners.ParentRunner.run(ParentRunner.java:300)
>>     org.junit.runner.JUnitCore.run(JUnitCore.java:157)
>>     com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
>>     com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
>>     com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
>>     com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
>>     com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
>>     com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)

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 Log.mapThreadName.

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:

  • 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 mapping overhead.
  • Make sure that this code is executed early in the lifecycle of your application.

For the sample of UI Frameworks, a good place to invoke Log.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 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 in a debug compilation 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.

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
{
public void Read(String fileName )
{
Log.setDomain( "READ", Scope.METHOD );
Log.info( "Reading " + fileName );
String fileVersion;
//...
//...
// Identified file version
fileVersion= "3.1";
Log.store( fileVersion, "FILE_VERSION" );
//...
//...
Log.info( "Success" );
}
}

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

Log.info( "Working on file version {!Q}", Log.retrieve( "FILE_VERSION" ) );

The output will be:

(UT_dox_tutorial.java:104) Read():       [0.001 +662 µs][UTThread]     [/READ] (002): Reading myfile.dat
(UT_dox_tutorial.java:116) Read():       [0.002 +250 µs][UTThread]     [/READ] (003): Success
(UT_dox_tutorial.java:683) Tut_LogData():[0.003 +328 µs][UTThread]     [/    ] (004): Working on file version "3.1"

If method "Read()" of the sample above was not executed and therefore no variable was stored, ALox just creates an empty, default object. The output in this case would look like this:

(UT_dox_tutorial.java:674) Tut_LogData():[0.001 +001 ms][UTThread]     [/](001): Working on file version "java.lang.Object@56cbfb61"

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.addDebugLogger();
Log.setVerbosity( new MemoryLogger(), Verbosity.VERBOSE );
// reduce meta information to limit tutorial output width
Log.debugLogger .metaInfo.format.__().__( "[%tN]%V[%D](%#): " );
((MemoryLogger) Log.getLogger("MEMORY")).metaInfo.format.__().__( "[%tN]%V[%D](%#): " );
((MemoryLogger) Log.getLogger("MEMORY")).multiLineMsgMode= 3;
// OK, let's use ALox
Log.setDomain( "PNS" , Scope.PACKAGE, 1 );
Log.setDomain( "PATH", Scope.PACKAGE );
Log.setDomain( "FN", Scope.CLASS );
Log.setDomain( "THREAD", Scope.THREAD_OUTER );
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( "", Verbosity.INFO, "Will we see this in the config?", "ONCEKEY", Scope.CLASS );
Log.store( "MyData 1", Scope.METHOD );
Log.store( "MyData 2", "DataKey", Scope.METHOD );
Log.store( "MyData 3", "DataKey", Scope.CLASS );
Log.store( "MyData 4", "DataKey", Scope.THREAD_OUTER );
Log.setPrefix( "TPre: " , Scope.THREAD_OUTER );
Log.setPrefix( "MPre: " , Scope.METHOD );
Log.setPrefix( "DomPre: " );
Log.setPrefix( "Mouse: ", "/UI/MOUSE" );
Log.setPrefix( ESC.RED, "/ERRORS", Inclusion.EXCLUDE );
Log.mapThreadName( "TUTORIAL" );
// now, log the current config
Log.state( null, Verbosity.INFO, "The current configuration of this Lox is:" );

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

[UTThread]     [/THREAD/PNS/PATH/FN](001): This goes to both loggers
[UTThread]     [/MEM               ](002): This goes only to the memory logger
[UTThread]     [/THREAD/PNS/PATH/FN](003): Will we see this in the config?
[UTThread]     [/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:
>> Name:            "LOG"
>> Version:         2402 (Rev. 0)
>> Thread-Safe:     SAFE
>> #Log Calls:      5
>> 
>> Domain Substitution Rules: 
>>   <no rules set>
>> 
>> Once() Counters: 
>>   Scope.CLASS        [ut_alox.UT_dox_tutorial]
>>     "ONCEKEY" =1
>> 
>>   Scope.Method       [ut_alox.UT_dox_tutorial @Tut_LogState()]
>>     "#583"    =1
>> 
>> Log Data: 
>>   Scope.THREAD_OUTER [Thread="TUTORIAL"]:
>>     "DataKey" =MyData 4
>> 
>>   Scope.CLASS        [ut_alox.UT_dox_tutorial]
>>     "DataKey" =MyData 3
>> 
>>   Scope.Method       [ut_alox.UT_dox_tutorial @Tut_LogState()]
>>     <global>  =MyData 1
>>     "DataKey" =MyData 2
>> 
>> Prefix Logables: 
>>   "TPre: "               Scope.THREAD_OUTER [Thread="TUTORIAL"]
>>   "MPre: "               Scope.Method       [ut_alox.UT_dox_tutorial @Tut_LogState()]
>>   "{ESC.RED}" (Excl.)    <domain>           [/ERRORS]
>>   "DomPre: "             <domain>           [/THREAD/PNS/PATH/FN]
>>   "Mouse: "              <domain>           [/UI/MOUSE]
>> 
>> Named Threads:   
>>   (1):   "TUTORIAL"
>> 
>> Scope Domains: 
>>   THREAD                 Scope.THREAD_OUTER [Thread="TUTORIAL"]
>>   PNS                    Scope.PACKAGE      []
>>   PATH                   Scope.PACKAGE      [ut_alox]
>>   FN                     Scope.CLASS        [ut_alox.UT_dox_tutorial]
>> 
>> Loggers:
>>   DEBUG_LOGGER (ANSI_CONSOLE)
>>     Lines logged:  4
>>     Creation time: 2024-03-02 14:57:28
>>     Last log time: 2024-03-02 14:57:28
>>     Verbosities:   /                   = VERBOSE (DEFAULT)
>>                    /MEM                = OFF     (DEFAULT)
>>                    /UI                 = ERROR   (DEFAULT)
>>                    /UI/DLG             = INFO    (DEFAULT)
>> 
>>   MEMORY
>>     Lines logged:  4
>>     Creation time: 2024-03-02 14:57:28
>>     Last log time: 2024-03-02 14:57:28
>>     Verbosities:   /                   = VERBOSE (DEFAULT)
>>                    /CON                = OFF     (DEFAULT)
>> 
>> 
>> Loggers on Internal Domains:
>>   DEBUG_LOGGER (ANSI_CONSOLE)
>>     Lines logged:  4
>>     Creation time: 2024-03-02 14:57:28
>>     Last log time: 2024-03-02 14:57:28
>>     Verbosities:   $/                  = WARNING (DEFAULT)
>> 
>> 
>> Internal Domains:
>>   $/                    [002]  { ([000], WARNING (DEFAULT)) }
>>   $/DMN                 [034]  { ([000], WARNING (DEFAULT)) }
>>   $/LGD                 [004]  { ([000], WARNING (DEFAULT)) }
>>   $/LGR                 [011]  { ([000], WARNING (DEFAULT)) }
>>   $/PFX                 [005]  { ([000], WARNING (DEFAULT)) }
>>   $/THR                 [002]  { ([000], WARNING (DEFAULT)) }
>>   $/VAR                 [000]  { ([000], WARNING (DEFAULT)) }
>> 
>> Domains:
>>   /                     [000]  { ([000], VERBOSE (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /CON                  [001]  { ([001], VERBOSE (DEFAULT)), ([000], OFF     (DEFAULT)) }
>>   /ERRORS               [000]  { ([000], VERBOSE (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /MEM                  [001]  { ([000], OFF     (DEFAULT)), ([001], VERBOSE (DEFAULT)) }
>>   /THREAD               [000]  { ([000], VERBOSE (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /THREAD/PNS           [000]  { ([000], VERBOSE (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /THREAD/PNS/PATH      [000]  { ([000], VERBOSE (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /THREAD/PNS/PATH/FN   [003]  { ([003], VERBOSE (DEFAULT)), ([003], VERBOSE (DEFAULT)) }
>>   /UI                   [000]  { ([000], ERROR   (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /UI/DLG               [000]  { ([000], INFO    (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>>   /UI/MOUSE             [000]  { ([000], ERROR   (DEFAULT)), ([000], VERBOSE (DEFAULT)) }
>> 

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.addDebugLogger();
Log.setVerbosity( new MemoryLogger(), Verbosity.VERBOSE );
// reduce meta information to limit tutorial output width
Log.debugLogger .metaInfo.format.__().__( "[%tN]%V[%D](%#): " );
((MemoryLogger) Log.getLogger("MEMORY")).metaInfo.format.__().__( "[%tN]%V[%D](%#): " );
// ... with one difference: we are activating the internal domain
Log.setVerbosity( Log.getLogger("MEMORY"), Verbosity.VERBOSE, ALox.INTERNAL_DOMAINS );
Log.setVerbosity( Log.debugLogger, Verbosity.VERBOSE, ALox.INTERNAL_DOMAINS );
Log.setDomain( "PNS" , Scope.PACKAGE, 1 );
Log.setDomain( "PATH", Scope.PACKAGE );
Log.setDomain( "FN", Scope.CLASS );
Log.setDomain( "THREAD", Scope.THREAD_OUTER );
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( "", Verbosity.INFO, "Will we see this in the config?", "ONCEKEY", Scope.CLASS );
Log.store( "MyData 1", Scope.METHOD );
Log.store( "MyData 2", "DataKey", Scope.METHOD );
Log.store( "MyData 3", "DataKey", Scope.CLASS );
Log.store( "MyData 4", "DataKey", Scope.THREAD_OUTER );
Log.setPrefix( "TPre: " , Scope.THREAD_OUTER );
Log.setPrefix( "MPre: " , Scope.METHOD );
Log.setPrefix( "DomPre: " );
Log.setPrefix( "Mouse: ", "/UI/MOUSE" );
Log.setPrefix( ESC.RED, "/ERRORS", Inclusion.EXCLUDE );
Log.mapThreadName( "TUTORIAL" );

The output will be:

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

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

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

  1. Method Log.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 ALox.INTERNAL_DOMAINS 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!
Note
The internal Log Domain found in static, read-only field ALox.INTERNAL_DOMAINS 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 ALox User 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 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 chapter: I - 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 Trimming Source File Paths and Clickable IDE Output.
  • Log Domain Substitution
    What this is, is described in Log Domain Substitution.

If however, you just want to start using ALox and get to know more details over time, you should only read IDE Setup for ALox for Java.

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