tracekit/php-apm

TraceKit APM for PHP - Framework-agnostic distributed tracing and code monitoring

Installs: 10

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/tracekit/php-apm

v1.0.3 2025-11-21 20:20 UTC

This package is auto-updated.

Last update: 2025-12-21 20:29:43 UTC


README

Framework-agnostic distributed tracing and performance monitoring for any PHP application.

Packagist Version Downloads License

Features

  • Framework Agnostic - Works with any PHP application (vanilla PHP, Symfony, Slim, etc.)
  • OpenTelemetry Standard - Built on OpenTelemetry for industry-standard tracing
  • Automatic Context Propagation - Child spans automatically inherit from parent
  • Manual Instrumentation - Full control over what and how you trace
  • HTTP Request Tracing - Track requests, database queries, and external API calls
  • Error Tracking - Capture exceptions with full context
  • Code Monitoring - Live debugging with breakpoints and variable inspection
  • Low Overhead - Minimal performance impact

Installation

composer require tracekit/php-apm

Quick Start

Basic Usage

<?php

require 'vendor/autoload.php';

use TraceKit\PHP\TracekitClient;

// Initialize TraceKit
$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'my-php-app',
    'endpoint' => 'https://app.tracekit.dev/v1/traces',
]);

// Start a trace (returns array with span and scope)
$span = $tracekit->startTrace('process-request', [
    'http.method' => $_SERVER['REQUEST_METHOD'],
    'http.url' => $_SERVER['REQUEST_URI'],
]);

try {
    // Your application logic here
    processRequest();

    $tracekit->endSpan($span, [
        'http.status_code' => 200,
    ]);
} catch (\Exception $e) {
    $tracekit->recordException($span, $e);
    $tracekit->endSpan($span, [], 'ERROR');
    throw $e;
}

// Important: flush traces before exit
$tracekit->flush();

Code Monitoring (Live Debugging)

TraceKit includes production-safe code monitoring for live debugging without redeployment.

Enable Code Monitoring

<?php

require 'vendor/autoload.php';

use TraceKit\PHP\TracekitClient;

// Enable code monitoring
$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'my-php-app',
    'endpoint' => 'https://app.tracekit.dev/v1/traces',
    'code_monitoring_enabled' => true,
    'code_monitoring_max_depth' => 3,      // Nested array/object depth
    'code_monitoring_max_string' => 1000,  // Truncate long strings
]);

Add Debug Points

Add checkpoints anywhere in your code to capture variable state and stack traces:

<?php

class CheckoutService
{
    private $tracekit;

    public function __construct($tracekit)
    {
        $this->tracekit = $tracekit;
    }

    public function processPayment($userId, $cart)
    {
        // Automatic snapshot capture with label
        $this->tracekit->captureSnapshot('checkout-validation', [
            'user_id' => $userId,
            'cart_items' => count($cart['items'] ?? []),
            'total_amount' => $cart['total'] ?? 0,
        ]);

        try {
            $result = $this->chargeCard($cart['total'], $userId);

            // Another checkpoint
            $this->tracekit->captureSnapshot('payment-success', [
                'user_id' => $userId,
                'payment_id' => $result['payment_id'],
                'amount' => $result['amount'],
            ]);

            return $result;

        } catch (Exception $e) {
            // Automatic error capture
            $this->tracekit->captureSnapshot('payment-error', [
                'user_id' => $userId,
                'amount' => $cart['total'],
                'error' => $e->getMessage(),
            ]);

            throw $e;
        }
    }

    private function chargeCard($amount, $userId)
    {
        // Simulate payment processing
        if ($amount > 1000) {
            throw new Exception('Amount exceeds limit');
        }

        return [
            'payment_id' => 'pay_' . uniqid(),
            'amount' => $amount,
            'status' => 'succeeded',
        ];
    }
}

// Usage
$checkout = new CheckoutService($tracekit);
$result = $checkout->processPayment(123, ['total' => 99.99, 'items' => ['item1']]);

Manual Breakpoint Polling

Since PHP doesn't have built-in background task scheduling, you need to poll for breakpoints manually:

// Option 1: Poll on every Nth request
if (rand(1, 100) <= 5) { // 5% of requests
    $tracekit->pollBreakpoints();
}

// Option 2: Use a cron job
// */1 * * * * php /path/to/poll-breakpoints.php

// poll-breakpoints.php
require 'vendor/autoload.php';
$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'my-php-app',
    'code_monitoring_enabled' => true,
]);
$tracekit->pollBreakpoints();

Automatic Breakpoint Management

  • Auto-Registration: First call to captureSnapshot() automatically creates breakpoints in TraceKit
  • Smart Matching: Breakpoints match by function name + label (stable across code changes)
  • Manual Polling: You must call pollBreakpoints() periodically to fetch active breakpoints
  • Production Safe: No performance impact when breakpoints are inactive

What Gets Captured

Snapshots include:

  • Variables: Local variables at capture point
  • Stack Trace: Full call stack with file/line numbers
  • Request Context: HTTP method, URL, headers, query params (when available)
  • Execution Time: When the snapshot was captured

Framework Integration Examples

Slim Framework

<?php

require 'vendor/autoload.php';

use TraceKit\PHP\TracekitClient;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'slim-app',
    'code_monitoring_enabled' => true,
]);

$app->post('/checkout', function ($request, $response) use ($tracekit) {
    $data = $request->getParsedBody();

    // Poll breakpoints occasionally
    if (rand(1, 20) === 1) { // 5% chance
        $tracekit->pollBreakpoints();
    }

    // Capture snapshot
    $tracekit->captureSnapshot('checkout-start', [
        'user_id' => $data['user_id'],
        'amount' => $data['amount'],
    ]);

    // Process payment...
    $result = ['payment_id' => 'pay_' . uniqid()];

    return $response->withJson($result);
});

$app->run();

Symfony Controller

<?php

namespace App\Controller;

use TraceKit\PHP\TracekitClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class PaymentController
{
    private $tracekit;

    public function __construct()
    {
        $this->tracekit = new TracekitClient([
            'api_key' => getenv('TRACEKIT_API_KEY'),
            'service_name' => 'symfony-app',
            'code_monitoring_enabled' => true,
        ]);
    }

    public function checkout(Request $request): JsonResponse
    {
        // Poll occasionally (you could also use a cron job)
        if (rand(1, 20) === 1) {
            $this->tracekit->pollBreakpoints();
        }

        $data = json_decode($request->getContent(), true);

        $this->tracekit->captureSnapshot('checkout-validation', [
            'user_id' => $data['user_id'],
            'cart_total' => $data['cart']['total'],
        ]);

        // Process payment...
        $result = $this->processPayment($data);

        $this->tracekit->captureSnapshot('checkout-complete', [
            'user_id' => $data['user_id'],
            'payment_id' => $result['payment_id'],
        ]);

        return new JsonResponse($result);
    }

    private function processPayment(array $data): array
    {
        // Payment logic here...
        return ['payment_id' => 'pay_' . uniqid()];
    }
}

Configuration

Basic Configuration

$tracekit = new TracekitClient([
    // Required: Your TraceKit API key
    'api_key' => getenv('TRACEKIT_API_KEY'),

    // Optional: Service name (default: 'php-app')
    'service_name' => 'my-service',

    // Optional: TraceKit endpoint (default: 'https://app.tracekit.dev/v1/traces')
    'endpoint' => 'https://app.tracekit.dev/v1/traces',

    // Optional: Enable/disable tracing (default: true)
    'enabled' => getenv('APP_ENV') === 'production',

    // Optional: Sample rate 0.0-1.0 (default: 1.0 = 100%)
    'sample_rate' => 0.5, // Trace 50% of requests

    // Optional: Enable live code debugging (default: false)
    'code_monitoring_enabled' => true,
    'code_monitoring_max_depth' => 3,      // Nested array/object depth
    'code_monitoring_max_string' => 1000,  // Truncate long strings

    // Optional: Map hostnames to service names for service graph
    'service_name_mappings' => [
        'localhost:8082' => 'payment-service',
        'localhost:8083' => 'user-service',
    ],
]);

Environment Variables

Create a .env file or set these environment variables:

TRACEKIT_API_KEY=ctxio_your_generated_api_key_here
TRACEKIT_ENDPOINT=https://app.tracekit.dev/v1/traces
TRACEKIT_SERVICE_NAME=my-php-app

Automatic HTTP Client Instrumentation

TraceKit provides instrumentation for outgoing HTTP calls to create service dependency graphs.

How It Works

When your service makes an HTTP request:

  1. ✅ TraceKit creates a CLIENT span for the outgoing request
  2. ✅ Trace context is injected into request headers (traceparent)
  3. peer.service attribute is set based on the target hostname
  4. ✅ The receiving service creates a SERVER span linked to your CLIENT span
  5. ✅ TraceKit maps the dependency: YourService → TargetService

Supported HTTP Clients

cURL (with wrapper)

$ch = curl_init("http://payment-service/charge");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['amount' => 99.99]));

// Wrap curl_exec with TraceKit instrumentation
$instrumentation = $tracekit->getHttpClientInstrumentation();
$result = $instrumentation->wrapCurlExec($ch);

curl_close($ch);

What This Does:

  • Creates a CLIENT span for the cURL request
  • Sets peer.service = "payment-service"
  • Injects traceparent header for distributed tracing
  • Records HTTP status code and errors

Guzzle (with middleware)

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

// Create Guzzle client with TraceKit middleware
$stack = HandlerStack::create();
$stack->push($tracekit->getHttpClientInstrumentation()->getGuzzleMiddleware());

$client = new Client(['handler' => $stack]);

// All Guzzle requests now automatically create CLIENT spans!
$response = $client->post('http://payment-service/charge', [
    'json' => ['amount' => 99.99],
]);

$response = $client->get('http://inventory-service/check');

Service Name Detection

TraceKit intelligently extracts service names from URLs:

URL Extracted Service Name
http://payment-service:3000 payment-service
http://payment.internal payment
http://payment.svc.cluster.local payment
https://api.example.com api.example.com

This works seamlessly with:

  • Kubernetes service names
  • Internal DNS names
  • Docker Compose service names
  • External APIs

Custom Service Name Mappings

For local development or when service names can't be inferred from hostnames, use service_name_mappings:

$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'my-service',
    // Map localhost URLs to actual service names
    'service_name_mappings' => [
        'localhost:8082' => 'payment-service',
        'localhost:8083' => 'user-service',
        'localhost:8084' => 'inventory-service',
        'localhost:5001' => 'analytics-service',
    ],
]);

// Now requests to localhost:8082 will show as "payment-service" in the service graph
$response = $httpClient->get('http://localhost:8082/charge');
// -> Creates CLIENT span with peer.service = "payment-service"

This is especially useful when:

  • Running microservices locally on different ports
  • Using Docker Compose with localhost networking
  • Testing distributed tracing in development

Complete Example: Multi-Service Application

<?php

require 'vendor/autoload.php';

use TraceKit\PHP\TracekitClient;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'checkout-service',
]);

// Setup Guzzle with TraceKit instrumentation
$stack = HandlerStack::create();
$stack->push($tracekit->getHttpClientInstrumentation()->getGuzzleMiddleware());
$httpClient = new Client(['handler' => $stack]);

// Start request trace
$requestSpan = $tracekit->startTrace('http-request', [
    'http.method' => 'POST',
    'http.url' => '/checkout',
]);

try {
    // These HTTP calls automatically create CLIENT spans
    $paymentResponse = $httpClient->post('http://payment-service/charge', [
        'json' => [
            'amount' => 99.99,
            'user_id' => 123,
        ],
    ]);

    $inventoryResponse = $httpClient->post('http://inventory-service/reserve', [
        'json' => ['item_id' => 456],
    ]);

    $tracekit->endSpan($requestSpan, ['http.status_code' => 200]);

    echo json_encode(['success' => true]);

} catch (\Exception $e) {
    $tracekit->recordException($requestSpan, $e);
    $tracekit->endSpan($requestSpan, [], 'ERROR');
    echo json_encode(['error' => $e->getMessage()]);
}

$tracekit->flush();

Viewing Service Dependencies

Visit your TraceKit dashboard to see:

  • Service Map: Visual graph showing which services call which
  • Service List: Table of all services with health metrics
  • Service Detail: Upstream/downstream dependencies with latency and error info

Why Manual Wrapping?

Unlike Node.js or Python, PHP doesn't support automatic function interception. Therefore:

  • cURL: Use the wrapper function wrapCurlExec()
  • Guzzle: Add the middleware once when creating the client
  • Other clients: Create middleware/wrappers as needed

This gives you full control while maintaining zero-overhead when not used.

Usage Examples

HTTP Request Tracing

<?php

require 'vendor/autoload.php';

use TraceKit\PHP\TracekitClient;

$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'api-server',
]);

// Start tracing the request
$requestSpan = $tracekit->startTrace('http-request', [
    'http.method' => $_SERVER['REQUEST_METHOD'],
    'http.url' => $_SERVER['REQUEST_URI'],
    'http.user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
]);

try {
    // Route the request
    $result = handleRequest($_SERVER['REQUEST_URI']);

    $tracekit->endSpan($requestSpan, [
        'http.status_code' => 200,
    ]);

    http_response_code(200);
    header('Content-Type: application/json');
    echo json_encode($result);

} catch (\Exception $e) {
    $tracekit->recordException($requestSpan, $e);
    $tracekit->endSpan($requestSpan, [
        'http.status_code' => 500,
    ], 'ERROR');

    http_response_code(500);
    echo json_encode(['error' => $e->getMessage()]);
}

$tracekit->flush();

Database Query Tracing

<?php

function getUserById($tracekit, $userId) {
    // Child span automatically links to active parent via context
    $span = $tracekit->startSpan('db.query.users', [
        'db.system' => 'mysql',
        'db.operation' => 'SELECT',
        'user.id' => $userId,
    ]);

    try {
        $pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
        $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$userId]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        $tracekit->endSpan($span, [
            'db.rows_affected' => $stmt->rowCount(),
        ]);

        return $user;
    } catch (\PDOException $e) {
        $tracekit->recordException($span, $e);
        $tracekit->endSpan($span, [], 'ERROR');
        throw $e;
    }
}

External API Call Tracing

<?php

function fetchExternalData($tracekit, $url) {
    $span = $tracekit->startSpan('http.client.get', [
        'http.url' => $url,
        'http.method' => 'GET',
    ]);

    try {
        $response = file_get_contents($url);
        $data = json_decode($response, true);

        $tracekit->endSpan($span, [
            'http.status_code' => 200,
            'response.size' => strlen($response),
        ]);

        return $data;
    } catch (\Exception $e) {
        $tracekit->recordException($span, $e);
        $tracekit->endSpan($span, [], 'ERROR');
        throw $e;
    }
}

Nested Spans (Automatic Context Propagation)

<?php

function processOrder($tracekit, $orderId) {
    // Parent span
    $orderSpan = $tracekit->startSpan('process-order', [
        'order.id' => $orderId,
    ]);

    try {
        // Child spans automatically link to orderSpan via context

        // Validate order
        $validationSpan = $tracekit->startSpan('validate-order', [
            'order.id' => $orderId,
        ]);
        $valid = validateOrder($orderId);
        $tracekit->endSpan($validationSpan, ['valid' => $valid]);

        if (!$valid) {
            throw new \Exception('Invalid order');
        }

        // Process payment
        $paymentSpan = $tracekit->startSpan('process-payment', [
            'order.id' => $orderId,
        ]);
        $paymentResult = processPayment($orderId);
        $tracekit->endSpan($paymentSpan, ['payment.status' => $paymentResult]);

        // Ship order
        $shippingSpan = $tracekit->startSpan('ship-order', [
            'order.id' => $orderId,
        ]);
        $trackingId = shipOrder($orderId);
        $tracekit->endSpan($shippingSpan, ['tracking.id' => $trackingId]);

        $tracekit->endSpan($orderSpan, [
            'order.status' => 'completed',
        ]);

        return true;
    } catch (\Exception $e) {
        $tracekit->recordException($orderSpan, $e);
        $tracekit->endSpan($orderSpan, [], 'ERROR');
        throw $e;
    }
}

Framework Integration

Vanilla PHP

<?php

require 'vendor/autoload.php';

use TraceKit\PHP\TracekitClient;

$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'my-app',
]);

$span = $tracekit->startTrace('http-request', [
    'http.method' => $_SERVER['REQUEST_METHOD'],
    'http.url' => $_SERVER['REQUEST_URI'],
]);

// Your application logic
echo "Hello World!";

$tracekit->endSpan($span);
$tracekit->flush();

Symfony

<?php

namespace App\EventListener;

use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use TraceKit\PHP\TracekitClient;

class TracekitListener
{
    private TracekitClient $tracekit;
    private $currentSpan;

    public function __construct()
    {
        $this->tracekit = new TracekitClient([
            'api_key' => $_ENV['TRACEKIT_API_KEY'],
            'service_name' => 'symfony-app',
        ]);
    }

    public function onKernelRequest(RequestEvent $event)
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $request = $event->getRequest();
        $this->currentSpan = $this->tracekit->startTrace('http-request', [
            'http.method' => $request->getMethod(),
            'http.url' => $request->getRequestUri(),
            'http.route' => $request->attributes->get('_route'),
        ]);
    }

    public function onKernelResponse(ResponseEvent $event)
    {
        if (!$event->isMainRequest() || !$this->currentSpan) {
            return;
        }

        $this->tracekit->endSpan($this->currentSpan, [
            'http.status_code' => $event->getResponse()->getStatusCode(),
        ]);
        $this->tracekit->flush();
    }

    public function onKernelException(ExceptionEvent $event)
    {
        if ($this->currentSpan) {
            $this->tracekit->recordException($this->currentSpan, $event->getThrowable());
            $this->tracekit->endSpan($this->currentSpan, [], 'ERROR');
            $this->tracekit->flush();
        }
    }
}

Slim Framework

<?php

use Slim\Factory\AppFactory;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use TraceKit\PHP\TracekitClient;

require 'vendor/autoload.php';

$tracekit = new TracekitClient([
    'api_key' => getenv('TRACEKIT_API_KEY'),
    'service_name' => 'slim-app',
]);

$app = AppFactory::create();

// Tracing middleware
$app->add(function (Request $request, $handler) use ($tracekit) {
    $span = $tracekit->startTrace('http-request', [
        'http.method' => $request->getMethod(),
        'http.url' => (string) $request->getUri(),
    ]);

    try {
        $response = $handler->handle($request);

        $tracekit->endSpan($span, [
            'http.status_code' => $response->getStatusCode(),
        ]);

        $tracekit->flush();
        return $response;
    } catch (\Exception $e) {
        $tracekit->recordException($span, $e);
        $tracekit->endSpan($span, [], 'ERROR');
        $tracekit->flush();
        throw $e;
    }
});

$app->get('/hello/{name}', function (Request $request, Response $response, $args) {
    $response->getBody()->write("Hello, " . $args['name']);
    return $response;
});

$app->run();

How Context Propagation Works

TraceKit uses OpenTelemetry's Context API to automatically manage span relationships:

  1. Root Span: startTrace() creates a root span and activates it in the context
  2. Child Spans: startSpan() automatically inherits from the currently active span
  3. Scope Management: Each span has a scope that's detached when endSpan() is called
  4. Automatic Hierarchy: All spans within the same request share the same trace ID
// Root span (activated in context)
$rootSpan = $tracekit->startTrace('http-request');

    // Child 1 (inherits from root automatically)
    $child1 = $tracekit->startSpan('database-query');
    $tracekit->endSpan($child1);  // Detaches child1, root becomes active again

    // Child 2 (also inherits from root)
    $child2 = $tracekit->startSpan('api-call');

        // Grandchild (inherits from child2)
        $grandchild = $tracekit->startSpan('process-data');
        $tracekit->endSpan($grandchild);  // Detaches grandchild, child2 active

    $tracekit->endSpan($child2);  // Detaches child2, root active

$tracekit->endSpan($rootSpan);  // Detaches root

API Reference

TracekitClient

__construct(array $config)

Initialize the TraceKit client.

Parameters:

  • api_key (string, required) - Your TraceKit API key
  • service_name (string, optional) - Service name (default: 'php-app')
  • endpoint (string, optional) - TraceKit endpoint URL
  • enabled (bool, optional) - Enable/disable tracing (default: true)
  • sample_rate (float, optional) - Sample rate 0.0-1.0 (default: 1.0)

startTrace(string $operationName, array $attributes = []): array

Start a new root trace span (server request). Returns an array with the span and scope.

Returns: ['span' => SpanInterface, 'scope' => ScopeInterface]

startSpan(string $operationName, array $attributes = []): array

Start a new child span. Automatically inherits from the currently active span via context.

Returns: ['span' => SpanInterface, 'scope' => ScopeInterface]

endSpan(array $spanData, array $finalAttributes = [], ?string $status = 'OK'): void

End a span and detach its scope from the context.

Parameters:

  • $spanData - Array returned from startTrace() or startSpan()
  • $finalAttributes - Optional attributes to add before ending
  • $status - Span status: 'OK' or 'ERROR'

recordException(array $spanData, \Throwable $exception): void

Record an exception on a span.

addEvent(array $spanData, string $name, array $attributes = []): void

Add an event to a span.

flush(): void

Force flush all pending spans to the backend.

shutdown(): void

Shutdown the tracer provider.

Performance

TraceKit APM is designed to have minimal performance impact:

  • < 5% overhead on average request time
  • Asynchronous trace sending
  • Configurable sampling for high-traffic applications
  • Efficient context propagation

Requirements

  • PHP 8.1 or higher
  • Composer
  • PSR-18 HTTP Client (e.g., Guzzle)

Support

License

MIT License. See LICENSE for details.

Credits

Built with ❤️ by the TraceKit team using OpenTelemetry.