Skip to content

Commit 961c4c8

Browse files
authored
feat: Add human readable date to logging formats (#441)
* feat: Add human readable date to logging formats Signed-off-by: Kaju Bubanja <[email protected]>
1 parent d1f292f commit 961c4c8

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed

include/rcutils/time.h

+31
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

+12
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ static const char * expand_time(
212212
return logging_output->buffer;
213213
}
214214

215+
static const char * expand_time_as_date(
216+
const logging_input_t * logging_input,
217+
rcutils_char_array_t * logging_output,
218+
size_t start_offset, size_t end_offset)
219+
{
220+
(void)start_offset;
221+
(void)end_offset;
222+
223+
return expand_time(logging_input, logging_output, rcutils_time_point_value_as_date_string);
224+
}
225+
215226
static const char * expand_time_as_seconds(
216227
const logging_input_t * logging_input,
217228
rcutils_char_array_t * logging_output,
@@ -383,6 +394,7 @@ static const token_map_entry_t tokens[] = {
383394
{.token = "function_name", .handler = expand_function_name},
384395
{.token = "file_name", .handler = expand_file_name},
385396
{.token = "time", .handler = expand_time_as_seconds},
397+
{.token = "date_time_with_ms", .handler = expand_time_as_date},
386398
{.token = "time_as_nanoseconds", .handler = expand_time_as_nanoseconds},
387399
{.token = "line_number", .handler = expand_line_number},
388400
};

src/time.c

+56-1
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

+40
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,46 @@ TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_nanoseconds_string) {
287287
EXPECT_STREQ("-0000000000000000100", buffer);
288288
}
289289

290+
// Tests the rcutils_time_point_value_as_date_string() function.
291+
TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_date_string) {
292+
rcutils_ret_t ret;
293+
rcutils_time_point_value_t timepoint;
294+
char buffer[256] = "";
295+
296+
// Typical use case.
297+
timepoint = 100;
298+
ret = rcutils_time_point_value_as_date_string(&timepoint, buffer, sizeof(buffer));
299+
EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str;
300+
std::tm t = {};
301+
std::istringstream ss(buffer);
302+
// To test that it works we call it once with the correct format string
303+
ss >> std::get_time(&t, "%Y-%m-%d %H:%M:%S");
304+
ASSERT_FALSE(ss.fail());
305+
std::istringstream ss2(buffer);
306+
// and once with the false one
307+
ss2 >> std::get_time(&t, "%Y-%b-%d %H:%M:%S");
308+
ASSERT_TRUE(ss2.fail());
309+
310+
// nullptr for timepoint
311+
ret = rcutils_time_point_value_as_date_string(nullptr, buffer, sizeof(buffer));
312+
EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret);
313+
rcutils_reset_error();
314+
315+
// nullptr for string
316+
timepoint = 100;
317+
ret = rcutils_time_point_value_as_date_string(&timepoint, nullptr, 0);
318+
EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret);
319+
rcutils_reset_error();
320+
321+
const char * test_str = "should not be touched";
322+
timepoint = 100;
323+
// buffer is of size 256, so it will fit
324+
(void)memmove(buffer, test_str, strlen(test_str) + 1);
325+
ret = rcutils_time_point_value_as_date_string(&timepoint, buffer, 0);
326+
EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str;
327+
EXPECT_STREQ(test_str, buffer);
328+
}
329+
290330
// Tests the rcutils_time_point_value_as_seconds_string() function.
291331
TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_seconds_string) {
292332
rcutils_ret_t ret;

0 commit comments

Comments
 (0)