Skip to content

Commit

Permalink
Make LogLevel a smart enum (#163)
Browse files Browse the repository at this point in the history
Makes LogLevel into a smart enum. It can be used as an enum still for
most cases, but makes the log API simplier, and also allows setting log
levels from strings instead of just enum constants which makes it easier
to configure
  • Loading branch information
TrentHouliston authored Dec 23, 2024
1 parent 8496dca commit 0fb9278
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 155 deletions.
211 changes: 148 additions & 63 deletions src/LogLevel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,93 +39,178 @@ namespace NUClear {
* Log levels are used to provide different levels of detail on a per-reactor basis.
* The logging level of a reactor can be changed by setting it in the install function.
*/
enum LogLevel : uint8_t {
/**
* Don't use this log level when emitting logs, it is for setting reactor log level from non reactor sources.
*
* Specifically when a NUClear::log is called from code that is not running in a reaction (even transitively) then
* the reactor_level will be set to UNKNOWN.
*/
UNKNOWN,
class LogLevel {
public:
enum Value : uint8_t {
/**
* Don't use this log level when emitting logs, it is for setting reactor log level from non reactor sources.
*
* Specifically when a NUClear::log is called from code that is not running in a reaction (even transitively)
* then the reactor_level will be set to UNKNOWN.
*/
UNKNOWN,

/**
* The Trace level contains messages that are used to trace the exact flow of execution.
*
* This level is extremely verbose and often has a message per line of code.
*/
TRACE,

/**
* Debug contains messages that represent the inputs and outputs of different computation units.
*
* If you have a function that performs three steps to do something then it's likely that you will have a
* message for the input and output of those three steps. Additionally you would likely have messages that check
* if it hit different branches.
*/
DEBUG,

/**
* The info level is used to provide high level goal messages such as function start or successful completion.
*
* This shows when key user-facing functionality is executed and tells us that everything is working without
* getting into the details.
*/
INFO,

/**
* The warning level is used to notify us that everything might not be working perfectly.
*
* Warnings are errors or inconsistencies that aren't fatal and generally do not completely break the system.
* However a warning message should require action and should point to a section of the system that needs
* attention.
*/
WARN,

/**
* The error level is used to report unexpected behavior.
* This level doesn't need to prefix a program-crashing issue but should be used to report major unexpected
branches
* in logic or other constraint breaking problems such as failed assertions.
* All errors should require action from someone and should be addressed immediately.
*/
ERROR,

/**
* Fatal is a program destroying error that needs to be addressed immediately.
*
* If a fatal message is sent it should point to something that should never ever happen and ideally provide as
* much information as possible as to why it crashed. Fatal messages require action immediately and should
* always be addressed.
*/
FATAL
};

/**
* The Trace level contains messages that are used to trace the exact flow of execution.
* Construct a LogLevel from a Value
*
* This level is extremely verbose and often has a message per line of code.
* @param value The value to construct the LogLevel from
*/
TRACE,
constexpr LogLevel(const Value& value = Value::UNKNOWN) : value(value) {};

/**
* Debug contains messages that represent the inputs and outputs of different computation units.
* Construct a LogLevel from a string
*
* If you have a function that performs three steps to do something then it's likely that you will have a message
* for the input and output of those three steps.
* Additionally you would likely have messages that check if it hit different branches.
* @param level The string to construct the LogLevel from
*/
DEBUG,
LogLevel(const std::string& level)
: value(level == "TRACE" ? LogLevel::TRACE
: level == "DEBUG" ? LogLevel::DEBUG
: level == "INFO" ? LogLevel::INFO
: level == "WARN" ? LogLevel::WARN
: level == "ERROR" ? LogLevel::ERROR
: level == "FATAL" ? LogLevel::FATAL
: LogLevel::UNKNOWN) {};

/**
* The info level is used to provide high level goal messages such as function start or successful completion.
* A call operator which will return the value of the LogLevel
* This can be useful in situations where the implicit conversion operators are ambiguous.
*
* This shows when key user-facing functionality is executed and tells us that everything is working without getting
* into the details.
* @return The value of the LogLevel
*/
INFO,
constexpr Value operator()() const {
return value;
}

/**
* The warning level is used to notify us that everything might not be working perfectly.
* A conversion operator which will return the value of the LogLevel
*
* Warnings are errors or inconsistencies that aren't fatal and generally do not completely break the system.
* However a warning message should require action and should point to a section of the system that needs attention.
* @return The value of the LogLevel
*/
WARN,
constexpr operator Value() const {
return value;
}

/**
* The error level is used to report unexpected behavior.
* This level doesn't need to prefix a program-crashing issue but should be used to report major unexpected branches
* in logic or other constraint breaking problems such as failed assertions.
* All errors should require action from someone and should be addressed immediately.
* A conversion operator which will return the string representation of the LogLevel
*
* @return The string representation of the LogLevel
*/
ERROR,
operator std::string() const {
return value == LogLevel::TRACE ? "TRACE"
: value == LogLevel::DEBUG ? "DEBUG"
: value == LogLevel::INFO ? "INFO"
: value == LogLevel::WARN ? "WARN"
: value == LogLevel::ERROR ? "ERROR"
: value == LogLevel::FATAL ? "FATAL"
: "UNKNOWN";
}

/**
* Fatal is a program destroying error that needs to be addressed immediately.
* Stream the LogLevel to an ostream, it will output the string representation of the LogLevel
*
* @param os The ostream to output to
* @param level The LogLevel to output
*
* If a fatal message is sent it should point to something that should never ever happen and ideally provide as much
* information as possible as to why it crashed.
* Fatal messages require action immediately and should always be addressed.
* @return The ostream that was passed in
*/
FATAL
friend std::ostream& operator<<(std::ostream& os, LogLevel level) {
return os << static_cast<std::string>(level);
}

// Operators to compare LogLevel values and LogLevel to Value
// clang-format off
friend constexpr bool operator<(const LogLevel& lhs, const LogLevel& rhs) { return lhs.value < rhs.value; }
friend constexpr bool operator>(const LogLevel& lhs, const LogLevel& rhs) { return lhs.value > rhs.value; }
friend constexpr bool operator<=(const LogLevel& lhs, const LogLevel& rhs) { return lhs.value <= rhs.value; }
friend constexpr bool operator>=(const LogLevel& lhs, const LogLevel& rhs) { return lhs.value >= rhs.value; }
friend constexpr bool operator==(const LogLevel& lhs, const LogLevel& rhs) { return lhs.value == rhs.value; }
friend constexpr bool operator!=(const LogLevel& lhs, const LogLevel& rhs) { return lhs.value != rhs.value; }

friend constexpr bool operator<(const LogLevel& lhs, const Value& rhs) { return lhs.value < rhs; }
friend constexpr bool operator>(const LogLevel& lhs, const Value& rhs) { return lhs.value > rhs; }
friend constexpr bool operator<=(const LogLevel& lhs, const Value& rhs) { return lhs.value <= rhs; }
friend constexpr bool operator>=(const LogLevel& lhs, const Value& rhs) { return lhs.value >= rhs; }
friend constexpr bool operator==(const LogLevel& lhs, const Value& rhs) { return lhs.value == rhs; }
friend constexpr bool operator!=(const LogLevel& lhs, const Value& rhs) { return lhs.value != rhs; }
friend constexpr bool operator<(const Value& lhs, const LogLevel& rhs) { return lhs < rhs.value; }
friend constexpr bool operator>(const Value& lhs, const LogLevel& rhs) { return lhs > rhs.value; }
friend constexpr bool operator<=(const Value& lhs, const LogLevel& rhs) { return lhs <= rhs.value; }
friend constexpr bool operator>=(const Value& lhs, const LogLevel& rhs) { return lhs >= rhs.value; }
friend constexpr bool operator==(const Value& lhs, const LogLevel& rhs) { return lhs == rhs.value; }
friend constexpr bool operator!=(const Value& lhs, const LogLevel& rhs) { return lhs != rhs.value; }

friend bool operator<(const LogLevel& lhs, const std::string& rhs) { return static_cast<std::string>(lhs) < rhs; }
friend bool operator>(const LogLevel& lhs, const std::string& rhs) { return static_cast<std::string>(lhs) > rhs; }
friend bool operator<=(const LogLevel& lhs, const std::string& rhs) { return static_cast<std::string>(lhs) <= rhs; }
friend bool operator>=(const LogLevel& lhs, const std::string& rhs) { return static_cast<std::string>(lhs) >= rhs; }
friend bool operator==(const LogLevel& lhs, const std::string& rhs) { return static_cast<std::string>(lhs) == rhs; }
friend bool operator!=(const LogLevel& lhs, const std::string& rhs) { return static_cast<std::string>(lhs) != rhs; }
friend bool operator<(const std::string& lhs, const LogLevel& rhs) { return lhs < static_cast<std::string>(rhs); }
friend bool operator>(const std::string& lhs, const LogLevel& rhs) { return lhs > static_cast<std::string>(rhs); }
friend bool operator<=(const std::string& lhs, const LogLevel& rhs) { return lhs <= static_cast<std::string>(rhs); }
friend bool operator>=(const std::string& lhs, const LogLevel& rhs) { return lhs >= static_cast<std::string>(rhs); }
friend bool operator==(const std::string& lhs, const LogLevel& rhs) { return lhs == static_cast<std::string>(rhs); }
friend bool operator!=(const std::string& lhs, const LogLevel& rhs) { return lhs != static_cast<std::string>(rhs); }
// clang-format on


private:
/// The stored enum value
Value value;
};

/**
* This function is used to convert a LogLevel into a string
*
* @param level the LogLevel to convert
*
* @return the string representation of the LogLevel
*/
std::string to_string(const LogLevel& level);

/**
* This function is used to convert a string into a LogLevel
*
* @param level the string to convert
*
* @return the LogLevel representation of the string
*/
LogLevel from_string(const std::string& level);

/**
* This function is used to convert a LogLevel into a string for printing.
*
* @param os the output stream to write to
* @param level the LogLevel to convert
* @return the output stream
*/
std::ostream& operator<<(std::ostream& os, const LogLevel& level);

} // namespace NUClear

#endif // NUCLEAR_LOGLEVEL_HPP
6 changes: 3 additions & 3 deletions src/PowerPlant.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class PowerPlant {
*
* @param args The arguments we are logging
*/
template <enum LogLevel level, typename... Arguments>
template <LogLevel::Value level, typename... Arguments>
void log(Arguments&&... args) {
logger.log(nullptr, level, std::forward<Arguments>(args)...);
}
Expand All @@ -216,7 +216,7 @@ class PowerPlant {
* @param reactor The reactor that is logging
* @param args The arguments we are logging
*/
template <enum LogLevel level, typename... Arguments>
template <LogLevel::Value level, typename... Arguments>
void log(const Reactor* reactor, Arguments&&... args) {
logger.log(reactor, level, std::forward<Arguments>(args)...);
}
Expand Down Expand Up @@ -349,7 +349,7 @@ class PowerPlant {
*
* @param args The arguments to log.
*/
template <enum LogLevel level = NUClear::DEBUG, typename... Arguments>
template <LogLevel::Value level = NUClear::LogLevel::DEBUG, typename... Arguments>
void log(Arguments&&... args) {
if (PowerPlant::powerplant != nullptr) {
PowerPlant::powerplant->log<level>(std::forward<Arguments>(args)...);
Expand Down
36 changes: 7 additions & 29 deletions src/LogLevel.cpp → src/Reactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,15 @@
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include "LogLevel.hpp"

#include <ostream>
#include "Reactor.hpp"

namespace NUClear {

std::string to_string(const LogLevel& level) {
switch (level) {
case LogLevel::TRACE: return "TRACE";
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default:
case LogLevel::UNKNOWN: return "UNKNOWN";
}
}

LogLevel from_string(const std::string& level) {
return level == "TRACE" ? LogLevel::TRACE
: level == "DEBUG" ? LogLevel::DEBUG
: level == "INFO" ? LogLevel::INFO
: level == "WARN" ? LogLevel::WARN
: level == "ERROR" ? LogLevel::ERROR
: level == "FATAL" ? LogLevel::FATAL
: LogLevel::UNKNOWN;
}

std::ostream& operator<<(std::ostream& os, const LogLevel& level) {
return os << to_string(level);
}
constexpr LogLevel Reactor::TRACE;
constexpr LogLevel Reactor::DEBUG;
constexpr LogLevel Reactor::INFO;
constexpr LogLevel Reactor::WARN;
constexpr LogLevel Reactor::ERROR;
constexpr LogLevel Reactor::FATAL;

} // namespace NUClear
15 changes: 14 additions & 1 deletion src/Reactor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ class Reactor {
using WATCHDOG = dsl::word::emit::Watchdog<T>;
};

/// @copydoc NUClear::LogLevel::Value::TRACE
static constexpr LogLevel TRACE = LogLevel::TRACE;
// @copydoc NUClear::LogLevel::Value::DEBUG
static constexpr LogLevel DEBUG = LogLevel::DEBUG;
// @copydoc NUClear::LogLevel::Value::INFO
static constexpr LogLevel INFO = LogLevel::INFO;
// @copydoc NUClear::LogLevel::Value::WARN
static constexpr LogLevel WARN = LogLevel::WARN;
// @copydoc NUClear::LogLevel::Value::ERROR
static constexpr LogLevel ERROR = LogLevel::ERROR;
// @copydoc NUClear::LogLevel::Value::FATAL
static constexpr LogLevel FATAL = LogLevel::FATAL;

/// This provides functions to modify how an on statement runs after it has been created
using ReactionHandle = threading::ReactionHandle;

Expand Down Expand Up @@ -436,7 +449,7 @@ class Reactor {
*
* @param args The arguments we are logging
*/
template <enum LogLevel level = DEBUG, typename... Arguments>
template <LogLevel::Value level = DEBUG, typename... Arguments>
void log(Arguments&&... args) const {
// Short circuit here before going to the more expensive log function
if (level >= min_log_level || level >= log_level) {
Expand Down
2 changes: 1 addition & 1 deletion src/util/Logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ namespace util {
* Describes the log levels for a particular reactor.
*/
struct LogLevels {
LogLevels(LogLevel display_log_level, LogLevel min_log_level)
LogLevels(const LogLevel& display_log_level, const LogLevel& min_log_level)
: display_log_level(display_log_level), min_log_level(min_log_level) {}

/// The log level that should be displayed
Expand Down
Loading

0 comments on commit 0fb9278

Please sign in to comment.