PHP mit Loki und Grafana

Individuelle Informationen senden

Kommentieren May 15 2024 .txt, .json, .md

Grafana kann bunt und daher ist es hip. Es kann Metriken für PHP mit Hilfe von OpenTelemetry.

Geht man von einer “normalen” PHP Anwendung aus (Webanwendung), gibt es keinen Endpunkt und auch die Verwendung von OpenTelemetry ist nicht mal so machbar. Für eine Webanwendung reicht ein Webspace zum Betrieb und da hat man keine weiteren Rechte und Möglichkeiten individuelle Software zu installieren.

Eine Lösung ist Grafana und Loki als Cloud Lösung zu verwenden. Dabei muss man keinen eigenen Server verwalten.

Da ich auf Server zurückgreifen kann, betreibe ich diese beiden Produkte selbst.

Die Funktionsweise ist das aktive Senden der Events direkt aus der Anwendung heraus.

Die wichtigsten Informationen über die zu sendenden Daten und deren Aufbau gibt es hier.

Ein Fallstrick ist die Formatierung:

When using /api/v1/push, you must send the timestamp as a string and not a number, otherwise the endpoint will return a 400 error.

Dies betrifft alle Nummern. Ist in der Doku so leider nicht ersichtlich.

Grafana

In der config für Grafana muss nichts spezielles angepasst werden.

Loki

Zusätzlich zur normalen Konfiguration muss Structured metadata aktiviert werden. Dies ist per default nicht der Fall.

limits_config:
  allow_structured_metadata: true

Es geht auch ohne. Dann muss man in Grafana die Daten über regex/json auseinander nehmen. Is nich so dolle.

PHP

Der Ablauf innerhalb einer PHP Anwendung ist das Sammeln der Events um diese dann am Ende gesammelt zu versenden. Somit ist besser sichergestellt, dass es keine Verzögerungen innerhalb der Anwendung gibt. Asynchrones abarbeiten oder sogar parallel lasse ich erstmal außen vor.

Hier mal ein wenig Beispielcode:

$Loki = new Loki("localhost", 3000, array("streamlabel1" => "value1", "streamlabel2" => "value2"));
$Loki->log("log line text", array("structuredData" => "value"));
$Loki->log("log line text", array("structuredData" => "value2"));
$Loki->send();

Hier die log() Methode:

public function log(string $msg, array $structuredData=array()): void {
    $_nanosec = strval(shell_exec("date +%s%9N")-1);
    if(!empty($structuredData)) {
        $this->_values[] = array($_nanosec, $msg, $structuredData);
    } else {
        $this->_values[] = array($_nanosec, $msg);
    }
}

Hierbei ist der Zeitstempel wichtig. Leider ist es aktuell in PHP nicht möglich sich den aktuellen Zeitstempel in Nanosekunden ausgeben zu lassen. Daher der Umweg über den Aufruf des date Befehls (Achtung ist Linux spezifisch!)

Die send() Methode:

public function send(): string {
    $data = array(
        "streams" => array(
            array(
                "stream" => $this->_stream,
                "values" => $this->_values
            )
        )
    );
    
    $data = json_encode($data);
    $out = "POST ".LOKI_PUSH_API." HTTP/1.1\r\n";
    $out .= "Host: $this->_host\r\n";
    $out .= "Content-Type: application/json\r\n";
    $out .= "Content-Length: ".strlen($data)."\r\n";
    $out .= "Connection: Close\r\n\r\n";
    
    $fp = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
    if($fp) {
        fwrite($fp, $out);
        fwrite($fp, $data."\r\n");
        fclose($fp);
    } else {
        $ret = $errno.' '.$errstr;
    }
    $this->_values = array();
}

Ein vollständiges Codebeispiel ist hier zu finden