Skip to content

Commit 1f961e2

Browse files
feat: Add human readable date to logging formats. (ros2#510)
Backport of ros2#441 Signed-off-by: Tomoya Fujita <Tomoya.Fujita@sony.com> Co-authored-by: Kaju Bubanja <bubanja.kaju@gmail.com>
1 parent 21a77b8 commit 1f961e2

4 files changed

Lines changed: 135 additions & 1 deletion

File tree

include/rcutils/time.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,37 @@ rcutils_time_point_value_as_nanoseconds_string(
143143
char * str,
144144
size_t str_size);
145145

146+
/// Return a time point as an datetime in local time with milliseconds in a string.
147+
/**
148+
*
149+
* If the given string is not large enough, the result will be truncated.
150+
* If you need a string with variable width, using `snprintf()` directly is
151+
* recommended.
152+
*
153+
* <hr>
154+
* Attribute | Adherence
155+
* ------------------ | -------------
156+
* Allocates Memory | No [1]
157+
* Thread-Safe | Yes
158+
* Uses Atomics | No
159+
* Lock-Free | Yes
160+
* <i>[1] if `snprintf()` does not allocate additional memory internally</i>
161+
*
162+
* \param[in] time_point the time to be made into a string
163+
* \param[out] str the output string in which it is stored
164+
* \param[in] str_size the size of the output string
165+
* \return #RCUTILS_RET_OK if successful (even if truncated), or
166+
* \return #RCUTILS_RET_INVALID_ARGUMENT if any arguments are invalid, or
167+
* \return #RCUTILS_RET_ERROR if an unspecified error occur.
168+
*/
169+
RCUTILS_PUBLIC
170+
RCUTILS_WARN_UNUSED
171+
rcutils_ret_t
172+
rcutils_time_point_value_as_date_string(
173+
const rcutils_time_point_value_t * time_point,
174+
char * str,
175+
size_t str_size);
176+
146177
/// Return a time point as floating point seconds in a string.
147178
/**
148179
* The number is always fixed width, with left padding zeros up to the maximum

src/logging.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,13 @@ const char * expand_time(
593593
APPEND_AND_RETURN_LOG_OUTPUT(numeric_storage);
594594
}
595595

596+
const char * expand_time_as_date(
597+
const logging_input * logging_input,
598+
rcutils_char_array_t * logging_output)
599+
{
600+
return expand_time(logging_input, logging_output, rcutils_time_point_value_as_date_string);
601+
}
602+
596603
const char * expand_time_as_seconds(
597604
const logging_input * logging_input,
598605
rcutils_char_array_t * logging_output)
@@ -686,6 +693,7 @@ static const token_map_entry tokens[] = {
686693
{.token = "time", .handler = expand_time_as_seconds},
687694
{.token = "time_as_nanoseconds", .handler = expand_time_as_nanoseconds},
688695
{.token = "line_number", .handler = expand_line_number},
696+
{.token = "date_time_with_ms", .handler = expand_time_as_date},
689697
};
690698

691699
token_handler find_token_handler(const char * token)

src/time.c

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ extern "C"
2020
#include "rcutils/time.h"
2121

2222
#include <inttypes.h>
23-
#include <stdint.h>
2423
#include <stdio.h>
2524
#include <stdlib.h>
25+
#include <time.h>
2626

2727
#include "rcutils/allocator.h"
2828
#include "rcutils/error_handling.h"
@@ -46,6 +46,61 @@ rcutils_time_point_value_as_nanoseconds_string(
4646
return RCUTILS_RET_OK;
4747
}
4848

49+
rcutils_ret_t
50+
rcutils_time_point_value_as_date_string(
51+
const rcutils_time_point_value_t * time_point,
52+
char * str,
53+
size_t str_size)
54+
{
55+
RCUTILS_CHECK_ARGUMENT_FOR_NULL(time_point, RCUTILS_RET_INVALID_ARGUMENT);
56+
RCUTILS_CHECK_ARGUMENT_FOR_NULL(str, RCUTILS_RET_INVALID_ARGUMENT);
57+
if (0 == str_size) {
58+
return RCUTILS_RET_OK;
59+
}
60+
// best to abs it to avoid issues with negative values in C89, see:
61+
// https://stackoverflow.com/a/3604984/671658
62+
uint64_t abs_time_point = (uint64_t)llabs(*time_point);
63+
// break into two parts to avoid floating point error
64+
uint64_t seconds = abs_time_point / (1000u * 1000u * 1000u);
65+
uint64_t nanoseconds = abs_time_point % (1000u * 1000u * 1000u);
66+
// Make sure the buffer is large enough to hold the largest possible uint64_t
67+
char nanoseconds_str[21];
68+
69+
if (rcutils_snprintf(nanoseconds_str, sizeof(nanoseconds_str), "%" PRIu64, nanoseconds) < 0) {
70+
RCUTILS_SET_ERROR_MSG("failed to format time point nanoseconds into string");
71+
return RCUTILS_RET_ERROR;
72+
}
73+
74+
time_t now_t = (time_t)(seconds);
75+
struct tm ptm = {.tm_year = 0, .tm_mday = 0};
76+
#ifdef _WIN32
77+
if (localtime_s(&ptm, &now_t) != 0) {
78+
RCUTILS_SET_ERROR_MSG("failed to get localtime");
79+
return RCUTILS_RET_ERROR;
80+
}
81+
#else
82+
if (localtime_r(&now_t, &ptm) == NULL) {
83+
RCUTILS_SET_ERROR_MSG("failed to get localtime");
84+
return RCUTILS_RET_ERROR;
85+
}
86+
#endif
87+
88+
if (str_size < 32 || strftime(str, 32, "%Y-%m-%d %H:%M:%S", &ptm) == 0) {
89+
RCUTILS_SET_ERROR_MSG("failed to format time point into string as iso8601_date");
90+
return RCUTILS_RET_ERROR;
91+
}
92+
static const int date_end_position = 19;
93+
if (rcutils_snprintf(
94+
&str[date_end_position], str_size - date_end_position, ".%.3s",
95+
nanoseconds_str) < 0)
96+
{
97+
RCUTILS_SET_ERROR_MSG("failed to format time point into string as date_time_with_ms");
98+
return RCUTILS_RET_ERROR;
99+
}
100+
101+
return RCUTILS_RET_OK;
102+
}
103+
49104
rcutils_ret_t
50105
rcutils_time_point_value_as_seconds_string(
51106
const rcutils_time_point_value_t * time_point,

test/test_time.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,46 @@ TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_nanoseconds_string) {
301301
EXPECT_STREQ("-0000000000000000100", buffer);
302302
}
303303

304+
// Tests the rcutils_time_point_value_as_date_string() function.
305+
TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_date_string) {
306+
rcutils_ret_t ret;
307+
rcutils_time_point_value_t timepoint;
308+
char buffer[256] = "";
309+
310+
// Typical use case.
311+
timepoint = 100;
312+
ret = rcutils_time_point_value_as_date_string(&timepoint, buffer, sizeof(buffer));
313+
EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str;
314+
std::tm t = {};
315+
std::istringstream ss(buffer);
316+
// To test that it works we call it once with the correct format string
317+
ss >> std::get_time(&t, "%Y-%m-%d %H:%M:%S");
318+
ASSERT_FALSE(ss.fail());
319+
std::istringstream ss2(buffer);
320+
// and once with the false one
321+
ss2 >> std::get_time(&t, "%Y-%b-%d %H:%M:%S");
322+
ASSERT_TRUE(ss2.fail());
323+
324+
// nullptr for timepoint
325+
ret = rcutils_time_point_value_as_date_string(nullptr, buffer, sizeof(buffer));
326+
EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret);
327+
rcutils_reset_error();
328+
329+
// nullptr for string
330+
timepoint = 100;
331+
ret = rcutils_time_point_value_as_date_string(&timepoint, nullptr, 0);
332+
EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret);
333+
rcutils_reset_error();
334+
335+
const char * test_str = "should not be touched";
336+
timepoint = 100;
337+
// buffer is of size 256, so it will fit
338+
(void)memmove(buffer, test_str, strlen(test_str) + 1);
339+
ret = rcutils_time_point_value_as_date_string(&timepoint, buffer, 0);
340+
EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str;
341+
EXPECT_STREQ(test_str, buffer);
342+
}
343+
304344
// Tests the rcutils_time_point_value_as_seconds_string() function.
305345
TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_seconds_string) {
306346
rcutils_ret_t ret;

0 commit comments

Comments
 (0)