]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
feat(dnsdist): Add optional `instance` label to Prometheus metrics
authorPieter Lexis <pieter.lexis@powerdns.com>
Tue, 13 Jan 2026 13:25:35 +0000 (14:25 +0100)
committerPieter Lexis <pieter.lexis@powerdns.com>
Thu, 15 Jan 2026 16:13:20 +0000 (17:13 +0100)
The label's value is based on the server_id, which is the hostname by
default.

pdns/dnsdistdist/dnsdist-configuration-yaml.cc
pdns/dnsdistdist/dnsdist-configuration.hh
pdns/dnsdistdist/dnsdist-console-completion.cc
pdns/dnsdistdist/dnsdist-lua.cc
pdns/dnsdistdist/dnsdist-settings-definitions.yml
pdns/dnsdistdist/dnsdist-web.cc
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_Prometheus.py

index 3b9836c59491e9df7bb53286c4246bbd7d329958..0a99dc39b9ef07413a3291ff49af7570db132916 100644 (file)
@@ -891,6 +891,7 @@ static void loadWebServer(const dnsdist::rust::settings::WebserverConfiguration&
     }
 
     config.d_apiRequiresAuthentication = webConfig.api_requires_authentication;
+    config.d_prometheusAddInstanceLabel = webConfig.prometheus_add_instance;
     config.d_dashboardRequiresAuthentication = webConfig.dashboard_requires_authentication;
     config.d_statsRequireAuthentication = webConfig.stats_require_authentication;
     dnsdist::webserver::setMaxConcurrentConnections(webConfig.max_concurrent_connections);
index 67b7906b1b155cbde82ad21260828aeade99fb56..a190d8a8db3771278930eed433889b24e30c4265 100644 (file)
@@ -157,6 +157,7 @@ struct RuntimeConfiguration
   bool d_apiRequiresAuthentication{true};
   bool d_dashboardRequiresAuthentication{true};
   bool d_statsRequireAuthentication{true};
+  bool d_prometheusAddInstanceLabel{false};
   bool d_truncateTC{false};
   bool d_fixupCase{false};
   bool d_queryCountEnabled{false};
index 4ab848cd60d9037a4ec75c15b3d86d35eb18fe59..2424808e10d85b7f6015f4c96548dc49328d0ac3 100644 (file)
@@ -309,7 +309,7 @@ static std::vector<dnsdist::console::completion::ConsoleKeyword> s_consoleKeywor
   {"setVerbose", true, "bool", "set whether log messages at the verbose level will be logged"},
   {"setVerboseHealthChecks", true, "bool", "set whether health check errors will be logged"},
   {"setVerboseLogDestination", true, "destination file", "Set a destination file to write the 'verbose' log messages to, instead of sending them to syslog and/or the standard output"},
-  {"setWebserverConfig", true, "[{password=string, apiKey=string, customHeaders, statsRequireAuthentication}]", "Updates webserver configuration"},
+  {"setWebserverConfig", true, "[{password=string, apiKey=string, customHeaders, statsRequireAuthentication, prometheusAddInstanceLabel=bool}]", "Updates webserver configuration"},
   {"setWeightedBalancingFactor", true, "factor", "Set the balancing factor for bounded-load weighted policies (whashed, wrandom)"},
   {"setWHashedPerturbation", true, "value", "Set the hash perturbation value to be used in the whashed policy instead of a random one, allowing to have consistent whashed results on different instance"},
   {"show", true, "string", "outputs `string`"},
index 5fc7a147a363abc574c6a0738a07dcab4fab5875..64c2098702a6533f9cf01f5625d51911caebc748 100644 (file)
@@ -1103,6 +1103,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
       LuaAssociativeTable<std::string> headers;
       bool statsRequireAuthentication{true};
       bool apiRequiresAuthentication{true};
+      bool prometheusAddInstance{false};
       bool dashboardRequiresAuthentication{true};
       bool hashPlaintextCredentials = false;
       getOptionalValue<bool>(vars, "hashPlaintextCredentials", hashPlaintextCredentials);
@@ -1137,6 +1138,10 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
         config.d_statsRequireAuthentication = statsRequireAuthentication;
       }
 
+      if (getOptionalValue<bool>(vars, "prometheusAddInstanceLabel", prometheusAddInstance) > 0) {
+        config.d_prometheusAddInstanceLabel = prometheusAddInstance;
+      }
+
       if (getOptionalValue<bool>(vars, "apiRequiresAuthentication", apiRequiresAuthentication) > 0) {
         config.d_apiRequiresAuthentication = apiRequiresAuthentication;
       }
index bfcb87ca68c583f08edae51467b0adc9da319bbf..6e1d0a82103391947e7c340a57f568c3dbd3ab0c 100644 (file)
@@ -410,6 +410,9 @@ key_value_stores:
       description: "List of lookup keys"
 
 webserver:
+  changes:
+    - version: 2.1.0
+      content: "Added the ``prometheus_add_instance`` option"
   parameters:
     - name: "listen_addresses"
       type: "Vec<String>"
@@ -435,6 +438,10 @@ webserver:
       type: "bool"
       default: "true"
       description: "Whether access to the statistics (/metrics and /jsonstat endpoints) requires a valid password or API key"
+    - name: "prometheus_add_instance"
+      type: "bool"
+      default: "false"
+      description: "Add the 'instance' label with the value of general.server_id to all prometheus metrics"
     - name: "dashboard_requires_authentication"
       type: "bool"
       default: "true"
index 3f90fb8e673b80ab5a0b2df63af107d6bca4fb08..836d3d3f230aa22794613df88b3720638d40b2a2 100644 (file)
@@ -159,6 +159,7 @@ std::string getConfig()
     out << "API requires authentication: " << (config.d_apiRequiresAuthentication ? "yes" : "no") << endl;
     out << "Dashboard requires authentication: " << (config.d_dashboardRequiresAuthentication ? "yes" : "no") << endl;
     out << "Statistics require authentication: " << (config.d_statsRequireAuthentication ? "yes" : "no") << endl;
+    out << "Add instance to Prometheus labels: " << (config.d_prometheusAddInstanceLabel ? "yes" : "no") << endl;
     out << "Password: " << (config.d_webPassword ? "set" : "unset") << endl;
     out << "API key: " << (config.d_webAPIKey ? "set" : "unset") << endl;
     out << "API writable: " << (config.d_apiReadWrite ? "yes" : "no") << endl;
@@ -455,11 +456,11 @@ static json11::Json::array someResponseRulesToJson(const std::vector<T>& someRes
 
 #ifndef DISABLE_PROMETHEUS
 template <typename T>
-static void addRulesToPrometheusOutput(std::ostringstream& output, const std::vector<T>& rules)
+static void addRulesToPrometheusOutput(std::ostringstream& output, const std::vector<T>& rules, const std::string instanceLabelWithComma)
 {
   for (const auto& entry : rules) {
     std::string identifier = !entry.d_name.empty() ? entry.d_name : boost::uuids::to_string(entry.d_id);
-    output << "dnsdist_rule_hits{id=\"" << identifier << "\"} " << entry.d_rule->d_matches << "\n";
+    output << "dnsdist_rule_hits{id=\"" << identifier << "\"" << instanceLabelWithComma << "} " << entry.d_rule->d_matches << "\n";
   }
 }
 
@@ -491,6 +492,11 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
   resp.status = 200;
 
   std::ostringstream output;
+  std::string instanceLabel; // MUST be empty when instance label is not requested
+  {
+    auto rtc = dnsdist::configuration::getCurrentRuntimeConfiguration();
+    instanceLabel = rtc.d_prometheusAddInstanceLabel ? "instance=\"" + rtc.d_server_id + "\"" : "";
+  }
   static const std::set<std::string> metricBlacklist = {"special-memory-usage", "latency-count", "latency-sum"};
   {
     auto entries = dnsdist::metrics::g_stats.entries.read_lock();
@@ -523,8 +529,18 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
         prometheusMetricName = metricDetails.customName;
       }
 
-      if (!entry.d_labels.empty()) {
-        prometheusMetricName += "{" + entry.d_labels + "}";
+      if (!entry.d_labels.empty() || !instanceLabel.empty()) {
+        prometheusMetricName += "{";
+        if (!entry.d_labels.empty()) {
+          prometheusMetricName += entry.d_labels;
+          if (!instanceLabel.empty()) {
+            prometheusMetricName += ",";
+          }
+        }
+        if (!instanceLabel.empty()) {
+          prometheusMetricName += instanceLabel;
+        }
+        prometheusMetricName += "}";
       }
 
       // for these we have the help and types encoded in the sources
@@ -551,10 +567,16 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
     }
   }
 
+  std::string instanceLabelPlusComma;
+  std::string instanceLabelPlusBrackets;
+  if (!instanceLabel.empty()) {
+    instanceLabelPlusComma = "," + instanceLabel;
+    instanceLabelPlusBrackets = "{" + instanceLabel + "}";
+  }
   // Latency histogram buckets
   output << "# HELP dnsdist_latency Histogram of responses by latency (in milliseconds)\n";
   output << "# TYPE dnsdist_latency histogram\n";
-  addHistogramToPrometheusOutput(output, dnsdist::metrics::g_stats, "dnsdist_latency", "");
+  addHistogramToPrometheusOutput(output, dnsdist::metrics::g_stats, "dnsdist_latency", instanceLabelPlusBrackets);
 
   const string statesbase = "dnsdist_server_";
 
@@ -639,8 +661,8 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
 
     std::replace(serverName.begin(), serverName.end(), '.', '_');
 
-    const std::string label = boost::str(boost::format(R"({server="%1%",address="%2%"})")
-                                         % serverName % state->d_config.remote.toStringWithPort());
+    const std::string label = boost::str(boost::format(R"({server="%1%",address="%2%"%3%})")
+                                         % serverName % state->d_config.remote.toStringWithPort() % (instanceLabel.empty() ? "" : instanceLabelPlusComma));
 
     output << statesbase << "status"                           << label << " " << (state->isUp() ? "1" : "0")            << "\n";
     output << statesbase << "queries"                          << label << " " << state->queries.load()                  << "\n";
@@ -738,8 +760,8 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
       threadNumber = dupPair.first->second;
       ++(dupPair.first->second);
     }
-    const std::string label = boost::str(boost::format(R"({frontend="%1%",proto="%2%",thread="%3%"} )")
-                                         % frontName % proto % threadNumber);
+    const std::string label = boost::str(boost::format(R"({frontend="%1%",proto="%2%",thread="%3%"%4%} )")
+                                         % frontName % proto % threadNumber % (instanceLabel.empty() ? "" : instanceLabelPlusComma));
 
     output << frontsbase << "queries" << label << front->queries.load() << "\n";
     output << frontsbase << "noncompliantqueries" << label << front->nonCompliantQueries.load() << "\n";
@@ -761,11 +783,11 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
         output << frontsbase << "tlsunknownticketkeys" << label << front->tlsUnknownTicketKey.load() << "\n";
         output << frontsbase << "tlsinactiveticketkeys" << label << front->tlsInactiveTicketKey.load() << "\n";
 
-        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls10"} )" << front->tls10queries.load() << "\n";
-        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls11"} )" << front->tls11queries.load() << "\n";
-        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls12"} )" << front->tls12queries.load() << "\n";
-        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls13"} )" << front->tls13queries.load() << "\n";
-        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="unknown"} )" << front->tlsUnknownqueries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls10"} )" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << front->tls10queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls11"} )" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << front->tls11queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls12"} )" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << front->tls12queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="tls13"} )" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << front->tls13queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",tls="unknown"} )" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << front->tlsUnknownqueries.load() << "\n";
 
         const TLSErrorCounters* errorCounters = nullptr;
         if (front->tlsFrontend != nullptr) {
@@ -776,14 +798,14 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
         }
 
         if (errorCounters != nullptr) {
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="dhKeyTooSmall"} )" << errorCounters->d_dhKeyTooSmall << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="inappropriateFallBack"} )" << errorCounters->d_inappropriateFallBack << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="noSharedCipher"} )" << errorCounters->d_noSharedCipher << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="unknownCipherType"} )" << errorCounters->d_unknownCipherType << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="unknownKeyExchangeType"} )" << errorCounters->d_unknownKeyExchangeType << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="unknownProtocol"} )" << errorCounters->d_unknownProtocol << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="unsupportedEC"} )" << errorCounters->d_unsupportedEC << "\n";
-          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << R"(",error="unsupportedProtocol"} )" << errorCounters->d_unsupportedProtocol << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="dhKeyTooSmall"} )" << errorCounters->d_dhKeyTooSmall << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="inappropriateFallBack"} )" << errorCounters->d_inappropriateFallBack << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="noSharedCipher"} )" << errorCounters->d_noSharedCipher << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="unknownCipherType"} )" << errorCounters->d_unknownCipherType << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="unknownKeyExchangeType"} )" << errorCounters->d_unknownKeyExchangeType << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="unknownProtocol"} )" << errorCounters->d_unknownProtocol << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="unsupportedEC"} )" << errorCounters->d_unsupportedEC << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << R"(",error="unsupportedProtocol"} )" << errorCounters->d_unsupportedProtocol << "\n";
         }
       }
     }
@@ -817,7 +839,7 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
       threadNumber = dupPair.first->second;
       ++(dupPair.first->second);
     }
-    const std::string addrlabel = boost::str(boost::format(R"(frontend="%1%",thread="%2%")") % frontName % threadNumber);
+    const std::string addrlabel = boost::str(boost::format(R"(frontend="%1%",thread="%2%"%3%)") % frontName % threadNumber % (instanceLabel.empty() ? "" : instanceLabelPlusComma));
     const std::string label = "{" + addrlabel + "} ";
 
     output << frontsbase << "http_connects" << label << doh->d_httpconnects << "\n";
@@ -881,7 +903,7 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
     if (poolName.empty()) {
       poolName = "_default_";
     }
-    const string label = "{pool=\"" + poolName + "\"}";
+    const string label = "{pool=\"" + poolName + "\"" + (instanceLabel.empty() ? "" : instanceLabelPlusComma) + "}";
     const auto& pool = entry.second;
     output << "dnsdist_pool_servers" << label << " " << pool.countServers(false) << "\n";
     output << "dnsdist_pool_active_servers" << label << " " << pool.countServers(true) << "\n";
@@ -907,11 +929,11 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
   const auto& chains = dnsdist::configuration::getCurrentRuntimeConfiguration().d_ruleChains;
   for (const auto& chainDescription : dnsdist::rules::getRuleChainDescriptions()) {
     const auto& chain = dnsdist::rules::getRuleChain(chains, chainDescription.identifier);
-    addRulesToPrometheusOutput(output, chain);
+    addRulesToPrometheusOutput(output, chain, instanceLabelPlusComma);
   }
   for (const auto& chainDescription : dnsdist::rules::getResponseRuleChainDescriptions()) {
     const auto& chain = dnsdist::rules::getResponseRuleChain(chains, chainDescription.identifier);
-    addRulesToPrometheusOutput(output, chain);
+    addRulesToPrometheusOutput(output, chain, instanceLabelPlusComma);
   }
 
 #ifndef DISABLE_DYNBLOCKS
@@ -920,7 +942,7 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
   auto topNetmasksByReason = DynBlockMaintenance::getHitsForTopNetmasks();
   for (const auto& entry : topNetmasksByReason) {
     for (const auto& netmask : entry.second) {
-      output << "dnsdist_dynblocks_nmg_top_offenders_hits_per_second{reason=\"" << entry.first << "\",netmask=\"" << netmask.first.toString() << "\"} " << netmask.second << "\n";
+      output << "dnsdist_dynblocks_nmg_top_offenders_hits_per_second{reason=\"" << entry.first << "\",netmask=\"" << netmask.first.toString() << "\"" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) <<"} " << netmask.second << "\n";
     }
   }
 
@@ -929,14 +951,14 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
   auto topSuffixesByReason = DynBlockMaintenance::getHitsForTopSuffixes();
   for (const auto& entry : topSuffixesByReason) {
     for (const auto& suffix : entry.second) {
-      output << "dnsdist_dynblocks_smt_top_offenders_hits_per_second{reason=\"" << entry.first << "\",suffix=\"" << suffix.first.toString() << "\"} " << suffix.second << "\n";
+      output << "dnsdist_dynblocks_smt_top_offenders_hits_per_second{reason=\"" << entry.first << "\",suffix=\"" << suffix.first.toString() << "\"" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << "} " << suffix.second << "\n";
     }
   }
 #endif /* DISABLE_DYNBLOCKS */
 
   output << "# HELP dnsdist_info " << "Info from dnsdist, value is always 1" << "\n";
   output << "# TYPE dnsdist_info " << "gauge" << "\n";
-  output << "dnsdist_info{version=\"" << VERSION << "\"} " << "1" << "\n";
+  output << "dnsdist_info{version=\"" << VERSION << "\"" << (instanceLabel.empty() ? "" : instanceLabelPlusComma) << "} " << "1" << "\n";
 
   resp.body = output.str();
   resp.headers["Content-Type"] = "text/plain; version=0.0.4";
index 65e82ae8fe123497867203dcde5c7533d222ba61..221a195c6ae823b57db510a4f9e48c9ca5cc3851 100644 (file)
@@ -467,6 +467,9 @@ Webserver configuration
   .. versionchanged:: 1.8.0
     ``apiRequiresAuthentication``, ``dashboardRequiresAuthentication`` optional parameters added.
 
+  .. versionchanged:: 2.1.0
+    ``prometheusAddInstanceLabel`` optional parameter added.
+
   Setup webserver configuration. See :func:`webserver` and :doc:`../guides/webserver`.
 
   :param table options: A table with key: value pairs with webserver options.
@@ -480,6 +483,7 @@ Webserver configuration
   * ``apiRequiresAuthentication``: bool - Whether access to the API (/api endpoints) require a valid API key. Defaults to true.
   * ``dashboardRequiresAuthentication``: bool - Whether access to the internal dashboard requires a valid password. Defaults to true.
   * ``statsRequireAuthentication``: bool - Whether access to the statistics (/metrics and /jsonstat endpoints) require a valid password or API key. Defaults to true.
+  * ``prometheusAddInstanceLabel``: bool - Whether to add an instance label to every metric. The value of this label is set by :func:`setServerID`. Defaults to false.
   * ``maxConcurrentConnections``: int - The maximum number of concurrent web connections, or 0 which means an unlimited number. Defaults to 100.
   * ``hashPlaintextCredentials``: bool - Whether passwords and API keys provided in plaintext should be hashed during startup, to prevent the plaintext versions from staying in memory. Doing so increases significantly the cost of verifying credentials. Defaults to false.
 
index c300f876d961c0283ed0ac0187e6a1ea9326e602..ee781d7b658abb52be067ddd498d68a4094ce2da 100644 (file)
@@ -158,3 +158,73 @@ class TestPrometheus(DNSDistTest):
         self.checkMetric(
             r.text, "dnsdist_custom_metric_foo", "counter", 1, '{x="baz",y="abc"}'
         )
+
+
+class TestPrometheusWithInstance(TestPrometheus):
+    instance_name = "my-id"
+    _config_template = """
+    newServer{address="127.0.0.1:%d"}
+    webserver("127.0.0.1:%d")
+    setServerID("my-id")
+    setWebserverConfig({password="%s", apiKey="%s", prometheusAddInstanceLabel=true})
+    pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
+    getPool(""):setCache(pc)
+
+    -- test custom metrics as well
+    declareMetric('custom-metric1', 'counter', 'Custom counter')
+    incMetric('custom-metric1')
+    declareMetric('custom-metric2', 'gauge', 'Custom gauge')
+    -- and custom names
+    declareMetric('custom-metric3', 'counter', 'Custom counter', 'custom_prometheus_name')
+
+    -- test prometheus labels in custom metrics
+    declareMetric('custom-metric-foo', 'counter', 'Custom counter with labels', { withLabels = true })
+    incMetric('custom-metric-foo', { labels = { x = 'bar', y = 'xyz' } })
+    incMetric('custom-metric-foo', { labels = { x = 'baz', y = 'abc' } })
+    """
+
+    def checkPrometheusContentWithInstance(self, content):
+        for line in content.splitlines():
+            if not line.startswith("#"):
+                tokens = line.split(" ")
+                self.assertIn(f'instance="{self.instance_name}"', tokens[0])
+
+    def testMetrics(self):
+        """
+        Prometheus: Retrieve metrics
+        """
+        url = "http://127.0.0.1:" + str(self._webServerPort) + "/metrics"
+        r = requests.get(
+            url,
+            auth=("whatever", self._webServerBasicAuthPassword),
+            timeout=self._webTimeout,
+        )
+        self.assertTrue(r)
+        self.assertEqual(r.status_code, 200)
+        self.checkPrometheusContentBasic(r.text)
+        self.checkPrometheusContentWithInstance(r.text)
+
+        self.checkPrometheusContentPromtool(r.content)
+        self.checkMetric(
+            r.text, "dnsdist_custom_metric1", "counter", 1, '{instance="my-id"}'
+        )
+        self.checkMetric(
+            r.text, "dnsdist_custom_metric2", "gauge", 0, '{instance="my-id"}'
+        )
+        self.checkMetric(
+            r.text, "custom_prometheus_name", "counter", 0, '{instance="my-id"}'
+        )
+        self.checkMetric(
+            r.text,
+            "dnsdist_custom_metric_foo",
+            "counter",
+            1,
+            '{x="bar",y="xyz",instance="my-id"}',
+        )
+        self.checkMetric(
+            r.text,
+            "dnsdist_custom_metric_foo",
+            "counter",
+            1,
+            '{x="baz",y="abc",instance="my-id"}',
+        )