gilbitron/canonical-context-logging

PHP SDK for Canonical Context Logging - structured JSON logs with one wide event per request

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/gilbitron/canonical-context-logging

0.1.0 2025-12-22 22:30 UTC

This package is auto-updated.

Last update: 2025-12-22 22:37:08 UTC


README

PHP SDK for Canonical Context Logging - structured JSON logs with one wide event per request.

Overview

This package implements the Canonical Context Logging pattern:

  1. Emit structured logs only - JSON, consistent keys, no free-text messages
  2. Log one "wide event" per request - Capture all context in a single final log entry
  3. Accumulate context via middleware - Build an event object at request start, enrich it throughout execution, emit once at the end
  4. Prefer context over volume - Fewer logs, richer data
  5. Tail-sample aggressively - Always keep errors + slow requests; sample the rest
  6. Store logs as queryable data - Columnar DB + SQL > grep
  7. OTel is transport, not design - You decide what to log; business context is your job

Requirements

  • PHP 8.1 or higher
  • OpenTelemetry SDK

Installation

composer require gilbitron/canonical-context-logging

Quick Start

Basic Usage (Framework-Agnostic)

use CanonicalContextLogging\Context\SingletonStorage;
use CanonicalContextLogging\Exporter\OtlpExporter;
use CanonicalContextLogging\Logger\CanonicalLogger;
use CanonicalContextLogging\Middleware\RequestMiddleware;

// Setup - OTLP exporter reads from environment variables by default
// Set OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4318
$storage = new SingletonStorage();
$exporter = new OtlpExporter(); // Reads config from environment
$logger = new CanonicalLogger(
    exporter: $exporter,
    slowRequestThreshold: 1.0, // Log requests slower than 1 second
    sampleRate: 0.1 // Sample 10% of normal requests
);
$middleware = new RequestMiddleware($storage, $logger);

// Start request
$context = $middleware->start();
$context->setService('my-service', '1.0.0');
$context->addContext('user_id', 123);
$context->addContext('org_id', 456);

// ... your application logic ...

// End request
$context->setStatus(200);
$context->setDuration(0.123);
$middleware->end($context);

With Error Handling

try {
    $context = $middleware->start();
    $context->setService('my-service', '1.0.0');

    // ... application logic ...

    $context->setStatus(200);
} catch (\Throwable $e) {
    $context->setError($e);
    $context->setStatus(500);
} finally {
    $context->setDuration($context->getDuration());
    $middleware->end($context);
}

Configuration

Exporter Options

Console Exporter (Development)

For local development, you can use the console exporter to output logs to stdout/stderr:

use CanonicalContextLogging\Exporter\ConsoleExporter;

// Output to stderr with pretty printing
$exporter = new ConsoleExporter(useStderr: true, prettyPrint: true);

// Output to stdout, compact JSON
$exporter = new ConsoleExporter(useStderr: false, prettyPrint: false);

OTLP Exporter (Production)

The OTLP exporter uses the official OpenTelemetry SDK and reads configuration from environment variables:

use CanonicalContextLogging\Exporter\OtlpExporter;
use CanonicalContextLogging\Exporter\OtlpConfig;

// Option 1: Use environment variables (recommended)
$exporter = new OtlpExporter(); // Reads from environment

// Option 2: Explicit configuration
$config = OtlpConfig::create(
    endpoint: 'https://otel-collector:4318',
    protocol: 'http/protobuf',
    headers: ['Authorization' => 'Bearer ' . $apiKey],
    timeout: 10
);
$exporter = new OtlpExporter($config);

Environment Variables:

The exporter reads standard OpenTelemetry environment variables:

  • OTEL_EXPORTER_OTLP_ENDPOINT - OTLP endpoint URL (default: http://localhost:4318)
  • OTEL_EXPORTER_OTLP_PROTOCOL - Protocol to use: http/protobuf or http/json (default: http/protobuf)
  • OTEL_EXPORTER_OTLP_HEADERS - Headers as comma-separated key=value pairs (e.g., key1=value1,key2=value2)
  • OTEL_EXPORTER_OTLP_TIMEOUT - Timeout in seconds (default: 10)

Example:

export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token123"
export OTEL_EXPORTER_OTLP_TIMEOUT=10

File Exporter

Write logs to a file in JSONL format (one JSON object per line):

use CanonicalContextLogging\Exporter\FileExporter;

// Compact JSON (default)
$exporter = new FileExporter('/var/log/app.jsonl');

// Pretty-printed JSON
$exporter = new FileExporter('/var/log/app.jsonl', prettyPrint: true);

The FileExporter automatically creates the directory structure if it doesn't exist and appends each log entry as a new line (JSONL format).

Custom Exporter

Implement ExporterInterface to create your own exporter:

use CanonicalContextLogging\Exporter\ExporterInterface;

class CustomExporter implements ExporterInterface
{
    public function export(array $data): void
    {
        // Your custom export logic
    }
}

Logger Configuration

use CanonicalContextLogging\Logger\CanonicalLogger;

$logger = new CanonicalLogger(
    exporter: $exporter,
    slowRequestThreshold: 1.0, // Always log requests slower than 1 second
    sampleRate: 0.1 // Sample 10% of normal requests (0.0 to 1.0)
);

Tail-Sampling Rules:

  • Errors are always logged
  • Slow requests (above threshold) are always logged
  • Other requests are sampled based on sampleRate
  • If sampleRate is null, all requests are logged

Context Storage

Singleton Storage (Framework-Agnostic)

use CanonicalContextLogging\Context\SingletonStorage;

$storage = new SingletonStorage();

Container Storage (Framework-Specific)

For framework integration, create a custom storage implementation that uses your framework's container.

EventContext API

The EventContext class accumulates all context for a single request:

$context = new EventContext();

// Initialize request
$context->startRequest($traceId, $spanId);

// Service metadata
$context->setService('my-service', '1.0.0');

// Business context (user, org, plan, feature flags, etc.)
$context->addContext('user_id', 123);
$context->addContext('org_id', 456);
$context->addContext('plan', 'premium');
$context->addContext('feature_flags', ['feature_a', 'feature_b']);

// Or add multiple at once
$context->addContexts([
    'user_id' => 123,
    'org_id' => 456,
    'plan' => 'premium',
]);

// Error handling
$context->setError($exception);

// HTTP status
$context->setStatus(200);

// Duration (auto-calculated if not set)
$context->setDuration(0.123);

// Export as array for JSON encoding
$data = $context->toArray();

Log Output Format

The structured log output follows this schema:

{
  "timestamp": "2024-01-15T10:30:45+00:00",
  "trace_id": "a1b2c3d4e5f6...",
  "span_id": "1234567890abcdef",
  "service": {
    "name": "my-service",
    "version": "1.0.0"
  },
  "status": 200,
  "duration": 0.123,
  "context": {
    "user_id": 123,
    "org_id": 456,
    "plan": "premium",
    "feature_flags": ["feature_a", "feature_b"]
  },
  "error": {
    "type": "RuntimeException",
    "message": "Something went wrong",
    "file": "/path/to/file.php",
    "line": 42,
    "code": 0
  }
}

Best Practices

  1. One log per request - Use middleware to emit a single log at request end
  2. Accumulate context - Add business context throughout request execution
  3. Structured data only - No free-text messages, use consistent keys
  4. Tail-sample - Configure sampling to reduce volume while keeping errors and slow requests
  5. Business context - Include user, org, plan, feature flags, etc. - anything that helps answer questions
  6. Error handling - Always capture exceptions in context, even if you re-throw them

License

MIT