Compare commits

9 Commits

Author SHA1 Message Date
550b5261e1 feat: Docker-Integration hinzugefügt - Dockerfile, docker-compose.yml und .dockerignore erstellt - README.md mit umfassender Docker-Dokumentation erweitert - Multi-Service-Architektur für Hauptservice und Statistik-Service - Lokale Volume-Mappings für .env und .json Dateien - Vollständige Docker-Handhabung dokumentiert 2025-08-11 10:03:40 +02:00
f79fc24b3b feat: Statistik-Funktionalität für TI-Status Checker hinzugefügt
- Neue Klasse OutageStatistics für automatische Störungsaufzeichnung
- Sammelt Häufigkeit und Dauer von Ausfällen
- Parameter --stats/-s zum Senden von Statistik-Berichten über Apprise
- Erweiterte Kommandozeilen-Argumente (--debug/-d)
- Automatische Aufzeichnung von Störungsbeginn und -ende
- Detaillierte Statistiken mit Trends und Service-spezifischen Daten
- Umfassende Dokumentation in README_STATISTICS.md
2025-08-11 09:44:58 +02:00
e092022774 Fix: Zeitstempel aus Duplikatserkennung entfernt, Wiederholungen von Meldungen verhindert 2025-08-06 10:32:42 +02:00
4b58b138c9 Fix: Entferne nicht unterstütztes sender-Argument bei Apprise.notify() 2025-08-06 10:04:07 +02:00
be89e66261 Change Apprise sender name to 'TI-Status Bot by medisoftware' 2025-08-01 14:23:52 +02:00
b5e767a265 Störungsmeldungen enthalten jetzt Zeitstempel für eindeutige Benachrichtigungen 2025-07-29 15:00:13 +02:00
6e6f58c32d Entwarnungs-Logik: Benachrichtigung bei Aufhebung einer Störung implementiert, Batch-Datei-Vorschlag dokumentiert 2025-07-29 10:51:43 +02:00
fcb6cd7995 Mock-API angepasst: /lageapi/v2/tilage mit Störung, .gitignore für *.json und __pycache__/ aktualisiert 2025-07-29 10:22:04 +02:00
93c693f481 Add Postman Collection für TI-Status-API 2025-07-29 08:33:14 +02:00
12 changed files with 1156 additions and 44 deletions

49
.dockerignore Normal file
View File

@@ -0,0 +1,49 @@
# Git
.git
.gitignore
# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env
pip-log.txt
pip-delete-this-directory.txt
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.mypy_cache
.pytest_cache
.hypothesis
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Dokumentation
README*.md
LICENSE.txt
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Lokale Konfiguration und Daten
.env
*.json
*.cmd

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
.venv/ .venv/
.env .env
test*
*.json
__pycache__/

View File

@@ -1,11 +1,26 @@
# Dockerfile für TI-Status2Mattermost # Dockerfile für TI-Status2Mattermost
FROM python:3.11-slim FROM python:3.11-slim
# Arbeitsverzeichnis setzen
WORKDIR /app WORKDIR /app
COPY requirements.txt ./ # System-Abhängigkeiten installieren
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Python-Abhängigkeiten kopieren und installieren
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . # Anwendungsdateien kopieren
COPY ti_status_checker.py .
COPY ti_statistics.py .
# Nicht-root Benutzer erstellen
RUN useradd --create-home --shell /bin/bash appuser && \
chown -R appuser:appuser /app
USER appuser
# Standardbefehl
CMD ["python", "ti_status_checker.py"] CMD ["python", "ti_status_checker.py"]

154
README.md
View File

@@ -13,9 +13,12 @@ Ein Python-Skript, das den TI-Status überwacht und neue Meldungen über Apprise
- Vermeidet Duplikate durch lokale Statusverfolgung - Vermeidet Duplikate durch lokale Statusverfolgung
- Debug-Ausgaben für bessere Transparenz - Debug-Ausgaben für bessere Transparenz
- Umfassendes Test-Tool - Umfassendes Test-Tool
- [Statistische Erfassung der Störungen](./README_STATISTICS.md)
## Installation ## Installation
### Option 1: Lokale Python-Installation
1. Repository klonen: 1. Repository klonen:
```bash ```bash
git clone https://gitea.medisoftware.org/Markus/TI-Status2Mattermost.git git clone https://gitea.medisoftware.org/Markus/TI-Status2Mattermost.git
@@ -36,6 +39,23 @@ source .venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
``` ```
### Option 2: Docker (Empfohlen)
1. Repository klonen:
```bash
git clone https://gitea.medisoftware.org/Markus/TI-Status2Mattermost.git
cd TI-Status2Mattermost
```
2. Docker und Docker Compose installieren (falls noch nicht vorhanden)
3. Konfiguration einrichten (siehe Abschnitt "Konfiguration")
4. Container starten:
```bash
docker-compose up --build
```
## Konfiguration ## Konfiguration
1. Kopiere die Beispiel-Konfiguration: 1. Kopiere die Beispiel-Konfiguration:
@@ -96,13 +116,112 @@ DEBUG_MODE=true
Weitere Apprise-URLs findest du in der [Apprise-Dokumentation](https://github.com/caronc/apprise#supported-notifications). Weitere Apprise-URLs findest du in der [Apprise-Dokumentation](https://github.com/caronc/apprise#supported-notifications).
## Docker-Architektur
### Services
#### ti-status-checker
- **Hauptservice** für die kontinuierliche Überwachung der TI-Status-API
- Läuft dauerhaft und prüft regelmäßig auf neue Meldungen
- Sendet Benachrichtigungen bei Änderungen
- Zeichnet Störungen in der Statistik auf
#### ti-statistics
- **Statistik-Service** für periodische Berichte
- Kann manuell oder automatisch ausgeführt werden
- Generiert und sendet detaillierte Ausfall-Statistiken
- Läuft unabhängig vom Hauptservice
### Volume-Mappings
Alle wichtigen Dateien werden als lokale Volumes eingebunden:
```yaml
volumes:
# Konfiguration (read-only)
- ./.env:/app/.env:ro
# Persistierte Daten
- ./ti_status_state.json:/app/ti_status_state.json
- ./ti_outage_statistics.json:/app/ti_outage_statistics.json
```
**Vorteile:**
- ✅ Daten bleiben auf dem lokalen System
- ✅ Einfache Updates ohne Datenverlust
- ✅ Backup der Konfiguration und Daten möglich
- ✅ Debugging und Monitoring von außen
### Netzwerk
- Isoliertes Docker-Netzwerk `ti-status-network`
- Services können untereinander kommunizieren
- Externe Verbindungen nur für API-Calls und Benachrichtigungen
### Umgebungsvariablen
```yaml
environment:
- DBG_MODE=false # Kann in .env überschrieben werden
```
### Automatischer Restart
```yaml
restart: unless-stopped
```
- Container startet automatisch nach Neustart des Hosts
- Bei Fehlern wird der Container neu gestartet
- Nur bei manuellem Stopp bleibt der Container gestoppt
## Verwendung ## Verwendung
### Hauptskript ausführen: ### Mit lokaler Python-Installation
#### Hauptskript ausführen:
```bash ```bash
python ti_status_checker.py python ti_status_checker.py
``` ```
### Mit Docker
#### Container starten:
```bash
# Alle Services starten
docker-compose up --build
# Nur den Hauptservice starten
docker-compose up ti-status-checker
# Im Hintergrund laufen lassen
docker-compose up -d
```
#### Container verwalten:
```bash
# Logs anzeigen
docker-compose logs -f ti-status-checker
# Container stoppen
docker-compose down
# Container neu starten
docker-compose restart ti-status-checker
# Status anzeigen
docker-compose ps
```
#### Statistik-Service:
```bash
# Nur Statistik-Bericht senden
docker-compose run --rm ti-statistics
# Statistik-Service dauerhaft starten
docker-compose up ti-statistics
```
### Verbindungstest: ### Verbindungstest:
```bash ```bash
python test_apprise.py python test_apprise.py
@@ -127,12 +246,23 @@ Das Skript gibt dann detaillierte Debug-Informationen aus:
- Benachrichtigungsregeln-Auswertung - Benachrichtigungsregeln-Auswertung
- Endpunkt-Status - Endpunkt-Status
### Für kontinuierliche Überwachung (z.B. mit cron): ### Für kontinuierliche Überwachung
#### Mit lokaler Python-Installation (cron):
```bash ```bash
# Alle 5 Minuten ausführen # Alle 5 Minuten ausführen
*/5 * * * * cd /path/to/TI-Status2Mattermost && python ti_status_checker.py */5 * * * * cd /path/to/TI-Status2Mattermost && python ti_status_checker.py
``` ```
#### Mit Docker (automatischer Restart):
```bash
# Container läuft dauerhaft und startet automatisch neu
docker-compose up -d
# Für periodische Ausführung alle 5 Minuten (in docker-compose.yml aktivieren):
# command: ["sh", "-c", "while true; do python ti_status_checker.py; sleep 300; done"]
```
## Benachrichtigungsregeln ## Benachrichtigungsregeln
### Filter ### Filter
@@ -162,15 +292,33 @@ Apprise unterstützt über 80 verschiedene Benachrichtigungsdienste, darunter:
## Dateien ## Dateien
### Anwendung
- `ti_status_checker.py` - Hauptskript mit Multi-Endpoint-Support - `ti_status_checker.py` - Hauptskript mit Multi-Endpoint-Support
- `ti_statistics.py` - Statistik-Funktionalität für Störungen
- `test_apprise.py` - Umfassendes Test-Tool für alle Endpunkte - `test_apprise.py` - Umfassendes Test-Tool für alle Endpunkte
### Konfiguration
- `requirements.txt` - Python-Abhängigkeiten (python-dotenv, apprise) - `requirements.txt` - Python-Abhängigkeiten (python-dotenv, apprise)
- `env.example` - Beispiel-Konfiguration mit allen Optionen - `env.example` - Beispiel-Konfiguration mit allen Optionen
- `ti_status_state.json` - Lokale Statusverfolgung (wird automatisch erstellt)
- `.env` - Deine Konfiguration (nicht im Repository) - `.env` - Deine Konfiguration (nicht im Repository)
### Docker
- `Dockerfile` - Container-Image für die Anwendung
- `docker-compose.yml` - Multi-Service-Orchestrierung
- `.dockerignore` - Dateien die vom Docker Build ausgeschlossen werden
### Daten
- `ti_status_state.json` - Lokale Statusverfolgung (wird automatisch erstellt)
- `ti_outage_statistics.json` - Statistik-Daten der Störungen (wird automatisch erstellt)
## Changelog ## Changelog
### Version 3.1 (Docker-Support)
- ✅ Vollständige Docker-Integration mit Dockerfile und docker-compose.yml
- ✅ Multi-Service-Architektur (Hauptservice + Statistik-Service)
- ✅ Lokale Volume-Mappings für .env und .json Dateien
- ✅ Automatischer Restart und Container-Management
### Version 3.0 (Multi-Endpoint & Rules) ### Version 3.0 (Multi-Endpoint & Rules)
- ✅ Mehrere Apprise-Endpunkte gleichzeitig - ✅ Mehrere Apprise-Endpunkte gleichzeitig
- ✅ Konfigurierbare Benachrichtigungsregeln (Filter, Zeiten, Verzögerungen) - ✅ Konfigurierbare Benachrichtigungsregeln (Filter, Zeiten, Verzögerungen)

149
README_STATISTICS.md Normal file
View File

@@ -0,0 +1,149 @@
# TI-Status Checker - Statistik-Funktionalität
## Übersicht
Der TI-Status Checker wurde um umfangreiche Statistik-Funktionalität erweitert, die automatisch alle Störungen und deren Dauer aufzeichnet.
## Neue Features
### 1. Automatische Störungsaufzeichnung
- **Störungsbeginn**: Wird automatisch erkannt und aufgezeichnet
- **Störungsende**: Wird automatisch erkannt und die Dauer berechnet
- **Störungstypen**: Unterscheidung zwischen "full" und "partial" Störungen
### 2. Detaillierte Statistiken
- **Service-spezifische Statistiken**: Für jeden betroffenen Service
- **Gesamtstatistiken**: Über alle Störungen hinweg
- **Trend-Analysen**: Durchschnittswerte der letzten 30 Tage
- **Zeitreihen**: Chronologische Aufzeichnung aller Störungen
### 3. Statistik-Berichte
- **Automatische Berichte**: Über Apprise versendbar
- **Markdown-Format**: Gut lesbar in Chat-Systemen
- **Zusammenfassungen**: Übersichtliche Darstellung der wichtigsten Kennzahlen
## Verwendung
### Normale Ausführung (Störungen überwachen)
```bash
python ti_status_checker.py
```
### Statistik-Bericht senden
```bash
python ti_status_checker.py --stats
# oder
python ti_status_checker.py -s
```
### Debug-Modus aktivieren
```bash
python ti_status_checker.py --debug
# oder
python ti_status_checker.py -d
```
## Statistik-Daten
### Gespeicherte Informationen
- **Störungsbeginn**: ISO-Zeitstempel
- **Störungsende**: ISO-Zeitstempel
- **Dauer**: In Minuten
- **Service-Name**: Betroffener Dienst
- **Störungstyp**: "full" oder "partial"
- **Status**: "active" oder "resolved"
### Berechnete Kennzahlen
- Gesamtanzahl Störungen
- Gesamtdauer aller Störungen
- Durchschnittsdauer pro Störung
- Längste Störung
- Am stärksten betroffener Service
- Tägliche Durchschnittswerte
- Trends der letzten 30 Tage
## Dateien
### Neue Dateien
- `ti_statistics.py`: Statistik-Funktionen und -Klasse
- `ti_outage_statistics.json`: Gespeicherte Statistik-Daten
### Erweiterte Dateien
- `ti_status_checker.py`: Hauptscript mit Statistik-Integration
## Beispiel-Statistik-Bericht
```
📊 **TI-Status Ausfall-Statistiken**
==================================================
**Zusammenfassung:**
• Gesamte Störungen: 15
• Aktive Störungen: 2
• Gesamtdauer: 1245 Minuten
• Durchschnittsdauer: 83.0 Minuten
• Längste Störung: 180 Minuten
• Am stärksten betroffen: konnektor
**Service-Statistiken:**
• **KONNEKTOR**:
- Störungen: 8
- Gesamtdauer: 720 Min
- Durchschnitt: 90.0 Min
- Längste: 180 Min
**Trends (letzte 30 Tage):**
• Durchschnitt Störungen/Tag: 0.5
• Durchschnitt Dauer/Tag: 41.5 Min
• Tage mit Störungen: 12
• Max. Störungen an einem Tag: 3
**Letzte Störungen (7 Tage):**
• **KONNEKTOR** (partial)
- 15.01. 14:30 - 15.01. 16:45 (135 Min)
```
## Konfiguration
Die Statistik-Funktionalität verwendet dieselben Apprise-Konfigurationen wie der normale Status-Checker. Stellen Sie sicher, dass in Ihrer `.env` Datei mindestens eine Apprise-URL konfiguriert ist:
```env
APPRISE_URL_MATTERMOST=mattermost://...
APPRISE_URL_SLACK=slack://...
# etc.
```
## Automatisierung
### Cron-Job für regelmäßige Statistiken
```bash
# Täglich um 9:00 Uhr Statistik-Bericht senden
0 9 * * * cd /path/to/script && python ti_status_checker.py --stats
```
### Windows Task Scheduler
Erstellen Sie eine geplante Aufgabe, die regelmäßig `python ti_status_checker.py --stats` ausführt.
## Fehlerbehebung
### Häufige Probleme
1. **Keine Apprise-URLs konfiguriert**: Prüfen Sie Ihre `.env` Datei
2. **Berechtigungsfehler**: Stellen Sie sicher, dass das Script Schreibrechte im Verzeichnis hat
3. **JSON-Fehler**: Bei korrupten Statistik-Daten wird automatisch eine neue Datei erstellt
### Debug-Modus
Verwenden Sie `--debug` für detaillierte Ausgaben und Fehlerdiagnose.
## Erweiterte Verwendung
### Statistik-Daten exportieren
Die Statistik-Daten werden in `ti_outage_statistics.json` gespeichert und können für weitere Analysen verwendet werden.
### Benutzerdefinierte Berichte
Die `OutageStatistics` Klasse kann in eigene Scripts importiert werden, um maßgeschneiderte Berichte zu erstellen.
## Support
Bei Fragen oder Problemen mit der Statistik-Funktionalität:
1. Aktivieren Sie den Debug-Modus mit `--debug`
2. Prüfen Sie die Logs auf Fehlermeldungen
3. Stellen Sie sicher, dass alle Abhängigkeiten installiert sind

View File

@@ -0,0 +1,234 @@
{
"info": {
"_postman_id": "ti-status-api-collection",
"name": "TI-Status API",
"description": "Collection für die TI-Status-API der gematik\n\nEndpunkte:\n- GET /lageapi/v2/tilage - Aktuelle TI-Lage Meldungen\n\nVerwendung:\n1. Importiere diese Collection in Postman\n2. Teste die API-Endpunkte\n3. Überprüfe die JSON-Antworten",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "TI-Lage API",
"item": [
{
"name": "GET Aktuelle TI-Lage Meldungen",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
},
{
"key": "User-Agent",
"value": "TI-Status-Checker/1.0",
"type": "text"
}
],
"url": {
"raw": "https://ti-lage.prod.ccs.gematik.solutions/lageapi/v2/tilage",
"protocol": "https",
"host": [
"ti-lage",
"prod",
"ccs",
"gematik",
"solutions"
],
"path": [
"lageapi",
"v2",
"tilage"
]
},
"description": "Holt alle aktuellen TI-Lage Meldungen von der gematik API"
},
"response": [
{
"name": "Erfolgreiche Antwort",
"originalRequest": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "https://ti-lage.prod.ccs.gematik.solutions/lageapi/v2/tilage",
"protocol": "https",
"host": [
"ti-lage",
"prod",
"ccs",
"gematik",
"solutions"
],
"path": [
"lageapi",
"v2",
"tilage"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"cookie": [],
"body": "{\n \"meldungen\": [\n {\n \"zeitpunkt\": \"2024-01-15T10:30:00Z\",\n \"titel\": \"Beispiel Meldung\",\n \"beschreibung\": \"Dies ist eine Beispiel-Beschreibung für eine TI-Lage Meldung.\",\n \"link\": \"https://fachportal.gematik.de/ti-status\",\n \"kategorie\": \"wartung\",\n \"prioritaet\": \"normal\"\n }\n ],\n \"letzteAktualisierung\": \"2024-01-15T10:30:00Z\",\n \"anzahlMeldungen\": 1\n}"
}
]
},
{
"name": "GET API Status (Health Check)",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "https://ti-lage.prod.ccs.gematik.solutions/lageapi/v2/health",
"protocol": "https",
"host": [
"ti-lage",
"prod",
"ccs",
"gematik",
"solutions"
],
"path": [
"lageapi",
"v2",
"health"
]
},
"description": "Prüft den Status der API (falls verfügbar)"
},
"response": []
}
],
"description": "Hauptendpunkte der TI-Status-API"
},
{
"name": "Tests & Beispiele",
"item": [
{
"name": "Test mit verschiedenen Headers",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
},
{
"key": "Cache-Control",
"value": "no-cache",
"type": "text"
},
{
"key": "X-Requested-With",
"value": "XMLHttpRequest",
"type": "text"
}
],
"url": {
"raw": "https://ti-lage.prod.ccs.gematik.solutions/lageapi/v2/tilage",
"protocol": "https",
"host": [
"ti-lage",
"prod",
"ccs",
"gematik",
"solutions"
],
"path": [
"lageapi",
"v2",
"tilage"
]
},
"description": "Test mit zusätzlichen Headers für bessere Kompatibilität"
},
"response": []
}
],
"description": "Zusätzliche Tests und Beispiele für die API"
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"// Pre-request Script für TI-Status API",
"console.log('TI-Status API Request gestartet');",
"console.log('URL:', pm.request.url.toString());",
"console.log('Method:', pm.request.method);"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"// Test Script für TI-Status API",
"pm.test('Status Code ist 200', function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('Response ist JSON', function () {",
" pm.response.to.be.json;",
"});",
"",
"pm.test('Response hat meldungen Array', function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('meldungen');",
" pm.expect(jsonData.meldungen).to.be.an('array');",
"});",
"",
"pm.test('Meldungen haben korrekte Struktur', function () {",
" const jsonData = pm.response.json();",
" if (jsonData.meldungen && jsonData.meldungen.length > 0) {",
" const meldung = jsonData.meldungen[0];",
" pm.expect(meldung).to.have.property('zeitpunkt');",
" pm.expect(meldung).to.have.property('titel');",
" pm.expect(meldung).to.have.property('beschreibung');",
" }",
"});",
"",
"// Log Response für Debugging",
"console.log('Response Status:', pm.response.status);",
"console.log('Response Time:', pm.response.responseTime + 'ms');",
"console.log('Response Size:', pm.response.size().body + ' bytes');"
]
}
}
],
"variable": [
{
"key": "base_url",
"value": "https://ti-lage.prod.ccs.gematik.solutions",
"type": "string"
},
{
"key": "api_version",
"value": "v2",
"type": "string"
}
]
}

70
api_dynamic.py Normal file
View File

@@ -0,0 +1,70 @@
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import List, Optional, Any
app = FastAPI()
# Pydantic-Modelle für die Struktur
class AffectedFunction(BaseModel):
function: str
critical: int
impactDesc: str
outage: str # "none"|"partial"|"full"
hasMaintenance: bool
class AppStatusEntry(BaseModel):
outage: str = "none"
hasMaintenance: bool = False
hasSubComponentMaintenance: bool = False
affectedFunctions: List[AffectedFunction] = Field(default_factory=list)
class AppStatus(BaseModel):
erezept: Optional[AppStatusEntry] = None
epa: Optional[AppStatusEntry] = None
kim: Optional[AppStatusEntry] = None
wanda: Optional[AppStatusEntry] = None
ogd: Optional[AppStatusEntry] = None
vsdm: Optional[AppStatusEntry] = None
tianschluss: Optional[AppStatusEntry] = None
class StatusModel(BaseModel):
appStatus: AppStatus
cause: List[Any] = Field(default_factory=list)
# Initialer Status mit einer Störung bei erezept
status_data = StatusModel(
appStatus=AppStatus(
erezept=AppStatusEntry(
outage="partial",
hasMaintenance=False,
hasSubComponentMaintenance=False,
affectedFunctions=[
AffectedFunction(
function="Signatur",
critical=1,
impactDesc="Signatur ist zeitweise nicht möglich",
outage="partial",
hasMaintenance=False
)
]
),
epa=AppStatusEntry(),
kim=AppStatusEntry(),
wanda=AppStatusEntry(),
ogd=AppStatusEntry(),
vsdm=AppStatusEntry(),
tianschluss=AppStatusEntry()
),
cause=[]
)
@app.get("/lageapi/v2/tilage", response_model=StatusModel)
def get_tilage():
return status_data
@app.post("/lageapi/v2/tilage", response_model=StatusModel)
def set_tilage(new_status: StatusModel):
global status_data
status_data = new_status
return status_data

46
docker-compose.yml Normal file
View File

@@ -0,0 +1,46 @@
services:
ti-status-checker:
build: .
container_name: ti-status-checker
restart: unless-stopped
volumes:
# .env Datei für Konfiguration
- ./.env:/app/.env:ro
# JSON-Dateien für Persistierung
- ./ti_status_state.json:/app/ti_status_state.json
- ./ti_outage_statistics.json:/app/ti_outage_statistics.json
environment:
# Debug-Modus (kann in .env überschrieben werden)
- DBG_MODE=false
networks:
- ti-status-network
# Optional: Cron-ähnliche Ausführung alle 5 Minuten
# command: ["sh", "-c", "while true; do python ti_status_checker.py; sleep 300; done"]
# Optional: Statistik-Service der täglich läuft
ti-statistics:
build: .
container_name: ti-statistics
restart: unless-stopped
volumes:
- ./.env:/app/.env:ro
- ./ti_outage_statistics.json:/app/ti_outage_statistics.json
command: ["python", "ti_status_checker.py", "--stats"]
environment:
- DBG_MODE=false
networks:
- ti-status-network
# Läuft einmal täglich um 8:00 Uhr
# depends_on:
# - ti-status-checker
networks:
ti-status-network:
driver: bridge
volumes:
# Lokale Volumes für Persistierung
ti-status-state:
driver: local
ti-outage-statistics:
driver: local

View File

@@ -0,0 +1,2 @@
fastapi
uvicorn

11
run_checker_loop.cmd Normal file
View File

@@ -0,0 +1,11 @@
@echo off
REM Setze den Pfad zu deinem venv (ggf. anpassen!)
set VENV_PATH=%~dp0.venv
REM Aktiviere das venv
call "%VENV_PATH%\Scripts\activate.bat"
:loop
python ti_status_checker.py
timeout /t 120
goto loop

240
ti_statistics.py Normal file
View File

@@ -0,0 +1,240 @@
import json
import os
from datetime import datetime, timedelta
from collections import defaultdict, Counter
import statistics
STATS_FILE = "ti_outage_statistics.json"
class OutageStatistics:
def __init__(self):
self.stats_file = STATS_FILE
self.stats = self.load_statistics()
def load_statistics(self):
"""Lädt gespeicherte Statistiken aus der JSON-Datei"""
try:
if os.path.exists(self.stats_file):
with open(self.stats_file, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(f"Warnung: Konnte Statistiken nicht laden: {e}")
# Standard-Struktur für neue Statistiken
return {
"outages": [],
"services": {},
"summary": {
"total_outages": 0,
"total_duration_minutes": 0,
"average_duration_minutes": 0,
"longest_outage_minutes": 0,
"most_affected_service": "",
"last_updated": ""
}
}
def save_statistics(self):
"""Speichert Statistiken in die JSON-Datei"""
try:
with open(self.stats_file, "w", encoding="utf-8") as f:
json.dump(self.stats, f, ensure_ascii=False, indent=2)
except IOError as e:
print(f"Fehler beim Speichern der Statistiken: {e}")
def record_outage_start(self, service_name, outage_type, timestamp=None):
"""Zeichnet den Beginn einer Störung auf"""
if timestamp is None:
timestamp = datetime.now().isoformat()
# Prüfe ob bereits eine aktive Störung für diesen Service existiert
for outage in self.stats["outages"]:
if (outage["service"] == service_name and
outage["status"] == "active" and
outage["type"] == outage_type):
# Störung bereits aktiv, nicht erneut aufzeichnen
return
outage_record = {
"service": service_name,
"type": outage_type,
"start_time": timestamp,
"end_time": None,
"status": "active",
"duration_minutes": None
}
self.stats["outages"].append(outage_record)
self.update_service_stats(service_name, "start")
self.save_statistics()
def record_outage_end(self, service_name, timestamp=None):
"""Zeichnet das Ende einer Störung auf"""
if timestamp is None:
timestamp = datetime.now().isoformat()
# Finde die aktive Störung für diesen Service
for outage in self.stats["outages"]:
if outage["service"] == service_name and outage["status"] == "active":
outage["end_time"] = timestamp
outage["status"] = "resolved"
# Berechne Dauer
start_time = datetime.fromisoformat(outage["start_time"])
end_time = datetime.fromisoformat(timestamp)
duration = end_time - start_time
outage["duration_minutes"] = int(duration.total_seconds() / 60)
self.update_service_stats(service_name, "end", outage["duration_minutes"])
break
self.save_statistics()
def update_service_stats(self, service_name, event_type, duration_minutes=None):
"""Aktualisiert die Service-spezifischen Statistiken"""
if service_name not in self.stats["services"]:
self.stats["services"][service_name] = {
"total_outages": 0,
"total_duration_minutes": 0,
"average_duration_minutes": 0,
"longest_outage_minutes": 0,
"last_outage": None
}
service_stats = self.stats["services"][service_name]
if event_type == "start":
service_stats["total_outages"] += 1
service_stats["last_outage"] = datetime.now().isoformat()
elif event_type == "end" and duration_minutes:
service_stats["total_duration_minutes"] += duration_minutes
service_stats["average_duration_minutes"] = (
service_stats["total_duration_minutes"] / service_stats["total_outages"]
)
if duration_minutes > service_stats["longest_outage_minutes"]:
service_stats["longest_outage_minutes"] = duration_minutes
def update_summary_stats(self):
"""Aktualisiert die Zusammenfassungs-Statistiken"""
active_outages = [o for o in self.stats["outages"] if o["status"] == "active"]
resolved_outages = [o for o in self.stats["outages"] if o["status"] == "resolved"]
total_duration = sum(o.get("duration_minutes", 0) for o in resolved_outages)
durations = [o.get("duration_minutes", 0) for o in resolved_outages if o.get("duration_minutes")]
self.stats["summary"] = {
"total_outages": len(resolved_outages),
"active_outages": len(active_outages),
"total_duration_minutes": total_duration,
"average_duration_minutes": statistics.mean(durations) if durations else 0,
"longest_outage_minutes": max(durations) if durations else 0,
"most_affected_service": self.get_most_affected_service(),
"last_updated": datetime.now().isoformat()
}
def get_most_affected_service(self):
"""Ermittelt den am stärksten betroffenen Service"""
if not self.stats["services"]:
return ""
most_affected = max(
self.stats["services"].items(),
key=lambda x: x[1]["total_outages"]
)
return most_affected[0]
def get_recent_outages(self, days=30):
"""Gibt Störungen der letzten X Tage zurück"""
cutoff_date = datetime.now() - timedelta(days=days)
recent_outages = []
for outage in self.stats["outages"]:
if outage["status"] == "resolved" and outage["end_time"]:
end_time = datetime.fromisoformat(outage["end_time"])
if end_time >= cutoff_date:
recent_outages.append(outage)
return sorted(recent_outages, key=lambda x: x["end_time"], reverse=True)
def get_outage_trends(self, days=30):
"""Analysiert Trends in den Störungen"""
recent_outages = self.get_recent_outages(days)
# Gruppiere nach Datum
daily_outages = defaultdict(list)
for outage in recent_outages:
date = outage["end_time"][:10] # YYYY-MM-DD
daily_outages[date].append(outage)
# Berechne Durchschnitt pro Tag
daily_counts = [len(outages) for outages in daily_outages.values()]
daily_durations = []
for outages in daily_outages.values():
daily_duration = sum(o.get("duration_minutes", 0) for o in outages)
daily_durations.append(daily_duration)
return {
"daily_average_outages": statistics.mean(daily_counts) if daily_counts else 0,
"daily_average_duration": statistics.mean(daily_durations) if daily_durations else 0,
"total_days_with_outages": len(daily_outages),
"most_outages_in_day": max(daily_counts) if daily_counts else 0
}
def generate_statistics_report(self):
"""Generiert einen detaillierten Statistik-Bericht"""
self.update_summary_stats()
report = []
report.append("📊 **TI-Status Ausfall-Statistiken**")
report.append("=" * 50)
# Zusammenfassung
summary = self.stats["summary"]
report.append(f"**Zusammenfassung:**")
report.append(f"• Gesamte Störungen: {summary['total_outages']}")
report.append(f"• Aktive Störungen: {summary['active_outages']}")
report.append(f"• Gesamtdauer: {summary['total_duration_minutes']} Minuten")
report.append(f"• Durchschnittsdauer: {summary['average_duration_minutes']:.1f} Minuten")
report.append(f"• Längste Störung: {summary['longest_outage_minutes']} Minuten")
report.append(f"• Am stärksten betroffen: {summary['most_affected_service']}")
report.append("")
# Service-spezifische Statistiken
if self.stats["services"]:
report.append("**Service-Statistiken:**")
for service, stats in sorted(
self.stats["services"].items(),
key=lambda x: x[1]["total_outages"],
reverse=True
)[:10]: # Top 10 Services
report.append(f"• **{service.upper()}**:")
report.append(f" - Störungen: {stats['total_outages']}")
report.append(f" - Gesamtdauer: {stats['total_duration_minutes']} Min")
report.append(f" - Durchschnitt: {stats['average_duration_minutes']:.1f} Min")
report.append(f" - Längste: {stats['longest_outage_minutes']} Min")
report.append("")
# Trends der letzten 30 Tage
trends = self.get_outage_trends(30)
report.append("**Trends (letzte 30 Tage):**")
report.append(f"• Durchschnitt Störungen/Tag: {trends['daily_average_outages']:.1f}")
report.append(f"• Durchschnitt Dauer/Tag: {trends['daily_average_duration']:.1f} Min")
report.append(f"• Tage mit Störungen: {trends['total_days_with_outages']}")
report.append(f"• Max. Störungen an einem Tag: {trends['most_outages_in_day']}")
report.append("")
# Letzte Störungen
recent_outages = self.get_recent_outages(7) # Letzte 7 Tage
if recent_outages:
report.append("**Letzte Störungen (7 Tage):**")
for outage in recent_outages[:5]: # Top 5
duration = outage.get("duration_minutes", 0)
start_time = datetime.fromisoformat(outage["start_time"]).strftime("%d.%m. %H:%M")
end_time = datetime.fromisoformat(outage["end_time"]).strftime("%d.%m. %H:%M")
report.append(f"• **{outage['service'].upper()}** ({outage['type']})")
report.append(f" - {start_time} - {end_time} ({duration} Min)")
report.append("")
report.append(f"_Bericht generiert am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}_")
return "\n".join(report)

View File

@@ -6,11 +6,23 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
import apprise import apprise
import time import time
import argparse
from ti_statistics import OutageStatistics
# Lade Umgebungsvariablen aus .env Datei # Lade Umgebungsvariablen aus .env Datei
load_dotenv() load_dotenv()
TI_API_URL = "https://ti-lage.prod.ccs.gematik.solutions/lageapi/v2/tilage" def is_debug_mode():
"""Prüft ob Debug-Modus aktiviert ist"""
return os.getenv('DBG_MODE', 'false').lower() == 'true'
def get_ti_api_url():
"""Gibt die API-URL zurück, im Debug-Modus ggf. die lokale Test-API"""
# if is_debug_mode():
# return os.getenv("TI_API_URL_DEBUG", "http://localhost:8000/lageapi/v2/tilage")
return os.getenv("TI_API_URL", "https://ti-lage.prod.ccs.gematik.solutions/lageapi/v2/tilage")
TI_API_URL = get_ti_api_url()
STATE_FILE = "ti_status_state.json" STATE_FILE = "ti_status_state.json"
# Apprise Konfiguration aus Umgebungsvariablen # Apprise Konfiguration aus Umgebungsvariablen
@@ -46,10 +58,6 @@ def get_apprise_urls():
return urls return urls
def is_debug_mode():
"""Prüft ob Debug-Modus aktiviert ist"""
return os.getenv('DEBUG_MODE', 'false').lower() == 'true'
def get_notification_level(): def get_notification_level():
"""Holt das konfigurierte Benachrichtigungslevel""" """Holt das konfigurierte Benachrichtigungslevel"""
return os.getenv('NOTIFICATION_LEVEL', 'all').lower() return os.getenv('NOTIFICATION_LEVEL', 'all').lower()
@@ -128,6 +136,7 @@ def fetch_status_messages():
print(f"API-Antwort erhalten. Anzahl Meldungen: {len(data.get('meldungen', []))}") print(f"API-Antwort erhalten. Anzahl Meldungen: {len(data.get('meldungen', []))}")
messages = [] messages = []
# 1. Bisherige Meldungen
for meldung in data.get("meldungen", []): for meldung in data.get("meldungen", []):
zeit = meldung.get("zeitpunkt", "") zeit = meldung.get("zeitpunkt", "")
titel = meldung.get("titel", "") titel = meldung.get("titel", "")
@@ -135,10 +144,30 @@ def fetch_status_messages():
link = meldung.get("link", "") link = meldung.get("link", "")
msg = f"{zeit}\n- {titel}: {beschreibung}\n{link}".strip() msg = f"{zeit}\n- {titel}: {beschreibung}\n{link}".strip()
messages.append(msg) messages.append(msg)
if is_debug_mode(): if is_debug_mode():
print(f"Verarbeite Meldung: {titel[:50]}...") print(f"Verarbeite Meldung: {titel[:50]}...")
# 2. Neue Auswertung von appStatus
app_status = data.get("appStatus", {})
for dienst, status in app_status.items():
outage = status.get("outage", "none")
if outage in ("partial", "full"):
has_maintenance = status.get("hasMaintenance", False)
sub_maintenance = status.get("hasSubComponentMaintenance", False)
affected = status.get("affectedFunctions", [])
# Baue eine verständliche Meldung mit Zeitstempel
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
msg_core = f"Störung bei {dienst.upper()} ({'Wartung' if has_maintenance else 'Störung'}): Status: {outage}"
if affected:
for func in affected:
func_name = func.get("function", "Unbekannte Funktion")
impact = func.get("impactDesc", "")
func_outage = func.get("outage", outage)
msg_core += f"\n- {func_name}: {impact} (Status: {func_outage})"
else:
msg_core += "\n- Keine weiteren Details."
messages.append(msg_core)
if is_debug_mode():
print(f"Erkannte Störung: {msg_core[:80]}...")
if is_debug_mode(): if is_debug_mode():
print(f"Insgesamt {len(messages)} Meldungen verarbeitet") print(f"Insgesamt {len(messages)} Meldungen verarbeitet")
return messages return messages
@@ -148,12 +177,21 @@ def load_state():
with open(STATE_FILE, "r", encoding="utf-8") as f: with open(STATE_FILE, "r", encoding="utf-8") as f:
return json.load(f) return json.load(f)
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
return {"messages": []} return {"messages": [], "last_status": {}}
def save_state(state): def save_state(state):
with open(STATE_FILE, "w", encoding="utf-8") as f: with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2) json.dump(state, f, ensure_ascii=False, indent=2)
def extract_outages(app_status):
"""Extrahiert alle aktuellen Störungen als Set von Strings"""
outages = set()
for dienst, status in app_status.items():
outage = status.get("outage", "none")
if outage in ("partial", "full"):
outages.add(dienst)
return outages
def markdownify_message(message): def markdownify_message(message):
# Datumsangaben fett hervorheben (z.B. 2025-06-23 oder 23.06.2025) # Datumsangaben fett hervorheben (z.B. 2025-06-23 oder 23.06.2025)
message = re.sub(r"(\d{4}-\d{2}-\d{2})", r"**\1**", message) message = re.sub(r"(\d{4}-\d{2}-\d{2})", r"**\1**", message)
@@ -175,7 +213,10 @@ def markdownify_message(message):
def send_notification(message): def send_notification(message):
"""Sendet Benachrichtigungen an alle konfigurierten Endpunkte""" """Sendet Benachrichtigungen an alle konfigurierten Endpunkte"""
# Debug-Ausgabe: Apprise-Version und URL aus .env
if is_debug_mode():
print(f"[DEBUG] Apprise-Version: {apprise.__version__}")
print(f"[DEBUG] APPRISE_URL_MATTERMOST aus .env: {os.getenv('APPRISE_URL_MATTERMOST')}")
# Prüfe ob Benachrichtigung gesendet werden soll # Prüfe ob Benachrichtigung gesendet werden soll
if not should_send_notification(message): if not should_send_notification(message):
if is_debug_mode(): if is_debug_mode():
@@ -199,23 +240,37 @@ def send_notification(message):
# Erstelle die Nachricht # Erstelle die Nachricht
title = "Neue TI-Status-Meldung" title = "Neue TI-Status-Meldung"
# Zeitstempel nur für die Anzeige, nicht für Duplikatserkennung
body = f"{md_message}\n\n[Zur Statusseite](https://fachportal.gematik.de/ti-status)\n_Gemeldet am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}_" body = f"{md_message}\n\n[Zur Statusseite](https://fachportal.gematik.de/ti-status)\n_Gemeldet am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}_"
# Setze den Absender-Namen
sender_name = "TI-Status Bot by medisoftware"
if is_debug_mode(): if is_debug_mode():
print(f"📤 Sende Benachrichtigung an {len(urls)} Endpunkt(e)") print(f"📤 Sende Benachrichtigung an {len(urls)} Endpunkt(e)")
print(f"Verwendete Apprise-URLs: {urls}")
print(f"Nachrichtentitel: {title}")
print(f"Nachrichtentext: {body[:200]}...")
# Sende die Nachricht # Sende die Nachricht mit Fehlerausgabe
try:
result = apobj.notify( result = apobj.notify(
title=title, title=title,
body=body, body=body,
body_format=apprise.NotifyFormat.MARKDOWN body_format=apprise.NotifyFormat.MARKDOWN
) )
if result: if result:
if is_debug_mode(): if is_debug_mode():
print("✅ Benachrichtigung erfolgreich gesendet") print("✅ Benachrichtigung erfolgreich gesendet")
else: else:
print("❌ Fehler beim Senden der Benachrichtigung") print("❌ Fehler beim Senden der Benachrichtigung")
if is_debug_mode():
print("[DEBUG] Apprise notify() Rückgabewert: False")
print("[DEBUG] Prüfe, ob die Webhook-URL korrekt ist, der Zielserver erreichbar ist und keine Authentifizierungsprobleme bestehen.")
except Exception as e:
print("❌ Fehler beim Senden der Benachrichtigung (Exception)")
if is_debug_mode():
print(f"[DEBUG] Exception: {e}")
# Verzögerung zwischen Benachrichtigungen # Verzögerung zwischen Benachrichtigungen
delay = get_notification_delay() delay = get_notification_delay()
@@ -224,7 +279,66 @@ def send_notification(message):
print(f"⏳ Warte {delay} Sekunden...") print(f"⏳ Warte {delay} Sekunden...")
time.sleep(delay) time.sleep(delay)
def send_statistics_notification():
"""Sendet Statistik-Bericht über Apprise"""
print("📊 Generiere und sende Statistik-Bericht...")
# Erstelle Statistik-Objekt
stats = OutageStatistics()
# Generiere Bericht
report = stats.generate_statistics_report()
# Hole alle konfigurierten URLs
urls = get_apprise_urls()
if not urls:
print("❌ Keine Apprise URLs konfiguriert!")
return
# Erstelle Apprise Objekt
apobj = apprise.Apprise()
# Füge alle URLs hinzu
for url in urls:
apobj.add(url)
# Erstelle die Nachricht
title = "📊 TI-Status Ausfall-Statistiken"
body = report
if is_debug_mode():
print(f"📤 Sende Statistik-Bericht an {len(urls)} Endpunkt(e)")
print(f"Bericht-Länge: {len(report)} Zeichen")
# Sende die Nachricht
try:
result = apobj.notify(
title=title,
body=body,
body_format=apprise.NotifyFormat.MARKDOWN
)
if result:
print("✅ Statistik-Bericht erfolgreich gesendet")
else:
print("❌ Fehler beim Senden des Statistik-Berichts")
except Exception as e:
print(f"❌ Fehler beim Senden des Statistik-Berichts: {e}")
def main(): def main():
# Parse Kommandozeilen-Argumente
parser = argparse.ArgumentParser(description="TI-Status Checker mit Statistik-Funktionalität")
parser.add_argument("--stats", "-s", action="store_true",
help="Generiert und sendet Statistik-Bericht über Apprise")
parser.add_argument("--debug", "-d", action="store_true",
help="Aktiviert Debug-Modus")
args = parser.parse_args()
# Wenn --stats gesetzt ist, sende nur Statistiken
if args.stats:
send_statistics_notification()
return
# Prüfe Konfiguration # Prüfe Konfiguration
urls = get_apprise_urls() urls = get_apprise_urls()
if not urls: if not urls:
@@ -232,7 +346,7 @@ def main():
print("Bitte erstelle eine .env Datei basierend auf env.example") print("Bitte erstelle eine .env Datei basierend auf env.example")
return return
if is_debug_mode(): if is_debug_mode() or args.debug:
print("🔧 Debug-Modus aktiviert") print("🔧 Debug-Modus aktiviert")
print(f"📋 Benachrichtigungslevel: {get_notification_level()}") print(f"📋 Benachrichtigungslevel: {get_notification_level()}")
print(f"🔍 Filter: {get_notification_filters()}") print(f"🔍 Filter: {get_notification_filters()}")
@@ -241,23 +355,54 @@ def main():
state = load_state() state = load_state()
known_messages = set(state.get("messages", [])) known_messages = set(state.get("messages", []))
last_status = state.get("last_status", {})
print("Prüfe TI-Status-API auf neue Meldungen...") print("Prüfe TI-Status-API auf neue Meldungen...")
# Erstelle Statistik-Objekt für die Aufzeichnung von Störungen
stats = OutageStatistics()
try: try:
messages = fetch_status_messages() messages = fetch_status_messages()
new_messages = [m for m in messages if m not in known_messages] # Extrahiere aktuelle und vorherige Störungen
resp = requests.get(TI_API_URL)
resp.raise_for_status()
data = resp.json()
app_status = data.get("appStatus", {})
current_outages = extract_outages(app_status)
previous_outages = extract_outages(last_status.get("appStatus", {})) if last_status else set()
# Neue Störungen aufzeichnen
new_outages = current_outages - previous_outages
for dienst in new_outages:
# Bestimme den Störungstyp
outage_type = "full" if app_status.get(dienst, {}).get("outage") == "full" else "partial"
stats.record_outage_start(dienst, outage_type)
if is_debug_mode() or args.debug:
print(f"📊 Neue Störung aufgezeichnet: {dienst} ({outage_type})")
# Entwarnungen erkennen und aufzeichnen
resolved = previous_outages - current_outages
for dienst in resolved:
stats.record_outage_end(dienst)
msg = f"✅ Entwarnung: Die Störung bei {dienst.upper()} wurde behoben."
print(msg)
send_notification(msg)
known_messages.add(msg)
if is_debug_mode() or args.debug:
print(f"📊 Störung beendet aufgezeichnet: {dienst}")
# Normale neue Meldungen
new_messages = [m for m in messages if m not in known_messages]
for msg in new_messages: for msg in new_messages:
print(f"Neue Meldung gefunden: {msg[:100]}...") print(f"Neue Meldung gefunden: {msg[:100]}...")
send_notification(msg) send_notification(msg)
known_messages.add(msg) known_messages.add(msg)
if new_messages: if new_messages or resolved:
save_state({"messages": list(known_messages)}) save_state({"messages": list(known_messages), "last_status": data})
print(f"{len(new_messages)} neue Meldung(en) verarbeitet") print(f"{len(new_messages)} neue Meldung(en) und {len(resolved)} Entwarnung(en) verarbeitet")
else: else:
print(f"Keine neuen Meldungen ({datetime.now().strftime('%H:%M:%S')})") print(f"Keine neuen Meldungen ({datetime.now().strftime('%H:%M:%S')})")
except Exception as e: except Exception as e:
print(f"Fehler: {e}") print(f"Fehler: {e}")