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
Requires
- php: >=8.1
- open-telemetry/exporter-otlp: ^1.0
- open-telemetry/sdk: ^1.0
Requires (Dev)
- phpunit/phpunit: ^10.0
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:
- Emit structured logs only - JSON, consistent keys, no free-text messages
- Log one "wide event" per request - Capture all context in a single final log entry
- Accumulate context via middleware - Build an event object at request start, enrich it throughout execution, emit once at the end
- Prefer context over volume - Fewer logs, richer data
- Tail-sample aggressively - Always keep errors + slow requests; sample the rest
- Store logs as queryable data - Columnar DB + SQL > grep
- 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/protobuforhttp/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
sampleRateisnull, 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
- One log per request - Use middleware to emit a single log at request end
- Accumulate context - Add business context throughout request execution
- Structured data only - No free-text messages, use consistent keys
- Tail-sample - Configure sampling to reduce volume while keeping errors and slow requests
- Business context - Include user, org, plan, feature flags, etc. - anything that helps answer questions
- Error handling - Always capture exceptions in context, even if you re-throw them
License
MIT