Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

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

  1. Install OpenTelemetry PHP extension

    Install the PHP extension following the OpenTelemetry PHP setup guide:

    # For Ubuntu/Debian
    sudo apt-get install php-dev
    pecl install opentelemetry
    # For macOS with Homebrew
    brew install php
    pecl install opentelemetry
    # Add to php.ini
    echo "extension=opentelemetry" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||")
  2. Create Composer project and install dependencies

    # Initialize composer if not already done
    composer init
    # Install OpenTelemetry SDK and instrumentation packages
    composer require \
    open-telemetry/sdk \
    open-telemetry/exporter-otlp \
    open-telemetry/opentelemetry-auto-psr18 \
    open-telemetry/opentelemetry-auto-pdo \
    php-http/guzzle7-adapter \
    monolog/monolog
  3. Install additional HTTP client libraries (optional)

    # For making HTTP requests
    composer require guzzlehttp/guzzle
    # For Redis support
    composer require predis/predis

Configuration

Environment Variables

Create a .env file in your project root:

# OpenTelemetry Configuration
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_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_PROPAGATORS=baggage,tracecontext
OTEL_TRACES_SAMPLER=always_on
OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"
OTEL_LOG_LEVEL=error
# Application Configuration
APP_ENV=production
APP_DEBUG=false
# Database
DB_HOST=localhost
DB_PORT=3306
DB_NAME=php_app_db
DB_USER=user
DB_PASSWORD=password
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379

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')
);
}
}

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';
}
}

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]);
}
}

Application Entry Point

<?php
// Include autoloader
require_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 OpenTelemetry
Bootstrap::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();

Production Deployment

Docker Configuration

FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
curl \
git \
icu-dev \
libzip-dev \
oniguruma-dev \
autoconf \
g++ \
make \
mysql-client
# Install PHP extensions
RUN docker-php-ext-install \
pdo_mysql \
mbstring \
zip \
intl \
opcache
# Install OpenTelemetry extension
RUN pecl install opentelemetry && docker-php-ext-enable opentelemetry
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy composer files
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
# Copy application files
COPY . .
# Set permissions
RUN chown -R www-data:www-data /var/www/html
# Copy PHP configuration
COPY docker/php/php.ini /usr/local/etc/php/php.ini
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
USER www-data
EXPOSE 9000
CMD ["php-fpm"]

Testing the Integration

  1. Start your application

    # Local development with PHP built-in server
    php -S localhost:8000 -t public
    # Or with Docker
    docker-compose up
  2. Test the endpoints

    # Health check
    curl http://localhost:8000/health
    # API endpoints
    curl http://localhost:8000/api/users
    curl http://localhost:8000/api/users/1
    # Create a user
    curl -X POST http://localhost:8000/api/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Test User", "email": "test@example.com", "password": "password123"}'
    # Update a user
    curl -X PUT http://localhost:8000/api/users/1 \
    -H "Content-Type: application/json" \
    -d '{"name": "Updated User"}'
    # Delete a user
    curl -X DELETE http://localhost:8000/api/users/1
    # Get stats
    curl http://localhost:8000/api/stats
  3. 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