|
Toast Engine
|
Authors: Xein
Created on: 26 Mar 2026
The logging system provides an easy way to write logs and visualize them after the program execution has finished. It contains a C++ implementation for producing messages, a Rust server for receiving, processing and storing messages, and a Rust TUI for easily reading and searching for information after reading.
To use this system you can simply just include <Toast/Log.hpp> and call any of the toast macros:
This is a quick guide of how to chose the severity of your messages:
This system was created to address two issues that existed on Toast Engine v1 regarding the logging system:
With this in mind, I took the approach of separating the log parser logic into a different process so that when the engine decides to terminate ungratefully, we could detect that event and handle it acordingly. This also liberates the main thread for having to parse or evaluate any logging logic. I decided to handle inter-process communication as a TCP/IP connection since it could be easily asyncronous, would allow a lot of space for our log reader implementation, and would allow me to learn about networking.
A naïve implementation of sending a log to our server could be like this:
where we just send the information every time we want to send a message. However, this will get convoluted as soon as the information we send on a message increases. We could create a struct that contains the information about our log, but the moment we need to introduce interoperability or support multiple compilers, memory alignment could be a potential issue. I fixed this with the addition of a protocol buffer that we can then just convert into a byte stream and send to our server to decode it. In the example above, logging::LogData is not a trivial struct but one that has been autogenerated by a protocol buffer, and the function SerializeToArray() (implemented by the protobuf autogeneration) just converts our message to a stream of bytes.
This method, however, comes with a drawback: big log messages could - potentially - saturate our TCP connection and stall all the threads in our pool. In order to fix this issue, we enqueue the logs rather than send them as soon as they get created, and send a batch of messages and we send all of them as once as soon as the TCP connection is idle. Our implementation of the log function would then look like this:
The implementation of drain() is trivial so it is left to the reader's imagination. Notice as well the std::string_view sink parameter, which just stores information about which class created the log. This will be used for easy filtering later on.
With this, we already have the messages on our server and saving to a CSV is implemented by using a simple library - there is no need to reinvent the wheel - when the TCP connection gets terminated.
While until now I have focused on our second motivation, this implementation also allows for an easy fix of our first concern by creating a client application that connects to our server. On connection, we will first send a batch of all the messages received until that point - easily done since I am already storing every message to serialize it to a CSV file on disconnect -, and we can then send the rest of the messages as soon as received. We do not really care about saturating the send thread of our logging server, since delaying the appearance of our logs in the TUI is not a big concern. This can be implemented as:
As the TUI now has all the logs stored in a list, we can now just create a frontend to the information we have received. This are the base features I implemented:
By using this tool, you can much easily read logs and understand what is happening during runtime.
The Logger can easily be initialized by using the create() function. Any log done after initialization will just work.
Note that the program will terminate if the logger is unable to create or connect to a server, as you will never want to use an application wihout a logging system.
As described above, the following macros are available for logging:
where sink is a valid alias type and the variadic ... is the fmt formatted message. The macro will automatically fill the rest of the parameters required on Logger::log()
In addition to this, the following macro replacing std::assert() exists
where condition is an evaluation that returns a bool and the assert will trigger only when that condition is not met. Think about it as you are declaring that not fulfilling the condition is impossible and you must terminate the program in the case that happens. This is why TOAST_ASSERT() - following the same route as std::assert() - will not be evaluated in Release builds, as an assert condition failing is considered erroneous behaviour and is optimized away.
Kenzo is the official TUI used to read the messages from the log server. It is named after one of my cats. You are allowed to ask for a picture of Kenzo if you are curious. The program will automatically try to connect to localhost by default. You can also connect to a different IP or read from a CSV file if the connection is not established. You can terminate your current connection at any moment by pressing q. Pressing q again on the screen will close Kenzo.
On the program, you can use hjkl to move around and shift + hjkl to change the current window. The program mostly follows vim motions so you can, for example, expect that / is used for searching and n and p to move across selections. Pressing f will open the filters panel, where you can toggle visibility by sink or by severity. In addition to this, you can also use the shorcuts 12345 to toggle severities without having to open the filters panel. You can use s to lock or unlock the automatic scrolling of the logs. Note that when unlocking the scroll, you will automatically select the latest message.
For easily opening CSV files for offline reading, you can use the --csv [path] flag when opening Kenzo.
Surprisingly, the logger is more performant than std::println() added on C++23, with the following times when stress tested:
My guess is that this happens because the actual processing of the log, and writing it to the buffer - which is the most expensive part of logging anyways - is happening server-side, so the game thread doesn't have to do all of that computation.
Anyways, this is the code I used for the stress test:
As a funny note, the CSV file generated after the session was over 800MiB - yummy
One of the implementation I will do at some point is create another client other than Kenzo, for example, a "Logs" window inside the editor.
This can easily be created by just connecting to localhost:12801 and implementing, at minimum, the following features:
Any program that fulfills those requirements is a valid client for the Logging server