First commit

This commit is contained in:
w1kl4s 2022-06-30 23:06:30 +02:00
commit 2a8a4c5115
Signed by: w1kl4s
GPG Key ID: 7C5B542C41DCE7AE
9 changed files with 372 additions and 0 deletions

135
.gitignore vendored Normal file
View File

@ -0,0 +1,135 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# 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
*.py,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
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# 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/
# Vim
*.swp
# VSCode
.vscode/

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM python:alpine3.16
COPY . /src
RUN pip install /src && rm -rf /src
ENTRYPOINT ["/usr/local/bin/radio_exporter"]

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2022 S.O.N.G@git.gammaspectra.live
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
README.md Normal file
View File

@ -0,0 +1 @@
# radio_exporter - Exporter for [FinalCommander](https://git.gammaspectra.live/S.O.N.G/FinalCommander "FinalCommander") and [OrbitalBeat](https://git.gammaspectra.live/S.O.N.G/OrbitalBeat "OrbitalBeat")

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[bdist_wheel]
universal=0

45
setup.py Normal file
View File

@ -0,0 +1,45 @@
from setuptools import setup, find_packages
# To use a consistent encoding
from codecs import open
from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(here, "README.md"), encoding="utf-8") as f:
long_description = f.read()
setup(
name="radio_exporter",
version='0.1.0',
description="",
long_description=long_description,
url="https://git.gammaspectra.live/S.O.N.G/radio_exporter",
author="w1kl4s",
author_email="w1kl4s@protonmail.com",
license="BSD 2-Clause",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: System Administrators",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
python_requires=">=3.6, <4",
keywords="monitoring metrics exporter radio",
packages=["src"],
install_requires=[
"requests"
],
entry_points={
"console_scripts": ["radio_exporter=src.main:start"]
}
)

96
src/Collector.py Normal file
View File

@ -0,0 +1,96 @@
import time
import json
import random
import requests
from collections.abc import MutableMapping
from dataclasses import dataclass
@dataclass
class Metric:
prefix: str
value_name: str
labels: dict
value: int
def format_prom(self) -> str:
labels = ','.join(x + '=' + '"' + str(self.labels[x]) + '"' for x in self.labels.keys())
return f"{self.prefix}_{self.value_name}{{{labels}}} {self.value}"
class Collector(object):
def __init__(self):
#self.finalcommander_url = "radio.animebits.moe/api/fcmm_status"
self.fcmm_urls = ["https://radio.animebits.moe/api/fcmm_status"]
def _fetch_json(self, url):
try:
r = requests.get(url)
data = json.loads(r.text)
return data
except json.JSONDecodeError:
raise CollectorException(f"Failed to parse JSON at {url}")
except requests.exceptions.ConnectionError:
raise CollectorException(f"Failed to reach {url}")
except requests.exceptions.ReadTimeout:
raise CollectorException(f"Connection timeout to {url}")
except:
raise CollectorException("Request failed.")
def get_fcmm(self, url):
data = self._fetch_json(url)
if "public_key" not in data.keys() or "servers" not in data.keys():
raise CollectorException("FinalCommander JSON seems to be wrong.")
return data
def get_instances(self, fcmm_data):
for server in fcmm_data["servers"]:
try:
data = self._fetch_json(f"https://{server['Address']}/stats")
except CollectorException:
data = {}
data["fcmm_data"] = server
yield data
def collect(self):
metrics = []
fcmm_data = []
for url in self.fcmm_urls:
try:
fcmm_data.append(self.get_fcmm(url))
up = 1
except CollectorException:
# TODO: log here, unreachable FCMM
up = 0
metrics.append(Metric("fcmm", "up", {"url": url}, up))
servers_data = [x for y in [self.get_instances(fcmm) for fcmm in fcmm_data] for x in y]
for server_data in servers_data:
baselabel = {"server": server_data["fcmm_data"]["Address"]}
if server_data["fcmm_data"]["LastCheckResult"]:
metrics.append(Metric("orbt", "up", {**baselabel}, 1))
else:
metrics.append(Metric("orbt", "up", {**baselabel}, 0))
continue
for name, data in server_data["statistics"].items():
if name == "http" or name == "tls":
for k,v in data.items():
for subtype,value in v.items():
metrics.append(Metric("orbt_stats", f"{name}_{k}", {**baselabel, **{k: subtype}}, value))
else:
for valuetype, value in data.items():
metrics.append(Metric("orbt_stats", name, {**baselabel, **{"type": valuetype}}, value))
for name, value in server_data["database"].items():
metrics.append(Metric("orbt_db", name, {**baselabel}, value))
return metrics
class CollectorException(Exception):
"""
Base exception class raised by Collector
"""

47
src/PrometheusServer.py Normal file
View File

@ -0,0 +1,47 @@
import socket
import signal
from ipaddress import ip_address
from http.server import HTTPServer,BaseHTTPRequestHandler
from selectors import DefaultSelector, EVENT_READ
from src import Collector
class PromServer(HTTPServer):
def __init__(self, address):
if ip_address(address[0]).version == 6:
self.address_family = socket.AF_INET6
super().__init__(address, self.RequestHandler)
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/metrics":
return self.send_error(404)
metrics = [x.format_prom() for x in self.server.collector.collect()]
output = '\n'.join(metrics) + '\n'
self.send_response(200)
self.end_headers()
self.wfile.write(output.encode())
def signal_handler(self, signum, frame):
print('Signal handler called with signal', signum)
self.interrupt_write.send(b'\0')
def serve_forever(self):
sel = DefaultSelector()
sel.register(self.interrupt_read, EVENT_READ)
sel.register(self, EVENT_READ)
while True:
for key, _ in sel.select():
if key.fileobj == self.interrupt_read:
self.interrupt_read.recv(1)
return
if key.fileobj == self:
self.handle_request()
def start(self):
self.collector = Collector.Collector()
self.interrupt_read, self.interrupt_write = socket.socketpair()
signal.signal(signal.SIGINT, self.signal_handler)
self.serve_forever()

16
src/main.py Normal file
View File

@ -0,0 +1,16 @@
import json
import argparse
from src import PrometheusServer
def start():
parser = argparse.ArgumentParser(description="Run radio exporter")
parser.add_argument("-p", "--port", metavar="port", action='store', type=int, default=8888, help='Port for http server to listen on')
parser.add_argument("-a", "--address", metavar="address", action='store', default='0.0.0.0', help='Address for http server to listen on')
args = parser.parse_args()
server = PrometheusServer.PromServer((args.address, args.port))
server.start()
if __name__ == '__main__':
start()