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
1diff --git a/src/maasserver/eventloop.py b/src/maasserver/eventloop.py
2index 3c71695..af4f10e 100644
3--- a/src/maasserver/eventloop.py
4+++ b/src/maasserver/eventloop.py
5@@ -104,6 +104,11 @@ def make_StatsService():
6 return stats.StatsService()
7
8
9+def make_PrometheusService():
10+ from maasserver import prometheus
11+ return prometheus.PrometheusService()
12+
13+
14 def make_ImportResourcesService():
15 from maasserver import bootresources
16 return bootresources.ImportResourcesService()
17@@ -259,6 +264,11 @@ class RegionEventLoop:
18 "factory": make_StatsService,
19 "requires": [],
20 },
21+ "prometheus": {
22+ "only_on_master": True,
23+ "factory": make_PrometheusService,
24+ "requires": [],
25+ },
26 "import-resources": {
27 "only_on_master": False,
28 "import_service": True,
29diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py
30index 730eba4..0b91e06 100644
31--- a/src/maasserver/forms/settings.py
32+++ b/src/maasserver/forms/settings.py
33@@ -729,6 +729,43 @@ CONFIG_ITEMS = {
34 'min_value': 1,
35 },
36 },
37+ 'prometheus_enabled': {
38+ 'default': False,
39+ 'form': forms.BooleanField,
40+ 'form_kwargs': {
41+ 'label': (
42+ "Enable sending stats to a prometheus gateway."),
43+ 'required': False,
44+ 'help_text': (
45+ "Allows MAAS to send statistics to Prometheus. This requires "
46+ "the 'prometheus_push_gateway' to be set.")
47+ }
48+ },
49+ 'prometheus_push_gateway': {
50+ 'default': None,
51+ 'form': forms.CharField,
52+ 'form_kwargs': {
53+ 'label': (
54+ "Address or hostname of the Prometheus push gateway."),
55+ 'required': False,
56+ 'help_text': (
57+ "Defines the address or hostname of the Prometheus push "
58+ "gateway where MAAS will send data to.")
59+ }
60+ },
61+ 'prometheus_push_interval': {
62+ 'default': 60,
63+ 'form': forms.IntegerField,
64+ 'form_kwargs': {
65+ 'label': (
66+ "Interval of how often to send data to Prometheus "
67+ "(default: to 60 minutes)."),
68+ 'required': False,
69+ 'help_text': (
70+ "The internal of how often MAAS will send stats to Prometheus "
71+ "in minutes.")
72+ }
73+ },
74 }
75
76
77diff --git a/src/maasserver/models/config.py b/src/maasserver/models/config.py
78index c7a8cbb..59e7c46 100644
79--- a/src/maasserver/models/config.py
80+++ b/src/maasserver/models/config.py
81@@ -125,6 +125,10 @@ def get_default_config():
82 # MAAS Architecture.
83 'use_rack_proxy': True,
84 'node_timeout': 30,
85+ # prometheus.
86+ 'prometheus_enabled': False,
87+ 'prometheus_push_gateway': None,
88+ 'prometheus_push_interval': 60,
89 }
90
91
92diff --git a/src/maasserver/prometheus.py b/src/maasserver/prometheus.py
93new file mode 100644
94index 0000000..f736de9
95--- /dev/null
96+++ b/src/maasserver/prometheus.py
97@@ -0,0 +1,96 @@
98+# Copyright 2017 Canonical Ltd. This software is licensed under the
99+# GNU Affero General Public License version 3 (see the file LICENSE).
100+
101+"""Prometheus integration"""
102+
103+__all__ = [
104+ "PrometheusService",
105+ "PROMETHEUS_SERVICE_PERIOD",
106+]
107+
108+from datetime import timedelta
109+import json
110+
111+from maasserver.models import Config
112+from maasserver.stats import get_maas_stats
113+from maasserver.utils.orm import transactional
114+from maasserver.utils.threads import deferToDatabase
115+from provisioningserver.logger import LegacyLogger
116+from twisted.application.internet import TimerService
117+
118+
119+try:
120+ from prometheus_client import (
121+ CollectorRegistry,
122+ Gauge,
123+ push_to_gateway,
124+ )
125+ PROMETHEUS = True
126+except:
127+ PROMETHEUS = False
128+
129+log = LegacyLogger()
130+
131+
132+def push_stats_to_prometheus(maas_name, push_gateway):
133+ registry = CollectorRegistry()
134+ stats = json.loads(get_maas_stats())
135+
136+ # Gather counter for machines per status
137+ counter = Gauge(
138+ "machine_status", "Number per machines per stats",
139+ ["status"], registry=registry)
140+ for status, machines in stats['machine_status'].items():
141+ counter.labels(status).set(machines)
142+
143+ push_to_gateway(
144+ push_gateway, job='stats_for_%s' % maas_name, registry=registry)
145+
146+
147+# Define the default time the service interval is run.
148+# This can be overriden by the config option.
149+PROMETHEUS_SERVICE_PERIOD = timedelta(minutes=60)
150+
151+
152+class PrometheusService(TimerService, object):
153+ """Service to periodically push stats to Prometheus
154+
155+ This will run immediately when it's started, by default, it will run
156+ every 60 minutes, though the interval can be overridden (see
157+ prometheus_push_internval global config).
158+ """
159+
160+ def __init__(self, interval=PROMETHEUS_SERVICE_PERIOD):
161+ super(PrometheusService, self).__init__(
162+ interval.total_seconds(), self.maybe_push_prometheus_stats)
163+
164+ def maybe_push_prometheus_stats(self):
165+ def determine_stats_request():
166+ config = Config.objects.get_configs([
167+ 'maas_name', 'prometheus_enabled', 'prometheus_push_gateway',
168+ 'prometheus_push_interval'])
169+ # Update interval
170+ self._update_interval(
171+ timedelta(minutes=config['prometheus_push_interval']))
172+ # Determine if we can run the actual update.
173+ if (not PROMETHEUS or not config['prometheus_enabled'] or
174+ config['prometheus_push_gateway'] is None):
175+ return
176+ # Run updates.
177+ push_stats_to_prometheus(
178+ config['maas_name'], config['prometheus_push_gateway'])
179+
180+ d = deferToDatabase(transactional(determine_stats_request))
181+ d.addErrback(
182+ log.err,
183+ "Failure pushing stats to prometheus gateway")
184+ return d
185+
186+ def _update_interval(self, interval):
187+ """Change the update interval."""
188+ interval_seconds = interval.total_seconds()
189+ if self.step == interval_seconds:
190+ return
191+ self._loop.interval = self.step = interval_seconds
192+ if self._loop.running:
193+ self._loop.reset()
194diff --git a/src/maasserver/tests/test_eventloop.py b/src/maasserver/tests/test_eventloop.py
195index aecb077..4a6f7c0 100644
196--- a/src/maasserver/tests/test_eventloop.py
197+++ b/src/maasserver/tests/test_eventloop.py
198@@ -18,6 +18,7 @@ from maasserver import (
199 eventloop,
200 ipc,
201 nonces_cleanup,
202+ prometheus,
203 rack_controller,
204 region_controller,
205 stats,
206@@ -376,6 +377,17 @@ class TestFactories(MAASTestCase):
207 self.assertTrue(
208 eventloop.loop.factories["stats"]["only_on_master"])
209
210+ def test_make_PrometheusService(self):
211+ service = eventloop.make_PrometheusService()
212+ self.assertThat(service, IsInstance(
213+ prometheus.PrometheusService))
214+ # It is registered as a factory in RegionEventLoop.
215+ self.assertIs(
216+ eventloop.make_PrometheusService,
217+ eventloop.loop.factories["prometheus"]["factory"])
218+ self.assertTrue(
219+ eventloop.loop.factories["prometheus"]["only_on_master"])
220+
221 def test_make_ImportResourcesService(self):
222 service = eventloop.make_ImportResourcesService()
223 self.assertThat(service, IsInstance(
224diff --git a/src/maasserver/tests/test_plugin.py b/src/maasserver/tests/test_plugin.py
225index 1ee6898..d3b04fe 100644
226--- a/src/maasserver/tests/test_plugin.py
227+++ b/src/maasserver/tests/test_plugin.py
228@@ -228,6 +228,7 @@ class TestRegionMasterServiceMaker(TestServiceMaker):
229 "service-monitor",
230 "status-monitor",
231 "stats",
232+ "prometheus",
233 "postgres-listener-master",
234 "networks-monitor",
235 "active-discovery",
236@@ -322,6 +323,7 @@ class TestRegionAllInOneServiceMaker(TestServiceMaker):
237 "dns-publication-cleanup",
238 "status-monitor",
239 "stats",
240+ "prometheus",
241 "import-resources",
242 "import-resources-progress",
243 "postgres-listener-master",
244diff --git a/src/maasserver/tests/test_prometheus.py b/src/maasserver/tests/test_prometheus.py
245new file mode 100644
246index 0000000..27dc7f4
247--- /dev/null
248+++ b/src/maasserver/tests/test_prometheus.py
249@@ -0,0 +1,103 @@
250+# Copyright 2014-2018 Canonical Ltd. This software is licensed under the
251+# GNU Affero General Public License version 3 (see the file LICENSE).
252+
253+"""Test maasserver.prometheus."""
254+
255+__all__ = []
256+
257+from django.db import transaction
258+from maasserver import prometheus
259+from maasserver.models import Config
260+from maasserver.prometheus import push_stats_to_prometheus
261+from maasserver.testing.factory import factory
262+from maasserver.testing.testcase import (
263+ MAASServerTestCase,
264+ MAASTransactionServerTestCase,
265+)
266+from maastesting.matchers import (
267+ MockCalledOnce,
268+ MockCalledOnceWith,
269+ MockNotCalled,
270+)
271+from maastesting.testcase import MAASTestCase
272+from maastesting.twisted import extract_result
273+from provisioningserver.utils.twisted import asynchronous
274+from twisted.application.internet import TimerService
275+from twisted.internet.defer import fail
276+
277+
278+class TestPrometheus(MAASServerTestCase):
279+
280+ def test_push_stats_to_prometheus(self):
281+ factory.make_RegionRackController()
282+ maas_name = 'random.maas'
283+ push_gateway = '127.0.0.1:2000'
284+ registry_mock = self.patch(prometheus, "CollectorRegistry")
285+ self.patch(prometheus, "Gauge")
286+ mock = self.patch(prometheus, "push_to_gateway")
287+ push_stats_to_prometheus(maas_name, push_gateway)
288+ self.assertThat(
289+ mock, MockCalledOnceWith(
290+ push_gateway,
291+ job="stats_for_%s" % maas_name,
292+ registry=registry_mock()))
293+
294+
295+class TestPrometheusService(MAASTestCase):
296+ """Tests for `ImportPrometheusService`."""
297+
298+ def test__is_a_TimerService(self):
299+ service = prometheus.PrometheusService()
300+ self.assertIsInstance(service, TimerService)
301+
302+ def test__runs_once_an_hour_by_default(self):
303+ service = prometheus.PrometheusService()
304+ self.assertEqual(3600, service.step)
305+
306+ def test__calls__maybe_make_stats_request(self):
307+ service = prometheus.PrometheusService()
308+ self.assertEqual(
309+ (service.maybe_push_prometheus_stats, (), {}),
310+ service.call)
311+
312+ def test_maybe_make_stats_request_does_not_error(self):
313+ service = prometheus.PrometheusService()
314+ deferToDatabase = self.patch(prometheus, "deferToDatabase")
315+ exception_type = factory.make_exception_type()
316+ deferToDatabase.return_value = fail(exception_type())
317+ d = service.maybe_push_prometheus_stats()
318+ self.assertIsNone(extract_result(d))
319+
320+
321+class TestPrometheusServiceAsync(MAASTransactionServerTestCase):
322+ """Tests for the async parts of `PrometheusService`."""
323+
324+ def test_maybe_make_stats_request_makes_request(self):
325+ mock_call = self.patch(prometheus, "push_stats_to_prometheus")
326+ setting = self.patch(prometheus, "PROMETHEUS")
327+ setting.return_value = True
328+
329+ with transaction.atomic():
330+ Config.objects.set_config('prometheus_enabled', True)
331+ Config.objects.set_config(
332+ 'prometheus_push_gateway', '192.168.1.1:8081')
333+
334+ service = prometheus.PrometheusService()
335+ maybe_push_prometheus_stats = asynchronous(
336+ service.maybe_push_prometheus_stats)
337+ maybe_push_prometheus_stats().wait(5)
338+
339+ self.assertThat(mock_call, MockCalledOnce())
340+
341+ def test_maybe_make_stats_request_doesnt_make_request(self):
342+ mock_call = self.patch(prometheus, "push_stats_to_prometheus")
343+
344+ with transaction.atomic():
345+ Config.objects.set_config('enable_analytics', False)
346+
347+ service = prometheus.PrometheusService()
348+ maybe_push_prometheus_stats = asynchronous(
349+ service.maybe_push_prometheus_stats)
350+ maybe_push_prometheus_stats().wait(5)
351+
352+ self.assertThat(mock_call, MockNotCalled())
353diff --git a/utilities/check-imports b/utilities/check-imports
354index 69c445c..9a8a90f 100755
355--- a/utilities/check-imports
356+++ b/utilities/check-imports
357@@ -273,6 +273,7 @@ RegionControllerRule = Rule(
358 Allow("OpenSSL|OpenSSL.**"),
359 Allow("petname"),
360 Allow("piston3|piston3.**"),
361+ Allow("prometheus_client.**"),
362 Allow("provisioningserver|provisioningserver.**"),
363 Allow("psycopg2|psycopg2.**"),
364 Allow("pytz.UTC"),

Subscribers

People subscribed via source and target branches