Skip to main content

Slim Framework

Implement OpenTelemetry instrumentation for Slim Framework applications to enable distributed tracing, metrics, and log correlation. This guide covers both Slim 4 (with fully automatic HTTP span instrumentation via opentelemetry-auto-slim) and Slim 3 (with a manual TelemetryMiddleware). The ~70% of setup that is identical between versions — environment variables, shutdown handlers, metrics, MongoDB auto-instrumentation, Docker deployment — is shared throughout.

Slim applications benefit from the OpenTelemetry PHP ecosystem: automatic MongoDB query tracing, Monolog log-trace correlation, and business metric counters — all with minimal application code. Whether you are building a new API on Slim 4 or maintaining a legacy Slim 3 service, this guide provides production-ready configurations for PHP-FPM deployments with base14 Scout.

Note: This guide provides a practical Slim-focused overview based on the official OpenTelemetry documentation. For complete PHP language information, please consult the official OpenTelemetry PHP documentation.

Slim 3 End-of-Life

Slim 3 is EOL and produces deprecation warnings on PHP 8.4. The opentelemetry-auto-slim package only supports Slim 4+, so HTTP spans must be created manually. If you are starting a new project, use Slim 4.

Prerequisites​

Compatibility Matrix​

ComponentSlim 4Slim 3
PHP8.1+ (8.4 recommended)8.0–8.4 (deprecation warnings suppressed)
Slim^4.15~3.12
opentelemetry-auto-slim^1.3Not supported
HTTP span creationAutomaticManual (TelemetryMiddleware)
MongoDB auto-spans^0.2^0.2
OTel SDK^1.13^1.13
Composer2.0+2.0+

You also need:

  • Scout Collector configured and accessible — see Docker Compose Setup for local development
  • Build tools for compiling the OpenTelemetry PHP extension (gcc, make, autoconf)

Installation​

Step 1: Install PHP Extensions​

Ubuntu 24.04 LTS​

Install PHP 8.4, the build toolchain, and PECL in one shot:

sudo apt-get update
sudo apt-get install -y php8.4-cli php8.4-dev php8.4-fpm php8.4-mbstring \
php8.4-zip php8.4-curl gcc make autoconf pkg-config libssl-dev

If your system ships an older PHP, add the Ondrej PPA first:

sudo add-apt-repository ppa:ondrej/php
sudo apt-get update

Then install the extensions:

sudo pecl install opentelemetry mongodb

Other Platforms​

# macOS (Homebrew)
brew install php@8.4 autoconf pkg-config
pecl install opentelemetry mongodb

# Alpine Linux (Docker) — see Dockerfile section below
apk add --no-cache autoconf build-base
pecl install opentelemetry mongodb

All Platforms​

Install Composer 2 if you haven't already:

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer

Enable them in your php.ini:

php.ini
extension=opentelemetry
extension=mongodb

Verify the extensions are loaded:

php -m | grep -E "opentelemetry|mongodb"

Step 2: Install Composer Packages​

composer require \
slim/slim:^4.15 \
slim/psr7:^1.8 \
php-di/php-di:^7.1 \
open-telemetry/sdk:^1.13 \
open-telemetry/exporter-otlp:^1.4 \
open-telemetry/opentelemetry-auto-slim:^1.3 \
open-telemetry/opentelemetry-auto-mongodb:^0.2 \
open-telemetry/opentelemetry-logger-monolog:^1.1 \
php-http/guzzle7-adapter:^1.1 \
guzzlehttp/psr7:^2.8

What each package does:

PackagePurpose
open-telemetry/sdkCore OTel PHP SDK (creates spans, manages context)
open-telemetry/exporter-otlpSends telemetry over OTLP protocol
opentelemetry-auto-slimAuto-instruments every Slim 4 route (Slim 4 only)
opentelemetry-auto-mongodbAuto-creates spans for all MongoDB driver operations
opentelemetry-logger-monologBridges Monolog to OTel logs with automatic traceId/spanId
guzzle7-adapter + psr7HTTP transport for the OTLP exporter
slim/psr7 + php-di/php-diPSR-7 implementation and DI container (Slim 4 only)

Environment Variables​

OTel auto-configures via environment. Set these before your app starts (in your shell, .env, or process manager):

.env
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=my-slim-app
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development

OTEL_PHP_AUTOLOAD_ENABLED=true is the key switch. It tells the SDK to automatically discover and activate the auto-instrumentation packages (like opentelemetry-auto-slim and opentelemetry-auto-mongodb). Without it, the packages sit idle.

Use deployment.environment.name (not the deprecated deployment.environment).

Scout Collector Integration​

When using Scout Collector, configure your application to send telemetry data with OAuth2 authentication:

.env
# Scout Collector Configuration
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-tenant.base14.io/v1/traces
SCOUT_CLIENT_ID=your_client_id
SCOUT_CLIENT_SECRET=your_client_secret
SCOUT_TOKEN_URL=https://your-tenant.base14.io/oauth/token

Scout Dashboard Integration: After configuration, your traces will appear in the Scout Dashboard. Navigate to the Traces section to view request flows, identify performance bottlenecks, and analyze distributed transactions.

Bootstrap​

Slim 4 requires a PSR-11 container (PHP-DI) and explicit PSR-7 implementation (slim/psr7):

public/index.php
<?php

use DI\ContainerBuilder;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\StatusCode;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->safeLoad();

require __DIR__ . '/../src/telemetry.php';

$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../src/dependencies.php');
$container = $builder->build();

AppFactory::setContainer($container);
$app = AppFactory::create();

$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();

require __DIR__ . '/../src/routes.php';

$displayErrors = ($_ENV['APP_DEBUG'] ?? 'false') === 'true';
$errorMiddleware = $app->addErrorMiddleware($displayErrors, true, true);
$errorMiddleware->setDefaultErrorHandler(function (
ServerRequestInterface $request,
\Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails,
) use ($app) {
// Record exception on the auto-instrumented span
$span = Span::getCurrent();
$span->recordException($exception);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());

$logger = $app->getContainer()->get(LoggerInterface::class);
$logger->error('Unhandled exception', [
'exception' => $exception,
'uri' => (string) $request->getUri(),
'method' => $request->getMethod(),
]);

$statusCode = 500;
if ($exception instanceof \Slim\Exception\HttpException) {
$statusCode = $exception->getCode();
}

$response = $app->getResponseFactory()->createResponse($statusCode);
$response->getBody()->write(json_encode([
'error' => $displayErrorDetails
? $exception->getMessage()
: 'Internal server error',
]));

return $response->withHeader('Content-Type', 'application/json');
});

$app->run();

The three lines that record exceptions on spans (Span::getCurrent(), recordException, setStatus) are the only OTel API calls in the entire entry point.

Shutdown Handler​

PHP-FPM workers can exit before the SDK flushes its buffer. Register a shutdown handler to force-flush all providers on process exit.

Create src/Telemetry/Shutdown.php:

src/Telemetry/Shutdown.php
<?php

namespace App\Telemetry;

use OpenTelemetry\API\Globals;

class Shutdown
{
public static function register(): void
{
register_shutdown_function([self::class, 'flush']);

if (!extension_loaded('pcntl')) {
return;
}

pcntl_async_signals(true);
$handler = function () {
self::flush();
exit(0);
};

pcntl_signal(SIGTERM, $handler);
pcntl_signal(SIGINT, $handler);
}

public static function flush(): void
{
try {
$tp = Globals::tracerProvider();
if (method_exists($tp, 'forceFlush')) {
$tp->forceFlush();
}

$mp = Globals::meterProvider();
if (method_exists($mp, 'forceFlush')) {
$mp->forceFlush();
}

$lp = Globals::loggerProvider();
if (method_exists($lp, 'forceFlush')) {
$lp->forceFlush();
}
} catch (\Throwable $e) {
// swallow — nothing useful to do during shutdown
}
}
}

Bootstrap it early in src/telemetry.php:

src/telemetry.php
<?php
use App\Telemetry\Shutdown;
Shutdown::register();

This file is required from public/index.php before the Slim app is created.

HTTP Request Tracing​

With opentelemetry-auto-slim installed and OTEL_PHP_AUTOLOAD_ENABLED=true, every Slim 4 request automatically gets:

  • Root SERVER span named {METHOD} {route_pattern} (e.g. GET /api/articles/{id})
  • Controller-level INTERNAL span (e.g. ArticleController::create)
  • Semantic convention attributes (http.request.method, http.route, http.response.status_code)
  • HTTP server metrics (request duration, count)

No manual TelemetryMiddleware is needed. Do not create duplicate HTTP metrics or span attributes in your application code.

Structured Logging​

Wire Monolog with both a stderr handler and the OTel log handler. The OTel handler automatically attaches traceId and spanId to every log record — no manual trace context injection needed.

Register in your PHP-DI definitions file:

src/dependencies.php
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use OpenTelemetry\API\Globals;
use OpenTelemetry\Contrib\Logs\Monolog\Handler as OtelLogHandler;
use Psr\Log\LoggerInterface;

return [
LoggerInterface::class => function () {
$logger = new Logger('slim-app');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));

try {
$loggerProvider = Globals::loggerProvider();
$logger->pushHandler(new OtelLogHandler($loggerProvider, Logger::DEBUG));
} catch (\Throwable $e) {
// OTel logger not available, continue with stderr only
}

return $logger;
},
];

Business Metrics​

Create counters with an app. namespace prefix. Do not add a .total suffix to counter names — the metric type already implies it. Use attributes for differentiation instead of separate counters:

src/Telemetry/Metrics.php
<?php

namespace App\Telemetry;

use OpenTelemetry\API\Globals;

class Metrics
{
private static function getCounter(string $name, string $desc)
{
return Globals::meterProvider()
->getMeter('slim-app')
->createCounter($name, '', $desc);
}

public static function authLoginSuccess(): void
{
self::getCounter('app.user.logins', 'User login attempts')
->add(1, ['result' => 'success']);
}

public static function authLoginFailed(): void
{
self::getCounter('app.user.logins', 'User login attempts')
->add(1, ['result' => 'failure']);
}

public static function articleCreated(): void
{
self::getCounter('app.article.creates', 'Articles created')
->add(1);
}
}

Call these from your controllers as one-liners:

Metrics::authLoginSuccess();
Metrics::articleCreated();

No span wrapping or trace context management needed. The counters flow through the OTEL_METRICS_EXPORTER=otlp pipeline independently.

Controllers​

With auto-instrumentation (Slim 4) or TelemetryMiddleware (Slim 3) handling traces, your controllers stay focused on business logic. The only OTel touchpoint is the Metrics::* one-liner calls.

src/Controllers/ArticleController.php
use App\Repositories\ArticleRepository;
use App\Telemetry\Metrics;
use Psr\Log\LoggerInterface;

class ArticleController
{
private ArticleRepository $articleRepository;
private LoggerInterface $logger;

public function __construct(
ArticleRepository $articleRepository,
LoggerInterface $logger
) {
$this->articleRepository = $articleRepository;
$this->logger = $logger;
}

public function create($request, $response)
{
$data = $request->getParsedBody();
$user = $request->getAttribute('user');

if (empty($data['title']) || empty($data['body'])) {
$this->logger->warning('Article validation failed',
['reason' => 'missing fields']);
return $this->json($response,
['error' => 'Title and body are required'], 422);
}

$data['author_id'] = $user['sub'];
$article = $this->articleRepository->create($data);
Metrics::articleCreated();

$this->logger->info('Article created',
['article.id' => $article['id'],
'user.id' => $user['sub']]);

$response->getBody()->write(json_encode([
'article' => $article,
]));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus(201);
}
}

Slim 4 uses constructor injection via PHP-DI. The $response->getBody()->write() pattern is standard PSR-7.

Docker Deployment​

Dockerfile​

Use a 2-stage build to keep the runtime image small. The builder stage installs Composer dependencies; the runtime stage installs the mongodb and opentelemetry PECL extensions:

Dockerfile
# syntax=docker/dockerfile:1

ARG PHP_VERSION=8.4

# Stage 1: Build dependencies
FROM php:${PHP_VERSION}-cli AS builder

WORKDIR /app

RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
git unzip libzip-dev libssl-dev pkg-config && \
pecl install mongodb && \
docker-php-ext-enable mongodb && \
docker-php-ext-install zip && \
rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY composer.json ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist \
--ignore-platform-req=ext-opentelemetry

COPY . .
RUN composer dump-autoload --optimize --no-scripts

# Stage 2: Runtime
FROM php:${PHP_VERSION}-fpm

WORKDIR /var/www/html

RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
curl libzip-dev libssl-dev libonig-dev libfcgi-bin pkg-config && \
pecl install mongodb opentelemetry && \
docker-php-ext-enable mongodb opentelemetry && \
docker-php-ext-install zip mbstring && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY config/php.ini /usr/local/etc/php/conf.d/99-app.ini
COPY config/php-fpm.conf /usr/local/etc/php-fpm.d/zz-app.conf

RUN groupadd --gid 1000 slim && \
useradd --uid 1000 --gid slim --shell /bin/bash --create-home slim

COPY --from=builder --chown=slim:slim /app /var/www/html

USER slim

EXPOSE 9000

CMD ["php-fpm"]

For Slim 3, the php.ini should include the error_reporting suppression line from the Installation section.

Docker Compose​

Use YAML anchors to share OTel environment variables across services:

compose.yml
x-otel-env: &otel-env
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_PHP_AUTOLOAD_ENABLED: "true"
OTEL_RESOURCE_ATTRIBUTES: deployment.environment.name=development

x-mongo-env: &mongo-env
MONGO_URI: mongodb://mongo:27017
MONGO_DATABASE: slim_app

services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.144.0
ports:
- "4317:4317"
- "4318:4318"
- "13133:13133"
volumes:
- ./config/otel-config.yaml:/etc/otelcol-contrib/config.yaml
environment:
- SCOUT_ENDPOINT=${SCOUT_ENDPOINT}
- SCOUT_CLIENT_ID=${SCOUT_CLIENT_ID}
- SCOUT_CLIENT_SECRET=${SCOUT_CLIENT_SECRET}
- SCOUT_TOKEN_URL=${SCOUT_TOKEN_URL}
restart: unless-stopped

mongo:
image: mongo:8
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

app:
build:
context: .
dockerfile: Dockerfile
environment:
<<: [*otel-env, *mongo-env]
OTEL_SERVICE_NAME: my-slim-app
JWT_SECRET: ${JWT_SECRET:-change-this-secret}
APP_DEBUG: "true"
depends_on:
mongo:
condition: service_healthy
otel-collector:
condition: service_started
restart: unless-stopped

nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./public:/var/www/html/public:ro
depends_on:
app:
condition: service_started
restart: unless-stopped

volumes:
mongo-data:

Start everything with:

docker compose up --build

Verification​

Start the collector and hit an endpoint:

curl -X POST http://localhost:8080/api/articles \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Hello","body":"World"}'

Slim 4 produces a 3-level span hierarchy, all from auto-instrumentation:

POST /api/articles              (SERVER   - auto-slim)
+-- ArticleController::create (INTERNAL - auto-slim)
+-- MongoDB articles.insert (CLIENT - auto-mongodb)

Check the collector output for:

  • Spans with your service name and proper parent-child nesting
  • Logs with traceId/spanId correlation
  • Metrics with app. prefix (e.g. app.user.logins, app.article.creates)

Troubleshooting​

No traces appearing​

  1. Check collector logs: docker compose logs otel-collector
  2. Verify Scout credentials are set correctly
  3. Ensure OTEL_PHP_AUTOLOAD_ENABLED=true is set
  4. Check extension: docker exec <container> php -m | grep opentelemetry

OpenTelemetry extension not loaded​

  1. Verify extension installation: pecl list | grep opentelemetry
  2. Check php.ini includes the extension directive: php --ini && php -m | grep opentelemetry
  3. Restart PHP-FPM if using FastCGI: kill -USR2 1 (inside the container)

No MongoDB spans​

  1. Verify opentelemetry-auto-mongodb is installed: composer show | grep auto-mongodb
  2. Confirm OTEL_PHP_AUTOLOAD_ENABLED=true
  3. Check that the mongodb PHP extension is loaded: php -m | grep mongodb

Slim 3: span names show raw paths instead of route patterns​

Ensure determineRouteBeforeAppMiddleware is set to true in your Slim 3 settings. Without it, the TelemetryMiddleware cannot read the matched route pattern and span names will contain high-cardinality paths like GET /api/articles/abc123 instead of GET /api/articles/{id}.

Telemetry lost on process exit​

PHP-FPM workers can exit before the SDK flushes its buffer. Ensure the Shutdown handler is registered and loaded early via src/telemetry.php. Without it, spans from the final request before worker recycling may be lost.

What's Next​

Complete Example​

Working examples with full source code, Docker Compose setup, and test scripts:

composer.json
{
"name": "base14/slim4-mongodb-otel",
"type": "project",
"require": {
"php": "^8.4",
"slim/slim": "^4.15",
"slim/psr7": "^1.8",
"php-di/php-di": "^7.1",
"mongodb/mongodb": "^2.2",
"open-telemetry/sdk": "^1.13",
"open-telemetry/exporter-otlp": "^1.4",
"open-telemetry/opentelemetry-auto-slim": "^1.3",
"open-telemetry/opentelemetry-auto-mongodb": "^0.2",
"open-telemetry/opentelemetry-logger-monolog": "^1.1",
"php-http/guzzle7-adapter": "^1.1",
"guzzlehttp/psr7": "^2.8"
}
}

References​

Was this page helpful?