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 is a PHP micro-framework, a lighter alternative to the full-featured Laravel and Symfony.

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.

TL;DR

Install the opentelemetry and mongodb PECL extensions, then set OTEL_PHP_AUTOLOAD_ENABLED=true - this single env var activates the opentelemetry-auto-slim package, which automatically instruments every Slim 4 route with no middleware code. For Slim 3, add a TelemetryMiddleware manually since opentelemetry-auto-slim only supports Slim 4+. Register a shutdown handler in both cases to ensure PHP-FPM workers flush telemetry before recycling.

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.

Running this in production

Storing and querying this data at production volume is what base14 Scout does. Check out Scout APM.

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,environment=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.

Scout's UI filters on the lowercase environment key, so emit it alongside the OTel-native deployment.environment.name. The legacy deployment.environment is still accepted for backward compatibility.

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,environment=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.

FAQ

Why are no traces appearing from my Slim OpenTelemetry setup?

Work through four checks. Read the collector logs with docker compose logs otel-collector, confirm the Scout credentials are set, confirm OTEL_PHP_AUTOLOAD_ENABLED=true is exported, and confirm the extension is loaded with php -m | grep opentelemetry.

Why is the OpenTelemetry PHP extension not loaded?

Verify the install with pecl list | grep opentelemetry, check that php.ini carries the extension=opentelemetry.so directive, and restart PHP-FPM if you run under FastCGI. The extension is loaded per SAPI, so a CLI check passing does not mean the web request path has it.

Why are no MongoDB spans appearing in my Slim application?

Confirm opentelemetry-auto-mongodb is installed with composer show | grep auto-mongodb, that OTEL_PHP_AUTOLOAD_ENABLED=true is set, and that the MongoDB PHP extension itself is loaded with php -m | grep mongodb.

Why do Slim 3 span names show raw paths instead of route patterns?

Set determineRouteBeforeAppMiddleware to true in your Slim 3 settings. Without it the telemetry middleware cannot read the matched route pattern, so span names carry high-cardinality paths like GET /api/articles/abc123 instead of GET /api/articles/{id}.

Why is telemetry lost when a PHP-FPM process exits?

PHP-FPM workers can exit before the SDK flushes its buffer. Make sure the shutdown handler is registered and loaded early through src/telemetry.php. Without it, spans from the final request before worker recycling are dropped.

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?