graftak/monolog-loki

Monolog handler for Grafana Loki (BC fork!)

1.4.0 2025-08-05 14:00 UTC

This package is auto-updated.

Last update: 2025-08-05 14:16:43 UTC


README

! ! ! PLEASE NOTE ! ! !

This is a fork from itspire/monolog-loki for older projects still using PHP >=7.4 and Monolog 2, but with the bugfixes/improvements since version 2.0.0 applied to it. Please use https://github.com/itspire/monolog-loki or use this with care!

This library follows the PSR-12 convention.

Usage

Recommended Usage

Since Loki log handling uses a remote server, logging is prone to be subject of timeout, network shortage and so on. To avoid your application being broken in such a case, we recommend wrapping the handler in a WhatFailureGroupHandler

Native

use Itspire\MonologLoki\Handler\LokiHandler;
use Monolog\Handler\WhatFailureGroupHandler;

$handler = new WhatFailureGroupHandler(
    [
        new LokiHandler(
            [
                'entrypoint' => 'https://loki:3100',
                'context' => [
                    // Set here your globally applicable context variables
                ],
                'labels' => [
                    // Set here your globally applicable labels
                ],
                'client_name' => 'your_host_name', // Here set a unique identifier for the client host
                // Optional: Sets tenant id (HTTP header X-Scope-OrgID), if null or missing -> no header
                'tenant_id' => 'some-tenant',
                // Optional: if you're using basic auth to authentify
                'auth' => [
                    'basic' => ['user', 'password'],
                ],
                // Optional: Override the default curl options with custom values
                'curl_options' => [
                    CURLOPT_CONNECTTIMEOUT_MS => 500,
                    CURLOPT_TIMEOUT_MS => 600
                ]
            ]
        )
    ]
);

Non-customizable curl options

The following options are not customizable in the configuration:

  • CURLOPT_CUSTOMREQUEST
  • CURLOPT_RETURNTRANSFER
  • CURLOPT_POSTFIELDS
  • CURLOPT_HTTPHEADER

Symfony App

Configure LokiHandler Service

  Itspire\MonologLoki\Handler\LokiHandler:
    arguments:
      $apiConfig:
        entrypoint: 'http://loki:3100'
        context:
          app: My-app
        labels:
          env: '%env(APP_ENV)%'
        client_name: my_app_server
        auth:
          basic:
            user: username
            password: password
        curl_options:
          !php/const CURLOPT_CONNECTTIMEOUT_MS: 500,
          !php/const CURLOPT_TIMEOUT_MS: 600

Note: We're currently working on a possible bundle-based implementation for Symfony but at the moment, this is the way.

Configure Monolog to use Loki Handler

monolog:
  handlers:
    loki:
      type: service
      id: Itspire\MonologLoki\Handler\LokiHandler

    my_loki_handler:
      type:   whatfailuregroup
      members: [loki]
      level: debug
      process_psr_3_messages: true # optional but we find it rather useful (Note: native handler required to use)

Laravel App

Add Loki to config/logging.php

'loki' => [
    'driver'         => 'monolog',
    'level'          => env('LOG_LEVEL', 'debug'),
    'handler'        => \Itspire\MonologLoki\Handler\LokiHandler::class,
    'formatter'      => \Itspire\MonologLoki\Formatter\LokiFormatter::class,
    'formatter_with' => [
        'labels' => [],
        'context' => [],
        'systemName' => env('LOKI_SYSTEM_NAME', null),
        'extraPrefix' => env('LOKI_EXTRA_PREFIX', ''),
        'contextPrefix' => env('LOKI_CONTEXT_PREFIX', '')
    ],
    'handler_with'   => [
        'apiConfig'  => [
            'entrypoint'  => env('LOKI_ENTRYPOINT', "http://localhost:3100"),
            'context'     => [],
            'labels'      => [],
            'client_name' => '',
            'auth' => [
                'basic' => [
                    env('LOKI_AUTH_BASIC_USER', ''), 
                    env('LOKI_AUTH_BASIC_PASSWORD', '')
                ],
            ],
        ],
    ],
],

Set env vars

LOKI_ENTRYPOINT="http://loki:3100"
LOKI_AUTH_BASIC_USER=
LOKI_AUTH_BASIC_PASSWORD=
LOKI_SYSTEM_NAME=null
LOKI_CONTEXT_PREFIX="context_"
LOKI_EXTRA_PREFIX=

These vars can be injected by Kubernetes, Docker or simply by setting them on the .env file

Laravel with WhatFailureGroupHandler

Since Loki log handling uses a remote server, logging is prone to be subject of timeout, network shortage and so on. To avoid your application being broken in such a case, we recommend wrapping the handler in a WhatFailureGroupHandler.

Create a custom Log handler and wrap the LokiHandler with a WhatFailureGroupHandler.

namespace App\Logging;

use Itspire\MonologLoki\Formatter\LokiFormatter;
use Itspire\MonologLoki\Handler\LokiHandler;
use Monolog\Handler\WhatFailureGroupHandler;
use Monolog\Logger;

class LokiNoFailureHandler
{
    public function __invoke(array $config)
    {
        return new Logger('loki-no-failure', [
            new WhatFailureGroupHandler([
                (new LokiHandler($config['handler_with']['apiConfig'], $config['level']))
                    ->setFormatter(new LokiFormatter(...array_values($config['formatter_with'])))
            ])
        ]);
    }
}

Update the config accordingly:

parse_str(env('LOKI_LABELS', ''), $loki_formatter_labels);

'loki' => [
    'driver'    => 'custom',
    'level'     => env('LOG_LEVEL', 'debug'),
    'via'       => \App\Logging\LokiNoFailureHandler::class,
    'formatter_with' => [
        // LOKI_LABELS: app=laravel&env=prod
        'labels' => $loki_formatter_labels,
        'context' => [],
        'systemName' => env('LOKI_SYSTEM_NAME', ''),
        'extraPrefix' => env('LOKI_EXTRA_PREFIX', ''),
        'contextPrefix' => env('LOKI_CONTEXT_PREFIX', '')
    ],
    'handler_with'   => [
        'apiConfig'  => [
            'entrypoint'  => env('LOKI_ENTRYPOINT', "http://localhost:3100"),
            'context'     => [],
            'labels'      => [],
            'client_name' => '',
            'auth' => [
                'basic' => [
                    env('LOKI_AUTH_BASIC_USER', ''),
                    env('LOKI_AUTH_BASIC_PASSWORD', '')
                ],
            ]
        ],
    ],
],

Testing

To test using the provided docker-compose file, you'll need an up-to-date docker/docker-compose installation You can start the Loki container by navigating to src/main/test/docker and running

docker-compose up

If you're testing from a local php installation, you'll need to retrieve the Loki container ip with :

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' itspire-monolog-loki_loki_1

If you're testing from containerized php, you'll need to start the container with an extra host named Loki mapped to your current host ip, using the following option :

--add-host loki:{your_host_ip}

Run the test using phpunit, and you can verify that posting to Loki works by running the following from your host terminal :

curl -G -s  "http://localhost:3100/loki/api/v1/query" --data-urlencode 'query={channel="test"}' | jq

For each time you ran the tests, you should see a log entry looking like the following :

{
    "stream": {
        "channel": "test",
        "host": "f2bbe48b0204",
        "level_name": "WARNING"
    },
    "values": [
        [
        "1591627127000000000",
        "{\"message\":\"test\",\"level\":300,\"level_name\":\"WARNING\",\"channel\":\"test\",\"datetime\":\"2020-06-08 14:38:47\",\"ctxt_data\":\"[object] (stdClass: {})\",\"ctxt_foo\":\"34\"}"
        ]
    ]
}