tracekit / php-apm
TraceKit APM for PHP - Framework-agnostic distributed tracing and code monitoring
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
- Client IP Capture - Automatic IP detection for DDoS & traffic analysis
- Error Tracking - Capture exceptions with full context
- Code Monitoring - Live debugging with breakpoints and variable inspection
- Metrics API - Counter, Gauge, and Histogram metrics with automatic OTLP export
- 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'], 'http.client_ip' => TracekitClient::extractClientIp(), // Automatic IP detection ]); 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();
Local Development
Debug your PHP application locally without creating a cloud account using TraceKit Local UI.
Quick Start
# Install Local UI globally npm install -g @tracekit/local-ui # Start it tracekit-local
The Local UI will start at http://localhost:9999 and automatically open in your browser.
How It Works
When running in development mode (APP_ENV=local or APP_ENV=development), the SDK automatically:
- Detects if Local UI is running at
http://localhost:9999 - Sends traces to both Local UI and cloud (if API key is present)
- Falls back gracefully if Local UI is not available
No code changes needed! Just set the environment variable:
export APP_ENV=development export TRACEKIT_API_KEY=your-key # Optional - works without it! php app.php
You'll see traces appear in real-time at http://localhost:9999.
Features
- Real-time trace viewing in your browser
- Works completely offline
- No cloud account required
- Zero configuration
- Automatic cleanup (1000 traces max, 1 hour retention)
Local-Only Development
To use Local UI without cloud sending:
# Don't set TRACEKIT_API_KEY export APP_ENV=development php app.php
Traces will only go to Local UI.
Disabling Local UI
To disable automatic Local UI detection:
export APP_ENV=production # or don't run Local UI
Learn More
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()]; } }
PII Scrubbing
TraceKit automatically scans snapshot variables for sensitive data before sending them to the server. This ensures that passwords, API keys, tokens, and other sensitive information never leave your application.
Auto-Detected Patterns
The SDK automatically detects and redacts the following sensitive data types:
- Passwords - Common password field values
- API Keys - API key strings and prefixes
- Tokens - Authentication and session tokens
- Credit Cards - Card numbers (Visa, Mastercard, Amex, etc.)
- Email Addresses - RFC-compliant email patterns
- SSNs - US Social Security Numbers
- JWTs - JSON Web Tokens (
eyJ...) - AWS Keys - AWS access key IDs (
AKIA...) - Stripe Keys - Stripe API keys (
sk_live_...,pk_live_...) - Private Keys - PEM-encoded private key blocks
Sensitive Variable Name Detection
Variables with sensitive names are automatically redacted as [REDACTED:sensitive_name]. The SDK matches the following names: password, passwd, pwd, secret, token, key, credential, api_key, apikey.
The SDK uses letter-based boundaries (not \b) to correctly match names like api_key and user_token, where the underscore would otherwise prevent a word boundary match.
Value Pattern Redaction
When a value matches a known sensitive pattern (e.g., a credit card number or JWT), it is redacted as [REDACTED:type] regardless of the variable name.
Example
<?php // These variables are automatically redacted before sending: $tracekit->captureSnapshot('checkout', [ 'user_id' => 123, // Sent as-is 'password' => 'hunter2', // -> [REDACTED:sensitive_name] 'api_key' => 'sk_live_abc123', // -> [REDACTED:sensitive_name] 'user_token' => 'eyJhbGci...', // -> [REDACTED:sensitive_name] 'card_number' => '4111111111111111', // -> [REDACTED:credit_card] 'email' => 'user@example.com', // -> [REDACTED:email] 'note' => 'contains eyJhbGci... in text', // -> [REDACTED:jwt] ]);
PII scrubbing is enabled by default when code monitoring is active. No additional configuration is needed.
Kill Switch
TraceKit provides a server-side kill switch to disable code monitoring per service without any code changes.
How It Works
- Enable: Toggle the kill switch from the TraceKit dashboard or API
- Immediate Effect: The SDK stops capturing snapshots as soon as the kill switch is detected
- Auto-Resume: When the kill switch is disabled, snapshot captures resume automatically on the next poll cycle
PHP Process-per-Request Consideration
Unlike long-running SDKs (Node.js, Python, Go), PHP uses a process-per-request model. This means there is no persistent in-memory state between requests. The kill switch status must be fetched on every request (or on a percentage of requests) via pollBreakpoints():
<?php $tracekit = new TracekitClient([ 'api_key' => getenv('TRACEKIT_API_KEY'), 'service_name' => 'my-php-app', 'code_monitoring_enabled' => true, ]); // Poll on every request to get current kill switch state $tracekit->pollBreakpoints(); // This call is a no-op when kill switch is active $tracekit->captureSnapshot('checkout-validation', [ 'user_id' => $userId, 'cart_total' => $cartTotal, ]);
To reduce overhead, you can poll on a percentage of requests and cache the result:
<?php // Option 1: Poll on every request (most responsive to kill switch changes) $tracekit->pollBreakpoints(); // Option 2: Poll on ~5% of requests (lower overhead, slower kill switch response) if (rand(1, 20) === 1) { $tracekit->pollBreakpoints(); } // Option 3: Use a cron job to update a shared cache (e.g., Redis, APCu) // */1 * * * * php /path/to/poll-breakpoints.php
You can toggle the kill switch from the TraceKit dashboard under Services > [Your Service] > Code Monitoring or via the API.
SSE Real-time Updates
Note: SSE (Server-Sent Events) is not applicable to the PHP SDK. PHP's process-per-request model means there is no long-running process to maintain an SSE connection. The PHP SDK relies on polling via pollBreakpoints() to receive breakpoint changes and kill switch updates.
For real-time updates in PHP applications, consider:
- Polling
pollBreakpoints()on every request for the most responsive experience - Using a cron job to poll and cache the result in a shared store (Redis, APCu, or file-based cache)
- Using the Laravel APM package, which integrates with Laravel's scheduler for automatic polling
Circuit Breaker
The SDK includes a built-in circuit breaker to protect your application if the TraceKit backend becomes unreachable.
How It Works
- Failure Threshold: After 3 consecutive capture failures within a 60-second window, the circuit breaker trips
- Pause: Code monitoring is automatically paused, stopping all snapshot capture attempts
- Cooldown: After a 5-minute cooldown period, the circuit breaker resets and captures resume
- Transparent: No exceptions are raised in your application code; snapshots are silently skipped while the circuit is open
PHP Behavior
Since PHP is process-per-request, the circuit breaker state is tracked per-request. If the backend is unreachable, the SDK will fail fast after the first failed HTTP call within a single request, avoiding repeated timeouts:
<?php $tracekit = new TracekitClient([ 'api_key' => getenv('TRACEKIT_API_KEY'), 'service_name' => 'my-php-app', 'code_monitoring_enabled' => true, ]); // Normal operation: snapshots are captured and sent $tracekit->captureSnapshot('label', ['key' => 'value']); // If backend is unreachable: // - Capture attempt fails silently (no exception thrown) // - Subsequent captures in the same request are skipped // - Next request starts fresh
No configuration is required. The circuit breaker is always active when code monitoring is enabled.
Metrics
TraceKit APM includes a powerful metrics API for tracking application performance and business metrics with automatic OTLP export.
Metric Types
- Counter: Monotonically increasing values (requests, errors, events)
- Gauge: Point-in-time values that can go up or down (active connections, queue size, memory usage)
- Histogram: Value distributions (request duration, payload sizes)
Basic Usage
<?php require 'vendor/autoload.php'; use TraceKit\PHP\TracekitClient; $tracekit = new TracekitClient([ 'api_key' => getenv('TRACEKIT_API_KEY'), 'service_name' => 'my-app', ]); // Create metrics $requestCounter = $tracekit->counter('http.requests.total', [ 'service' => 'my-app' ]); $activeRequestsGauge = $tracekit->gauge('http.requests.active', [ 'service' => 'my-app' ]); $requestDurationHistogram = $tracekit->histogram('http.request.duration', [ 'unit' => 'ms' ]);
Counter
Counters track values that only increase (never decrease).
<?php // Create a counter $requestCounter = $tracekit->counter('http.requests.total', [ 'service' => 'my-app', 'environment' => 'production' ]); // Increment by 1 $requestCounter->inc(); // Add a specific value $requestCounter->add(5.0);
Common Use Cases:
- Request count
- Error count
- Cache hits/misses
- Items processed
Gauge
Gauges track values that can go up or down.
<?php // Create a gauge $activeRequestsGauge = $tracekit->gauge('http.requests.active', [ 'service' => 'my-app' ]); // Set to specific value $activeRequestsGauge->set(42.0); // Increment by 1 $activeRequestsGauge->inc(); // Decrement by 1 $activeRequestsGauge->dec();
Common Use Cases:
- Active requests
- Queue size
- Memory usage
- Active connections
Histogram
Histograms track the distribution of values.
<?php // Create a histogram $requestDurationHistogram = $tracekit->histogram('http.request.duration', [ 'service' => 'my-app', 'unit' => 'ms' ]); // Record values $requestDurationHistogram->record(45.2); // 45.2ms $requestDurationHistogram->record(123.8); // 123.8ms
Common Use Cases:
- Request duration
- Response size
- Query execution time
- Processing time
Complete Example: HTTP Request Metrics
<?php require 'vendor/autoload.php'; use TraceKit\PHP\TracekitClient; $tracekit = new TracekitClient([ 'api_key' => getenv('TRACEKIT_API_KEY'), 'service_name' => 'php-api', ]); // Initialize metrics $requestCounter = $tracekit->counter('http.requests.total', [ 'service' => 'php-api' ]); $activeRequestsGauge = $tracekit->gauge('http.requests.active', [ 'service' => 'php-api' ]); $requestDurationHistogram = $tracekit->histogram('http.request.duration', [ 'unit' => 'ms' ]); $errorCounter = $tracekit->counter('http.errors.total', [ 'service' => 'php-api' ]); // Track request start $startTime = microtime(true); $activeRequestsGauge->inc(); try { // Your application logic $result = processRequest(); http_response_code(200); echo json_encode($result); } catch (\Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); // Track errors $errorCounter->inc(); } // Track metrics at request end $requestCounter->inc(); $activeRequestsGauge->dec(); $duration = (microtime(true) - $startTime) * 1000; // Convert to ms $requestDurationHistogram->record($duration); // Track error status codes $statusCode = http_response_code(); if ($statusCode >= 400) { $errorCounter->inc(); } // Flush all data $tracekit->shutdown();
Framework Examples
Vanilla PHP Middleware
<?php class MetricsMiddleware { private $tracekit; private $requestCounter; private $activeRequestsGauge; private $requestDurationHistogram; private $errorCounter; public function __construct($tracekit) { $this->tracekit = $tracekit; // Initialize metrics once $this->requestCounter = $tracekit->counter('http.requests.total', [ 'service' => 'my-app' ]); $this->activeRequestsGauge = $tracekit->gauge('http.requests.active', [ 'service' => 'my-app' ]); $this->requestDurationHistogram = $tracekit->histogram('http.request.duration', [ 'unit' => 'ms' ]); $this->errorCounter = $tracekit->counter('http.errors.total', [ 'service' => 'my-app' ]); } public function handle(callable $next) { $startTime = microtime(true); $this->activeRequestsGauge->inc(); try { $response = $next(); return $response; } finally { // Track metrics $this->requestCounter->inc(); $this->activeRequestsGauge->dec(); $duration = (microtime(true) - $startTime) * 1000; $this->requestDurationHistogram->record($duration); if (http_response_code() >= 400) { $this->errorCounter->inc(); } } } } // Usage $tracekit = new TracekitClient([ 'api_key' => getenv('TRACEKIT_API_KEY'), 'service_name' => 'my-app', ]); $metrics = new MetricsMiddleware($tracekit); $metrics->handle(function() { // Your application logic echo "Hello World!"; }); $tracekit->shutdown();
Slim Framework
<?php use Slim\Factory\AppFactory; use TraceKit\PHP\TracekitClient; $tracekit = new TracekitClient([ 'api_key' => getenv('TRACEKIT_API_KEY'), 'service_name' => 'slim-app', ]); // Initialize metrics $requestCounter = $tracekit->counter('http.requests.total', ['service' => 'slim-app']); $activeRequestsGauge = $tracekit->gauge('http.requests.active', ['service' => 'slim-app']); $requestDurationHistogram = $tracekit->histogram('http.request.duration', ['unit' => 'ms']); $errorCounter = $tracekit->counter('http.errors.total', ['service' => 'slim-app']); $app = AppFactory::create(); // Metrics middleware $app->add(function ($request, $handler) use ( $tracekit, $requestCounter, $activeRequestsGauge, $requestDurationHistogram, $errorCounter ) { $startTime = microtime(true); $activeRequestsGauge->inc(); try { $response = $handler->handle($request); // Track metrics $requestCounter->inc(); $activeRequestsGauge->dec(); $duration = (microtime(true) - $startTime) * 1000; $requestDurationHistogram->record($duration); if ($response->getStatusCode() >= 400) { $errorCounter->inc(); } return $response; } catch (\Exception $e) { $errorCounter->inc(); throw $e; } }); $app->get('/hello', function ($request, $response) { $response->getBody()->write("Hello World!"); return $response; }); $app->run(); // Shutdown to flush metrics $tracekit->shutdown();
Tags for Dimensional Analysis
Add tags to metrics for filtering and grouping:
<?php // Metrics with tags $requestCounter = $tracekit->counter('http.requests.total', [ 'service' => 'my-app', 'environment' => 'production', 'region' => 'us-east-1' ]); $errorCounter = $tracekit->counter('http.errors.total', [ 'service' => 'my-app', 'error_type' => '4xx' ]); $cacheCounter = $tracekit->counter('cache.hits', [ 'service' => 'my-app', 'cache_type' => 'redis' ]);
Common Use Cases
Database Metrics
<?php $dbQueryCounter = $tracekit->counter('db.queries.total', [ 'service' => 'my-app', 'db' => 'mysql' ]); $dbConnectionsGauge = $tracekit->gauge('db.connections.active', [ 'service' => 'my-app', 'db' => 'mysql' ]); $dbQueryDuration = $tracekit->histogram('db.query.duration', [ 'service' => 'my-app', 'unit' => 'ms' ]); // Track a query $dbQueryCounter->inc(); $dbConnectionsGauge->inc(); $startTime = microtime(true); $result = $pdo->query("SELECT * FROM users"); $duration = (microtime(true) - $startTime) * 1000; $dbQueryDuration->record($duration); $dbConnectionsGauge->dec();
Business Metrics
<?php $checkoutCounter = $tracekit->counter('business.checkouts.total', [ 'service' => 'checkout-service' ]); $revenueGauge = $tracekit->gauge('business.revenue.total', [ 'service' => 'checkout-service', 'currency' => 'USD' ]); $orderValueHistogram = $tracekit->histogram('business.order.value', [ 'service' => 'checkout-service', 'currency' => 'USD' ]); // Track a successful checkout $checkoutCounter->inc(); $revenueGauge->set($totalRevenue); $orderValueHistogram->record($orderAmount);
Metric Export
Metrics are automatically buffered and exported to TraceKit:
- Buffer size: 100 metrics
- Flush interval: 10 seconds
- Endpoint: Automatically resolved to
/v1/metrics - Format: OpenTelemetry Protocol (OTLP)
Metrics are automatically sent when:
- Buffer reaches 100 metrics
- 10 seconds have elapsed since last export
shutdown()is called
<?php // Explicit flush of all pending data (traces + metrics) $tracekit->shutdown(); // At the end of your script register_shutdown_function(function() use ($tracekit) { $tracekit->shutdown(); });
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.