]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
OpenTelemetry trace data protobuf encode/decode using protozero
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Mon, 28 Apr 2025 11:45:41 +0000 (13:45 +0200)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Thu, 12 Jun 2025 12:21:06 +0000 (14:21 +0200)
Signed-off-by: Otto Moerbeek <otto.moerbeek@open-xchange.com>
pdns/recursordist/decode_ot.py [new file with mode: 0755]
pdns/recursordist/encode_ot.py [new file with mode: 0755]
pdns/recursordist/meson.build
pdns/recursordist/protozero-trace.cc [new file with mode: 0644]
pdns/recursordist/protozero-trace.hh [new file with mode: 0644]
pdns/recursordist/test-protozero-trace.cc [new file with mode: 0644]
pdns/recursordist/trace-example.proto [new file with mode: 0644]

diff --git a/pdns/recursordist/decode_ot.py b/pdns/recursordist/decode_ot.py
new file mode 100755 (executable)
index 0000000..4970a10
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+
+# run protoc -I ~/opentelemetry-proto ~/opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto --python_out=.
+# run protoc -I ~/opentelemetry-proto ~/opentelemetry-proto/opentelemetry/proto/common/v1/common.proto --python_out=.
+# run protoc -I ~/opentelemetry-proto ~/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.proto --python_out=.
+# to generate opentelemetry directory
+
+import sys
+
+import google.protobuf.message
+import google.protobuf.json_format
+
+import opentelemetry.proto.trace.v1.trace_pb2
+
+data = sys.stdin.buffer.read()
+
+msg = opentelemetry.proto.trace.v1.trace_pb2.TracesData()
+msg.ParseFromString(data)
+
+json_string = google.protobuf.json_format.MessageToJson(msg)
+print(json_string)
+
diff --git a/pdns/recursordist/encode_ot.py b/pdns/recursordist/encode_ot.py
new file mode 100755 (executable)
index 0000000..fbdfc45
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+
+# run protoc -I ~/opentelemetry-proto ~/opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto --python_out=.
+# run protoc -I ~/opentelemetry-proto ~/opentelemetry-proto/opentelemetry/proto/common/v1/common.proto --python_out=.
+# run protoc -I ~/opentelemetry-proto ~/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.proto --python_out=.
+# to generate opentelemetry directory
+
+import sys
+
+import google.protobuf.message
+import google.protobuf.json_format
+
+import opentelemetry.proto.trace.v1.trace_pb2
+
+json = sys.stdin.buffer.read()
+
+msg = opentelemetry.proto.trace.v1.trace_pb2.TracesData()
+google.protobuf.json_format.Parse(json, msg);
+
+sys.stdout.buffer.write(msg.SerializeToString())
index efa8e5c603b8f31aa1d55d6d9c3893aec822cd61..b81584dd6182bb8500fcfd0138186c86db59b05b 100644 (file)
@@ -135,6 +135,7 @@ common_sources += files(
   src_dir / 'negcache.cc',
   src_dir / 'nsecrecords.cc',
   src_dir / 'protozero.cc',
+  src_dir / 'protozero-trace.cc',
   src_dir / 'proxy-protocol.cc',
   src_dir / 'pubsuffixloader.cc',
   src_dir / 'qtype.cc',
@@ -491,6 +492,7 @@ test_sources += files(
       src_dir / 'test-mtasker.cc',
       src_dir / 'test-negcache_cc.cc',
       src_dir / 'test-packetcache_hh.cc',
+      src_dir / 'test-protozero-trace.cc',
       src_dir / 'test-rcpgenerator_cc.cc',
       src_dir / 'test-rec-system-resolve.cc',
       src_dir / 'test-rec-taskqueue.cc',
@@ -534,6 +536,7 @@ if get_option('unit-tests')
           dep_boost_test,
           dep_lua,
           dep_nod,
+          dep_protozero,
           dep_recrust,
           dep_rust_recrust,
           librec_signers_openssl,
diff --git a/pdns/recursordist/protozero-trace.cc b/pdns/recursordist/protozero-trace.cc
new file mode 100644 (file)
index 0000000..054885e
--- /dev/null
@@ -0,0 +1,505 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "protozero-trace.hh"
+
+namespace pdns::trace
+{
+
+void AnyValue::encode(protozero::pbf_writer& writer) const
+{
+  if (std::holds_alternative<std::string>(*this)) {
+    pdns::trace::encode(writer, 1, std::get<std::string>(*this), true);
+  }
+  else if (std::holds_alternative<bool>(*this)) {
+    pdns::trace::encode(writer, 2, std::get<bool>(*this), true);
+  }
+  else if (std::holds_alternative<int64_t>(*this)) {
+    pdns::trace::encode(writer, 3, std::get<int64_t>(*this), true);
+  }
+  else if (std::holds_alternative<double>(*this)) {
+    pdns::trace::encode(writer, 4, std::get<double>(*this), true);
+  }
+  else if (std::holds_alternative<ArrayValue>(*this)) {
+    protozero::pbf_writer sub{writer, 5};
+    std::get<ArrayValue>(*this).encode(sub);
+  }
+  else if (std::holds_alternative<KeyValueList>(*this)) {
+    protozero::pbf_writer sub{writer, 6};
+    std::get<KeyValueList>(*this).encode(sub);
+  }
+  else if (std::holds_alternative<std::vector<uint8_t>>(*this)) {
+    pdns::trace::encode(writer, 7, std::get<std::vector<uint8_t>>(*this), true);
+  }
+}
+
+AnyValue AnyValue::decode(protozero::pbf_reader& reader)
+{
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      return AnyValue{reader.get_string()};
+      break;
+    case 2:
+      return AnyValue{reader.get_bool()};
+      break;
+    case 3:
+      return AnyValue{reader.get_int64()};
+      break;
+    case 4:
+      return AnyValue{reader.get_double()};
+      break;
+    case 5: {
+      protozero::pbf_reader arrayvalue = reader.get_message();
+      return AnyValue{ArrayValue::decode(arrayvalue)};
+      break;
+    }
+    case 6: {
+      protozero::pbf_reader kvlist = reader.get_message();
+      return AnyValue{KeyValueList::decode(kvlist)};
+      break;
+    }
+    case 7: {
+      auto value = reader.get_view();
+      std::vector<uint8_t> data{};
+      data.reserve(value.size());
+      for (size_t i = 0; i < value.size(); ++i) {
+        data.push_back(static_cast<uint8_t>(value.data()[i]));
+      }
+      return AnyValue{std::move(data)};
+      break;
+    }
+    default:
+      break;
+    }
+  }
+
+  return {};
+}
+
+void EntityRef::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, schema_url);
+  pdns::trace::encode(writer, 2, type);
+  for (auto const& element : id_keys) {
+    pdns::trace::encode(writer, 3, element);
+  }
+  for (auto const& element : description_keys) {
+    pdns::trace::encode(writer, 4, element);
+  }
+}
+
+EntityRef EntityRef::decode(protozero::pbf_reader& reader)
+{
+  EntityRef ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      ret.schema_url = reader.get_string();
+      break;
+    case 2:
+      ret.type = reader.get_string();
+      break;
+    case 3:
+      ret.id_keys.emplace_back(reader.get_string());
+      break;
+    case 4:
+      ret.description_keys.emplace_back(reader.get_string());
+      break;
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void KeyValue::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, key);
+  {
+    protozero::pbf_writer val_sub{writer, 2};
+    value.encode(val_sub);
+  }
+}
+
+void Resource::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, attributes);
+  pdns::trace::encode(writer, 2, dropped_attributes_count);
+  pdns::trace::encode(writer, 3, entity_refs);
+}
+
+Resource Resource::decode(protozero::pbf_reader& reader)
+{
+  Resource ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1: {
+      auto sub = reader.get_message();
+      ret.attributes.emplace_back(KeyValue::decode(sub));
+      break;
+    }
+    case 2:
+      ret.dropped_attributes_count = reader.get_uint32();
+      break;
+    case 3: {
+      auto sub = reader.get_message();
+      ret.entity_refs.emplace_back(EntityRef::decode(sub));
+      break;
+    }
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void InstrumentationScope::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, name);
+  pdns::trace::encode(writer, 2, version);
+  pdns::trace::encode(writer, 3, attributes);
+  pdns::trace::encode(writer, 4, dropped_attributes_count);
+}
+
+InstrumentationScope InstrumentationScope::decode(protozero::pbf_reader& reader)
+{
+  InstrumentationScope ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      ret.name = reader.get_string();
+      break;
+    case 2:
+      ret.version = reader.get_string();
+      break;
+    case 3: {
+      auto sub = reader.get_message();
+      ret.attributes.emplace_back(KeyValue::decode(sub));
+      break;
+    }
+    case 4:
+      ret.dropped_attributes_count = reader.get_uint32();
+      break;
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void Status::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 2, message);
+  pdns::trace::encode(writer, 3, uint32_t(code));
+}
+
+Status Status::decode(protozero::pbf_reader& reader)
+{
+  Status ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 2:
+      ret.message = reader.get_string();
+      break;
+    case 3:
+      ret.code = static_cast<StatusCode>(reader.get_uint32());
+      break;
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void Span::Event::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encodeFixed(writer, 1, time_unix_nano);
+  pdns::trace::encode(writer, 2, name);
+  pdns::trace::encode(writer, 3, attributes);
+  pdns::trace::encode(writer, 4, dropped_attribute_count);
+}
+
+Span::Event Span::Event::decode(protozero::pbf_reader& reader)
+{
+  Span::Event ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      ret.time_unix_nano = reader.get_fixed64();
+      break;
+    case 2:
+      ret.name = reader.get_string();
+      break;
+    case 3: {
+      auto sub = reader.get_message();
+      ret.attributes.emplace_back(KeyValue::decode(sub));
+      break;
+    }
+    case 4:
+      ret.dropped_attribute_count = reader.get_uint32();
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void Span::Link::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, trace_id);
+  pdns::trace::encode(writer, 2, span_id);
+  pdns::trace::encode(writer, 3, trace_state);
+  pdns::trace::encode(writer, 4, attributes);
+  pdns::trace::encode(writer, 5, dropped_attribute_count);
+  pdns::trace::encodeFixed(writer, 6, flags);
+}
+
+Span::Link Span::Link::decode(protozero::pbf_reader& reader)
+{
+  Link ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      ret.trace_id = decodeTraceID(reader);
+      break;
+    case 2:
+      ret.span_id = decodeSpanID(reader);
+      break;
+    case 3:
+      ret.trace_state = reader.get_string();
+      break;
+    case 4: {
+      auto sub = reader.get_message();
+      ret.attributes.emplace_back(KeyValue::decode(sub));
+      break;
+    }
+    case 5:
+      ret.dropped_attribute_count = reader.get_uint32();
+      break;
+    case 6:
+      ret.flags = reader.get_uint32();
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void Span::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, trace_id);
+  pdns::trace::encode(writer, 2, span_id);
+  pdns::trace::encode(writer, 3, trace_state);
+  pdns::trace::encode(writer, 4, parent_span_id);
+  pdns::trace::encode(writer, 5, name);
+  pdns::trace::encode(writer, 6, uint32_t(kind));
+  pdns::trace::encodeFixed(writer, 7, start_time_unix_nano);
+  pdns::trace::encodeFixed(writer, 8, end_time_unix_nano);
+  pdns::trace::encode(writer, 9, attributes);
+  pdns::trace::encode(writer, 10, dropped_attribute_count);
+  pdns::trace::encode(writer, 11, events);
+  pdns::trace::encode(writer, 12, dropped_events_count);
+  pdns::trace::encode(writer, 13, links);
+  pdns::trace::encode(writer, 14, dropped_links_count);
+  if (status.code != Status::StatusCode::STATUS_CODE_UNSET || !status.message.empty()) {
+    protozero::pbf_writer sub{writer, 15};
+    status.encode(sub);
+  }
+}
+
+Span Span::decode(protozero::pbf_reader& reader)
+{
+  Span ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      ret.trace_id = decodeTraceID(reader);
+      break;
+    case 2:
+      ret.span_id = decodeSpanID(reader);
+      break;
+    case 3:
+      ret.trace_state = reader.get_string();
+      break;
+    case 4:
+      ret.parent_span_id = decodeSpanID(reader);
+      break;
+    case 5:
+      ret.name = reader.get_string();
+      break;
+    case 6:
+      ret.kind = static_cast<Span::SpanKind>(reader.get_uint32());
+      break;
+    case 7:
+      ret.start_time_unix_nano = reader.get_fixed64();
+      break;
+    case 8:
+      ret.end_time_unix_nano = reader.get_fixed64();
+      break;
+    case 9: {
+      auto sub = reader.get_message();
+      ret.attributes.emplace_back(KeyValue::decode(sub));
+      break;
+    }
+    case 10:
+      ret.dropped_attribute_count = reader.get_uint32();
+      break;
+    case 11: {
+      auto sub = reader.get_message();
+      ret.events.emplace_back(Span::Event::decode(sub));
+      break;
+    }
+    case 12:
+      ret.dropped_events_count = reader.get_uint32();
+      break;
+    case 13: {
+      auto sub = reader.get_message();
+      ret.links.emplace_back(Span::Link::decode(sub));
+      break;
+    }
+    case 14:
+      ret.dropped_links_count = reader.get_uint32();
+      break;
+    case 15: {
+      auto sub = reader.get_message();
+      ret.status = Status::decode(sub);
+      break;
+    }
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void ScopeSpans::encode(protozero::pbf_writer& writer) const
+{
+  {
+    protozero::pbf_writer sub{writer, 1};
+    scope.encode(sub);
+  }
+  pdns::trace::encode(writer, 2, spans);
+  pdns::trace::encode(writer, 3, schema_url);
+}
+
+ScopeSpans ScopeSpans::decode(protozero::pbf_reader& reader)
+{
+  ScopeSpans ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1: {
+      auto sub = reader.get_message();
+      ret.scope = InstrumentationScope::decode(sub);
+      break;
+    }
+    case 2: {
+      auto sub = reader.get_message();
+      ret.spans.emplace_back(Span::decode(sub));
+      break;
+    }
+    case 3:
+      ret.schema_url = reader.get_string();
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void ResourceSpans::encode(protozero::pbf_writer& writer) const
+{
+  {
+    protozero::pbf_writer sub{writer, 1};
+    resource.encode(sub);
+  }
+  pdns::trace::encode(writer, 2, scope_spans);
+  pdns::trace::encode(writer, 3, schema_url);
+}
+
+ResourceSpans ResourceSpans::decode(protozero::pbf_reader& reader)
+{
+  ResourceSpans ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1: {
+      protozero::pbf_reader sub = reader.get_message();
+      ret.resource = Resource::decode(sub);
+      break;
+    }
+    case 2: {
+      protozero::pbf_reader sub = reader.get_message();
+      ret.scope_spans.emplace_back(ScopeSpans::decode(sub));
+      break;
+    }
+    case 3:
+      ret.schema_url = reader.get_string();
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+void TracesData::encode(protozero::pbf_writer& writer) const
+{
+  pdns::trace::encode(writer, 1, resource_spans);
+}
+
+TracesData TracesData::decode(protozero::pbf_reader& reader)
+{
+  TracesData ret;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1: {
+      auto sub = reader.get_message();
+      ret.resource_spans.emplace_back(ResourceSpans::decode(sub));
+      break;
+    }
+    default:
+      break;
+    }
+  }
+  return ret;
+}
+
+KeyValue KeyValue::decode(protozero::pbf_reader& reader)
+{
+  KeyValue value;
+  while (reader.next()) {
+    switch (reader.tag()) {
+    case 1:
+      value.key = reader.get_string();
+      break;
+    case 2: {
+      protozero::pbf_reader sub = reader.get_message();
+      value.value = AnyValue::decode(sub);
+      break;
+    }
+    default:
+      break;
+    }
+  }
+  return value;
+}
+
+}
diff --git a/pdns/recursordist/protozero-trace.hh b/pdns/recursordist/protozero-trace.hh
new file mode 100644 (file)
index 0000000..695635d
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <array>
+#include <variant>
+#include <vector>
+
+#include <protozero/pbf_reader.hpp>
+#include <protozero/pbf_writer.hpp>
+
+// See https://github.com/open-telemetry/opentelemetry-proto/tree/main/opentelemetry/proto
+
+namespace pdns::trace
+{
+
+struct AnyValue;
+struct ArrayValue;
+struct KeyValue;
+struct KeyValueList;
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, bool value, bool always = false)
+{
+  if (always || value) {
+    writer.add_bool(field, value);
+  }
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, uint32_t value, bool always = false)
+{
+  if (always || value != 0) {
+    writer.add_uint32(field, value);
+  }
+}
+
+inline void encodeFixed(protozero::pbf_writer& writer, uint8_t field, uint32_t value)
+{
+  writer.add_fixed32(field, value);
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, int64_t value, bool always = false)
+{
+  if (always || value != 0) {
+    writer.add_int64(field, value);
+  }
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, uint64_t value, bool always = false)
+{
+  if (always || value != 0) {
+    writer.add_uint64(field, value);
+  }
+}
+
+inline void encodeFixed(protozero::pbf_writer& writer, uint8_t field, uint64_t value)
+{
+  writer.add_fixed64(field, value);
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, double value, bool always = false)
+{
+  if (always || value != 0.0) {
+    writer.add_double(field, value);
+  }
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, const std::string& value, bool always = false)
+{
+  if (always || !value.empty()) {
+    writer.add_string(field, value);
+  }
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, const std::vector<uint8_t>& value, bool always = false)
+{
+  if (always || !value.empty()) {
+    writer.add_bytes(field, reinterpret_cast<const char*>(value.data()), value.size()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) it's the API
+  }
+}
+
+template <typename T>
+void encode(protozero::pbf_writer& writer, const std::vector<T>& vec)
+{
+  for (auto const& element : vec) {
+    element.encode(writer);
+  }
+}
+
+template <typename T>
+void encode(protozero::pbf_writer& writer, uint8_t field, const std::vector<T>& vec)
+{
+  for (auto const& element : vec) {
+    protozero::pbf_writer sub{writer, field};
+    element.encode(sub);
+  }
+}
+
+template <typename T, typename E>
+T decode(protozero::pbf_reader& reader)
+{
+  std::vector<E> vec;
+  while (reader.next()) {
+    if (reader.tag() == 1) {
+      protozero::pbf_reader sub = reader.get_message();
+      vec.emplace_back(E::decode(sub));
+    }
+  }
+  return {vec};
+}
+
+struct ArrayValue
+{
+  std::vector<AnyValue> values; // = 1
+
+  void encode(protozero::pbf_writer& writer) const
+  {
+    pdns::trace::encode(writer, 1, values);
+  }
+
+  static ArrayValue decode(protozero::pbf_reader& reader);
+
+  bool operator==(const ArrayValue& rhs) const
+  {
+    return values == rhs.values;
+  }
+};
+
+struct KeyValueList
+{
+  std::vector<KeyValue> values; // = 1
+
+  void encode(protozero::pbf_writer& writer) const
+  {
+    pdns::trace::encode(writer, 1, values);
+  }
+
+  static KeyValueList decode(protozero::pbf_reader& reader);
+
+  bool operator==(const KeyValueList& rhs) const
+  {
+    return values == rhs.values;
+  }
+};
+
+struct AnyValue : public std::variant<char, std::string, bool, int64_t, double, ArrayValue, KeyValueList, std::vector<uint8_t>>
+{
+  void encode(protozero::pbf_writer& writer) const;
+  static AnyValue decode(protozero::pbf_reader& reader);
+};
+
+struct EntityRef
+{
+  std::string schema_url; // == 1
+  std::string type; // == 2
+  std::vector<std::string> id_keys; // == 3
+  std::vector<std::string> description_keys; // == 4
+
+  void encode(protozero::pbf_writer& writer) const;
+  static EntityRef decode(protozero::pbf_reader& reader);
+};
+
+struct KeyValue
+{
+  std::string key; // = 1
+  AnyValue value; // = 2
+  void encode(protozero::pbf_writer& writer) const;
+  static KeyValue decode(protozero::pbf_reader& reader);
+
+  bool operator==(const KeyValue& rhs) const
+  {
+    return key == rhs.key && value == rhs.value;
+  }
+};
+
+struct Resource
+{
+  std::vector<KeyValue> attributes; // = 1
+  uint32_t dropped_attributes_count{0}; // = 2;
+  std::vector<EntityRef> entity_refs; // = 3
+
+  void encode(protozero::pbf_writer& writer) const;
+  static Resource decode(protozero::pbf_reader& reader);
+};
+
+struct InstrumentationScope
+{
+  std::string name; // = 1
+  std::string version; // = 2
+  std::vector<KeyValue> attributes; // = 3
+  uint32_t dropped_attributes_count{0}; // = 4
+
+  void encode(protozero::pbf_writer& writer) const;
+  static InstrumentationScope decode(protozero::pbf_reader& reader);
+};
+
+using TraceID = std::array<uint8_t, 16>;
+using SpanID = std::array<uint8_t, 8>;
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, const TraceID& value)
+{
+  writer.add_bytes(field, reinterpret_cast<const char*>(value.data()), value.size()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) it's the API
+}
+
+inline TraceID decodeTraceID(protozero::pbf_reader& reader)
+{
+  TraceID bytes;
+  auto [data, len] = reader.get_data();
+  memcpy(bytes.data(), data, std::min(bytes.size(), static_cast<size_t>(len)));
+  return bytes;
+}
+
+inline void encode(protozero::pbf_writer& writer, uint8_t field, const SpanID& value)
+{
+  writer.add_bytes(field, reinterpret_cast<const char*>(value.data()), value.size()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) it's the API
+}
+
+inline SpanID decodeSpanID(protozero::pbf_reader& reader)
+{
+  SpanID bytes;
+  auto [data, len] = reader.get_data();
+  memcpy(bytes.data(), data, std::min(bytes.size(), static_cast<size_t>(len)));
+  return bytes;
+}
+
+struct Status
+{
+  std::string message; // = 2;
+
+  // For the semantics of status codes see
+  // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status
+  enum class StatusCode : uint8_t
+  {
+    STATUS_CODE_UNSET = 0,
+    STATUS_CODE_OK = 1,
+    STATUS_CODE_ERROR = 2,
+  };
+
+  // The status code.
+  StatusCode code{StatusCode::STATUS_CODE_UNSET}; //  = 3;
+
+  void encode(protozero::pbf_writer& writer) const;
+  static Status decode(protozero::pbf_reader& reader);
+};
+
+struct Span
+{
+  TraceID trace_id; // = 1
+  SpanID span_id; // = 2
+  std::string trace_state; // = 3
+  SpanID parent_span_id; // = 4
+  std::string name; // = 5
+  enum class SpanKind : uint8_t
+  {
+    SPAN_KINUNSPECIFIED = 0,
+    SPAN_KININTERNAL = 1,
+    SPAN_KINSERVER = 2,
+    SPAN_KINCLIENT = 3,
+    SPAN_KINPRODUCER = 4,
+    SPAN_KINCONSUMER = 5,
+  };
+  SpanKind kind{Span::SpanKind::SPAN_KINUNSPECIFIED}; // = 6
+  uint64_t start_time_unix_nano{0}; // = 7
+  uint64_t end_time_unix_nano{0}; // = 8
+  std::vector<KeyValue> attributes; // = 9
+  uint32_t dropped_attribute_count{0}; // = 10
+  struct Event
+  {
+    uint64_t time_unix_nano; // = 1
+    std::string name; // = 2
+    std::vector<KeyValue> attributes; // = 3
+    uint32_t dropped_attribute_count{0}; // = 4
+
+    void encode(protozero::pbf_writer& writer) const;
+    static Event decode(protozero::pbf_reader& reader);
+  };
+  std::vector<Event> events; // = 11
+  uint32_t dropped_events_count; // = 12
+  struct Link
+  {
+    TraceID trace_id; // = 1
+    SpanID span_id; // = 2
+    std::string trace_state; // = 3
+    std::vector<KeyValue> attributes; // = 4
+    uint32_t dropped_attribute_count{0}; // = 5
+    uint32_t flags{0}; // = 6
+
+    void encode(protozero::pbf_writer& writer) const;
+    static Link decode(protozero::pbf_reader& reader);
+  };
+  std::vector<Link> links; // = 13
+  uint32_t dropped_links_count{0}; // = 14
+  Status status; // = 15
+
+  void encode(protozero::pbf_writer& writer) const;
+  static Span decode(protozero::pbf_reader& reader);
+};
+
+struct ScopeSpans
+{
+  InstrumentationScope scope; // = 1
+  std::vector<Span> spans; // = 2
+  std::string schema_url; // = 3
+
+  void encode(protozero::pbf_writer& writer) const;
+  static ScopeSpans decode(protozero::pbf_reader& reader);
+};
+
+struct ResourceSpans
+{
+  Resource resource; // = 1
+  std::vector<ScopeSpans> scope_spans; // = 2
+  std::string schema_url; // = 3
+
+  void encode(protozero::pbf_writer& writer) const;
+  static ResourceSpans decode(protozero::pbf_reader& reader);
+};
+
+struct TracesData
+{
+  std::vector<ResourceSpans> resource_spans; // = 1
+
+  void encode(protozero::pbf_writer& writer) const;
+  static TracesData decode(protozero::pbf_reader& reader);
+};
+
+inline ArrayValue ArrayValue::decode(protozero::pbf_reader& reader)
+{
+  return pdns::trace::decode<ArrayValue, AnyValue>(reader);
+}
+
+inline KeyValueList KeyValueList::decode(protozero::pbf_reader& reader)
+{
+  return pdns::trace::decode<KeyValueList, KeyValue>(reader);
+}
+
+}
diff --git a/pdns/recursordist/test-protozero-trace.cc b/pdns/recursordist/test-protozero-trace.cc
new file mode 100644 (file)
index 0000000..4b40374
--- /dev/null
@@ -0,0 +1,160 @@
+#ifndef BOOST_TEST_DYN_LINK
+#define BOOST_TEST_DYN_LINK
+#endif
+
+#define BOOST_TEST_NO_MAIN
+
+#include "config.h"
+#include <fstream>
+#include <boost/test/unit_test.hpp>
+
+#include "protozero-trace.hh"
+#include "misc.hh"
+
+BOOST_AUTO_TEST_SUITE(test_protobuf_trace)
+
+BOOST_AUTO_TEST_CASE(resource0)
+{
+  pdns::trace::Resource resource{};
+  std::string data;
+  protozero::pbf_writer writer{data};
+  resource.encode(writer);
+#if 0
+  std::ofstream x("x");
+  x << data;
+#endif
+  BOOST_CHECK_EQUAL(makeHexDump(data, " "), "");
+}
+
+BOOST_AUTO_TEST_CASE(resource1)
+{
+  pdns::trace::Resource resource{
+    {
+      {"foo0", {"bar"}},
+      {"foo1", {99.99}},
+    },
+    99,
+    {{{"schema0", "type0", {"id00", "id01"}, {"desc00", "desc01"}},
+      {"schema1", "type1", {"id10", "id11"}, {"desc10", "desc11"}}}}};
+  std::string data;
+  protozero::pbf_writer writer{data};
+  resource.encode(writer);
+#if 0
+  std::ofstream x("x");
+  x << data;
+#endif
+  BOOST_CHECK_EQUAL(makeHexDump(data, " "), "0a 0d 0a 04 66 6f 6f 30 12 05 0a 03 62 61 72 0a 11 0a 04 66 6f 6f 31 12 09 21 8f c2 f5 28 5c ff 58 40 10 63 1a 2c 0a 07 73 63 68 65 6d 61 30 12 05 74 79 70 65 30 1a 04 69 64 30 30 1a 04 69 64 30 31 22 06 64 65 73 63 30 30 22 06 64 65 73 63 30 31 1a 2c 0a 07 73 63 68 65 6d 61 31 12 05 74 79 70 65 31 1a 04 69 64 31 30 1a 04 69 64 31 31 22 06 64 65 73 63 31 30 22 06 64 65 73 63 31 31 ");
+}
+
+template <typename T>
+static void testAny(const T& testcase)
+{
+  std::string data;
+  protozero::pbf_writer writer{data};
+  pdns::trace::AnyValue wrapper{testcase};
+  wrapper.encode(writer);
+#if 0
+  std::ofstream x("x");
+  x << data;
+#endif
+
+  protozero::pbf_reader reader{data};
+  pdns::trace::AnyValue value = pdns::trace::AnyValue::decode(reader);
+  if (!std::holds_alternative<char>(value)) {
+    BOOST_CHECK(testcase == std::get<T>(value));
+  }
+  else {
+    if (std::holds_alternative<pdns::trace::ArrayValue>(wrapper)) {
+      BOOST_CHECK(std::get<pdns::trace::ArrayValue>(wrapper).values.empty());
+    }
+    else if (std::holds_alternative<pdns::trace::KeyValueList>(wrapper)) {
+      BOOST_CHECK(std::get<pdns::trace::KeyValueList>(wrapper).values.empty());
+    }
+  }
+}
+
+BOOST_AUTO_TEST_CASE(any)
+{
+  testAny(std::string{"foo"});
+  testAny(false);
+  testAny(true);
+  testAny(static_cast<int64_t>(0));
+  testAny(static_cast<int64_t>(1));
+  testAny(static_cast<int64_t>(-1));
+  testAny(std::numeric_limits<int64_t>::min());
+  testAny(std::numeric_limits<int64_t>::max());
+  testAny(0.0);
+  testAny(1.0);
+  testAny(-1.0);
+  testAny(std::numeric_limits<double>::min());
+  testAny(std::numeric_limits<double>::max());
+
+  pdns::trace::ArrayValue avalue;
+  testAny(avalue);
+  avalue.values.emplace_back(pdns::trace::AnyValue{"foo"});
+  avalue.values.emplace_back(pdns::trace::AnyValue{1.99});
+  testAny(avalue);
+
+  pdns::trace::KeyValueList kvlist;
+  testAny(kvlist);
+  kvlist.values.emplace_back(pdns::trace::KeyValue{"foo", {"bar"}});
+  kvlist.values.emplace_back(pdns::trace::KeyValue{"baz", {1.99}});
+  testAny(kvlist);
+
+  std::vector<uint8_t> bytes;
+  testAny(bytes);
+  bytes.push_back(0);
+  bytes.push_back(1);
+  bytes.push_back(2);
+  testAny(bytes);
+}
+
+BOOST_AUTO_TEST_CASE(traces)
+{
+  pdns::trace::Span span = {
+    .trace_id = {0x5B, 0x8E, 0xFF, 0xF7, 0x98, 0x03, 0x81, 0x03, 0xD2, 0x69, 0xB6, 0x33, 0x81, 0x3F, 0xC6, 0x0C},
+    .span_id = {0xEE, 0xE1, 0x9B, 0x7E, 0xC3, 0xC1, 0xB1, 0x74},
+    .parent_span_id = {0xEE, 0xE1, 0x9B, 0x7E, 0xC3, 0xC1, 0xB1, 0x73},
+    .name = "I'm a server span",
+    .start_time_unix_nano = 1544712660000000000UL,
+    .end_time_unix_nano = 1544712661000000000UL,
+    .kind = pdns::trace::Span::SpanKind::SPAN_KINSERVER,
+    .attributes = {{"my.span.attr", {"some value"}}}};
+  pdns::trace::InstrumentationScope scope = {"my.library", "1.0.0", {{"my.scope.attribute", {"some scope attribute"}}}};
+  pdns::trace::ScopeSpans scopespans = {.scope = scope, .spans = {span}};
+  pdns::trace::Resource res = {.attributes = {{"service.name", {"my.service"}}}};
+  pdns::trace::ResourceSpans resspans = {{res}, .scope_spans = {scopespans}};
+  pdns::trace::TracesData traces = {.resource_spans = {resspans}};
+
+  std::string data;
+  protozero::pbf_writer writer{data};
+  traces.encode(writer);
+#if 0
+  std::ofstream z("z");
+  z << data;
+#endif
+  const string expected = ""
+                          "0a d3 01 0a 1e 0a 1c 0a 0c 73 65 72 76 69 63 65 "
+                          "2e 6e 61 6d 65 12 0c 0a 0a 6d 79 2e 73 65 72 76 "
+                          "69 63 65 12 b0 01 0a 41 0a 0a 6d 79 2e 6c 69 62 "
+                          "72 61 72 79 12 05 31 2e 30 2e 30 1a 2c 0a 12 6d "
+                          "79 2e 73 63 6f 70 65 2e 61 74 74 72 69 62 75 74 "
+                          "65 12 16 0a 14 73 6f 6d 65 20 73 63 6f 70 65 20 "
+                          "61 74 74 72 69 62 75 74 65 12 6b 0a 10 5b 8e ff "
+                          "f7 98 03 81 03 d2 69 b6 33 81 3f c6 0c 12 08 ee "
+                          "e1 9b 7e c3 c1 b1 74 22 08 ee e1 9b 7e c3 c1 b1 "
+                          "73 2a 11 49 27 6d 20 61 20 73 65 72 76 65 72 20 "
+                          "73 70 61 6e 30 02 39 00 48 59 e3 fa eb 6f 15 41 "
+                          "00 12 f4 1e fb eb 6f 15 4a 1c 0a 0c 6d 79 2e 73 "
+                          "70 61 6e 2e 61 74 74 72 12 0c 0a 0a 73 6f 6d 65 "
+                          "20 76 61 6c 75 65 ";
+  BOOST_CHECK_EQUAL(makeHexDump(data, " "), expected);
+
+  protozero::pbf_reader reader{data};
+  auto copy = pdns::trace::TracesData::decode(reader);
+  data.clear();
+  protozero::pbf_writer copyWriter{data};
+  copy.encode(copyWriter);
+  BOOST_CHECK_EQUAL(makeHexDump(data, " "), expected);
+}
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/pdns/recursordist/trace-example.proto b/pdns/recursordist/trace-example.proto
new file mode 100644 (file)
index 0000000..8927ecc
--- /dev/null
@@ -0,0 +1,51 @@
+{
+  "resourceSpans": [
+    {
+      "resource": {
+        "attributes": [
+          {
+            "key": "service.name",
+            "value": {
+              "stringValue": "my.service"
+            }
+          }
+        ]
+      },
+      "scopeSpans": [
+        {
+          "scope": {
+            "name": "my.library",
+            "version": "1.0.0",
+            "attributes": [
+              {
+                "key": "my.scope.attribute",
+                "value": {
+                  "stringValue": "some scope attribute"
+                }
+              }
+            ]
+          },
+          "spans": [
+            {
+              "traceId": "W47/95gDgQPSabYzgT/GDA==",
+              "spanId": "7uGbfsPBsXQ=",
+              "parentSpanId": "7uGbfsPBsXM=",
+              "name": "I'm a server span",
+              "startTimeUnixNano": 1544712660000000000,
+              "endTimeUnixNano": 1544712661000000000,
+              "kind": 2,
+              "attributes": [
+                {
+                  "key": "my.span.attr",
+                  "value": {
+                    "stringValue": "some value"
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file