]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
feat(dnsdist): Allow passing RemoteLoggers to SetTrace action
authorPieter Lexis <pieter.lexis@powerdns.com>
Wed, 26 Nov 2025 13:38:02 +0000 (14:38 +0100)
committerPieter Lexis <pieter.lexis@powerdns.com>
Fri, 12 Dec 2025 15:30:51 +0000 (16:30 +0100)
pdns/dnsdistdist/dnsdist-actions-definitions.yml
pdns/dnsdistdist/dnsdist-actions-factory.cc
pdns/dnsdistdist/dnsdist-actions-factory.hh
pdns/dnsdistdist/dnsdist-configuration-yaml.cc
pdns/dnsdistdist/dnsdist-idstate.cc
pdns/dnsdistdist/dnsdist-idstate.hh
pdns/dnsdistdist/dnsdist-lua-actions.cc
pdns/dnsdistdist/docs/reference/ottrace.rst
regression-tests.dnsdist/test_OpenTelemetryTracing.py

index a9ea1c3ca690fa807978a6c177bbb926ae74fd92..e80e3f990f0001d522cfa778232d22e5290a19d5 100644 (file)
@@ -402,10 +402,18 @@ are processed after this action"
       description: "The TTL to use"
 - name: "SetTrace"
   description: "Enable or disable OpenTelemetry tracing for this query. Don't forget to also use a RemoteLog action to actually send the trace. Subsequent rules are processed after this action"
+  skip-cpp: true
+  skip-rust: true
   parameters:
     - name: "value"
       type: "bool"
       description: "Whether or not to enable tracing"
+    - name: remote_loggers
+      type: "Vec<String>"
+      default: ""
+      description: |
+        The names of all the loggers that should be sent the protobuf messages that includes the tracing information.
+        This message will be sent once dnsdist is finished with the DNS transaction (i.e. the response has been sent, the query was dropped, or when a timeout occured).
     - name: "use_incoming_traceid"
       type: "bool"
       default: "false"
index 1bd2a4ed42e655d49cf1b9be47cccfb8d7dd8f07..ba3245acb0b675b4c02d7c640ac226f6cadbc657 100644 (file)
@@ -23,6 +23,7 @@
 #include <optional>
 #include <unordered_map>
 #include <utility>
+#include <vector>
 
 #include "dnsdist-actions-factory.hh"
 
@@ -1680,8 +1681,8 @@ private:
 class SetTraceAction : public DNSAction
 {
 public:
-  SetTraceAction(bool value, std::optional<bool> useIncomingTraceID, std::optional<short unsigned int> incomingTraceIDOptionCode) :
-    d_value{value}, d_useIncomingTraceID(useIncomingTraceID), d_incomingTraceIDOptionCode(incomingTraceIDOptionCode) {};
+  SetTraceAction(SetTraceActionConfiguration& config) :
+    d_value{config.value}, d_loggers(config.remote_loggers), d_useIncomingTraceID(config.use_incoming_traceid), d_incomingTraceIDOptionCode(config.trace_edns_option) {};
 
   DNSAction::Action operator()([[maybe_unused]] DNSQuestion* dnsquestion, [[maybe_unused]] std::string* ruleresult) const override
   {
@@ -1716,6 +1717,7 @@ public:
           }
         }
       }
+      dnsquestion->ids.ottraceLoggers = d_loggers;
     }
 #endif
     return Action::None;
@@ -1728,6 +1730,9 @@ public:
 
 private:
   bool d_value;
+
+  std::vector<std::shared_ptr<RemoteLoggerInterface>> d_loggers;
+
   std::optional<bool> d_useIncomingTraceID;
   std::optional<short unsigned int> d_incomingTraceIDOptionCode;
 };
@@ -2506,6 +2511,11 @@ std::shared_ptr<DNSResponseAction> getLuaFFIResponseAction(dnsdist::actions::Lua
 }
 
 #ifndef DISABLE_PROTOBUF
+std::shared_ptr<DNSAction> getSetTraceAction(SetTraceActionConfiguration& config)
+{
+  return std::shared_ptr<DNSAction>(new SetTraceAction(config));
+}
+
 std::shared_ptr<DNSAction> getRemoteLogAction(RemoteLogActionConfiguration& config)
 {
   return std::shared_ptr<DNSAction>(new RemoteLogAction(config));
index 9c0a441affe9314777f39ccd9cd30d3c68d26122..041d9c7fe1e94b7bf6ec9757f6d2c4b6772ca498 100644 (file)
@@ -122,5 +122,14 @@ std::shared_ptr<DNSAction> getRemoteLogAction(RemoteLogActionConfiguration& conf
 std::shared_ptr<DNSResponseAction> getRemoteLogResponseAction(RemoteLogActionConfiguration& config);
 std::shared_ptr<DNSAction> getDnstapLogAction(const std::string& identity, std::shared_ptr<RemoteLoggerInterface> logger, std::optional<DnstapAlterFunction> alterFunc);
 std::shared_ptr<DNSResponseAction> getDnstapLogResponseAction(const std::string& identity, std::shared_ptr<RemoteLoggerInterface> logger, std::optional<DnstapAlterResponseFunction> alterFunc);
+
+struct SetTraceActionConfiguration
+{
+  bool value = false;
+  std::vector<std::shared_ptr<RemoteLoggerInterface>> remote_loggers;
+  bool use_incoming_traceid = false;
+  std::uint16_t trace_edns_option = 0;
+};
+std::shared_ptr<DNSAction> getSetTraceAction(SetTraceActionConfiguration& config);
 #endif /* DISABLE_PROTOBUF */
 }
index 112de6227cda078d292c75c78e5b4413159b52ae..b156353254a1b2c644da4ca0e1a02aad63a1dd14 100644 (file)
@@ -19,7 +19,9 @@
  * 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 <vector>
 
 #include "dnsdist-configuration-yaml.hh"
 
@@ -1687,6 +1689,33 @@ std::shared_ptr<DNSResponseActionWrapper> getDnstapLogResponseAction([[maybe_unu
 #endif
 }
 
+std::shared_ptr<DNSActionWrapper> getSetTraceAction(const SetTraceActionConfiguration& config)
+{
+#if defined(DISABLE_PROTOBUF)
+  throw std::runtime_error("Unable to create set trace action: protobuf support is disabled");
+#else
+  std::vector<std::shared_ptr<RemoteLoggerInterface>> loggers;
+  for (auto const& logger_name : config.remote_loggers) {
+    auto logger = dnsdist::configuration::yaml::getRegisteredTypeByName<RemoteLoggerInterface>(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) + "'");
+    }
+    loggers.push_back(logger);
+  }
+
+  dnsdist::actions::SetTraceActionConfiguration actionConfig{
+    .value = config.value,
+    .remote_loggers = std::move(loggers),
+    .use_incoming_traceid = config.use_incoming_traceid,
+    .trace_edns_option = config.trace_edns_option,
+  };
+
+  auto action = dnsdist::actions::getSetTraceAction(actionConfig);
+
+  return newDNSActionWrapper(std::move(action), config.name);
+#endif
+}
+
 std::shared_ptr<DNSActionWrapper> getRemoteLogAction(const RemoteLogActionConfiguration& config)
 {
 #if defined(DISABLE_PROTOBUF)
index efb2c0af37a8ac54207acffe8d25d1a7e2a514dc..37fb3a8e1db33288a4b9a303030d558ddca138e4 100644 (file)
 
 #include "dnsdist-idstate.hh"
 #include "dnsdist-doh-common.hh"
+#include "dnsdist-protobuf.hh"
 #include "doh3.hh"
 #include "doq.hh"
+#include "protozero.hh"
 #include <string>
 #include <string_view>
 
@@ -63,11 +65,15 @@ InternalQueryState InternalQueryState::partialCloneForXFR() const
 InternalQueryState::~InternalQueryState()
 {
 #ifndef DISABLE_PROTOBUF
+
   static thread_local string otPBBuf;
   otPBBuf.clear();
+  std::string OTData;
   if (tracingEnabled && d_OTTracer != nullptr) {
+
     pdns::ProtoZero::Message msg{otPBBuf};
-    msg.setOpenTelemetryData(d_OTTracer->getOTProtobuf());
+    OTData = d_OTTracer->getOTProtobuf();
+    msg.setOpenTelemetryData(OTData);
   }
 
   for (auto const& msg_logger : delayedResponseMsgs) {
@@ -80,6 +86,15 @@ InternalQueryState::~InternalQueryState()
     // that only contains the OTTrace data as a single bytes field
     msg_logger.second->queueData(msg_logger.first + otPBBuf);
   }
+
+  static thread_local string minimalPBBuf;
+  minimalPBBuf.clear();
+  pdns::ProtoZero::Message minimalMsg{minimalPBBuf};
+  minimalMsg.setType(pdns::ProtoZero::Message::MessageType::DNSQueryType);
+  minimalMsg.setOpenTelemetryData(OTData);
+  for (auto const& msg_logger : ottraceLoggers) {
+    msg_logger->queueData(minimalPBBuf);
+  }
 #endif
 }
 
index 7259f96f6a0da09965cc497a40aa4678cee61ef4..fec10cefdb276edbdbcfa8c1930b233106791a1f 100644 (file)
@@ -201,6 +201,7 @@ public:
   std::unique_ptr<ProtoBufData> d_protoBufData{nullptr};
 #ifndef DISABLE_PROTOBUF
   std::vector<std::pair<std::string, std::shared_ptr<RemoteLoggerInterface>>> delayedResponseMsgs;
+  std::vector<std::shared_ptr<RemoteLoggerInterface>> ottraceLoggers;
 #endif
   std::unique_ptr<EDNSExtendedError> d_extendedError{nullptr};
   std::optional<uint32_t> tempFailureTTL{std::nullopt}; // 8
index 73a2ed4d23c1bc03b1c43a66b3c4be9dc89235cf..3aeb391743120e10ba99bcbe5542792c30dbdb3e 100644 (file)
 #include "dnsdist-rule-chains.hh"
 #include "dnstap.hh"
 #include "remote_logger.hh"
+#include <memory>
+#include <optional>
 #include <stdexcept>
+#include <vector>
 
 using responseParams_t = std::unordered_map<std::string, boost::variant<bool, uint32_t>>;
 
@@ -235,6 +238,31 @@ void setupLuaActions(LuaContext& luaCtx)
   });
 
 #ifndef DISABLE_PROTOBUF
+  luaCtx.writeFunction("SetTraceAction", [](bool value, std::optional<LuaAssociativeTable<std::shared_ptr<RemoteLoggerInterface>>> remote_loggers, std::optional<bool> use_incoming_traceid, std::optional<uint16_t> trace_edns_option) {
+    dnsdist::actions::SetTraceActionConfiguration config;
+
+    if (remote_loggers) {
+      std::vector<std::shared_ptr<RemoteLoggerInterface>> loggers;
+      for (auto& remote_logger : remote_loggers.value()) {
+        if (remote_logger.second != nullptr) {
+          // avoids potentially-evaluated-expression warning with clang.
+          RemoteLoggerInterface& remoteLoggerRef = *remote_logger.second;
+          if (typeid(remoteLoggerRef) != typeid(RemoteLogger)) {
+            // 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."));
+          }
+          loggers.push_back(remote_logger.second);
+        }
+      }
+      config.remote_loggers = loggers;
+    }
+    config.value = value;
+    config.trace_edns_option = trace_edns_option.value_or(65500);
+    config.use_incoming_traceid = use_incoming_traceid.value_or(false);
+
+    return dnsdist::actions::getSetTraceAction(config);
+  });
+
   // Used for both RemoteLogAction and RemoteLogResponseAction
   static const std::array<std::string, 2> s_validIpEncryptMethods = {"legacy", "ipcrypt-pfx"};
 
index 61a00f0e562747ac192e37038f6b01adf9c1446c..9424c0b5a19f4c24cf10f2443fe0fe06f7c4c30b 100644 (file)
@@ -15,16 +15,15 @@ Per-query tracing can be enabled using the :func:`SetTraceAction`. However, :pro
 When tracing is enabled in the query, :program:`dnsdist` stores start and end times of certain (but not all) functions that are called during the lifetime of the query and the response.
 It is recommended to send the traces out through a RemoteLogger in ResponseRules, to capture as much information as possible.
 
-.. note::
-   At the moment, the ProtoBuf message is sent out **during** the processing of the response rules.
-   Hence, the traces are not complete.
-   There are plans to remedy this, but no timeline to do so.
-
 Tracing uses more memory and CPU than usual query processing and it is recommended to enable tracing only for certain queries using specific :doc:`selectors <selectors>`.
 
 Example configuration
 =====================
 
+In this configuration, the RemoteLogger is passed directly to the ``SetTrace`` action.
+Doing this ensures that no matter what happens with the query (timeout, self-answered, cache-hit, dropped, answered by the backend), the trace will be sent out.
+When sending the trace in this way, the Protobuf message is essentially empty apart from the OpenTelemetry Trace.
+
 .. code-block:: yaml
 
    logging:
@@ -41,18 +40,39 @@ Example configuration
        action:
          type: SetTrace
          value: true
-   response_rules:
-     - name: Do PB logging
+         remote_loggers:
+           - pblog
+
+Should you only want to receive the trace, including a fully filled Protobuf message, a `RemoteLog` can be used:
+
+.. code-block:: yaml
+
+   logging:
+     open_telemetry_tracing: true
+   remote_logging:
+     protobuf_loggers:
+       - name: pblog
+         address: 127.0.0.1:5301
+   query_rules:
+     - name: Enable tracing
        selector:
+         # Just as an example, in production don't trace all the queries
          type: All
        action:
-         type: RemoteLog
-         logger_name: pblog
-         # Delay ensures that the PB message is sent
-         # after the response is sent to client, instead
-         # of immediately. This ensures all Trace Spans
-         # have proper end timestamps.
-         delay: true
+         type: SetTrace
+         value: true
+    response_rules:
+      - name: Send PB log
+        selector:
+          type: All
+        action:
+          type: RemoteLog
+          logger_name: pblog
+          # Delay ensures that the PB message is sent
+          # after the response is sent to client, instead
+          # of immediately. This ensures all Trace Spans
+          # have proper end timestamps.
+          delay: true
 
 Passing Trace ID and Span ID to downstream servers
 ==================================================
index 50a8aea89d936f9b3db208c2d440aa686da009b1..8a7837261765bba05c5139983514a32be8a8c8b6 100644 (file)
@@ -21,7 +21,13 @@ class DNSDistOpenTelemetryProtobufTest(test_Protobuf.DNSDistProtobufTest):
         self.assertTrue(msg.HasField("openTelemetry"))
 
     def sendQueryAndGetProtobuf(
-        self, useTCP=False, traceID="", spanID="", ednsTraceIDOpt=65500
+        self,
+        useTCP=False,
+        traceID="",
+        spanID="",
+        ednsTraceIDOpt=65500,
+        dropped=False,
+        querySentByDNSDist=True,
     ):
         name = "query.ot.tests.powerdns.com."
 
@@ -54,11 +60,14 @@ class DNSDistOpenTelemetryProtobufTest(test_Protobuf.DNSDistProtobufTest):
         else:
             (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
 
-        self.assertTrue(receivedQuery)
-        self.assertTrue(receivedResponse)
-        receivedQuery.id = query.id
-        self.assertEqual(query, receivedQuery)
-        self.assertEqual(response, receivedResponse)
+        if querySentByDNSDist:
+            self.assertTrue(receivedQuery)
+            receivedQuery.id = query.id
+            self.assertEqual(query, receivedQuery)
+
+        if not dropped:
+            self.assertTrue(receivedResponse)
+            self.assertEqual(response, receivedResponse)
 
         if self._protobufQueue.empty():
             # let the protobuf messages the time to get there
@@ -67,36 +76,28 @@ class DNSDistOpenTelemetryProtobufTest(test_Protobuf.DNSDistProtobufTest):
         # check the protobuf message corresponding to the UDP query
         return self.getFirstProtobufMessage()
 
-
-class DNSDistOpenTelemetryProtobufBaseTest(DNSDistOpenTelemetryProtobufTest):
-    def doTest(self, wasDelayed=False, useTCP=False, traceID="", spanID=""):
-        msg = self.sendQueryAndGetProtobuf(useTCP, traceID, spanID)
-
-        self.assertTrue(msg.HasField("openTelemetryTraceID"))
-        self.assertNotEqual(msg.openTelemetryTraceID, "")
-
-        if traceID != "":
-            self.assertEqual(msg.openTelemetryTraceID, binascii.a2b_hex(traceID))
-
-        self.assertTrue(msg.HasField("openTelemetryData"))
-        traces_data = opentelemetry.proto.trace.v1.trace_pb2.TracesData()
-        traces_data.ParseFromString(msg.openTelemetryData)
-        ot_data = google.protobuf.json_format.MessageToDict(
-            traces_data, preserving_proto_field_name=True
-        )
-
-        self.assertEqual(len(ot_data["resource_spans"]), 1)
-        self.assertEqual(len(ot_data["resource_spans"][0]["resource"]["attributes"]), 1)
+    def checkOTData(
+        self,
+        otData,
+        hasProcessResponseAfterRules=False,
+        useTCP=False,
+        hasRemoteLogResponseAction=True,
+        hasSelectBackendForOutgoingQuery=True,
+        hasResponse=True,
+        extraFunctions=set(),
+    ):
+        self.assertEqual(len(otData["resource_spans"]), 1)
+        self.assertEqual(len(otData["resource_spans"][0]["resource"]["attributes"]), 1)
 
         # Ensure all attributes exist
-        for field in ot_data["resource_spans"][0]["resource"]["attributes"]:
+        for field in otData["resource_spans"][0]["resource"]["attributes"]:
             self.assertIn(field["key"], ["service.name"])
 
         # Ensure the values are correct
         # TODO: query.remote with port
         msg_scope_attr_keys = [
             v["key"]
-            for v in ot_data["resource_spans"][0]["scope_spans"][0]["scope"][
+            for v in otData["resource_spans"][0]["scope_spans"][0]["scope"][
                 "attributes"
             ]
         ]
@@ -104,7 +105,7 @@ class DNSDistOpenTelemetryProtobufBaseTest(DNSDistOpenTelemetryProtobufTest):
 
         root_span_attr_keys = [
             v["key"]
-            for v in ot_data["resource_spans"][0]["scope_spans"][0]["spans"][0][
+            for v in otData["resource_spans"][0]["scope_spans"][0]["spans"][0][
                 "attributes"
             ]
         ]
@@ -116,7 +117,7 @@ class DNSDistOpenTelemetryProtobufBaseTest(DNSDistOpenTelemetryProtobufTest):
         # No way to guess the test port, but check the rest of the values
         root_span_attrs = {
             v["key"]: v["value"]["string_value"]
-            for v in ot_data["resource_spans"][0]["scope_spans"][0]["spans"][0][
+            for v in otData["resource_spans"][0]["scope_spans"][0]["spans"][0][
                 "attributes"
             ]
             if v["key"] not in ["query.remote.port"]
@@ -131,7 +132,7 @@ class DNSDistOpenTelemetryProtobufBaseTest(DNSDistOpenTelemetryProtobufTest):
         )
 
         msg_span_name = {
-            v["name"] for v in ot_data["resource_spans"][0]["scope_spans"][0]["spans"]
+            v["name"] for v in otData["resource_spans"][0]["scope_spans"][0]["spans"]
         }
 
         funcs = {
@@ -139,23 +140,54 @@ class DNSDistOpenTelemetryProtobufBaseTest(DNSDistOpenTelemetryProtobufTest):
             "applyRulesToQuery",
             "Rule: Enable tracing",
             "applyRulesChainToQuery",
-            "selectBackendForOutgoingQuery",
-            "processResponse",
             "applyRulesToResponse",
-            "Rule: Do PB logging",
         }
 
+        if hasSelectBackendForOutgoingQuery:
+            funcs.add("selectBackendForOutgoingQuery")
+
+        if hasResponse:
+            funcs.add("processResponse")
+
+        if hasRemoteLogResponseAction:
+            funcs.add("Rule: Do PB logging")
+
         if useTCP:
             funcs.add("IncomingTCPConnectionState::handleQuery")
         else:
             funcs.add("processUDPQuery")
-            funcs.add("assignOutgoingUDPQueryToBackend")
+            if hasSelectBackendForOutgoingQuery:
+                funcs.add("assignOutgoingUDPQueryToBackend")
 
-        if wasDelayed:
+        if hasProcessResponseAfterRules:
             funcs.add("processResponseAfterRules")
 
+        funcs = funcs.union(extraFunctions)
+
         self.assertSetEqual(msg_span_name, funcs)
 
+
+class DNSDistOpenTelemetryProtobufBaseTest(DNSDistOpenTelemetryProtobufTest):
+    def doTest(
+        self, hasProcessResponseAfterRules=False, useTCP=False, traceID="", spanID=""
+    ):
+        msg = self.sendQueryAndGetProtobuf(useTCP, traceID, spanID)
+
+        self.assertTrue(msg.HasField("openTelemetryTraceID"))
+        self.assertNotEqual(msg.openTelemetryTraceID, "")
+
+        if traceID != "":
+            self.assertEqual(msg.openTelemetryTraceID, binascii.a2b_hex(traceID))
+
+        self.assertTrue(msg.HasField("openTelemetryData"))
+        traces_data = opentelemetry.proto.trace.v1.trace_pb2.TracesData()
+        traces_data.ParseFromString(msg.openTelemetryData)
+        ot_data = google.protobuf.json_format.MessageToDict(
+            traces_data, preserving_proto_field_name=True
+        )
+
+        self.checkOTData(ot_data, hasProcessResponseAfterRules, useTCP)
+
         traceId = base64.b64encode(msg.openTelemetryTraceID).decode()
         for msg_span in ot_data["resource_spans"][0]["scope_spans"][0]["spans"]:
             self.assertEqual(
@@ -275,7 +307,7 @@ response_rules:
         self.doTest(True)
 
     def testTCP(self):
-        self.doTest(wasDelayed=True, useTCP=True)
+        self.doTest(hasProcessResponseAfterRules=True, useTCP=True)
 
 
 class TestOpenTelemetryTracingBaseDelayLua(DNSDistOpenTelemetryProtobufBaseTest):
@@ -297,7 +329,7 @@ addResponseAction(AllRule(), RemoteLogResponseAction(rl, nil, false, {}, {}, tru
         self.doTest(True)
 
     def testTCP(self):
-        self.doTest(wasDelayed=True, useTCP=True)
+        self.doTest(hasProcessResponseAfterRules=True, useTCP=True)
 
 
 class TestOpenTelemetryTracingUseIncomingYAML(DNSDistOpenTelemetryProtobufBaseTest):
@@ -359,7 +391,7 @@ newServer{address="127.0.0.1:%d"}
 rl = newRemoteLogger('127.0.0.1:%d')
 setOpenTelemetryTracing(true)
 
-addAction(AllRule(), SetTraceAction(true, true), {name="Enable tracing"})
+addAction(AllRule(), SetTraceAction(true, {}, true), {name="Enable tracing"})
 addResponseAction(AllRule(), RemoteLogResponseAction(rl, nil, false, {}, {}, false), {name="Do PB logging"})
 """
 
@@ -494,3 +526,153 @@ addResponseAction(AllRule(), RemoteLogResponseAction(rl))
 
     def testEnabledButUnset(self):
         self.doTest()
+
+
+class TestOpenTelemetryTracingBaseYAMLIncludedRemoteLoggerDropped(
+    DNSDistOpenTelemetryProtobufTest
+):
+    _yaml_config_params = [
+        "_testServerPort",
+        "_protobufServerPort",
+    ]
+    _yaml_config_template = """---
+logging:
+  open_telemetry_tracing: true
+
+backends:
+  - address: 127.0.0.1:%d
+    protocol: Do53
+
+remote_logging:
+ protobuf_loggers:
+   - name: pblog
+     address: 127.0.0.1:%d
+
+query_rules:
+ - name: Enable tracing
+   selector:
+     type: All
+   action:
+     type: SetTrace
+     value: true
+     remote_loggers:
+       - pblog
+
+response_rules:
+ - name: Drop
+   selector:
+     type: All
+   action:
+     type: Drop
+"""
+
+    def doTest(self, useTCP=False):
+        msg = self.sendQueryAndGetProtobuf(useTCP=useTCP, dropped=True)
+        traces_data = opentelemetry.proto.trace.v1.trace_pb2.TracesData()
+        traces_data.ParseFromString(msg.openTelemetryData)
+        ot_data = google.protobuf.json_format.MessageToDict(
+            traces_data, preserving_proto_field_name=True
+        )
+
+        self.checkOTData(
+            ot_data,
+            hasProcessResponseAfterRules=False,
+            hasRemoteLogResponseAction=False,
+            useTCP=useTCP,
+            extraFunctions={
+                "Rule: Drop",
+            },
+        )
+
+    def testBasic(self):
+        self.doTest(False)
+
+    def testTCP(self):
+        self.doTest(True)
+
+
+class TestOpenTelemetryTracingBaseLuaIncludedRemoteLoggerDropped(
+    TestOpenTelemetryTracingBaseYAMLIncludedRemoteLoggerDropped
+):
+    _yaml_config_params = []
+    _yaml_config_template = ""
+
+    _config_params = [
+        "_testServerPort",
+        "_protobufServerPort",
+    ]
+    _config_template = """
+newServer{address="127.0.0.1:%d"}
+rl = newRemoteLogger('127.0.0.1:%d')
+setOpenTelemetryTracing(true)
+
+addAction(AllRule(), SetTraceAction(true, {rl}), {name="Enable tracing"})
+addResponseAction(AllRule(), DropResponseAction(), {name="Drop"})
+"""
+
+
+class TestOpenTelemetryTracingBaseYAMLIncludedRemoteLoggerSpoofed(
+    DNSDistOpenTelemetryProtobufTest
+):
+    _yaml_config_params = [
+        "_testServerPort",
+        "_protobufServerPort",
+    ]
+    _yaml_config_template = """---
+logging:
+  open_telemetry_tracing: true
+
+backends:
+  - address: 127.0.0.1:%d
+    protocol: Do53
+
+remote_logging:
+ protobuf_loggers:
+   - name: pblog
+     address: 127.0.0.1:%d
+
+query_rules:
+ - name: Enable tracing
+   selector:
+     type: All
+   action:
+     type: SetTrace
+     value: true
+     remote_loggers:
+       - pblog
+ - name: Spoof A record
+   selector:
+     type: All
+   action:
+     type: Spoof
+     ips:
+       - 192.0.2.1
+"""
+
+    def doTest(self, useTCP=False):
+        msg = self.sendQueryAndGetProtobuf(
+            useTCP=useTCP, querySentByDNSDist=False, dropped=True
+        )
+        traces_data = opentelemetry.proto.trace.v1.trace_pb2.TracesData()
+        traces_data.ParseFromString(msg.openTelemetryData)
+        ot_data = google.protobuf.json_format.MessageToDict(
+            traces_data, preserving_proto_field_name=True
+        )
+
+        self.checkOTData(
+            ot_data,
+            hasProcessResponseAfterRules=False,
+            hasRemoteLogResponseAction=False,
+            useTCP=useTCP,
+            hasSelectBackendForOutgoingQuery=False,
+            hasResponse=False,
+            extraFunctions={
+                "Rule: Spoof A record",
+            },
+        )
+
+    def testBasic(self):
+        self.doTest()
+
+    def testTCP(self):
+        self.doTest(useTCP=True)