The VUnit log package (log
) is used internally by VUnit for various logging purposes but it can also be used standalone for general purpose logging. It provides many of the features that you find in SW logging frameworks. For example
- Logging of simple text messages
- Standard and user defined log levels to filter messages
- Custom hierarchical grouping to filter messages
- Configurable formatting of message output
- Automatic file and line number tagging of output messages
- File and stdout output channels with independent formatting and filtering settings
- Support for multiple logs with their own output files and their own settings
- Logging of non-string objects
To understand log
it's important to understand its architecture. Whenever you do a log entry that entry is handled by a logger. There is a default logger that is used when none is specified but you can also create multiple custom loggers. For example
log("Hello world");
will use the default logger while
log(my_logger, "Hello world");
will use the custom my_logger
.
Each logger has two output handlers, one for handling display (stdout) output and one for handling file output. Every log entry you do is passed to both these handlers. For each handler there is one formatter and zero or more filters. The formatter is used to control the format of the output while the filters control if a log entry is passed to the output or not.
The most basic log you can do is a simple text message using the default logger.
log("Hello world");
The default settings of the default logger is such that this will be output on stdout (no file output) just as you wrote it (raw formatter).
Every log entry you make has a log level which is info
for the log call procedure unless you specify anything using the log_level
parameter. There are also dedicated procedure calls for each log level. The standard log levels and their associated procedure calls are
failure("Highest level of *error* message (most severe).");
error("Normal severity error message.");
warning("Error message with lowest severity.");
info("Highest level of *debug* message (least details) is an info message.");
debug("Debug messages are on the middle level.");
verbose("Verbose debug messages contain the most details (lowest level).");
The six standard levels are also interleaved with a number of other levels. The names of these has the following format to indicate their relative severity/detail (x is one of the standard levels)
- x_high2 (e.g. debug_high2)
- x_high1
- x
- x_low1
- x_low2
These extra levels can be used to create additional custom levels. For example, if a status level is needed in between info and debug you can rename the info_low2 level to status and then create a status alias for the info_low2 procedure call.
alias status is info_low2[string, string, natural, string];
...
rename_level(info_low2, "status");
status("Here is the new status message.");
By default the simulation will stop if the log level is failure
or more severe. This can be changed to any of the other levels by changing the stop level configuration.
logger_init(stop_level => error);
With the default raw formatter you won't see the log level in the output message produced. To do that you have to change the formatter to something else. Here I'm changing the formatter for the default logger display handler
logger_init(display_format => level);
info("Hello world");
which will result in the following output.
INFO: Hello world
There is also a verbose
formatter which adds more details to the output.
1000 ps: INFO: Hello World
The verbose output will always contain the simulator time, the log level, and the message. More information about log is shown if available, see the grouping and location chapters.
There is also a verbose_csv
formatter, typically used for file output, that provides the same information comma-separated. The default separator is a comma but you can change that with the separator
parameter of logger_init
. The CSV format enables you to use the power of your spreadsheet tool to handle (large) log files.
Finally, there's a formatter called off
and it's used to prevent all output from a handler.
The path to the file targeted with the file handler is also controlled with logger_init
. Typically you would have something like this.
logger_init(display_format => verbose, file_format => verbose_csv, file_name = "path/to/my/logs/my_log.csv");
The default file name is log.csv
in the current directory and the default file format is off
. By default an existing file will be replaced when calling logger_init
but you can change that by setting the input parameter append
true.
Log calls can be given a source ID such that it can be associated to a group of logs like logs coming from the same module or logs of a specific type.
warning("Over-temperature (73 degrees C)!", "Temperature sensor");
results in something like this with the verbose
formatter.
1000 ps: WARNING in Temperature sensor: Over-temperature (73 degrees C)!
It's also possible to give a logger a default source ID with the logger_init
call.
logger_init(default_src => "Test runner");
You can have the file name and the line number of a log entry if the testbench is compiled with the location preprocessor provided with VUnit. It's enabled like this in your VUnit run script
ui = VUnit.from_argv()
ui.enable_location_preprocessing()
and will change the output to something like this.
1000 ps: WARNING in Temperature sensor (logging_example.vhd:79): Over-temperature (73 degrees C)!
If you've placed your log call(s) in a convenience procedure you most likely want the location of the calls to that procedure to be in the output and not the location of the log call in the definition of the convenience procedure. You can do that by adding the line_num
and file_name
parameters to the end of the parameter list for that convenience procedure
procedure my_convenience_procedure(
<my parameters>
line_num : natural := 0;
file_name : string := "") is
begin
<some code>
info("Some message", line_num => line_num, file_name => file_name);
<some code>
end procedure my_convenience_procedure;
and then let the location preprocessor know about the added procedure
ui = VUnit.from_argv()
ui.enable_location_preprocessing(additional_subprograms=['my_convenience_procedure'])
One or more filters can be attached to the display and file handlers to control what logs that are passed to the output. Filters are either pass filters that pass logs of the specified type or stop filters which pass anything but the specified type. The type to pass or stop is based on either the log level or the log source ID
The two procedures below show how you can create pass and stop filters on one or more log levels.
stop_level((debug, verbose), display_handler, my_display_filter);
pass_level(error, file_handler, my_file_filter);
The last procedure parameter is of type log_filter_t
and returns the created filter which is used as reference if you want to remove the filter.
remove_filter(my_display_filter);
You can also apply the same filter to both handlers.
stop_level((debug, verbose), (display_handler, file_handler), my_filter);
Pass and stop filters that act on the log source can also be created. For example
stop_source("UART receiver", display_handler, uart_rx_stop_filter);
pass_source("status message", file_handler, status_msg_pass_filter);
It's also possible to filter hierarchies of sources. If the sources of a number of logs have the same prefix and the prefix starts with a point or a colon then you can create a filter that apply to all of them. The typical use case is something like this.
info("Some message", my_module'path_name & "monitor1");
info("Another message", my_module'path_name & "monitor2");
The following filter will stop both of them.
stop_source(my_module'path_name, display_handler, my_module_stop_filter);
Log level(s) and log source can be combined in a single filter. For example, this filter will stop debug logs from the UART source.
stop_source_level("UART", debug, display_handler, stop_uart_debug_msg_filter);
Any type of data object can be logged as long as you can express it as a string. For example, you can log an integer by using the associated to_string
function. The VUnit com
package also has functionality for generating to_string
functions for your custom data types which makes them easy to log as well. For more information see the com
user guide.
Previous chapters have used the built-in default logger for the examples but you can also create your own loggers. You do that by declaring a (shared) variable of type logger_t
.
shared variable my_logger : logger_t;
and then you use that variable as the first parameter in the procedure calls presented in the previous chapters, for example.
log(my_logger, "Hello world");