From cd951c10eedaacb40feb393ec802f96e07546082 Mon Sep 17 00:00:00 2001
From: Quentin Duchemin <quentinduchemin@tuta.io>
Date: Tue, 13 Oct 2020 09:55:48 +0200
Subject: [PATCH] Add Wekan stat collection

---
 config/config_example.json |   8 ++
 main.py                    |  12 ++-
 mattermost/mattermost.py   |   3 +-
 wekan/__init__.py          |   5 ++
 wekan/wekan.py             | 150 +++++++++++++++++++++++++++++++++++++
 5 files changed, 176 insertions(+), 2 deletions(-)
 mode change 100644 => 100755 main.py
 create mode 100644 wekan/__init__.py
 create mode 100644 wekan/wekan.py

diff --git a/config/config_example.json b/config/config_example.json
index 024eb8e..6137f97 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 c9ca30c..f2cf88b
--- 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 f4cd162..9c8cb79 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 0000000..ebd0af2
--- /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 0000000..c6fbd4e
--- /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
-- 
GitLab