PHPackages                             labelgrupnetworks/opentelemetry-sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. labelgrupnetworks/opentelemetry-sdk

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

labelgrupnetworks/opentelemetry-sdk
===================================

Grafana Loki Logging Package

019↓50%PHP

Since May 20Pushed 2w agoCompare

[ Source](https://github.com/labelgrupnetworks/opentelemetry-sdk)[ Packagist](https://packagist.org/packages/labelgrupnetworks/opentelemetry-sdk)[ RSS](/packages/labelgrupnetworks-opentelemetry-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Loki Logging Package
====================

[](#loki-logging-package)

Laravel package for structured logging to [Grafana Loki](https://grafana.com/oss/loki/) with [OpenTelemetry](https://opentelemetry.io/) tracing support.

Every log entry is enriched with a `trace_id` and `span_id` so HTTP requests, CLI commands, queued jobs, and exceptions can be correlated in Loki/Grafana.

---

📦 Installation
--------------

[](#-installation)

```
composer require labelgrupnetworks/opentelemetry-sdk
```

Publish the config file:

```
php artisan vendor:publish --provider="LokiLogging\LokiLoggingServiceProvider"
```

---

⚙️ Configuration
----------------

[](#️-configuration)

`config/loki-logging.php`:

```
return [
    // Loki push API endpoint
    'url' => env('LOG_LOKI_URL'),

    // Service name attached to every log entry
    'service_name' => env('SERVICE_NAME'),

    'classes' => [
        // Optional: class that adds custom fields to every log entry
        // Must implement LokiLogging\Core\Context\ContextInterface
        'context' => null,

        // Optional: custom Monolog formatter. Must extend LokiLogging\Core\Formatter\Formatter.
        // When null, the built-in OpenTelemetryFormatter is used.
        'formatter' => null,
    ],

    // Exception class → ExceptionLogInterface class mappings.
    // Matched via instanceof (first match wins).
    // Falls back to the built-in ExceptionLog if no match is found.
    'exceptions_logs' => [
        // \App\Exceptions\MyException::class => \App\Exceptions\Loki\MyExceptionLog::class,
    ],
];
```

Register the service provider and configure the Loki channel in `config/logging.php`:

```
// config/app.php
'providers' => [
    LokiLogging\LokiLoggingServiceProvider::class,
],

// config/logging.php
'channels' => [
    'loki' => [
        'driver' => 'monolog',
        'level' => env('LOG_LEVEL', 'debug'),
        'handler' => \LokiLogging\Core\Loki\LokiHandler::class
    ],
],
```

Set `LOG_CHANNEL=loki` in your `.env`.

> **Migrating from the old `monolog` driver config?** The previous format (`driver: monolog`, `handler`, `formatter: default`, `processors`) still works. The new `loki` driver is the recommended approach going forward.

---

🌐 HTTP Request Tracing
----------------------

[](#-http-request-tracing)

Register `LokiTraceMiddleware` in `app/Http/Kernel.php`:

```
protected $middleware = [
    \LokiLogging\Http\Middleware\LokiTraceMiddleware::class,
];
```

For every HTTP request this middleware:

1. Extracts an incoming `traceparent` header (if present) to continue a distributed trace.
2. Opens an OpenTelemetry span (`HTTP METHOD /path`).
3. On response, logs the request/response to Loki with `trace_id`, `span_id`, method, path, status code, and any baggage fields.
4. Marks the span `ERROR` on 5xx responses or unhandled exceptions.

### Propagating trace context from an external caller

[](#propagating-trace-context-from-an-external-caller)

The middleware uses the [W3C TraceContext](https://www.w3.org/TR/trace-context/) standard. To continue a distributed trace, the caller must send a `traceparent` header:

```
traceparent: 00-{traceId}-{parentSpanId}-{traceFlags}

```

PartLengthDescription`00`2 charsVersion (always `00`)`traceId`32 hex charsIdentifies the whole distributed trace`parentSpanId`16 hex charsThe caller's current span`traceFlags`2 hex chars`01` = sampled, `00` = not sampled**curl:**

```
curl -H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
     https://your-app.com/api/orders
```

**From another service that also uses this package:**

```
use LokiLogging\Helpers\LokiTrace;

$http->withHeader(
    'traceparent',
    sprintf('00-%s-%s-01', LokiTrace::traceId(), LokiTrace::spanId())
);
```

---

🖥️ CLI Command Tracing
----------------------

[](#️-cli-command-tracing)

Handled automatically by `CommandTracingManager` (wired in the service provider). Each `php artisan` invocation opens a span for the duration of the command and logs the result (success/failure) to Loki.

Daemon commands (`queue:work`, `horizon`, etc.) are excluded from span creation to avoid infinite spans.

---

🔄 Queue Job Tracing
-------------------

[](#-queue-job-tracing)

Handled automatically by `JobTracingManager`. Each job dispatch injects the current `trace_id` into the queue payload so the job can be correlated with the HTTP request or CLI command that dispatched it. A span is opened when the job starts processing and closed (with the appropriate status) when it finishes, fails, or throws.

---

🚨 Exception Logging
-------------------

[](#-exception-logging)

### How it works

[](#how-it-works)

Exceptions are logged to Loki from your `app/Exceptions/Handler.php`. The full flow is:

```
Exception thrown
  → Handler::reportable()
  → LokiLoggingExceptionLogger::log($exception)
  → LokiExceptionLog::exception($exception)
  → config('loki-logging.exceptions_logs') resolved via instanceof
      → match found  → CustomExceptionLog::severity() + ::data()
      → no match     → ExceptionLog::severity() + ExceptionData::fromException()
  → Log::error/warning($message, ['exception' => $exceptionData])
  → Loki

```

### Handler setup

[](#handler-setup)

Wire up the logger once in `Handler::register()`:

```
use LokiLogging\Exception\LokiLoggingExceptionLogger;

public function register(): void
{
    $this->reportable(function (Throwable $exception) {
        // Exclude exceptions that should not be logged
        if ($exception instanceof OAuthServerException) {
            return false;
        }

        LokiLoggingExceptionLogger::log($exception);

        return false; // prevent Laravel's default logging
    });
}
```

`return false` stops Laravel's default log chain so exceptions are not double-logged.

### Default behaviour

[](#default-behaviour)

Any exception not listed in `exceptions_logs` is handled by the built-in `ExceptionLog`:

Exception codeSeverity`>= 500``ERROR``2xx``DEBUG`everything else`WARNING`The data sent to Loki contains: `class`, `code`, `file:line`, and the first 5 frames of the stack trace.

### Custom exception mapping

[](#custom-exception-mapping)

To enrich specific exception types, create a class implementing `ExceptionLogInterface`:

```
namespace App\Exceptions\Loki;

use LokiLogging\Core\Data\ExceptionData;use LokiLogging\Core\Enums\Severity;use LokiLogging\Core\Exceptions\ExceptionLog;use LokiLogging\Core\Exceptions\ExceptionLogInterface;

class MyExceptionLog implements ExceptionLogInterface
{
    public static function data(\Throwable $exception): ExceptionData
    {
        return ExceptionData::fromException($exception)
            ->withErrorCode($exception->errorCode)          // adds 'error_code' field
            ->withExtraData(['extra' => $exception->extra]); // adds 'extra_data' field
    }

    public static function severity(\Throwable $exception): Severity
    {
        // Delegate to the default HTTP-code-based logic, or hardcode a level
        return ExceptionLog::severity($exception);
    }
}
```

Register it in `config/loki-logging.php`:

```
'exceptions_logs' => [
    \App\Exceptions\MyException::class => \App\Exceptions\Loki\MyExceptionLog::class,
],
```

Matching is done via `instanceof`, so a mapping on a base class also covers all its subclasses. The first match in the array wins.

### `ExceptionData` API

[](#exceptiondata-api)

```
ExceptionData::fromException($e) // base data: class, code, message, file, trace
    ->withErrorCode(string $code) // add an application-level error code
    ->withExtraData(array $extra) // merge extra key/value fields
    // Read
    ->getMessage()
    ->getClass()
    ->getCode()
    ->getFile()
    ->getLine()
    ->getErrorCode()
    ->getExtraData()
    ->toArray()           // serialised form sent to Loki
```

---

📐 Log entry format (`Formatter`)
--------------------------------

[](#-log-entry-format-formatter)

`src/Core/Formatter/Formatter.php` is the abstract Monolog formatter that serialises every log record into the JSON string pushed to Loki. The concrete implementation is `src/Formatter/OpenTelemetryFormatter.php`. Understanding its output is useful when querying Loki/Grafana.

### Custom formatter

[](#custom-formatter)

If you need a different output format, create a class extending `LokiLogging\Core\Formatter\Formatter` and implement `logData()`:

```
namespace App\Logging;

use LokiLogging\Core\Formatter\Formatter;
use Monolog\LogRecord;

class MyLokiFormatter extends Formatter
{
    protected function logData(LogRecord $record, array $context): array
    {
        return [
            'message' => $record->message,
            'context' => $context,
            // ...your custom fields
        ];
    }
}
```

Register it in `config/loki-logging.php`:

```
'classes' => [
    'formatter' => \App\Logging\MyLokiFormatter::class,
],
```

You can also override the formatter per-channel in `config/logging.php` (takes precedence over `classes.formatter`):

```
'loki' => [
    'driver' => 'loki',
    'level' => env('LOG_LEVEL', 'debug'),
    'formatter' => \App\Logging\MyLokiFormatter::class,
],
```

> **Note:** the `getContext()`, `getErrorCode()`, `getContextData()`, and `SEVERITY_MAP` helpers are available to subclasses via `protected` visibility.

### Output JSON structure

[](#output-json-structure)

```
{
    "body": "The log message",
    "context": { ... },
    "env": "production",
    "error_code": "ERR#001",
    "http.method": "POST",
    "http.route": "api/orders",
    "http.status_code": 422,
    "severity": "ERROR",
    "severity_number": 17,
    "service_name": "NAP",
    "span_id": "abc123",
    "trace_id": "def456",
    "trace_flags": 1,
    "type": "http",
    "user": 42
}
```

FieldSourceNotes`body``LogRecord::$message`The string passed to `Log::error(...)``context``LogRecord::$context` merged with `context_class`See below`env``ResourceInfo` → `config('app.env')``error_code``ExceptionData::getErrorCode()`Only present for exception logs`http.method``$record->extra` (injected by middleware)`http.route``$record->extra` (injected by middleware)`http.status_code``$record->extra` (injected by middleware)`severity`Monolog level name, uppercased`DEBUG`, `WARNING`, `ERROR`…`severity_number`OTel severity number scaleSee table below`service_name``ResourceInfo` → `config('loki-logging.service_name')``span_id``$record->extra` (injected by `Processor`)`trace_id``$record->extra` (injected by `Processor`)`trace_flags``$record->extra` (injected by `Processor`)`type``$record->extra` (injected by middleware / tracing managers)`http`, `cli`, `job`, `exception`, `log``user_id``auth()->id()``null` for unauthenticated or CLI### Severity number mapping

[](#severity-number-mapping)

Monolog level`severity``severity_number``DEBUG``DEBUG`5`INFO``INFO`9`NOTICE``NOTICE`10`WARNING``WARNING`13`ERROR``ERROR`17`CRITICAL``CRITICAL`21`ALERT``ALERT`22`EMERGENCY``EMERGENCY`24Numbers follow the [OpenTelemetry Log Data Model](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) specification.

### How `context` is built

[](#how-context-is-built)

`context` is the merge of two sources, in this order:

```
context = [ ...classes.context::context(), ...LogRecord::$context ]

```

1. **`classes.context::context()`** — global fields added to every entry (e.g. `tenant_id`, `user_id`). Configured via `config('loki-logging.classes.context')`. Returns `[]` if not set.
2. **`LogRecord::$context`** — the array passed as the second argument to `Log::error($msg, $context)`.

If a value inside `$context` is an `ExceptionData` instance (set automatically by `LokiLoggingExceptionLogger`), it is serialised via `ExceptionData::toArray()` before being stored:

```
"context": {
    "tenant_id": "acme",
    "exception": {
        "class": "Domain\\Nap\\Shared\\Exceptions\\UseCaseException",
        "code": 422,
        "file": "app/Http/Controllers/OrderController.php:45",
        "trace": ["...", "..."],
        "error_code": "ERR#001",
        "extra_data": { "report_data": { "order_id": 99 } }
    }
}
```

### How `error_code` is extracted

[](#how-error_code-is-extracted)

`error_code` is promoted to a top-level field (outside `context`) so it can be used as a Loki label or for direct filtering. The formatter tries to extract it in this order:

1. `$record->context['exception']` is an `ExceptionData` → calls `->getErrorCode()`
2. `$record->context['exception']` is an array → reads `errorCode`, `error_code`, or `system_error` key
3. Not found → `null` (field omitted from the output)

---

🧩 Custom Context
----------------

[](#-custom-context)

To append extra fields to **every** log entry (e.g. tenant ID, user ID), create a class implementing `ContextInterface` and set it in config:

```
namespace App\Services\Loki;

use LokiLogging\Core\Context\ContextInterface;

class LokiContext implements ContextInterface
{
    public static function context(): array
    {
        return [
            'user_id' => auth()->id(),
            'tenant_id' => tenant()->id,
        ];
    }
}
```

```
// config/loki-logging.php
'classes' => [
    'context' => \App\Services\Loki\LokiContext::class,
],
```

---

🔍 Reading `trace_id` / `span_id` in your code
---------------------------------------------

[](#-reading-trace_id--span_id-in-your-code)

```
use LokiLogging\Helpers\LokiTrace;

LokiTrace::traceId(); // string|null
LokiTrace::spanId(); // string|null
LokiTrace::context(); // ['trace_id' => ..., 'span_id' => ...]
```

---

🗂️ Package structure
--------------------

[](#️-package-structure)

```
src/
├── Core/
│   ├── Context/
│   │   └── ContextInterface.php         # Contract for custom context fields
│   ├── Enums/
│   │   ├── Severity.php                 # Log severity levels
│   │   └── Type.php                     # Log entry type (http, cli, job, exception)
│   ├── Exceptions/
│   │   ├── Data/
│   │   │   └── ExceptionData.php        # DTO sent to Loki for exceptions
│   │   ├── ExceptionLog.php             # Default ExceptionLogInterface implementation
│   │   └── ExceptionLogInterface.php    # Contract for custom exception mapping
│   ├── Formatter/
│   │   ├── Formatter.php                # Abstract base formatter — extend to customise output
│   │   └── FormatterInterface.php       # Interface contract for formatters
│   ├── Loki/
│   │   └── LokiHandler.php              # Monolog handler — pushes to Loki API
│   ├── Processor/
│   │   └── Processor.php               # Injects trace_id / span_id into every record
│   └── TracingManager/
│       ├── CommandTracingManager.php    # Span lifecycle for artisan commands
│       └── JobTracingManager.php        # Span lifecycle for queued jobs
├── Exception/
│   ├── LokiLoggingException.php         # Internal package exception
│   └── LokiLoggingExceptionLogger.php   # Entry point for exception logging
├── Formatter/
│   └── OpenTelemetryFormatter.php       # Default formatter — OpenTelemetry JSON output
├── Helpers/
│   ├── LokiExceptionLog.php             # Static helper — resolves and dispatches exception logs
│   └── LokiTrace.php                    # Static helper — reads current trace_id / span_id
├── Http/
│   └── Middleware/
│       └── LokiTraceMiddleware.php      # HTTP request span + response log
├── LokiLoggingConfig.php                # Typed accessors for config/loki-logging.php
└── LokiLoggingServiceProvider.php       # Registers singletons, wires events

```

###  Health Score

23

—

LowBetter than 26% of packages

Maintenance63

Regular maintenance activity

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/d5b77962e3f3a1469f8110bb7d043d62b160e73ed2d938117cbdd3ed3a1b05e4?d=identicon)[labelgrupnetworks](/maintainers/labelgrupnetworks)

---

Top Contributors

[![drkbcn](https://avatars.githubusercontent.com/u/2090227?v=4)](https://github.com/drkbcn "drkbcn (3 commits)")

### Embed Badge

![Health badge](/badges/labelgrupnetworks-opentelemetry-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/labelgrupnetworks-opentelemetry-sdk/health.svg)](https://phpackages.com/packages/labelgrupnetworks-opentelemetry-sdk)
```

###  Alternatives

[psr/log

Common interface for logging libraries

10.4k1.2B10.8k](/packages/psr-log)[open-telemetry/api

API for OpenTelemetry PHP.

1938.5M259](/packages/open-telemetry-api)[open-telemetry/sdk

SDK for OpenTelemetry PHP.

2326.5M315](/packages/open-telemetry-sdk)[illuminated/console-logger

Logging and Notifications for Laravel Console Commands.

8676.7k](/packages/illuminated-console-logger)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
