From 961c4c8d7afed2800fbd4299335385dbc2375d0b Mon Sep 17 00:00:00 2001 From: Kaju-Bubanja Date: Fri, 15 Mar 2024 15:14:26 +0100 Subject: [PATCH] feat: Add human readable date to logging formats (#441) * feat: Add human readable date to logging formats Signed-off-by: Kaju Bubanja --- include/rcutils/time.h | 31 +++++++++++++++++++++++ src/logging.c | 12 +++++++++ src/time.c | 57 +++++++++++++++++++++++++++++++++++++++++- test/test_time.cpp | 40 +++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/include/rcutils/time.h b/include/rcutils/time.h index bff3e41a..0fb765e7 100644 --- a/include/rcutils/time.h +++ b/include/rcutils/time.h @@ -143,6 +143,37 @@ rcutils_time_point_value_as_nanoseconds_string( char * str, size_t str_size); +/// Return a time point as an datetime in local time with milliseconds in a string. +/** + * + * If the given string is not large enough, the result will be truncated. + * If you need a string with variable width, using `snprintf()` directly is + * recommended. + * + *
+ * Attribute | Adherence + * ------------------ | ------------- + * Allocates Memory | No [1] + * Thread-Safe | Yes + * Uses Atomics | No + * Lock-Free | Yes + * [1] if `snprintf()` does not allocate additional memory internally + * + * \param[in] time_point the time to be made into a string + * \param[out] str the output string in which it is stored + * \param[in] str_size the size of the output string + * \return #RCUTILS_RET_OK if successful (even if truncated), or + * \return #RCUTILS_RET_INVALID_ARGUMENT if any arguments are invalid, or + * \return #RCUTILS_RET_ERROR if an unspecified error occur. + */ +RCUTILS_PUBLIC +RCUTILS_WARN_UNUSED +rcutils_ret_t +rcutils_time_point_value_as_date_string( + const rcutils_time_point_value_t * time_point, + char * str, + size_t str_size); + /// Return a time point as floating point seconds in a string. /** * The number is always fixed width, with left padding zeros up to the maximum diff --git a/src/logging.c b/src/logging.c index a8cc40c5..7aac274a 100644 --- a/src/logging.c +++ b/src/logging.c @@ -212,6 +212,17 @@ static const char * expand_time( return logging_output->buffer; } +static const char * expand_time_as_date( + const logging_input_t * logging_input, + rcutils_char_array_t * logging_output, + size_t start_offset, size_t end_offset) +{ + (void)start_offset; + (void)end_offset; + + return expand_time(logging_input, logging_output, rcutils_time_point_value_as_date_string); +} + static const char * expand_time_as_seconds( const logging_input_t * logging_input, rcutils_char_array_t * logging_output, @@ -383,6 +394,7 @@ static const token_map_entry_t tokens[] = { {.token = "function_name", .handler = expand_function_name}, {.token = "file_name", .handler = expand_file_name}, {.token = "time", .handler = expand_time_as_seconds}, + {.token = "date_time_with_ms", .handler = expand_time_as_date}, {.token = "time_as_nanoseconds", .handler = expand_time_as_nanoseconds}, {.token = "line_number", .handler = expand_line_number}, }; diff --git a/src/time.c b/src/time.c index c1b20878..882c9d98 100644 --- a/src/time.c +++ b/src/time.c @@ -20,9 +20,9 @@ extern "C" #include "rcutils/time.h" #include -#include #include #include +#include #include "rcutils/allocator.h" #include "rcutils/error_handling.h" @@ -46,6 +46,61 @@ rcutils_time_point_value_as_nanoseconds_string( return RCUTILS_RET_OK; } +rcutils_ret_t +rcutils_time_point_value_as_date_string( + const rcutils_time_point_value_t * time_point, + char * str, + size_t str_size) +{ + RCUTILS_CHECK_ARGUMENT_FOR_NULL(time_point, RCUTILS_RET_INVALID_ARGUMENT); + RCUTILS_CHECK_ARGUMENT_FOR_NULL(str, RCUTILS_RET_INVALID_ARGUMENT); + if (0 == str_size) { + return RCUTILS_RET_OK; + } + // best to abs it to avoid issues with negative values in C89, see: + // https://stackoverflow.com/a/3604984/671658 + uint64_t abs_time_point = (uint64_t)llabs(*time_point); + // break into two parts to avoid floating point error + uint64_t seconds = abs_time_point / (1000u * 1000u * 1000u); + uint64_t nanoseconds = abs_time_point % (1000u * 1000u * 1000u); + // Make sure the buffer is large enough to hold the largest possible uint64_t + char nanoseconds_str[21]; + + if (rcutils_snprintf(nanoseconds_str, sizeof(nanoseconds_str), "%" PRIu64, nanoseconds) < 0) { + RCUTILS_SET_ERROR_MSG("failed to format time point nanoseconds into string"); + return RCUTILS_RET_ERROR; + } + + time_t now_t = (time_t)(seconds); + struct tm ptm = {.tm_year = 0, .tm_mday = 0}; +#ifdef _WIN32 + if (localtime_s(&ptm, &now_t) != 0) { + RCUTILS_SET_ERROR_MSG("failed to get localtime"); + return RCUTILS_RET_ERROR; + } +#else + if (localtime_r(&now_t, &ptm) == NULL) { + RCUTILS_SET_ERROR_MSG("failed to get localtime"); + return RCUTILS_RET_ERROR; + } +#endif + + if (str_size < 32 || strftime(str, 32, "%Y-%m-%d %H:%M:%S", &ptm) == 0) { + RCUTILS_SET_ERROR_MSG("failed to format time point into string as iso8601_date"); + return RCUTILS_RET_ERROR; + } + static const int date_end_position = 19; + if (rcutils_snprintf( + &str[date_end_position], str_size - date_end_position, ".%.3s", + nanoseconds_str) < 0) + { + RCUTILS_SET_ERROR_MSG("failed to format time point into string as date_time_with_ms"); + return RCUTILS_RET_ERROR; + } + + return RCUTILS_RET_OK; +} + rcutils_ret_t rcutils_time_point_value_as_seconds_string( const rcutils_time_point_value_t * time_point, diff --git a/test/test_time.cpp b/test/test_time.cpp index 4cf6af1a..5976cabf 100644 --- a/test/test_time.cpp +++ b/test/test_time.cpp @@ -287,6 +287,46 @@ TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_nanoseconds_string) { EXPECT_STREQ("-0000000000000000100", buffer); } +// Tests the rcutils_time_point_value_as_date_string() function. +TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_date_string) { + rcutils_ret_t ret; + rcutils_time_point_value_t timepoint; + char buffer[256] = ""; + + // Typical use case. + timepoint = 100; + ret = rcutils_time_point_value_as_date_string(&timepoint, buffer, sizeof(buffer)); + EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str; + std::tm t = {}; + std::istringstream ss(buffer); + // To test that it works we call it once with the correct format string + ss >> std::get_time(&t, "%Y-%m-%d %H:%M:%S"); + ASSERT_FALSE(ss.fail()); + std::istringstream ss2(buffer); + // and once with the false one + ss2 >> std::get_time(&t, "%Y-%b-%d %H:%M:%S"); + ASSERT_TRUE(ss2.fail()); + + // nullptr for timepoint + ret = rcutils_time_point_value_as_date_string(nullptr, buffer, sizeof(buffer)); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + rcutils_reset_error(); + + // nullptr for string + timepoint = 100; + ret = rcutils_time_point_value_as_date_string(&timepoint, nullptr, 0); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + rcutils_reset_error(); + + const char * test_str = "should not be touched"; + timepoint = 100; + // buffer is of size 256, so it will fit + (void)memmove(buffer, test_str, strlen(test_str) + 1); + ret = rcutils_time_point_value_as_date_string(&timepoint, buffer, 0); + EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str; + EXPECT_STREQ(test_str, buffer); +} + // Tests the rcutils_time_point_value_as_seconds_string() function. TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_seconds_string) { rcutils_ret_t ret;