Compositional C++ log framework for ROS2

I have been bothered by the verbosity of ROS2 logging macros for a while, so today I tried my hand at building an alternative. I wanted a solution built around C++ streams, that was terse and compositional, and minimized the number of arguments I’d have to provide in a logging statement.

What I did was to create a template class log with two parameters, the severity level and a Logger callable class. log encapsulates a stringstream, and on destruction passes the severity and the accumulated state of that object to the Logger callable. By default Logger simply prints the accumulated statement with the given severity, but it can be replaced by e.g. a throttle logger that implements throttling logging.

See the code below for details:

/**
 * @file
 *
 * Logging utilities.
 *
 * @author Helio Perroni Filho
 */

#ifndef ROS_COMMONS_LOG_H
#define ROS_COMMONS_LOG_H

#include <rclcpp/rclcpp.hpp>

namespace ros_commons {

namespace ros = rclcpp;

/**
 * @brief Debug severity level.
 */
constexpr enum RCUTILS_LOG_SEVERITY debug = RCUTILS_LOG_SEVERITY_DEBUG;

/**
 * @brief Info severity level.
 */
constexpr enum RCUTILS_LOG_SEVERITY info = RCUTILS_LOG_SEVERITY_INFO;

/**
 * @brief Warning severity level.
 */
constexpr enum RCUTILS_LOG_SEVERITY warn = RCUTILS_LOG_SEVERITY_WARN;

/**
 * @brief Error severity level.
 */
constexpr enum RCUTILS_LOG_SEVERITY error = RCUTILS_LOG_SEVERITY_ERROR;

/**
 * @brief Fatal error severity level.
 */
constexpr enum RCUTILS_LOG_SEVERITY fatal = RCUTILS_LOG_SEVERITY_FATAL;

/**
 * @brief Simple logger, emits statements with given severity level.
 */
struct simple {
  inline void operator () (enum RCUTILS_LOG_SEVERITY severity, ros::Node *node, const std::string &statement) {
    RCUTILS_LOG_COND_NAMED(
      severity,
      RCUTILS_LOG_CONDITION_EMPTY, RCUTILS_LOG_CONDITION_EMPTY,
      node->get_logger().get_name(), "%s", statement.c_str()
    );
  }
};

/**
 * @brief Throttled logger, emits statements with given severity level at a set interval.
 */
template<rcutils_duration_value_t duration>
struct throttle {
  inline void operator () (enum RCUTILS_LOG_SEVERITY severity, ros::Node *node, const std::string &statement) {
    auto get_time_point = [node](rcutils_time_point_value_t *time_point) -> rcutils_ret_t {
      try {
        *time_point = node->now().nanoseconds();
      }
      catch (...) {
        RCUTILS_SAFE_FWRITE_TO_STDERR("[rclcpp|logging.hpp] RCLCPP_DEBUG_THROTTLE could not get current time stamp\n");
        return RCUTILS_RET_ERROR;
      }

      return RCUTILS_RET_OK;
    };

    RCUTILS_LOG_COND_NAMED(
      severity,
      RCUTILS_LOG_CONDITION_THROTTLE_BEFORE(get_time_point, duration), RCUTILS_LOG_CONDITION_THROTTLE_AFTER,
      node->get_logger().get_name(), "%s", statement.c_str()
    );
  }
};

/**
 * @brief Build and execute a log statement.
 */
template<enum RCUTILS_LOG_SEVERITY severity, class Logger = simple>
class log {
  /** @brief Logging function. */
  Logger log_;

  /** @brief Pointer to underlying ROS node. */
  ros::Node *node_;

  /** @brief String stream to build log statement. */
  std::stringstream stream_;

public:
  /**
   * @brief Create the log statement builder.
   */
  inline log(ros::Node *node):
    node_(node)
  {
    // Nothing to do.
  }

  /**
   * @brief Class destructor.
   */
  inline ~log() {
    log_(severity, node_, stream_.str());
  }

  /**
   * @brief Add the given value to the log statement.
   */
  template<class T>
  inline log &operator << (const T &value) {
    stream_ << value;
    return *this;
  }
};

} // namespace ros_commons

#endif

With appropriate using statements the above allows me to write logging calls as below:

using ros_commons::log;
using ros_commons::info;
using ros_commons::warn;
using ros_commons::error;
using ros_commons::fatal;
using ros_commons::throttle;

// ...

// "this" here is assumed to be of type Node*.
log<info>(this) << "A logging statement of INFO severity."

log<warn, throttle<1000>>(this) << "A warning that is repeated every " << 1 << " second at most."

Compared to standard ROS2 macros, this has the disadvantage that logging statements can’t be as easily suppressed at compile time: I thought of setting the severity levels to RCUTILS_LOG_SEVERITY_UNSET depending on the value of RCLCPP_LOG_MIN_SEVERITY, then using template specialization to select an empty version of ~log(), but that still wouldn’t suppress the operator << calls.

On the bright side, this does tick all my boxes: logging statements are simpler and more compact, with an easier to remember API. And I don’t usually play with compile-time severity level filtering, so the loss of that ability doesn’t really affect me.

Thoughts?

1 Like

I have been bothered by the verbosity of ROS2 logging macros for a while

Same experience :smiley:

the disadvantage that logging statements can’t be as easily suppressed at compile time

I managed to get a solution working that leverages the native rcl logging mechanism and it’s goodies:

ros2_fmt_logger

Did you know of this solution or are we tackling the same problem? :slight_smile:

Did you know of this solution or are we tackling the same problem?

No, I didn’t know of your library. I see a lot of good stuff there, but I think that while we are addressing the same basic problem (streamlining ROS2 logging), our requirements are somewhat different.

In my case, I want logging calls to be as concise as possible, for options to be specified by composition rather than by different “monolithic” identifiers (e.g. log<info, throttle<period>> rather than Logger::info_throttle) and for formatting to be done with C++ streams, which leverages existing operator << overloads for ROS message and the like.

I admit this is ultimately a matter of personal taste. I see a lot of modern C++ code using {fmt}-style string formatting, and of course there’s std::format in C++ 20, but I still prefer to just place an object at the exact location I want it to be embedded in a generated string, without having to think about placeholder tags.

As for compile-time suppression, I looked into it and I think it’s actually possible to have operator << calls optimized away if the code is inlined and has no side-effects. This could be implemented by setting the severity level identifiers to RCUTILS_LOG_SEVERITY_UNSET depending on the value of RCLCPP_LOG_MIN_SEVERITY, then using template specialization to select a hollowed-out version of the log class. But as I said this is not high on my priority list, since I don’t really switch log calls that way in my own code.