Framework-agnostic distributed tracing and performance monitoring for any PHP application.
- 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
composer require tracekit/php-apm<?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();Debug your PHP application locally without creating a cloud account using TraceKit Local UI.
# Install Local UI globally
npm install -g @tracekit/local-ui
# Start it
tracekit-localThe Local UI will start at http://localhost:9999 and automatically open in your browser.
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.phpYou'll see traces appear in real-time at http://localhost:9999.
- Real-time trace viewing in your browser
- Works completely offline
- No cloud account required
- Zero configuration
- Automatic cleanup (1000 traces max, 1 hour retention)
To use Local UI without cloud sending:
# Don't set TRACEKIT_API_KEY
export APP_ENV=development
php app.phpTraces will only go to Local UI.
To disable automatic Local UI detection:
export APP_ENV=production
# or don't run Local UITraceKit includes production-safe code monitoring for live debugging without redeployment.
<?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 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']]);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();- 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
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
<?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();<?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()];
}
}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.
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
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.
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.
<?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.
TraceKit provides a server-side kill switch to disable code monitoring per service without any code changes.
- 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
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.phpYou can toggle the kill switch from the TraceKit dashboard under Services > [Your Service] > Code Monitoring or via the API.
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
The SDK includes a built-in circuit breaker to protect your application if the TraceKit backend becomes unreachable.
- 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
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 freshNo configuration is required. The circuit breaker is always active when code monitoring is enabled.
TraceKit APM includes a powerful metrics API for tracking application performance and business metrics with automatic OTLP export.
- 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)
<?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'
]);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
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
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.8msCommon Use Cases:
- Request duration
- Response size
- Query execution time
- Processing time
<?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();<?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();<?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();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'
]);<?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();<?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);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();
});$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',
],
]);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-appTraceKit provides instrumentation for outgoing HTTP calls to create service dependency graphs.
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
$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
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');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
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
<?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();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
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.
<?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();<?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;
}
}<?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;
}
}<?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;
}
}<?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();<?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();
}
}
}<?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();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 rootInitialize 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)
Start a new root trace span (server request). Returns an array with the span and scope.
Returns: ['span' => SpanInterface, 'scope' => ScopeInterface]
Start a new child span. Automatically inherits from the currently active span via context.
Returns: ['span' => SpanInterface, 'scope' => ScopeInterface]
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'
Record an exception on a span.
Add an event to a span.
Force flush all pending spans to the backend.
Shutdown the tracer provider.
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
- PHP 8.1 or higher
- Composer
- PSR-18 HTTP Client (e.g., Guzzle)
- Documentation: https://app.tracekit.dev/docs
- Issues: https://github.com/Tracekit-Dev/php-apm/issues
- Email: support@tracekit.dev
MIT License. See LICENSE for details.
Built with ❤️ by the TraceKit team using OpenTelemetry.