Elukey has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/396051 )
Change subject: List of fixes:
......................................................................
List of fixes:
* Allow a metric to be emitted by multiple daemons,
even with different metrics. The use case of 'segment/count'
is unique since the metric is emitted by both coordinator and
historical, with different labels.
* Correct the historical's metric labels for segment/count
and segment/used.
* Change name of druid_(broker|historical)_query_cache_sizebytes_count
to a more appropriate _query_cache_size_bytes
* No-op variable name changes to be more consistent in the code.
* Prefer absolute import for the collector.py module
* Add a more precise and robust set of tests.
Change-Id: I792dfc34f5fd0185c5ec379e861aa50d28e88869
---
M druid_exporter/collector.py
M druid_exporter/exporter.py
M setup.py
M test/test_collector.py
4 files changed, 352 insertions(+), 64 deletions(-)
Approvals:
Elukey: Verified; Looks good to me, approved
diff --git a/druid_exporter/collector.py b/druid_exporter/collector.py
index fc4affd..1cf4518 100644
--- a/druid_exporter/collector.py
+++ b/druid_exporter/collector.py
@@ -34,44 +34,62 @@
# List of supported metrics and their fields of the JSON dictionary
# sent by a Druid daemon. These fields will be added as labels
# when returning the available metrics in @collect.
+ # Due to the fact that metric names are not unique (like
segment/count),
+ # it is necessary to split the data structure by daemon.
self.supported_metric_names = {
- # Broker, Historical
- 'query/time': ['dataSource'],
- 'query/bytes': ['dataSource'],
- 'query/cache/total/numEntries': None,
- 'query/cache/total/sizeBytes': None,
- 'query/cache/total/hits': None,
- 'query/cache/total/misses': None,
- 'query/cache/total/evictions': None,
- 'query/cache/total/timeouts': None,
- 'query/cache/total/errors': None,
- # Historical + Coordinator
- 'segment/count': ['dataSource'],
- # Historical
- 'segment/max': None,
- 'segment/used': ['tier', 'dataSource'],
- 'segment/scan/pending': None,
- # Coordinator
- 'segment/assigned/count': ['tier'],
- 'segment/moved/count': ['tier'],
- 'segment/dropped/count': ['tier'],
- 'segment/deleted/count': ['tier'],
- 'segment/unneeded/count': ['tier'],
- 'segment/overShadowed/count': None,
- 'segment/loadQueue/failed': ['server'],
- 'segment/loadQueue/count': ['server'],
- 'segment/dropQueue/count': ['server'],
- 'segment/size': ['dataSource'],
- 'segment/unavailable/count': ['dataSource'],
- 'segment/underReplicated/count': ['tier', 'dataSource'],
- 'ingest/events/thrownAway': ['dataSource'],
- 'ingest/events/unparseable': ['dataSource'],
- 'ingest/events/processed': ['dataSource'],
- 'ingest/rows/output': ['dataSource'],
- 'ingest/persists/count': ['dataSource'],
- 'ingest/persists/failed': ['dataSource'],
- 'ingest/handoff/failed': ['dataSource'],
- 'ingest/handoff/count': ['dataSource'],
+ 'broker': {
+ 'query/time': ['dataSource'],
+ 'query/bytes': ['dataSource'],
+ 'query/cache/total/numEntries': None,
+ 'query/cache/total/sizeBytes': None,
+ 'query/cache/total/hits': None,
+ 'query/cache/total/misses': None,
+ 'query/cache/total/evictions': None,
+ 'query/cache/total/timeouts': None,
+ 'query/cache/total/errors': None,
+ },
+ 'historical': {
+ 'query/time': ['dataSource'],
+ 'query/bytes': ['dataSource'],
+ 'query/cache/total/numEntries': None,
+ 'query/cache/total/sizeBytes': None,
+ 'query/cache/total/hits': None,
+ 'query/cache/total/misses': None,
+ 'query/cache/total/evictions': None,
+ 'query/cache/total/timeouts': None,
+ 'query/cache/total/errors': None,
+ 'segment/count': ['tier', 'dataSource'],
+ 'segment/max': None,
+ 'segment/used': ['tier', 'dataSource'],
+ 'segment/scan/pending': None,
+ },
+ 'coordinator': {
+ 'segment/count': ['dataSource'],
+ 'segment/assigned/count': ['tier'],
+ 'segment/moved/count': ['tier'],
+ 'segment/dropped/count': ['tier'],
+ 'segment/deleted/count': ['tier'],
+ 'segment/unneeded/count': ['tier'],
+ 'segment/overShadowed/count': None,
+ 'segment/loadQueue/failed': ['server'],
+ 'segment/loadQueue/count': ['server'],
+ 'segment/dropQueue/count': ['server'],
+ 'segment/size': ['dataSource'],
+ 'segment/unavailable/count': ['dataSource'],
+ 'segment/underReplicated/count': ['tier', 'dataSource'],
+ },
+ 'peon': {
+ 'query/time': ['dataSource'],
+ 'query/bytes': ['dataSource'],
+ 'ingest/events/thrownAway': ['dataSource'],
+ 'ingest/events/unparseable': ['dataSource'],
+ 'ingest/events/processed': ['dataSource'],
+ 'ingest/rows/output': ['dataSource'],
+ 'ingest/persists/count': ['dataSource'],
+ 'ingest/persists/failed': ['dataSource'],
+ 'ingest/handoff/failed': ['dataSource'],
+ 'ingest/handoff/count': ['dataSource'],
+ },
}
# Buckets used when storing histogram metrics.
@@ -190,7 +208,7 @@
'druid_' + daemon + '_query_cache_numentries_count',
'Number of cache entries.'),
'query/cache/total/sizeBytes': GaugeMetricFamily(
- 'druid_' + daemon + '_query_cache_sizebytes_count',
+ 'druid_' + daemon + '_query_cache_size_bytes',
'Size in bytes of cache entries.'),
'query/cache/total/hits': GaugeMetricFamily(
'druid_' + daemon + '_query_cache_hits_count',
@@ -217,11 +235,11 @@
'segment/count': GaugeMetricFamily(
'druid_historical_segment_count',
'Number of served segments.',
- labels=['datasource']),
+ labels=['tier', 'datasource']),
'segment/used': GaugeMetricFamily(
'druid_historical_segment_used_bytes',
'Bytes used for served segments.',
- labels=['datasource']),
+ labels=['tier', 'datasource']),
'segment/scan/pending': GaugeMetricFamily(
'druid_historical_segment_scan_pending',
'Number of segments in queue waiting to be scanned.'),
@@ -299,17 +317,17 @@
The algorithm is generic enough to support all metrics handled by
self.counters without caring about the number of labels needed.
"""
- daemon_name = DruidCollector.sanitize_field(str(datapoint['service']))
+ daemon = DruidCollector.sanitize_field(str(datapoint['service']))
metric_name = str(datapoint['metric'])
metric_value = float(datapoint['value'])
metrics_storage = self.counters[metric_name]
- metric_labels = self.supported_metric_names[metric_name]
+ metric_labels = self.supported_metric_names[daemon][metric_name]
- metrics_storage.setdefault(daemon_name, {})
+ metrics_storage.setdefault(daemon, {})
if metric_labels:
- metrics_storage_cursor = metrics_storage[daemon_name]
+ metrics_storage_cursor = metrics_storage[daemon]
for label in metric_labels:
label_value = str(datapoint[label])
if metric_labels[-1] != label:
@@ -318,7 +336,7 @@
else:
metrics_storage_cursor[label_value] = metric_value
else:
- metrics_storage[daemon_name] = metric_value
+ metrics_storage[daemon] = metric_value
log.debug("The datapoint {} modified the counters dictionary to: \n{}"
.format(datapoint, self.counters))
@@ -336,17 +354,17 @@
self.counters = {'query/time': {'broker':
{'test': {'10': 1, '100': 1, etc.., 'sum': 10}}}}}
"""
- daemon_name = DruidCollector.sanitize_field(str(datapoint['service']))
+ daemon = DruidCollector.sanitize_field(str(datapoint['service']))
metric_name = str(datapoint['metric'])
metric_value = float(datapoint['value'])
datasource = str(datapoint['dataSource'])
- self.histograms.setdefault(metric_name, {daemon_name: {datasource:
{}}})
- self.histograms[metric_name].setdefault(daemon_name, {datasource: {}})
- self.histograms[metric_name][daemon_name].setdefault(datasource, {})
+ self.histograms.setdefault(metric_name, {daemon: {datasource: {}}})
+ self.histograms[metric_name].setdefault(daemon, {datasource: {}})
+ self.histograms[metric_name][daemon].setdefault(datasource, {})
for bucket in self.metric_buckets[metric_name]:
- stored_buckets =
self.histograms[metric_name][daemon_name][datasource]
+ stored_buckets = self.histograms[metric_name][daemon][datasource]
if bucket not in stored_buckets:
stored_buckets[bucket] = 0
if bucket != 'sum' and metric_value <= float(bucket):
@@ -381,7 +399,7 @@
for metric in cache_metrics:
if not self.counters[metric] or daemon not in
self.counters[metric]:
- if not self.supported_metric_names[metric]:
+ if not self.supported_metric_names[daemon][metric]:
cache_metrics[metric].add_metric([], float('nan'))
else:
continue
@@ -397,12 +415,12 @@
('peon', realtime_metrics)]:
for metric in metrics:
if not self.counters[metric] or daemon not in
self.counters[metric]:
- if not self.supported_metric_names[metric]:
+ if not self.supported_metric_names[daemon][metric]:
metrics[metric].add_metric([], float('nan'))
else:
continue
else:
- labels = self.supported_metric_names[metric]
+ labels = self.supported_metric_names[daemon][metric]
if not labels:
metrics[metric].add_metric(
[], self.counters[metric][daemon])
@@ -425,8 +443,16 @@
yield registered
def register_datapoint(self, datapoint):
+ if (datapoint['feed'] != 'metrics'):
+ log.debug("The following feed does not contain a datapoint, "
+ "dropping it: {}"
+ .format(datapoint))
+ return
+
+ daemon = DruidCollector.sanitize_field(str(datapoint['service']))
if (datapoint['feed'] != 'metrics' or
- datapoint['metric'] not in self.supported_metric_names):
+ daemon not in self.supported_metric_names or
+ datapoint['metric'] not in
self.supported_metric_names[daemon]):
log.debug("The following datapoint is not supported, either "
"because the 'feed' field is not 'metrics' or "
"the metric itself is not supported: {}"
diff --git a/druid_exporter/exporter.py b/druid_exporter/exporter.py
index a269190..400005b 100644
--- a/druid_exporter/exporter.py
+++ b/druid_exporter/exporter.py
@@ -19,9 +19,9 @@
import logging
import sys
+from druid_exporter import collector
from prometheus_client import generate_latest, make_wsgi_app, REGISTRY
from wsgiref.simple_server import make_server
-from . import collector
log = logging.getLogger(__name__)
diff --git a/setup.py b/setup.py
index 9340668..30434ea 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,7 @@
from setuptools import setup
setup(name='druid_exporter',
- version='0.5',
+ version='0.6',
description='Prometheus exporter for Druid',
url='https://github.com/wikimedia/operations-software-druid_exporter',
author='Luca Toscano',
diff --git a/test/test_collector.py b/test/test_collector.py
index 8a60d66..f504ab5 100644
--- a/test/test_collector.py
+++ b/test/test_collector.py
@@ -23,19 +23,212 @@
def setUp(self):
self.collector = DruidCollector()
+
+ # List of metric names as emitted by Druid, coupled with their
+ # Prometheus metric name and labels.
+ self.supported_metric_names = {
+ 'broker': {
+ 'query/time': {
+ 'metric_name': 'druid_broker_query_time_ms',
+ 'labels': ['dataSource']
+ },
+ 'query/bytes': {
+ 'metric_name': 'druid_broker_query_bytes',
+ 'labels': ['dataSource']
+ },
+ 'query/cache/total/numEntries': {
+ 'metric_name': 'druid_broker_query_cache_numentries_count',
+ 'labels': None
+ },
+ 'query/cache/total/sizeBytes': {
+ 'metric_name': 'druid_broker_query_cache_size_bytes',
+ 'labels': None
+ },
+ 'query/cache/total/hits': {
+ 'metric_name': 'druid_broker_query_cache_hits_count',
+ 'labels': None
+ },
+ 'query/cache/total/misses': {
+ 'metric_name':'druid_broker_query_cache_hits_count',
+ 'labels': None
+ },
+ 'query/cache/total/evictions': {
+ 'metric_name':'druid_broker_query_cache_evictions_count',
+ 'labels': None
+ },
+ 'query/cache/total/timeouts': {
+ 'metric_name':'druid_broker_query_cache_timeouts_count',
+ 'labels': None
+ },
+ 'query/cache/total/errors': {
+ 'metric_name':'druid_broker_query_cache_errors_count',
+ 'labels': None
+ }
+ },
+ 'historical': {
+ 'query/time': {
+ 'metric_name': 'druid_historical_query_time_ms',
+ 'labels': ['dataSource']
+ },
+ 'query/bytes': {
+ 'metric_name': 'druid_historical_query_bytes',
+ 'labels': ['dataSource']
+ },
+ 'query/cache/total/numEntries': {
+ 'metric_name':
'druid_historical_query_cache_numentries_count',
+ 'labels': None
+ },
+ 'query/cache/total/sizeBytes': {
+ 'metric_name': 'druid_historical_query_cache_size_bytes',
+ 'labels': None
+ },
+ 'query/cache/total/hits': {
+ 'metric_name': 'druid_historical_query_cache_hits_count',
+ 'labels': None
+ },
+ 'query/cache/total/misses': {
+ 'metric_name':'druid_historical_query_cache_hits_count',
+ 'labels': None
+ },
+ 'query/cache/total/evictions': {
+
'metric_name':'druid_historical_query_cache_evictions_count',
+ 'labels': None
+ },
+ 'query/cache/total/timeouts': {
+
'metric_name':'druid_historical_query_cache_timeouts_count',
+ 'labels': None
+ },
+ 'query/cache/total/errors': {
+ 'metric_name':'druid_historical_query_cache_errors_count',
+ 'labels': None
+ },
+ 'segment/count': {
+ 'metric_name': 'druid_historical_segment_count',
+ 'labels': ['tier', 'dataSource']
+ },
+ 'segment/max': {
+ 'metric_name': 'druid_historical_max_segment_bytes',
+ 'labels': None
+ },
+ 'segment/used': {
+ 'metric_name': 'druid_historical_segment_used_bytes',
+ 'labels': ['tier', 'dataSource']
+ },
+ 'segment/scan/pending': {
+ 'metric_name': 'druid_historical_segment_scan_pending',
+ 'labels': None
+ }
+ },
+ 'coordinator': {
+ 'segment/assigned/count': {
+ 'metric_name': 'druid_coordinator_segment_assigned_count',
+ 'labels': ['tier'],
+ },
+ 'segment/moved/count': {
+ 'metric_name': 'druid_coordinator_segment_moved_count',
+ 'labels': ['tier']
+ },
+ 'segment/dropped/count': {
+ 'metric_name': 'druid_coordinator_segment_dropped_count',
+ 'labels': ['tier']
+ },
+ 'segment/deleted/count': {
+ 'metric_name': 'druid_coordinator_segment_deleted_count',
+ 'labels': ['tier']
+ },
+ 'segment/unneeded/count': {
+ 'metric_name': 'druid_coordinator_segment_unneeded_count',
+ 'labels': ['tier']
+ },
+ 'segment/overShadowed/count': {
+ 'metric_name':
'druid_coordinator_segment_overshadowed_count',
+ 'labels': None
+ },
+ 'segment/loadQueue/failed': {
+ 'metric_name':
'druid_coordinator_segment_loadqueue_failed_count',
+ 'labels': ['server']
+ },
+ 'segment/loadQueue/count': {
+ 'metric_name': 'druid_coordinator_segment_loadqueue_count',
+ 'labels': ['server']
+ },
+ 'segment/dropQueue/count': {
+ 'metric_name': 'druid_coordinator_segment_dropqueue_count',
+ 'labels': ['server']
+ },
+ 'segment/size': {
+ 'metric_name': 'druid_coordinator_segment_size_bytes',
+ 'labels': ['dataSource']
+ },
+ 'segment/count': {
+ 'metric_name': 'druid_coordinator_segment_count',
+ 'labels': ['dataSource']
+ },
+ 'segment/unavailable/count': {
+ 'metric_name':
'druid_coordinator_segment_unavailable_count',
+ 'labels': ['dataSource']
+ },
+ 'segment/underReplicated/count': {
+ 'metric_name':
'druid_coordinator_segment_under_replicated_count',
+ 'labels': ['tier', 'dataSource']
+ }
+ },
+ 'peon': {
+ 'query/time': {
+ 'metric_name': 'druid_peon_query_time_ms',
+ 'labels': ['dataSource']
+ },
+ 'query/bytes': {
+ 'metric_name': 'druid_peon_query_bytes',
+ 'labels': ['dataSource']
+ },
+ 'ingest/events/thrownAway': {
+ 'metric_name':
'druid_realtime_ingest_events_thrown_away_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/events/unparseable': {
+ 'metric_name':
'druid_realtime_ingest_events_unparseable_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/events/processed': {
+ 'metric_name':
'druid_realtime_ingest_events_processed_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/rows/output': {
+ 'metric_name': 'druid_realtime_ingest_rows_output_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/persists/count': {
+ 'metric_name': 'druid_realtime_ingest_persists_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/persists/failed': {
+ 'metric_name':
'druid_realtime_ingest_persists_failed_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/handoff/failed': {
+ 'metric_name':
'druid_realtime_ingest_handoff_failed_count',
+ 'labels': ['dataSource'],
+ },
+ 'ingest/handoff/count': {
+ 'metric_name': 'druid_realtime_ingest_handoff_count',
+ 'labels': ['dataSource'],
+ }
+ }
+ }
self.metrics_without_labels = [
'druid_historical_segment_scan_pending',
'druid_historical_max_segment_bytes',
'druid_coordinator_segment_overshadowed_count',
'druid_broker_query_cache_numentries_count',
- 'druid_broker_query_cache_sizebytes_count',
+ 'druid_broker_query_cache_size_bytes',
'druid_broker_query_cache_hits_count',
'druid_broker_query_cache_misses_count',
'druid_broker_query_cache_evictions_count',
'druid_broker_query_cache_timeouts_count',
'druid_broker_query_cache_errors_count',
'druid_historical_query_cache_numentries_count',
- 'druid_historical_query_cache_sizebytes_count',
+ 'druid_historical_query_cache_size_bytes',
'druid_historical_query_cache_hits_count',
'druid_historical_query_cache_misses_count',
'druid_historical_query_cache_evictions_count',
@@ -122,16 +315,16 @@
# Third datapoint for the same metric as used in the first test, should
# add a key to the already existent dictionary.
datapoint = {'feed': 'metrics', 'service': 'druid/historical',
'dataSource': 'test2',
- 'metric': 'segment/used', 'tier': '_default_tier',
'value': 543}
+ 'metric': 'segment/count', 'tier': '_default_tier',
'value': 543}
self.collector.register_datapoint(datapoint)
-
expected_result['segment/used']['historical']['_default_tier']['test2'] = 543.0
+ expected_result['segment/count'] = {'historical': {'_default_tier':
{'test2': 543.0}}}
self.assertEqual(self.collector.counters, expected_result)
- # Fourth datapoint for an already seen metric but differen broker
- datapoint = {'feed': 'metrics', 'service': 'druid/broker',
'dataSource': 'test',
- 'metric': 'segment/used', 'tier': '_default_tier',
'value': 111}
+ # Fourth datapoint for an already seen metric but different daemon
+ datapoint = {'feed': 'metrics', 'service': 'druid/coordinator',
'dataSource': 'test',
+ 'metric': 'segment/count', 'value': 111}
self.collector.register_datapoint(datapoint)
- expected_result['segment/used']['broker'] = {'_default_tier': {'test':
111.0}}
+ expected_result['segment/count']['coordinator'] = {'test': 111.0}
self.assertEqual(self.collector.counters, expected_result)
# Fifth datapoint should override a pre-existent value
@@ -303,6 +496,12 @@
"service": "druid/coordinator", "host": "druid1001.eqiad.wmnet:
8081",
"metric": "segment/count", "value": 56, "dataSource": "netflow"},
+ {"feed": "metrics", "timestamp": "2017-12-07T09:55:04.937Z",
+ "service": "druid/historical", "host":
"druid1001.eqiad.wmnet:8083",
+ "metric": "segment/used", "value": 3252671142,
+ "dataSource": "banner_activity_minutely",
+ "priority": "0", "tier": "_default_tier"},
+
{"feed": "metrics", "timestamp": "2017-11-14T13:08:20.820Z",
"service": "druid/historical", "host":
"druid1001.eqiad.wmnet:8083",
"metric": "segment/max", "value": 2748779069440},
@@ -440,13 +639,19 @@
for datapoint in datapoints:
self.collector.register_datapoint(datapoint)
+ # Running it twice should not produce more metrics
+ for datapoint in datapoints:
+ self.collector.register_datapoint(datapoint)
+
collected_metrics = 0
+ prometheus_metric_samples = []
for metric in self.collector.collect():
# Metrics should not be returned if no sample is associated
# (not even a 'nan')
self.assertNotEqual(metric.samples, [])
if metric.samples and metric.samples[0][0].startswith('druid_'):
collected_metrics += 1
+ prometheus_metric_samples.append(metric.samples)
# Number of metrics pushed using register_datapoint plus the ones
# generated by the exporter for bookeeping,
@@ -454,6 +659,63 @@
expected_druid_metrics_len = len(datapoints) + 1
self.assertEqual(collected_metrics, expected_druid_metrics_len)
+ for datapoint in datapoints:
+ metric = datapoint['metric']
+ daemon = datapoint['service'].split('/')[1]
+ prometheus_metric_name =
self.supported_metric_names[daemon][metric]['metric_name']
+ prometheus_metric_labels =
self.supported_metric_names[daemon][metric]['labels']
+
+ # The prometheus metric samples are in two forms:
+ # 1) histograms:
+ # [('druid_broker_query_time_ms_bucket', {'datasource':
'NavigationTiming', 'le': '10'}, 2),
+ # [...]
+ # ('druid_broker_query_time_ms_bucket', {'datasource':
'NavigationTiming', 'le': 'inf'}, 2),
+ # ('druid_broker_query_time_ms_count', {'datasource':
'NavigationTiming'}, 2),
+ # ('druid_broker_query_time_ms_sum', {'datasource':
'NavigationTiming'}, 20.0)]
+ #
+ # 2) counter/gauge
+ # [('druid_coordinator_segment_unneeded_count', {'tier':
'_default_tier'}, 0.0)]
+ #
+ # The idea of the following test is to make sure that after sending
+ # one data point for each metric, the sample contains the
information
+ # needed.
+ #
+ if 'query' not in metric:
+ for sample in prometheus_metric_samples:
+ if prometheus_metric_name == sample[0][0]:
+ if prometheus_metric_labels:
+ for label in prometheus_metric_labels:
+ self.assertTrue(label.lower() in sample[0][1])
+ else:
+ self.assertTrue(sample[0][1] == {})
+ break
+ else:
+ for sample in [s for s in prometheus_metric_samples if len(s)
== 8]:
+ if metric in sample[0][0]:
+ bucket_counter = 0
+ sum_counter = 0
+ count_counter = 0
+ for s in sample:
+ if s[0] == metric + "_sum":
+ sum_counter += 1
+ elif s[0] == metric + "_count":
+ count_counter += 1
+ elif s[0] == metric + "_bucket":
+ bucket_counter += 1
+ else:
+ raise RuntimeError(
+ 'Histogram sample not supported: {}'
+ .format(s))
+ assertEqual(sum_counter, 1)
+ assertEqual(count_counter, 1)
+ assertEqual(bucket_counter, 6)
+ break
+ else:
+ RuntimeError(
+ 'The metric {} does not have a valid sample!'
+ .format(metric))
+
+
def test_register_datapoints_count(self):
datapoints = [
--
To view, visit https://gerrit.wikimedia.org/r/396051
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I792dfc34f5fd0185c5ec379e861aa50d28e88869
Gerrit-PatchSet: 12
Gerrit-Project: operations/software/druid_exporter
Gerrit-Branch: master
Gerrit-Owner: Elukey <[email protected]>
Gerrit-Reviewer: Elukey <[email protected]>
Gerrit-Reviewer: Joal <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits