Skip to content
Last9
Book demo

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