Telemetry
A production-grade observability toolkit for PHP 8.4+ providing metrics, distributed tracing, and structured logging in a single, unified package. Ships with Prometheus, StatsD, W3C Trace Context, and PSR-3/PSR-15 integrations out of the box.
What's New in 2.0
| Area | v1 | v2 |
|---|---|---|
| PHP version | 8.1+ | 8.4+ — readonly, final, enums, named args |
| Metrics | Basic Prometheus only | MetricsInterface — Prometheus, StatsD, InMemory, Null drivers |
| Tracing | ❌ | Full W3C Trace Context — spans, events, exporters |
| Logging | ❌ | PSR-3 TelemetryLogger with automatic trace correlation |
| Middleware | ❌ | PSR-15 RequestMetricsMiddleware + RequestTracingMiddleware |
| Attributes | ❌ | #[Traced], #[Counted], #[Timed] |
| Factory | Manual construction | TelemetryFactory::create() — one-line bootstrapping |
| Facade | ❌ | Telemetry::counter(), Telemetry::trace(), Telemetry::log() |
Features
📊 Metrics — Counter, Gauge, Histogram, Summary with Prometheus and StatsD backends
🔍 Distributed Tracing — W3C Trace Context compatible spans, events, and exception recording
📝 Structured Logging — PSR-3 logger with automatic trace/span ID injection
🌐 PSR-15 Middleware — Automatic HTTP request metrics and distributed tracing
🏷️ PHP 8 Attributes — #[Traced], #[Counted], #[Timed] for declarative instrumentation
🏭 Factory Pattern — One-line bootstrap via TelemetryFactory::create()
⚡ Zero-cost Null Drivers — NullMetrics, NullTracer for safe development
Installation
composer require monkeyscloud/monkeyslegion-telemetry:^2.0
Optional Dependencies
# Prometheus exposition format
composer require promphp/prometheus_client_php
# PSR-15 middleware support
composer require psr/http-message psr/http-server-middleware
Quick Start
One-line Bootstrap
use MonkeysLegion\Telemetry\Telemetry;
Telemetry::init([
'metrics' => ['driver' => 'memory', 'namespace' => 'myapp'],
'tracing' => ['service_name' => 'myapp', 'exporter' => 'console'],
'logging' => ['json' => true, 'level' => 'info'],
]);
// Metrics
Telemetry::counter('requests_total');
Telemetry::gauge('active_connections', 42);
Telemetry::histogram('response_time', 0.123);
// Tracing
$result = Telemetry::trace('fetch-user', fn () => $userRepo->find($id));
// Logging (with automatic trace correlation)
Telemetry::log()->info('User fetched', ['user_id' => $id]);
Metrics
All metric drivers implement MetricsInterface and support Counter, Gauge, Histogram, Summary, and Timer operations.
Drivers
| Driver | Class | Use Case |
|---|---|---|
null | NullMetrics | Development — zero overhead |
memory | InMemoryMetrics | Testing, single-request |
prometheus | PrometheusMetrics | Production — Prometheus scraping |
statsd | StatsDMetrics | Production — StatsD / DogStatsD / Telegraf |
Counter (Monotonically Increasing)
use MonkeysLegion\Telemetry\Telemetry;
// Increment by 1
Telemetry::counter('http_requests_total');
// Increment by N with dimensional labels
Telemetry::counter('http_requests_total', 1, [
'method' => 'GET',
'status' => '200',
]);
Gauge (Point-in-Time Value)
Telemetry::gauge('active_connections', 42);
Telemetry::gauge('queue_depth', 128, ['queue' => 'emails']);
Histogram (Distribution / Timing)
Telemetry::histogram('response_time_seconds', 0.157);
// Custom buckets
Telemetry::histogram('payload_size_bytes', 4096, [], [
100, 500, 1000, 5000, 10000, 50000,
]);
Timer Helper
$stop = Telemetry::timer('db_query_duration');
$result = $db->query('SELECT ...');
$elapsed = $stop(['query' => 'user_lookup']);
// Records a histogram observation automatically
Direct Driver Construction
use MonkeysLegion\Telemetry\Metrics\StatsDMetrics;
use MonkeysLegion\Telemetry\Metrics\PrometheusMetrics;
use MonkeysLegion\Telemetry\Metrics\InMemoryMetrics;
use Prometheus\Storage\InMemory;
// StatsD
$metrics = new StatsDMetrics(
host: '127.0.0.1',
port: 8125,
namespace: 'myapp',
dogstatsd: true, // DogStatsD tag format
sampleRate: 0.5, // 50% sampling
);
// Prometheus
$metrics = new PrometheusMetrics(
adapter: new InMemory(),
namespace: 'myapp',
);
// InMemory (testing)
$metrics = new InMemoryMetrics('myapp');
$metrics->counter('requests', 1, ['method' => 'GET']);
$all = $metrics->getMetrics(); // inspect recorded values
Distributed Tracing
W3C Trace Context compatible tracing with automatic parent-child span relationships.
Traced Callback (Recommended)
use MonkeysLegion\Telemetry\Telemetry;
$order = Telemetry::trace('create-order', function () use ($cart) {
// Nested spans are automatically parented
$items = Telemetry::trace('validate-items', fn () => $cart->validate());
$payment = Telemetry::trace('charge-payment', fn () => $stripe->charge($cart));
return new Order($items, $payment);
});
Manual Span Management
use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;
use MonkeysLegion\Telemetry\Tracing\SpanStatus;
$span = Telemetry::startSpan('db.query', SpanKind::CLIENT, [
'db.system' => 'mysql',
'db.statement' => 'SELECT * FROM users WHERE id = ?',
]);
try {
$result = $db->query($sql, $params);
$span->setStatus(SpanStatus::OK);
$span->setAttribute('db.row_count', count($result));
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(SpanStatus::ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
}
Span Events
$span = Telemetry::startSpan('process-order');
$span->addEvent('payment.started', ['amount' => 99.99]);
// ... process payment ...
$span->addEvent('payment.completed', ['transaction_id' => 'txn_123']);
$span->end();
Trace Context Propagation
use MonkeysLegion\Telemetry\Factory\TelemetryFactory;
$tracer = TelemetryFactory::createTracer([
'service_name' => 'api-gateway',
'exporter' => 'http',
'endpoint' => 'http://jaeger:4318/v1/traces',
]);
// Extract from incoming HTTP headers
$context = $tracer->extract($request->getHeaders());
// Inject into outgoing HTTP headers
$headers = $tracer->inject([]);
// $headers = ['traceparent' => '00-<trace_id>-<span_id>-01']
Span Kinds
| Kind | Use |
|---|---|
SpanKind::INTERNAL | Default — internal application logic |
SpanKind::SERVER | Incoming HTTP request handling |
SpanKind::CLIENT | Outgoing HTTP / DB / RPC call |
SpanKind::PRODUCER | Message queue publish |
SpanKind::CONSUMER | Message queue consume |
Exporters
| Exporter | Class | Use Case |
|---|---|---|
console | ConsoleExporter | Local development / debugging |
http | JsonHttpExporter | OTLP/HTTP — Jaeger, Tempo, Zipkin |
| — | InMemoryExporter | Unit testing |
Logging with Trace Correlation
PSR-3 compatible logging that automatically injects trace_id and span_id into every log entry.
use MonkeysLegion\Telemetry\Telemetry;
Telemetry::init([
'tracing' => ['service_name' => 'api'],
'logging' => ['json' => true, 'stream' => 'php://stderr'],
]);
Telemetry::trace('handle-request', function () {
// trace_id and span_id injected automatically
Telemetry::log()->info('Processing request', ['path' => '/api/users']);
Telemetry::log()->warning('Slow query detected', ['duration_ms' => 450]);
});
// Output (JSON):
// {"level":"info","message":"Processing request","trace_id":"abc123...","span_id":"def456...","path":"/api/users","timestamp":"2026-04-27T20:00:00+00:00"}
Direct Logger Construction
use MonkeysLegion\Telemetry\Logging\TelemetryLogger;
use MonkeysLegion\Telemetry\Logging\StreamLogger;
use MonkeysLegion\Telemetry\Logging\JsonFormatter;
use MonkeysLegion\Telemetry\Logging\TracingContextProvider;
use Psr\Log\LogLevel;
$streamLogger = new StreamLogger(
stream: '/var/log/app.log',
minLevel: LogLevel::INFO,
formatter: new JsonFormatter(prettyPrint: false),
);
$logger = new TelemetryLogger(
logger: $streamLogger,
contextProvider: new TracingContextProvider($tracer),
);
$logger->info('User created', ['user_id' => 42]);
// With explicit trace context
$logger->logWithTelemetry(
level: 'info',
message: 'Payment processed',
context: ['amount' => 99.99],
traceId: 'abc123',
spanId: 'def456',
);
Log Processors
$logger->addProcessor(function (array $record): array {
$record['context']['hostname'] = gethostname();
$record['context']['pid'] = getmypid();
return $record;
});
$logger->setDefaultContext(['app' => 'myapp', 'env' => 'production']);
PSR-15 Middleware
Request Metrics
Automatically records http_requests_total, http_request_duration_seconds, and http_requests_in_progress for every request.
use MonkeysLegion\Telemetry\Middleware\RequestMetricsMiddleware;
$middleware = new RequestMetricsMiddleware(
metrics: $metrics,
includeRoute: true, // label: route
includeMethod: true, // label: method
includeStatus: true, // label: status
);
// Add to your PSR-15 middleware pipeline
$app->pipe($middleware);
Recorded metrics:
| Metric | Type | Labels |
|---|---|---|
http_requests_total | Counter | method, route, status |
http_request_duration_seconds | Histogram | method, route, status |
http_requests_in_progress | Gauge | — |
Request Tracing
Creates a root SERVER span per request with W3C trace context propagation.
use MonkeysLegion\Telemetry\Middleware\RequestTracingMiddleware;
$middleware = new RequestTracingMiddleware(
tracer: $tracer,
propagateContext: true, // inject traceparent in response headers
);
$app->pipe($middleware);
Span attributes:
| Attribute | Example |
|---|---|
http.method | GET |
http.url | https://api.example.com/users/42 |
http.target | /users/42 |
http.status_code | 200 |
http.host | api.example.com |
net.peer.ip | 192.168.1.1 |
PHP 8 Attributes
Declarative instrumentation via attributes. Combine with an AOP framework or the MonkeysLegion service container for automatic weaving.
#[Traced] — Automatic Span Creation
use MonkeysLegion\Telemetry\Attribute\Traced;
use MonkeysLegion\Telemetry\Tracing\SpanKind;
class UserService
{
#[Traced('fetch-user')]
public function find(int $id): User { /* ... */ }
#[Traced(kind: SpanKind::CLIENT, attributes: ['db.system' => 'mysql'])]
public function query(string $sql): array { /* ... */ }
}
#[Counted] — Automatic Call Counting
use MonkeysLegion\Telemetry\Attribute\Counted;
class OrderController
{
#[Counted('api_orders_total')]
public function create(Request $request): Response { /* ... */ }
#[Counted(labels: ['type' => 'refund'], countExceptions: true)]
public function refund(string $orderId): void { /* ... */ }
}
#[Timed] — Automatic Duration Measurement
use MonkeysLegion\Telemetry\Attribute\Timed;
class ReportGenerator
{
#[Timed('report_generation_duration')]
public function generate(string $type): Report { /* ... */ }
#[Timed(buckets: [0.1, 0.5, 1.0, 5.0, 10.0])]
public function export(Report $report): string { /* ... */ }
}
Factory & Configuration
TelemetryFactory
use MonkeysLegion\Telemetry\Factory\TelemetryFactory;
// Create individual components
$metrics = TelemetryFactory::createMetrics([
'driver' => 'statsd',
'namespace' => 'myapp',
'host' => '127.0.0.1',
'port' => 8125,
'dogstatsd' => true,
'sample_rate' => 0.5,
]);
$tracer = TelemetryFactory::createTracer([
'enabled' => true,
'service_name' => 'api',
'sample_rate' => 1.0,
'exporter' => 'http',
'endpoint' => 'http://jaeger:4318/v1/traces',
'headers' => ['Authorization' => 'Bearer token'],
]);
$logger = TelemetryFactory::createLogger([
'stream' => 'php://stderr',
'level' => 'info',
'json' => true,
'pretty' => false,
]);
// Or create the full stack in one call
$stack = TelemetryFactory::create([
'metrics' => ['driver' => 'prometheus'],
'tracing' => ['service_name' => 'api', 'exporter' => 'http'],
'logging' => ['json' => true],
]);
// $stack['metrics'], $stack['tracer'], $stack['logger']
Configuration Reference
Metrics
| Key | Type | Default | Description |
|---|---|---|---|
driver | string | 'null' | null, memory, prometheus, statsd |
namespace | string | 'app' | Metric name prefix |
host | string | '127.0.0.1' | StatsD host |
port | int | 8125 | StatsD port |
dogstatsd | bool | false | Enable DogStatsD tag format |
sample_rate | float | 1.0 | StatsD sampling rate (0.0–1.0) |
prometheus_adapter | Adapter | InMemory | Prometheus storage adapter |
Tracing
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable/disable tracing |
service_name | string | 'app' | Service identifier |
sample_rate | float | 1.0 | Trace sampling rate (0.0–1.0) |
exporter | string | 'console' | console, http, none |
endpoint | string | 'http://localhost:4318/v1/traces' | OTLP endpoint |
headers | array | [] | Extra HTTP headers for exporter |
Logging
| Key | Type | Default | Description |
|---|---|---|---|
stream | string|resource | 'php://stderr' | Output stream |
level | string | 'debug' | Minimum log level (PSR-3) |
json | bool | false | Enable JSON formatting |
pretty | bool | false | Pretty-print JSON |
Complete Application Example
use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;
use MonkeysLegion\Telemetry\Middleware\RequestMetricsMiddleware;
use MonkeysLegion\Telemetry\Middleware\RequestTracingMiddleware;
// ── Bootstrap ──────────────────────────────────────────────
Telemetry::init([
'metrics' => [
'driver' => 'statsd',
'namespace' => 'ecommerce',
'host' => 'statsd.internal',
],
'tracing' => [
'service_name' => 'order-service',
'exporter' => 'http',
'endpoint' => 'http://jaeger:4318/v1/traces',
'sample_rate' => 0.1,
],
'logging' => [
'json' => true,
'level' => 'info',
'stream' => '/var/log/app.log',
],
]);
// ── Middleware Pipeline ────────────────────────────────────
$app->pipe(new RequestTracingMiddleware(Telemetry::tracer()));
$app->pipe(new RequestMetricsMiddleware(Telemetry::metrics()));
// ── Application Code ───────────────────────────────────────
function placeOrder(Cart $cart): Order
{
return Telemetry::trace('place-order', function () use ($cart) {
Telemetry::log()->info('Order started', ['items' => count($cart)]);
// Validate stock (traced)
$stock = Telemetry::trace('check-inventory', function () use ($cart) {
$stop = Telemetry::timer('inventory_check_duration');
$result = InventoryService::check($cart->items());
$stop(['warehouse' => 'us-east']);
return $result;
});
// Charge payment (traced as CLIENT span)
$payment = Telemetry::trace(
'charge-payment',
fn () => PaymentGateway::charge($cart->total()),
SpanKind::CLIENT,
['payment.provider' => 'stripe'],
);
Telemetry::counter('orders_completed_total', 1, ['region' => 'us']);
Telemetry::log()->info('Order completed', ['order_id' => $payment->orderId]);
return new Order($stock, $payment);
});
}
Prometheus Metrics Endpoint
use MonkeysLegion\Telemetry\Metrics\PrometheusMetrics;
use Prometheus\Storage\Redis;
use Prometheus\RenderTextFormat;
// Shared Redis-backed storage for multi-process
$metrics = new PrometheusMetrics(
adapter: new Redis(['host' => 'redis']),
namespace: 'myapp',
);
// Exposition route: GET /metrics
$renderer = new RenderTextFormat();
$registry = $metrics->getRegistry();
header('Content-Type', $renderer->getMimeType());
echo $renderer->render($registry->getMetricFamilySamples());
Architecture
MonkeysLegion\Telemetry\
├── Telemetry # Static facade
├── Factory/
│ └── TelemetryFactory # Component factory
├── Metrics/
│ ├── MetricsInterface # Driver contract
│ ├── AbstractMetrics # Base class (timer, default labels)
│ ├── NullMetrics # No-op
│ ├── InMemoryMetrics # Testing
│ ├── PrometheusMetrics # Prometheus via promphp/prometheus_client_php
│ └── StatsDMetrics # UDP-based StatsD / DogStatsD
├── Tracing/
│ ├── TracerInterface # Tracer contract
│ ├── Tracer # W3C Trace Context implementation
│ ├── SpanInterface # Span contract
│ ├── Span # Span implementation
│ ├── SpanKind # Enum: INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER
│ ├── SpanStatus # Enum: UNSET, OK, ERROR
│ ├── NullTracer # No-op tracer
│ ├── NullSpan # No-op span
│ └── Exporter/
│ ├── SpanExporterInterface
│ ├── ConsoleExporter
│ ├── InMemoryExporter
│ └── JsonHttpExporter
├── Logging/
│ ├── TelemetryLoggerInterface # PSR-3 extension
│ ├── TelemetryLogger # Core logger with processors
│ ├── StreamLogger # File/stdout/stderr writer
│ ├── JsonFormatter # Structured JSON output
│ ├── ContextProvider # Context injection contract
│ └── TracingContextProvider # Auto-injects trace/span IDs
├── Middleware/
│ ├── RequestMetricsMiddleware # PSR-15 HTTP metrics
│ └── RequestTracingMiddleware # PSR-15 distributed tracing
├── Attribute/
│ ├── Traced # #[Traced] — automatic span creation
│ ├── Counted # #[Counted] — automatic call counting
│ └── Timed # #[Timed] — automatic duration measurement
└── Exception/
├── TelemetryException
├── MetricsException
└── TracingException
Testing
composer test # Run all tests
composer test:coverage # Generate HTML coverage report
composer analyse # PHPStan level 8
composer cs-check # Code style check
composer cs-fix # Auto-fix code style
License
MIT © MonkeysCloud