Merge pull request #2 from tubearchivist/testing

Minimum Viable Product created and functional
This commit is contained in:
Ainsey11 2022-04-15 01:16:36 +01:00 committed by GitHub
commit 5ba7d310ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 256 additions and 4 deletions

6
Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM python:3.10-slim-buster
WORKDIR /app
COPY tubearchivist-metrics/requirements.txt .
RUN pip3 install -r requirements.txt
COPY . .
CMD ["python3", "-u", "tubearchivist-metrics/main.py"]

View File

@ -3,7 +3,83 @@ Provide Tube Archivist metrics in Prometheus/OpenMetrics format
This is an optional service as part of the Tube Archivist stack.
This is WIP
**This is WIP**
---
 
## Metrics reported
```
channel_count = Number of channels
playlist_count = Number of playlists
download_count = Number of downloads
download_queue = Number of pending downloads
subtitle_count = Number of subtitles downloaded for videos
```
## Configuration
---
### Environment variables
```
ES_URL: The URL to your ElasticSearch server. Defaults to http://archivist-es:9200
ES_USER: The username for authentication to ElasticSearch. Defaults to elastic
ES_PASSWORD: The password for authentication to ElasticSearch. No default is set.
LISTEN_PORT: The listen port for the metrics server to run on. Defaults to 9934
POLL_INTERVAL: The interval in seconds for the data to be scraped from ElasticSearch. Defaults to 60
```
---
### Running via Docker Compose (supported)
This metrics service is designed to be ran inside of docker.
To view the main compose file for TubeArchivist, please see the main repository here: [TA Repo](https://github.com/bbilly1/tubearchivist)
To add the metrics service in, place this into your compose file and update the environment variables as required.
```
archivist-metrics:
image: tubearchivist-metrics:latest
container_name: archivist-metrics
restart: always
environment:
- "ES_USER=elastic"
- "ES_PASSWORD=verysecret"
- "ES_URL=http://archivist-es:9200"
- "LISTEN_PORT=9934"
- "POLL_INTERVAL=60"
ports:
- 9934:9934
```
---
### Running via Standalone
Should you want to, you can install and run this without docker.
To do so, clone this repo, install the python libraries with `pip3 install -r requirements.txt`
then run `python3 tubearchivist-metrics/main.py`
Environment variables can be passed in via normal syntax for your OS.
---
### Prometheus example config
```
- job_name: 'tubearchivist-metrics'
metrics_path: /
static_configs:
- targets:
- <server>:9934
```
---
## How are metrics gathered?
Typically, a prometheus server will poll the HTTP endpoint of the metrics service to obtain its metrics.
In most scenarios, a service will then retrieve the data for the metric, and then respond to the prometheus http call. However this can be quite harsh on databases and applications, especially when prometheus is polling every 15 seconds.
To prevent performance issues and unncessecary load on ElasticSearch. We prefetch the metric information from ES every 60 seconds (default). The metric is then updated on the HTTP endpoint after we have retrieved the data and cached for prometheus to scrape.
This means prometheus can scrape the endpoint every second if it likes, but no database calls to ES will be made until the polling interval is reached.
If you require more granular polling, you can update the `POLLING_INTERVAL` environment variable

8
deploy.sh Normal file → Executable file
View File

@ -1,3 +1,5 @@
#!/bin/bash
function validate {
if [[ $1 ]]; then
@ -21,10 +23,10 @@ function validate {
}
exit 0
if [[ $1 == "validate" ]]; then
validate
else
echo "valid options are: validate"
fi
fi
exit 0

View File

@ -0,0 +1,32 @@
"""
Functionality for setting up the environment for the metrics package.
Reads in environment variables for the application to use.
"""
import os
class AppConfig:
def __init__(self) -> None:
self.config = self.get_config()
@staticmethod
def get_config():
"""
Reads in environment variables for the application to use.
"""
es_pass = os.environ.get("ES_PASSWORD")
es_user = os.environ.get("ES_USER", default="elastic")
es_url = os.environ.get("ES_URL", default="http://archivist-es:9200")
listen_port = os.environ.get("LISTEN_PORT", default="9934")
poll_interval = os.environ.get("POLL_INTERVAL", default="60")
application = {
"es_url": es_url,
"es_user": es_user,
"es_pass": es_pass,
"listen_port": listen_port,
"poll_interval": poll_interval,
}
return application

View File

@ -0,0 +1,59 @@
from multiprocessing import AuthenticationError
from elasticsearch import (
Elasticsearch,
ConnectionError,
ConnectionTimeout,
AuthenticationException,
AuthorizationException,
)
from environment import AppConfig
from time import sleep
class ElasticWrapper:
def handle_err(error):
print("Connection Error: " + str(error))
print("There was a problem connecting to Elasticsearch")
print(
"Please see the error above. This may be as Elasticsearch is still starting up or a misconfiguration"
)
print("Sleeping for 10 seconds...")
sleep(10)
def get_count(index_name):
"""
Returns the number of documents in the index
"""
config = AppConfig().config
es_url = config["es_url"]
es_user = config["es_user"]
es_pass = config["es_pass"]
es = Elasticsearch(
[es_url],
basic_auth=(es_user, es_pass),
timeout=10,
max_retries=12,
retry_on_timeout=True,
)
response = 0
try:
response = es.count(index=index_name)["count"]
except AuthenticationException as e:
ElasticWrapper.handle_err(e)
except ConnectionError as e:
ElasticWrapper.handle_err(e)
except ConnectionTimeout as e:
ElasticWrapper.handle_err(e)
except AuthenticationError as e:
ElasticWrapper.handle_err(e)
except AuthorizationException as e:
ElasticWrapper.handle_err(e)
except:
print("Unknown error occurred. Check your credentials, url and try again.")
print("Sleeping for 10 seconds...")
sleep(10)
else:
return response

View File

@ -0,0 +1,11 @@
from esconnect import ElasticWrapper
class GetMetrics:
@staticmethod
def count(index_name):
"""Get count of documents from ES"""
result = ElasticWrapper.get_count(index_name)
print("Metric for " + index_name + ": " + str(result))
return int(result)

View File

@ -0,0 +1,64 @@
import time
from prometheus_client import start_http_server, Gauge
from environment import AppConfig
from getmetrics import GetMetrics
config = AppConfig().config
# Print configuration on console when starting the application
print("Configuration is currently set to:")
print("Elasticsearch URL: " + config["es_url"])
print("Listen Port: " + config["listen_port"])
print("Poll Interval (seconds): " + config["poll_interval"])
class AppMetrics:
# fmt: off
def __init__(self, poll_interval=int(config["poll_interval"])):
self.poll_interval = poll_interval
# Metrics to expose
self.channel_count = Gauge("yta_channel_count", "Number of channels")
self.playlist_count = Gauge("yta_playlist_count", "Number of playlists")
self.download_count = Gauge("yta_download_count", "Number of downloads")
self.download_queue = Gauge("yta_download_queue", "Number of pending downloads")
self.subtitle_count = Gauge("yta_subtitle_count", "Number of subtitles downloaded for videos")
# fmt: on
def run_metrics_loop(self):
"""
Runs a loop that will update the metrics every second.
"""
while True:
self.retrieve_metrics()
time.sleep(self.poll_interval)
def retrieve_metrics(self):
"""
Retrieves the metrics from the database and updates the metrics.
"""
print("Obtaining Metrics from Elasticsearch")
self.channel_count.set(GetMetrics.count(index_name="ta_channel"))
self.playlist_count.set(GetMetrics.count(index_name="ta_playlist"))
self.download_queue.set(GetMetrics.count(index_name="ta_download"))
self.download_count.set(GetMetrics.count(index_name="ta_video"))
self.subtitle_count.set(GetMetrics.count(index_name="ta_subtitle"))
def main():
"""Main Entry Point"""
listen_port = int(config["listen_port"])
poll_interval = int(config["poll_interval"])
app_metrics = AppMetrics(
poll_interval=poll_interval,
)
start_http_server(listen_port)
app_metrics.run_metrics_loop()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,2 @@
elasticsearch==8.1.2
prometheus_client==0.14.1