diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..334bb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# https://editorconfig.org/ + +root = true + +[*.py] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4291be5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +env/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.pyc + +exporter/*/__pycache__/ + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ac6445e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.pythonPath": "env/bin/python3", + "python.linting.enabled": true, + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9574a27 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.7.4-alpine3.10 + +ADD exporter exporter/ +add requirements.txt exporter/requirements.txt + +WORKDIR exporter + +RUN pip install -r requirements.txt + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..329ca8b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Status Cake Exporter diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..0614d88 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,53 @@ +name: 0.1.$(rev:r) + +trigger: + batch: true + branches: + include: + - "*" + +pr: none + +pool: + vmImage: 'Ubuntu-16.04' + +variables: + IMAGE_NAME: chelnak/status-cake-exporter + +steps: + +- task: Docker@1 + displayName: Build image + inputs: + command: Build an image + imageName: $(IMAGE_NAME) + dockerFile: Dockerfile + addDefaultLabels: false + +- task: Docker@1 + displayName: Tag image with current build number $(Build.BuildNumber) + inputs: + command: Tag image + imageName: $(IMAGE_NAME) + arguments: $(IMAGE_NAME):$(Build.BuildNumber) + +- task: Docker@1 + displayName: Docker Hub login + inputs: + command: login + containerregistrytype: Container Registry + dockerRegistryEndpoint: Docker Hub + +- task: Docker@1 + displayName: Push tagged image + inputs: + command: Push an image + imageName: $(IMAGE_NAME):$(Build.BuildNumber) + +- task: Docker@1 + displayName: Push tagged image (latest) if master + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) + inputs: + command: Push an image + imageName: '$(IMAGE_NAME):latest' + diff --git a/exporter/app.py b/exporter/app.py new file mode 100644 index 0000000..c220140 --- /dev/null +++ b/exporter/app.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +import time +import logging + +from prometheus_client import start_http_server, REGISTRY +from collectors import test_collector +from utilities import logs, arguments + +if __name__ == "__main__": + + args = arguments.get_args() + + logs.configure_logging(args.log_level) + logger = logging.getLogger(__name__) + + logger.info("Starting web server") + start_http_server(8000) + + logger.info("Registering collectors") + REGISTRY.register(test_collector.TestCollector( + args.username, args.api_key, args.tags)) + + while True: + time.sleep(1) diff --git a/exporter/collectors/__init__.py b/exporter/collectors/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/exporter/collectors/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/exporter/collectors/test_collector.py b/exporter/collectors/test_collector.py new file mode 100644 index 0000000..7727bb9 --- /dev/null +++ b/exporter/collectors/test_collector.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import sys +import logging +from prometheus_client.core import GaugeMetricFamily +from status_cake_client import tests + +logger = logging.getLogger("test_collector") + + +def parse_test_response(r): + tests = [] + for i in r.json(): + tests.append( + { + "test_id": str(i['TestID']), + "test_type": i['TestType'], + "test_name": i['WebsiteName'], + "test_url": i['WebsiteURL'], + "test_status": i['Status'], + "test_uptime": str(i['Uptime']) + } + ) + + return tests + + +class TestCollector(object): + + def __init__(self, username, api_key, tags): + self.username = username + self.api_key = api_key + self.tags = tags + + def collect(self): + + logger.info("Collector started") + + try: + + response = tests.get_tests(self.api_key, self.username, self.tags) + + test_results = parse_test_response(response) + + label_names = test_results[0].keys() + + gauge = GaugeMetricFamily( + "status_cake_tests", + "A basic listing of the tests under the current account.", + labels=label_names) + + for i in test_results: + status = 1 if (i["test_status"] == "Up") else 0 + gauge.add_metric(i.values(), status) + + yield gauge + + except Exception as e: + logger.error(e) + sys.exit(1) + + logger.info("Collector finished") diff --git a/exporter/status_cake_client/__init__.py b/exporter/status_cake_client/__init__.py new file mode 100644 index 0000000..632ddec --- /dev/null +++ b/exporter/status_cake_client/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +STATUS_CAKE_BASE_URL = "https://app.statuscake.com/API/" diff --git a/exporter/status_cake_client/tests.py b/exporter/status_cake_client/tests.py new file mode 100644 index 0000000..f0e3f4f --- /dev/null +++ b/exporter/status_cake_client/tests.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import requests +import logging +from status_cake_client import STATUS_CAKE_BASE_URL + + +logger = logging.getLogger(__name__) + + +def get_tests(apikey, username, tags=""): + endpoint = "Tests" + request_url = "{base}{endpoint}".format( + base=STATUS_CAKE_BASE_URL, endpoint=endpoint) + + logger.info("Sending request to {request_url}".format( + request_url=request_url)) + + headers = { + "API": apikey, + "Username": username + } + + params = { + "tags": tags + } + + response = requests.get(url=request_url, params=params, headers=headers) + response.raise_for_status() + + return response diff --git a/exporter/utilities/__init__.py b/exporter/utilities/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/exporter/utilities/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/exporter/utilities/arguments.py b/exporter/utilities/arguments.py new file mode 100644 index 0000000..1600faa --- /dev/null +++ b/exporter/utilities/arguments.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import sys +import configargparse + + +def get_args(): + + parser = configargparse.ArgParser() + parser.add("--username", + dest="username", + env_var="USERNAME", + help='Username for the account') + + parser.add("--api-key", + dest="api_key", + env_var="API_KEY", + help="API key for the account") + + parser.add("--tests.tags", + dest="tags", + env_var="TAGS", + help="A comma separated list of tags used to filter tests " + "returned from the api") + + parser.add("--logging.level", + dest="log_level", + env_var="LOG_LEVEL", + default="info", + choices={'debug', 'info', 'warn', 'error'}, + help="Set a log level for the application") + + args = parser.parse_args() + + if args.username is None: + print("Required argument --username is missing") + print(parser.print_help()) + sys.exit(1) + + if args.api_key is None: + print("Required argument --username is missing") + print(parser.print_help()) + sys.exit(1) + + return args diff --git a/exporter/utilities/logs.py b/exporter/utilities/logs.py new file mode 100644 index 0000000..f0d16a6 --- /dev/null +++ b/exporter/utilities/logs.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import sys +import logging + + +def configure_logging(log_level): + + root_logger = logging.getLogger() + + log_levels = { + "debug": logging.DEBUG, + "info": logging.INFO, + "warn": logging.WARN, + "error": logging.ERROR, + } + + root_logger.setLevel(log_levels.get(log_level)) + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(log_levels.get(log_level)) + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + root_logger.addHandler(handler) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c5d5bd6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +prometheus-client==0.7.1 +requests==2.22.0 +flake8==3.7.8 +ConfigArgParse==0.14.0 + +