📦 Marketplace⭐ GitHub
Toolingv2.0

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.

PHP Version License Tests


What's New in 2.0

Areav1v2
PHP version8.1+8.4+readonly, final, enums, named args
MetricsBasic Prometheus onlyMetricsInterface — Prometheus, StatsD, InMemory, Null drivers
TracingFull W3C Trace Context — spans, events, exporters
LoggingPSR-3 TelemetryLogger with automatic trace correlation
MiddlewarePSR-15 RequestMetricsMiddleware + RequestTracingMiddleware
Attributes#[Traced], #[Counted], #[Timed]
FactoryManual constructionTelemetryFactory::create() — one-line bootstrapping
FacadeTelemetry::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 DriversNullMetrics, 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

DriverClassUse Case
nullNullMetricsDevelopment — zero overhead
memoryInMemoryMetricsTesting, single-request
prometheusPrometheusMetricsProduction — Prometheus scraping
statsdStatsDMetricsProduction — 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.

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

KindUse
SpanKind::INTERNALDefault — internal application logic
SpanKind::SERVERIncoming HTTP request handling
SpanKind::CLIENTOutgoing HTTP / DB / RPC call
SpanKind::PRODUCERMessage queue publish
SpanKind::CONSUMERMessage queue consume

Exporters

ExporterClassUse Case
consoleConsoleExporterLocal development / debugging
httpJsonHttpExporterOTLP/HTTP — Jaeger, Tempo, Zipkin
InMemoryExporterUnit 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:

MetricTypeLabels
http_requests_totalCountermethod, route, status
http_request_duration_secondsHistogrammethod, route, status
http_requests_in_progressGauge

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:

AttributeExample
http.methodGET
http.urlhttps://api.example.com/users/42
http.target/users/42
http.status_code200
http.hostapi.example.com
net.peer.ip192.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

KeyTypeDefaultDescription
driverstring'null'null, memory, prometheus, statsd
namespacestring'app'Metric name prefix
hoststring'127.0.0.1'StatsD host
portint8125StatsD port
dogstatsdboolfalseEnable DogStatsD tag format
sample_ratefloat1.0StatsD sampling rate (0.0–1.0)
prometheus_adapterAdapterInMemoryPrometheus storage adapter

Tracing

KeyTypeDefaultDescription
enabledbooltrueEnable/disable tracing
service_namestring'app'Service identifier
sample_ratefloat1.0Trace sampling rate (0.0–1.0)
exporterstring'console'console, http, none
endpointstring'http://localhost:4318/v1/traces'OTLP endpoint
headersarray[]Extra HTTP headers for exporter

Logging

KeyTypeDefaultDescription
streamstring|resource'php://stderr'Output stream
levelstring'debug'Minimum log level (PSR-3)
jsonboolfalseEnable JSON formatting
prettyboolfalsePretty-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