Core PHP
Learn how to integrate OpenTelemetry with Core PHP applications and send telemetry data to Last9
This guide shows you how to instrument your Core PHP application with OpenTelemetry and send traces, metrics, and logs to Last9.
Prerequisites
- PHP 8.1 or later
- Composer (for dependency management)
- Last9 account with OTLP endpoint configured
Installation
-
Install OpenTelemetry PHP extension
Install the PHP extension following the OpenTelemetry PHP setup guide:
# For Ubuntu/Debiansudo apt-get install php-devpecl install opentelemetry# For macOS with Homebrewbrew install phppecl install opentelemetry# Add to php.iniecho "extension=opentelemetry" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||") -
Create Composer project and install dependencies
# Initialize composer if not already donecomposer init# Install OpenTelemetry SDK and instrumentation packagescomposer require \open-telemetry/sdk \open-telemetry/exporter-otlp \open-telemetry/opentelemetry-auto-psr18 \open-telemetry/opentelemetry-auto-pdo \php-http/guzzle7-adapter \monolog/monolog -
Install additional HTTP client libraries (optional)
# For making HTTP requestscomposer require guzzlehttp/guzzle# For Redis supportcomposer require predis/predis
Configuration
Environment Variables
Create a .env file in your project root:
# OpenTelemetry ConfigurationOTEL_PHP_AUTOLOAD_ENABLED=trueOTEL_SERVICE_NAME=core-php-appOTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpointOTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"OTEL_TRACES_EXPORTER=otlpOTEL_METRICS_EXPORTER=otlpOTEL_LOGS_EXPORTER=otlpOTEL_PROPAGATORS=baggage,tracecontextOTEL_TRACES_SAMPLER=always_onOTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"OTEL_LOG_LEVEL=error
# Application ConfigurationAPP_ENV=productionAPP_DEBUG=false
# DatabaseDB_HOST=localhostDB_PORT=3306DB_NAME=php_app_dbDB_USER=userDB_PASSWORD=password
# RedisREDIS_HOST=localhostREDIS_PORT=6379Create config/config.php:
<?php
// Load environment variablesfunction loadEnv($path) { if (!file_exists($path)) { return; }
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { if (strpos($line, '#') === 0) continue;
[$name, $value] = explode('=', $line, 2); $name = trim($name); $value = trim($value, '"\'');
if (!array_key_exists($name, $_ENV)) { $_ENV[$name] = $value; } }}
loadEnv(__DIR__ . '/../.env');
return [ 'app' => [ 'name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'core-php-app', 'env' => $_ENV['APP_ENV'] ?? 'production', 'debug' => filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN), ],
'otel' => [ 'enabled' => filter_var($_ENV['OTEL_PHP_AUTOLOAD_ENABLED'] ?? true, FILTER_VALIDATE_BOOLEAN), 'service_name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'core-php-app', 'endpoint' => $_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? '', 'headers' => $_ENV['OTEL_EXPORTER_OTLP_HEADERS'] ?? '', 'resource_attributes' => [ 'service.name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'core-php-app', 'service.version' => '1.0.0', 'deployment.environment' => $_ENV['APP_ENV'] ?? 'production', 'framework.name' => 'core-php', 'php.version' => PHP_VERSION, ], ],
'database' => [ 'host' => $_ENV['DB_HOST'] ?? 'localhost', 'port' => $_ENV['DB_PORT'] ?? 3306, 'dbname' => $_ENV['DB_NAME'] ?? 'php_app_db', 'username' => $_ENV['DB_USER'] ?? 'user', 'password' => $_ENV['DB_PASSWORD'] ?? 'password', 'charset' => 'utf8mb4', ],
'redis' => [ 'host' => $_ENV['REDIS_HOST'] ?? 'localhost', 'port' => $_ENV['REDIS_PORT'] ?? 6379, 'timeout' => 2.5, ],];OpenTelemetry Bootstrap
<?php
namespace App\OpenTelemetry;
use OpenTelemetry\API\Globals;use OpenTelemetry\API\Instrumentation\CachedInstrumentation;use OpenTelemetry\API\Trace\TracerInterface;use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;use OpenTelemetry\Contrib\Otlp\SpanExporter;use OpenTelemetry\SDK\Common\Attribute\Attributes;use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;use OpenTelemetry\SDK\Resource\ResourceInfo;use OpenTelemetry\SDK\Resource\ResourceInfoFactory;use OpenTelemetry\SDK\Sdk;use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;use OpenTelemetry\SDK\Trace\Sampler\ParentBased;use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;use OpenTelemetry\SDK\Trace\TracerProvider;use OpenTelemetry\SemConv\ResourceAttributes;
class Bootstrap{ private static ?TracerProvider $tracerProvider = null; private static ?TracerInterface $tracer = null; private static bool $initialized = false;
public static function initialize(array $config): void { if (self::$initialized) { return; }
if (!$config['otel']['enabled']) { return; }
$resource = self::createResource($config['otel']['resource_attributes']); $tracerProvider = self::createTracerProvider($resource, $config);
$sdk = Sdk::builder() ->setTracerProvider($tracerProvider) ->setAutoShutdown(true) ->build();
self::$tracerProvider = $tracerProvider; self::$tracer = $tracerProvider->getTracer($config['otel']['service_name'], '1.0.0'); self::$initialized = true;
// Register shutdown handler register_shutdown_function([self::class, 'shutdown']); }
public static function getTracer(): ?TracerInterface { return self::$tracer; }
public static function getInstrumentation(string $name, string $version = '1.0.0'): CachedInstrumentation { return new CachedInstrumentation($name, $version); }
public static function shutdown(): void { if (self::$tracerProvider) { self::$tracerProvider->shutdown(); } }
private static function createResource(array $attributes): ResourceInfo { return ResourceInfoFactory::create( Attributes::create($attributes), ResourceAttributes::SCHEMA_URL ); }
private static function createTracerProvider(ResourceInfo $resource, array $config): TracerProvider { $exporter = self::createExporter($config['otel']);
$spanProcessor = new BatchSpanProcessor( $exporter, Globals::clockInterface(), 512, // maxExportBatchSize 5000, // scheduleDelayMillis 30000, // exportTimeoutMillis 2048 // maxQueueSize );
return TracerProvider::builder() ->addSpanProcessor($spanProcessor) ->setResource($resource) ->setSampler(new ParentBased(new AlwaysOnSampler())) ->build(); }
private static function createExporter(array $config): SpanExporter { $endpoint = $config['endpoint']; $headers = [];
if ($authHeader = $config['headers']) { if (str_starts_with($authHeader, 'Authorization=')) { $headers['Authorization'] = substr($authHeader, 14); } }
if ($endpoint) { $transport = (new OtlpHttpTransportFactory())->create( $endpoint . '/v1/traces', 'application/json', $headers );
return new SpanExporter($transport); }
// Fallback to console exporter for development return new SpanExporter( (new StreamTransportFactory())->create(STDOUT, 'application/json') ); }}<?php
namespace App;
use App\OpenTelemetry\Bootstrap;use OpenTelemetry\API\Trace\Span;use OpenTelemetry\API\Trace\SpanKind;use OpenTelemetry\API\Trace\StatusCode;use OpenTelemetry\API\Trace\TracerInterface;
class TelemetryService{ private TracerInterface $tracer;
public function __construct() { $this->tracer = Bootstrap::getTracer() ?? throw new \RuntimeException('OpenTelemetry not initialized'); }
public function traceOperation(string $operationName, callable $operation, array $attributes = []): mixed { $span = $this->tracer->spanBuilder($operationName) ->setSpanKind(SpanKind::KIND_INTERNAL) ->setAttributes($attributes) ->startSpan();
$scope = $span->activate();
try { $result = $operation($span); $span->setStatus(StatusCode::STATUS_OK); return $result;
} catch (\Throwable $e) { $span->recordException($e); $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); throw $e;
} finally { $span->end(); $scope->detach(); } }
public function traceHttpRequest(string $method, string $uri, callable $handler): mixed { $spanName = "{$method} {$uri}";
$span = $this->tracer->spanBuilder($spanName) ->setSpanKind(SpanKind::KIND_SERVER) ->setAttributes([ 'http.request.method' => $method, 'url.full' => $this->getCurrentUrl(), 'url.path' => $uri, 'user_agent.original' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'network.peer.address' => $this->getClientIp(), 'http.request.body.size' => $_SERVER['CONTENT_LENGTH'] ?? 0, ]) ->startSpan();
$scope = $span->activate(); $startTime = hrtime(true);
try { $result = $handler($span);
// If result has HTTP status code, use it $statusCode = 200; if (is_array($result) && isset($result['status_code'])) { $statusCode = $result['status_code']; }
$duration = (hrtime(true) - $startTime) / 1_000_000; // Convert to milliseconds
$span->setAttributes([ 'http.response.status_code' => $statusCode, 'http.request.duration_ms' => $duration, ]);
if ($statusCode >= 400) { $span->setStatus(StatusCode::STATUS_ERROR, "HTTP {$statusCode}"); } else { $span->setStatus(StatusCode::STATUS_OK); }
return $result;
} catch (\Throwable $e) { $span->recordException($e); $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); throw $e;
} finally { $span->end(); $scope->detach(); } }
public function getCurrentSpan(): ?Span { return Span::getCurrent(); }
public function addAttributes(array $attributes): void { $span = $this->getCurrentSpan(); if ($span) { $span->setAttributes($attributes); } }
public function addEvent(string $name, array $attributes = []): void { $span = $this->getCurrentSpan(); if ($span) { $span->addEvent($name, $attributes); } }
public function recordException(\Throwable $exception): void { $span = $this->getCurrentSpan(); if ($span) { $span->recordException($exception); } }
private function getCurrentUrl(): string { $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost'; $uri = $_SERVER['REQUEST_URI'] ?? '/';
return "{$protocol}://{$host}{$uri}"; }
private function getClientIp(): string { // Check for various headers $headers = [ 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ];
foreach ($headers as $header) { if (!empty($_SERVER[$header])) { $ips = explode(',', $_SERVER[$header]); return trim($ips[0]); } }
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; }}Database Integration
<?php
namespace App\Database;
use App\TelemetryService;use OpenTelemetry\API\Trace\Span;use PDO;use PDOStatement;
class Connection{ private PDO $pdo; private TelemetryService $telemetry;
public function __construct(array $config, TelemetryService $telemetry) { $this->telemetry = $telemetry;
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['dbname']};charset={$config['charset']}";
$this->pdo = new PDO($dsn, $config['username'], $config['password'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); }
public function query(string $sql, array $params = []): array { return $this->telemetry->traceOperation('database.query', function (Span $span) use ($sql, $params) { $span->setAttributes([ 'db.system' => 'mysql', 'db.statement' => $this->sanitizeSql($sql), 'db.operation.name' => $this->getOperationType($sql), 'db.params.count' => count($params), ]);
$stmt = $this->pdo->prepare($sql);
$span->addEvent('query.prepared');
$executeResult = $stmt->execute($params);
if ($executeResult) { $results = $stmt->fetchAll();
$span->setAttributes([ 'db.rows_affected' => $stmt->rowCount(), 'db.results.count' => count($results), ]);
$span->addEvent('query.executed', [ 'rows_returned' => count($results), ]);
return $results; }
throw new \RuntimeException('Query execution failed'); }); }
public function execute(string $sql, array $params = []): int { return $this->telemetry->traceOperation('database.execute', function (Span $span) use ($sql, $params) { $span->setAttributes([ 'db.system' => 'mysql', 'db.statement' => $this->sanitizeSql($sql), 'db.operation.name' => $this->getOperationType($sql), 'db.params.count' => count($params), ]);
$stmt = $this->pdo->prepare($sql);
$span->addEvent('statement.prepared');
$executeResult = $stmt->execute($params);
if ($executeResult) { $rowCount = $stmt->rowCount();
$span->setAttributes([ 'db.rows_affected' => $rowCount, ]);
$span->addEvent('statement.executed', [ 'rows_affected' => $rowCount, ]);
return $rowCount; }
throw new \RuntimeException('Statement execution failed'); }); }
public function lastInsertId(): string { return $this->pdo->lastInsertId(); }
public function beginTransaction(): bool { $this->telemetry->addEvent('transaction.begin'); return $this->pdo->beginTransaction(); }
public function commit(): bool { $this->telemetry->addEvent('transaction.commit'); return $this->pdo->commit(); }
public function rollback(): bool { $this->telemetry->addEvent('transaction.rollback'); return $this->pdo->rollback(); }
private function sanitizeSql(string $sql): string { // Remove sensitive data and normalize for better span grouping $sql = preg_replace('/\s+/', ' ', $sql); $sql = preg_replace('/\'[^\']*\'/', '?', $sql); $sql = preg_replace('/\d+/', '?', $sql); return trim($sql); }
private function getOperationType(string $sql): string { $sql = trim(strtoupper($sql));
if (str_starts_with($sql, 'SELECT')) return 'SELECT'; if (str_starts_with($sql, 'INSERT')) return 'INSERT'; if (str_starts_with($sql, 'UPDATE')) return 'UPDATE'; if (str_starts_with($sql, 'DELETE')) return 'DELETE'; if (str_starts_with($sql, 'CREATE')) return 'CREATE'; if (str_starts_with($sql, 'DROP')) return 'DROP'; if (str_starts_with($sql, 'ALTER')) return 'ALTER';
return 'UNKNOWN'; }}<?php
namespace App\Models;
use App\Database\Connection;use App\TelemetryService;
class User{ private Connection $db; private TelemetryService $telemetry;
public function __construct(Connection $db, TelemetryService $telemetry) { $this->db = $db; $this->telemetry = $telemetry; }
public function findAll(int $limit = 10, int $offset = 0): array { return $this->telemetry->traceOperation('user.find_all', function (Span $span) use ($limit, $offset) { $span->setAttributes([ 'model' => 'User', 'operation' => 'findAll', 'limit' => $limit, 'offset' => $offset, ]);
$sql = "SELECT id, name, email, created_at, updated_at FROM users ORDER BY id DESC LIMIT ? OFFSET ?"; $users = $this->db->query($sql, [$limit, $offset]);
$span->setAttributes([ 'users.count' => count($users), ]);
return $users; }); }
public function findById(int $id): ?array { return $this->telemetry->traceOperation('user.find_by_id', function (Span $span) use ($id) { $span->setAttributes([ 'model' => 'User', 'operation' => 'findById', 'user.id' => $id, ]);
$sql = "SELECT id, name, email, created_at, updated_at FROM users WHERE id = ?"; $users = $this->db->query($sql, [$id]);
$found = !empty($users); $span->setAttributes(['user.found' => $found]);
return $found ? $users[0] : null; }); }
public function create(array $data): array { return $this->telemetry->traceOperation('user.create', function (Span $span) use ($data) { $span->setAttributes([ 'model' => 'User', 'operation' => 'create', 'user.email' => $data['email'] ?? 'unknown', ]);
$this->db->beginTransaction();
try { $sql = "INSERT INTO users (name, email, password, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW())"; $params = [ $data['name'], $data['email'], isset($data['password']) ? password_hash($data['password'], PASSWORD_DEFAULT) : null, ];
$this->db->execute($sql, $params); $userId = $this->db->lastInsertId();
$this->db->commit();
$span->setAttributes([ 'user.created' => true, 'user.id' => $userId, ]);
// Fetch the created user return $this->findById((int) $userId);
} catch (\Exception $e) { $this->db->rollback(); throw $e; } }); }
public function update(int $id, array $data): ?array { return $this->telemetry->traceOperation('user.update', function (Span $span) use ($id, $data) { $span->setAttributes([ 'model' => 'User', 'operation' => 'update', 'user.id' => $id, 'fields_to_update' => array_keys($data), ]);
$this->db->beginTransaction();
try { $setParts = []; $params = [];
if (isset($data['name'])) { $setParts[] = 'name = ?'; $params[] = $data['name']; }
if (isset($data['email'])) { $setParts[] = 'email = ?'; $params[] = $data['email']; }
if (isset($data['password'])) { $setParts[] = 'password = ?'; $params[] = password_hash($data['password'], PASSWORD_DEFAULT); }
if (empty($setParts)) { $span->setAttributes(['user.updated' => false, 'reason' => 'no_fields_to_update']); return $this->findById($id); }
$setParts[] = 'updated_at = NOW()'; $params[] = $id;
$sql = "UPDATE users SET " . implode(', ', $setParts) . " WHERE id = ?"; $rowsAffected = $this->db->execute($sql, $params);
$this->db->commit();
$span->setAttributes([ 'user.updated' => $rowsAffected > 0, 'rows_affected' => $rowsAffected, ]);
return $rowsAffected > 0 ? $this->findById($id) : null;
} catch (\Exception $e) { $this->db->rollback(); throw $e; } }); }
public function delete(int $id): bool { return $this->telemetry->traceOperation('user.delete', function (Span $span) use ($id) { $span->setAttributes([ 'model' => 'User', 'operation' => 'delete', 'user.id' => $id, ]);
$sql = "DELETE FROM users WHERE id = ?"; $rowsAffected = $this->db->execute($sql, [$id]);
$deleted = $rowsAffected > 0; $span->setAttributes([ 'user.deleted' => $deleted, 'rows_affected' => $rowsAffected, ]);
return $deleted; }); }
public function count(): int { return $this->telemetry->traceOperation('user.count', function (Span $span) { $span->setAttributes([ 'model' => 'User', 'operation' => 'count', ]);
$result = $this->db->query("SELECT COUNT(*) as count FROM users"); $count = (int) $result[0]['count'];
$span->setAttributes(['users.total_count' => $count]);
return $count; }); }}HTTP Handlers and Routing
<?php
namespace App\Http;
use App\TelemetryService;
class Router{ private array $routes = []; private TelemetryService $telemetry;
public function __construct(TelemetryService $telemetry) { $this->telemetry = $telemetry; }
public function get(string $path, callable $handler): void { $this->addRoute('GET', $path, $handler); }
public function post(string $path, callable $handler): void { $this->addRoute('POST', $path, $handler); }
public function put(string $path, callable $handler): void { $this->addRoute('PUT', $path, $handler); }
public function delete(string $path, callable $handler): void { $this->addRoute('DELETE', $path, $handler); }
private function addRoute(string $method, string $path, callable $handler): void { $this->routes[] = [ 'method' => $method, 'path' => $path, 'handler' => $handler, 'pattern' => $this->pathToPattern($path), ]; }
public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->routes as $route) { if ($route['method'] === $method && preg_match($route['pattern'], $uri, $matches)) { array_shift($matches); // Remove full match
$this->telemetry->traceHttpRequest($method, $route['path'], function () use ($route, $matches) { try { $result = call_user_func($route['handler'], ...$matches);
if (is_array($result)) { $this->sendJsonResponse($result); } elseif (is_string($result)) { $this->sendTextResponse($result); }
return $result;
} catch (\Exception $e) { $this->sendErrorResponse($e->getMessage(), 500); throw $e; } });
return; } }
$this->telemetry->traceHttpRequest($method, $uri, function () use ($uri) { $this->sendErrorResponse("Route not found: {$uri}", 404); return ['status_code' => 404]; }); }
private function pathToPattern(string $path): string { // Convert {id} style parameters to regex groups $pattern = preg_replace('/\{(\w+)\}/', '([^/]+)', $path); return '#^' . $pattern . '$#'; }
private function sendJsonResponse(array $data, int $statusCode = 200): void { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode($data); }
private function sendTextResponse(string $text, int $statusCode = 200): void { http_response_code($statusCode); header('Content-Type: text/plain'); echo $text; }
private function sendErrorResponse(string $message, int $statusCode = 500): void { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode(['error' => $message, 'status' => $statusCode]); }}<?php
namespace App\Http\Controllers;
use App\Models\User;use App\TelemetryService;
class UserController{ private User $userModel; private TelemetryService $telemetry;
public function __construct(User $userModel, TelemetryService $telemetry) { $this->userModel = $userModel; $this->telemetry = $telemetry; }
public function index(): array { $this->telemetry->addAttributes([ 'controller' => 'UserController', 'action' => 'index', ]);
$page = (int) ($_GET['page'] ?? 1); $perPage = (int) ($_GET['per_page'] ?? 10); $offset = ($page - 1) * $perPage;
$this->telemetry->addAttributes([ 'pagination.page' => $page, 'pagination.per_page' => $perPage, ]);
$users = $this->userModel->findAll($perPage, $offset); $totalUsers = $this->userModel->count();
$this->telemetry->addAttributes([ 'users.count' => count($users), 'users.total' => $totalUsers, ]);
return [ 'data' => $users, 'pagination' => [ 'page' => $page, 'per_page' => $perPage, 'total' => $totalUsers, 'pages' => ceil($totalUsers / $perPage), ], ]; }
public function show(string $id): array { $userId = (int) $id;
$this->telemetry->addAttributes([ 'controller' => 'UserController', 'action' => 'show', 'user.id' => $userId, ]);
$user = $this->userModel->findById($userId);
if (!$user) { http_response_code(404); return ['error' => 'User not found', 'status' => 404]; }
$this->telemetry->addAttributes([ 'user.found' => true, 'user.email' => $user['email'], ]);
return ['data' => $user]; }
public function create(): array { $this->telemetry->addAttributes([ 'controller' => 'UserController', 'action' => 'create', ]);
// Parse JSON input $input = json_decode(file_get_contents('php://input'), true);
if (!$input) { http_response_code(400); return ['error' => 'Invalid JSON input', 'status' => 400]; }
// Validate required fields $requiredFields = ['name', 'email']; $missingFields = [];
foreach ($requiredFields as $field) { if (!isset($input[$field]) || empty($input[$field])) { $missingFields[] = $field; } }
if (!empty($missingFields)) { $this->telemetry->addAttributes([ 'validation.failed' => true, 'validation.missing_fields' => $missingFields, ]);
http_response_code(422); return [ 'error' => 'Validation failed', 'missing_fields' => $missingFields, 'status' => 422, ]; }
if (isset($input['email']) && !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) { http_response_code(422); return ['error' => 'Invalid email format', 'status' => 422]; }
$this->telemetry->addAttributes(['user.email' => $input['email']]);
try { $user = $this->userModel->create($input);
$this->telemetry->addAttributes([ 'user.created' => true, 'user.id' => $user['id'], ]);
http_response_code(201); return ['data' => $user, 'message' => 'User created successfully'];
} catch (\Exception $e) { $this->telemetry->recordException($e);
if (str_contains($e->getMessage(), 'Duplicate entry')) { http_response_code(409); return ['error' => 'Email already exists', 'status' => 409]; }
http_response_code(500); return ['error' => 'Failed to create user', 'status' => 500]; } }
public function update(string $id): array { $userId = (int) $id;
$this->telemetry->addAttributes([ 'controller' => 'UserController', 'action' => 'update', 'user.id' => $userId, ]);
// Parse JSON input $input = json_decode(file_get_contents('php://input'), true);
if (!$input) { http_response_code(400); return ['error' => 'Invalid JSON input', 'status' => 400]; }
// Validate email if provided if (isset($input['email']) && !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) { http_response_code(422); return ['error' => 'Invalid email format', 'status' => 422]; }
$this->telemetry->addAttributes(['fields_to_update' => array_keys($input)]);
try { $user = $this->userModel->update($userId, $input);
if (!$user) { http_response_code(404); return ['error' => 'User not found', 'status' => 404]; }
$this->telemetry->addAttributes([ 'user.updated' => true, 'user.email' => $user['email'], ]);
return ['data' => $user, 'message' => 'User updated successfully'];
} catch (\Exception $e) { $this->telemetry->recordException($e);
if (str_contains($e->getMessage(), 'Duplicate entry')) { http_response_code(409); return ['error' => 'Email already exists', 'status' => 409]; }
http_response_code(500); return ['error' => 'Failed to update user', 'status' => 500]; } }
public function delete(string $id): array { $userId = (int) $id;
$this->telemetry->addAttributes([ 'controller' => 'UserController', 'action' => 'delete', 'user.id' => $userId, ]);
$deleted = $this->userModel->delete($userId);
if (!$deleted) { http_response_code(404); return ['error' => 'User not found', 'status' => 404]; }
$this->telemetry->addAttributes(['user.deleted' => true]);
return ['message' => 'User deleted successfully']; }}Application Entry Point
<?php
// Include autoloaderrequire_once __DIR__ . '/../vendor/autoload.php';
use App\Database\Connection;use App\Http\Controllers\UserController;use App\Http\Router;use App\Models\User;use App\OpenTelemetry\Bootstrap;use App\TelemetryService;
// Load configuration$config = require __DIR__ . '/../config/config.php';
// Initialize OpenTelemetryBootstrap::initialize($config);
// Initialize services$telemetry = new TelemetryService();$database = new Connection($config['database'], $telemetry);
// Initialize models$userModel = new User($database, $telemetry);
// Initialize controllers$userController = new UserController($userModel, $telemetry);
// Initialize router$router = new Router($telemetry);
// Define routes$router->get('/', function () { return [ 'message' => 'Welcome to Core PHP API', 'version' => '1.0.0', 'timestamp' => date('c'), ];});
$router->get('/health', function () { return [ 'status' => 'healthy', 'timestamp' => date('c'), 'memory_usage' => memory_get_usage(true), 'peak_memory' => memory_get_peak_usage(true), ];});
// User routes$router->get('/api/users', [$userController, 'index']);$router->post('/api/users', [$userController, 'create']);$router->get('/api/users/{id}', [$userController, 'show']);$router->put('/api/users/{id}', [$userController, 'update']);$router->delete('/api/users/{id}', [$userController, 'delete']);
// Stats endpoint$router->get('/api/stats', function () use ($userModel) { return [ 'users_count' => $userModel->count(), 'memory_usage' => memory_get_usage(true), 'peak_memory' => memory_get_peak_usage(true), 'php_version' => PHP_VERSION, ];});
// Dispatch the request$router->dispatch();-- Create database schemaCREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email), INDEX idx_created_at (created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert sample dataINSERT INTO users (name, email, password) VALUES ('John Doe', 'john@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'), ('Jane Smith', 'jane@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'), ('Bob Johnson', 'bob@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi')ON DUPLICATE KEY UPDATE name=name;Production Deployment
Docker Configuration
FROM php:8.2-fpm-alpine
# Install system dependenciesRUN apk add --no-cache \ curl \ git \ icu-dev \ libzip-dev \ oniguruma-dev \ autoconf \ g++ \ make \ mysql-client
# Install PHP extensionsRUN docker-php-ext-install \ pdo_mysql \ mbstring \ zip \ intl \ opcache
# Install OpenTelemetry extensionRUN pecl install opentelemetry && docker-php-ext-enable opentelemetry
# Install ComposerCOPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directoryWORKDIR /var/www/html
# Copy composer filesCOPY composer.json composer.lock ./RUN composer install --no-dev --optimize-autoloader
# Copy application filesCOPY . .
# Set permissionsRUN chown -R www-data:www-data /var/www/html
# Copy PHP configurationCOPY docker/php/php.ini /usr/local/etc/php/php.iniCOPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
USER www-data
EXPOSE 9000
CMD ["php-fpm"]version: "3.8"
services: app: build: . container_name: core-php-app environment: # OpenTelemetry - OTEL_PHP_AUTOLOAD_ENABLED=true - OTEL_SERVICE_NAME=core-php-app - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header - OTEL_TRACES_EXPORTER=otlp - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0
# Application - APP_ENV=production - APP_DEBUG=false - DB_HOST=mysql - DB_NAME=php_app_db - DB_USER=php_user - DB_PASSWORD=php_password depends_on: - mysql - redis
nginx: image: nginx:alpine container_name: core-php-nginx ports: - "80:80" volumes: - .:/var/www/html - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - app
mysql: image: mysql:8.0 container_name: core-php-mysql environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: php_app_db MYSQL_USER: php_user MYSQL_PASSWORD: php_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./database/schema.sql:/docker-entrypoint-initdb.d/schema.sql
redis: image: redis:7-alpine container_name: core-php-redis volumes: - redis_data:/data
volumes: mysql_data: redis_data:server { listen 80; index index.php index.html; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; root /var/www/html/public;
location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass app:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; }
location / { try_files $uri $uri/ /index.php?$query_string; gzip_static on; }
# Health check location /health { try_files $uri /index.php?$query_string; access_log off; }}Testing the Integration
-
Start your application
# Local development with PHP built-in serverphp -S localhost:8000 -t public# Or with Dockerdocker-compose up -
Test the endpoints
# Health checkcurl http://localhost:8000/health# API endpointscurl http://localhost:8000/api/userscurl http://localhost:8000/api/users/1# Create a usercurl -X POST http://localhost:8000/api/users \-H "Content-Type: application/json" \-d '{"name": "Test User", "email": "test@example.com", "password": "password123"}'# Update a usercurl -X PUT http://localhost:8000/api/users/1 \-H "Content-Type: application/json" \-d '{"name": "Updated User"}'# Delete a usercurl -X DELETE http://localhost:8000/api/users/1# Get statscurl http://localhost:8000/api/stats -
View telemetry in Last9
Check your Last9 dashboard for:
- HTTP request traces with Core PHP specific attributes
- Database operation spans with PDO instrumentation
- Custom business logic spans and events
- Error tracking and exception recording