Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion docs/OtlpHttpLogger.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,49 @@ LogIt++ -> OtlpHttpLogger -> kurlyk::HttpClient -> OpenTelemetry Collector / Lok
| `file`, `line`, `function` | `code.file.path`, `code.line.number`, `code.function.name` |
| `thread_id` | `thread.id` |
| `format` | `logit.format` |
| `arg_names` | `logit.arg_names` |
| `arg_names` | `logit.arg_names` (legacy, deprecated) |
| `args_array` elements | typed attributes under `logit.arg.*` prefix |

## Structured typed attributes

When `include_args = true` (the default), each element of `args_array` is emitted as a separate OTLP attribute with a typed value. The key is built from `args_prefix` + the sanitized argument name.

### Configuration flags

| Flag | Default | Description |
| --- | --- | --- |
| `include_args` | `true` | Emit structured typed arg attributes. |
| `include_arg_names` | `false` | Emit legacy `logit.arg_names` string attribute. |
| `args_prefix` | `"logit.arg."` | Key prefix for structured arg attributes. |

### Type mapping

| VariableValue type | OTLP AnyValue field |
| --- | --- |
| `BOOL_VAL` | `boolValue` |
| `INT8_VAL`..`INT64_VAL` | `intValue` |
| `UINT8_VAL`..`UINT32_VAL` | `intValue` |
| `UINT64_VAL` | `intValue` if <= INT64_MAX, else `stringValue` |
| `FLOAT_VAL`, `DOUBLE_VAL`, `LONG_DOUBLE_VAL` | `doubleValue` if finite, else `stringValue` |
| All other types | `stringValue` (via `to_string()`) |

### Name sanitization and deduplication

Argument names are sanitized: characters that are not alphanumeric, `_`, `.`, or `-` are replaced with `_`. If the sanitized name is empty or consists only of underscores, a positional key (`args_prefix` + index) is used instead.

Duplicate keys are resolved by appending `.N` (starting at 1 for the second occurrence). For example, two args both named `px` produce keys `logit.arg.px` and `logit.arg.px.1`.

### Reserved prefix

The default prefix `logit.arg.` is reserved. Changing `args_prefix` is supported but may cause attribute collisions with other OTLP receivers.

### Cardinality warning

Avoid putting unique values (timestamps, request IDs, UUIDs) into arg attributes. High-cardinality attributes increase memory and storage costs in OTLP collectors and backends (Loki, Tempo, etc.). Use structured attributes for low-cardinality dimensions like `symbol`, `side`, or `region`.

### Deprecation notice

The `logit.arg_names` OTLP attribute (controlled by `include_arg_names`) is deprecated. It emits argument names as a single comma-separated string with no type information. Prefer `include_args = true` for typed, queryable attributes.

Resource attributes are configured through `OtlpHttpLoggerConfig`, including `service.name`, `service.namespace`, `service.instance.id`, and `deployment.environment.name`.

Expand Down
2 changes: 1 addition & 1 deletion include/logit_cpp/logit/loggers/WindowsDebugLogger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ namespace logit {
/// \details This call is blocking. If asynchronous logging is currently enabled,
/// it waits until already accepted tasks are drained before changing async mode
/// or executor ownership. Only async, use_dedicated_executor, queue_capacity,
/// and queue_policy are applied. File/rotation/compression fields are ignored.
/// and queue_policy are applied. Other fields in Config are ignored.
/// For hot queue tuning without switching executor mode, use set_queue_config().
void set_config(const Config& config) {
std::unique_ptr<detail::SingleThreadExecutor> old_executor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ namespace logit {
bool include_source = true; ///< Export source file, line, and function attributes.
bool include_thread_id = true; ///< Export thread id attribute.
bool include_format = true; ///< Export original format string as `logit.format`.
bool include_arg_names = true; ///< Export original argument names as `logit.arg_names`.
bool include_arg_names = false; ///< Export original argument names as `logit.arg_names` (legacy).
bool include_args = true; ///< Export structured typed arg attributes.
std::string args_prefix = "logit.arg."; ///< Key prefix for structured arg attributes.
};

} // namespace logit
Expand Down
123 changes: 123 additions & 0 deletions include/logit_cpp/logit/loggers/otlp/OtlpJsonSerializer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@

#include "OtlpHttpLoggerConfig.hpp"
#include "OtlpRecordSnapshot.hpp"
#include <cctype>
#include <cstdint>
#include <cmath>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>

namespace logit {
Expand Down Expand Up @@ -107,6 +112,48 @@ namespace logit {
<< "\",\"value\":{\"boolValue\":" << (value ? "true" : "false") << "}}";
}

/// \brief Writes a double OTLP attribute.
/// \param os Output stream.
/// \param key Attribute key.
/// \param value Attribute double value.
inline void otlp_write_double_attr(std::ostringstream& os, const std::string& key, double value) {
if (std::isfinite(value)) {
os << "{\"key\":\"" << otlp_json_escape(key)
<< "\",\"value\":{\"doubleValue\":"
<< std::setprecision(std::numeric_limits<double>::max_digits10)
<< value << "}}";
} else {
os << "{\"key\":\"" << otlp_json_escape(key)
<< "\",\"value\":{\"stringValue\":\"" << otlp_json_escape(std::to_string(value)) << "\"}}";
}
}

/// \brief Writes a uint64 OTLP attribute.
/// \param os Output stream.
/// \param key Attribute key.
/// \param value Attribute uint64 value.
inline void otlp_write_uint_attr(std::ostringstream& os, const std::string& key, uint64_t value) {
if (value <= static_cast<uint64_t>((std::numeric_limits<int64_t>::max)())) {
otlp_write_int_attr(os, key, static_cast<int64_t>(value));
} else {
os << "{\"key\":\"" << otlp_json_escape(key)
<< "\",\"value\":{\"stringValue\":\"" << value << "\"}}";
}
}

/// \brief Sanitizes an attribute key for OTLP.
/// \param name Raw key name.
/// \return Sanitized key with invalid chars replaced by underscore.
inline std::string sanitize_otlp_key(const std::string& name) {
std::string result = name;
for (char& c : result) {
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' && c != '.' && c != '-') {
c = '_';
}
}
return result;
}

/// \brief Writes one OTLP JSON log record.
/// \param os Output stream.
/// \param item Log item to serialize.
Expand Down Expand Up @@ -153,6 +200,82 @@ namespace logit {
otlp_write_string_attr(os, "logit.arg_names", r.arg_names);
}

if (config.include_args && !r.args_array.empty()) {
std::unordered_map<std::string, std::size_t> key_count;

for (std::size_t i = 0; i < r.args_array.size(); ++i) {
const VariableValue& arg = r.args_array[i];

std::string sanitized = sanitize_otlp_key(arg.name);

bool all_underscore = true;
for (std::size_t j = 0; j < sanitized.size(); ++j) {
if (sanitized[j] != '_') {
all_underscore = false;
break;
}
}

std::string key;
if (sanitized.empty() || all_underscore) {
key = config.args_prefix + std::to_string(i);
} else {
key = config.args_prefix + sanitized;
}

auto it = key_count.find(key);
if (it != key_count.end()) {
++(it->second);
key += "." + std::to_string(it->second);
}
key_count[key] = 0;

otlp_write_comma_if_needed(os, first);

switch (arg.type) {
case VariableValue::ValueType::BOOL_VAL:
otlp_write_bool_attr(os, key, arg.pod_value.bool_value);
break;
case VariableValue::ValueType::INT8_VAL:
otlp_write_int_attr(os, key, static_cast<int64_t>(arg.pod_value.int8_value));
break;
case VariableValue::ValueType::INT16_VAL:
otlp_write_int_attr(os, key, static_cast<int64_t>(arg.pod_value.int16_value));
break;
case VariableValue::ValueType::INT32_VAL:
otlp_write_int_attr(os, key, static_cast<int64_t>(arg.pod_value.int32_value));
break;
case VariableValue::ValueType::INT64_VAL:
otlp_write_int_attr(os, key, arg.pod_value.int64_value);
break;
case VariableValue::ValueType::UINT8_VAL:
otlp_write_uint_attr(os, key, static_cast<uint64_t>(arg.pod_value.uint8_value));
break;
case VariableValue::ValueType::UINT16_VAL:
otlp_write_uint_attr(os, key, static_cast<uint64_t>(arg.pod_value.uint16_value));
break;
case VariableValue::ValueType::UINT32_VAL:
otlp_write_uint_attr(os, key, static_cast<uint64_t>(arg.pod_value.uint32_value));
break;
case VariableValue::ValueType::UINT64_VAL:
otlp_write_uint_attr(os, key, arg.pod_value.uint64_value);
break;
case VariableValue::ValueType::FLOAT_VAL:
otlp_write_double_attr(os, key, static_cast<double>(arg.pod_value.float_value));
break;
case VariableValue::ValueType::DOUBLE_VAL:
otlp_write_double_attr(os, key, arg.pod_value.double_value);
break;
case VariableValue::ValueType::LONG_DOUBLE_VAL:
otlp_write_double_attr(os, key, static_cast<double>(arg.pod_value.long_double_value));
break;
default:
otlp_write_string_attr(os, key, arg.to_string());
break;
}
}
}

otlp_write_comma_if_needed(os, first);
otlp_write_int_attr(os, "logit.logger_index", r.logger_index);

Expand Down
3 changes: 3 additions & 0 deletions include/logit_cpp/logit/loggers/otlp/OtlpRecordSnapshot.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <sstream>
#include <string>
#include <thread>
#include <vector>

namespace logit {

Expand All @@ -22,6 +23,7 @@ namespace logit {
std::string function; ///< Source function name.
std::string format; ///< Original message or format string.
std::string arg_names; ///< Original argument names.
std::vector<VariableValue> args_array; ///< Structured argument values.
std::string thread_id; ///< Stringified std::thread::id.
int logger_index = -1; ///< Target logger index, or -1 for all.
bool print_mode = false; ///< Raw argument print mode flag.
Expand Down Expand Up @@ -50,6 +52,7 @@ namespace logit {
out.function = record.function;
out.format = record.format;
out.arg_names = record.arg_names;
out.args_array = record.args_array;
out.thread_id = otlp_thread_id_to_string(record.thread_id);
out.logger_index = record.logger_index;
out.print_mode = record.print_mode;
Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ else()
os_error_macros_test.cpp
otlp_http_logger_integration_test.cpp
otlp_json_serializer_test.cpp
otlp_structured_attributes_test.cpp
per_logger_isolation_test.cpp
per_logger_mixed_mode_test.cpp
printf_format_macros_test.cpp
Expand Down Expand Up @@ -78,6 +79,7 @@ else()
endif()
if(NOT LOGIT_WITH_OTLP)
list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_integration_test.cpp)
list(REMOVE_ITEM TEST_SOURCES otlp_structured_attributes_test.cpp)
endif()
foreach(test_src ${TEST_SOURCES})
get_filename_component(test_name ${test_src} NAME_WE)
Expand Down
1 change: 1 addition & 0 deletions tests/otlp_json_serializer_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

int main() {
logit::OtlpHttpLoggerConfig config;
config.include_arg_names = true;
config.service_name = "trade-bot";
config.service_namespace = "tests";
config.service_instance_id = "instance-1";
Expand Down
Loading
Loading