# Apache: Access-Logs → Loki (roh) UND Request-Metriken → Prometheus # # Zwei Wege aus einem Log — sie ergaenzen sich: # inputs.tail : jede Zeile roh nach Loki (fuer die Fehlersuche im Einzelfall) # inputs.logparser: dieselben Zeilen als Metriken (Request-Rate, Status-Klassen, # Antwortzeit) — das ist, was Panels und Alerts brauchen # # Die Logs landen NICHT in Prometheus: der Remote-Write-Output hat # `namedrop = ["docker_log", "journal"]` — hier kommt `apache_log` dazu. # # FALLE: `from_beginning` existiert bei inputs.tail NICHT (weder im Docker-Image # noch im InfluxData-Paket 1.40). Ein solcher Block laesst den Dienst beim Start # hart scheitern: "plugin inputs.tail: configuration specified the fields # [from_beginning]" → systemctl start schlaegt fehl, ALLE Metriken aus. # Stattdessen: initial_read_offset = "saved-or-end" (Default), damit beim ersten # Lauf nicht die komplette Historie ausgeschuettet wird. # -------------------------------------------------------------------------- # 1) Roh-Logs → Loki # -------------------------------------------------------------------------- # ${APACHE_FILES} rendert zu einer echten TOML-Liste — der Installer baut sie # aus allen gefundenen Logpfaden (Panel-Hoster haben pro VHost eigene Dateien). # Ein einzelner String mit Leerzeichen ("/a.log /b.log") waere ein ungueltiger # Pfad und der Input liefe still leer. [[inputs.tail]] files = ${APACHE_FILES} name_override = "journal" # Beim Erstanlauf nur Neues lesen; danach Position merken. initial_read_offset = "saved-or-end" data_format = "value" data_type = "string" # Vereinheitlicht den Feldnamen auf `message` — das Logs-Panel in Grafana # erwartet es; `value` wuerde als value="…" erscheinen. path_tag = "logfile" [inputs.tail.tags] source = "apache" [[processors.rename]] namepass = ["journal"] order = 1 [[processors.rename.replace]] field = "value" dest = "message" # -------------------------------------------------------------------------- # 2) Request-Metriken (Common/Combined Log Format) → Prometheus # -------------------------------------------------------------------------- # Common Log Format: # 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 # VHost-Apache (Debian-Default) stellt den Hostnamen voran: # example.com:80 127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET / HTTP/1.0" 200 2326 [[inputs.tail]] files = ${APACHE_FILES} name_override = "apache_access" initial_read_offset = "saved-or-end" data_format = "grok" # Korrekte Namen der Telegraf-Grok-Sammlung: COMMON_LOG_FORMAT und # COMBINED_LOG_FORMAT — NICHT HTTPD_* (das sind andere Muster, Telegraf kennt # sie nicht: "no pattern found for %{HTTPD_COMBINED_LOG_FORMAT}"). # Reihenfolge: spezifischste zuerst — VHost-Varianten vor der einfachen Form. # EIGENES MUSTER statt %{COMBINED_LOG_FORMAT} — wegen eines realen Ausfalls: # Die Log-Zeile hat nur SEKUNDEN-Aufloesung. Telegraf uebernimmt einen # erkannten Zeitstempel als Sample-Zeit; bei mehreren Treffern pro Sekunde # vergibt es +1 ms Aufschlag. Bei hoher Last (>1000 Zeilen/s) laeuft dieser # Aufschlag in die naechste Sekunde und kollidiert mit echten Zeilen: # "duplicate sample for timestamp ...; overrides not allowed" # Gemessen an echten x22-Logs: 2000 Zeilen -> 286 Kollisionen. # Prometheus weist den Batch ab, Telegraf wiederholt ihn endlos — der Node # liefert dann GAR NICHTS mehr (auch keine Systemmetriken). # # Der Fix: das Zeitfeld heisst `log_time` (kein `ts`), darum nutzt Telegraf # seine EIGENE Uhr mit Nanosekunden-Aufloesung. Die Log-Zeit bleibt als Feld # erhalten, der Zeitstempel ist eindeutig. grok_custom_patterns = ''' APACHE_LINE (?:%{HOSTNAME:vh}(?::%{NUMBER:vport})? )?%{IPORHOST:client_ip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:log_time}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:http_version})?|-)" %{NUMBER:resp_code:int} (?:%{NUMBER:resp_bytes:int}|-)(?: "%{DATA:referrer}" "%{DATA:agent}")? ''' grok_patterns = ["%{APACHE_LINE}"] grok_timezone = "UTC" # Kein Tag-Wildwuchs: nur das Noetige als Tag, der Rest als Feld. [inputs.tail.tags] source = "apache" # resp_code und verb waren im eingebauten Muster TAGS. Das eigene Muster # liefert sie als Felder — ohne diesen Schritt haetten die Grafana-Panels, # die nach resp_code filtern, plotzlich keine Daten mehr. [[processors.converter]] namepass = ["apache_access"] order = 1 [processors.converter.fields] tag = ["resp_code", "verb"] # -------------------------------------------------------------------------- # 3) mod_status (optional, auskommentiert) # -------------------------------------------------------------------------- # Liefert Auslastung (BusyWorkers, ReqPerSec) — nuetzlich, braucht aber # `a2enmod status`, `ExtendedStatus On` und eine lokale Freigabe von # /server-status in der Apache-Config. # # [[inputs.apache]] # urls = ["http://127.0.0.1/server-status?auto"]