Merge ~andreserl/maas:2.5_prometheus_integration into maas:master
- Git
- lp:~andreserl/maas
- 2.5_prometheus_integration
- Merge into master
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) |
Related bugs: |
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.
Description of the change
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.
Andres Rodriguez (andreserl) wrote : | # |
Question inlien!
Blake Rouse (blake-rouse) : | # |
Blake Rouse (blake-rouse) wrote : | # |
Looks good, just one comment on the testing.
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b 2.5_prometheus_
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b 2.5_prometheus_
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b 2.5_prometheus_
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b 2.5_prometheus_
STATUS: FAILED BUILD
LOG: http://
- 0e9c7f2... by Andres Rodriguez
-
Make format
Preview Diff
1 | diff --git a/src/maasserver/eventloop.py b/src/maasserver/eventloop.py |
2 | index 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, |
29 | diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py |
30 | index 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 | |
77 | diff --git a/src/maasserver/models/config.py b/src/maasserver/models/config.py |
78 | index 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 | |
92 | diff --git a/src/maasserver/prometheus.py b/src/maasserver/prometheus.py |
93 | new file mode 100644 |
94 | index 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() |
194 | diff --git a/src/maasserver/tests/test_eventloop.py b/src/maasserver/tests/test_eventloop.py |
195 | index 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( |
224 | diff --git a/src/maasserver/tests/test_plugin.py b/src/maasserver/tests/test_plugin.py |
225 | index 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", |
244 | diff --git a/src/maasserver/tests/test_prometheus.py b/src/maasserver/tests/test_prometheus.py |
245 | new file mode 100644 |
246 | index 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()) |
353 | diff --git a/utilities/check-imports b/utilities/check-imports |
354 | index 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"), |
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.