Logging
It's often useful to print log messages in an application, either as a quick way of troubleshooting a problem without having to use a debugger, or as a more permanent means of reporting errors or other interesting information at runtime. In the latter case you can use one of the many logging frameworks for C++ that are available. Examples of such frameworks include, but are certainly not limited to:
However, the TargetRTS also includes a simple logging service which you can directly use without the need for a special logging library. This logging service provides a thread-safe way of logging messages and data objects to stderr
and stdout
, but not much more than that.
Log Port
A capsule can access the logging service by means of a log port, which is a port typed by the predefined Log
protocol. Log ports should be non-service behavior ports, and don't accept any events, but rather provide a set of functions which the capsule can call for logging messages. Here is an example of a capsule which uses a log port for writing an error message if the initial transition does not receive an expected data object:
capsule C {
behavior port log : Log;
statemachine {
state S;
initial - State1
`
if (rtdata == nullptr) {
log.log("Missing initialization data for capsule C");
log.commit();
}
`;
};
};
Note the following:
- To flush the log and make sure the logged message becomes visible you must call
commit()
. - Use
show()
instead oflog()
to avoid printing a carriage return after the message. - You can print an indented message to the log by calling
crtab()
.
Using a log port comes with certain limitations:
- Since ports only can be added to capsules, you can only use log ports in code that is located in a capsule.
- Log ports only let you log messages to
stderr
. - Logging with log ports is thread-safe but if you want to log bigger messages by making several calls to
show()
orlog()
from different threads, the log messages could be interleaved onstderr
. This is because all logging functions eventually use the class RTDiagStream for writing in a thread-safe way tostderr
. You can avoid interleaving printouts in a multi-threaded application by first creating the string to log (for example usingstd::stringstream
) and then print it with a single call tolog()
orshow()
. - While you can log not only strings but also log data by calling different overloads of
show()
andlog()
, the type of the data will be printed before the value. This may or may not be desirable.
If any of these limitations is a concern, you can use a log stream instead of a log port.
Logging Data with a Log Port
A log port provides several overloaded versions of log()
(and show()
) which lets you log the values of variables. There are overloads for most primitive C++ types, and for some types from the TargetRTS such as RTString
. You can also log objects of user-defined types, provided that they have a type descriptor. Here is an example of using a log port called log
for logging a few values of different types:
struct [[rt::auto_descriptor]] MyType {
int x = 0;
int y = 0;
};
// ...
int x = 14;
RTString s = "Hello!";
MyType mt; // User-defined type with a type descriptor
log.log(x); // Calls Log::Base::log(int)
log.log(s); // Calls Log::Base::log(const RTString&)
log.log(&mt, &RTType_MyType); // Calls Log::Base::log(const void*, RTObject_class*)
Note that when logging an object of a user-defined type, you must provide the type descriptor as a second argument to log()
or show()
.
When data is logged like this, the standard RTAsciiEncoding
is used, which creates a string that typically contains both the type of the data, and the value. For the above example, the output will look like this:
int 14
Hello!
MyType{x 0,y 0}
If you want to encode data differently when logging, you can construct a string with std::stringstream
and then log it. For example:
std::stringstream ss;
ss << x << " " << s.Contents << " " << mt;
log.log(ss.str().c_str());
To allow a user-defined type to be handled by std::stringstream
you need to implement the global operator<<
for it. For example:
std::ostream& operator<< (std::ostream& out, const MyType& mt) {
out << "[" << mt.x << "," << mt.y << "]";
return out;
}
With this implementation, the data will instead be logged like this:
14 Hello! [0,0]
See also Logging Data with a Log Stream.
Example
You can find a sample application that uses a log port for logging data here.
It shows both how data is string encoded by default, and how you can implement a custom encoding by means of std::stringstream
.
Log Stream
An alternative to logging with a log port is to use a log stream. The Log
struct has two members out
and err
which lets you stream log messages either to stdout
or stderr
using the C++ insertion operator (<<
). Here is an example:
Log::out << "info" << Log::endl;
Log::err << "error" << std::endl;
Log::endl
will print a newline and flush the log stream. It's also possible to use the standard std::endl
with the same result.
Logging Data with a Log Stream
With a log stream data can be logged in the same way as when using standard C++ stream objects. In fact, the log stream internally uses an std::ostream
. This means that you can use all the standard manipulators to control how to format the data. Here are some examples:
Log::out << "int: " << 5 << Log::endl;
bool b = true;
Log::out << b << Log::endl;
Log::out << std::boolalpha << b << Log::endl;
double pi = 3.14159;
Log::out << std::setprecision(2) << pi << Log::endl;
Log::out << std::setprecision(3) << std::scientific << pi << Log::endl;
std::string world = "world";
Log::out << "hello" << std::setw(10) << world << Log::endl;
The output will look like this:
int: 5
1
true
3.1
3.142e+00
hello world
To log data of a user-defined type you need to provide the data object and its type descriptor by means of an RTTypedValue
object. For example:
struct [[rt::auto_descriptor]] MyType {
int x = 1;
int y = 2;
};
// ...
MyType mt;
Log::out << "MyType value: " << RTTypedValue(&mt, &RTType_MyType) << Log::endl;
Just like when logging data with a log port the value of the user-defined type will be encoded using the standard RTAsciiEncoding
, so the output will look like this:
MyType value: MyType{x 1,y 2}
Locking and Unlocking the Log Streams
If you use a log stream from multiple threads, log messages could become interleaved when printed. To prevent this you can lock the log streams before logging a message, and then unlock them afterwards. While the log streams are locked by a thread, other threads that attempt to lock them will be blocked until the log streams are unlocked again. This is hence a simple way to give a certain thread exclusive thread-safe access to the log streams so it can fully print a certain log message, without risk that other threads write something to the log streams at the same time.
As an example, assume a message is logged to stderr
by printing three separate strings:
Log::err << "This is a compound log message, " << "printed by thread " << context()->name() << Log::endl;
If this piece of code runs at the same time from multiple threads, the messages could be interleaved, because there can be a thread context switch after the printing of each substring. For example, the logged messages could look like this:
This is a compound log message, printed by thread Thread4
This is a compound log message, This is a compound log message, printed by thread Thread2
printed by thread Thread1
This is a compound log message, printed by thread Thread3
To avoid this you can use Log::lock
before we start to log the message, and Log::unlock
after it has been logged:
Log::err << Log::lock << "This is a compound log message, " << "printed from thread " << context()->name() << Log::endl << Log::unlock;
Note the following:
- Locking/unlocking applies to both log streams at the same time. That is, if you lock/unlock
Log::err
, thenLog::out
is also locked/unlocked and vice versa. - Calls to
Log::lock
andLog::unlock
should always be balanced. - To avoid performance problems you should not print too many log messages while the log streams are locked. Printing a single sentence followed by
Log::endl
can be a good compromise between performance and log readability.
Example
You can find a sample application that uses a log stream and locks/unlocks it for thread-safe logging of compound log messages here.
TargetRTS Error Logging
The TargetRTS itself uses logging to stderr
to report run-time errors that may happen when your application runs. In a correctly implemented application there of course should not be any such error messages reported, but while the application is developed and still contains bugs, it's useful to be informed about run-time problems when they do occur.
For example, assume you have a fixed capsule part p
in capsule Top
, and try to create a capsule instance dynamically in p
by calling incarnate()
on a Frame port. This is an error, since a fixed capsule part gets automatically incarnated with all capsule instances it can contain (according to its multiplicity) already when the container capsule instance is created. After that the fixed capsule part is "full" and cannot contain any more capsule instances. The TargetRTS reports this error by logging this message:
incarnate(application(Top)<<machine>>.p[-1]): Reference full.
In addition to the actual error message ("Reference full") the text contains information that helps you identify the source of the problem. For this situation:
incarnate
The TargetRTS function that was called when the error occurred.application
The capsule part in which the error occured. The special nameapplication
denotes the implicit part in which the top capsule instance is located, i.e. the very root of the capsule instance tree.Top
The type (i.e. capsule) of the capsule part in which the error occurred.<machine>
The state of the capsule state machine that is currently active when the error occurred. The special name<machine>
denotes the implicit state that is active before the first state is activated (i.e. if the error occurs in the initial transition).p
The capsule part that could not be incarnated.-1
The index at which a new capsule instance was attempted to be incarnated. -1 means it was not specified and the first "free" index would be used.
Other error messages may provide other types of information, but many use the same or a similar format for specifying the capsule part, capsule and active state.
Let's look at another common run-time error which occurs if a capsule receives an event which doesn't trigger a transition in its state machine (typically because you forgot to create a transition with a matching trigger for the event). In this case RTActor::unexpectedMessage()
gets called and prints information that can help you understand what happened. Here is an example of what such a log message may look like:
engine(1)@TOP::Waiting received unexpected message: wheel[2]%alarm data: RTString"FATAL"
This text means that a message for the event alarm
was received on the port wheel
at index 2. The message carried a string parameter with the value FATAL
. When the message was dispatched to the capsule instance, its state machine was in the state Waiting
(which is a substate of TOP
). The capsule instance is located in a part engine
at index 1. All indices here are 1-based.
Retrieving the Most Recent Error
In addition to logging run-time errors, the TargetRTS also stores the most recently occurred error in the RTController. This means that for every thread in the application, you can find out the most recent error that occurred in one of the capsule instances that are run by that thread. Errors are identified by literals from the RTController::Error
enumeration. If no error has occurred, the ok
literal (with value 0) is used.
You can access the most recent error either by calling RTController::getError()
, or directly from capsule context by calling RTActor::getError()
.
The typical use case when retrieving the most recent error is useful, is when you call a TargetRTS function that fails, but not because of a problem in the application itself, but perhaps because of a problem in the environment, such as an out-of-memory situation. Most TargetRTS functions indicate failure by returning 0 (and success by returning 1). In some cases the function may return an object on which you can call a function such as isValid()
to check for success.
If you detect that a function failed, you can find out more information by retrieving the most recent error. Note that you have to do this immediately after the failing function returns, since only the last occurred error is stored. If you return from your code snippet, the TargetRTS will invoke a transition of another capsule managed by the same controller, which would overwrite the stored error if another error occurs (since that then would be the most recent error).
Here is an example of how to check if an event could not be sent, and if so, retrieve the error that explains why it failed:
if (!p.myEvent().send()) {
RTController::Error error = getError();
if (error == RTController::noMem) {
/* out of memory */
context()->perror("Out of memory");
}
}
The function RTController::perror()
prints the error message to stderr
with a custom prefix. If you want to access the error message without printing it, you can call RTController::strerror()
.