Skip to content

Commit ebc41fe

Browse files
committed
[charts] Add way to register and unregister new chart configurations #86
Closes #86
1 parent 1eed8a1 commit ebc41fe

File tree

9 files changed

+294
-135
lines changed

9 files changed

+294
-135
lines changed

README.rst

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,77 @@ information by performing lookups on the OUI
476476

477477
This feature is enabled by default.
478478

479+
Registering / Unregistering Chart Configuration
480+
-----------------------------------------------
481+
482+
**OpenWISP Monitoring** provides registering and unregistering chart configuration through utility functions
483+
``openwisp_monitoring.monitoring.charts.register_chart`` and ``openwisp_monitoring.monitoring.charts.unregister_chart``.
484+
Using these functions you can register or unregister chart configurations from anywhere in your code.
485+
486+
register_chart
487+
~~~~~~~~~~~~~~
488+
489+
This function is used to register a new chart configuration from anywhere in your code.
490+
491+
+--------------------------+-----------------------------------------------------+
492+
| **Parameter** | **Description** |
493+
+--------------------------+-----------------------------------------------------+
494+
| **chart_name**: | A ``str`` defining name of the chart configuration. |
495+
+--------------------------+-----------------------------------------------------+
496+
| **chart_configuration**: | A ``dict`` defining configuration of the chart. |
497+
+--------------------------+-----------------------------------------------------+
498+
499+
An example usage has been shown below.
500+
501+
.. code-block:: python
502+
503+
from openwisp_monitoring.monitoring import register_chart
504+
505+
# Define configuration of your chart
506+
chart_config = {
507+
'type': 'histogram',
508+
'title': 'Histogram',
509+
'description': 'Histogram',
510+
'top_fields': 2,
511+
'order': 999,
512+
'query': {
513+
'influxdb': (
514+
"SELECT {fields|SUM|/ 1} FROM {key} "
515+
"WHERE time >= '{time}' AND content_type = "
516+
"'{content_type}' AND object_id = '{object_id}'"
517+
)
518+
},
519+
}
520+
521+
# Register your custom chart configuration
522+
register_chart('chart_name', chart_config)
523+
524+
**Note**: It will raise ``ImproperlyConfigured`` exception if a chart configuration
525+
is already registered with same name (not to be confused with verbose_name).
526+
527+
unregister_chart
528+
~~~~~~~~~~~~~~~~
529+
530+
This function is used to unregister a chart configuration from anywhere in your code.
531+
532+
+------------------+-----------------------------------------------------+
533+
| **Parameter** | **Description** |
534+
+------------------+-----------------------------------------------------+
535+
| **chart_name**: | A ``str`` defining name of the chart configuration. |
536+
+------------------+-----------------------------------------------------+
537+
538+
An example usage is shown below.
539+
540+
.. code-block:: python
541+
542+
from openwisp_monitoring.monitoring import unregister_chart
543+
544+
# Unregister previously registered chart configuration
545+
unregister_chart('chart_name')
546+
547+
**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned chart
548+
configuration is not registered.
549+
479550
Signals
480551
-------
481552

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
from .charts import register_chart, unregister_chart # noqa
2+
13
default_app_config = 'openwisp_monitoring.monitoring.apps.MonitoringConfig'

openwisp_monitoring/monitoring/base/models.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
from ...db import default_chart_query, timeseries_db
2323
from ..charts import (
24+
CHART_CONFIGURATION_CHOICES,
2425
DEFAULT_COLORS,
2526
get_chart_configuration,
26-
get_chart_configuration_choices,
2727
)
2828
from ..exceptions import InvalidChartConfigException, InvalidMetricConfigException
2929
from ..metrics import get_metric_configuration, get_metric_configuration_choices
@@ -224,12 +224,11 @@ def _notify_users(self, notification_type, alert_settings):
224224

225225

226226
class AbstractChart(TimeStampedEditableModel):
227-
CHARTS = get_chart_configuration()
228227
metric = models.ForeignKey(
229228
get_model_name('monitoring', 'Metric'), on_delete=models.CASCADE
230229
)
231230
configuration = models.CharField(
232-
max_length=16, null=True, choices=get_chart_configuration_choices()
231+
max_length=16, null=True, choices=CHART_CONFIGURATION_CHOICES
233232
)
234233
GROUP_MAP = {
235234
'1d': '10m',
@@ -261,7 +260,7 @@ def _clean_query(self):
261260
@property
262261
def config_dict(self):
263262
try:
264-
return self.CHARTS[self.configuration]
263+
return get_chart_configuration()[self.configuration]
265264
except KeyError as e:
266265
raise InvalidChartConfigException(
267266
f'Invalid chart configuration: "{self.configuration}"'

openwisp_monitoring/monitoring/charts.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.core.exceptions import ImproperlyConfigured
12
from django.utils.translation import gettext_lazy as _
23
from openwisp_monitoring.db import chart_query
34

@@ -140,29 +141,75 @@
140141
}
141142

142143

144+
def _validate_chart_configuration(chart_config):
145+
assert 'type' in chart_config
146+
assert 'title' in chart_config
147+
assert 'description' in chart_config
148+
assert 'order' in chart_config
149+
assert 'query' in chart_config
150+
if chart_config['query'] is None:
151+
assert 'unit' in chart_config
152+
if 'colorscale' in chart_config:
153+
assert 'max' in chart_config['colorscale']
154+
assert 'min' in chart_config['colorscale']
155+
assert 'label' in chart_config['colorscale']
156+
assert 'scale' in chart_config['colorscale']
157+
158+
143159
def get_chart_configuration():
144160
charts = deep_merge_dicts(DEFAULT_CHARTS, app_settings.ADDITIONAL_CHARTS)
145161
# ensure configuration is not broken
146162
for key, options in charts.items():
147-
assert 'type' in options
148-
assert 'title' in options
149-
assert 'description' in options
150-
assert 'order' in options
151-
assert 'query' in options
152-
if options['query'] is None:
153-
assert 'unit' in options
154-
if 'colorscale' in options:
155-
assert 'max' in options['colorscale']
156-
assert 'min' in options['colorscale']
157-
assert 'label' in options['colorscale']
158-
assert 'scale' in options['colorscale']
163+
_validate_chart_configuration(options)
159164
return charts
160165

161166

167+
def register_chart(chart_name, chart_config):
168+
"""
169+
Registers a new chart configuration.
170+
"""
171+
if not isinstance(chart_name, str):
172+
raise ImproperlyConfigured('Chart name should be type "str".')
173+
if not isinstance(chart_config, dict):
174+
raise ImproperlyConfigured('Chart configuration should be type "dict".')
175+
if chart_name in DEFAULT_CHARTS:
176+
raise ImproperlyConfigured(
177+
f'{chart_name} is an already registered Chart Configuration.'
178+
)
179+
180+
_validate_chart_configuration(chart_config)
181+
DEFAULT_CHARTS.update({chart_name: chart_config})
182+
_register_chart_configuration_choice(chart_name, chart_config)
183+
184+
185+
def unregister_chart(chart_name):
186+
if not isinstance(chart_name, str):
187+
raise ImproperlyConfigured('Chart configuration name should be type "str"')
188+
if chart_name not in DEFAULT_CHARTS:
189+
raise ImproperlyConfigured(f'No such Chart configuation "{chart_name}"')
190+
DEFAULT_CHARTS.pop(chart_name)
191+
_unregister_chart_configuration_choice(chart_name)
192+
193+
194+
def _register_chart_configuration_choice(chart_name, chart_config):
195+
name = chart_config.get('verbose_name', chart_name)
196+
CHART_CONFIGURATION_CHOICES.append((chart_name, name))
197+
198+
199+
def _unregister_chart_configuration_choice(chart_name):
200+
for index, (key, name) in enumerate(CHART_CONFIGURATION_CHOICES):
201+
if key == chart_name:
202+
CHART_CONFIGURATION_CHOICES.pop(index)
203+
return
204+
205+
162206
def get_chart_configuration_choices():
163207
charts = get_chart_configuration()
164208
choices = []
165209
for key in sorted(charts.keys()):
166210
label = charts[key].get('label', charts[key]['title'])
167211
choices.append((key, label))
168212
return choices
213+
214+
215+
CHART_CONFIGURATION_CHOICES = get_chart_configuration_choices()

openwisp_monitoring/monitoring/migrations/0006_add_configuration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Django 3.0.4 on 2020-04-28 17:22
22

33
from django.db import migrations, models
4-
from ..charts import get_chart_configuration_choices
4+
from ..charts import CHART_CONFIGURATION_CHOICES
55

66

77
class Migration(migrations.Migration):
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
1515
model_name='graph',
1616
name='configuration',
1717
field=models.CharField(
18-
choices=get_chart_configuration_choices(), max_length=16, null=True
18+
choices=CHART_CONFIGURATION_CHOICES, max_length=16, null=True
1919
),
2020
),
2121
]

openwisp_monitoring/monitoring/tests/__init__.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,114 @@
77

88
from ...db import timeseries_db
99
from ...db.backends import TIMESERIES_DB
10+
from .. import register_chart, unregister_chart
1011

1112
start_time = now()
1213
ten_minutes_ago = start_time - timedelta(minutes=10)
1314
Chart = load_model('monitoring', 'Chart')
1415
Metric = load_model('monitoring', 'Metric')
1516
AlertSettings = load_model('monitoring', 'AlertSettings')
1617

18+
# this custom chart configuration is used for automated testing purposes
19+
charts = {
20+
'histogram': {
21+
'type': 'histogram',
22+
'title': 'Histogram',
23+
'description': 'Histogram',
24+
'top_fields': 2,
25+
'order': 999,
26+
'query': {
27+
'influxdb': (
28+
"SELECT {fields|SUM|/ 1} FROM {key} "
29+
"WHERE time >= '{time}' AND content_type = "
30+
"'{content_type}' AND object_id = '{object_id}'"
31+
)
32+
},
33+
},
34+
'dummy': {
35+
'type': 'line',
36+
'title': 'Dummy chart',
37+
'description': 'Dummy chart for testing purposes.',
38+
'unit': 'candies',
39+
'order': 999,
40+
'query': None,
41+
},
42+
'bad_test': {
43+
'type': 'line',
44+
'title': 'Bugged chart for testing purposes',
45+
'description': 'Bugged chart for testing purposes.',
46+
'unit': 'bugs',
47+
'order': 999,
48+
'query': {'influxdb': "BAD"},
49+
},
50+
'default': {
51+
'type': 'line',
52+
'title': 'Default query for testing purposes',
53+
'description': 'Default query for testing purposes',
54+
'unit': 'n.',
55+
'order': 999,
56+
'query': {
57+
'influxdb': (
58+
"SELECT {field_name} FROM {key} WHERE time >= '{time}' AND "
59+
"content_type = '{content_type}' AND object_id = '{object_id}'"
60+
)
61+
},
62+
},
63+
'multiple_test': {
64+
'type': 'line',
65+
'title': 'Multiple test',
66+
'description': 'For testing purposes',
67+
'unit': 'n.',
68+
'order': 999,
69+
'query': {
70+
'influxdb': (
71+
"SELECT {field_name}, value2 FROM {key} WHERE time >= '{time}' AND "
72+
"content_type = '{content_type}' AND object_id = '{object_id}'"
73+
)
74+
},
75+
},
76+
'mean_test': {
77+
'type': 'line',
78+
'title': 'Mean test',
79+
'description': 'For testing purposes',
80+
'unit': 'n.',
81+
'order': 999,
82+
'query': {
83+
'influxdb': (
84+
"SELECT MEAN({field_name}) AS {field_name} FROM {key} WHERE time >= '{time}' AND "
85+
"content_type = '{content_type}' AND object_id = '{object_id}'"
86+
)
87+
},
88+
},
89+
'sum_test': {
90+
'type': 'line',
91+
'title': 'Sum test',
92+
'description': 'For testing purposes',
93+
'unit': 'n.',
94+
'order': 999,
95+
'query': {
96+
'influxdb': (
97+
"SELECT SUM({field_name}) AS {field_name} FROM {key} WHERE time >= '{time}' AND "
98+
"content_type = '{content_type}' AND object_id = '{object_id}'"
99+
)
100+
},
101+
},
102+
'top_fields_mean': {
103+
'type': 'histogram',
104+
'title': 'Top fields mean test',
105+
'description': 'For testing purposes',
106+
'top_fields': 2,
107+
'order': 999,
108+
'query': {
109+
'influxdb': (
110+
"SELECT {fields|MEAN} FROM {key} "
111+
"WHERE time >= '{time}' AND content_type = "
112+
"'{content_type}' AND object_id = '{object_id}'"
113+
)
114+
},
115+
},
116+
}
117+
17118

18119
class TestMonitoringMixin(TestOrganizationMixin):
19120
ORIGINAL_DB = TIMESERIES_DB['NAME']
@@ -26,10 +127,14 @@ def setUpClass(cls):
26127
timeseries_db.db_name = cls.TEST_DB
27128
del timeseries_db.get_db
28129
timeseries_db.create_database()
130+
for key, value in charts.items():
131+
register_chart(key, value)
29132

30133
@classmethod
31134
def tearDownClass(cls):
32135
timeseries_db.drop_database()
136+
for key in charts.keys():
137+
unregister_chart(key)
33138

34139
def tearDown(self):
35140
timeseries_db.delete_metric_data()

0 commit comments

Comments
 (0)