Schritt-fuer-Schritt Anleitung zur Konfiguration des Betriebsmonitoring-Dashboards
Version 1.0 | Stand: 2026-02-27 | Klassifikation: INTERN
Das Betriebsmonitoring-Dashboard bietet eine CheckMK-aehnliche Uebersicht ueber:
cribl-ops. Kibana visualisiert diese Daten.
Dashboards:
| Dashboard | ID | Beschreibung |
|---|---|---|
| Navigation | navigation-menu | Hauptmenue mit Links |
| Betriebsmonitoring | ops-monitoring-v1 | System- & Dienste-Uebersicht (25 Panels) |
| Docker Log Classification | icedataemphasise-docker-log-dashboard | Log-Klassifikation |
| Komponente | Version | Container | Port |
|---|---|---|---|
| Elasticsearch | 8.12.2 | cribl-elasticsearch | 9200 |
| Kibana | 8.12.2 | cribl-kibana | 5601 |
| Cribl Stream | - | via Docker Compose | - |
| MinIO | - | cribl-minio | - |
Alle Container laufen im selben Docker-Netzwerk und kommunizieren intern.
Der Index cribl-ops enthaelt zwei Hauptdatenquellen:
Empfangen via Syslog, verarbeitet durch die Cribl Pipeline pipeline_sophos_fw.
| Feld | Typ | Beschreibung | Beispielwert |
|---|---|---|---|
log_type | keyword | Hauptkategorie | Firewall, System Health, Event, Content Filtering |
log_component | keyword | Unterkategorie | Firewall Rule, CPU, Memory, Disk, Interface, DHCP Server |
log_subtype | keyword | Aktion | Allowed, Denied, Usage |
action | keyword | Normalisierte Aktion | Allowed, Denied, Drop |
src / dest | keyword | Quell-/Ziel-IP (normalisiert) | 10.10.0.111, 192.168.178.1 |
protocol | keyword | Netzwerkprotokoll | TCP, UDP, ICMP |
fw_rule_name | keyword | Name der FW-Regel | General out from all LAN |
device_name | keyword | Geraetename | Schwalbe.paulis.net |
source_type | keyword | Cribl Source Type | sophos_firewall |
| Komponente | Felder (Originaltyp: text) | Beispiel |
|---|---|---|
| CPU | system, user, idle | 2.33%, 4.14%, 93.52% |
| Memory | total_memory, free, used, unit | 16779149312, 11381645312, byte |
| Disk | Configuration, Reports, Signature, Temp | 9.00%, 18.00%, 6.00% |
| Interface | interface, receivedkbits, transmittedkbits | Port2, 562.49, 137.11 |
Empfangen via Cribl Edge (Journald-Quelle), klassifiziert durch ollama_classifier Pipeline.
| Feld | Typ | Beschreibung |
|---|---|---|
CONTAINER_NAME | keyword | Docker Container Name |
IMAGE_NAME | keyword | Docker Image |
appname | keyword | Applikationsname (journald) |
classification | keyword | siem / operational |
classification_method | keyword | rule_based / ollama |
severityName | keyword | info, notice, error, warning |
host.name | keyword | Hostname (NUC-HA fuer Docker-Logs) |
| Feld | Beschreibung |
|---|---|
@timestamp | Zeitstempel (date) - Grundlage aller Zeitreihen |
severity | Syslog Severity (numerisch: 0-7) |
severityName | Severity als Text: info, notice, warning, error |
cribl_pipe | Array der durchlaufenen Cribl Pipelines |
message | Volltextmeldung |
_raw | Original-Rohdaten |
Da Sophos System-Health-Werte als Text gespeichert werden, muessen Runtime Fields angelegt werden, die zur Abfragezeit die Werte in numerische Typen konvertieren.
1Runtime Fields via Elasticsearch API anlegen
Folgender API-Call fuegt alle benoetigten Runtime Fields zum Index cribl-ops hinzu:
curl -X PUT "http://10.10.0.100:9200/cribl-ops/_mapping" \
-H 'Content-Type: application/json' \
-d '{
"runtime": {
"cpu_idle_pct": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"idle.keyword\") && doc[\"idle.keyword\"].size() > 0) { String v = doc[\"idle.keyword\"].value; if (v.endsWith(\"%\")) { v = v.substring(0, v.length()-1); } try { emit(Double.parseDouble(v)); } catch(Exception e) {} }"
}
},
"cpu_user_pct": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"user.keyword\") && doc[\"user.keyword\"].size() > 0) { String v = doc[\"user.keyword\"].value; if (v.endsWith(\"%\")) { v = v.substring(0, v.length()-1); } try { emit(Double.parseDouble(v)); } catch(Exception e) {} }"
}
},
"cpu_system_pct": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"system.keyword\") && doc[\"system.keyword\"].size() > 0) { String v = doc[\"system.keyword\"].value; if (v.endsWith(\"%\")) { v = v.substring(0, v.length()-1); } try { emit(Double.parseDouble(v)); } catch(Exception e) {} }"
}
},
"mem_total_bytes": {
"type": "long",
"script": {
"source": "if (doc.containsKey(\"total_memory.keyword\") && doc[\"total_memory.keyword\"].size() > 0) { try { emit(Long.parseLong(doc[\"total_memory.keyword\"].value)); } catch(Exception e) {} }"
}
},
"mem_used_bytes": {
"type": "long",
"script": {
"source": "if (doc.containsKey(\"used.keyword\") && doc[\"used.keyword\"].size() > 0) { try { emit(Long.parseLong(doc[\"used.keyword\"].value)); } catch(Exception e) {} }"
}
},
"mem_free_bytes": {
"type": "long",
"script": {
"source": "if (doc.containsKey(\"free.keyword\") && doc[\"free.keyword\"].size() > 0) { try { emit(Long.parseLong(doc[\"free.keyword\"].value)); } catch(Exception e) {} }"
}
},
"net_rx_kbits": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"receivedkbits.keyword\") && doc[\"receivedkbits.keyword\"].size() > 0) { try { emit(Double.parseDouble(doc[\"receivedkbits.keyword\"].value)); } catch(Exception e) {} }"
}
},
"net_tx_kbits": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"transmittedkbits.keyword\") && doc[\"transmittedkbits.keyword\"].size() > 0) { try { emit(Double.parseDouble(doc[\"transmittedkbits.keyword\"].value)); } catch(Exception e) {} }"
}
},
"disk_config_pct": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"Configuration.keyword\") && doc[\"Configuration.keyword\"].size() > 0) { String v = doc[\"Configuration.keyword\"].value; if (v.endsWith(\"%\")) { v = v.substring(0, v.length()-1); } try { emit(Double.parseDouble(v)); } catch(Exception e) {} }"
}
},
"disk_reports_pct": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"Reports.keyword\") && doc[\"Reports.keyword\"].size() > 0) { String v = doc[\"Reports.keyword\"].value; if (v.endsWith(\"%\")) { v = v.substring(0, v.length()-1); } try { emit(Double.parseDouble(v)); } catch(Exception e) {} }"
}
}
}
}'
Die Antwort sollte {"acknowledged": true} sein.
| Runtime Field | Typ | Quellfeld | Beschreibung |
|---|---|---|---|
cpu_user_pct | double | user | CPU User % (ohne %-Zeichen) |
cpu_system_pct | double | system | CPU System % |
cpu_idle_pct | double | idle | CPU Idle % |
mem_total_bytes | long | total_memory | Gesamter RAM in Bytes |
mem_used_bytes | long | used | Belegter RAM in Bytes |
mem_free_bytes | long | free | Freier RAM in Bytes |
net_rx_kbits | double | receivedkbits | Empfangene kbit/s |
net_tx_kbits | double | transmittedkbits | Gesendete kbit/s |
disk_config_pct | double | Configuration | Disk Config % |
disk_reports_pct | double | Reports | Disk Reports % |
2Index Pattern pruefen/erstellen
Das Index Pattern cribl-ops* sollte bereits existieren (ID: 99f5381a-3d59-4cec-8ce4-77ee8a90595d).
Pruefen via API:
curl -s 'http://10.10.0.100:5601/api/saved_objects/_find?type=index-pattern' \
-H 'kbn-xsrf: true' | python3 -m json.tool
Falls nicht vorhanden, erstellen:
curl -X POST "http://10.10.0.100:5601/api/saved_objects/index-pattern/99f5381a-3d59-4cec-8ce4-77ee8a90595d" \
-H "kbn-xsrf: true" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"title": "cribl-ops*",
"timeFieldName": "@timestamp",
"name": "Cribl Operations Logs"
}
}'
3Index Pattern aktualisieren (Felder refreshen)
Nach Anlage der Runtime Fields muessen die Felder im Index Pattern aktualisiert werden:
Cribl Operations Logs klickencpu_user_pct, etc.) sollten nun in der Feldliste erscheinenDas Dashboard besteht aus 25 Panels in 5 Sektionen. Jeder Panel-Typ wird im Folgenden erklaert, damit eigene Panels erstellt oder bestehende angepasst werden koennen.
Grosse Zahlenwerte als Uebersicht (z.B. "Events Gesamt", "Errors", "FW Denied").
Manuell in Kibana erstellen:
severityName.keyword: error fuer Error-Zaehler)API-Struktur (Lens Metric):
{
"visualizationType": "lnsMetric",
"state": {
"datasourceStates": {
"formBased": {
"layers": {
"l1": {
"columns": {
"c1": {
"label": "Errors", // Angezeigter Name
"operationType": "count", // Aggregation: count, sum, avg, min, max
"sourceField": "___records___"
}
}
}
}
}
},
"visualization": {
"metricAccessor": "c1",
"color": "#E7664C" // Farbe als Hex
},
"filters": [{ // Optional: Vorfilterung
"query": { "match_phrase": { "severityName.keyword": "error" }}
}]
}
}
Fuer CPU, Memory, Netzwerk-Durchsatz und andere zeitbasierte Metriken.
Manuell erstellen:
@timestamp (Date Histogram, Auto-Interval)cpu_user_pct (Runtime Field)log_component.keyword: CPUWichtige Einstellungen:
| Einstellung | Wert | Zweck |
|---|---|---|
seriesType | area_stacked / line / bar_stacked | Diagrammtyp |
fittingFunction | Linear / None | Luecken interpolieren |
yLeftExtent | {lowerBound: 0, upperBound: 100} | Y-Achse fixieren |
splitAccessor | Feld-ID | Nach Kategorie aufteilen (z.B. Severity) |
colorMapping | Assignments | Feste Farben pro Wert zuweisen |
Beispiel: CPU-Chart mit fixierten Farben
// colorMapping-Beispiel: Feste Farben pro Serie
"colorMapping": {
"assignments": [
{
"rule": {"type": "matchExactly", "values": ["CPU User %"]},
"color": {"type": "colorCode", "colorCode": "#6092C0"}
},
{
"rule": {"type": "matchExactly", "values": ["CPU System %"]},
"color": {"type": "colorCode", "colorCode": "#E7664C"}
}
],
"specialAssignments": [
{"rule": {"type": "other"}, "color": {"type": "loop"}}
]
}
Horizontale Balken fuer "Top Denied Sources", "Container Errors", "Top Hosts".
Manuell erstellen:
src.keyword (Top 15)action.keyword: DeniedAPI-Schluesselfelder:
// Terms-Spalte fuer Top-N
{
"label": "Source IP",
"operationType": "terms",
"sourceField": "src.keyword",
"params": {
"size": 15, // Anzahl Top-Eintraege
"orderDirection": "desc", // Absteigend sortiert
"exclude": ["-"], // Werte ausschliessen
"include": [] // Oder nur bestimmte einschliessen
}
}
Fuer Verteilungen (Protokolle, Log-Komponenten).
Manuell erstellen:
protocol.keywordAPI:
"visualizationType": "lnsPie",
"visualization": {
"shape": "donut", // oder "pie" fuer Vollkreis
"layers": [{
"primaryGroups": ["proto"], // Slice-Feld
"metrics": ["cnt"], // Groesse
"numberDisplay": "percent" // Prozent anzeigen
}]
}
Ideal fuer Container × Severity Matrix.
Manuell erstellen:
severityName.keywordCONTAINER_NAME.keywordAPI:
"visualizationType": "lnsHeatmap",
"visualization": {
"xAccessor": "sev", // Severity (X)
"yAccessor": "cname", // Container (Y)
"valueAccessor": "cnt", // Wert (Farbe)
"palette": {"type": "palette", "name": "status"},
"gridConfig": {"isCellLabelVisible": true}
}
Fuer detaillierte Auflistungen (Event-Typen, Firewall Rules).
Manuell erstellen:
log_type.keyword, dann log_component.keywordAPI:
"visualizationType": "lnsDatatable",
"visualization": {
"columns": [
{"columnId": "lt", "isTransposed": false},
{"columnId": "cnt", "isTransposed": false}
],
"paging": {"size": 10, "enabled": true}
}
Dashboards koennen ueber die Kibana Saved Objects API erstellt werden:
4Dashboard erstellen via POST
curl -X POST "http://10.10.0.100:5601/api/saved_objects/dashboard/DASHBOARD-ID" \
-H "kbn-xsrf: true" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"title": "Mein Dashboard",
"description": "Beschreibung",
"timeRestore": true,
"timeTo": "now",
"timeFrom": "now-24h",
"refreshInterval": {"pause": false, "value": 60000},
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
},
"panelsJSON": "[...]", // JSON-String mit Panel-Array
"optionsJSON": "{\"useMargins\":true,\"syncColors\":true}",
"version": 1
}
}'
5Bestehendes Dashboard exportieren (als Vorlage)
# Export eines bestehenden Dashboards
curl -s "http://10.10.0.100:5601/api/saved_objects/dashboard/ops-monitoring-v1" \
-H "kbn-xsrf: true" | python3 -m json.tool > dashboard-export.json
# PanelsJSON extrahieren und formatiert anzeigen
cat dashboard-export.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
panels = json.loads(data['attributes']['panelsJSON'])
print(json.dumps(panels, indent=2))
" > panels-formatted.json
6Dashboard aktualisieren
Zum Aktualisieren eines bestehenden Dashboards DELETE + POST verwenden:
# Loeschen
curl -X DELETE "http://10.10.0.100:5601/api/saved_objects/dashboard/ops-monitoring-v1" \
-H "kbn-xsrf: true"
# Neu erstellen mit geaenderten Panels
curl -X POST "http://10.10.0.100:5601/api/saved_objects/dashboard/ops-monitoring-v1" \
-H "kbn-xsrf: true" \
-H "Content-Type: application/json" \
-d @updated-dashboard.json
Ein spezielles Dashboard dient als Hauptmenue mit Links zu allen anderen Dashboards:
navigation-menuhttp://10.10.0.100:5601/app/dashboards#/view/DASHBOARD-IDMarkdown-Format fuer Dashboard-Links:
| Dashboard | Beschreibung | Link |
|-----------|-------------|------|
| **Betriebsmonitoring** | System Health & Dienste | [Oeffnen](http://10.10.0.100:5601/app/dashboards#/view/ops-monitoring-v1) |
| **Docker Logs** | Log-Klassifikation | [Oeffnen](http://10.10.0.100:5601/app/dashboards#/view/icedataemphasise-docker-log-dashboard) |
Cribl Operations Logs waehlenJedes Panel kann eigene Filter haben. In der API-Struktur unter state.filters:
// Einfacher Term-Filter
{"query": {"match_phrase": {"log_component.keyword": "CPU"}}}
// Exists-Filter (Feld muss vorhanden sein)
{"query": {"exists": {"field": "CONTAINER_NAME"}}}
// Mehrere Werte (should/or)
{"query": {"bool": {"should": [
{"match_phrase": {"action.keyword": "Denied"}},
{"match_phrase": {"action.keyword": "Drop"}}
]}}}
Fuer weitere numerische Felder, die als Text gespeichert sind:
curl -X PUT "http://10.10.0.100:9200/cribl-ops/_mapping" \
-H 'Content-Type: application/json' \
-d '{
"runtime": {
"mein_neues_feld": {
"type": "double",
"script": {
"source": "if (doc.containsKey(\"quellfeld.keyword\") && doc[\"quellfeld.keyword\"].size() > 0) { try { emit(Double.parseDouble(doc[\"quellfeld.keyword\"].value)); } catch(Exception e) {} }"
}
}
}
}'
Danach in Kibana unter Data Views die Felder refreshen.
Die Farbzuweisungen in colorMapping verwenden Hex-Codes:
| Farbe | Code | Verwendung |
|---|---|---|
| Rot | #E7664C | Error, Denied, kritisch |
| Gelb | #D6BF57 | Warning |
| Gruen | #54B399 | OK, Allowed, Info |
| Blau | #6092C0 | Neutral, Notice |
| Pink | #D36086 | Drop, kritisch |
| Gold | #B9A888 | SIEM, speziell |
| Zweck | Filter |
|---|---|
| Nur Firewall-Logs | log_type.keyword: Firewall |
| Nur System Health | log_type.keyword: System Health |
| Nur CPU-Daten | log_component.keyword: CPU |
| Nur Memory-Daten | log_component.keyword: Memory |
| Nur Disk-Daten | log_component.keyword: Disk |
| Nur Interface-Daten | log_component.keyword: Interface |
| Nur Denied Traffic | action.keyword: Denied |
| Nur Docker-Container-Logs | CONTAINER_NAME exists |
| Nur Errors | severityName.keyword: error |
| Nur SIEM-klassifizierte Logs | classification.keyword: siem |
| Bestimmter Container | CONTAINER_NAME.keyword: homeassistant |
| Container | Kategorie | Docs (ca.) |
|---|---|---|
| homeassistant | Smart Home | 2.1M |
| oa-tika | Open Archiver | 153K |
| oa-postgres | Open Archiver DB | 13K |
| tax-ai-stack-paperless-1 | Steuer-Pipeline | 3.5K |
| hassio_supervisor | HA Supervisor | 2.9K |
| tax-ai-stack-db-1 | Steuer-Pipeline DB | 1.2K |
| addon_a0d7b954_nodered | Node-RED | 387 |
| cribl-kibana | Monitoring Stack | 129 |
| addon_core_matter_server | Matter/Thread | 107 |
| addon_core_mosquitto | MQTT Broker | 56 |