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.
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 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.
Storing and querying this data at production volume is what base14 Scout does. Check out Scout APM.
Prerequisites
Compatibility Matrix
| Component | Slim 4 | Slim 3 |
|---|---|---|
| PHP | 8.1+ (8.4 recommended) | 8.0–8.4 (deprecation warnings suppressed) |
| Slim | ^4.15 | ~3.12 |
opentelemetry-auto-slim | ^1.3 | Not supported |
| HTTP span creation | Automatic | Manual (TelemetryMiddleware) |
| MongoDB auto-spans | ^0.2 | ^0.2 |
| OTel SDK | ^1.13 | ^1.13 |
| Composer | 2.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
- Slim 4
- Slim 3
Enable them in your php.ini:
extension=opentelemetry
extension=mongodb
Enable them in your php.ini and suppress Slim 3 deprecation warnings:
extension=opentelemetry
extension=mongodb
; Slim 3 triggers deprecation warnings on PHP 8.4
error_reporting = E_ALL & ~E_DEPRECATED & ~E_NOTICE
Verify the extensions are loaded:
php -m | grep -E "opentelemetry|mongodb"
Step 2: Install Composer Packages
- Slim 4
- Slim 3
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
composer require \
slim/slim:~3.12 \
mongodb/mongodb:^2.0 \
monolog/monolog:^3.7 \
open-telemetry/sdk:^1.13 \
open-telemetry/exporter-otlp:^1.4 \
open-telemetry/opentelemetry-auto-mongodb:^0.2 \
open-telemetry/opentelemetry-logger-monolog:^1.1 \
php-http/guzzle7-adapter:^1.0 \
guzzlehttp/psr7:^2.7
Note: opentelemetry-auto-slim is not included because it only supports
Slim 4+. HTTP spans are created manually via TelemetryMiddleware (see
HTTP Request Tracing).
What each package does:
| Package | Purpose |
|---|---|
open-telemetry/sdk | Core OTel PHP SDK (creates spans, manages context) |
open-telemetry/exporter-otlp | Sends telemetry over OTLP protocol |
opentelemetry-auto-slim | Auto-instruments every Slim 4 route (Slim 4 only) |
opentelemetry-auto-mongodb | Auto-creates spans for all MongoDB driver operations |
opentelemetry-logger-monolog | Bridges Monolog to OTel logs with automatic traceId/spanId |
guzzle7-adapter + psr7 | HTTP transport for the OTLP exporter |
slim/psr7 + php-di/php-di | PSR-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):
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:
# 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
- Slim 3
Slim 4 requires a PSR-11 container (PHP-DI) and explicit PSR-7 implementation
(slim/psr7):
<?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.
The determineRouteBeforeAppMiddleware setting is required so the
TelemetryMiddleware can read the matched route pattern for span names:
<?php
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\StatusCode;
require __DIR__ . '/../vendor/autoload.php';
Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->safeLoad();
require __DIR__ . '/../src/telemetry.php';
$app = new \Slim\App([
'settings' => [
'displayErrorDetails' => ($_ENV['APP_DEBUG'] ?? 'false') === 'true',
'addContentLengthHeader' => false,
'determineRouteBeforeAppMiddleware' => true,
],
]);
require __DIR__ . '/../src/dependencies.php';
require __DIR__ . '/../src/middleware.php';
require __DIR__ . '/../src/routes.php';
$container = $app->getContainer();
$container['errorHandler'] = function ($c) {
return function ($request, $response, $exception) use ($c) {
$span = Span::getCurrent();
$span->recordException($exception);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
$c['logger']->error('Unhandled exception', [
'exception' => $exception,
'uri' => (string) $request->getUri(),
'method' => $request->getMethod(),
]);
$statusCode = 500;
if (method_exists($exception, 'getCode')
&& $exception->getCode() >= 400
&& $exception->getCode() < 600) {
$statusCode = $exception->getCode();
}
return $response->withJson([
'error' => ($c['settings']['displayErrorDetails'] ?? false)
? $exception->getMessage()
: 'Internal server error',
], $statusCode);
};
};
$app->run();
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:
<?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:
<?php
use App\Telemetry\Shutdown;
Shutdown::register();
This file is required from public/index.php before the Slim app is created.
HTTP Request Tracing
- Slim 4
- Slim 3
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.
Since opentelemetry-auto-slim doesn't support Slim 3, create a
TelemetryMiddleware that produces the root SERVER span:
<?php
namespace App\Middleware;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class TelemetryMiddleware
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
): ResponseInterface {
$tracer = Globals::tracerProvider()->getTracer('slim-app');
$method = $request->getMethod();
$path = (string) $request->getUri()->getPath();
$span = $tracer->spanBuilder("$method $path")
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
$span->setAttribute('http.method', $method);
$span->setAttribute('http.url', (string) $request->getUri());
$span->setAttribute('http.target', $path);
$span->setAttribute('http.scheme',
$request->getUri()->getScheme() ?: 'http');
try {
$response = $next($request, $response);
// After route matching, update span name to low-cardinality pattern
$route = $request->getAttribute('route');
if ($route instanceof \Slim\Route) {
$pattern = $route->getPattern();
$span->updateName("$method $pattern");
$span->setAttribute('http.route', $pattern);
}
$span->setAttribute('http.status_code',
$response->getStatusCode());
return $response;
} catch (\Exception $e) {
$span->recordException($e);
$span->setStatus(
StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$scope->detach();
$span->end();
}
}
}
Key details:
- Scope activation (
$span->activate()) makes this span current so MongoDB auto-spans and logs inherit the trace context - Span name updates from
/api/articles/abc123to/api/articles/{id}after route matching (low cardinality) determineRouteBeforeAppMiddleware: truein your Slim 3 settings is required for the route pattern to be available inside middleware- Do not set
STATUS_OKon success - leave as UNSET per OTel conventions - Do not set
STATUS_ERRORfor 4xx responses - only for exceptions
Register it in src/middleware.php:
$app->add(new \App\Middleware\TelemetryMiddleware());
Because Slim 3 uses LIFO middleware ordering, TelemetryMiddleware should be
added last so it executes first (outermost wrapper).
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.
- Slim 4
- Slim 3
Register in your PHP-DI definitions file:
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;
},
];
Register on the Pimple container:
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use OpenTelemetry\API\Globals;
use OpenTelemetry\Contrib\Logs\Monolog\Handler as OtelLogHandler;
$container['logger'] = 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:
<?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.
- Slim 4
- Slim 3
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.
use App\Telemetry\Metrics;
class ArticleController
{
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 $response->withJson(
['error' => 'Title and body are required'], 422);
}
$data['author_id'] = $user['sub'];
$article = $this->container['articleRepository']->create($data);
Metrics::articleCreated();
$this->logger->info('Article created',
['article.id' => $article['id'],
'user.id' => $user['sub']]);
return $response->withJson(['article' => $article], 201);
}
}
Slim 3 accesses dependencies via $this->container['...'] (Pimple) and uses the
convenience method $response->withJson().
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:
# 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 /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 /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:
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
- Slim 3
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)
Slim 3 produces a 2-level span hierarchy (no INTERNAL controller span):
POST /api/articles (SERVER - TelemetryMiddleware)
+-- MongoDB articles.insert (CLIENT - auto-mongodb)
Check the collector output for:
- Spans with your service name and proper parent-child nesting
- Logs with
traceId/spanIdcorrelation - Metrics with
app.prefix (e.g.app.user.logins,app.article.creates)
Troubleshooting
No traces appearing
- Check collector logs:
docker compose logs otel-collector - Verify Scout credentials are set correctly
- Ensure
OTEL_PHP_AUTOLOAD_ENABLED=trueis set - Check extension:
docker exec <container> php -m | grep opentelemetry
OpenTelemetry extension not loaded
- Verify extension installation:
pecl list | grep opentelemetry - Check
php.iniincludes the extension directive:php --ini && php -m | grep opentelemetry - Restart PHP-FPM if using FastCGI:
kill -USR2 1(inside the container)
No MongoDB spans
- Verify
opentelemetry-auto-mongodbis installed:composer show | grep auto-mongodb - Confirm
OTEL_PHP_AUTOLOAD_ENABLED=true - Check that the
mongodbPHP 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
- Custom PHP Instrumentation - add manual spans for business-critical operations
- Docker Compose Setup — configure Scout Collector for local development
- Creating Alerts - set up alerts for error rates, latency thresholds, and custom metrics
- Dashboard Creation — build custom dashboards combining traces, metrics, and business KPIs
Related Guides
- PHP Custom Instrumentation - Manual spans and advanced patterns
- All framework guides - Auto-instrumentation overview for every language
Complete Example
Working examples with full source code, Docker Compose setup, and test scripts:
- Slim 4: php84-slim4-mongodb
- Slim 3: php84-slim3-mongodb
- Slim 4 composer.json
- Slim 3 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"
}
}
{
"name": "base14/slim3-mongodb-otel",
"type": "project",
"require": {
"php": "^8.4",
"slim/slim": "~3.12",
"mongodb/mongodb": "^2.0",
"open-telemetry/sdk": "^1.13",
"open-telemetry/exporter-otlp": "^1.4",
"open-telemetry/opentelemetry-auto-mongodb": "^0.2",
"open-telemetry/opentelemetry-logger-monolog": "^1.1",
"php-http/guzzle7-adapter": "^1.0",
"guzzlehttp/psr7": "^2.7"
}
}
References
- Official OpenTelemetry PHP Documentation
- Slim 4 Documentation
- Slim 3 Documentation
- MongoDB PHP Library
Related Guides
- Docker Compose Setup - Set up collector for local development.
- PHP Custom Instrumentation - Manual spans and advanced patterns.
- WordPress - self-hosted WordPress on Apache or PHP-FPM.
- All framework guides - Auto-instrumentation overview for every language.