From 52cb25070142f6648c759996cdafe992ae37d94a Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 27 Aug 2021 16:42:23 +0000 Subject: [PATCH 01/35] This is the handler we need to create. --- src/allmydata/web/status.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 158d897f9..8c78b1156 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1551,6 +1551,21 @@ class Statistics(MultiFormatResource): req.setHeader("content-type", "text/plain") return json.dumps(stats, indent=1) + "\n" + @render_exception + def render_OPENMETRICS(self, req): + req.setHeader("content-type", "application/openmetrics-text") + return "3. occurence. This should be the one.\n" + + # @render_exception + # def render_OPENMETRICS(self, req): + # req.setHeader("content-type", "text/plain") + # if self._helper: + # stats = self._helper.get_stats() + # import pprint + # return pprint.PrettyPrinter().pprint(stats) + "\n" + # return "uh oh\n" + + class StatisticsElement(Element): loader = XMLFile(FilePath(__file__).sibling("statistics.xhtml")) From 4000116c244aaa6dda0c4cb87f7fc9bf7d332cb8 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 3 Sep 2021 13:42:09 +0000 Subject: [PATCH 02/35] Newsfragment for OpenMetrics endpoint --- newsfragments/3786.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3786.feature diff --git a/newsfragments/3786.feature b/newsfragments/3786.feature new file mode 100644 index 000000000..82ce3f974 --- /dev/null +++ b/newsfragments/3786.feature @@ -0,0 +1 @@ +tahoe-lafs now provides its statistics also in OpenMetrics format (for Prometheus et. al.) on `/statistics?t=openmetrics`. From 8a64f50b79cefb8d76ce6111b51290c255099caf Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 3 Sep 2021 14:40:34 +0000 Subject: [PATCH 03/35] WIP - Could be wronger --- src/allmydata/web/status.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 8c78b1156..741f93061 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -14,6 +14,7 @@ from past.builtins import long import itertools import hashlib +import re from twisted.internet import defer from twisted.python.filepath import FilePath from twisted.web.resource import Resource @@ -1553,18 +1554,22 @@ class Statistics(MultiFormatResource): @render_exception def render_OPENMETRICS(self, req): - req.setHeader("content-type", "application/openmetrics-text") - return "3. occurence. This should be the one.\n" + def mangle_name(name): + return re.sub( + "_(\d\d)_(\d)_percentile", + '{quantile="0.\g<1>\g<2>"}', + name.replace(".", "_") + ) - # @render_exception - # def render_OPENMETRICS(self, req): - # req.setHeader("content-type", "text/plain") - # if self._helper: - # stats = self._helper.get_stats() - # import pprint - # return pprint.PrettyPrinter().pprint(stats) + "\n" - # return "uh oh\n" + req.setHeader( + "content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8" + ) + stats = self._provider.get_stats() + return (str({mangle_name(k): v for k, v in stats['counters'].items()}) + + str({mangle_name(k): v for k, v in stats['stats'].items()}) + + "\n" + ) class StatisticsElement(Element): From 2dbb9434b0d10b086e6d5367087aa1a4cdd77be0 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 8 Sep 2021 14:54:57 +0000 Subject: [PATCH 04/35] OpenMetrics endpoint WIP --- src/allmydata/web/status.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 741f93061..7d3fdd06e 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1554,6 +1554,10 @@ class Statistics(MultiFormatResource): @render_exception def render_OPENMETRICS(self, req): + req.setHeader("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8") + stats = self._provider.get_stats() + ret = u"" + def mangle_name(name): return re.sub( "_(\d\d)_(\d)_percentile", @@ -1561,15 +1565,13 @@ class Statistics(MultiFormatResource): name.replace(".", "_") ) - req.setHeader( - "content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8" - ) + for (k, v) in sorted(stats['counters'].items()): + ret += u"tahoe_counters_%s %s\n" % (mangle_name(k), v) - stats = self._provider.get_stats() - return (str({mangle_name(k): v for k, v in stats['counters'].items()}) - + str({mangle_name(k): v for k, v in stats['stats'].items()}) - + "\n" - ) + for (k, v) in sorted(stats['stats'].items()): + ret += u"tahoe_stats_%s %s\n" % (mangle_name(k), v) + + return ret class StatisticsElement(Element): From ca865e60db6d94bba6abb749a2db196a77b25d36 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 8 Sep 2021 15:08:25 +0000 Subject: [PATCH 05/35] OpenMetrics endpoint --- src/allmydata/web/status.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 7d3fdd06e..4be935a54 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1565,12 +1565,13 @@ class Statistics(MultiFormatResource): name.replace(".", "_") ) + def mangle_value(val): + return str(val) if val is not None else "NaN" + for (k, v) in sorted(stats['counters'].items()): - ret += u"tahoe_counters_%s %s\n" % (mangle_name(k), v) - + ret += u"tahoe_counters_%s %s\n" % (mangle_name(k), mangle_value(v)) for (k, v) in sorted(stats['stats'].items()): - ret += u"tahoe_stats_%s %s\n" % (mangle_name(k), v) - + ret += u"tahoe_stats_%s %s\n" % (mangle_name(k), mangle_value(v)) return ret class StatisticsElement(Element): From 4674bccde799be427c20dcfa8cc133126b620b99 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Thu, 9 Sep 2021 13:54:03 +0000 Subject: [PATCH 06/35] OpenMetrics: add trailing EOF marker --- src/allmydata/web/status.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 4be935a54..3e0e1a3ee 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1572,6 +1572,9 @@ class Statistics(MultiFormatResource): ret += u"tahoe_counters_%s %s\n" % (mangle_name(k), mangle_value(v)) for (k, v) in sorted(stats['stats'].items()): ret += u"tahoe_stats_%s %s\n" % (mangle_name(k), mangle_value(v)) + + ret += u"# EOF\n" + return ret class StatisticsElement(Element): From d05e373d4236f7e5a106120da11882b64abd7ded Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Thu, 9 Sep 2021 13:57:59 +0000 Subject: [PATCH 07/35] OpenMetrics: All strings are unicode. --- src/allmydata/web/status.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 3e0e1a3ee..6d36861ad 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1560,13 +1560,13 @@ class Statistics(MultiFormatResource): def mangle_name(name): return re.sub( - "_(\d\d)_(\d)_percentile", - '{quantile="0.\g<1>\g<2>"}', - name.replace(".", "_") + u"_(\d\d)_(\d)_percentile", + u'{quantile="0.\g<1>\g<2>"}', + name.replace(u".", u"_") ) def mangle_value(val): - return str(val) if val is not None else "NaN" + return str(val) if val is not None else u"NaN" for (k, v) in sorted(stats['counters'].items()): ret += u"tahoe_counters_%s %s\n" % (mangle_name(k), mangle_value(v)) From 74965b271c026fe35436207a3d358ff9c40b32dc Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Thu, 9 Sep 2021 22:36:30 +0000 Subject: [PATCH 08/35] typo --- newsfragments/3786.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/3786.feature b/newsfragments/3786.feature index 82ce3f974..ecbfc0372 100644 --- a/newsfragments/3786.feature +++ b/newsfragments/3786.feature @@ -1 +1 @@ -tahoe-lafs now provides its statistics also in OpenMetrics format (for Prometheus et. al.) on `/statistics?t=openmetrics`. +tahoe-lafs now provides its statistics also in OpenMetrics format (for Prometheus et. al.) at `/statistics?t=openmetrics`. From 30771149fc7467fd7a61076b1572bcfddaa9a218 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Thu, 9 Sep 2021 23:31:39 +0000 Subject: [PATCH 09/35] Openmetrics: Add test case scaffold --- src/allmydata/test/test_openmetrics.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/allmydata/test/test_openmetrics.py diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py new file mode 100644 index 000000000..ad11f3468 --- /dev/null +++ b/src/allmydata/test/test_openmetrics.py @@ -0,0 +1,11 @@ +from twisted.trial import unittest + +class FakeStatsProvider(object): + def get_stats(self): + stats = {'stats': {}, 'counters': {}} + return stats + +class OpenMetrics(unittest.TestCase): + def test_spec_compliance(self): + self.assertEqual('1', '2') + From fca1482b35b2360d07ec700c0a22d20162bc5acb Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 10 Sep 2021 00:10:11 +0000 Subject: [PATCH 10/35] OpenMetrics Tests WIP --- src/allmydata/test/test_openmetrics.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index ad11f3468..2211c3561 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -1,4 +1,6 @@ +import mock from twisted.trial import unittest +from allmydata.web.status import Statistics class FakeStatsProvider(object): def get_stats(self): @@ -6,6 +8,18 @@ class FakeStatsProvider(object): return stats class OpenMetrics(unittest.TestCase): - def test_spec_compliance(self): - self.assertEqual('1', '2') + def test_header(self): + req = mock.Mock() + stats = mock.Mock() + stats._provider = FakeStatsProvider() + metrics = Statistics.render_OPENMETRICS(stats, req) + req.setHeader.assert_called_with("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8") + + def test_spec_compliance(self): + req = mock.Mock() + stats = mock.Mock() + stats._provider = FakeStatsProvider() + metrics = Statistics.render_OPENMETRICS(stats, req) + # TODO test that output adheres to spec + # TODO add more realistic stats, incl. missing (None) values From d04157d18a0fbe9a1832f94bf352b04e931039e1 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 10 Sep 2021 13:00:15 +0000 Subject: [PATCH 11/35] OpenMetrics test: Add parser to check against spec --- src/allmydata/test/test_openmetrics.py | 18 +++++++++++------- tox.ini | 2 ++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 2211c3561..cd5c5d407 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -1,4 +1,5 @@ import mock +from prometheus_client.openmetrics import parser from twisted.trial import unittest from allmydata.web.status import Statistics @@ -8,18 +9,21 @@ class FakeStatsProvider(object): return stats class OpenMetrics(unittest.TestCase): - def test_header(self): + def test_spec_compliance(self): + """ + Does our output adhere to the OpenMetrics spec? + https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md + """ req = mock.Mock() stats = mock.Mock() stats._provider = FakeStatsProvider() metrics = Statistics.render_OPENMETRICS(stats, req) + + # "The content type MUST be..." req.setHeader.assert_called_with("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8") - def test_spec_compliance(self): - req = mock.Mock() - stats = mock.Mock() - stats._provider = FakeStatsProvider() - metrics = Statistics.render_OPENMETRICS(stats, req) - # TODO test that output adheres to spec + # The parser throws if it can't parse. + # Wrap in a list() to drain the generator. + families = list(parser.text_string_to_metric_families(metrics)) # TODO add more realistic stats, incl. missing (None) values diff --git a/tox.ini b/tox.ini index 9b0f71038..0e8e58ea6 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,8 @@ deps = certifi # VCS hooks support py36,!coverage: pre-commit + # Does our OpenMetrics endpoint adhere to the spec: + prometheus-client==0.11.0 # We add usedevelop=False because testing against a true installation gives # more useful results. From 6c18983f7b182fe696dd466dab517312692c1648 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 10 Sep 2021 13:13:13 +0000 Subject: [PATCH 12/35] OpenMetrics test: Use realistic input data --- src/allmydata/test/test_openmetrics.py | 92 +++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index cd5c5d407..c7e26213a 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -5,7 +5,95 @@ from allmydata.web.status import Statistics class FakeStatsProvider(object): def get_stats(self): - stats = {'stats': {}, 'counters': {}} + # Parsed into a dict from a running tahoe's /statistics?t=json + stats = {'stats': { + 'storage_server.latencies.get.99_9_percentile': None, + 'storage_server.latencies.close.10_0_percentile': 0.00021910667419433594, + 'storage_server.latencies.read.01_0_percentile': 2.8848648071289062e-05, + 'storage_server.latencies.writev.99_9_percentile': None, + 'storage_server.latencies.read.99_9_percentile': None, + 'storage_server.latencies.allocate.99_0_percentile': 0.000988006591796875, + 'storage_server.latencies.writev.mean': 0.00045332245070571654, + 'storage_server.latencies.close.99_9_percentile': None, + 'cpu_monitor.15min_avg': 0.00017592000079223033, + 'storage_server.disk_free_for_root': 103289454592, + 'storage_server.latencies.get.99_0_percentile': 0.000347137451171875, + 'storage_server.latencies.get.mean': 0.00021158285060171353, + 'storage_server.latencies.read.90_0_percentile': 8.893013000488281e-05, + 'storage_server.latencies.write.01_0_percentile': 3.600120544433594e-05, + 'storage_server.latencies.write.99_9_percentile': 0.00017690658569335938, + 'storage_server.latencies.close.90_0_percentile': 0.00033211708068847656, + 'storage_server.disk_total': 103497859072, + 'storage_server.latencies.close.95_0_percentile': 0.0003509521484375, + 'storage_server.latencies.readv.samplesize': 1000, + 'storage_server.disk_free_for_nonroot': 103289454592, + 'storage_server.latencies.close.mean': 0.0002715024480059103, + 'storage_server.latencies.writev.95_0_percentile': 0.0007410049438476562, + 'storage_server.latencies.readv.90_0_percentile': 0.0003781318664550781, + 'storage_server.latencies.readv.99_0_percentile': 0.0004050731658935547, + 'storage_server.latencies.allocate.mean': 0.0007128627429454784, + 'storage_server.latencies.close.samplesize': 326, + 'storage_server.latencies.get.50_0_percentile': 0.0001819133758544922, + 'storage_server.latencies.write.50_0_percentile': 4.482269287109375e-05, + 'storage_server.latencies.readv.01_0_percentile': 0.0002970695495605469, + 'storage_server.latencies.get.10_0_percentile': 0.00015687942504882812, + 'storage_server.latencies.allocate.90_0_percentile': 0.0008189678192138672, + 'storage_server.latencies.get.samplesize': 472, + 'storage_server.total_bucket_count': 393, + 'storage_server.latencies.read.mean': 5.936201880959903e-05, + 'storage_server.latencies.allocate.01_0_percentile': 0.0004208087921142578, + 'storage_server.latencies.allocate.99_9_percentile': None, + 'storage_server.latencies.readv.mean': 0.00034061360359191893, + 'storage_server.disk_used': 208404480, + 'storage_server.latencies.allocate.50_0_percentile': 0.0007410049438476562, + 'storage_server.latencies.read.99_0_percentile': 0.00011992454528808594, + 'node.uptime': 3805759.8545179367, + 'storage_server.latencies.writev.10_0_percentile': 0.00035190582275390625, + 'storage_server.latencies.writev.90_0_percentile': 0.0006821155548095703, + 'storage_server.latencies.close.01_0_percentile': 0.00021505355834960938, + 'storage_server.latencies.close.50_0_percentile': 0.0002579689025878906, + 'cpu_monitor.1min_avg': 0.0002130000000003444, + 'storage_server.latencies.writev.50_0_percentile': 0.0004138946533203125, + 'storage_server.latencies.read.95_0_percentile': 9.107589721679688e-05, + 'storage_server.latencies.readv.95_0_percentile': 0.0003859996795654297, + 'storage_server.latencies.write.10_0_percentile': 3.719329833984375e-05, + 'storage_server.accepting_immutable_shares': 1, + 'storage_server.latencies.writev.samplesize': 309, + 'storage_server.latencies.get.95_0_percentile': 0.0003190040588378906, + 'storage_server.latencies.readv.10_0_percentile': 0.00032210350036621094, + 'storage_server.latencies.get.90_0_percentile': 0.0002999305725097656, + 'storage_server.latencies.get.01_0_percentile': 0.0001239776611328125, + 'cpu_monitor.total': 641.4941180000001, + 'storage_server.latencies.write.samplesize': 1000, + 'storage_server.latencies.write.95_0_percentile': 9.489059448242188e-05, + 'storage_server.latencies.read.50_0_percentile': 6.890296936035156e-05, + 'storage_server.latencies.writev.01_0_percentile': 0.00033211708068847656, + 'storage_server.latencies.read.10_0_percentile': 3.0994415283203125e-05, + 'storage_server.latencies.allocate.10_0_percentile': 0.0004949569702148438, + 'storage_server.reserved_space': 0, + 'storage_server.disk_avail': 103289454592, + 'storage_server.latencies.write.99_0_percentile': 0.00011301040649414062, + 'storage_server.latencies.write.90_0_percentile': 9.083747863769531e-05, + 'cpu_monitor.5min_avg': 0.0002370666691157502, + 'storage_server.latencies.write.mean': 5.8008909225463864e-05, + 'storage_server.latencies.readv.50_0_percentile': 0.00033020973205566406, + 'storage_server.latencies.close.99_0_percentile': 0.0004038810729980469, + 'storage_server.allocated': 0, + 'storage_server.latencies.writev.99_0_percentile': 0.0007710456848144531, + 'storage_server.latencies.readv.99_9_percentile': 0.0004780292510986328, + 'storage_server.latencies.read.samplesize': 170, + 'storage_server.latencies.allocate.samplesize': 406, + 'storage_server.latencies.allocate.95_0_percentile': 0.0008411407470703125 + }, 'counters': { + 'storage_server.writev': 309, + 'storage_server.bytes_added': 197836146, + 'storage_server.close': 326, + 'storage_server.readv': 14299, + 'storage_server.allocate': 406, + 'storage_server.read': 170, + 'storage_server.write': 3775, + 'storage_server.get': 472} + } return stats class OpenMetrics(unittest.TestCase): @@ -25,5 +113,3 @@ class OpenMetrics(unittest.TestCase): # The parser throws if it can't parse. # Wrap in a list() to drain the generator. families = list(parser.text_string_to_metric_families(metrics)) - # TODO add more realistic stats, incl. missing (None) values - From 339e1747e7b2636a49c4155f9786e79fdb37de3f Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 10 Sep 2021 13:15:56 +0000 Subject: [PATCH 13/35] clean up --- src/allmydata/test/test_openmetrics.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index c7e26213a..9ef7c9c8e 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -108,8 +108,10 @@ class OpenMetrics(unittest.TestCase): metrics = Statistics.render_OPENMETRICS(stats, req) # "The content type MUST be..." - req.setHeader.assert_called_with("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8") + req.setHeader.assert_called_with( + "content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8" + ) - # The parser throws if it can't parse. - # Wrap in a list() to drain the generator. + # The parser throws if it does not like its input. + # Wrapped in a list() to drain the generator. families = list(parser.text_string_to_metric_families(metrics)) From ad84f5df2b9310df2fac25c5dc989c4044af491b Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 10 Sep 2021 13:21:06 +0000 Subject: [PATCH 14/35] newline at the end. --- src/allmydata/test/test_openmetrics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 9ef7c9c8e..fdb645b42 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -115,3 +115,4 @@ class OpenMetrics(unittest.TestCase): # The parser throws if it does not like its input. # Wrapped in a list() to drain the generator. families = list(parser.text_string_to_metric_families(metrics)) + From 88a2e7a4fb993fb28a14a6cfe1ac1025a998bb93 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 10:09:55 +0000 Subject: [PATCH 15/35] OpenMetrics test suite: Get rid of status mock --- src/allmydata/test/test_openmetrics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index fdb645b42..57ed989e0 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -96,6 +96,10 @@ class FakeStatsProvider(object): } return stats +class FakeStats(): + def __init__(self): + self._provider = FakeStatsProvider() + class OpenMetrics(unittest.TestCase): def test_spec_compliance(self): """ @@ -103,8 +107,7 @@ class OpenMetrics(unittest.TestCase): https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md """ req = mock.Mock() - stats = mock.Mock() - stats._provider = FakeStatsProvider() + stats = FakeStats() metrics = Statistics.render_OPENMETRICS(stats, req) # "The content type MUST be..." From 57a3c1168e98fbce9b6b10d8a20100f4384a1620 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:03:31 +0000 Subject: [PATCH 16/35] OpenMetrics: Use list of strings instead of string concatenation --- src/allmydata/web/status.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 6d36861ad..2542ca75f 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1556,7 +1556,7 @@ class Statistics(MultiFormatResource): def render_OPENMETRICS(self, req): req.setHeader("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8") stats = self._provider.get_stats() - ret = u"" + ret = [] def mangle_name(name): return re.sub( @@ -1569,13 +1569,13 @@ class Statistics(MultiFormatResource): return str(val) if val is not None else u"NaN" for (k, v) in sorted(stats['counters'].items()): - ret += u"tahoe_counters_%s %s\n" % (mangle_name(k), mangle_value(v)) + ret.append(u"tahoe_counters_%s %s" % (mangle_name(k), mangle_value(v))) for (k, v) in sorted(stats['stats'].items()): - ret += u"tahoe_stats_%s %s\n" % (mangle_name(k), mangle_value(v)) + ret.append(u"tahoe_stats_%s %s" % (mangle_name(k), mangle_value(v))) - ret += u"# EOF\n" + ret.append(u"# EOF") - return ret + return u"\n".join(ret) class StatisticsElement(Element): From d864cab5b0861a7c4a4caec294bafac884f318a6 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:14:52 +0000 Subject: [PATCH 17/35] OpenMetrics: Add test dep to nix packaging --- nix/tahoe-lafs.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/tahoe-lafs.nix b/nix/tahoe-lafs.nix index 35b29f1cc..17b2f4463 100644 --- a/nix/tahoe-lafs.nix +++ b/nix/tahoe-lafs.nix @@ -107,6 +107,7 @@ EOF beautifulsoup4 html5lib tenacity + prometheus_client ]; checkPhase = '' From c66ae302c8412365c0f3848298351a1d69d975d8 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:26:30 +0000 Subject: [PATCH 18/35] OpenMetrics: Extra newline at the end --- src/allmydata/web/status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 2542ca75f..7b16a60b2 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1573,7 +1573,7 @@ class Statistics(MultiFormatResource): for (k, v) in sorted(stats['stats'].items()): ret.append(u"tahoe_stats_%s %s" % (mangle_name(k), mangle_value(v))) - ret.append(u"# EOF") + ret.append(u"# EOF\n") return u"\n".join(ret) From cbe5ea1115e3e77e86d69a4dc2b29e508724e18e Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:28:39 +0000 Subject: [PATCH 19/35] OpenMetrics: Add docstring --- src/allmydata/web/status.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 7b16a60b2..65647f491 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1554,6 +1554,12 @@ class Statistics(MultiFormatResource): @render_exception def render_OPENMETRICS(self, req): + """ + Render our stats in `OpenMetrics ` format. + For example Prometheus and Victoriametrics can parse this. + Point the scraper to ``/statistics?t=openmetrics`` (instead of the + default ``/metrics``). + """ req.setHeader("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8") stats = self._provider.get_stats() ret = [] From 21c471ed8113ce29fd1b6c6dc02e2acf124d550a Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:39:32 +0000 Subject: [PATCH 20/35] OpenMetrics test: Add hopefully more stable URIs to OpenMetrics spec info --- src/allmydata/test/test_openmetrics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 57ed989e0..7753960d4 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -103,8 +103,9 @@ class FakeStats(): class OpenMetrics(unittest.TestCase): def test_spec_compliance(self): """ - Does our output adhere to the OpenMetrics spec? - https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md + Does our output adhere to the `OpenMetrics ` spec? + https://github.com/OpenObservability/OpenMetrics/ + https://prometheus.io/docs/instrumenting/exposition_formats/ """ req = mock.Mock() stats = FakeStats() From 6bcff5472b9aca6dd09e585e80bb937832d254e7 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:50:20 +0000 Subject: [PATCH 21/35] OpenMetrics test suite: Add a check to see whether our stats were parsed at all. --- src/allmydata/test/test_openmetrics.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 7753960d4..a9a0e5712 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -120,3 +120,7 @@ class OpenMetrics(unittest.TestCase): # Wrapped in a list() to drain the generator. families = list(parser.text_string_to_metric_families(metrics)) + # Has the parser parsed our data? + # Just check the last item. + self.assertEqual(families[-1].name, u"tahoe_stats_storage_server_total_bucket_count") + From 383ab4729a7c362f25526097c3f6c66e9451ef2c Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 11:53:48 +0000 Subject: [PATCH 22/35] OpenMetrics tests: Tryfix resolve TypeError on CI Was: > TypeError: unbound method render_OPENMETRICS() must be called with Statistics instance as first argument (got FakeStats instance instead) --- src/allmydata/test/test_openmetrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index a9a0e5712..9c840714c 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -96,7 +96,7 @@ class FakeStatsProvider(object): } return stats -class FakeStats(): +class FakeStats(Statistics): def __init__(self): self._provider = FakeStatsProvider() From b0e1cf924d44b271314efac94106247afdc7be10 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Wed, 15 Sep 2021 15:14:29 +0000 Subject: [PATCH 23/35] OpenMetrics test: White space only: Format JSON fixture to be easier on the eyes --- src/allmydata/test/test_openmetrics.py | 177 +++++++++++++------------ 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 9c840714c..7d9bd6429 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -6,94 +6,95 @@ from allmydata.web.status import Statistics class FakeStatsProvider(object): def get_stats(self): # Parsed into a dict from a running tahoe's /statistics?t=json - stats = {'stats': { - 'storage_server.latencies.get.99_9_percentile': None, - 'storage_server.latencies.close.10_0_percentile': 0.00021910667419433594, - 'storage_server.latencies.read.01_0_percentile': 2.8848648071289062e-05, - 'storage_server.latencies.writev.99_9_percentile': None, - 'storage_server.latencies.read.99_9_percentile': None, - 'storage_server.latencies.allocate.99_0_percentile': 0.000988006591796875, - 'storage_server.latencies.writev.mean': 0.00045332245070571654, - 'storage_server.latencies.close.99_9_percentile': None, - 'cpu_monitor.15min_avg': 0.00017592000079223033, - 'storage_server.disk_free_for_root': 103289454592, - 'storage_server.latencies.get.99_0_percentile': 0.000347137451171875, - 'storage_server.latencies.get.mean': 0.00021158285060171353, - 'storage_server.latencies.read.90_0_percentile': 8.893013000488281e-05, - 'storage_server.latencies.write.01_0_percentile': 3.600120544433594e-05, - 'storage_server.latencies.write.99_9_percentile': 0.00017690658569335938, - 'storage_server.latencies.close.90_0_percentile': 0.00033211708068847656, - 'storage_server.disk_total': 103497859072, - 'storage_server.latencies.close.95_0_percentile': 0.0003509521484375, - 'storage_server.latencies.readv.samplesize': 1000, - 'storage_server.disk_free_for_nonroot': 103289454592, - 'storage_server.latencies.close.mean': 0.0002715024480059103, - 'storage_server.latencies.writev.95_0_percentile': 0.0007410049438476562, - 'storage_server.latencies.readv.90_0_percentile': 0.0003781318664550781, - 'storage_server.latencies.readv.99_0_percentile': 0.0004050731658935547, - 'storage_server.latencies.allocate.mean': 0.0007128627429454784, - 'storage_server.latencies.close.samplesize': 326, - 'storage_server.latencies.get.50_0_percentile': 0.0001819133758544922, - 'storage_server.latencies.write.50_0_percentile': 4.482269287109375e-05, - 'storage_server.latencies.readv.01_0_percentile': 0.0002970695495605469, - 'storage_server.latencies.get.10_0_percentile': 0.00015687942504882812, - 'storage_server.latencies.allocate.90_0_percentile': 0.0008189678192138672, - 'storage_server.latencies.get.samplesize': 472, - 'storage_server.total_bucket_count': 393, - 'storage_server.latencies.read.mean': 5.936201880959903e-05, - 'storage_server.latencies.allocate.01_0_percentile': 0.0004208087921142578, - 'storage_server.latencies.allocate.99_9_percentile': None, - 'storage_server.latencies.readv.mean': 0.00034061360359191893, - 'storage_server.disk_used': 208404480, - 'storage_server.latencies.allocate.50_0_percentile': 0.0007410049438476562, - 'storage_server.latencies.read.99_0_percentile': 0.00011992454528808594, - 'node.uptime': 3805759.8545179367, - 'storage_server.latencies.writev.10_0_percentile': 0.00035190582275390625, - 'storage_server.latencies.writev.90_0_percentile': 0.0006821155548095703, - 'storage_server.latencies.close.01_0_percentile': 0.00021505355834960938, - 'storage_server.latencies.close.50_0_percentile': 0.0002579689025878906, - 'cpu_monitor.1min_avg': 0.0002130000000003444, - 'storage_server.latencies.writev.50_0_percentile': 0.0004138946533203125, - 'storage_server.latencies.read.95_0_percentile': 9.107589721679688e-05, - 'storage_server.latencies.readv.95_0_percentile': 0.0003859996795654297, - 'storage_server.latencies.write.10_0_percentile': 3.719329833984375e-05, - 'storage_server.accepting_immutable_shares': 1, - 'storage_server.latencies.writev.samplesize': 309, - 'storage_server.latencies.get.95_0_percentile': 0.0003190040588378906, - 'storage_server.latencies.readv.10_0_percentile': 0.00032210350036621094, - 'storage_server.latencies.get.90_0_percentile': 0.0002999305725097656, - 'storage_server.latencies.get.01_0_percentile': 0.0001239776611328125, - 'cpu_monitor.total': 641.4941180000001, - 'storage_server.latencies.write.samplesize': 1000, - 'storage_server.latencies.write.95_0_percentile': 9.489059448242188e-05, - 'storage_server.latencies.read.50_0_percentile': 6.890296936035156e-05, - 'storage_server.latencies.writev.01_0_percentile': 0.00033211708068847656, - 'storage_server.latencies.read.10_0_percentile': 3.0994415283203125e-05, - 'storage_server.latencies.allocate.10_0_percentile': 0.0004949569702148438, - 'storage_server.reserved_space': 0, - 'storage_server.disk_avail': 103289454592, - 'storage_server.latencies.write.99_0_percentile': 0.00011301040649414062, - 'storage_server.latencies.write.90_0_percentile': 9.083747863769531e-05, - 'cpu_monitor.5min_avg': 0.0002370666691157502, - 'storage_server.latencies.write.mean': 5.8008909225463864e-05, - 'storage_server.latencies.readv.50_0_percentile': 0.00033020973205566406, - 'storage_server.latencies.close.99_0_percentile': 0.0004038810729980469, - 'storage_server.allocated': 0, - 'storage_server.latencies.writev.99_0_percentile': 0.0007710456848144531, - 'storage_server.latencies.readv.99_9_percentile': 0.0004780292510986328, - 'storage_server.latencies.read.samplesize': 170, - 'storage_server.latencies.allocate.samplesize': 406, - 'storage_server.latencies.allocate.95_0_percentile': 0.0008411407470703125 - }, 'counters': { - 'storage_server.writev': 309, - 'storage_server.bytes_added': 197836146, - 'storage_server.close': 326, - 'storage_server.readv': 14299, - 'storage_server.allocate': 406, - 'storage_server.read': 170, - 'storage_server.write': 3775, - 'storage_server.get': 472} - } + stats = { + 'stats': { + 'storage_server.latencies.get.99_9_percentile': None, + 'storage_server.latencies.close.10_0_percentile': 0.00021910667419433594, + 'storage_server.latencies.read.01_0_percentile': 2.8848648071289062e-05, + 'storage_server.latencies.writev.99_9_percentile': None, + 'storage_server.latencies.read.99_9_percentile': None, + 'storage_server.latencies.allocate.99_0_percentile': 0.000988006591796875, + 'storage_server.latencies.writev.mean': 0.00045332245070571654, + 'storage_server.latencies.close.99_9_percentile': None, + 'cpu_monitor.15min_avg': 0.00017592000079223033, + 'storage_server.disk_free_for_root': 103289454592, + 'storage_server.latencies.get.99_0_percentile': 0.000347137451171875, + 'storage_server.latencies.get.mean': 0.00021158285060171353, + 'storage_server.latencies.read.90_0_percentile': 8.893013000488281e-05, + 'storage_server.latencies.write.01_0_percentile': 3.600120544433594e-05, + 'storage_server.latencies.write.99_9_percentile': 0.00017690658569335938, + 'storage_server.latencies.close.90_0_percentile': 0.00033211708068847656, + 'storage_server.disk_total': 103497859072, + 'storage_server.latencies.close.95_0_percentile': 0.0003509521484375, + 'storage_server.latencies.readv.samplesize': 1000, + 'storage_server.disk_free_for_nonroot': 103289454592, + 'storage_server.latencies.close.mean': 0.0002715024480059103, + 'storage_server.latencies.writev.95_0_percentile': 0.0007410049438476562, + 'storage_server.latencies.readv.90_0_percentile': 0.0003781318664550781, + 'storage_server.latencies.readv.99_0_percentile': 0.0004050731658935547, + 'storage_server.latencies.allocate.mean': 0.0007128627429454784, + 'storage_server.latencies.close.samplesize': 326, + 'storage_server.latencies.get.50_0_percentile': 0.0001819133758544922, + 'storage_server.latencies.write.50_0_percentile': 4.482269287109375e-05, + 'storage_server.latencies.readv.01_0_percentile': 0.0002970695495605469, + 'storage_server.latencies.get.10_0_percentile': 0.00015687942504882812, + 'storage_server.latencies.allocate.90_0_percentile': 0.0008189678192138672, + 'storage_server.latencies.get.samplesize': 472, + 'storage_server.total_bucket_count': 393, + 'storage_server.latencies.read.mean': 5.936201880959903e-05, + 'storage_server.latencies.allocate.01_0_percentile': 0.0004208087921142578, + 'storage_server.latencies.allocate.99_9_percentile': None, + 'storage_server.latencies.readv.mean': 0.00034061360359191893, + 'storage_server.disk_used': 208404480, + 'storage_server.latencies.allocate.50_0_percentile': 0.0007410049438476562, + 'storage_server.latencies.read.99_0_percentile': 0.00011992454528808594, + 'node.uptime': 3805759.8545179367, + 'storage_server.latencies.writev.10_0_percentile': 0.00035190582275390625, + 'storage_server.latencies.writev.90_0_percentile': 0.0006821155548095703, + 'storage_server.latencies.close.01_0_percentile': 0.00021505355834960938, + 'storage_server.latencies.close.50_0_percentile': 0.0002579689025878906, + 'cpu_monitor.1min_avg': 0.0002130000000003444, + 'storage_server.latencies.writev.50_0_percentile': 0.0004138946533203125, + 'storage_server.latencies.read.95_0_percentile': 9.107589721679688e-05, + 'storage_server.latencies.readv.95_0_percentile': 0.0003859996795654297, + 'storage_server.latencies.write.10_0_percentile': 3.719329833984375e-05, + 'storage_server.accepting_immutable_shares': 1, + 'storage_server.latencies.writev.samplesize': 309, + 'storage_server.latencies.get.95_0_percentile': 0.0003190040588378906, + 'storage_server.latencies.readv.10_0_percentile': 0.00032210350036621094, + 'storage_server.latencies.get.90_0_percentile': 0.0002999305725097656, + 'storage_server.latencies.get.01_0_percentile': 0.0001239776611328125, + 'cpu_monitor.total': 641.4941180000001, + 'storage_server.latencies.write.samplesize': 1000, + 'storage_server.latencies.write.95_0_percentile': 9.489059448242188e-05, + 'storage_server.latencies.read.50_0_percentile': 6.890296936035156e-05, + 'storage_server.latencies.writev.01_0_percentile': 0.00033211708068847656, + 'storage_server.latencies.read.10_0_percentile': 3.0994415283203125e-05, + 'storage_server.latencies.allocate.10_0_percentile': 0.0004949569702148438, + 'storage_server.reserved_space': 0, + 'storage_server.disk_avail': 103289454592, + 'storage_server.latencies.write.99_0_percentile': 0.00011301040649414062, + 'storage_server.latencies.write.90_0_percentile': 9.083747863769531e-05, + 'cpu_monitor.5min_avg': 0.0002370666691157502, + 'storage_server.latencies.write.mean': 5.8008909225463864e-05, + 'storage_server.latencies.readv.50_0_percentile': 0.00033020973205566406, + 'storage_server.latencies.close.99_0_percentile': 0.0004038810729980469, + 'storage_server.allocated': 0, + 'storage_server.latencies.writev.99_0_percentile': 0.0007710456848144531, + 'storage_server.latencies.readv.99_9_percentile': 0.0004780292510986328, + 'storage_server.latencies.read.samplesize': 170, + 'storage_server.latencies.allocate.samplesize': 406, + 'storage_server.latencies.allocate.95_0_percentile': 0.0008411407470703125}, + 'counters': { + 'storage_server.writev': 309, + 'storage_server.bytes_added': 197836146, + 'storage_server.close': 326, + 'storage_server.readv': 14299, + 'storage_server.allocate': 406, + 'storage_server.read': 170, + 'storage_server.write': 3775, + 'storage_server.get': 472} + } return stats class FakeStats(Statistics): From 5825b8bd42328618dfdc9f7a2aa02dd761fe2887 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Thu, 16 Sep 2021 15:58:04 +0000 Subject: [PATCH 24/35] OpenMetrics: rework test suite with exarkun --- src/allmydata/test/test_openmetrics.py | 90 ++++++++++++++++++++------ 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 7d9bd6429..a0822d594 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -1,7 +1,25 @@ -import mock from prometheus_client.openmetrics import parser -from twisted.trial import unittest + +from treq.testing import RequestTraversalAgent + +from twisted.web.http import OK +from twisted.web.client import readBody +from twisted.web.resource import Resource + +from testtools.twistedsupport import succeeded +from testtools.matchers import ( + Always, + AfterPreprocessing, + Equals, + MatchesAll, + MatchesStructure, + MatchesPredicate, + ) +from testtools.content import text_content + from allmydata.web.status import Statistics +from allmydata.test.common import SyncTestCase + class FakeStatsProvider(object): def get_stats(self): @@ -97,31 +115,67 @@ class FakeStatsProvider(object): } return stats -class FakeStats(Statistics): - def __init__(self): - self._provider = FakeStatsProvider() +class HackItResource(Resource): + def getChildWithDefault(self, path, request): + request.fields = None + return Resource.getChildWithDefault(self, path, request) -class OpenMetrics(unittest.TestCase): + +class OpenMetrics(SyncTestCase): def test_spec_compliance(self): """ Does our output adhere to the `OpenMetrics ` spec? https://github.com/OpenObservability/OpenMetrics/ https://prometheus.io/docs/instrumenting/exposition_formats/ """ - req = mock.Mock() - stats = FakeStats() - metrics = Statistics.render_OPENMETRICS(stats, req) + root = HackItResource() + root.putChild(b"", Statistics(FakeStatsProvider())) + rta = RequestTraversalAgent(root) + d = rta.request(b"GET", b"http://localhost/?t=openmetrics") + self.assertThat(d, succeeded(matches_stats(self))) - # "The content type MUST be..." - req.setHeader.assert_called_with( - "content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8" +def matches_stats(testcase): + def add_detail(testcase): + def predicate(body): + testcase.addDetail("body", text_content(body)) + return True + return predicate + + return MatchesAll( + MatchesStructure( + code=Equals(OK), + # "The content type MUST be..." + headers=has_header("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8"), + ), + AfterPreprocessing( + readBodyText, + succeeded(MatchesAll( + MatchesPredicate(add_detail(testcase), "%s dummy"), + parses_as_openmetrics(), + )) ) + ) - # The parser throws if it does not like its input. - # Wrapped in a list() to drain the generator. - families = list(parser.text_string_to_metric_families(metrics)) +def readBodyText(response): + d = readBody(response) + d.addCallback(lambda body: body.decode("utf-8")) + return d + +def has_header(name, value): + return AfterPreprocessing( + lambda headers: headers.getRawHeaders(name), + Equals([value]), + ) + +def parses_as_openmetrics(): + # The parser throws if it does not like its input. + # Wrapped in a list() to drain the generator. + return AfterPreprocessing( + lambda body: list(parser.text_string_to_metric_families(body)), + AfterPreprocessing( + lambda families: families[-1].name, + Equals(u"tahoe_stats_storage_server_total_bucket_count"), + ), + ) - # Has the parser parsed our data? - # Just check the last item. - self.assertEqual(families[-1].name, u"tahoe_stats_storage_server_total_bucket_count") From a2378d0e704add3741aa269adc37ac3f781ac36a Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 17 Sep 2021 12:04:12 +0000 Subject: [PATCH 25/35] OpenMetrics test suite: Make CI happy: No old style objects --- src/allmydata/test/test_openmetrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index a0822d594..43704c4a3 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -115,7 +115,7 @@ class FakeStatsProvider(object): } return stats -class HackItResource(Resource): +class HackItResource(Resource, object): def getChildWithDefault(self, path, request): request.fields = None return Resource.getChildWithDefault(self, path, request) From fb0335cc17c709d5663f7ea2c31ccb98b6d6ddf1 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 17 Sep 2021 12:16:37 +0000 Subject: [PATCH 26/35] OpenMetrics test suite: More clean up The Linter complains: > 'testtools.matchers.Always' imported but unused --- src/allmydata/test/test_openmetrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 43704c4a3..91692af33 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -8,7 +8,6 @@ from twisted.web.resource import Resource from testtools.twistedsupport import succeeded from testtools.matchers import ( - Always, AfterPreprocessing, Equals, MatchesAll, From e5e0d71ef518c73e3fe2a0bc860e1c7428ba91a9 Mon Sep 17 00:00:00 2001 From: Florian Sesser Date: Fri, 17 Sep 2021 13:20:59 +0000 Subject: [PATCH 27/35] OpenMetrics test suite: More clean ups trailing whitespace --- src/allmydata/test/test_openmetrics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 91692af33..9677de4a9 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -3,7 +3,7 @@ from prometheus_client.openmetrics import parser from treq.testing import RequestTraversalAgent from twisted.web.http import OK -from twisted.web.client import readBody +from twisted.web.client import readBody from twisted.web.resource import Resource from testtools.twistedsupport import succeeded @@ -128,7 +128,7 @@ class OpenMetrics(SyncTestCase): https://prometheus.io/docs/instrumenting/exposition_formats/ """ root = HackItResource() - root.putChild(b"", Statistics(FakeStatsProvider())) + root.putChild(b"", Statistics(FakeStatsProvider())) rta = RequestTraversalAgent(root) d = rta.request(b"GET", b"http://localhost/?t=openmetrics") self.assertThat(d, succeeded(matches_stats(self))) @@ -168,11 +168,11 @@ def has_header(name, value): def parses_as_openmetrics(): # The parser throws if it does not like its input. - # Wrapped in a list() to drain the generator. + # Wrapped in a list() to drain the generator. return AfterPreprocessing( lambda body: list(parser.text_string_to_metric_families(body)), AfterPreprocessing( - lambda families: families[-1].name, + lambda families: families[-1].name, Equals(u"tahoe_stats_storage_server_total_bucket_count"), ), ) From 5e26f25b3714167a628e93ae76a529b074fb4c69 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:41:43 -0400 Subject: [PATCH 28/35] It's ported to Python 3! --- src/allmydata/test/test_openmetrics.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 9677de4a9..350d6abbd 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -1,3 +1,18 @@ +""" +Tests for ``/statistics?t=openmetrics``. + +Ported to Python 3. +""" + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + from prometheus_client.openmetrics import parser from treq.testing import RequestTraversalAgent From f8c07bfd11edfee9b62b8bb13cbe46f643267322 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:42:59 -0400 Subject: [PATCH 29/35] add some docstrings --- src/allmydata/test/test_openmetrics.py | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 350d6abbd..34dcd266b 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -36,6 +36,10 @@ from allmydata.test.common import SyncTestCase class FakeStatsProvider(object): + """ + A stats provider that hands backed a canned collection of performance + statistics. + """ def get_stats(self): # Parsed into a dict from a running tahoe's /statistics?t=json stats = { @@ -130,12 +134,21 @@ class FakeStatsProvider(object): return stats class HackItResource(Resource, object): + """ + A bridge between ``RequestTraversalAgent`` and ``MultiFormatResource`` + (used by ``Statistics``). ``MultiFormatResource`` expects the request + object to have a ``fields`` attribute but Twisted's ``IRequest`` has no + such attribute. Create it here. + """ def getChildWithDefault(self, path, request): request.fields = None return Resource.getChildWithDefault(self, path, request) class OpenMetrics(SyncTestCase): + """ + Tests for ``/statistics?t=openmetrics``. + """ def test_spec_compliance(self): """ Does our output adhere to the `OpenMetrics ` spec? @@ -171,17 +184,41 @@ def matches_stats(testcase): ) def readBodyText(response): + """ + Read the response body and decode it using UTF-8. + + :param twisted.web.iweb.IResponse response: The response from which to + read the body. + + :return: A ``Deferred`` that fires with the ``str`` body. + """ d = readBody(response) d.addCallback(lambda body: body.decode("utf-8")) return d def has_header(name, value): + """ + Create a matcher that matches a response object that includes the given + name / value pair. + + :param str name: The name of the item in the HTTP header to match. + :param str value: The value of the item in the HTTP header to match by equality. + + :return: A matcher. + """ return AfterPreprocessing( lambda headers: headers.getRawHeaders(name), Equals([value]), ) def parses_as_openmetrics(): + """ + Create a matcher that matches a ``str`` string that can be parsed as an + OpenMetrics response and includes a certain well-known value expected by + the tests. + + :return: A matcher. + """ # The parser throws if it does not like its input. # Wrapped in a list() to drain the generator. return AfterPreprocessing( From 4d8164773c404373f4aeea37f2969680fa02c265 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:43:18 -0400 Subject: [PATCH 30/35] factor helper function out to top-level --- src/allmydata/test/test_openmetrics.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 34dcd266b..a92098c88 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -162,11 +162,6 @@ class OpenMetrics(SyncTestCase): self.assertThat(d, succeeded(matches_stats(self))) def matches_stats(testcase): - def add_detail(testcase): - def predicate(body): - testcase.addDetail("body", text_content(body)) - return True - return predicate return MatchesAll( MatchesStructure( @@ -177,12 +172,26 @@ def matches_stats(testcase): AfterPreprocessing( readBodyText, succeeded(MatchesAll( - MatchesPredicate(add_detail(testcase), "%s dummy"), + MatchesPredicate(add_detail(testcase, u"response body"), u"%s dummy"), parses_as_openmetrics(), )) ) ) +def add_detail(testcase, name): + """ + Create a matcher that always matches and as a side-effect adds the matched + value as detail to the testcase. + + :param testtools.TestCase testcase: The case to which to add the detail. + + :return: A matcher. + """ + def predicate(value): + testcase.addDetail(name, text_content(value)) + return True + return predicate + def readBodyText(response): """ Read the response body and decode it using UTF-8. From cbb96bd57a2fa3fa5a0a11662ea4890a5ad9bc41 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:43:33 -0400 Subject: [PATCH 31/35] one more docstring --- src/allmydata/test/test_openmetrics.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index a92098c88..385bf32a8 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -162,7 +162,20 @@ class OpenMetrics(SyncTestCase): self.assertThat(d, succeeded(matches_stats(self))) def matches_stats(testcase): + """ + Create a matcher that matches a response that confirms to the OpenMetrics + specification. + * The ``Content-Type`` is **application/openmetrics-text; version=1.0.0; charset=utf-8**. + * The status is **OK**. + * The body can be parsed by an OpenMetrics parser. + * The metric families in the body are grouped and sorted. + * At least one of the expected families appears in the body. + + :param testtools.TestCase testcase: The case to which to add detail about the matching process. + + :return: A matcher. + """ return MatchesAll( MatchesStructure( code=Equals(OK), From f66a8ab1369f197163832a940c25efdb44f3dcde Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:43:37 -0400 Subject: [PATCH 32/35] formatting and explicit unicode string literals --- src/allmydata/test/test_openmetrics.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 385bf32a8..d74958ffd 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -180,7 +180,10 @@ def matches_stats(testcase): MatchesStructure( code=Equals(OK), # "The content type MUST be..." - headers=has_header("content-type", "application/openmetrics-text; version=1.0.0; charset=utf-8"), + headers=has_header( + u"content-type", + u"application/openmetrics-text; version=1.0.0; charset=utf-8", + ), ), AfterPreprocessing( readBodyText, From 4b6d00221e289a94fa9603dc0d1050605c0411ab Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:48:19 -0400 Subject: [PATCH 33/35] protect this crazy line from black --- src/allmydata/test/test_openmetrics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index d74958ffd..71433c11a 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -10,8 +10,11 @@ from __future__ import division from __future__ import unicode_literals from future.utils import PY2 + if PY2: + # fmt: off from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + # fmt: on from prometheus_client.openmetrics import parser From 2f60ab300ba724bd67c0ca0825af089eecf13e36 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:48:27 -0400 Subject: [PATCH 34/35] black formatting --- src/allmydata/test/test_openmetrics.py | 211 +++++++++++++------------ 1 file changed, 112 insertions(+), 99 deletions(-) diff --git a/src/allmydata/test/test_openmetrics.py b/src/allmydata/test/test_openmetrics.py index 71433c11a..66cbc7dec 100644 --- a/src/allmydata/test/test_openmetrics.py +++ b/src/allmydata/test/test_openmetrics.py @@ -31,7 +31,7 @@ from testtools.matchers import ( MatchesAll, MatchesStructure, MatchesPredicate, - ) +) from testtools.content import text_content from allmydata.web.status import Statistics @@ -43,99 +43,103 @@ class FakeStatsProvider(object): A stats provider that hands backed a canned collection of performance statistics. """ + def get_stats(self): # Parsed into a dict from a running tahoe's /statistics?t=json stats = { - 'stats': { - 'storage_server.latencies.get.99_9_percentile': None, - 'storage_server.latencies.close.10_0_percentile': 0.00021910667419433594, - 'storage_server.latencies.read.01_0_percentile': 2.8848648071289062e-05, - 'storage_server.latencies.writev.99_9_percentile': None, - 'storage_server.latencies.read.99_9_percentile': None, - 'storage_server.latencies.allocate.99_0_percentile': 0.000988006591796875, - 'storage_server.latencies.writev.mean': 0.00045332245070571654, - 'storage_server.latencies.close.99_9_percentile': None, - 'cpu_monitor.15min_avg': 0.00017592000079223033, - 'storage_server.disk_free_for_root': 103289454592, - 'storage_server.latencies.get.99_0_percentile': 0.000347137451171875, - 'storage_server.latencies.get.mean': 0.00021158285060171353, - 'storage_server.latencies.read.90_0_percentile': 8.893013000488281e-05, - 'storage_server.latencies.write.01_0_percentile': 3.600120544433594e-05, - 'storage_server.latencies.write.99_9_percentile': 0.00017690658569335938, - 'storage_server.latencies.close.90_0_percentile': 0.00033211708068847656, - 'storage_server.disk_total': 103497859072, - 'storage_server.latencies.close.95_0_percentile': 0.0003509521484375, - 'storage_server.latencies.readv.samplesize': 1000, - 'storage_server.disk_free_for_nonroot': 103289454592, - 'storage_server.latencies.close.mean': 0.0002715024480059103, - 'storage_server.latencies.writev.95_0_percentile': 0.0007410049438476562, - 'storage_server.latencies.readv.90_0_percentile': 0.0003781318664550781, - 'storage_server.latencies.readv.99_0_percentile': 0.0004050731658935547, - 'storage_server.latencies.allocate.mean': 0.0007128627429454784, - 'storage_server.latencies.close.samplesize': 326, - 'storage_server.latencies.get.50_0_percentile': 0.0001819133758544922, - 'storage_server.latencies.write.50_0_percentile': 4.482269287109375e-05, - 'storage_server.latencies.readv.01_0_percentile': 0.0002970695495605469, - 'storage_server.latencies.get.10_0_percentile': 0.00015687942504882812, - 'storage_server.latencies.allocate.90_0_percentile': 0.0008189678192138672, - 'storage_server.latencies.get.samplesize': 472, - 'storage_server.total_bucket_count': 393, - 'storage_server.latencies.read.mean': 5.936201880959903e-05, - 'storage_server.latencies.allocate.01_0_percentile': 0.0004208087921142578, - 'storage_server.latencies.allocate.99_9_percentile': None, - 'storage_server.latencies.readv.mean': 0.00034061360359191893, - 'storage_server.disk_used': 208404480, - 'storage_server.latencies.allocate.50_0_percentile': 0.0007410049438476562, - 'storage_server.latencies.read.99_0_percentile': 0.00011992454528808594, - 'node.uptime': 3805759.8545179367, - 'storage_server.latencies.writev.10_0_percentile': 0.00035190582275390625, - 'storage_server.latencies.writev.90_0_percentile': 0.0006821155548095703, - 'storage_server.latencies.close.01_0_percentile': 0.00021505355834960938, - 'storage_server.latencies.close.50_0_percentile': 0.0002579689025878906, - 'cpu_monitor.1min_avg': 0.0002130000000003444, - 'storage_server.latencies.writev.50_0_percentile': 0.0004138946533203125, - 'storage_server.latencies.read.95_0_percentile': 9.107589721679688e-05, - 'storage_server.latencies.readv.95_0_percentile': 0.0003859996795654297, - 'storage_server.latencies.write.10_0_percentile': 3.719329833984375e-05, - 'storage_server.accepting_immutable_shares': 1, - 'storage_server.latencies.writev.samplesize': 309, - 'storage_server.latencies.get.95_0_percentile': 0.0003190040588378906, - 'storage_server.latencies.readv.10_0_percentile': 0.00032210350036621094, - 'storage_server.latencies.get.90_0_percentile': 0.0002999305725097656, - 'storage_server.latencies.get.01_0_percentile': 0.0001239776611328125, - 'cpu_monitor.total': 641.4941180000001, - 'storage_server.latencies.write.samplesize': 1000, - 'storage_server.latencies.write.95_0_percentile': 9.489059448242188e-05, - 'storage_server.latencies.read.50_0_percentile': 6.890296936035156e-05, - 'storage_server.latencies.writev.01_0_percentile': 0.00033211708068847656, - 'storage_server.latencies.read.10_0_percentile': 3.0994415283203125e-05, - 'storage_server.latencies.allocate.10_0_percentile': 0.0004949569702148438, - 'storage_server.reserved_space': 0, - 'storage_server.disk_avail': 103289454592, - 'storage_server.latencies.write.99_0_percentile': 0.00011301040649414062, - 'storage_server.latencies.write.90_0_percentile': 9.083747863769531e-05, - 'cpu_monitor.5min_avg': 0.0002370666691157502, - 'storage_server.latencies.write.mean': 5.8008909225463864e-05, - 'storage_server.latencies.readv.50_0_percentile': 0.00033020973205566406, - 'storage_server.latencies.close.99_0_percentile': 0.0004038810729980469, - 'storage_server.allocated': 0, - 'storage_server.latencies.writev.99_0_percentile': 0.0007710456848144531, - 'storage_server.latencies.readv.99_9_percentile': 0.0004780292510986328, - 'storage_server.latencies.read.samplesize': 170, - 'storage_server.latencies.allocate.samplesize': 406, - 'storage_server.latencies.allocate.95_0_percentile': 0.0008411407470703125}, - 'counters': { - 'storage_server.writev': 309, - 'storage_server.bytes_added': 197836146, - 'storage_server.close': 326, - 'storage_server.readv': 14299, - 'storage_server.allocate': 406, - 'storage_server.read': 170, - 'storage_server.write': 3775, - 'storage_server.get': 472} - } + "stats": { + "storage_server.latencies.get.99_9_percentile": None, + "storage_server.latencies.close.10_0_percentile": 0.00021910667419433594, + "storage_server.latencies.read.01_0_percentile": 2.8848648071289062e-05, + "storage_server.latencies.writev.99_9_percentile": None, + "storage_server.latencies.read.99_9_percentile": None, + "storage_server.latencies.allocate.99_0_percentile": 0.000988006591796875, + "storage_server.latencies.writev.mean": 0.00045332245070571654, + "storage_server.latencies.close.99_9_percentile": None, + "cpu_monitor.15min_avg": 0.00017592000079223033, + "storage_server.disk_free_for_root": 103289454592, + "storage_server.latencies.get.99_0_percentile": 0.000347137451171875, + "storage_server.latencies.get.mean": 0.00021158285060171353, + "storage_server.latencies.read.90_0_percentile": 8.893013000488281e-05, + "storage_server.latencies.write.01_0_percentile": 3.600120544433594e-05, + "storage_server.latencies.write.99_9_percentile": 0.00017690658569335938, + "storage_server.latencies.close.90_0_percentile": 0.00033211708068847656, + "storage_server.disk_total": 103497859072, + "storage_server.latencies.close.95_0_percentile": 0.0003509521484375, + "storage_server.latencies.readv.samplesize": 1000, + "storage_server.disk_free_for_nonroot": 103289454592, + "storage_server.latencies.close.mean": 0.0002715024480059103, + "storage_server.latencies.writev.95_0_percentile": 0.0007410049438476562, + "storage_server.latencies.readv.90_0_percentile": 0.0003781318664550781, + "storage_server.latencies.readv.99_0_percentile": 0.0004050731658935547, + "storage_server.latencies.allocate.mean": 0.0007128627429454784, + "storage_server.latencies.close.samplesize": 326, + "storage_server.latencies.get.50_0_percentile": 0.0001819133758544922, + "storage_server.latencies.write.50_0_percentile": 4.482269287109375e-05, + "storage_server.latencies.readv.01_0_percentile": 0.0002970695495605469, + "storage_server.latencies.get.10_0_percentile": 0.00015687942504882812, + "storage_server.latencies.allocate.90_0_percentile": 0.0008189678192138672, + "storage_server.latencies.get.samplesize": 472, + "storage_server.total_bucket_count": 393, + "storage_server.latencies.read.mean": 5.936201880959903e-05, + "storage_server.latencies.allocate.01_0_percentile": 0.0004208087921142578, + "storage_server.latencies.allocate.99_9_percentile": None, + "storage_server.latencies.readv.mean": 0.00034061360359191893, + "storage_server.disk_used": 208404480, + "storage_server.latencies.allocate.50_0_percentile": 0.0007410049438476562, + "storage_server.latencies.read.99_0_percentile": 0.00011992454528808594, + "node.uptime": 3805759.8545179367, + "storage_server.latencies.writev.10_0_percentile": 0.00035190582275390625, + "storage_server.latencies.writev.90_0_percentile": 0.0006821155548095703, + "storage_server.latencies.close.01_0_percentile": 0.00021505355834960938, + "storage_server.latencies.close.50_0_percentile": 0.0002579689025878906, + "cpu_monitor.1min_avg": 0.0002130000000003444, + "storage_server.latencies.writev.50_0_percentile": 0.0004138946533203125, + "storage_server.latencies.read.95_0_percentile": 9.107589721679688e-05, + "storage_server.latencies.readv.95_0_percentile": 0.0003859996795654297, + "storage_server.latencies.write.10_0_percentile": 3.719329833984375e-05, + "storage_server.accepting_immutable_shares": 1, + "storage_server.latencies.writev.samplesize": 309, + "storage_server.latencies.get.95_0_percentile": 0.0003190040588378906, + "storage_server.latencies.readv.10_0_percentile": 0.00032210350036621094, + "storage_server.latencies.get.90_0_percentile": 0.0002999305725097656, + "storage_server.latencies.get.01_0_percentile": 0.0001239776611328125, + "cpu_monitor.total": 641.4941180000001, + "storage_server.latencies.write.samplesize": 1000, + "storage_server.latencies.write.95_0_percentile": 9.489059448242188e-05, + "storage_server.latencies.read.50_0_percentile": 6.890296936035156e-05, + "storage_server.latencies.writev.01_0_percentile": 0.00033211708068847656, + "storage_server.latencies.read.10_0_percentile": 3.0994415283203125e-05, + "storage_server.latencies.allocate.10_0_percentile": 0.0004949569702148438, + "storage_server.reserved_space": 0, + "storage_server.disk_avail": 103289454592, + "storage_server.latencies.write.99_0_percentile": 0.00011301040649414062, + "storage_server.latencies.write.90_0_percentile": 9.083747863769531e-05, + "cpu_monitor.5min_avg": 0.0002370666691157502, + "storage_server.latencies.write.mean": 5.8008909225463864e-05, + "storage_server.latencies.readv.50_0_percentile": 0.00033020973205566406, + "storage_server.latencies.close.99_0_percentile": 0.0004038810729980469, + "storage_server.allocated": 0, + "storage_server.latencies.writev.99_0_percentile": 0.0007710456848144531, + "storage_server.latencies.readv.99_9_percentile": 0.0004780292510986328, + "storage_server.latencies.read.samplesize": 170, + "storage_server.latencies.allocate.samplesize": 406, + "storage_server.latencies.allocate.95_0_percentile": 0.0008411407470703125, + }, + "counters": { + "storage_server.writev": 309, + "storage_server.bytes_added": 197836146, + "storage_server.close": 326, + "storage_server.readv": 14299, + "storage_server.allocate": 406, + "storage_server.read": 170, + "storage_server.write": 3775, + "storage_server.get": 472, + }, + } return stats + class HackItResource(Resource, object): """ A bridge between ``RequestTraversalAgent`` and ``MultiFormatResource`` @@ -143,6 +147,7 @@ class HackItResource(Resource, object): object to have a ``fields`` attribute but Twisted's ``IRequest`` has no such attribute. Create it here. """ + def getChildWithDefault(self, path, request): request.fields = None return Resource.getChildWithDefault(self, path, request) @@ -152,6 +157,7 @@ class OpenMetrics(SyncTestCase): """ Tests for ``/statistics?t=openmetrics``. """ + def test_spec_compliance(self): """ Does our output adhere to the `OpenMetrics ` spec? @@ -164,6 +170,7 @@ class OpenMetrics(SyncTestCase): d = rta.request(b"GET", b"http://localhost/?t=openmetrics") self.assertThat(d, succeeded(matches_stats(self))) + def matches_stats(testcase): """ Create a matcher that matches a response that confirms to the OpenMetrics @@ -184,19 +191,22 @@ def matches_stats(testcase): code=Equals(OK), # "The content type MUST be..." headers=has_header( - u"content-type", - u"application/openmetrics-text; version=1.0.0; charset=utf-8", + "content-type", + "application/openmetrics-text; version=1.0.0; charset=utf-8", ), ), AfterPreprocessing( readBodyText, - succeeded(MatchesAll( - MatchesPredicate(add_detail(testcase, u"response body"), u"%s dummy"), - parses_as_openmetrics(), - )) - ) + succeeded( + MatchesAll( + MatchesPredicate(add_detail(testcase, "response body"), "%s dummy"), + parses_as_openmetrics(), + ) + ), + ), ) + def add_detail(testcase, name): """ Create a matcher that always matches and as a side-effect adds the matched @@ -206,11 +216,14 @@ def add_detail(testcase, name): :return: A matcher. """ + def predicate(value): testcase.addDetail(name, text_content(value)) return True + return predicate + def readBodyText(response): """ Read the response body and decode it using UTF-8. @@ -224,6 +237,7 @@ def readBodyText(response): d.addCallback(lambda body: body.decode("utf-8")) return d + def has_header(name, value): """ Create a matcher that matches a response object that includes the given @@ -239,6 +253,7 @@ def has_header(name, value): Equals([value]), ) + def parses_as_openmetrics(): """ Create a matcher that matches a ``str`` string that can be parsed as an @@ -253,8 +268,6 @@ def parses_as_openmetrics(): lambda body: list(parser.text_string_to_metric_families(body)), AfterPreprocessing( lambda families: families[-1].name, - Equals(u"tahoe_stats_storage_server_total_bucket_count"), + Equals("tahoe_stats_storage_server_total_bucket_count"), ), ) - - From 7183d53c23e3126032a59b5617259af240ead552 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 23 Sep 2021 07:58:02 -0400 Subject: [PATCH 35/35] put test dependency in the setuptools test extra --- setup.py | 2 ++ tox.ini | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3433e93f4..27407a403 100644 --- a/setup.py +++ b/setup.py @@ -404,6 +404,8 @@ setup(name="tahoe-lafs", # also set in __init__.py "tenacity", "paramiko", "pytest-timeout", + # Does our OpenMetrics endpoint adhere to the spec: + "prometheus-client == 0.11.0", ] + tor_requires + i2p_requires, "tor": tor_requires, "i2p": i2p_requires, diff --git a/tox.ini b/tox.ini index 0e8e58ea6..9b0f71038 100644 --- a/tox.ini +++ b/tox.ini @@ -52,8 +52,6 @@ deps = certifi # VCS hooks support py36,!coverage: pre-commit - # Does our OpenMetrics endpoint adhere to the spec: - prometheus-client==0.11.0 # We add usedevelop=False because testing against a true installation gives # more useful results.