monkeyscloud/monkeyslegion-telemetry

Comprehensive telemetry package with PSR-3 logging, metrics (Prometheus/StatsD), and distributed tracing for MonkeysLegion projects

Installs: 179

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 1

Forks: 0

pkg:composer/monkeyscloud/monkeyslegion-telemetry

2.0.0 2025-12-09 02:57 UTC

This package is auto-updated.

Last update: 2025-12-10 23:46:17 UTC


README

PHP Version License

A comprehensive telemetry package for PHP 8.4+ providing:

  • Metrics - Counter, Gauge, Histogram, Summary with Prometheus and StatsD adapters
  • Distributed Tracing - W3C Trace Context compatible spans and trace propagation
  • Logging - PSR-3 compatible logging with automatic trace correlation

Table of Contents

Installation

composer require monkeyscloud/monkeyslegion-telemetry

Optional Dependencies

# For Prometheus support
composer require promphp/prometheus_client_php

# For PSR-15 middleware
composer require psr/http-message psr/http-server-middleware

Quick Start

<?php

use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;

// Initialize once (e.g., in bootstrap.php)
Telemetry::init([
    'metrics' => [
        'driver' => 'statsd',      // 'null', 'memory', 'statsd', 'prometheus'
        'host' => '127.0.0.1',
        'port' => 8125,
        'namespace' => 'myapp',
    ],
    'tracing' => [
        'enabled' => true,
        'service_name' => 'myapp',
        'sample_rate' => 1.0,
        'exporter' => 'console',   // 'console', 'http', 'none'
    ],
    'logging' => [
        'stream' => 'php://stderr',
        'level' => 'debug',
        'json' => true,
    ],
]);

// Record metrics
Telemetry::counter('http_requests_total', 1, ['method' => 'GET', 'status' => '200']);
Telemetry::histogram('http_request_duration_seconds', 0.123, ['endpoint' => '/api/users']);
Telemetry::gauge('active_connections', 42);

// Distributed tracing
$result = Telemetry::trace('fetch-user', function () use ($userId) {
    return $this->userRepository->find($userId);
}, SpanKind::CLIENT, ['user.id' => $userId]);

// Logging with trace correlation
Telemetry::log()->info('User fetched', ['user_id' => $userId]);

Metrics

Counter (monotonically increasing)

Counters are cumulative metrics that only increase (or reset to zero on restart).

use MonkeysLegion\Telemetry\Telemetry;

// Simple increment
Telemetry::counter('http_requests_total');

// Increment by value with labels
Telemetry::counter('http_requests_total', 1, [
    'method' => 'POST',
    'endpoint' => '/api/users',
    'status' => '201',
]);

// Count errors
Telemetry::counter('errors_total', 1, ['type' => 'validation']);

// Count multiple at once
Telemetry::counter('bytes_processed', 1024);

Gauge (point-in-time value)

Gauges represent a single numerical value that can go up or down.

// Set absolute value
Telemetry::gauge('queue_size', 42);
Telemetry::gauge('memory_usage_bytes', memory_get_usage());
Telemetry::gauge('active_connections', $connectionPool->count());

// With labels
Telemetry::gauge('cache_items', 1500, ['cache' => 'redis']);
Telemetry::gauge('temperature_celsius', 23.5, ['location' => 'server_room']);

Histogram (distribution/timing)

Histograms sample observations and count them in configurable buckets.

// Record a value (e.g., response time in seconds)
Telemetry::histogram('http_request_duration_seconds', 0.125);

// With labels
Telemetry::histogram('db_query_duration_seconds', 0.045, [
    'query_type' => 'select',
    'table' => 'users',
]);

// Custom buckets for specific use cases
Telemetry::histogram('file_size_bytes', 1024, [], [100, 1000, 10000, 100000, 1000000]);

// Response size tracking
Telemetry::histogram('http_response_size_bytes', strlen($response->getBody()));

Timer Helper

Convenient way to measure operation duration.

// Start timer
$stopTimer = Telemetry::timer('operation_duration_seconds');

// ... do work ...
$result = $this->heavyOperation();
sleep(1); // simulate work

// Stop and record (returns duration in seconds)
$duration = $stopTimer(['operation' => 'heavy_task']);
// $duration = 1.002345

// Use in try/finally for guaranteed recording
$stop = Telemetry::timer('risky_operation_seconds');
try {
    $this->riskyOperation();
} finally {
    $stop(['status' => 'completed']);
}

Distributed Tracing

Simple Tracing with Callback

The trace() method automatically creates a span, handles exceptions, and records duration.

use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;

// Basic trace
$user = Telemetry::trace('fetch-user', function () use ($userId) {
    return $this->userRepository->find($userId);
});

// With span kind and attributes
$user = Telemetry::trace('fetch-user', function () use ($userId) {
    return $this->userRepository->find($userId);
}, SpanKind::CLIENT, [
    'user.id' => $userId,
    'db.system' => 'mysql',
]);

// Nested traces (automatic parent-child relationship)
$result = Telemetry::trace('process-order', function () use ($order) {
    
    // Child span 1
    $inventory = Telemetry::trace('check-inventory', function () use ($order) {
        return $this->inventory->check($order->items);
    }, SpanKind::CLIENT);
    
    // Child span 2
    $payment = Telemetry::trace('process-payment', function () use ($order) {
        return $this->payment->charge($order);
    }, SpanKind::CLIENT);
    
    // Child span 3
    Telemetry::trace('send-confirmation', function () use ($order) {
        $this->mailer->sendOrderConfirmation($order);
    }, SpanKind::PRODUCER);
    
    return ['inventory' => $inventory, 'payment' => $payment];
});

Manual Span Management

For complex scenarios where you need full control.

use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;
use MonkeysLegion\Telemetry\Tracing\SpanStatus;

$span = Telemetry::startSpan('complex-operation', SpanKind::INTERNAL);
$span->setAttribute('operation.type', 'batch');
$span->setAttribute('batch.size', 100);

try {
    // Add events for important moments
    $span->addEvent('validation-started');
    $this->validate($data);
    $span->addEvent('validation-completed');
    
    $span->addEvent('processing-started', ['items' => count($items)]);
    
    foreach ($items as $index => $item) {
        $this->process($item);
        
        // Add event for milestones
        if ($index % 100 === 0) {
            $span->addEvent('progress', ['processed' => $index]);
        }
    }
    
    $span->addEvent('processing-completed');
    $span->setStatus(SpanStatus::OK);
    
} catch (ValidationException $e) {
    $span->recordException($e);
    $span->setStatus(SpanStatus::ERROR, 'Validation failed');
    throw $e;
    
} catch (Throwable $e) {
    $span->recordException($e);
    $span->setStatus(SpanStatus::ERROR, $e->getMessage());
    throw $e;
    
} finally {
    $span->end();
    Telemetry::flush(); // Send to exporter
}

Get Current Trace Context

Access trace information for correlation.

// Get current trace ID (for logging, external calls)
$traceId = Telemetry::traceId();
// "a1b2c3d4e5f67890a1b2c3d4e5f67890"

// Get active span for adding attributes
$span = Telemetry::activeSpan();
if ($span) {
    $span->setAttribute('custom.attribute', 'value');
    $span->addEvent('checkpoint', ['data' => 'info']);
}

// Inject trace context into outgoing HTTP request
$tracer = Telemetry::tracer();
$headers = $tracer->inject([]);
// $headers = ['traceparent' => '00-{trace_id}-{span_id}-01']

// Make HTTP call with trace propagation
$response = $httpClient->request('GET', $url, [
    'headers' => $headers,
]);

Logging with Trace Correlation

Logs are automatically enriched with trace context.

use MonkeysLegion\Telemetry\Telemetry;

$logger = Telemetry::log();

// Basic logging (trace_id and span_id auto-injected when in a trace)
$logger->debug('Starting operation', ['step' => 1]);
$logger->info('User logged in', ['user_id' => 123, 'ip' => '192.168.1.1']);
$logger->notice('Configuration changed', ['key' => 'cache_ttl', 'value' => 3600]);
$logger->warning('Rate limit approaching', ['current' => 95, 'limit' => 100]);
$logger->error('Payment failed', ['order_id' => 456, 'reason' => 'insufficient_funds']);
$logger->critical('Database connection lost', ['host' => 'db-master']);
$logger->alert('Disk space critical', ['free_percent' => 2]);
$logger->emergency('System shutdown initiated', ['reason' => 'hardware_failure']);

// With exception
try {
    $this->riskyOperation();
} catch (Exception $e) {
    $logger->error('Operation failed', [
        'exception' => $e,
        'context' => 'processing order',
        'order_id' => $orderId,
    ]);
}

// Set default context for all logs
$logger->setDefaultContext([
    'service' => 'user-api',
    'version' => '2.0.0',
    'environment' => 'production',
]);

// Add custom processor
$logger->addProcessor(function (array $record): array {
    $record['context']['hostname'] = gethostname();
    $record['context']['pid'] = getmypid();
    $record['context']['memory_mb'] = round(memory_get_usage() / 1024 / 1024, 2);
    return $record;
});

JSON Output Example:

{
  "timestamp": "2025-01-15T10:30:00+00:00",
  "level": "INFO",
  "message": "User logged in",
  "user_id": 123,
  "ip": "192.168.1.1",
  "trace_id": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
  "span_id": "1234567890abcdef",
  "service": "user-api",
  "hostname": "web-server-01"
}

Direct Component Usage

InMemoryMetrics (Testing)

Perfect for unit tests and single-request metrics.

use MonkeysLegion\Telemetry\Metrics\InMemoryMetrics;

$metrics = new InMemoryMetrics('test');

// Record metrics
$metrics->counter('requests', 1, ['method' => 'GET']);
$metrics->counter('requests', 4, ['method' => 'GET']);
$metrics->counter('requests', 2, ['method' => 'POST']);

$metrics->gauge('temperature', 23.5);

$metrics->histogram('duration', 0.1);
$metrics->histogram('duration', 0.2);
$metrics->histogram('duration', 0.15);

// Assert in tests
$this->assertEquals(5.0, $metrics->getCounter('requests', ['method' => 'GET']));
$this->assertEquals(2.0, $metrics->getCounter('requests', ['method' => 'POST']));
$this->assertEquals(23.5, $metrics->getGauge('temperature'));

// Get histogram statistics
$stats = $metrics->getHistogramStats('duration');
$this->assertEquals(3, $stats['count']);
$this->assertEquals(0.45, $stats['sum']);
$this->assertEquals(0.15, $stats['mean']);
$this->assertEquals(0.1, $stats['min']);
$this->assertEquals(0.2, $stats['max']);
$this->assertArrayHasKey(50, $stats['percentiles']); // p50
$this->assertArrayHasKey(99, $stats['percentiles']); // p99

// Get all metrics
$all = $metrics->getMetrics();
// ['counters' => [...], 'gauges' => [...], 'histograms' => [...], 'summaries' => [...]]

// Export as Prometheus format
echo $metrics->exportPrometheus();
/*
# TYPE test_requests counter
test_requests{method="GET"} 5
test_requests{method="POST"} 2
# TYPE test_temperature gauge
test_temperature 23.5
# TYPE test_duration histogram
test_duration_count 3
test_duration_sum 0.45
...
*/

// Reset for next test
$metrics->reset();

PrometheusMetrics

Production-ready Prometheus integration.

use MonkeysLegion\Telemetry\Metrics\PrometheusMetrics;
use Prometheus\Storage\Redis;
use Prometheus\Storage\APC;
use Prometheus\Storage\InMemory;

// With Redis storage (recommended for production)
$adapter = new Redis([
    'host' => '127.0.0.1',
    'port' => 6379,
    'password' => null,
    'database' => 0,
]);
$metrics = new PrometheusMetrics($adapter, 'myapp');

// With APC storage (single server)
$metrics = new PrometheusMetrics(new APC(), 'myapp');

// In-memory (single request, testing)
$metrics = new PrometheusMetrics(new InMemory(), 'myapp');

// Record metrics
$metrics->counter('http_requests_total', 1, ['method' => 'GET', 'status' => '200']);
$metrics->counter('http_requests_total', 1, ['method' => 'POST', 'status' => '201']);
$metrics->gauge('goroutines', 42);
$metrics->histogram('http_request_duration_seconds', 0.125, ['handler' => '/api/users']);

// Set default labels (added to all metrics)
$metrics->setDefaultLabels([
    'instance' => gethostname(),
    'job' => 'api-server',
]);

// Render for /metrics endpoint
header('Content-Type: text/plain; version=0.0.4');
echo $metrics->render();

// Wipe all metrics (useful for testing)
$metrics->wipe();

StatsDMetrics

StatsD/DogStatsD/Telegraf support.

use MonkeysLegion\Telemetry\Metrics\StatsDMetrics;

// Standard StatsD
$metrics = new StatsDMetrics(
    host: '127.0.0.1',
    port: 8125,
    namespace: 'myapp',
);

// DogStatsD (with tags support)
$metrics = new StatsDMetrics(
    host: 'localhost',
    port: 8125,
    namespace: 'myapp',
    dogstatsd: true,
    sampleRate: 1.0,
);

// With sampling (reduce network traffic)
$metrics = new StatsDMetrics(
    host: 'statsd.internal',
    port: 8125,
    namespace: 'myapp',
    dogstatsd: true,
    sampleRate: 0.5,  // Only send 50% of metrics
);

// Record metrics
$metrics->counter('events', 1, ['type' => 'click']);
$metrics->gauge('queue_depth', 42, ['queue' => 'emails']);
$metrics->histogram('response_time', 0.123, ['endpoint' => '/api']);

// StatsD-specific: set metric (unique values)
$metrics->set('unique_users', $userId);

// StatsD-specific: increment/decrement gauge
$metrics->gaugeIncrement('connections', 1);
$metrics->gaugeIncrement('connections', -1);

// Close connection when done (optional, auto-closes on destruct)
$metrics->close();

Tracer with HTTP Exporter

Send traces to Jaeger, Zipkin, or any OTLP-compatible backend.

use MonkeysLegion\Telemetry\Tracing\Tracer;
use MonkeysLegion\Telemetry\Tracing\SpanKind;
use MonkeysLegion\Telemetry\Tracing\Exporter\JsonHttpExporter;
use MonkeysLegion\Telemetry\Tracing\Exporter\ConsoleExporter;
use MonkeysLegion\Telemetry\Tracing\Exporter\InMemoryExporter;

// Create tracer
$tracer = new Tracer(
    serviceName: 'my-service',
    sampleRate: 1.0,  // 100% sampling
);

// Add OTLP HTTP exporter (Jaeger, Tempo, etc.)
$tracer->addExporter(new JsonHttpExporter(
    endpoint: 'http://localhost:4318/v1/traces',
    headers: [
        'Authorization' => 'Bearer your-token',
    ],
    timeout: 5,
    async: true,  // Fire and forget (non-blocking)
));

// Or console exporter for debugging
$tracer->addExporter(new ConsoleExporter(
    stream: STDERR,
    prettyPrint: true,
));

// Or in-memory for testing
$inMemory = new InMemoryExporter();
$tracer->addExporter($inMemory);

// Use the tracer
$result = $tracer->trace('operation', function () {
    return doWork();
}, SpanKind::INTERNAL, ['key' => 'value']);

// Flush all spans to exporters
$tracer->flush();

// For testing: get exported spans
$spans = $inMemory->getSpans();
$spansAsArrays = $inMemory->getSpansAsArrays();

PSR-15 Middleware

Automatic HTTP Metrics

use MonkeysLegion\Telemetry\Middleware\RequestMetricsMiddleware;
use MonkeysLegion\Telemetry\Metrics\PrometheusMetrics;

$metrics = new PrometheusMetrics($adapter, 'myapp');

$middleware = new RequestMetricsMiddleware(
    metrics: $metrics,
    includeRoute: true,   // Add route pattern as label
    includeMethod: true,  // Add HTTP method as label
    includeStatus: true,  // Add status code as label
);

// Add to your middleware stack (Slim, Mezzio, etc.)
$app->add($middleware);

// Automatically records for every request:
// - http_requests_total{method="GET",route="/users/{id}",status="200"} counter
// - http_request_duration_seconds{...} histogram
// - http_requests_in_progress gauge

Automatic Request Tracing

use MonkeysLegion\Telemetry\Middleware\RequestTracingMiddleware;
use MonkeysLegion\Telemetry\Tracing\Tracer;

$tracer = new Tracer('my-api');

$middleware = new RequestTracingMiddleware(
    tracer: $tracer,
    propagateContext: true,  // Add traceparent header to response
);

$app->add($middleware);

// Automatically for every request:
// - Extracts trace context from incoming 'traceparent' header
// - Creates root span named "{METHOD} {PATH}"
// - Sets HTTP attributes (method, url, status_code, user_agent, etc.)
// - Records exceptions
// - Injects trace context into response headers

// Access span in your handlers:
$span = $request->getAttribute('telemetry.span');
$traceId = $request->getAttribute('telemetry.trace_id');

PHP 8 Attributes

Use attributes for declarative instrumentation (requires AOP integration).

use MonkeysLegion\Telemetry\Attribute\Timed;
use MonkeysLegion\Telemetry\Attribute\Counted;
use MonkeysLegion\Telemetry\Attribute\Traced;
use MonkeysLegion\Telemetry\Tracing\SpanKind;

class UserService
{
    // Automatic timing + counting + tracing
    #[Timed('user_fetch_duration_seconds')]
    #[Counted('user_fetch_total')]
    #[Traced('UserService::fetchUser', kind: SpanKind::CLIENT)]
    public function fetchUser(int $id): User
    {
        return $this->repository->find($id);
    }

    // Just timing with auto-generated name
    #[Timed]  // Creates: user_service_save_user_duration
    public function saveUser(User $user): void
    {
        $this->repository->save($user);
    }

    // Tracing with custom attributes
    #[Traced(attributes: ['db.system' => 'mysql', 'db.operation' => 'query'])]
    public function searchUsers(string $query): array
    {
        return $this->repository->search($query);
    }

    // Counting with labels
    #[Counted('api_calls_total', labels: ['service' => 'external'])]
    public function callExternalApi(): Response
    {
        return $this->httpClient->get('https://api.example.com');
    }
}

Complete Application Example

Bootstrap

<?php
// bootstrap.php

use MonkeysLegion\Telemetry\Telemetry;

// Load configuration from environment
$config = [
    'metrics' => [
        'driver' => getenv('METRICS_DRIVER') ?: 'prometheus',
        'namespace' => getenv('METRICS_NAMESPACE') ?: 'myapp',
    ],
    'tracing' => [
        'enabled' => getenv('TRACING_ENABLED') !== 'false',
        'service_name' => getenv('SERVICE_NAME') ?: 'myapp-api',
        'sample_rate' => (float) (getenv('TRACING_SAMPLE_RATE') ?: 1.0),
        'exporter' => getenv('TRACING_EXPORTER') ?: 'http',
        'endpoint' => getenv('OTLP_ENDPOINT') ?: 'http://localhost:4318/v1/traces',
    ],
    'logging' => [
        'stream' => getenv('LOG_STREAM') ?: 'php://stderr',
        'json' => getenv('LOG_FORMAT') === 'json',
        'level' => getenv('LOG_LEVEL') ?: 'info',
    ],
];

// Initialize telemetry
Telemetry::init($config);

// Set default labels for all metrics
Telemetry::metrics()->setDefaultLabels([
    'service' => $config['tracing']['service_name'],
    'instance' => gethostname(),
]);

// Register shutdown to flush traces
register_shutdown_function(function () {
    Telemetry::flush();
});

// Log startup
Telemetry::log()?->info('Application started', [
    'php_version' => PHP_VERSION,
    'environment' => getenv('APP_ENV') ?: 'development',
]);

Controller Example

<?php
// UserController.php

use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;

class UserController
{
    public function __construct(
        private UserRepository $userRepo,
        private CacheInterface $cache,
    ) {}

    public function show(int $id): Response
    {
        $logger = Telemetry::log();
        
        return Telemetry::trace('UserController::show', function () use ($id, $logger) {
            
            $logger?->info('Fetching user', ['user_id' => $id]);
            
            // Try cache first
            $cacheKey = "user:{$id}";
            $user = Telemetry::trace('cache-lookup', function () use ($cacheKey) {
                return $this->cache->get($cacheKey);
            }, SpanKind::CLIENT, ['cache.key' => $cacheKey]);
            
            if ($user) {
                Telemetry::counter('cache_hits_total', 1, ['type' => 'user']);
                $logger?->debug('Cache hit', ['user_id' => $id]);
            } else {
                Telemetry::counter('cache_misses_total', 1, ['type' => 'user']);
                
                // Fetch from database
                $user = Telemetry::trace('database-fetch', function () use ($id) {
                    return $this->userRepo->find($id);
                }, SpanKind::CLIENT, ['db.operation' => 'select']);
                
                if ($user) {
                    // Store in cache
                    Telemetry::trace('cache-store', function () use ($cacheKey, $user) {
                        $this->cache->set($cacheKey, $user, 3600);
                    }, SpanKind::CLIENT);
                }
            }
            
            if (!$user) {
                Telemetry::counter('user_not_found_total');
                $logger?->warning('User not found', ['user_id' => $id]);
                
                return new Response(404, json_encode([
                    'error' => 'User not found',
                    'trace_id' => Telemetry::traceId(),
                ]));
            }
            
            Telemetry::counter('user_fetched_total', 1, ['status' => 'success']);
            
            return new Response(200, json_encode([
                'data' => $user,
                'meta' => ['trace_id' => Telemetry::traceId()],
            ]));
            
        }, SpanKind::SERVER, ['user.id' => $id]);
    }
    
    public function create(Request $request): Response
    {
        $logger = Telemetry::log();
        $stopTimer = Telemetry::timer('user_creation_duration_seconds');
        
        try {
            return Telemetry::trace('UserController::create', function () use ($request, $logger) {
                
                $data = json_decode($request->getBody(), true);
                
                // Validate
                Telemetry::trace('validation', function () use ($data) {
                    $this->validator->validate($data, UserSchema::class);
                });
                
                // Create user
                $user = Telemetry::trace('create-user', function () use ($data) {
                    return $this->userRepo->create($data);
                }, SpanKind::CLIENT);
                
                Telemetry::counter('users_created_total');
                $logger?->info('User created', ['user_id' => $user->id]);
                
                return new Response(201, json_encode($user));
                
            }, SpanKind::SERVER);
            
        } catch (ValidationException $e) {
            Telemetry::counter('validation_errors_total', 1, ['entity' => 'user']);
            $logger?->warning('Validation failed', ['errors' => $e->getErrors()]);
            
            return new Response(400, json_encode(['errors' => $e->getErrors()]));
            
        } finally {
            $stopTimer(['operation' => 'create_user']);
        }
    }
}

Service Example

<?php
// PaymentService.php

use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Tracing\SpanKind;

class PaymentService
{
    public function processPayment(Order $order): PaymentResult
    {
        $logger = Telemetry::log();
        
        return Telemetry::trace('PaymentService::processPayment', function () use ($order, $logger) {
            
            $span = Telemetry::activeSpan();
            $span?->setAttributes([
                'order.id' => $order->id,
                'order.total' => $order->total,
                'order.currency' => $order->currency,
            ]);
            
            $logger?->info('Processing payment', [
                'order_id' => $order->id,
                'amount' => $order->total,
            ]);
            
            // Call payment gateway
            $result = Telemetry::trace('payment-gateway-call', function () use ($order) {
                return $this->gateway->charge([
                    'amount' => $order->total,
                    'currency' => $order->currency,
                    'source' => $order->paymentMethod,
                ]);
            }, SpanKind::CLIENT, [
                'http.method' => 'POST',
                'http.url' => 'https://api.stripe.com/v1/charges',
                'peer.service' => 'stripe',
            ]);
            
            if ($result->success) {
                Telemetry::counter('payments_total', 1, ['status' => 'success']);
                Telemetry::histogram('payment_amount', $order->total, [
                    'currency' => $order->currency,
                ]);
                
                $logger?->info('Payment successful', [
                    'order_id' => $order->id,
                    'transaction_id' => $result->transactionId,
                ]);
            } else {
                Telemetry::counter('payments_total', 1, ['status' => 'failed']);
                Telemetry::counter('payment_failures_total', 1, [
                    'reason' => $result->errorCode,
                ]);
                
                $logger?->error('Payment failed', [
                    'order_id' => $order->id,
                    'error' => $result->errorMessage,
                    'code' => $result->errorCode,
                ]);
            }
            
            return $result;
            
        }, SpanKind::INTERNAL, ['payment.provider' => 'stripe']);
    }
}

Prometheus /metrics Endpoint

<?php
// public/metrics.php

use MonkeysLegion\Telemetry\Metrics\PrometheusMetrics;
use Prometheus\Storage\Redis;

// Use same storage as your application
$adapter = new Redis([
    'host' => getenv('REDIS_HOST') ?: '127.0.0.1',
    'port' => (int) (getenv('REDIS_PORT') ?: 6379),
]);

$metrics = new PrometheusMetrics($adapter, 'myapp');

// Add some runtime metrics
$metrics->gauge('php_info', 1, [
    'version' => PHP_VERSION,
]);

// Output Prometheus format
header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
echo $metrics->render();

Configuration Reference

Metrics Configuration

Option Type Default Description
driver string 'null' null, memory, statsd, prometheus
namespace string 'app' Metric name prefix
host string '127.0.0.1' StatsD host
port int 8125 StatsD port
dogstatsd bool false Enable DogStatsD tags
sample_rate float 1.0 Sampling rate (0.0-1.0)
prometheus_adapter Adapter InMemory Prometheus storage adapter

Tracing Configuration

Option Type Default Description
enabled bool true Enable/disable tracing
service_name string 'app' Service name for spans
sample_rate float 1.0 Sampling rate (0.0-1.0)
exporter string 'console' console, http, none
endpoint string - OTLP HTTP endpoint URL
headers array [] Additional HTTP headers

Logging Configuration

Option Type Default Description
stream string|resource 'php://stderr' Log output destination
level string 'debug' Minimum log level
json bool false Use JSON format
pretty bool false Pretty print JSON

Environment Variables Example

# Metrics
METRICS_DRIVER=prometheus
METRICS_NAMESPACE=myapp

# Tracing
TRACING_ENABLED=true
TRACING_SAMPLE_RATE=0.1
TRACING_EXPORTER=http
OTLP_ENDPOINT=http://tempo:4318/v1/traces
SERVICE_NAME=user-api

# Logging
LOG_STREAM=php://stderr
LOG_FORMAT=json
LOG_LEVEL=info

# Redis (for Prometheus storage)
REDIS_HOST=redis
REDIS_PORT=6379

Testing

# Run tests
composer test

# Run tests with coverage
composer test:coverage

# Static analysis
composer analyse

Testing with InMemoryMetrics

use MonkeysLegion\Telemetry\Telemetry;
use MonkeysLegion\Telemetry\Metrics\InMemoryMetrics;
use PHPUnit\Framework\TestCase;

class MyServiceTest extends TestCase
{
    private InMemoryMetrics $metrics;
    
    protected function setUp(): void
    {
        $this->metrics = new InMemoryMetrics('test');
        Telemetry::setMetrics($this->metrics);
    }
    
    protected function tearDown(): void
    {
        Telemetry::reset();
    }
    
    public function testItRecordsMetrics(): void
    {
        $service = new MyService();
        $service->doSomething();
        
        $this->assertEquals(1.0, $this->metrics->getCounter('operations_total'));
        $this->assertNotNull($this->metrics->getHistogramStats('operation_duration'));
    }
}

License

MIT License. See LICENSE for details.

🤝 Contributing

  1. Fork 🍴
  2. Create a feature branch 🌱
  3. Submit a PR 🚀

Happy hacking with MonkeysLegion! 🎉

Contributors

Jorge Peraza
Jorge Peraza
Amanar Marouane
Amanar Marouane