Merge ~andreserl/maas:2.5_prometheus_integration into maas:master

Proposed by Andres Rodriguez
Status: Merged
Approved by: Andres Rodriguez
Approved revision: 0e9c7f26a8a20eace796136a2f5a0688d2964b29
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~andreserl/maas:2.5_prometheus_integration
Merge into: maas:master
Diff against target: 364 lines (+265/-0)
8 files modified
src/maasserver/eventloop.py (+10/-0)
src/maasserver/forms/settings.py (+37/-0)
src/maasserver/models/config.py (+4/-0)
src/maasserver/prometheus.py (+96/-0)
src/maasserver/tests/test_eventloop.py (+12/-0)
src/maasserver/tests/test_plugin.py (+2/-0)
src/maasserver/tests/test_prometheus.py (+103/-0)
utilities/check-imports (+1/-0)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+355835@code.launchpad.net

Commit message

Add initial integration to publish stats to prometheus.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Its a start, but has a lot of issues with usage of the database at the wrong times. Those must be fixed before this can land.

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

You do have one issues. The interval will not be updated automatically, it will only be updated one the old interval is reached. So on startup it will work fine, but changing it to 30 minutes, will not update until 60 minutes is reached. Then it will update from that point.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

Question inlien!

Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good, just one comment on the testing.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
0e9c7f2... by Andres Rodriguez

Make format

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/eventloop.py b/src/maasserver/eventloop.py
index 3c71695..af4f10e 100644
--- a/src/maasserver/eventloop.py
+++ b/src/maasserver/eventloop.py
@@ -104,6 +104,11 @@ def make_StatsService():
104 return stats.StatsService()104 return stats.StatsService()
105105
106106
107def make_PrometheusService():
108 from maasserver import prometheus
109 return prometheus.PrometheusService()
110
111
107def make_ImportResourcesService():112def make_ImportResourcesService():
108 from maasserver import bootresources113 from maasserver import bootresources
109 return bootresources.ImportResourcesService()114 return bootresources.ImportResourcesService()
@@ -259,6 +264,11 @@ class RegionEventLoop:
259 "factory": make_StatsService,264 "factory": make_StatsService,
260 "requires": [],265 "requires": [],
261 },266 },
267 "prometheus": {
268 "only_on_master": True,
269 "factory": make_PrometheusService,
270 "requires": [],
271 },
262 "import-resources": {272 "import-resources": {
263 "only_on_master": False,273 "only_on_master": False,
264 "import_service": True,274 "import_service": True,
diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py
index 730eba4..0b91e06 100644
--- a/src/maasserver/forms/settings.py
+++ b/src/maasserver/forms/settings.py
@@ -729,6 +729,43 @@ CONFIG_ITEMS = {
729 'min_value': 1,729 'min_value': 1,
730 },730 },
731 },731 },
732 'prometheus_enabled': {
733 'default': False,
734 'form': forms.BooleanField,
735 'form_kwargs': {
736 'label': (
737 "Enable sending stats to a prometheus gateway."),
738 'required': False,
739 'help_text': (
740 "Allows MAAS to send statistics to Prometheus. This requires "
741 "the 'prometheus_push_gateway' to be set.")
742 }
743 },
744 'prometheus_push_gateway': {
745 'default': None,
746 'form': forms.CharField,
747 'form_kwargs': {
748 'label': (
749 "Address or hostname of the Prometheus push gateway."),
750 'required': False,
751 'help_text': (
752 "Defines the address or hostname of the Prometheus push "
753 "gateway where MAAS will send data to.")
754 }
755 },
756 'prometheus_push_interval': {
757 'default': 60,
758 'form': forms.IntegerField,
759 'form_kwargs': {
760 'label': (
761 "Interval of how often to send data to Prometheus "
762 "(default: to 60 minutes)."),
763 'required': False,
764 'help_text': (
765 "The internal of how often MAAS will send stats to Prometheus "
766 "in minutes.")
767 }
768 },
732}769}
733770
734771
diff --git a/src/maasserver/models/config.py b/src/maasserver/models/config.py
index c7a8cbb..59e7c46 100644
--- a/src/maasserver/models/config.py
+++ b/src/maasserver/models/config.py
@@ -125,6 +125,10 @@ def get_default_config():
125 # MAAS Architecture.125 # MAAS Architecture.
126 'use_rack_proxy': True,126 'use_rack_proxy': True,
127 'node_timeout': 30,127 'node_timeout': 30,
128 # prometheus.
129 'prometheus_enabled': False,
130 'prometheus_push_gateway': None,
131 'prometheus_push_interval': 60,
128 }132 }
129133
130134
diff --git a/src/maasserver/prometheus.py b/src/maasserver/prometheus.py
131new file mode 100644135new file mode 100644
index 0000000..f736de9
--- /dev/null
+++ b/src/maasserver/prometheus.py
@@ -0,0 +1,96 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Prometheus integration"""
5
6__all__ = [
7 "PrometheusService",
8 "PROMETHEUS_SERVICE_PERIOD",
9]
10
11from datetime import timedelta
12import json
13
14from maasserver.models import Config
15from maasserver.stats import get_maas_stats
16from maasserver.utils.orm import transactional
17from maasserver.utils.threads import deferToDatabase
18from provisioningserver.logger import LegacyLogger
19from twisted.application.internet import TimerService
20
21
22try:
23 from prometheus_client import (
24 CollectorRegistry,
25 Gauge,
26 push_to_gateway,
27 )
28 PROMETHEUS = True
29except:
30 PROMETHEUS = False
31
32log = LegacyLogger()
33
34
35def push_stats_to_prometheus(maas_name, push_gateway):
36 registry = CollectorRegistry()
37 stats = json.loads(get_maas_stats())
38
39 # Gather counter for machines per status
40 counter = Gauge(
41 "machine_status", "Number per machines per stats",
42 ["status"], registry=registry)
43 for status, machines in stats['machine_status'].items():
44 counter.labels(status).set(machines)
45
46 push_to_gateway(
47 push_gateway, job='stats_for_%s' % maas_name, registry=registry)
48
49
50# Define the default time the service interval is run.
51# This can be overriden by the config option.
52PROMETHEUS_SERVICE_PERIOD = timedelta(minutes=60)
53
54
55class PrometheusService(TimerService, object):
56 """Service to periodically push stats to Prometheus
57
58 This will run immediately when it's started, by default, it will run
59 every 60 minutes, though the interval can be overridden (see
60 prometheus_push_internval global config).
61 """
62
63 def __init__(self, interval=PROMETHEUS_SERVICE_PERIOD):
64 super(PrometheusService, self).__init__(
65 interval.total_seconds(), self.maybe_push_prometheus_stats)
66
67 def maybe_push_prometheus_stats(self):
68 def determine_stats_request():
69 config = Config.objects.get_configs([
70 'maas_name', 'prometheus_enabled', 'prometheus_push_gateway',
71 'prometheus_push_interval'])
72 # Update interval
73 self._update_interval(
74 timedelta(minutes=config['prometheus_push_interval']))
75 # Determine if we can run the actual update.
76 if (not PROMETHEUS or not config['prometheus_enabled'] or
77 config['prometheus_push_gateway'] is None):
78 return
79 # Run updates.
80 push_stats_to_prometheus(
81 config['maas_name'], config['prometheus_push_gateway'])
82
83 d = deferToDatabase(transactional(determine_stats_request))
84 d.addErrback(
85 log.err,
86 "Failure pushing stats to prometheus gateway")
87 return d
88
89 def _update_interval(self, interval):
90 """Change the update interval."""
91 interval_seconds = interval.total_seconds()
92 if self.step == interval_seconds:
93 return
94 self._loop.interval = self.step = interval_seconds
95 if self._loop.running:
96 self._loop.reset()
diff --git a/src/maasserver/tests/test_eventloop.py b/src/maasserver/tests/test_eventloop.py
index aecb077..4a6f7c0 100644
--- a/src/maasserver/tests/test_eventloop.py
+++ b/src/maasserver/tests/test_eventloop.py
@@ -18,6 +18,7 @@ from maasserver import (
18 eventloop,18 eventloop,
19 ipc,19 ipc,
20 nonces_cleanup,20 nonces_cleanup,
21 prometheus,
21 rack_controller,22 rack_controller,
22 region_controller,23 region_controller,
23 stats,24 stats,
@@ -376,6 +377,17 @@ class TestFactories(MAASTestCase):
376 self.assertTrue(377 self.assertTrue(
377 eventloop.loop.factories["stats"]["only_on_master"])378 eventloop.loop.factories["stats"]["only_on_master"])
378379
380 def test_make_PrometheusService(self):
381 service = eventloop.make_PrometheusService()
382 self.assertThat(service, IsInstance(
383 prometheus.PrometheusService))
384 # It is registered as a factory in RegionEventLoop.
385 self.assertIs(
386 eventloop.make_PrometheusService,
387 eventloop.loop.factories["prometheus"]["factory"])
388 self.assertTrue(
389 eventloop.loop.factories["prometheus"]["only_on_master"])
390
379 def test_make_ImportResourcesService(self):391 def test_make_ImportResourcesService(self):
380 service = eventloop.make_ImportResourcesService()392 service = eventloop.make_ImportResourcesService()
381 self.assertThat(service, IsInstance(393 self.assertThat(service, IsInstance(
diff --git a/src/maasserver/tests/test_plugin.py b/src/maasserver/tests/test_plugin.py
index 1ee6898..d3b04fe 100644
--- a/src/maasserver/tests/test_plugin.py
+++ b/src/maasserver/tests/test_plugin.py
@@ -228,6 +228,7 @@ class TestRegionMasterServiceMaker(TestServiceMaker):
228 "service-monitor",228 "service-monitor",
229 "status-monitor",229 "status-monitor",
230 "stats",230 "stats",
231 "prometheus",
231 "postgres-listener-master",232 "postgres-listener-master",
232 "networks-monitor",233 "networks-monitor",
233 "active-discovery",234 "active-discovery",
@@ -322,6 +323,7 @@ class TestRegionAllInOneServiceMaker(TestServiceMaker):
322 "dns-publication-cleanup",323 "dns-publication-cleanup",
323 "status-monitor",324 "status-monitor",
324 "stats",325 "stats",
326 "prometheus",
325 "import-resources",327 "import-resources",
326 "import-resources-progress",328 "import-resources-progress",
327 "postgres-listener-master",329 "postgres-listener-master",
diff --git a/src/maasserver/tests/test_prometheus.py b/src/maasserver/tests/test_prometheus.py
328new file mode 100644330new file mode 100644
index 0000000..27dc7f4
--- /dev/null
+++ b/src/maasserver/tests/test_prometheus.py
@@ -0,0 +1,103 @@
1# Copyright 2014-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test maasserver.prometheus."""
5
6__all__ = []
7
8from django.db import transaction
9from maasserver import prometheus
10from maasserver.models import Config
11from maasserver.prometheus import push_stats_to_prometheus
12from maasserver.testing.factory import factory
13from maasserver.testing.testcase import (
14 MAASServerTestCase,
15 MAASTransactionServerTestCase,
16)
17from maastesting.matchers import (
18 MockCalledOnce,
19 MockCalledOnceWith,
20 MockNotCalled,
21)
22from maastesting.testcase import MAASTestCase
23from maastesting.twisted import extract_result
24from provisioningserver.utils.twisted import asynchronous
25from twisted.application.internet import TimerService
26from twisted.internet.defer import fail
27
28
29class TestPrometheus(MAASServerTestCase):
30
31 def test_push_stats_to_prometheus(self):
32 factory.make_RegionRackController()
33 maas_name = 'random.maas'
34 push_gateway = '127.0.0.1:2000'
35 registry_mock = self.patch(prometheus, "CollectorRegistry")
36 self.patch(prometheus, "Gauge")
37 mock = self.patch(prometheus, "push_to_gateway")
38 push_stats_to_prometheus(maas_name, push_gateway)
39 self.assertThat(
40 mock, MockCalledOnceWith(
41 push_gateway,
42 job="stats_for_%s" % maas_name,
43 registry=registry_mock()))
44
45
46class TestPrometheusService(MAASTestCase):
47 """Tests for `ImportPrometheusService`."""
48
49 def test__is_a_TimerService(self):
50 service = prometheus.PrometheusService()
51 self.assertIsInstance(service, TimerService)
52
53 def test__runs_once_an_hour_by_default(self):
54 service = prometheus.PrometheusService()
55 self.assertEqual(3600, service.step)
56
57 def test__calls__maybe_make_stats_request(self):
58 service = prometheus.PrometheusService()
59 self.assertEqual(
60 (service.maybe_push_prometheus_stats, (), {}),
61 service.call)
62
63 def test_maybe_make_stats_request_does_not_error(self):
64 service = prometheus.PrometheusService()
65 deferToDatabase = self.patch(prometheus, "deferToDatabase")
66 exception_type = factory.make_exception_type()
67 deferToDatabase.return_value = fail(exception_type())
68 d = service.maybe_push_prometheus_stats()
69 self.assertIsNone(extract_result(d))
70
71
72class TestPrometheusServiceAsync(MAASTransactionServerTestCase):
73 """Tests for the async parts of `PrometheusService`."""
74
75 def test_maybe_make_stats_request_makes_request(self):
76 mock_call = self.patch(prometheus, "push_stats_to_prometheus")
77 setting = self.patch(prometheus, "PROMETHEUS")
78 setting.return_value = True
79
80 with transaction.atomic():
81 Config.objects.set_config('prometheus_enabled', True)
82 Config.objects.set_config(
83 'prometheus_push_gateway', '192.168.1.1:8081')
84
85 service = prometheus.PrometheusService()
86 maybe_push_prometheus_stats = asynchronous(
87 service.maybe_push_prometheus_stats)
88 maybe_push_prometheus_stats().wait(5)
89
90 self.assertThat(mock_call, MockCalledOnce())
91
92 def test_maybe_make_stats_request_doesnt_make_request(self):
93 mock_call = self.patch(prometheus, "push_stats_to_prometheus")
94
95 with transaction.atomic():
96 Config.objects.set_config('enable_analytics', False)
97
98 service = prometheus.PrometheusService()
99 maybe_push_prometheus_stats = asynchronous(
100 service.maybe_push_prometheus_stats)
101 maybe_push_prometheus_stats().wait(5)
102
103 self.assertThat(mock_call, MockNotCalled())
diff --git a/utilities/check-imports b/utilities/check-imports
index 69c445c..9a8a90f 100755
--- a/utilities/check-imports
+++ b/utilities/check-imports
@@ -273,6 +273,7 @@ RegionControllerRule = Rule(
273 Allow("OpenSSL|OpenSSL.**"),273 Allow("OpenSSL|OpenSSL.**"),
274 Allow("petname"),274 Allow("petname"),
275 Allow("piston3|piston3.**"),275 Allow("piston3|piston3.**"),
276 Allow("prometheus_client.**"),
276 Allow("provisioningserver|provisioningserver.**"),277 Allow("provisioningserver|provisioningserver.**"),
277 Allow("psycopg2|psycopg2.**"),278 Allow("psycopg2|psycopg2.**"),
278 Allow("pytz.UTC"),279 Allow("pytz.UTC"),

Subscribers

People subscribed via source and target branches