Commit aaad57fd authored by wolfsack's avatar wolfsack
Browse files

from private git repo

parent bb36ef96
# Auto detect text files and perform LF normalization
* text=auto
/exporter/venv
/.idea
/prometheus/prom
/grafana/grafana-storage
# RaspberryPiMonitoring
On the RaspberryPi:
git clone https://github.com/wolfsack/RaspberryPiMonitoring.git
cd RaspberryPiMonitoring
bash setup.sh
docker-compose up -d
# Demo
Hier gibt es eine laufende [Demo](http://wutterfly.com/grafana).
username: user
password: user
# How it was made
Das Projekt besteht aus drei Teilen:
- einem Python Server welcher Daten über den Pi erhebt und bereitstellt
- einem Prometheus Server welcher die Daten sammelt
- einem Graphana Server welcher sich die Daten vom Prometheus Server holt und hübsch anzeigt
Grundsätzlich können alle 3 Server auf verschiedenen Machinen laufen, wichtig ist nur das der Python Server auf dem RaspberryPi läuft.
In diesem Projekt werden alle drei Server als Docker Container bereitgestellt.
----
## Der Python Server
Der Python Server läuft auf dem RaspberryPi und soll Daten über den Pi abfragen können und als WebSite darstellen.
Um eine WebSite mit Python zu erstellen bietet sich das Mini-Framework Flask an.
Folgende Abhängigkeiten werden in diesem Projekt gebraucht:
Flask==2.0.2 # Framework zum erstellen von HTTP-Server Anwendungen
psutil==5.8.0 # Paket zum sammeln von System Daten
waitress==2.0.0 # WSGI Server um Flask in einer Production Umgebung zu starten
Der Einstiegspunkt für die Flask App ist die Datei [app.py](./exporter/app/app.py). Hier wird eine FlaskApp erstellt und ein HTTP-Endpoint regestriert. Der Pfad des Endpoints kann dabei beliebig gewählt werden.
In der Methode die beim abfragen des Endpoints aufgerufen wird, wird zunächst ein HTTP-Response Objekt mit dem Status-Code 200 erstellt. Der Inhalt des Response Objekts sind die Daten welche wir über den RaspberryPi erheben wollen als String. Anschließen wird der [Mime-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) der Antwort auf "text/plain" gesetzt. Die Formatierung der Daten als auch der Response-Type sind wichtig, da Prometheus die Daten parsen muss. Zuletzt wird die Antwort zurück geschickt.
Das sammeln der gewünschten Daten passiert in der Funktion "metrics" in [metrics.py](./exporter/app/metrics.py).
Auf den meisten (und auch auf meinem) RaspberryPis läuft ein Linux basiertes OS. Um Metriken über das System und Hardware zu erhalten werden Dateien in dem Ordner "/proc" ausgelesen. Daher ist es wichtig psutil mitzuteilen wo dieser Ordner ist.
Hier werden zunächst alle Daten gesammelt, und dann in "Metrics" Objekte verpackt. Die "Metrics" Klasse aus [metric.py](./exporter/app/metric.py) macht es einfacher einzelne Daten formatiert als String auszugeben. Wie genau eine Metric formatiert werden muss ist von Prometheus vorgegeben und kann [hier](https://prometheus.io/docs/instrumenting/writing_exporters/) nachgelesen werden. Alternativ kann auch ein Python Package verwendet werden, statt die Formatierung selber zu schreiben. Die "Metric" Objekte werden in einer Liste gesammelt und in der Funktion "generate_metrics" aus [metrics.py](./exporter/app/metrics.py) in einen String formatiert. Diese kann dann in einer HTTP-Response verwendet werde.
Im Momement werden folgende Metriken gesammlt:
cpu_usage -> CPU Last
cpu_cores -> Nummer der CPU Kerne
memory_usage -> Arbeitsspeicher Last
boot_time -> Zeitpunkt an dem der Pi gestarted wurde
system_time -> Zeitpunkt der Abfrage
tcp_connections -> Anzahl der TCP Verbindungen
partitions -> Partitionen
cpu_temperature -> CPU Temperatur
Zusätzlich zu dem Typ und dem Namen der Metrik können auch noch Tags definiert werden, welche die Metrik näher spezifizeren.
Folgende Tags gibt es:
- für "cpu_cores":
- "type":
- "all" -> alle CPU Kerne die das System erkennt
- "physical" -> nur physische CPU Kerne
- für "memory_usage":
- "type":
- "total" -> größe des Arbeitsspeichers
- "available" -> verfügbarer Arbeitsspeicher
- "used" -> genutzter Arbeitsspeicher
- "free" -> freier Arbeitsspeicher
(Näheres zur Klassifizierung von Arbeitsspeicher kann [hier](https://haydenjames.io/free-vs-available-memory-in-linux/) gefunden werden.)
Alle Metriken haben zusätzlich einen Tag "node". Dieser gibt die Nummber des RaspberryPis an. Diese ist nützlich falls meherer Pis überwacht werden sollen. Dies macht es möglich die Metriken zu gruppieren.
Die Flask App ist hiermit schon fertig. Flask bietet zum erstellen einer Flask App einen Development Server bereit, diese ist jedoch nicht für eine Production Umgebung geeignet.
Eine Flask App bietet jedoch auch ein WSG Interface, welches von WSGI Servern genutzt werden kann. Ein solcher Server ist "waitress".
Damit "waitress" die Flask App nutzen kann wird ein Python file [wsgi.py](./exporter/wsgi.py) erstellt. Wichtig ist hier nur der import Teil.
Nun kann die App mit
waitress-serve --listen=*:5000 wsgi:app
auf port 5000 gestartet werden.
Zu letzt wird die Python App noch in ein Dockerfile verpackt.
Als BaseImage wird "Python:3.9" genutzt. Anschließend werden die nötigen Abhängigkeiten mit pip installiert, die Python Datei in das Image Copiert und waitress als Entrypoint festgelegt. Sollte das DockerImage nun als Container gestartet werden, startet automatisch die Flask App.
----
## Prometheus
Prometheus ist ein Programm zum sammeln und speichern von Zeitreihen-Daten. Auf DockerHub wird ein DockerImage für Prometheus bereitgestellt. Es müssen nur noch Ziele konfiguriert werden, welche Prometheus abfragen soll.
In der [prometheus.yml](./prometheus/prometheus.yml) Konfigurations Datei wird als Ziel die FlaskApp angegeben.
scrape_configs:
- job_name: 'raspberry-1'
metrics_path: /node/1/metrics
scrape_interval: 5s
static_configs:
- targets: ['wutterfly.com']
Als job_name kann ein beliebiger Name gewählt werden.
Der metrics_path ist vom Deployment der Flask App abhängig. Wenn kein ReverseProxy oder ähnliches verwendet wird welcher den Request Pfad ändert, muss hier der Pfad zum HTTP-Endpoint angegeben werden welcher in der FlaskApp ("@app.route('/node/1/metrics')") angegben wurde.
Der scrape_interval legt fest in welchem Interval an dem Ziel Daten gesammelt werden soll.
Und unter static_config > targets wird der host und port festgelegt unter dem das Ziel zu finden ist. Das kann eine Domain, aber auch eine IP-Addresse sein.
Achtung mit Localhost. Da sich Prometheus in einem DockerContainer befindet funktionier Localhost nicht, auch wenn der Flask DockerContainer auf der gleichen Maschine läuft. Dafür kann hier aber auch Docker DNS genutzt werden.
----
## Graphana
Graphana ist ein Programm welches Daten aus einer Datenquelle, hier Prometheus, abfragt und in Graphen und ähnlichem anzeigt.
Auch für Graphana gibt es bereits ein DockerImage auf DockerHub.
Graphana ist ein wenig komplizierter zu konfigurieren als Prometheus.
Hier gibt es verschiedene Wege Graphana zu konfigurieren. In diesem Projekt wird ein eigenes DockerImage gebaut welches alle Dashbords und Datenquellen Konfigurationen enthält.
In der [grafana.ini](./grafana/grafana.ini) Datei werden algemeine Konfigurationen eingetragen.
In dem Ordner [provisioning](./grafana/provisioning/) gibt es die beiden Ordner [dashboards](./grafana/provisioning/dashboards/) und [datasources](./grafana/provisioning/datasources/).
In dem Ordner [dashboards](./grafana/provisioning/dashboards/) werden Konfigurations Dateien hinterlegt welche beschreiben welche Dashboards es gibt. Die konkreten Dashboards werden als JSON Datei im Order [dashboards](./grapahana/dashboards) hinterlegt.
Tipp: Dashboards können in der Graphana GUI erzeugt und als JSON exportiert werden.
In dem Ordner [datasources](./grafana/provisioning/datasources/) werden Konfigurations Dateien hinterlegt welche beschreiben welche Datenquellen es gibt. Hier wird als Datenquelle der Prometheus Server angegben.
In dem Dockerfile wird als "grafana/grafana:8.2.1" gewählt, und anschließend die Order [provisioning](./grafana/provisioning/) und [dashboards](./grapahana/dashboards) hinzugefügt. Zusätzlich können über Umgebungs Variablen zusätzliche Konfigurationen getätigt werden.
----
## All in One - DockerCompose
Um alle Server auf einmal zu starten und um zusätzliche Konfiguration so einfach zu möglich zu machen wird DockerCompose genutzt.
Als Services wird die FlaskApp, Prometheus und Graphana angegeben.
In der FlaskApp ist es wichtig das der "/proc" Ordner in den Container gemountet wird. Über die Umgebungsvariable "ROOT_FS" wird der FlaskApp mitgeteilt wo im Container der "/proc" Ordner ist. Als Image wird das FlaskApp DockerFile angegeben. Zuletzt wird noch der Port 5000 freigegeben damit Anfragen von außerhalb in den Container gelangen können.
Für den Prometheus Service muss nur der Port 9090 freigegeben werden und die Konfigurations Datei [prometheus.yml](./prometheus/prometheus.yml) in den Container gemounted werden. Zusätzlich wird noch ein Volume bereitgestellt in dem Prometheus die gesammelten Daten persistent speichern kann.
Für den Graphana Service muss der Port 3000 freigeben werden. Zusätzlich wird noch ein Volume bereit gestellt in dem Graphana entstehende Daten persistent speichern kann, als auch der Admin Nutzername und Password über Umgebungsvariablen gesetzt.
Mit
docker-compose up -d
können so alle 3 Services gleichzeitig im Hintergrund gestartet werden.
----
## Volumes und Rechte
In der DockerCompose Datei wurden einige Volumes festgelegt in dem Programm e Daten persistent speichern können. Volumes sind in dem Fall Order in dem Daten gespeichert werden können. Sollten diese Order noch nicht existieren werden sie automatisch von Docker erstellt.
Dies kann jedoch zu Problemen führen, wenn die Programme in den DockerContainern nicht die Rechte haben neue Datei anzulegen oder diese zu bearbeiten.
Da ich Linux nur nutze wenn es sein muss und mich nie wirklich mit dem System hinter Berechtigungen und Besitz von Ordnern und Datei beschäftigt habe, gibt es ein einfaches [Script](./setup.sh) welches vor dem ersten Aufruf der DockerCompose Datei die benötigten Order mit entsprechenden Rechten erstellt.
Dies ist zwar eine Lösung die Funktioniert, sollte jedoch noch überarbeitet werden.
version: "3"
services:
pi:
build: ./exporter/.
image: pi:latest
container_name: pi
environment:
- ROOT_FS=/rootfs
volumes:
- /:/rootfs
ports:
- "5000:5000"
restart: unless-stopped
prometheus:
image: prom/prometheus:v2.30.3
container_name: prometheus
ports:
- 9090:9090
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./prom:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/prom:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
networks:
moni_ipv6:
ipv6_address: 2001:638:408:200:FC36:dddd::11
restart: unless-stopped
depends_on:
- pi
graphana:
image: grafana/grafana:6.5.0
container_name: graphana
grafana:
build: ./grafana/.
image: grafana-pi:latest
container_name: grafana
user: '472'
ports:
- 3000:3000
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin_pass
- GF_USERS_ALLOW_SIGN_UP=false
networks:
moni_ipv6:
ipv6_address: 2001:638:408:200:FC36:dddd::19
volumes:
- ./grafana/grafana-storage:/var/lib/grafana
restart: unless-stopped
networks:
moni_ipv6:
external: true
name: moni_ipv6
depends_on:
- prometheus
- pi
......@@ -2,16 +2,20 @@ FROM python:3.9
ENV PYTHONUNBUFFERED 1
WORKDIR /app
WORKDIR /wrk
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY /app .
COPY /app /wrk/app
COPY wsgi.py wsgi.py
ENV ROOT_FS "/rootfs"
EXPOSE 5000
ENTRYPOINT ["python"]
ENTRYPOINT [ "waitress-serve" ]
CMD ["app.py"]
\ No newline at end of file
CMD ["--listen=*:5000", "wsgi:app"]
\ No newline at end of file
......@@ -2,20 +2,24 @@ from flask import Flask, make_response
from app.metrics import generate_metrics
# create Flask App
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
# define HTTP endpoint
@app.route('/node/<path:node>/metrics')
def metrics(node):
# create response with a body containing metrics as string and status-code 200
response = make_response(generate_metrics(node), 200)
# set response type to text/plain
# important because prometheus needs plain text to parse the data correctly
response.mimetype = "text/plain"
# send response
return response
# only for testing purpose
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
# class to create new metrics
class Metric:
# constructor
def __init__(self, metric_name, metric_type, comment, value, params):
# if some key values are missing return error
if metric_name is None or metric_type is None or value is None:
raise ValueError
# safe values in new entity
self.metric_name = metric_name
self.metric_type = metric_type
self.comment = comment
self.value = value
self.params = params
# return entity values as formatted string for prometheus to parse
def to_string(self):
# "# HELP metrics_name comment"
help_string = f'# HELP {self.metric_name} {self.comment}'
# "# TYPE metrics_name metric_type"
type_string = f'# TYPE {self.metric_name} {self.metric_type}'
output = ""
# only add a pure comment line if there is a comment
if self.comment is not None:
output += help_string + "\n"
output += type_string + "\n" + f'{self.metric_name}{self.__params_to_string()} {self.value}'
return output
# format params to string
def __params_to_string(self):
if self.params is None:
......
from app.metric import Metric
import platform
import os
import time
import psutil
psutil.PROCFS_PATH = os.environ['ROOT_FS'] + "/proc"
#psutil.PROCFS_PATH = "/rootfs" + "/proc"
#psutil.PROCFS_PATH = '/rootfs/proc'
# get data and create list of Metrics
def metrics(node: int):
# get CPU
cpu = psutil.cpu_percent()
# get number of cpu cores
cores = psutil.cpu_count()
# get number of physical cpu cores
cores_physical = psutil.cpu_count(logical=False)
# get memory data
memory = psutil.virtual_memory()
# get boot time of host system
boot_time = psutil.boot_time()
# get system time
system_time = time.time()
# get number of tcp connections
tcp_connections = len(psutil.net_connections(kind="tcp"))
# get disk partitions
partitions = psutil.disk_partitions()
# create list of Metrics
metrics_list = [
Metric(
metric_name="cpu_usage",
metric_type="gauge",
comment="CPU Usage in Percent",
value=cpu,
params={"node": f"{node}"},
),
Metric(
metric_name="cpu_cores",
metric_type="gauge",
comment="Total CPU Cores",
value=cores,
params={"type": "all", "node": f"{node}"},
),
Metric(
metric_name="cpu_cores",
metric_type="gauge",
comment="Total CPU Cores",
value=cores_physical,
params={"type": "physical", "node": f"{node}"},
),
Metric(
metric_name="boot_time",
metric_type="gauge",
comment="Time in sec since epoch",
value=boot_time,
params={"node": f"{node}"},
),
Metric(
metric_name="system_time",
metric_type="gauge",
comment="Time in sec since epoch",
value=system_time,
params={"node": f"{node}"},
),
Metric(
metric_name="tcp_connections",
metric_type="gauge",
comment="Number of TCP connections",
value=tcp_connections,
params={"node": f"{node}"},
),
Metric(
metric_name="memory_usage",
metric_type="gauge",
comment="Memory Usage Data",
value=memory[0],
params={"type": "total", "node": f"{node}"},
),
Metric(
metric_name="memory_usage",
metric_type="gauge",
comment="Memory Usage Data",
value=memory[1],
params={"type": "available", "node": f"{node}"},
),
Metric(
metric_name="memory_usage",
metric_type="gauge",
comment="Memory Usage Data",
value=memory[3],
params={"type": "used", "node": f"{node}"},
),
Metric(
metric_name="memory_usage",
metric_type="gauge",
comment="Memory Usage Data",
value=memory[4],
params={"type": "free", "node": f"{node}"},
),
]
# raspberry pi exclusive
if platform.system() == "Linux":
from app.temperature import get_temp
# get cpu temperature
temp = get_temp()
# add to metrics list
metrics_list.append(
Metric(
metric_name="cpu_temperature",
metric_type="gauge",
comment="CPU Temperature",
value=temp,
params={"node": f"{node}"},
)
)
# iterate over partitions and add important data to Metrics list
for partition in partitions:
disk = psutil.disk_usage(partition[1])
metrics_list.append(
Metric(
metric_name="disk_usage",
metric_type="gauge",
comment="Disk Usage Data",
value=disk[0],
params={"mount": partition[1],
"type": "total", "node": f"{node}"},
)
)
metrics_list.append(
Metric(
metric_name="disk_usage",
metric_type="gauge",
comment="Disk Usage Data",
value=disk[1],
params={"mount": partition[1],
"type": "used", "node": f"{node}"},
)
)
metrics_list.append(
Metric(
metric_name="disk_usage",
metric_type="gauge",
comment="Disk Usage Data",
value=disk[2],
params={"mount": partition[1],
"type": "free", "node": f"{node}"},
)
)
# return list of Metrics
return metrics_list
# generates formatted string of metrics
def generate_metrics(node: int):
output = ""
# get list of Metrics and concatenates them to a single string
for metric in metrics(node):
output += metric.to_string() + "\n" + "\n"
return output
import os
# get file to read
TEMPERATURE_FILE = os.environ['ROOT_FS'] + "/sys/class/hwmon/hwmon0/temp1_input"
# function to get temperature as float
def get_temp() -> float :
# open file in read mode
with open(TEMPERATURE_FILE, 'r') as f:
# read file and cast to int
tmp_string = f.read()
tmp_int = int(tmp_string)
# return as float
return tmp_int / 1000
\ No newline at end of file
Flask==2.0.2 # Framework for creating HTTP server applications
psutil==5.8.0 # Package to collect system data
waitress==2.0.0 # WSGI Server to run the Flask app
FROM grafana/grafana:8.2.1
ENV GF_USERS_ALLOW_SIGN_UP "true"
# Add provisioning
ADD ./provisioning /etc/grafana/provisioning
# Add configuration file
ADD ./grafana.ini /etc/grafana/grafana.ini
# Add dashboard json files
ADD ./dashboards /etc/grafana/dashboards
\ No newline at end of file
......@@ -27,7 +27,7 @@
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": null,
"datasource": "Prometheus",
"decimals": null,
"format": "h",
"gauge": {
......@@ -38,7 +38,7 @@
"thresholdMarkers": true
},
"gridPos": {
"h": 5,
"h": 7,
"w": 5,
"x": 0,
"y": 0
......@@ -112,7 +112,7 @@
"rgba(237, 129, 40, 0.89)",
"#F2495C"
],
"datasource": null,
"datasource": "Prometheus",
"format": "none",
"gauge": {
"maxValue": 100,
......@@ -122,7 +122,7 @@
"thresholdMarkers": true
},
"gridPos": {
"h": 5,
"h": 7,
"w": 4,
"x": 5,
"y": 0
......@@ -195,8 +195,8 @@
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": null,
"format": "decmbytes",
"datasource": "Prometheus",
"format": "decgbytes",
"gauge": {
"maxValue": 100,
"minValue": 0,
......@@ -205,12 +205,12 @@
"thresholdMarkers": true
},
"gridPos": {
"h": 5,
"h": 2,
"w": 4,
"x": 9,
"y": 0
},
"id": 12,
"id": 16,
"interval": null,
"links": [],
"mappingType": 1,
......@@ -250,14 +250,14 @@
"tableColumn": "",
"targets": [
{
"expr": "memory_usage{type=\"total\" ,node=\"1\"} / 1000000000",
"expr": "disk_usage{mount=\"/rootfs\" ,type=\"total\" ,node=\"1\"} / 1000000000",
"refId": "A"
}
],
"thresholds": "",