From: Pieter Lexis Date: Tue, 9 Jun 2026 14:57:56 +0000 (+0200) Subject: feat(dnsdist): Add a simple OTLP exporter X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4d370a3cd7f42453e68ee4776fe4e10851bc93a5;p=thirdparty%2Fpdns.git feat(dnsdist): Add a simple OTLP exporter --- diff --git a/pdns/dnsdistdist/meson.build b/pdns/dnsdistdist/meson.build index 775fdfcea0..1f9d69ee83 100644 --- a/pdns/dnsdistdist/meson.build +++ b/pdns/dnsdistdist/meson.build @@ -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 index 0000000000..cc8cd420ea --- /dev/null +++ b/pdns/dnsdistdist/meson/libcurl/meson.build @@ -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') diff --git a/pdns/dnsdistdist/meson_options.txt b/pdns/dnsdistdist/meson_options.txt index fbed8ada1a..7d8e85a157 100644 --- a/pdns/dnsdistdist/meson_options.txt +++ b/pdns/dnsdistdist/meson_options.txt @@ -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 index 0000000000..3f48b8c54e --- /dev/null +++ b/pdns/dnsdistdist/minicurl.cc @@ -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 index 0000000000..bba06c379e --- /dev/null +++ b/pdns/dnsdistdist/minicurl.hh @@ -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 index 0000000000..37407d231c --- /dev/null +++ b/pdns/dnsdistdist/otlp_logger.cc @@ -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 +#include + +#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("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< + +#if !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) +#include + +#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 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 index 0000000000..a4157d97b5 --- /dev/null +++ b/pdns/dnsdistdist/protozero-otlp.hh @@ -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 +#include +#include + +#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 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 diff --git a/tasks.py b/tasks.py index 4268fd47bf..afaea4035c 100644 --- 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", ] )