|
2 | 2 | # (c) 2019 Andreas Motl <[email protected]>
|
3 | 3 | # License: GNU Affero General Public License, Version 3
|
4 | 4 | import json
|
| 5 | +from pprint import pprint |
| 6 | + |
5 | 7 | import colored
|
6 | 8 | import logging
|
7 | 9 | import asyncio
|
|
13 | 15 | from urllib.parse import urlparse, urljoin
|
14 | 16 | from concurrent.futures.thread import ThreadPoolExecutor
|
15 | 17 |
|
| 18 | +from grafana_wtf.model import DatasourceBreakdownItem |
16 | 19 | from grafana_wtf.monkey import monkeypatch_grafana_api
|
17 | 20 | # Apply monkeypatch to grafana-api
|
18 | 21 | # https://github.com/m0nhawk/grafana_api/pull/85/files
|
@@ -61,23 +64,30 @@ def clear_cache(self):
|
61 | 64 | def enable_concurrency(self, concurrency):
|
62 | 65 | self.concurrency = concurrency
|
63 | 66 |
|
64 |
| - def setup(self): |
65 |
| - url = urlparse(self.grafana_url) |
| 67 | + @staticmethod |
| 68 | + def grafana_client_factory(grafana_url, grafana_token=None): |
| 69 | + url = urlparse(grafana_url) |
66 | 70 |
|
67 | 71 | # Grafana API Key auth
|
68 |
| - if self.grafana_token: |
69 |
| - auth = self.grafana_token |
| 72 | + if grafana_token: |
| 73 | + auth = grafana_token |
70 | 74 |
|
71 | 75 | # HTTP basic auth
|
72 | 76 | else:
|
73 | 77 | username = url.username or 'admin'
|
74 | 78 | password = url.password or 'admin'
|
75 | 79 | auth = (username, password)
|
76 | 80 |
|
77 |
| - self.grafana = GrafanaFace( |
| 81 | + grafana = GrafanaFace( |
78 | 82 | auth, protocol=url.scheme,
|
79 | 83 | host=url.hostname, port=url.port, url_path_prefix=url.path.lstrip('/'))
|
80 | 84 |
|
| 85 | + return grafana |
| 86 | + |
| 87 | + def setup(self): |
| 88 | + |
| 89 | + self.grafana = self.grafana_client_factory(self.grafana_url, grafana_token=self.grafana_token) |
| 90 | + |
81 | 91 | # Configure a larger HTTP request pool.
|
82 | 92 | # Todo: Review the pool settings and eventually adjust according to concurrency level or other parameters.
|
83 | 93 | # https://urllib3.readthedocs.io/en/latest/advanced-usage.html#customizing-pool-behavior
|
@@ -175,6 +185,7 @@ def scan_datasources(self):
|
175 | 185 | try:
|
176 | 186 | self.data.datasources = munchify(self.grafana.datasource.list_datasources())
|
177 | 187 | log.info('Found {} data sources'.format(len(self.data.datasources)))
|
| 188 | + return self.data.datasources |
178 | 189 | except GrafanaClientError as ex:
|
179 | 190 | message = '{name}: {ex}'.format(name=ex.__class__.__name__, ex=ex)
|
180 | 191 | log.error(self.get_red_message(message))
|
@@ -218,6 +229,8 @@ def scan_dashboards(self, dashboard_uids=None):
|
218 | 229 | if self.progressbar:
|
219 | 230 | self.taqadum.close()
|
220 | 231 |
|
| 232 | + return self.data.dashboards |
| 233 | + |
221 | 234 | def handle_grafana_error(self, ex):
|
222 | 235 | message = '{name}: {ex}'.format(name=ex.__class__.__name__, ex=ex)
|
223 | 236 | message = colored.stylize(message, colored.fg("red") + colored.attr("bold"))
|
@@ -272,3 +285,93 @@ def get_dashboard_versions(self, dashboard_id):
|
272 | 285 | get_dashboard_versions_path = '/dashboards/id/%s/versions' % dashboard_id
|
273 | 286 | r = self.grafana.dashboard.api.GET(get_dashboard_versions_path)
|
274 | 287 | return r
|
| 288 | + |
| 289 | + def datasource_breakdown(self): |
| 290 | + |
| 291 | + # Prepare indexes, mapping dashboards by uid, datasources by name |
| 292 | + # as well as dashboards to datasources and vice versa. |
| 293 | + ix = Indexer(engine=self) |
| 294 | + |
| 295 | + # Compute list of breakdown items, associating datasources with the dashboards that use them. |
| 296 | + results_used = [] |
| 297 | + results_unused = [] |
| 298 | + for name in sorted(ix.datasource_by_name): |
| 299 | + datasource = ix.datasource_by_name[name] |
| 300 | + dashboard_uids = ix.datasource_dashboard_index.get(name, []) |
| 301 | + dashboards = list(map(ix.dashboard_by_uid.get, dashboard_uids)) |
| 302 | + item = DatasourceBreakdownItem(datasource=datasource, used_in=dashboards, grafana_url=self.grafana_url) |
| 303 | + |
| 304 | + # Format results in a more compact form, using only a subset of all the attributes. |
| 305 | + result = item.format_compact() |
| 306 | + |
| 307 | + if dashboard_uids: |
| 308 | + results_used.append(result) |
| 309 | + else: |
| 310 | + results_unused.append(result) |
| 311 | + |
| 312 | + response = OrderedDict( |
| 313 | + used=results_used, |
| 314 | + unused=results_unused, |
| 315 | + ) |
| 316 | + |
| 317 | + return response |
| 318 | + |
| 319 | + |
| 320 | +class Indexer: |
| 321 | + |
| 322 | + def __init__(self, engine: GrafanaSearch): |
| 323 | + self.engine = engine |
| 324 | + |
| 325 | + # Prepare index data structures. |
| 326 | + self.dashboard_by_uid = {} |
| 327 | + self.datasource_by_name = {} |
| 328 | + self.dashboard_datasource_index = {} |
| 329 | + self.datasource_dashboard_index = {} |
| 330 | + |
| 331 | + # Gather all data. |
| 332 | + self.dashboards = self.engine.scan_dashboards() |
| 333 | + self.datasources = self.engine.scan_datasources() |
| 334 | + |
| 335 | + # Invoke indexer. |
| 336 | + self.index() |
| 337 | + |
| 338 | + def index(self): |
| 339 | + self.index_dashboards() |
| 340 | + self.index_datasources() |
| 341 | + |
| 342 | + @staticmethod |
| 343 | + def collect_datasource_names(root): |
| 344 | + return list(set([item.datasource for item in root if item.datasource])) |
| 345 | + |
| 346 | + def index_dashboards(self): |
| 347 | + |
| 348 | + self.dashboard_by_uid = {} |
| 349 | + self.dashboard_datasource_index = {} |
| 350 | + |
| 351 | + for dashboard in self.dashboards: |
| 352 | + if dashboard.meta.isFolder: |
| 353 | + continue |
| 354 | + |
| 355 | + # Index by uid. |
| 356 | + uid = dashboard.dashboard.uid |
| 357 | + self.dashboard_by_uid[uid] = dashboard |
| 358 | + |
| 359 | + # Map to data source names. |
| 360 | + ds_panels = self.collect_datasource_names(dashboard.dashboard.panels) |
| 361 | + ds_annotations = self.collect_datasource_names(dashboard.dashboard.annotations.list) |
| 362 | + ds_templating = self.collect_datasource_names(dashboard.dashboard.templating.list) |
| 363 | + self.dashboard_datasource_index[uid] = list(sorted(set(ds_panels + ds_annotations + ds_templating))) |
| 364 | + |
| 365 | + def index_datasources(self): |
| 366 | + |
| 367 | + self.datasource_by_name = {} |
| 368 | + self.datasource_dashboard_index = {} |
| 369 | + |
| 370 | + for datasource in self.datasources: |
| 371 | + name = datasource.name |
| 372 | + self.datasource_by_name[name] = datasource |
| 373 | + |
| 374 | + for dashboard_uid, datasource_names in self.dashboard_datasource_index.items(): |
| 375 | + for datasource_name in datasource_names: |
| 376 | + self.datasource_dashboard_index.setdefault(datasource_name, []) |
| 377 | + self.datasource_dashboard_index[datasource_name].append(dashboard_uid) |
0 commit comments