diff --git a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc index 60bd3c7f1ea7..b0e6d165ff3b 100644 --- a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc +++ b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc @@ -46,6 +46,7 @@ #include "dnsdist-web.hh" #include "dnsdist-xsk.hh" #include "fstrm_logger.hh" +#include "otlp_logger.hh" #include "iputils.hh" #include "mmdb.hh" #include "remote_logger.hh" @@ -1066,16 +1067,23 @@ static void handleLoggingConfiguration(const Context& context, const dnsdist::ru for (const auto& internal_trace_config : settings.open_telemetry_tracing.internal_tracing) { dnsdist::configuration::updateRuntimeConfiguration([context, internal_trace_config](dnsdist::configuration::RuntimeConfiguration& config) { if (internal_trace_config.kind == "maintenance") { + config.d_opentelemetryMaintenanceInterval = internal_trace_config.sample_rate == 0 ? 60 : internal_trace_config.sample_rate; std::vector> loggers; for (const auto& logger_name : internal_trace_config.remote_loggers) { auto logger = dnsdist::configuration::yaml::getRegisteredTypeByName(std::string(logger_name)); - if (!logger && !(dnsdist::configuration::yaml::s_inClientMode || dnsdist::configuration::yaml::s_inConfigCheckMode)) { - throw std::runtime_error("Unable to find the protobuf logger named '" + std::string(logger_name) + "'"); + if (!logger) { + if (!(dnsdist::configuration::yaml::s_inClientMode || dnsdist::configuration::yaml::s_inConfigCheckMode)) { + throw std::runtime_error("Unable to find the remote logger named '" + std::string(logger_name) + "'"); + } + continue; + } + RemoteLoggerInterface& remoteLoggerRef = *logger; + if (typeid(remoteLoggerRef) != typeid(RemoteLogger) && typeid(remoteLoggerRef) != typeid(OTLPLogger)) { + throw std::runtime_error("The remote logger '" + std::string(logger_name) + "' is not a Protobuf or OTLP logger and can not be used for maintenance traces"); } loggers.push_back(std::move(logger)); } config.d_maintenanceRemoteLoggers = std::move(loggers); - config.d_opentelemetryMaintenanceInterval = internal_trace_config.sample_rate == 0 ? 60 : internal_trace_config.sample_rate; } }); } @@ -2010,6 +2018,21 @@ void registerDnstapLogger([[maybe_unused]] const DnstapLoggerConfiguration& conf #endif } +void registerOtlpLogger([[maybe_unused]] const OtlpLoggerConfiguration& config) +{ +#if !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) + if (dnsdist::configuration::yaml::s_inClientMode || dnsdist::configuration::yaml::s_inConfigCheckMode) { + auto object = std::shared_ptr(nullptr); + dnsdist::configuration::yaml::registerType(object, config.name); + return; + } + std::shared_ptr object = std::make_shared(std::string(config.address), config.interval, config.queue_size, config.batch_size); + dnsdist::configuration::yaml::registerType(object, config.name); +#else + throw std::runtime_error("Unable to create OTLP logger: OTLP support is disabled"); +#endif /* !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) */ +} + void registerKVSObjects([[maybe_unused]] const KeyValueStoresConfiguration& config) { #if defined(HAVE_LMDB) || defined(HAVE_CDB) || defined(HAVE_MMDB) diff --git a/pdns/dnsdistdist/dnsdist-lua-actions.cc b/pdns/dnsdistdist/dnsdist-lua-actions.cc index 5f9f7e9b11e5..449955593042 100644 --- a/pdns/dnsdistdist/dnsdist-lua-actions.cc +++ b/pdns/dnsdistdist/dnsdist-lua-actions.cc @@ -29,6 +29,7 @@ #include "dnsdist-rule-chains.hh" #include "dnstap.hh" #include "dolog.hh" +#include "otlp_logger.hh" #include "remote_logger.hh" #include #include @@ -253,9 +254,9 @@ void setupLuaActions(LuaContext& luaCtx) if (remote_logger.second != nullptr) { // avoids potentially-evaluated-expression warning with clang. RemoteLoggerInterface& remoteLoggerRef = *remote_logger.second; - if (typeid(remoteLoggerRef) != typeid(RemoteLogger)) { + if (typeid(remoteLoggerRef) != typeid(RemoteLogger) && typeid(remoteLoggerRef) != typeid(OTLPLogger)) { // We could let the user do what he wants, but wrapping PowerDNS Protobuf inside a FrameStream tagged as dnstap is logically wrong. - throw std::runtime_error(std::string("SetTraceAction only takes RemoteLogger.")); + throw std::runtime_error(std::string("SetTraceAction only takes RemoteLogger or OTLPLogger.")); } loggers.push_back(remote_logger.second); } diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-protobuf.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-protobuf.cc index 9cfbead089a6..32a2e6d578e7 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-protobuf.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-protobuf.cc @@ -28,6 +28,7 @@ #include "dnsdist-protobuf.hh" #include "dnstap.hh" #include "fstrm_logger.hh" +#include "otlp_logger.hh" #include "ipcipher.hh" #include "remote_logger.hh" #include "remote_logger_pool.hh" @@ -196,6 +197,26 @@ void setupLuaBindingsProtoBuf(LuaContext& luaCtx, bool client, bool configCheck) #endif /* HAVE_FSTRM */ }); + luaCtx.writeFunction("newOtlpLogger", [client, configCheck]([[maybe_unused]] const std::string& address, [[maybe_unused]] std::optional> params) { +#if !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) + if (client || configCheck) { + return std::shared_ptr(nullptr); + } + size_t interval{5}; + size_t batchSize{100}; + size_t cacheSize{500}; + + getOptionalValue(params, "interval", interval); + getOptionalValue(params, "batchSize", batchSize); + getOptionalValue(params, "queueSize", cacheSize); + checkAllParametersConsumed("newOtlpLogger", params); + + return std::shared_ptr(new OTLPLogger(address, interval, cacheSize, batchSize)); +#else + throw std::runtime_error("Protobuf and CURL are required for OTLP remote loggers"); +#endif /* !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) */ + }); + luaCtx.registerFunction::*)() const>("toString", [](const std::shared_ptr& logger) { if (logger) { return logger->toString(); diff --git a/pdns/dnsdistdist/dnsdist-lua-configuration-items.cc b/pdns/dnsdistdist/dnsdist-lua-configuration-items.cc index bac172f695cb..c4be049d4448 100644 --- a/pdns/dnsdistdist/dnsdist-lua-configuration-items.cc +++ b/pdns/dnsdistdist/dnsdist-lua-configuration-items.cc @@ -29,6 +29,7 @@ #include "dnsdist-configuration.hh" #include "dnsdist-lua.hh" #include "dolog.hh" +#include "otlp_logger.hh" #include "ext/luawrapper/include/LuaContext.hpp" namespace dnsdist::lua @@ -197,9 +198,9 @@ static void setupOpenTelemetryConfigurationItems(LuaContext& luaCtx) if (remote_logger.second != nullptr) { // avoids potentially-evaluated-expression warning with clang. RemoteLoggerInterface& remoteLoggerRef = *remote_logger.second; - if (typeid(remoteLoggerRef) != typeid(RemoteLogger)) { + if (typeid(remoteLoggerRef) != typeid(RemoteLogger) && typeid(remoteLoggerRef) != typeid(OTLPLogger)) { // We could let the user do what he wants, but wrapping PowerDNS Protobuf inside a FrameStream tagged as dnstap is logically wrong. - throw std::runtime_error(std::string("setOpenTelemetryInternalTrace only takes RemoteLogger.")); + throw std::runtime_error(std::string("setOpenTelemetryInternalTrace only takes RemoteLogger and OTLPLogger.")); } loggers.push_back(remote_logger.second); } diff --git a/pdns/dnsdistdist/dnsdist-opentelemetry.cc b/pdns/dnsdistdist/dnsdist-opentelemetry.cc index f631698ae485..9f269b2c3f23 100644 --- a/pdns/dnsdistdist/dnsdist-opentelemetry.cc +++ b/pdns/dnsdistdist/dnsdist-opentelemetry.cc @@ -24,12 +24,14 @@ #include "dnsdist-ecs.hh" #include "sanitizer.hh" +#include #include #include #ifndef DISABLE_PROTOBUF #include "protozero.hh" #include "protozero-trace.hh" +#include "otlp_logger.hh" #endif namespace pdns::trace::dnsdist @@ -436,12 +438,31 @@ void sendTracesToRemoteLoggers(const std::shared_ptr& tracer, [[maybe_un #ifndef DISABLE_PROTOBUF static thread_local string pbBuf; pbBuf.clear(); + + bool haveNonOTLPLogger = false; + for (const auto& remotelogger : remoteloggers) { + RemoteLoggerInterface& remoteLoggerRef = *remotelogger; + haveNonOTLPLogger = haveNonOTLPLogger || typeid(remoteLoggerRef) != typeid(OTLPLogger); + if (haveNonOTLPLogger) { + break; + } + } + pdns::ProtoZero::Message minimalMsg{pbBuf}; - minimalMsg.setType(pdns::ProtoZero::Message::MessageType::InternalType); - minimalMsg.setOpenTelemetryTraceID(tracer->getTraceID()); - minimalMsg.setOpenTelemetryData(tracer->getOTProtobuf()); + if (haveNonOTLPLogger) { + minimalMsg.setType(pdns::ProtoZero::Message::MessageType::InternalType); + minimalMsg.setOpenTelemetryTraceID(tracer->getTraceID()); + minimalMsg.setOpenTelemetryData(tracer->getOTProtobuf()); + } + for (const auto& remotelogger : remoteloggers) { - remotelogger->queueData(pbBuf); + RemoteLoggerInterface& remoteLoggerRef = *remotelogger; + if (typeid(remoteLoggerRef) != typeid(OTLPLogger)) { + remotelogger->queueData(pbBuf); + } + else { + std::dynamic_pointer_cast(remotelogger)->queueData(tracer->getTracesData()); + } } #endif // DISABLE_PROTOBUF } diff --git a/pdns/dnsdistdist/dnsdist-rust-bridge.hh b/pdns/dnsdistdist/dnsdist-rust-bridge.hh index a03f8391e091..dac2511f5fed 100644 --- a/pdns/dnsdistdist/dnsdist-rust-bridge.hh +++ b/pdns/dnsdistdist/dnsdist-rust-bridge.hh @@ -32,6 +32,7 @@ struct DNSResponseActionWrapper struct ProtobufLoggerConfiguration; struct DnstapLoggerConfiguration; +struct OtlpLoggerConfiguration; struct KeyValueStoresConfiguration; struct MmdbConfiguration; struct NetmaskGroupConfiguration; @@ -39,6 +40,7 @@ struct TimedIpSetConfiguration; void registerProtobufLogger(const ProtobufLoggerConfiguration& config); void registerDnstapLogger(const DnstapLoggerConfiguration& config); +void registerOtlpLogger(const OtlpLoggerConfiguration& config); void registerKVSObjects(const KeyValueStoresConfiguration& config); void registerMMDBObjects(const ::rust::Vec& config); void registerNMGObjects(const ::rust::Vec& nmgs); diff --git a/pdns/dnsdistdist/dnsdist-rust-lib/rust-middle-in.rs b/pdns/dnsdistdist/dnsdist-rust-lib/rust-middle-in.rs index bdaaaf69a186..0ea5e5de2d3f 100644 --- a/pdns/dnsdistdist/dnsdist-rust-lib/rust-middle-in.rs +++ b/pdns/dnsdistdist/dnsdist-rust-lib/rust-middle-in.rs @@ -14,6 +14,7 @@ type DNSResponseActionWrapper; fn registerProtobufLogger(config: &ProtobufLoggerConfiguration); fn registerDnstapLogger(config: &DnstapLoggerConfiguration); + fn registerOtlpLogger(config: &OtlpLoggerConfiguration); fn registerKVSObjects(config: &KeyValueStoresConfiguration); fn registerMMDBObjects(config: &Vec); fn registerNMGObjects(nmgs: &Vec); diff --git a/pdns/dnsdistdist/dnsdist-rust-lib/rust-post-in.rs b/pdns/dnsdistdist/dnsdist-rust-lib/rust-post-in.rs index 5ab74482201e..dbbc1a88bd03 100644 --- a/pdns/dnsdistdist/dnsdist-rust-lib/rust-post-in.rs +++ b/pdns/dnsdistdist/dnsdist-rust-lib/rust-post-in.rs @@ -54,6 +54,9 @@ fn register_remote_loggers( for logger in &config.dnstap_loggers { dnsdistsettings::registerDnstapLogger(logger); } + for logger in &config.otlp_loggers { + dnsdistsettings::registerOtlpLogger(logger); + } } fn get_global_configuration_from_serde( diff --git a/pdns/dnsdistdist/dnsdist-settings-definitions.yml b/pdns/dnsdistdist/dnsdist-settings-definitions.yml index 1b7b8244c88d..d9a01e42c7a0 100644 --- a/pdns/dnsdistdist/dnsdist-settings-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-settings-definitions.yml @@ -203,6 +203,10 @@ remote_logging: type: "Vec" default: true description: "List of endpoints to send queries and/or responses data to, using the dnstap format" + - name: "otlp_loggers" + type: "Vec" + default: true + description: "List of endpoints to send OpenTelemetry Traces to, using OTLP" protobuf_logger: description: "Endpoint to send queries and/or responses data to, using the native PowerDNS format" @@ -279,6 +283,28 @@ dnstap_logger: default: 1 description: "Number of connections to open to the endpoint" +otlp_logger: + description: "List of endpoints to send OpenTelemetry Traces to, using OTLP." + parameters: + - name: "name" + type: "String" + description: "Name of this endpoint" + - name: "address" + type: "String" + description: "The URL of the endpoint. Must contain the scheme (http, https) and the path (like /v1/traces)" + - name: "interval" + type: "u32" + description: "Interval in seconds to wait before sending the next batch of OTLP messages" + default: 5 + - name: "queue_size" + type: "u32" + description: "Maximum number of traces in the backlog" + default: 500 + - name: "batch_size" + type: "u32" + description: "Maximum number of traces to send to the endpoint" + default: 100 + proto_buf_meta: description: "Meta-data entry to be added to a Protocol Buffer message" parameters: diff --git a/pdns/dnsdistdist/meson.build b/pdns/dnsdistdist/meson.build index 775fdfcea082..d3ea1bc0df46 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,7 +210,9 @@ 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-otlp.cc', src_dir / 'protozero-trace.cc', src_dir / 'proxy-protocol.cc', src_dir / 'qtype.cc', @@ -231,6 +234,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 +404,7 @@ deps = [ dep_ipcrypt2, dep_libcap, dep_libcrypto, + dep_libcurl, dep_gnutls, dep_libedit, dep_json11, @@ -577,6 +587,7 @@ test_sources += files( src_dir / 'test-iputils_hh.cc', src_dir / 'test-luawrapper.cc', src_dir / 'test-mplexer.cc', + src_dir / 'test-protozero-otlp_cc.cc', src_dir / 'test-proxy_protocol_cc.cc', src_dir / 'test-sholder_hh.cc', ) diff --git a/pdns/dnsdistdist/meson/libcurl/meson.build b/pdns/dnsdistdist/meson/libcurl/meson.build new file mode 100644 index 000000000000..cc8cd420ea5f --- /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 fbed8ada1ad5..7d8e85a15755 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 000000000000..3f48b8c54eea --- /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 000000000000..bba06c379e70 --- /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 000000000000..bfc5894bce00 --- /dev/null +++ b/pdns/dnsdistdist/otlp_logger.cc @@ -0,0 +1,174 @@ +/* + * 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 +#include + +#include "otlp_logger.hh" +#include "dnsdist-logging.hh" +#include "logging.hh" +#include "dolog.hh" +#include "logr.hh" +#include "remote_logger.hh" +#include "threadname.hh" + +#if !defined(DISABLE_PROTOBUF) && defined(HAVE_LIBCURL) +#include "minicurl.hh" +#include "protozero-trace.hh" +#include "protozero-otlp.hh" + +OTLPLogger::OTLPLogger(std::string address, const size_t interval, const size_t cacheSize, const size_t batchSize) : + d_address(std::move(address)), d_interval(interval), d_batchSize(batchSize) +{ + 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_miniCurl = std::make_unique("dnsdist/" + std::string(PACKAGE_VERSION)); + break; + case LoggerType::gRPC: + throw std::runtime_error("gRPC support is not implemented in the OTLP logger"); + } + + d_logger = dnsdist::logging::getTopLogger("OTLPLogger"); + d_traces.lock()->reserve(cacheSize); + d_thread = std::thread(&OTLPLogger::senderThread, this); +} + +// If we get some nice data, queue it +RemoteLoggerInterface::Result OTLPLogger::queueData(const TracesData& data) +{ + auto lockedTraces = d_traces.lock(); + + if (lockedTraces->size() >= lockedTraces->capacity()) { + d_queueFullDrops++; + return RemoteLoggerInterface::Result::PipeFull; + } + + lockedTraces->push_back(data); + return RemoteLoggerInterface::Result::Queued; +} + +// When we get bytes (or whatever), send as-is +RemoteLoggerInterface::Result OTLPLogger::queueData(const std::string& data) +{ + 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_miniCurl->postURL(d_address, data, d_httpHeaders); + 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; +} + +// Returns the number of traces sent, 0 on empty, -1 on error +// TODO: make it do size_t and throw otherwise +int OTLPLogger::sendBatch() +{ + auto lockedTraces = d_traces.lock(); + if (lockedTraces->empty()) { + return 0; + } + + pdns::trace::ExportTraceServiceRequest etsr; + + auto sentNum = lockedTraces->size() > d_batchSize ? d_batchSize : lockedTraces->size(); + etsr.resource_spans.reserve(sentNum); + + auto endIt = lockedTraces->begin() + sentNum; + auto it = lockedTraces->begin(); + while (it != endIt) { + etsr.resource_spans.insert(etsr.resource_spans.end(), it->resource_spans.begin(), it->resource_spans.end()); + it++; + } + + std::string buf; + protozero::pbf_writer writer{buf}; + etsr.encode(writer); + + switch (d_type) { + case LoggerType::HTTP: + if (queueHttpData(buf) == RemoteLoggerInterface::Result::Queued) { + d_framesSent += sentNum; + lockedTraces->erase(lockedTraces->begin(), endIt); + d_failedSends = 0; + return sentNum; + } + d_failedSends++; + if (d_failedSends >= 5) { + lockedTraces->erase(lockedTraces->begin(), endIt); + // Not really a queue full, but the best we got + d_queueFullDrops += sentNum; + d_failedSends = 0; + } + return -1; + case LoggerType::gRPC: + // Should never get here + throw std::runtime_error("gRPC support is not implemented"); + } +} + +void OTLPLogger::senderThread() +{ + // TODO: Support OTLP for recursor and auth + setThreadName("dnsdist/otlplog"); + + for (;;) { + try { + sendBatch(); + } + catch (const std::exception& exp) { + SLOG( + errlog("Unable to send traces to OTLP receiver %s: %s", d_address, exp.what()), + d_logger->error(Logr::Error, exp.what(), "Unable to send traces to OTLP receiver", "server.address", Logging::Loggable{d_address}, "failure-count", Logging::Loggable{d_failedSends})); + } + if (d_exiting) { + while (sendBatch() != 0) {} + return; + } + std::this_thread::sleep_for(std::chrono::seconds(d_interval)); + } +} +#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 index 000000000000..c73ae5fd2dff --- /dev/null +++ b/pdns/dnsdistdist/otlp_logger.hh @@ -0,0 +1,121 @@ +/* + * 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 "dnsdist-opentelemetry.hh" + +#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, const size_t interval = 5, const size_t cacheSize = 500, const size_t batchSize = 100); + OTLPLogger(const OTLPLogger&) = delete; + OTLPLogger(OTLPLogger&&) = delete; + OTLPLogger& operator=(const OTLPLogger&) = delete; + OTLPLogger& operator=(OTLPLogger&&) = delete; + ~OTLPLogger() override + { + d_exiting = true; + d_thread.join(); + }; + + [[nodiscard]] RemoteLoggerInterface::Result queueData(const std::string& data) override; + RemoteLoggerInterface::Result queueData(const 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 = d_framesSent, + .d_pipeFull = d_queueFullDrops, + .d_tooLarge = d_tooLargeCount, + .d_otherError = d_permanentFailures}; + } + +private: + LoggerType d_type; + std::string d_address; + size_t d_interval; + size_t d_batchSize; + + std::unique_ptr d_miniCurl{nullptr}; + MiniCurl::MiniCurlHeaders d_httpHeaders{{"Content-Type", "application/x-protobuf"}}; + + [[nodiscard]] RemoteLoggerInterface::Result queueHttpData(const std::string& data); + void senderThread(); + int sendBatch(); + + LockGuarded> d_traces; + size_t d_failedSends{0}; + + std::thread d_thread; + std::shared_ptr d_logger{nullptr}; + + std::atomic d_framesSent{0}; + std::atomic d_queueFullDrops{0}; + std::atomic d_tooLargeCount{0}; + std::atomic d_permanentFailures{0}; + + bool d_exiting{false}; +}; +#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.cc b/pdns/dnsdistdist/protozero-otlp.cc new file mode 100644 index 000000000000..c9488526b854 --- /dev/null +++ b/pdns/dnsdistdist/protozero-otlp.cc @@ -0,0 +1,87 @@ +#include "protozero-otlp.hh" +#include "protozero-trace.hh" +#include + +namespace pdns::trace +{ + +void ExportTraceServiceRequest::encode(protozero::pbf_writer& writer) const +{ + pdns::trace::encode(writer, 1, resource_spans); +} + +ExportTraceServiceRequest ExportTraceServiceRequest::decode(protozero::pbf_reader& reader) +{ + ExportTraceServiceRequest ret; + while (reader.next()) { + switch (reader.tag()) { + case 1: + protozero::pbf_reader sub = reader.get_message(); + ret.resource_spans.emplace_back(ResourceSpans::decode(sub)); + break; + } + } + return ret; +} + +bool ExportTraceServiceRequest::operator==(const ExportTraceServiceRequest& rhs) const +{ + return resource_spans == rhs.resource_spans; +} + +void ExportTracePartialSuccess::encode(protozero::pbf_writer& writer) const +{ + pdns::trace::encode(writer, 1, rejected_spans); + pdns::trace::encode(writer, 2, error_message); +} + +ExportTracePartialSuccess ExportTracePartialSuccess::decode(protozero::pbf_reader& reader) +{ + ExportTracePartialSuccess ret{ + .rejected_spans = 0, + .error_message = "", + }; + + while (reader.next()) { + switch (reader.tag()) { + case 1: + ret.rejected_spans = reader.get_int64(); + break; + case 2: + ret.error_message = reader.get_string(); + break; + } + } + return ret; +} + +bool ExportTracePartialSuccess::operator==(const ExportTracePartialSuccess& rhs) const +{ + return (rejected_spans == rhs.rejected_spans && error_message == rhs.error_message); +} + +void ExportTraceServiceResponse::encode(protozero::pbf_writer& writer) const +{ + protozero::pbf_writer sub{writer, 1}; + partial_success.encode(sub); +} + +ExportTraceServiceResponse 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); + break; + } + } + return ret; +} + +bool ExportTraceServiceResponse::operator==(const ExportTraceServiceResponse& rhs) const +{ + return partial_success == rhs.partial_success; +} +} diff --git a/pdns/dnsdistdist/protozero-otlp.hh b/pdns/dnsdistdist/protozero-otlp.hh new file mode 100644 index 000000000000..80db919838b7 --- /dev/null +++ b/pdns/dnsdistdist/protozero-otlp.hh @@ -0,0 +1,64 @@ +/* + * 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; + static ExportTraceServiceRequest decode(protozero::pbf_reader& reader); + bool operator==(const ExportTraceServiceRequest& rhs) const; +}; + +struct ExportTracePartialSuccess +{ + int64_t rejected_spans{0}; // = 1 + std::string error_message{""}; // = 2 + + void encode(protozero::pbf_writer& writer) const; + static ExportTracePartialSuccess decode(protozero::pbf_reader& reader); + bool operator==(const ExportTracePartialSuccess& rhs) const; +}; + +struct ExportTraceServiceResponse +{ + ExportTracePartialSuccess partial_success; // = 1 + + void encode(protozero::pbf_writer& writer) const; + static ExportTraceServiceResponse decode(protozero::pbf_reader& reader); + bool operator==(const ExportTraceServiceResponse& rhs) const; +}; + +} // namespace pdns::trace diff --git a/pdns/dnsdistdist/test-protozero-otlp_cc.cc b/pdns/dnsdistdist/test-protozero-otlp_cc.cc new file mode 100644 index 000000000000..70e352b9120d --- /dev/null +++ b/pdns/dnsdistdist/test-protozero-otlp_cc.cc @@ -0,0 +1,77 @@ +/* + * 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 "misc.hh" +#include "protozero-trace.hh" +#include +#include +#include + +#ifndef DISABLE_PROTOBUF + +#ifndef BOOST_TEST_DYN_LINK +#define BOOST_TEST_DYN_LINK +#endif + +#include "protozero-otlp.hh" +#define BOOST_TEST_NO_MAIN + +BOOST_AUTO_TEST_SUITE(protozeroOtlp) +BOOST_AUTO_TEST_CASE(ExportTraceServiceRequest) +{ + std::string buf; + protozero::pbf_writer writer{buf}; + + pdns::trace::ExportTraceServiceRequest etsr{ + .resource_spans = { + {.resource = { + .attributes = {{pdns::trace::KeyValue{.key = "foo", .value = {"bar"}}}}}, + .scope_spans = {{.scope = {.name = "test"}}}}}}; + + etsr.encode(writer); + BOOST_CHECK_EQUAL(buf, "\x0a\x1a\x0a\x0e\x0a\x0c\x0a\x03\x66\x6f\x6f\x12\x05\x0a\x03\x62\x61\x72\x12\x08\x0a\x06\x0a\x04\x74\x65\x73\x74"); + + protozero::pbf_reader reader{buf}; + pdns::trace::ExportTraceServiceRequest outEtsr = pdns::trace::ExportTraceServiceRequest::decode(reader); + BOOST_CHECK(outEtsr == etsr); +} + +BOOST_AUTO_TEST_CASE(ExportTraceServiceResponse) +{ + std::string buf; + protozero::pbf_writer writer{buf}; + + pdns::trace::ExportTraceServiceResponse etsr; + etsr.partial_success = { + 2, + "I'm an error message!", + }; + etsr.encode(writer); + + BOOST_CHECK_EQUAL(buf, "\x0a\x19\x08\x02\x12\x15\x49\x27\x6d\x20\x61\x6e\x20\x65\x72\x72\x6f\x72\x20\x6d\x65\x73\x73\x61\x67\x65\x21"); + + protozero::pbf_reader reader{buf}; + pdns::trace::ExportTraceServiceResponse outEtsr = pdns::trace::ExportTraceServiceResponse::decode(reader); + BOOST_CHECK(outEtsr == etsr); +} +BOOST_AUTO_TEST_SUITE_END() +#endif // DISABLE_PROTOBUF diff --git a/pdns/protozero-trace.cc b/pdns/protozero-trace.cc index 132f1d1e7859..53f29bef349f 100644 --- a/pdns/protozero-trace.cc +++ b/pdns/protozero-trace.cc @@ -179,6 +179,11 @@ EntityRef EntityRef::decode(protozero::pbf_reader& reader) return ret; } +bool EntityRef::operator==(const EntityRef& rhs) const +{ + return (schema_url == rhs.schema_url && type == rhs.type && id_keys == rhs.id_keys && description_keys == rhs.description_keys); +} + void KeyValue::encode(protozero::pbf_writer& writer) const { pdns::trace::encode(writer, 1, key); @@ -220,6 +225,11 @@ Resource Resource::decode(protozero::pbf_reader& reader) return ret; } +bool Resource::operator==(const Resource& rhs) const +{ + return (attributes == rhs.attributes && dropped_attributes_count == rhs.dropped_attributes_count && entity_refs == rhs.entity_refs); +} + void InstrumentationScope::encode(protozero::pbf_writer& writer) const { pdns::trace::encode(writer, 1, name); @@ -254,6 +264,11 @@ InstrumentationScope InstrumentationScope::decode(protozero::pbf_reader& reader) return ret; } +bool InstrumentationScope::operator==(const InstrumentationScope& rhs) const +{ + return (name == rhs.name && version == rhs.version && attributes == rhs.attributes && dropped_attributes_count == rhs.dropped_attributes_count); +} + void Status::encode(protozero::pbf_writer& writer) const { pdns::trace::encode(writer, 2, message); @@ -278,6 +293,11 @@ Status Status::decode(protozero::pbf_reader& reader) return ret; } +bool Status::operator==(const Status& rhs) const +{ + return (message == rhs.message && code == rhs.code); +} + void Span::Event::encode(protozero::pbf_writer& writer) const { pdns::trace::encodeFixed(writer, 1, time_unix_nano); @@ -311,6 +331,11 @@ Span::Event Span::Event::decode(protozero::pbf_reader& reader) return ret; } +bool Span::Event::operator==(const Span::Event& rhs) const +{ + return (time_unix_nano == rhs.time_unix_nano && name == rhs.name && attributes == rhs.attributes && dropped_attributes_count == rhs.dropped_attributes_count); +} + void Span::Link::encode(protozero::pbf_writer& writer) const { pdns::trace::encode(writer, 1, trace_id); @@ -352,6 +377,11 @@ Span::Link Span::Link::decode(protozero::pbf_reader& reader) return ret; } +bool Span::Link::operator==(const Span::Link& rhs) const +{ + return (trace_id == rhs.trace_id && span_id == rhs.span_id && trace_state == rhs.trace_state && attributes == rhs.attributes && dropped_attributes_count == rhs.dropped_attributes_count && flags == rhs.flags); +} + void Span::encode(protozero::pbf_writer& writer) const { pdns::trace::encode(writer, 1, trace_id); @@ -443,6 +473,11 @@ Span Span::decode(protozero::pbf_reader& reader) return ret; } +bool Span::operator==(const Span& rhs) const +{ + return (trace_id == rhs.trace_id && span_id == rhs.span_id && trace_state == rhs.trace_state && parent_span_id == rhs.parent_span_id && name == rhs.name && kind == rhs.kind && start_time_unix_nano == rhs.start_time_unix_nano && end_time_unix_nano == rhs.end_time_unix_nano && attributes == rhs.attributes && dropped_attributes_count == rhs.dropped_attributes_count && events == rhs.events && dropped_events_count == rhs.dropped_events_count && links == rhs.links && dropped_links_count == rhs.dropped_links_count && status == rhs.status && flags == rhs.flags); +} + void ScopeSpans::encode(protozero::pbf_writer& writer) const { { @@ -477,6 +512,11 @@ ScopeSpans ScopeSpans::decode(protozero::pbf_reader& reader) return ret; } +bool ScopeSpans::operator==(const ScopeSpans& rhs) const +{ + return (scope == rhs.scope && spans == rhs.spans && schema_url == rhs.schema_url); +} + void ResourceSpans::encode(protozero::pbf_writer& writer) const { { @@ -511,6 +551,11 @@ ResourceSpans ResourceSpans::decode(protozero::pbf_reader& reader) return ret; } +bool ResourceSpans::operator==(const ResourceSpans& rhs) const +{ + return (resource == rhs.resource && scope_spans == rhs.scope_spans && schema_url == rhs.schema_url); +} + void TracesData::encode(protozero::pbf_writer& writer) const { pdns::trace::encode(writer, 1, resource_spans); diff --git a/pdns/protozero-trace.hh b/pdns/protozero-trace.hh index 10d16896c49d..fd9621a33538 100644 --- a/pdns/protozero-trace.hh +++ b/pdns/protozero-trace.hh @@ -187,6 +187,7 @@ struct EntityRef void encode(protozero::pbf_writer& writer) const; static EntityRef decode(protozero::pbf_reader& reader); + bool operator==(const EntityRef& rhs) const; }; struct KeyValue @@ -207,6 +208,7 @@ struct Resource void encode(protozero::pbf_writer& writer) const; static Resource decode(protozero::pbf_reader& reader); + bool operator==(const Resource& rhs) const; }; struct InstrumentationScope @@ -218,6 +220,7 @@ struct InstrumentationScope void encode(protozero::pbf_writer& writer) const; static InstrumentationScope decode(protozero::pbf_reader& reader); + bool operator==(const InstrumentationScope& rhs) const; }; struct TraceID : public std::array @@ -374,6 +377,7 @@ struct Status } void encode(protozero::pbf_writer& writer) const; static Status decode(protozero::pbf_reader& reader); + bool operator==(const Status& rhs) const; }; inline uint64_t timestamp() @@ -527,6 +531,7 @@ struct Span void encode(protozero::pbf_writer& writer) const; static Event decode(protozero::pbf_reader& reader); + bool operator==(const Span::Event& rhs) const; }; // events is a collection of Event items. std::vector events{}; // = 11 @@ -575,6 +580,7 @@ struct Span void encode(protozero::pbf_writer& writer) const; static Link decode(protozero::pbf_reader& reader); + bool operator==(const Span::Link& rhs) const; }; std::vector links{}; // = 13 uint32_t dropped_links_count{0}; // = 14 @@ -629,6 +635,7 @@ struct Span } void encode(protozero::pbf_writer& writer) const; static Span decode(protozero::pbf_reader& reader); + bool operator==(const Span& rhs) const; }; // SpanFlags represents constants used to interpret the @@ -684,6 +691,7 @@ struct ScopeSpans } void encode(protozero::pbf_writer& writer) const; static ScopeSpans decode(protozero::pbf_reader& reader); + bool operator==(const ScopeSpans& rhs) const; }; // A collection of ScopeSpans from a Resource. @@ -710,6 +718,7 @@ struct ResourceSpans } void encode(protozero::pbf_writer& writer) const; static ResourceSpans decode(protozero::pbf_reader& reader); + bool operator==(const ResourceSpans& rhs) const; }; // TracesData represents the traces data that can be stored in a persistent storage, diff --git a/regression-tests.dnsdist/test_OTLP.py b/regression-tests.dnsdist/test_OTLP.py new file mode 100644 index 000000000000..be5569c30d35 --- /dev/null +++ b/regression-tests.dnsdist/test_OTLP.py @@ -0,0 +1,192 @@ +import http.server +import threading +import time + +import dns.message +import dns.rdataclass +import dns.rdatatype +import dns.rrset +import google.protobuf.json_format +import opentelemetry.proto.collector.trace.v1.trace_service_pb2 + +from dnsdisttests import DNSDistTest, Queue, pickAvailablePort + + +class OTLPRequestHandler(http.server.BaseHTTPRequestHandler): + queue = Queue() + + def __init__(self, queue: Queue, *args): + self.queue = queue + super().__init__(*args) + + def do_POST(self): + if self.path != "/v1/traces": + self.send_error(http.HTTPStatus.NOT_FOUND) + return + + ctypeHdr = self.headers.get("Content-Type") + if ctypeHdr is None or ctypeHdr != "application/x-protobuf": + self.send_error(http.HTTPStatus.BAD_REQUEST, "I need protobuf") + return + + content_length = int(self.headers.get("Content-Length", 0)) + + if content_length == 0: + self.send_error(http.HTTPStatus.BAD_REQUEST, "No data in request") + + body = self.rfile.read(content_length) + + messages = opentelemetry.proto.collector.trace.v1.trace_service_pb2.ExportTraceServiceRequest() + messages.ParseFromString(body) + + # Let the client know we succesfully received their traces + response = opentelemetry.proto.collector.trace.v1.trace_service_pb2.ExportTraceServiceResponse() + response.partial_success.rejected_spans = 0 + self.send_response(http.HTTPStatus.OK) + self.end_headers() + self.wfile.write(response.SerializeToString()) + + for span in messages.resource_spans: + rs_data = google.protobuf.json_format.MessageToDict(span, preserving_proto_field_name=True) + print(rs_data) + self.queue.put(rs_data, True, timeout=2.0) + + +class OTLPServer: + def __init__(self, port, queue) -> None: + def handler(*args): + return OTLPRequestHandler(queue, *args) + + server = http.server.HTTPServer(("127.0.0.1", port), handler) + server.serve_forever() + + +class DNSDistOtlpTest(DNSDistTest): + _otlpServerPort = pickAvailablePort() + _otlpQueue = Queue() + _otlpServer = None + + @classmethod + def startResponders(cls): + cls._UDPResponder = threading.Thread( + name="UDP Responder", + target=cls.UDPResponder, + args=[cls._testServerPort, cls._toResponderQueue, cls._fromResponderQueue], + ) + cls._UDPResponder.daemon = True + cls._UDPResponder.start() + + cls._TCPResponder = threading.Thread( + name="TCP Responder", + target=cls.TCPResponder, + args=[cls._testServerPort, cls._toResponderQueue, cls._fromResponderQueue], + ) + cls._TCPResponder.daemon = True + cls._TCPResponder.start() + + cls._otlpServer = threading.Thread( + name="OTLP server", target=OTLPServer, args=[cls._otlpServerPort, cls._otlpQueue] + ) + cls._otlpServer.daemon = True + cls._otlpServer.start() + + def getFirstResourceSpan(self, timeout=None): + if timeout is not None: + self.waitUntilQueueIsNoLongerEmpty(timeout) + self.assertFalse(self._otlpQueue.empty()) + data = self._otlpQueue.get(False) + self.assertTrue(data) + return data + + def waitUntilQueueIsNoLongerEmpty(self, timeout=1): + remaining = timeout * 1000 # milliseconds + while self._otlpQueue.empty() and remaining > 0: + time.sleep(0.01) + remaining -= 10 + + def sendQueryAndGetResourceSpan(self, useTCP=False): + name = "query.ot.tests.powerdns.com." + + target = "target.ot.tests.powerdns.com." + query = dns.message.make_query(name, "A", "IN") + response = dns.message.make_response(query) + + rrset = dns.rrset.from_text(name, 3600, dns.rdataclass.IN, dns.rdatatype.CNAME, target) + response.answer.append(rrset) + + rrset = dns.rrset.from_text(target, 3600, dns.rdataclass.IN, dns.rdatatype.A, "127.0.0.1") + response.answer.append(rrset) + + if useTCP: + self.sendTCPQuery(query, response) + else: + self.sendUDPQuery(query, response) + + # check the protobuf message corresponding to the UDP query + return self.getFirstResourceSpan(timeout=2) + + +class TestDNSDistOtlpYAML(DNSDistOtlpTest): + # We're not testing the _content_ of the traces, as those are tested in test_OpenTelemetryTracing.py. + # These tests are mostly to verify that the OTLP exporter sends us proper data + + _yaml_config_params = [ + "_testServerPort", + "_otlpServerPort", + ] + _yaml_config_template = """--- +logging: + open_telemetry_tracing: + enabled: true + +backends: + - address: 127.0.0.1:%d + protocol: Do53 + health_checks: + mode: up + +remote_logging: + otlp_loggers: + - name: otlplog + address: http://127.0.0.1:%d/v1/traces + interval: 1 + +query_rules: + - name: Enable tracing + selector: + type: All + action: + type: SetTrace + value: true + remote_loggers: + - otlplog +""" + + def testUDP(self): + print(self._otlpServerPort) + self.sendQueryAndGetResourceSpan() + + def testTCP(self): + print(self._otlpServerPort) + self.sendQueryAndGetResourceSpan(useTCP=True) + + +class TestDNSDistOtlpLua(TestDNSDistOtlpYAML): + # We're not testing the _content_ of the traces, as those are tested in test_OpenTelemetryTracing.py. + # These tests are mostly to verify that the OTLP exporter sends us proper data + + _otlpServerPort = pickAvailablePort() + _yaml_config_params = None + _yaml_config_template = None + _config_params = [ + "_testServerPort", + "_otlpServerPort", + ] + _config_template = """ +setOpenTelemetryTracing(true) +newServer{address="127.0.0.1:%d"} +getServer(0):setUp() + +otlpLogger = newOtlpLogger('http://127.0.0.1:%d/v1/traces', {interval=1}) +addAction(AllRule(), SetTraceAction(true, {remoteLoggers={otlpLogger}}), {name="Enable tracing"}) +""" diff --git a/tasks.py b/tasks.py index 4268fd47bf35..afaea4035c1a 100644 --- a/tasks.py +++ b/tasks.py @@ -85,6 +85,7 @@ "libcap-dev", "catch2", "libcdb-dev", + "libcurl4", "libedit-dev", "libfstrm-dev", "libgnutls28-dev", @@ -999,6 +1000,7 @@ def ci_dnsdist_configure(c, features, build_dir, benchmark=False): "-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 @@ def ci_dnsdist_configure(c, features, build_dir, benchmark=False): "-D reproducible=true", "-D snmp=enabled", "-D yaml=enabled", + "-D otlp=enabled", ] ) @@ -1025,6 +1028,7 @@ def ci_dnsdist_configure(c, features, build_dir, benchmark=False): "-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 @@ def ci_dnsdist_configure(c, features, build_dir, benchmark=False): "-D reproducible=false", "-D snmp=disabled", "-D yaml=disabled", + "-D otlp=disabled", ] )