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
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.8
- open-telemetry/api: ^1.0
- open-telemetry/exporter-otlp: ^1.0
- open-telemetry/sdk: ^1.0
Requires (Dev)
- phpunit/phpunit: ^10.0
README
Framework-agnostic distributed tracing and performance monitoring for any PHP application.
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:
- ✅ TraceKit creates a CLIENT span for the outgoing request
- ✅ Trace context is injected into request headers (
traceparent) - ✅
peer.serviceattribute is set based on the target hostname - ✅ The receiving service creates a SERVER span linked to your CLIENT span
- ✅ 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
traceparentheader 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:
- Root Span:
startTrace()creates a root span and activates it in the context - Child Spans:
startSpan()automatically inherits from the currently active span - Scope Management: Each span has a scope that's detached when
endSpan()is called - 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 keyservice_name(string, optional) - Service name (default: 'php-app')endpoint(string, optional) - TraceKit endpoint URLenabled(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 fromstartTrace()orstartSpan()$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
- Documentation: https://app.tracekit.dev/docs
- Issues: https://github.com/Tracekit-Dev/php-apm/issues
- Email: support@tracekit.dev
License
MIT License. See LICENSE for details.
Credits
Built with ❤️ by the TraceKit team using OpenTelemetry.