Skip to content

Commit 38c6275

Browse files
committed
feat: add support for additional std::chrono types and streamable types in structured logging
- Add template specializations for std::chrono::day, month, year, year_month_day - Add support for std::error_code, std::bitset<N>, std::complex<T>, std::chrono::duration - Update test registration function name to register_structured_log_stream_tests - Change check_nothrow to require_nothrow in tests - Fix HTTP server test to use HTTP_CONN_CLOSED message ID
1 parent 28c8196 commit 38c6275

File tree

3 files changed

+240
-44
lines changed

3 files changed

+240
-44
lines changed

net/net-http_server.test.c++

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ auto register_server_tests()
165165
check_contains(output, "\"user_agent\":\"TestAgent/1.0\"");
166166
check_contains(output, "\"content_type\":\"application/json\"");
167167
check_contains(output, "\"accept\":\"text/plain, application/json\"");
168-
check_contains(output, "\"msg_id\":\"CONN_CLOSED\"");
168+
check_contains(output, "\"msg_id\":\"HTTP_CONN_CLOSED\"");
169169
check_contains(output, "\"connection_duration_ms\"");
170170

171171
// Verify request_id correlation

net/net-structured_log_stream.c++m

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,20 @@ concept string_convertible = !exact_value_type<T> &&
121121
!floating_convertible<T> &&
122122
std::convertible_to<T, std::string>;
123123

124+
// Generic concept for types with operator<< - must be last fallback
125+
// Note: operator<< returns std::basic_ostream<CharT, Traits>& (not stringstream&),
126+
// so we check that it returns something convertible to std::ostream&
127+
template<typename T>
128+
concept streamable = !exact_value_type<T> &&
129+
!c_string_type<T> &&
130+
!string_view_convertible<T> &&
131+
!integral_convertible<T> &&
132+
!floating_convertible<T> &&
133+
!string_convertible<T> &&
134+
requires(std::stringstream& ss, const T& v) {
135+
{ ss << v } -> std::convertible_to<std::ostream&>;
136+
};
137+
124138
struct level_manip {
125139
syslog::severity sev;
126140
std::string_view msg_id;
@@ -246,14 +260,23 @@ private:
246260
return std::forward<T>(v);
247261
}
248262

263+
// C-strings (const char*, char*) are handled separately even though they're convertible
264+
// to std::string, because:
265+
// 1. Template overload resolution: more specific (c_string_type) is checked first
266+
// 2. Concept dependencies: c_string_type is used to exclude C-strings from other
267+
// concepts (string_view_convertible, integral_convertible, etc.)
268+
// 3. Clear intent: explicit handling for C-strings vs. other string-like types
269+
// Both do the same conversion (std::string{v}), but the separation ensures proper
270+
// ordering and prevents C-strings from matching string_view_convertible or other
271+
// concepts in the chain
249272
template<c_string_type T>
250273
static value convert_to_value(T v) {
251-
return std::string(v);
274+
return std::string{v};
252275
}
253276

254277
template<string_view_convertible T>
255278
static value convert_to_value(T&& v) {
256-
return std::string(v);
279+
return std::string{v};
257280
}
258281

259282
template<integral_convertible T>
@@ -266,25 +289,29 @@ private:
266289
return static_cast<double>(v);
267290
}
268291

269-
// Special handling for std::chrono::hh_mm_ss - convert to string with HH:MM:SS format
270-
// Must come before string_convertible to avoid conflicts
271-
template<typename Duration>
272-
static value convert_to_value(const std::chrono::hh_mm_ss<Duration>& time) {
273-
auto ss = std::stringstream{};
274-
ss << std::setw(2) << std::setfill('0') << time.hours() << ':'
275-
<< std::setw(2) << std::setfill('0') << time.minutes() << ':'
276-
<< std::setw(2) << std::setfill('0') << time.seconds();
277-
return ss.str();
278-
}
279-
280-
// Special handling for std::error_code - convert to string with value and message
292+
// Special handling for std::error_code - provides more informative format than standard operator<<
293+
// Standard operator<< outputs: "generic:17" (category:value)
294+
// Our format outputs: "17 (File exists)" (value + human-readable message)
295+
// This is more useful for logging because it includes the error message
281296
static value convert_to_value(const std::error_code& ec) {
282297
return std::to_string(ec.value()) + " (" + ec.message() + ")";
283298
}
284299

300+
// Handles types convertible to std::string (but NOT C-strings, which are handled above)
301+
// This catches other string-like types that aren't exact matches
285302
template<string_convertible T>
286303
static value convert_to_value(T&& v) {
287-
return std::string(v);
304+
return std::string{v};
305+
}
306+
307+
// Generic fallback: any type with operator<< - convert via stringstream
308+
// This handles chrono types (hh_mm_ss, day, month, year, year_month_day) and any other streamable types
309+
// ADL will find custom operator<< implementations (e.g., net::operator<< for hh_mm_ss)
310+
template<streamable T>
311+
static value convert_to_value(const T& v) {
312+
auto ss = std::stringstream{};
313+
ss << v;
314+
return ss.str();
288315
}
289316

290317
void output() {
@@ -457,17 +484,7 @@ public:
457484

458485
// Structured fields: {"key", value} or std::pair{"key", value}
459486
// Optimized: uses emplace to avoid double conversion
460-
template<class K, class V>
461-
structured_log_stream& operator<<(const std::pair<K, V>& kv) {
462-
if (entry_enabled) {
463-
structured_fields.emplace(std::piecewise_construct,
464-
std::forward_as_tuple(kv.first),
465-
std::forward_as_tuple(convert_to_value(kv.second)));
466-
}
467-
return *this;
468-
}
469-
470-
// Support for braced-init-list syntax: {"key", value}
487+
// Handles both lvalue and rvalue references via forwarding reference
471488
template<class K, class V>
472489
structured_log_stream& operator<<(std::pair<K, V>&& kv) {
473490
if (entry_enabled) {

0 commit comments

Comments
 (0)