diff --git a/config/config_example.json b/config/config_example.json index 024eb8e4918bbe3929c7225253c94eb5625cbc6a..6137f97031011a7d2cb7e6ee428fc53ce57d80c2 100644 --- a/config/config_example.json +++ b/config/config_example.json @@ -19,6 +19,14 @@ "password" : "admin_password", "name" : "instancename" } + ], + "wekan" : [ + { + "url" : "https://my.wekan.tld", + "name" : "instancename", + "user": "admin_username", + "password": "admin_password" + } ] } } diff --git a/main.py b/main.py old mode 100644 new mode 100755 index c9ca30cb4e361e4e18e6eae4617615ee58f47107..f2cf88bf247d113ccb3a5c2eb211e57b496bcfac --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ from urllib.parse import urlparse from influxdb import InfluxDBClient from etherpad import EtherpadCollector from mattermost import MattermostCollector +from wekan import WekanCollector # Some constants DIR_NAME = os.path.dirname(os.path.realpath(__file__)) @@ -49,7 +50,7 @@ def influxb_connect(config): # Connect to influx db try: - return InfluxDBClient( + client = InfluxDBClient( host=o.hostname, port=port, username=config['user'], @@ -58,6 +59,8 @@ def influxb_connect(config): ssl=ssl, verify_ssl=verify ) + client.ping() + return client except Exception as e: print("Cannot connect to {} : {}".format(o.hostname, e)) print("If InfluxDB has just started, this is normal, please wait!") @@ -97,6 +100,13 @@ def main(): mattermost_data = mattermost.collect() influx_client.write_points(mattermost_data, 'ms') + # Get Wekan metrics and push to InfluxDB + if 'wekan' in config['modules']: + wekan = WekanCollector(config['modules']['wekan']) + wekan_data = wekan.collect() + print(wekan_data) + influx_client.write_points(wekan_data, 'ms') + if __name__ == "__main__": main() diff --git a/mattermost/mattermost.py b/mattermost/mattermost.py index f4cd162dfa3b13587f0852473f4f2478b1d581bb..9c8cb79a3c48f2844a4f64a2587dd00597cbedc5 100644 --- a/mattermost/mattermost.py +++ b/mattermost/mattermost.py @@ -9,7 +9,7 @@ from urllib.parse import urlparse from mattermostdriver import Driver -class MattermostCollector(object): +class MattermostCollector(): """ MattermostCollector. Collector for Mattermost stats. @@ -72,6 +72,7 @@ class MattermostCollector(object): # Get all instances stats for instance in self.instances: # Get current stats + print("Mattermost : collecting for instance {}".format(instance['name'])) data = self._get_stats(instance) if data is None: print('Unable to get stats from Mattermost instance ' + instance['config']['url']) diff --git a/wekan/__init__.py b/wekan/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ebd0af2a30f2d8a7d4335f12216ec099b4a5f4cf --- /dev/null +++ b/wekan/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 + +"""Package to collect Wekan metrics.""" + +from .wekan import WekanCollector diff --git a/wekan/wekan.py b/wekan/wekan.py new file mode 100644 index 0000000000000000000000000000000000000000..c6fbd4e9165430ce7c05701c72d09a1f8ab38504 --- /dev/null +++ b/wekan/wekan.py @@ -0,0 +1,150 @@ +# coding=utf-8 + +"""Functions to export Wekan metrics.""" + +import json +import datetime +import requests +from urllib.parse import urlparse +import sys + +class WekanCollector(): + """ + WekanCollector. + Collector for Wekan stats. + """ + + def __init__(self, config): + """ + Initialize a wekan collector object. + :param config: Configuration for Wekan module (list of instances) + """ + # Initialize list of instances connector + self.instances = [] + for instance in config: + if 'url' not in instance or 'name' not in instance or 'user' not in instance or 'password' not in instance: + print('Incorrect instance configuration\n') + print(instance) + print('"wekan" key on configuration file should be a list of object with "url", "user", "password" and "name" attributes') + continue + else: + if instance['url'].endswith('/'): + instance['url'] = instance['url'][:-1] + instance['token'] = self._login(instance) + if instance['token']: + self.instances.append(instance) + + def collect(self): + """ + Get the analytics of etherpad instances and returns a list of InfluxDB points. + :returns: List of InfluxDB formatted objects + """ + metrics = [] + # Get all instances stats + for instance in self.instances: + print("Wekan : collecting for instance {}".format(instance['name'])) + data = self._get_stats(instance) + # Get current miliseconds timestamp + current_timestamp = int(datetime.datetime.now().timestamp()*1000) + # Create metrics + metrics.append({ + 'measurement': 'wekan_total_users', + 'tags': { + 'name': instance['name'] + }, + 'time': current_timestamp, + 'fields': { + 'value': data['totalUsers'] + } + }) + metrics.append({ + 'measurement': 'wekan_public_boards', + 'tags': { + 'name': instance['name'] + }, + 'time': current_timestamp, + 'fields': { + 'value': data['publicBoards'] + } + }) + metrics.append({ + 'measurement': 'wekan_private_boards', + 'tags': { + 'name': instance['name'] + }, + 'time': current_timestamp, + 'fields': { + 'value': data['privateBoards'] + } + }) + print("Wekan : data collected for instance {}".format(instance['name'])) + + return metrics + + @classmethod + def _login(cls, instance): + """ + Login via the API to a Wekan instance. + :param instance: Configuration for Wekan instance (dict with at least "url", "user" and "password" keys) + :returns: Authentication token + """ + login = requests.post(instance['url'] + '/users/login', + data={ + "username": instance['user'], + "password": instance['password'] + }) + + if login.status_code != 200: + print('Unable to login to {} instance : {}'.format(instance['url'], login.reason)) + return None + + return login.json()['token'] + + @classmethod + def _get_stats(cls, instance): + """ + Get stats for an Wekan instance. + :param instance: Configuration for Wekan instance (dict with at least "url" and "token" key) + :returns: JSON data + """ + headers = {'Authorization': f'Bearer {instance["token"]}'} + data = {} + + # Get user count + users = requests.get(instance['url'] + '/api/users', headers=headers).json() + # Users are a list, a JSON is generated if the request fails + # But the request itself will always send a HTTP 200, even if it fails... + if isinstance(users, dict) and users.get('statusCode', 200) != 200: + print('Unable to get users from {} instance : {}'.format(instance['url'], users['reason'])) + data['totalUsers'] = 0 + else: + data['totalUsers'] = len(users) + + public_boards = requests.get(instance['url'] + '/api/boards', headers=headers).json() + if isinstance(public_boards, dict) and public_boards.get('statusCode', 200) != 200: + print('Unable to get public boards from {} instance : {}'.format(instance['url'], public_boards['reason'])) + data['publicBoards'] = 0 + else: + data['publicBoards'] = len(public_boards) + + # API does not have a method to get all boards, so first get + # boards for each users + all_boards = [] + for user in users: + boards = requests.get(instance['url'] + f'/api/users/{user["_id"]}/boards', headers=headers).json() + if isinstance(public_boards, dict) and public_boards.get('statusCode', 200) != 200: + print('Unable to get public boards from {} instance : {}'.format(instance['url'], public_boards['reason'])) + else: + # Then remove default boards (we don't care) + for e in boards: + if e['title'] == 'Welcome Board' or e['title'] == 'Templates': + boards.remove(e) + all_boards.extend(boards) + + # Finally filter unique boards + all_boards_unique = list({v['_id']: v for v in all_boards}.values()) + + # Then infer number of private boards from total boards - public boards + data['privateBoards'] = len(all_boards_unique) - len(public_boards) + + return data