]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Make structured logging usable with DNSdist
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 7 Nov 2025 16:49:30 +0000 (17:49 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 19 Jan 2026 09:49:31 +0000 (10:49 +0100)
Signed-off-by: Remi Gacogne <remi.gacogne@powerdns.com>
pdns/dnsdistdist/dnsdist-logging.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-logging.hh [new file with mode: 0644]
pdns/dnsdistdist/logging.cc [new symlink]
pdns/dnsdistdist/logr.hh [new symlink]
pdns/dnsdistdist/meson.build
pdns/logging.cc [new file with mode: 0644]
pdns/logging.hh
pdns/recursordist/logging.cc [changed from file to symlink]

diff --git a/pdns/dnsdistdist/dnsdist-logging.cc b/pdns/dnsdistdist/dnsdist-logging.cc
new file mode 100644 (file)
index 0000000..114d3c2
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * 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 "dnsdist-logging.hh"
+
+#include <iomanip>
+#include <set>
+#include <stdexcept>
+
+#include "ext/json11/json11.hpp"
+
+#include "config.h"
+#if defined(HAVE_SYSTEMD)
+#include <systemd/sd-journal.h>
+#endif /* HAVE_SYSTEMD */
+
+namespace dnsdist::logging
+{
+#if defined(HAVE_SYSTEMD)
+static void loggerSDBackend(const Logging::Entry& entry)
+{
+  static const std::set<std::string, CIStringComparePOSIX> special{
+    "message",
+    "message_id",
+    "priority",
+    "code_file",
+    "code_line",
+    "code_func",
+    "errno",
+    "invocation_id",
+    "user_invocation_id",
+    "syslog_facility",
+    "syslog_identifier",
+    "syslog_pid",
+    "syslog_timestamp",
+    "syslog_raw",
+    "documentation",
+    "tid",
+    "unit",
+    "user_unit",
+    "object_pid"};
+
+  // We need to keep the string in mem until sd_journal_sendv has been called
+  std::vector<std::string> strings;
+  auto appendKeyAndVal = [&strings](const string& key, const string& value) {
+    strings.emplace_back(key + "=" + value);
+  };
+  appendKeyAndVal("MESSAGE", entry.message);
+  if (entry.error) {
+    appendKeyAndVal("ERROR", entry.error.value());
+  }
+  appendKeyAndVal("LEVEL", std::to_string(entry.level));
+  appendKeyAndVal("PRIORITY", std::to_string(entry.d_priority));
+  if (entry.name) {
+    appendKeyAndVal("SUBSYSTEM", entry.name.value());
+  }
+  std::array<char, 64> timebuf{};
+  appendKeyAndVal("TIMESTAMP", Logging::toTimestampStringMilli(entry.d_timestamp, timebuf));
+  for (const auto& value : entry.values) {
+    if (value.first.at(0) == '_' || special.count(value.first) != 0) {
+      string key{"PDNS"};
+      key.append(value.first);
+      appendKeyAndVal(toUpper(key), value.second);
+    }
+    else {
+      appendKeyAndVal(toUpper(value.first), value.second);
+    }
+  }
+
+  std::vector<iovec> iov;
+  iov.reserve(strings.size());
+  for (const auto& str : strings) {
+    // iovec has no 2 arg constructor, so make it explicit
+    iov.emplace_back(iovec{const_cast<void*>(reinterpret_cast<const void*>(str.data())), str.size()}); // NOLINT: it's the API
+  }
+  sd_journal_sendv(iov.data(), static_cast<int>(iov.size()));
+}
+#endif /* HAVE_SYSTEMD */
+
+static void loggerJSONBackend(const Logging::Entry& entry)
+{
+  std::array<char, 64> timebuf{};
+  json11::Json::object json = {
+    {"msg", entry.message},
+    {"level", std::to_string(entry.level)},
+    {"ts", Logging::toTimestampStringMilli(entry.d_timestamp, timebuf)},
+  };
+
+  if (entry.error) {
+    json.emplace("error", entry.error.value());
+  }
+
+  if (entry.name) {
+    json.emplace("subsystem", entry.name.value());
+  }
+
+  if (entry.d_priority != 0) {
+    json.emplace("priority", std::to_string(entry.d_priority));
+  }
+
+  for (auto const& value : entry.values) {
+    json.emplace(value.first, value.second);
+  }
+
+  static thread_local std::string out;
+  out.clear();
+  json11::Json doc(std::move(json));
+  doc.dump(out);
+  std::cerr << out << std::endl;
+}
+
+static void loggerBackend(const Logging::Entry& entry)
+{
+  static thread_local std::stringstream buf;
+
+  buf.str("");
+  buf << "msg=" << std::quoted(entry.message);
+  if (entry.error) {
+    buf << " error=" << std::quoted(entry.error.value());
+  }
+
+  if (entry.name) {
+    buf << " subsystem=" << std::quoted(entry.name.value());
+  }
+  buf << " level=" << std::quoted(std::to_string(entry.level));
+  if (entry.d_priority != 0) {
+    buf << " prio=" << std::quoted(Logr::Logger::toString(entry.d_priority));
+  }
+
+  std::array<char, 64> timebuf{};
+  buf << " ts=" << std::quoted(Logging::toTimestampStringMilli(entry.d_timestamp, timebuf));
+  for (auto const& value : entry.values) {
+    buf << " ";
+    buf << value.first << "=" << std::quoted(value.second);
+  }
+
+  std::cout << buf.str() << endl;
+}
+
+static std::shared_ptr<Logging::Logger> s_topLogger{nullptr};
+
+void setup(const std::string& backend)
+{
+  if (backend == "systemd-journal") {
+#if defined(HAVE_SYSTEMD)
+    if (int fileDesc = sd_journal_stream_fd("dnsdist", LOG_DEBUG, 0); fileDesc >= 0) {
+      s_topLogger = Logging::Logger::create(loggerSDBackend);
+      close(fileDesc);
+    }
+#endif
+    if (s_topLogger == nullptr) {
+      cerr << "Requested structured logging to systemd-journal, but it is not available" << endl;
+    }
+  }
+  else if (backend == "json") {
+    s_topLogger = Logging::Logger::create(loggerJSONBackend);
+    if (s_topLogger == nullptr) {
+      cerr << "JSON logging requested but it is not available" << endl;
+    }
+  }
+
+  if (s_topLogger == nullptr) {
+    s_topLogger = Logging::Logger::create(loggerBackend);
+  }
+}
+
+std::shared_ptr<const Logging::Logger> getTopLogger()
+{
+  if (!s_topLogger) {
+    throw std::runtime_error("Trying to access the top-level logger before logging has been setup");
+  }
+
+  return s_topLogger;
+}
+
+}
diff --git a/pdns/dnsdistdist/dnsdist-logging.hh b/pdns/dnsdistdist/dnsdist-logging.hh
new file mode 100644 (file)
index 0000000..db014db
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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 <memory>
+#include <string>
+
+#include "logging.hh"
+
+namespace dnsdist::logging
+{
+void setup(const std::string& backend);
+std::shared_ptr<const Logging::Logger> getTopLogger();
+}
diff --git a/pdns/dnsdistdist/logging.cc b/pdns/dnsdistdist/logging.cc
new file mode 120000 (symlink)
index 0000000..574d9eb
--- /dev/null
@@ -0,0 +1 @@
+../logging.cc
\ No newline at end of file
diff --git a/pdns/dnsdistdist/logr.hh b/pdns/dnsdistdist/logr.hh
new file mode 120000 (symlink)
index 0000000..2c796bf
--- /dev/null
@@ -0,0 +1 @@
+../logr.hh
\ No newline at end of file
index 84c08c7750eaa03fef8d49b05204835d4abd15fa..20b63b9bf5842ab92f360ac5f6090f137602bbbf 100644 (file)
@@ -147,6 +147,7 @@ common_sources += files(
   src_dir / 'dnsdist-ipcrypt2.cc',
   src_dir / 'dnsdist-kvs.cc',
   src_dir / 'dnsdist-lbpolicies.cc',
+  src_dir / 'dnsdist-logging.cc',
   src_dir / 'dnsdist-lua-actions.cc',
   src_dir / 'dnsdist-lua-bindings.cc',
   src_dir / 'dnsdist-lua-bindings-dnscrypt.cc',
@@ -201,6 +202,7 @@ common_sources += files(
   src_dir / 'gettime.cc',
   src_dir / 'iputils.cc',
   src_dir / 'libssl.cc',
+  src_dir / 'logging.cc',
   src_dir / 'misc.cc',
   src_dir / 'protozero.cc',
   src_dir / 'protozero-trace.cc',
diff --git a/pdns/logging.cc b/pdns/logging.cc
new file mode 100644 (file)
index 0000000..9567f4d
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * 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 "logging.hh"
+#include <string>
+#include <mutex>
+
+namespace Logging
+{
+
+std::shared_ptr<const Logger> Logger::getptr() const
+{
+  return shared_from_this();
+}
+
+bool Logger::enabled(Logr::Priority prio) const
+{
+  return _level <= _verbosity || prio != Logr::Absent;
+}
+
+void Logger::info(const std::string& msg) const
+{
+  logMessage(msg, Logr::Absent, std::nullopt);
+}
+
+void Logger::info(Logr::Priority prio, const std::string& msg) const
+{
+  logMessage(msg, prio, std::nullopt);
+}
+
+void Logger::logMessage(const std::string& msg, const std::optional<std::string>& err) const
+{
+  logMessage(msg, Logr::Absent, err);
+}
+
+void Logger::logMessage(const std::string& msg, Logr::Priority prio, const std::optional<std::string>& err) const
+{
+  if (!enabled(prio)) {
+    return;
+  }
+  Entry entry;
+  entry.level = _level;
+  entry.d_priority = prio;
+  ::gettimeofday(&entry.d_timestamp, nullptr);
+  entry.name = _name;
+  entry.message = msg;
+  entry.error = err;
+  auto parent = _parent;
+  entry.values.insert(_values.begin(), _values.end());
+  while (parent) {
+    entry.values.insert(parent->_values.begin(), parent->_values.end());
+    parent = parent->_parent;
+  }
+  _callback(entry);
+}
+
+void Logger::error(Logr::Priority prio, int err, const std::string& msg) const
+{
+  logMessage(msg, prio, std::string(stringerror(err)));
+}
+
+void Logger::error(Logr::Priority prio, const std::string& err, const std::string& msg) const
+{
+  logMessage(msg, prio, err);
+}
+
+void Logger::error(int err, const std::string& msg) const
+{
+  logMessage(msg, Logr::Absent, std::string(stringerror(err)));
+}
+
+void Logger::error(const std::string& err, const std::string& msg) const
+{
+  logMessage(msg, Logr::Absent, err);
+}
+
+std::shared_ptr<Logr::Logger> Logger::v(size_t level) const
+{
+  auto res = std::make_shared<Logger>(getptr(), _name, getVerbosity(), level + _level, _callback);
+  return res;
+}
+
+std::shared_ptr<Logr::Logger> Logger::withValues(const std::map<std::string, std::string>& values) const
+{
+  auto res = std::make_shared<Logger>(getptr(), _name, getVerbosity(), _level, _callback);
+  res->_values = values;
+  return res;
+}
+
+std::shared_ptr<Logr::Logger> Logger::withName(const std::string& name) const
+{
+  std::shared_ptr<Logger> res;
+  if (_name) {
+    res = std::make_shared<Logger>(getptr(), _name.value() + "." + name, getVerbosity(), _level, _callback);
+  }
+  else {
+    res = std::make_shared<Logger>(getptr(), name, getVerbosity(), _level, _callback);
+  }
+  res->setVerbosity(getVerbosity());
+  return res;
+}
+std::shared_ptr<Logger> Logger::create(EntryLogger callback)
+{
+  return std::make_shared<Logger>(callback);
+}
+std::shared_ptr<Logger> Logger::create(EntryLogger callback, const std::string& name)
+{
+  return std::make_shared<Logger>(callback, name);
+}
+
+size_t Logger::getVerbosity() const
+{
+  return _verbosity;
+}
+
+void Logger::setVerbosity(size_t verbosity)
+{
+  _verbosity = verbosity;
+}
+
+Logger::Logger(EntryLogger callback) :
+  _callback(callback)
+{
+}
+Logger::Logger(EntryLogger callback, std::optional<std::string> name) :
+  _callback(callback), _name(std::move(name))
+{
+}
+Logger::Logger(std::shared_ptr<const Logger> parent, std::optional<std::string> name, size_t verbosity, size_t lvl, EntryLogger callback) :
+  _parent(std::move(parent)), _callback(callback), _name(std::move(name)), _level(lvl), _verbosity(verbosity)
+{
+}
+
+Logger::~Logger() = default;
+};
+
+std::shared_ptr<Logging::Logger> g_slog{nullptr};
+
+const char* Logging::toTimestampStringMilli(const struct timeval& tval, std::array<char, 64>& buf, const std::string& format)
+{
+  size_t len = 0;
+  if (format != "%s") {
+    // strftime is not thread safe, it can access locale information
+    static std::mutex mutex;
+    auto lock = std::scoped_lock(mutex);
+    struct tm theTime // clang-format insists on formatting it like this
+      {};
+    len = strftime(buf.data(), buf.size(), format.c_str(), localtime_r(&tval.tv_sec, &theTime));
+  }
+  if (len == 0) {
+    len = snprintf(buf.data(), buf.size(), "%lld", static_cast<long long>(tval.tv_sec));
+  }
+
+  snprintf(&buf.at(len), buf.size() - len, ".%03ld", static_cast<long>(tval.tv_usec) / 1000);
+  return buf.data();
+}
index 8c2345306542b148166e3df9e443e9b41e6af51b..c781319e1b3a52df4d63a51cd460ea487a279f62 100644 (file)
@@ -24,7 +24,7 @@
 
 #include "config.h"
 
-#ifdef RECURSOR
+#if defined(RECURSOR) || defined(DNSDIST)
 
 #include <map>
 #include <memory>
@@ -160,7 +160,7 @@ struct IterLoggable : public Logr::Loggable
   }
 };
 
-typedef void (*EntryLogger)(const Entry&);
+using EntryLogger = void (*)(const Entry&);
 
 class Logger : public Logr::Logger, public std::enable_shared_from_this<const Logger>
 {
@@ -205,6 +205,7 @@ private:
 };
 }
 
+#if !defined(DNSDIST)
 extern std::shared_ptr<Logging::Logger> g_slog;
 
 // Prefer structured logging? Since Recursor 5.1.0, we always do. We keep a const, to allow for
@@ -223,11 +224,12 @@ constexpr bool g_slogStructured = true;
   do {                           \
     slogCall;                    \
   } while (0)
-
 #else // No structured logging (e.g. auth)
 // NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
 #define SLOG(oldStyle, slogCall) \
   do {                           \
     oldStyle;                    \
   } while (0)
-#endif // RECURSOR
+#endif /* ! DNSDIST */
+
+#endif // RECURSOR || DNSDIST
deleted file mode 100644 (file)
index 1db6da080e7a32cdfc8f384b2f4485be12412801..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * 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 "logging.hh"
-#include <string>
-#include <mutex>
-#include "utility.hh"
-
-namespace Logging
-{
-
-std::shared_ptr<const Logger> Logger::getptr() const
-{
-  return shared_from_this();
-}
-
-bool Logger::enabled(Logr::Priority prio) const
-{
-  return _level <= _verbosity || prio != Logr::Absent;
-}
-
-void Logger::info(const std::string& msg) const
-{
-  logMessage(msg, Logr::Absent, std::nullopt);
-}
-
-void Logger::info(Logr::Priority prio, const std::string& msg) const
-{
-  logMessage(msg, prio, std::nullopt);
-}
-
-void Logger::logMessage(const std::string& msg, const std::optional<std::string>& err) const
-{
-  logMessage(msg, Logr::Absent, err);
-}
-
-void Logger::logMessage(const std::string& msg, Logr::Priority prio, const std::optional<std::string>& err) const
-{
-  if (!enabled(prio)) {
-    return;
-  }
-  Entry entry;
-  entry.level = _level;
-  entry.d_priority = prio;
-  Utility::gettimeofday(&entry.d_timestamp);
-  entry.name = _name;
-  entry.message = msg;
-  entry.error = err;
-  auto parent = _parent;
-  entry.values.insert(_values.begin(), _values.end());
-  while (parent) {
-    entry.values.insert(parent->_values.begin(), parent->_values.end());
-    parent = parent->_parent;
-  }
-  _callback(entry);
-}
-
-void Logger::error(Logr::Priority prio, int err, const std::string& msg) const
-{
-  logMessage(msg, prio, std::string(stringerror(err)));
-}
-
-void Logger::error(Logr::Priority prio, const std::string& err, const std::string& msg) const
-{
-  logMessage(msg, prio, err);
-}
-
-void Logger::error(int err, const std::string& msg) const
-{
-  logMessage(msg, Logr::Absent, std::string(stringerror(err)));
-}
-
-void Logger::error(const std::string& err, const std::string& msg) const
-{
-  logMessage(msg, Logr::Absent, err);
-}
-
-std::shared_ptr<Logr::Logger> Logger::v(size_t level) const
-{
-  auto res = std::make_shared<Logger>(getptr(), _name, getVerbosity(), level + _level, _callback);
-  return res;
-}
-
-std::shared_ptr<Logr::Logger> Logger::withValues(const std::map<std::string, std::string>& values) const
-{
-  auto res = std::make_shared<Logger>(getptr(), _name, getVerbosity(), _level, _callback);
-  res->_values = values;
-  return res;
-}
-
-std::shared_ptr<Logr::Logger> Logger::withName(const std::string& name) const
-{
-  std::shared_ptr<Logger> res;
-  if (_name) {
-    res = std::make_shared<Logger>(getptr(), _name.value() + "." + name, getVerbosity(), _level, _callback);
-  }
-  else {
-    res = std::make_shared<Logger>(getptr(), name, getVerbosity(), _level, _callback);
-  }
-  res->setVerbosity(getVerbosity());
-  return res;
-}
-std::shared_ptr<Logger> Logger::create(EntryLogger callback)
-{
-  return std::make_shared<Logger>(callback);
-}
-std::shared_ptr<Logger> Logger::create(EntryLogger callback, const std::string& name)
-{
-  return std::make_shared<Logger>(callback, name);
-}
-
-size_t Logger::getVerbosity() const
-{
-  return _verbosity;
-}
-
-void Logger::setVerbosity(size_t verbosity)
-{
-  _verbosity = verbosity;
-}
-
-Logger::Logger(EntryLogger callback) :
-  _callback(callback)
-{
-}
-Logger::Logger(EntryLogger callback, std::optional<std::string> name) :
-  _callback(callback), _name(std::move(name))
-{
-}
-Logger::Logger(std::shared_ptr<const Logger> parent, std::optional<std::string> name, size_t verbosity, size_t lvl, EntryLogger callback) :
-  _parent(std::move(parent)), _callback(callback), _name(std::move(name)), _level(lvl), _verbosity(verbosity)
-{
-}
-
-Logger::~Logger() = default;
-};
-
-std::shared_ptr<Logging::Logger> g_slog{nullptr};
-
-const char* Logging::toTimestampStringMilli(const struct timeval& tval, std::array<char, 64>& buf, const std::string& format)
-{
-  size_t len = 0;
-  if (format != "%s") {
-    // strftime is not thread safe, it can access locale information
-    static std::mutex mutex;
-    auto lock = std::scoped_lock(mutex);
-    struct tm theTime // clang-format insists on formatting it like this
-      {};
-    len = strftime(buf.data(), buf.size(), format.c_str(), localtime_r(&tval.tv_sec, &theTime));
-  }
-  if (len == 0) {
-    len = snprintf(buf.data(), buf.size(), "%lld", static_cast<long long>(tval.tv_sec));
-  }
-
-  snprintf(&buf.at(len), buf.size() - len, ".%03ld", static_cast<long>(tval.tv_usec) / 1000);
-  return buf.data();
-}
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..574d9ebacaa2bd3bfce3da2ba955d6c3315f907e
--- /dev/null
@@ -0,0 +1 @@
+../logging.cc
\ No newline at end of file