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

Lumen

Learn how to integrate OpenTelemetry with Lumen applications and send telemetry data to Last9

This guide shows you how to instrument your Lumen micro-framework application with OpenTelemetry and send traces, metrics, and logs to Last9.

Prerequisites

  • PHP 8.1 or later
  • Lumen 9.x or Lumen 10.x/11.x
  • Composer
  • 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. Install OpenTelemetry SDK and auto-instrumentation packages

    composer require \
    open-telemetry/sdk \
    open-telemetry/exporter-otlp \
    open-telemetry/opentelemetry-auto-lumen \
    open-telemetry/opentelemetry-auto-psr18 \
    php-http/guzzle7-adapter
  3. Install additional instrumentation packages (optional)

    composer require \
    open-telemetry/opentelemetry-auto-pdo \
    open-telemetry/opentelemetry-auto-redis \
    predis/predis \
    monolog/monolog

Configuration

Environment Variables

# Application
APP_NAME=Lumen-API
APP_ENV=production
APP_DEBUG=false
APP_KEY=your-app-key-here
# OpenTelemetry Configuration
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=lumen-api
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
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lumen_db
DB_USERNAME=user
DB_PASSWORD=password
# Cache
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# Queue
QUEUE_CONNECTION=redis

Bootstrap Configuration

<?php
require_once __DIR__.'/../vendor/autoload.php';
(new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
dirname(__DIR__)
))->bootstrap();
date_default_timezone_set(env('APP_TIMEZONE', 'UTC'));
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework.
|
*/
$app = new Laravel\Lumen\Application(
dirname(__DIR__)
);
$app->withFacades();
$app->withEloquent();
/*
|--------------------------------------------------------------------------
| Register Config Files
|--------------------------------------------------------------------------
*/
$app->configure('otel');
$app->configure('database');
$app->configure('cache');
/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
*/
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
*/
$app->register(App\Providers\OpenTelemetryServiceProvider::class);
$app->register(App\Providers\TelemetryServiceProvider::class);
/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
*/
$app->middleware([
App\Http\Middleware\OpenTelemetryMiddleware::class,
]);
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
'telemetry' => App\Http\Middleware\TelemetryMiddleware::class,
]);
/*
|--------------------------------------------------------------------------
| Register Routes
|--------------------------------------------------------------------------
*/
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
});
return $app;

Middleware Implementation

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SemConv\TraceAttributes;
class OpenTelemetryMiddleware
{
private CachedInstrumentation $instrumentation;
public function __construct()
{
$this->instrumentation = new CachedInstrumentation('lumen-middleware', '1.0.0');
}
public function handle(Request $request, Closure $next)
{
$tracer = $this->instrumentation->tracer();
$spanName = $this->getSpanName($request);
$span = $tracer->spanBuilder($spanName)
->setSpanKind(SpanKind::KIND_SERVER)
->setAttributes([
TraceAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
TraceAttributes::URL_FULL => $request->fullUrl(),
TraceAttributes::HTTP_ROUTE => $request->getPathInfo(),
TraceAttributes::USER_AGENT_ORIGINAL => $request->userAgent(),
TraceAttributes::CLIENT_ADDRESS => $request->ip(),
'lumen.version' => app()->version(),
'request.body.size' => $request->server('CONTENT_LENGTH', 0),
'request.query.count' => count($request->query()),
])
->startSpan();
$scope = $span->activate();
$startTime = hrtime(true);
try {
// Store span for use in controllers
$request->attributes->set('otel.span', $span);
$request->attributes->set('otel.context', Context::getCurrent());
$response = $next($request);
// Record response attributes
$duration = (hrtime(true) - $startTime) / 1_000_000;
$span->setAttributes([
TraceAttributes::HTTP_RESPONSE_STATUS_CODE => $response->getStatusCode(),
'response.body.size' => strlen($response->getContent()),
'http.request.duration_ms' => $duration,
]);
// Set span status based on HTTP status code
if ($response->getStatusCode() >= 400) {
$span->setStatus(StatusCode::STATUS_ERROR, 'HTTP ' . $response->getStatusCode());
} else {
$span->setStatus(StatusCode::STATUS_OK);
}
// Log request completion
Log::info('Request completed', [
'method' => $request->getMethod(),
'url' => $request->fullUrl(),
'status' => $response->getStatusCode(),
'duration_ms' => $duration,
'trace_id' => $span->getContext()->getTraceId(),
'span_id' => $span->getContext()->getSpanId(),
]);
return $response;
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
Log::error('Request failed', [
'method' => $request->getMethod(),
'url' => $request->fullUrl(),
'error' => $e->getMessage(),
'trace_id' => $span->getContext()->getTraceId(),
'span_id' => $span->getContext()->getSpanId(),
]);
throw $e;
} finally {
$span->end();
$scope->detach();
}
}
private function getSpanName(Request $request): string
{
$method = $request->getMethod();
$path = $request->getPathInfo();
// Normalize path for better span naming
$normalizedPath = $this->normalizePath($path);
return "{$method} {$normalizedPath}";
}
private function normalizePath(string $path): string
{
// Replace numeric IDs with placeholders
$path = preg_replace('/\/\d+/', '/{id}', $path);
// Replace UUIDs with placeholders
$path = preg_replace('/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/', '/{uuid}', $path);
return $path;
}
}

Controller Examples

<?php
namespace App\Http\Controllers;
use App\Services\TelemetryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Lumen\Routing\Controller as BaseController;
class ApiController extends BaseController
{
protected TelemetryService $telemetry;
public function __construct(TelemetryService $telemetry)
{
$this->telemetry = $telemetry;
}
protected function successResponse($data, string $message = 'Success', int $status = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $status);
}
protected function errorResponse(string $message = 'Error', int $status = 400, array $errors = []): JsonResponse
{
$response = [
'success' => false,
'message' => $message,
];
if (!empty($errors)) {
$response['errors'] = $errors;
}
return response()->json($response, $status);
}
}

Service Layer Integration

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use OpenTelemetry\API\Trace\Span;
class UserService
{
public function __construct(
private TelemetryService $telemetry
) {}
public function getUsers(int $page = 1, int $perPage = 15, ?string $search = null): array
{
return $this->telemetry->traceOperation('user_service.get_users', function (Span $span) use ($page, $perPage, $search) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'getUsers',
'page' => $page,
'per_page' => $perPage,
'has_search' => !empty($search),
]);
$query = User::query();
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
$span->setAttributes(['search_term' => $search]);
}
$total = $query->count();
$users = $query->skip(($page - 1) * $perPage)
->take($perPage)
->get()
->toArray();
$span->setAttributes([
'results.count' => count($users),
'results.total' => $total,
]);
return [
'data' => $users,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'pages' => ceil($total / $perPage),
];
});
}
public function getUserById(int $id): ?array
{
return $this->telemetry->traceOperation('user_service.get_user_by_id', function (Span $span) use ($id) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'getUserById',
'user.id' => $id,
]);
// Try cache first
$cacheKey = "user_{$id}";
$user = Cache::get($cacheKey);
if ($user) {
$span->setAttributes(['cache.hit' => true]);
return $user;
}
$span->setAttributes(['cache.hit' => false]);
$user = User::find($id);
if ($user) {
$userData = $user->toArray();
// Cache for 5 minutes
Cache::put($cacheKey, $userData, 300);
$span->setAttributes([
'user.found' => true,
'user.email' => $userData['email'],
'cache.stored' => true,
]);
return $userData;
}
$span->setAttributes(['user.found' => false]);
return null;
});
}
public function createUser(array $data): array
{
return $this->telemetry->traceOperation('user_service.create_user', function (Span $span) use ($data) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'createUser',
'user.email' => $data['email'],
]);
return DB::transaction(function () use ($data, $span) {
$span->addEvent('transaction_started');
// Hash password
if (isset($data['password'])) {
$data['password'] = Hash::make($data['password']);
}
// Create user
$user = User::create($data);
$userData = $user->toArray();
$span->setAttributes([
'user.created' => true,
'user.id' => $userData['id'],
]);
// Clear user list cache
$this->clearUserListCache();
$span->addEvent('transaction_completed');
return $userData;
});
});
}
public function updateUser(int $id, array $data): ?array
{
return $this->telemetry->traceOperation('user_service.update_user', function (Span $span) use ($id, $data) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'updateUser',
'user.id' => $id,
'fields_to_update' => array_keys($data),
]);
return DB::transaction(function () use ($id, $data, $span) {
$span->addEvent('transaction_started');
$user = User::find($id);
if (!$user) {
$span->setAttributes(['user.found' => false]);
return null;
}
// Hash password if provided
if (isset($data['password'])) {
$data['password'] = Hash::make($data['password']);
}
$user->update($data);
$userData = $user->fresh()->toArray();
$span->setAttributes([
'user.updated' => true,
'user.email' => $userData['email'],
]);
// Clear caches
$this->clearUserCache($id);
$this->clearUserListCache();
$span->addEvent('transaction_completed');
return $userData;
});
});
}
public function deleteUser(int $id): bool
{
return $this->telemetry->traceOperation('user_service.delete_user', function (Span $span) use ($id) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'deleteUser',
'user.id' => $id,
]);
return DB::transaction(function () use ($id, $span) {
$span->addEvent('transaction_started');
$user = User::find($id);
if (!$user) {
$span->setAttributes(['user.found' => false]);
return false;
}
$deleted = $user->delete();
$span->setAttributes([
'user.deleted' => $deleted,
'deletion_type' => 'soft_delete',
]);
// Clear caches
$this->clearUserCache($id);
$this->clearUserListCache();
$span->addEvent('transaction_completed');
return $deleted;
});
});
}
private function clearUserCache(int $id): void
{
$this->telemetry->traceOperation('user_service.clear_user_cache', function (Span $span) use ($id) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'clearUserCache',
'user.id' => $id,
]);
Cache::forget("user_{$id}");
$span->setAttributes(['cache.cleared' => true]);
});
}
private function clearUserListCache(): void
{
$this->telemetry->traceOperation('user_service.clear_user_list_cache', function (Span $span) {
$span->setAttributes([
'service' => 'UserService',
'operation' => 'clearUserListCache',
]);
// Clear any user list caches
Cache::tags(['users'])->flush();
$span->setAttributes(['cache.tags_cleared' => ['users']]);
});
}
}

Routes Definition

<?php
/** @var \Laravel\Lumen\Routing\Router $router */
/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
*/
$router->get('/', function () use ($router) {
return response()->json([
'message' => 'Welcome to Lumen API',
'version' => $router->app->version(),
'timestamp' => now()->toISOString(),
]);
});
// Health check
$router->get('/health', function () {
return response()->json([
'status' => 'healthy',
'timestamp' => now()->toISOString(),
'version' => app()->version(),
]);
});
// API Routes
$router->group(['prefix' => 'api/v1'], function () use ($router) {
// Users
$router->group(['prefix' => 'users'], function () use ($router) {
$router->get('/', 'UserController@index');
$router->post('/', 'UserController@store');
$router->get('/{id:[0-9]+}', 'UserController@show');
$router->put('/{id:[0-9]+}', 'UserController@update');
$router->delete('/{id:[0-9]+}', 'UserController@destroy');
});
// Stats endpoint
$router->get('/stats', function () {
return response()->json([
'users_count' => \App\Models\User::count(),
'memory_usage' => memory_get_usage(true),
'peak_memory' => memory_get_peak_usage(true),
]);
});
});

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
# 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 --no-scripts
# Copy application files
COPY . .
# Set permissions
RUN chown -R www-data:www-data /var/www/html
RUN chmod -R 755 /var/www/html/storage
# 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
# Generate optimized autoload
RUN composer dump-autoload --optimize
USER www-data
EXPOSE 9000
CMD ["php-fpm"]

Testing the Integration

  1. Start your application

    # Local development
    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/v1/users
    curl http://localhost:8000/api/v1/users/1
    # Create a user
    curl -X POST http://localhost:8000/api/v1/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Test User", "email": "test@example.com", "password": "password123"}'
    # Get stats
    curl http://localhost:8000/api/v1/stats
  3. View telemetry in Last9

    Check your Last9 dashboard for:

    • HTTP request traces with Lumen-specific attributes
    • Database operation spans with Eloquent ORM details
    • Cache operation traces
    • Custom business logic spans and events