]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
feat(dnsdist): Add a simple OTLP exporter
authorPieter Lexis <pieter.lexis@powerdns.com>
Tue, 9 Jun 2026 14:57:56 +0000 (16:57 +0200)
committerPieter Lexis <pieter.lexis@powerdns.com>
Fri, 3 Jul 2026 13:10:53 +0000 (15:10 +0200)
pdns/dnsdistdist/meson.build
pdns/dnsdistdist/meson/libcurl/meson.build [new file with mode: 0644]
pdns/dnsdistdist/meson_options.txt
pdns/dnsdistdist/minicurl.cc [new symlink]
pdns/dnsdistdist/minicurl.hh [new symlink]
pdns/dnsdistdist/otlp_logger.cc [new file with mode: 0644]
pdns/dnsdistdist/otlp_logger.hh [new file with mode: 0644]
pdns/dnsdistdist/protozero-otlp.hh [new file with mode: 0644]
tasks.py

index 775fdfcea082366c5ca7923aa81566c97f0b2825..1f9d69ee832682428179e552a566b17954d2216e 100644 (file)
@@ -50,6 +50,7 @@ subdir('meson' / 'net-libs')                # Network Libraries
 subdir('meson' / 'tm-gmtoff')               # Check for tm_gmtoff field in struct tm
 subdir('meson' / 'mmap')                    # Check for mmap
 subdir('meson' / 'libcap')                  # Libcap to drop capabilities
+subdir('meson' / 'libcurl')                 # Libcurl for OTLPLogger
 subdir('meson' / 'libedit')                 # Libedit
 subdir('meson' / 'libsodium')               # Libsodium
 subdir('meson' / 'libcrypto')               # OpenSSL libcrypto
@@ -209,6 +210,7 @@ common_sources += files(
   src_dir / 'libssl.cc',
   src_dir / 'logging.cc',
   src_dir / 'misc.cc',
+  src_dir / 'otlp_logger.cc',
   src_dir / 'protozero.cc',
   src_dir / 'protozero-trace.cc',
   src_dir / 'proxy-protocol.cc',
@@ -231,6 +233,12 @@ conditional_sources = {
     ],
     'condition': dep_cdb.found(),
   },
+  'minicurl': {
+    'sources': [
+      src_dir / 'minicurl.cc',
+    ],
+    'condition': get_option('otlp').allowed() and dep_libcurl.found(),
+  },
   'doq': {
     'sources': [
       src_dir / 'doq.cc',
@@ -395,6 +403,7 @@ deps = [
   dep_ipcrypt2,
   dep_libcap,
   dep_libcrypto,
+  dep_libcurl,
   dep_gnutls,
   dep_libedit,
   dep_json11,
diff --git a/pdns/dnsdistdist/meson/libcurl/meson.build b/pdns/dnsdistdist/meson/libcurl/meson.build
new file mode 100644 (file)
index 0000000..cc8cd42
--- /dev/null
@@ -0,0 +1,12 @@
+opt_otlp = get_option('otlp')
+opt_libcurl = get_option('libcurl')
+
+if opt_otlp.allowed() and opt_libcurl.allowed()
+  dep_libcurl = dependency('libcurl', version: '>= 7.21.3', required: false)
+else
+  dep_libcurl = dependency('', required: false)
+endif
+
+conf.set('HAVE_LIBCURL', dep_libcurl.found(), description: 'Whether we have libcurl')
+summary('cURL', dep_libcurl.found(), bool_yn: true, section: 'Configuration')
+summary('OTLP remote logger', dep_libcurl.found() and opt_otlp.allowed(), bool_yn: true, section: 'Configuration')
index fbed8ada1ad599b29a6592c11b3ce887216fa74c..7d8e85a15755f40bff115cce673d38b721b641f6 100644 (file)
@@ -2,6 +2,7 @@ option('dnscrypt', type: 'feature', value: 'disabled', description: 'Enable DNSC
 option('libcap', type: 'feature', value: 'auto', description: 'Enable libcap for capabilities handling')
 option('libedit', type: 'feature', value: 'enabled', description: 'Enable libedit')
 option('libsodium', type: 'feature', value: 'auto', description: 'Enable libsodium')
+option('libcurl', type: 'feature', value: 'auto', description: 'Enable libcurl (for the OTLP remote logger)')
 option('libcrypto', type: 'feature', value: 'auto', description: 'Enable OpenSSL libcrypto)')
 option('libcrypto-path', type: 'string', value: '', description: 'Custom path to find OpenSSL libcrypto')
 option('tls-gnutls', type: 'feature', value: 'auto', description: 'GnuTLS-based TLS')
@@ -28,6 +29,7 @@ option('systemd-service-group', type: 'string', value: 'dnsdist', description: '
 option('auto-var-init', type: 'combo', value: 'disabled', choices: ['zero', 'pattern', 'disabled'], description: 'Enable initialization of automatic variables')
 option('snmp', type: 'feature', value: 'disabled', description: 'Enable SNMP')
 option('dnstap', type: 'feature', value: 'auto', description: 'Enable DNSTAP support through libfstrm')
+option('otlp', type: 'feature', value: 'auto', description: 'Enable OTLP support')
 option('nghttp2', type: 'feature', value: 'auto', description: 'Enable nghttp2 library support for DNS over HTTP/2')
 option('cdb', type: 'feature', value: 'auto', description: 'CDB key-value store support')
 option('lmdb', type: 'feature', value: 'auto', description: 'LMDB key-value store support')
diff --git a/pdns/dnsdistdist/minicurl.cc b/pdns/dnsdistdist/minicurl.cc
new file mode 120000 (symlink)
index 0000000..3f48b8c
--- /dev/null
@@ -0,0 +1 @@
+../minicurl.cc
\ No newline at end of file
diff --git a/pdns/dnsdistdist/minicurl.hh b/pdns/dnsdistdist/minicurl.hh
new file mode 120000 (symlink)
index 0000000..bba06c3
--- /dev/null
@@ -0,0 +1 @@
+../minicurl.hh
\ No newline at end of file
diff --git a/pdns/dnsdistdist/otlp_logger.cc b/pdns/dnsdistdist/otlp_logger.cc
new file mode 100644 (file)
index 0000000..37407d2
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * 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 <memory>
+#include <stdexcept>
+
+#include "otlp_logger.hh"
+#include "remote_logger.hh"
+
+#if !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL)
+#include "minicurl.hh"
+#include "protozero-trace.hh"
+#include "protozero-otlp.hh"
+
+OTLPLogger::OTLPLogger(std::string address) :
+  d_address(std::move(address))
+{
+  LoggerType loggerType;
+  if (d_address.find("http://") == 0 || d_address.find("https://") == 0) {
+    loggerType = LoggerType::HTTP;
+  }
+  else if (d_address.find("grpc://") == 0 || d_address.find("grpcs://") == 0) {
+    // XXX: this is not the actual scheme, see https://github.com/grpc/grpc/blob/master/doc/naming.md
+    loggerType = LoggerType::gRPC;
+  }
+  else {
+    throw std::runtime_error("Can not determine OTLP logger type from address " + address);
+  }
+  d_type = loggerType;
+
+  switch (d_type) {
+    case LoggerType::HTTP:
+      d_httpConn = std::make_unique<MiniCurl>("dnsdist/" + std::string(PACKAGE_VERSION));
+      break;
+    case LoggerType::gRPC:
+      throw std::runtime_error("gRPC support is not implemented");
+  }
+}
+
+RemoteLoggerInterface::Result OTLPLogger::queueData(const pdns::trace::TracesData& data)
+{
+  pdns::trace::ExportTraceServiceRequest req = {
+    .resource_spans = data.resource_spans
+  };
+  std::string buf;
+  protozero::pbf_writer writer(buf);
+  req.encode(writer);
+  return queueData(buf);
+}
+
+RemoteLoggerInterface::Result OTLPLogger::queueData(const std::string& data)
+{
+  // TODO: Proper queuing, batching, and retries
+  switch (d_type) {
+  case LoggerType::HTTP:
+    return queueHttpData(data);
+  case LoggerType::gRPC:
+    // Should never get here
+    throw std::runtime_error("gRPC support is not implemented");
+  }
+}
+
+RemoteLoggerInterface::Result OTLPLogger::queueHttpData(const std::string& data)
+{
+  auto response = d_httpConn->postURL(d_address, data, d_httpHeaders);
+  std::cout<<response<<std::endl;
+  protozero::pbf_reader reader(response);
+  auto exportTraceServiceResponse = pdns::trace::ExportTraceServiceResponse::decode(reader);
+  if (exportTraceServiceResponse.partial_success.rejected_spans == 0) {
+    return RemoteLoggerInterface::Result::Queued;
+  }
+  return RemoteLoggerInterface::Result::OtherError;
+}
+#endif /* !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) */
diff --git a/pdns/dnsdistdist/otlp_logger.hh b/pdns/dnsdistdist/otlp_logger.hh
new file mode 100644 (file)
index 0000000..4c17d93
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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 "remote_logger.hh"
+#include <memory>
+
+#if !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL)
+#include <memory>
+
+#include "logr.hh"
+#include "minicurl.hh"
+class OTLPLogger : public RemoteLoggerInterface
+{
+public:
+  enum class LoggerType : uint8_t
+  {
+    HTTP,
+    gRPC,
+  };
+
+  OTLPLogger(std::string address);
+  OTLPLogger(const OTLPLogger&) = delete;
+  OTLPLogger(OTLPLogger&&) = delete;
+  OTLPLogger& operator=(const OTLPLogger&) = delete;
+  OTLPLogger& operator=(OTLPLogger&&) = delete;
+  ~OTLPLogger() override {};
+
+  [[nodiscard]] RemoteLoggerInterface::Result queueData(const std::string& data) override;
+  [[nodiscard]] RemoteLoggerInterface::Result queueData(const pdns::trace::TracesData& data);
+
+  [[nodiscard]] std::string address() const override
+  {
+    return d_address;
+  }
+
+  [[nodiscard]] std::string name() const override
+  {
+    return "OTLP";
+  }
+
+  [[nodiscard]] std::string toString() override
+  {
+    return "OTLPLogger to " + d_address;
+  }
+
+  [[nodiscard]] RemoteLoggerInterface::Stats getStats() override
+  {
+    return Stats{.d_queued = 0,
+                 .d_pipeFull = 0,
+                 .d_tooLarge = 0,
+                 .d_otherError = 0};
+  }
+
+private:
+  LoggerType d_type;
+  std::string d_address;
+
+  std::unique_ptr<MiniCurl> d_httpConn{nullptr};
+  MiniCurl::MiniCurlHeaders d_httpHeaders{{"Content-Type", "application/x-protobuf"}};
+  [[nodiscard]] RemoteLoggerInterface::Result queueHttpData(const std::string& data);
+};
+#else
+class OTLPLogger : public RemoteLoggerInterface
+{
+  OTLPLogger(const OTLPLogger&) = delete;
+  OTLPLogger(OTLPLogger&&) = delete;
+  OTLPLogger& operator=(const OTLPLogger&) = delete;
+  OTLPLogger& operator=(OTLPLogger&&) = delete;
+
+public:
+  [[nodiscard]] RemoteLoggerInterface::Result queueData([[maybe_unused]] const std::string& data) override
+  {
+    return RemoteLogger::Result::Queued;
+  }
+  RemoteLoggerInterface::Result queueData([[maybe_unused]] const TracesData& data)
+  {
+    return RemoteLogger::Result::Queued;
+  }
+};
+#endif /* !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) */
diff --git a/pdns/dnsdistdist/protozero-otlp.hh b/pdns/dnsdistdist/protozero-otlp.hh
new file mode 100644 (file)
index 0000000..a4157d9
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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 <protozero/pbf_reader.hpp>
+#include <protozero/pbf_writer.hpp>
+#include <vector>
+
+#include "protozero-trace.hh"
+
+namespace pdns::trace
+{
+// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.10.0/opentelemetry/proto/collector/trace/v1/trace_service.proto
+
+struct ExportTraceServiceRequest
+{
+  // This message is the same as TracesData
+  std::vector<ResourceSpans> resource_spans; // = 1
+
+  void encode(protozero::pbf_writer& writer) const
+  {
+    pdns::trace::encode(writer, 1, resource_spans);
+  }
+
+  static ExportTraceServiceRequest decode(protozero::pbf_reader& reader);
+};
+
+struct ExportTracePartialSuccess
+{
+  int64_t rejected_spans; // = 1
+  std::string error_message; // = 2
+
+  void encode(protozero::pbf_writer& writer) const;
+  static ExportTracePartialSuccess decode(protozero::pbf_reader& reader) {
+    ExportTracePartialSuccess ret;
+    while (reader.next()) {
+      switch (reader.tag()) {
+      case 1:
+        ret.rejected_spans = reader.get_int64();
+      case 2:
+        ret.error_message = reader.get_string();
+      }
+    }
+    return ret;
+  };
+};
+
+struct ExportTraceServiceResponse
+{
+  ExportTracePartialSuccess partial_success; // = 1
+
+  void encode(protozero::pbf_writer& writer) const;
+  static ExportTraceServiceResponse decode(protozero::pbf_reader& reader)
+  {
+    ExportTraceServiceResponse ret;
+    while (reader.next()) {
+      switch (reader.tag()) {
+      case 1:
+        auto sub = reader.get_message();
+        ret.partial_success = ExportTracePartialSuccess::decode(sub);
+      }
+    }
+    return ret;
+  }
+};
+
+} // namespace pdns::trace
index 4268fd47bf356991f4bc532280b50ce38d21b3c3..afaea4035c1aded65595fdba73affdd5b0756631 100644 (file)
--- a/tasks.py
+++ b/tasks.py
@@ -85,6 +85,7 @@ dnsdist_build_deps = [
     "libcap-dev",
     "catch2",
     "libcdb-dev",
+    "libcurl4",
     "libedit-dev",
     "libfstrm-dev",
     "libgnutls28-dev",
@@ -999,6 +1000,7 @@ DNSDIST_CONFIGURE_MESON_FEATURE_SET_FULL = " ".join(
         "-D ebpf=enabled",
         "-D ipcipher=enabled",
         "-D ipcrypt2=enabled",
+        "-D libcurl=enabled",
         "-D libedit=enabled",
         "-D libsodium=enabled",
         "-D lmdb=enabled",
@@ -1014,6 +1016,7 @@ DNSDIST_CONFIGURE_MESON_FEATURE_SET_FULL = " ".join(
         "-D reproducible=true",
         "-D snmp=enabled",
         "-D yaml=enabled",
+        "-D otlp=enabled",
     ]
 )
 
@@ -1025,6 +1028,7 @@ DNSDIST_CONFIGURE_MESON_FEATURE_SET_LEAST = " ".join(
         "-D ebpf=disabled",
         "-D ipcipher=disabled",
         "-D ipcrypt2=disabled",
+        "-D libcurl=disabled",
         "-D libedit=disabled",
         "-D libsodium=disabled",
         "-D lmdb=disabled",
@@ -1039,6 +1043,7 @@ DNSDIST_CONFIGURE_MESON_FEATURE_SET_LEAST = " ".join(
         "-D reproducible=false",
         "-D snmp=disabled",
         "-D yaml=disabled",
+        "-D otlp=disabled",
     ]
 )