aboutsummaryrefslogtreecommitdiff
path: root/src/zentelemetry/otlpencoder.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2025-10-22 17:57:29 +0200
committerGitHub Enterprise <[email protected]>2025-10-22 17:57:29 +0200
commit5c139e2d8a260544bc5e730de0440edbab4b0f03 (patch)
treeb477208925fe3b373d4833460b90d61a8051cf05 /src/zentelemetry/otlpencoder.cpp
parent5.7.7-pre3 (diff)
downloadzen-5c139e2d8a260544bc5e730de0440edbab4b0f03.tar.xz
zen-5c139e2d8a260544bc5e730de0440edbab4b0f03.zip
add support for OTLP logging/tracing (#599)
- adds `zentelemetry` project which houses new functionality for serializing logs and traces in OpenTelemetry Protocol format (OTLP) - moved existing stats functionality from `zencore` to `zentelemetry` - adds `TRefCounted<T>` for vtable-less refcounting - adds `MemoryArena` class which allows for linear allocation of memory from chunks - adds `protozero` which is used to encode OTLP protobuf messages
Diffstat (limited to 'src/zentelemetry/otlpencoder.cpp')
-rw-r--r--src/zentelemetry/otlpencoder.cpp478
1 files changed, 478 insertions, 0 deletions
diff --git a/src/zentelemetry/otlpencoder.cpp b/src/zentelemetry/otlpencoder.cpp
new file mode 100644
index 000000000..677545066
--- /dev/null
+++ b/src/zentelemetry/otlpencoder.cpp
@@ -0,0 +1,478 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zentelemetry/otlpencoder.h"
+
+#include <zenbase/zenbase.h>
+#include <zentelemetry/otlptrace.h>
+
+#include <spdlog/sinks/sink.h>
+#include <zencore/testing.h>
+
+#include <protozero/buffer_string.hpp>
+#include <protozero/pbf_builder.hpp>
+
+#include "otellogprotozero.h"
+#include "otelmetricsprotozero.h"
+#include "otelprotozero.h"
+#include "oteltraceprotozero.h"
+
+#if ZEN_WITH_OTEL
+
+namespace zen {
+
+OtlpEncoder::OtlpEncoder()
+{
+}
+
+OtlpEncoder::~OtlpEncoder()
+{
+}
+
+static int
+MapSeverity(const spdlog::level::level_enum Level)
+{
+ switch (Level)
+ {
+ case spdlog::level::critical:
+ return otel::SEVERITY_NUMBER_FATAL;
+ case spdlog::level::err:
+ return otel::SEVERITY_NUMBER_ERROR;
+ case spdlog::level::warn:
+ return otel::SEVERITY_NUMBER_WARN;
+ case spdlog::level::info:
+ return otel::SEVERITY_NUMBER_INFO;
+ case spdlog::level::debug:
+ return otel::SEVERITY_NUMBER_DEBUG;
+ default:
+ case spdlog::level::trace:
+ return otel::SEVERITY_NUMBER_TRACE;
+ }
+}
+
+static const char*
+MapSeverityText(const spdlog::level::level_enum Level)
+{
+ switch (Level)
+ {
+ case spdlog::level::critical:
+ return "fatal";
+ case spdlog::level::err:
+ return "error";
+ case spdlog::level::warn:
+ return "warn";
+ case spdlog::level::info:
+ return "info";
+ case spdlog::level::debug:
+ return "debug";
+ default:
+ case spdlog::level::trace:
+ return "trace";
+ }
+}
+
+std::string
+OtlpEncoder::FormatOtelProtobuf(const spdlog::details::log_msg& Msg) const
+{
+ std::string Data;
+
+ // LogsData
+ {
+ protozero::pbf_builder<otel::LogsData> Builder{Data};
+
+ // ResourceLogs
+ {
+ protozero::pbf_builder<otel::ResourceLogs> RlBuilder{Builder, otel::LogsData::required_repeated_ResourceLogs_resource_logs};
+
+ // ResourceLogs / Resource
+ {
+ protozero::pbf_builder<otel::Resource> Res{RlBuilder, otel::ResourceLogs::optional_Resource_resource};
+
+ AppendResourceAttributes(Res);
+ }
+
+ // ScopeLogs scope_logs
+ {
+ protozero::pbf_builder<otel::ScopeLogs> SlBuilder{RlBuilder, otel::ResourceLogs::required_repeated_ScopeLogs_scope_logs};
+
+ {
+ protozero::pbf_builder<otel::InstrumentationScope> IsBuilder{SlBuilder,
+ otel::ScopeLogs::required_InstrumentationScope_scope};
+
+ IsBuilder.add_string(otel::InstrumentationScope::string_name, Msg.logger_name.data(), Msg.logger_name.size());
+ }
+
+ // LogRecord log_records
+ {
+ protozero::pbf_builder<otel::LogRecord> LrBuilder{SlBuilder, otel::ScopeLogs::required_repeated_LogRecord_log_records};
+
+ LrBuilder.add_fixed64(otel::LogRecord::required_fixed64_time_unix_nano,
+ std::chrono::duration_cast<std::chrono::nanoseconds>(Msg.time.time_since_epoch()).count());
+
+ const int Severity = MapSeverity(Msg.level);
+
+ LrBuilder.add_enum(otel::LogRecord::optional_SeverityNumber_severity_number, Severity);
+
+ LrBuilder.add_string(otel::LogRecord::optional_string_severity_text, MapSeverityText(Msg.level));
+
+ otel::TraceId TraceId;
+ const otel::SpanId SpanId = otel::Span::GetCurrentSpanId(TraceId);
+
+ if (SpanId && TraceId)
+ {
+ LrBuilder.add_bytes(otel::LogRecord::optional_bytes_trace_id, TraceId.GetData(), TraceId.kSize);
+ LrBuilder.add_bytes(otel::LogRecord::optional_bytes_span_id, SpanId.GetData(), SpanId.kSize);
+ }
+
+ // body
+ {
+ protozero::pbf_builder<otel::AnyValue> BodyBuilder{LrBuilder, otel::LogRecord::optional_anyvalue_body};
+
+ BodyBuilder.add_string(otel::AnyValue::string_string_value, Msg.payload.data(), Msg.payload.size());
+ }
+
+ // attributes
+
+ {
+ protozero::pbf_builder<otel::KeyValue> KvBuilder{LrBuilder, otel::LogRecord::optional_repeated_kv_attributes};
+ KvBuilder.add_string(otel::KeyValue::string_key, "thread_id");
+
+ {
+ protozero::pbf_builder<otel::AnyValue> AvBuilder{KvBuilder, otel::KeyValue::AnyValue_value};
+
+ AvBuilder.add_int64(otel::AnyValue::int64_int_value, Msg.thread_id);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return Data;
+}
+
+std::string
+OtlpEncoder::FormatOtelMetrics() const
+{
+ std::string Data;
+
+# if 0
+ static int64_t LastNanos = 0;
+
+ const int64_t NowNanos =
+ std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
+
+ // MetricsData
+ {
+ protozero::pbf_builder<otel::MetricsData> Builder{Data};
+
+ // ResourceMetrics
+ protozero::pbf_builder<otel::ResourceMetrics> Rm{Builder, otel::MetricsData::repeated_ResourceMetrics_resource_metrics};
+
+ {
+ protozero::pbf_builder<otel::Resource> Res{Rm, otel::ResourceMetrics::Resource_resource};
+
+ AppendResourceAttributes(Res);
+ }
+
+ // ScopeMetrics
+ protozero::pbf_builder<otel::ScopeMetrics> Sm{Rm, otel::ResourceMetrics::repeated_ScopeMetrics_scope_metrics};
+
+ {
+ // InstrumentationScope
+ protozero::pbf_builder<otel::InstrumentationScope> Is{Sm, otel::ScopeMetrics::InstrumentationScope_scope};
+ Is.add_string(otel::InstrumentationScope::string_name, "scope_name");
+ }
+
+ {
+ protozero::pbf_builder<otel::Metric> Metric{Sm, otel::ScopeMetrics::repeated_Metric_metrics};
+ Metric.add_string(otel::Metric::string_name, "metric_name");
+ Metric.add_string(otel::Metric::string_unit, "KiB");
+
+ // Gauge
+ {
+ protozero::pbf_builder<otel::Gauge> Gauge{Metric, otel::Metric::oneof_data_Gauge_gauge};
+
+ protozero::pbf_builder<otel::NumberDataPoint> Dp{Gauge, otel::Gauge::repeated_NumberDataPoint_data_points};
+ Dp.add_fixed64(otel::NumberDataPoint::fixed64_time_unix_nano, NowNanos);
+ Dp.add_fixed64(otel::NumberDataPoint::fixed64_start_time_unix_nano, LastNanos);
+ Dp.add_sfixed64(otel::NumberDataPoint::oneof_value_sfixed64_as_int, rand() * 470 / RAND_MAX);
+ }
+ }
+
+ const int RequestCount = rand() % 600;
+
+ {
+ protozero::pbf_builder<otel::Metric> Metric{Sm, otel::ScopeMetrics::repeated_Metric_metrics};
+ Metric.add_string(otel::Metric::string_name, "request_count");
+ Metric.add_string(otel::Metric::string_unit, "requests");
+
+ static int SumValue = 0;
+ SumValue += RequestCount;
+
+ // Sum
+ {
+ protozero::pbf_builder<otel::Sum> Sum{Metric, otel::Metric::oneof_data_Sum_sum};
+ Sum.add_enum(otel::Sum::AggregationTemporality_aggregation_temporality, otel::AGGREGATION_TEMPORALITY_CUMULATIVE);
+ Sum.add_bool(otel::Sum::bool_is_monotonic, true);
+
+ protozero::pbf_builder<otel::NumberDataPoint> Dp{Sum, otel::Sum::repeated_NumberDataPoint_data_points};
+ Dp.add_fixed64(otel::NumberDataPoint::fixed64_time_unix_nano, NowNanos);
+ Dp.add_fixed64(otel::NumberDataPoint::fixed64_start_time_unix_nano, LastNanos);
+ Dp.add_double(otel::NumberDataPoint::oneof_value_double_as_double, SumValue);
+ }
+ }
+
+ {
+ protozero::pbf_builder<otel::Metric> Metric{Sm, otel::ScopeMetrics::repeated_Metric_metrics};
+ Metric.add_string(otel::Metric::string_name, "request_latency");
+ Metric.add_string(otel::Metric::string_unit, "ms");
+
+ // Histogram
+ {
+ protozero::pbf_builder<otel::Histogram> Histogram{Metric, otel::Metric::oneof_data_Histogram_histogram};
+ Histogram.add_enum(otel::Histogram::AggregationTemporality_aggregation_temporality,
+ otel::AGGREGATION_TEMPORALITY_CUMULATIVE);
+
+ protozero::pbf_builder<otel::HistogramDataPoint> Dp{Histogram, otel::Histogram::repeated_HistogramDataPoint_data_points};
+ Dp.add_fixed64(otel::HistogramDataPoint::fixed64_time_unix_nano, NowNanos);
+ Dp.add_fixed64(otel::HistogramDataPoint::fixed64_start_time_unix_nano, LastNanos);
+
+ // Simulated latency value
+
+ double Sum = 0, Min = 0, Max = 0;
+ int Buckets[6] = {0};
+
+ for (int i = 0; i < RequestCount; ++i)
+ {
+ const int Latency = rand() % 250;
+
+ if (i == 0 || Latency < Min)
+ {
+ Min = Latency;
+ }
+ if (i == 0 || Latency > Max)
+ {
+ Max = Latency;
+ }
+
+ Sum += Latency;
+
+ if (Latency >= 0 && Latency < 10)
+ {
+ ++Buckets[0]; // [0,10)
+ }
+ else if (Latency >= 10 && Latency < 25)
+ {
+ ++Buckets[1]; // [10,25)
+ }
+ else if (Latency >= 25 && Latency < 50)
+ {
+ ++Buckets[2]; // [25,50)
+ }
+ else if (Latency >= 50 && Latency < 100)
+ {
+ ++Buckets[3]; // [50,100)
+ }
+ else if (Latency >= 100 && Latency < 250)
+ {
+ ++Buckets[4]; // [100,250)
+ }
+ else
+ {
+ ++Buckets[5]; // [250,+inf)
+ }
+ }
+
+ Dp.add_fixed64(otel::HistogramDataPoint::fixed64_count, RequestCount);
+ Dp.add_double(otel::HistogramDataPoint::optional_double_sum, Sum);
+ Dp.add_double(otel::HistogramDataPoint::optional_double_min, Min);
+ Dp.add_double(otel::HistogramDataPoint::optional_double_max, Max);
+
+ // Bucket bounds
+ Dp.add_double(otel::HistogramDataPoint::repeated_double_explicit_bounds, 10.0);
+ Dp.add_double(otel::HistogramDataPoint::repeated_double_explicit_bounds, 25.0);
+ Dp.add_double(otel::HistogramDataPoint::repeated_double_explicit_bounds, 50.0);
+ Dp.add_double(otel::HistogramDataPoint::repeated_double_explicit_bounds, 100.0);
+ Dp.add_double(otel::HistogramDataPoint::repeated_double_explicit_bounds, 250.0);
+
+ // Buckets
+ for (int i = 0; i < 6; ++i)
+ {
+ Dp.add_fixed64(otel::HistogramDataPoint::repeated_fixed64_bucket_counts, Buckets[i]);
+ }
+ }
+ }
+
+# if 0
+ {
+ protozero::pbf_builder<otel::Metric> Metric{Sm, otel::ScopeMetrics::repeated_Metric_metrics};
+ Metric.add_string(otel::Metric::string_name, "request_payload");
+ Metric.add_string(otel::Metric::string_unit, "KiB");
+
+ // ExponentialHistogram
+ {
+ protozero::pbf_builder<otel::ExponentialHistogram> ExponentialHistogram{
+ Metric, otel::Metric::oneof_data_ExponentialHistogram_exponential_histogram};
+ ExponentialHistogram.add_enum(otel::ExponentialHistogram::AggregationTemporality_aggregation_temporality,
+ otel::AGGREGATION_TEMPORALITY_CUMULATIVE);
+
+ protozero::pbf_builder<otel::ExponentialHistogramDataPoint> Dp{
+ ExponentialHistogram, otel::ExponentialHistogram::repeated_ExponentialHistogramDataPoint_data_points};
+ Dp.add_fixed64(otel::ExponentialHistogramDataPoint::fixed64_time_unix_nano, NowNanos);
+ Dp.add_fixed64(otel::ExponentialHistogramDataPoint::fixed64_start_time_unix_nano, LastNanos);
+
+ // Simulated payload size values
+
+ int64_t sum = 0;
+ int count = 0;
+
+ for (int i = 0; i < RequestCount; ++i)
+ {
+ const int PayloadSize = rand() % 1'500'000; // [0,1500000) bytes
+
+ sum += PayloadSize;
+ ++count;
+ }
+
+ Dp.add_fixed64(otel::ExponentialHistogramDataPoint::fixed64_count, count);
+ Dp.add_double(otel::ExponentialHistogramDataPoint::optional_double_sum, sum);
+ Dp.add_sint32(otel::ExponentialHistogramDataPoint::sint32_scale, 4);
+ }
+ }
+# endif
+ }
+ LastNanos = NowNanos;
+# endif
+ return Data;
+}
+
+void
+OtlpEncoder::AddResourceAttribute(const std::string_view& Key, const std::string_view& Value)
+{
+ m_ResourceLock.WithExclusiveLock([&] { m_ResourceAttributes[std::string(Key)] = std::string(Value); });
+}
+
+void
+OtlpEncoder::AddResourceAttribute(const std::string_view& Key, int64_t Value)
+{
+ m_ResourceLock.WithExclusiveLock([&] { m_ResourceIntAttributes[std::string(Key)] = Value; });
+}
+
+void
+OtlpEncoder::AppendResourceAttributes(protozero::pbf_builder<otel::Resource>& Res) const
+{
+ using namespace otel;
+
+ m_ResourceLock.WithSharedLock([&] {
+ for (auto const& [K, V] : m_ResourceAttributes)
+ {
+ protozero::pbf_builder<otel::KeyValue> KvBuilder{Res, otel::Resource::repeated_KeyValue_attributes};
+ KvBuilder.add_string(otel::KeyValue::string_key, K.c_str());
+ protozero::pbf_builder<otel::AnyValue> AnyBuilder{KvBuilder, otel::KeyValue::AnyValue_value};
+ AnyBuilder.add_string(otel::AnyValue::string_string_value, V.c_str());
+ }
+
+ for (auto const& [K, V] : m_ResourceIntAttributes)
+ {
+ protozero::pbf_builder<otel::KeyValue> KvBuilder{Res, otel::Resource::repeated_KeyValue_attributes};
+ KvBuilder.add_string(otel::KeyValue::string_key, K.c_str());
+ protozero::pbf_builder<otel::AnyValue> AnyBuilder{KvBuilder, otel::KeyValue::AnyValue_value};
+ AnyBuilder.add_int64(otel::AnyValue::int64_int_value, V);
+ }
+ });
+}
+
+template<typename ParentBuilder>
+static void
+AppendAttributesToBuilder(ParentBuilder& Parent, auto KeyValueField, const otel::AttributePair* Attrs)
+{
+ for (const otel::AttributePair* Attr = Attrs; Attr != nullptr; Attr = Attr->Next)
+ {
+ protozero::pbf_builder<otel::KeyValue> KvBuilder{Parent, KeyValueField};
+ KvBuilder.add_string(otel::KeyValue::string_key, Attr->Key);
+
+ protozero::pbf_builder<otel::AnyValue> AnyBuilder{KvBuilder, otel::KeyValue::AnyValue_value};
+
+ if (Attr->IsNumeric())
+ {
+ AnyBuilder.add_int64(otel::AnyValue::int64_int_value, Attr->GetNumericValue());
+ }
+ else if (Attr->IsString())
+ {
+ AnyBuilder.add_string(otel::AnyValue::string_string_value, Attr->GetStringValue());
+ }
+ }
+}
+
+std::string
+OtlpEncoder::FormatOtelTrace(zen::otel::TraceId Id, std::span<const zen::otel::Span*> Spans) const
+{
+ std::string Data;
+
+ using namespace otel;
+
+ // TracesData
+ {
+ protozero::pbf_builder<pbf::TracesData> Builder{Data};
+
+ {
+ protozero::pbf_builder<pbf::ResourceSpans> Rs{Builder, pbf::TracesData::repeated_ResourceSpans_resource_spans};
+
+ {
+ protozero::pbf_builder<otel::Resource> Res{Rs, pbf::ResourceSpans::Resource_resource};
+
+ AppendResourceAttributes(Res);
+ }
+
+ for (const zen::otel::Span* SpanPtr : Spans)
+ {
+ protozero::pbf_builder<pbf::ScopeSpans> Ss{Rs, pbf::ResourceSpans::repeated_ScopeSpans_scope_spans};
+
+ {
+ // InstrumentationScope
+ protozero::pbf_builder<otel::InstrumentationScope> Is{Ss, pbf::ScopeSpans::InstrumentationScope_scope};
+ Is.add_string(otel::InstrumentationScope::string_name, "scope_name");
+ }
+
+ const SpanId ThisSpanId = SpanPtr->GetSpanId();
+
+ {
+ protozero::pbf_builder<pbf::Span> Sb{Ss, pbf::ScopeSpans::repeated_Span_spans};
+
+ Sb.add_bytes(pbf::Span::required_bytes_trace_id, Id.GetData(), TraceId::kSize);
+ Sb.add_bytes(pbf::Span::required_bytes_span_id, ThisSpanId.GetData(), SpanId::kSize);
+ // Sb.add_string(pbf::Span::string_trace_state, "state-value");
+ //
+ if (const otel::Span* ParentSpan = SpanPtr->GetParentSpan())
+ {
+ const SpanId ParentSpanId = ParentSpan->GetSpanId();
+ Sb.add_bytes(pbf::Span::bytes_parent_span_id, ParentSpanId.GetData(), SpanId::kSize);
+ }
+
+ Sb.add_fixed32(pbf::Span::fixed32_flags, 0);
+ Sb.add_string(pbf::Span::required_string_name, SpanPtr->GetName());
+ Sb.add_enum(pbf::Span::SpanKind_kind, (int)SpanPtr->GetKind());
+ Sb.add_fixed64(pbf::Span::required_fixed64_start_time_unix_nano, SpanPtr->GetStartTime());
+ Sb.add_fixed64(pbf::Span::required_fixed64_end_time_unix_nano, SpanPtr->GetEndTime());
+
+ AppendAttributesToBuilder(Sb, pbf::Span::repeated_KeyValue_attributes, SpanPtr->GetAttributes());
+
+ for (const otel::Event* Event = SpanPtr->GetEvents(); Event != nullptr; Event = Event->NextEvent)
+ {
+ protozero::pbf_builder<pbf::Span_Event> EventBuilder{Sb, pbf::Span::repeated_Event_events};
+ EventBuilder.add_fixed64(pbf::Span_Event::fixed64_time_unix_nano, Event->Timestamp);
+ EventBuilder.add_string(pbf::Span_Event::string_name, Event->Name);
+
+ AppendAttributesToBuilder(EventBuilder, pbf::Span_Event::repeated_KeyValue_attributes, Event->Attributes);
+ }
+ }
+ }
+ }
+ }
+
+ return Data;
+}
+
+} // namespace zen
+
+#endif