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
-
Install OpenTelemetry PHP extension
Install the PHP extension following the OpenTelemetry PHP setup guide:
# For Ubuntu/Debiansudo apt-get install php-devpecl install opentelemetry# For macOS with Homebrewbrew install phppecl install opentelemetry# Add to php.iniecho "extension=opentelemetry" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||") -
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 -
Install additional instrumentation packages (optional)
composer require \open-telemetry/opentelemetry-auto-pdo \open-telemetry/opentelemetry-auto-redis \predis/predis \monolog/monolog
Configuration
Environment Variables
# ApplicationAPP_NAME=Lumen-APIAPP_ENV=productionAPP_DEBUG=falseAPP_KEY=your-app-key-here
# OpenTelemetry ConfigurationOTEL_PHP_AUTOLOAD_ENABLED=trueOTEL_SERVICE_NAME=lumen-apiOTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpointOTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"OTEL_TRACES_EXPORTER=otlpOTEL_METRICS_EXPORTER=otlpOTEL_LOGS_EXPORTER=otlpOTEL_PROPAGATORS=baggage,tracecontextOTEL_TRACES_SAMPLER=always_onOTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"OTEL_LOG_LEVEL=error
# DatabaseDB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=lumen_dbDB_USERNAME=userDB_PASSWORD=password
# CacheCACHE_DRIVER=redisREDIS_HOST=127.0.0.1REDIS_PASSWORD=nullREDIS_PORT=6379
# QueueQUEUE_CONNECTION=redisCreate config/otel.php and register it in bootstrap/app.php:
<?php
return [ /* |-------------------------------------------------------------------------- | OpenTelemetry Configuration |-------------------------------------------------------------------------- | | Configuration settings for OpenTelemetry instrumentation | */
'enabled' => env('OTEL_PHP_AUTOLOAD_ENABLED', true),
'service_name' => env('OTEL_SERVICE_NAME', env('APP_NAME', 'lumen-api')),
'exporter' => [ 'otlp' => [ 'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT'), 'headers' => env('OTEL_EXPORTER_OTLP_HEADERS'), ], ],
'resource_attributes' => [ 'service.name' => env('OTEL_SERVICE_NAME', env('APP_NAME', 'lumen-api')), 'service.version' => env('OTEL_SERVICE_VERSION', '1.0.0'), 'deployment.environment' => env('APP_ENV', 'production'), 'framework.name' => 'lumen', 'framework.version' => app()->version(), ],
'sampling' => [ 'traces' => [ 'sampler' => env('OTEL_TRACES_SAMPLER', 'always_on'), 'probability' => env('OTEL_TRACES_SAMPLER_PROBABILITY', 1.0), ], ],
'propagators' => explode(',', env('OTEL_PROPAGATORS', 'baggage,tracecontext')),
'log_level' => env('OTEL_LOG_LEVEL', 'error'),];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;<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;use OpenTelemetry\API\Globals;use OpenTelemetry\API\Instrumentation\CachedInstrumentation;use OpenTelemetry\API\Trace\TracerInterface;use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;use OpenTelemetry\Contrib\Otlp\SpanExporter;use OpenTelemetry\SDK\Common\Attribute\Attributes;use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;use OpenTelemetry\SDK\Resource\ResourceInfo;use OpenTelemetry\SDK\Resource\ResourceInfoFactory;use OpenTelemetry\SDK\Sdk;use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;use OpenTelemetry\SDK\Trace\Sampler\ParentBased;use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;use OpenTelemetry\SDK\Trace\TracerProvider;use OpenTelemetry\SemConv\ResourceAttributes;
class OpenTelemetryServiceProvider extends ServiceProvider{ public function register() { $this->app->singleton(TracerInterface::class, function ($app) { return $this->createTracer(); });
$this->app->singleton('otel.instrumentation', function ($app) { return new CachedInstrumentation('lumen-api', '1.0.0'); }); }
public function boot() { if (!config('otel.enabled', true)) { return; }
$this->initializeOpenTelemetry(); }
private function createTracer(): TracerInterface { return Globals::tracerProvider()->getTracer( config('otel.service_name', 'lumen-api'), '1.0.0' ); }
private function initializeOpenTelemetry(): void { $resource = $this->createResource(); $tracerProvider = $this->createTracerProvider($resource);
$sdk = Sdk::builder() ->setTracerProvider($tracerProvider) ->setAutoShutdown(true) ->build(); }
private function createResource(): ResourceInfo { $attributes = config('otel.resource_attributes', []);
return ResourceInfoFactory::create( Attributes::create($attributes), ResourceAttributes::SCHEMA_URL ); }
private function createTracerProvider(ResourceInfo $resource): TracerProvider { $exporter = $this->createExporter();
$spanProcessor = new BatchSpanProcessor( $exporter, Globals::clockInterface(), 512, // maxExportBatchSize 5000, // scheduleDelayMillis 30000, // exportTimeoutMillis 2048 // maxQueueSize );
return TracerProvider::builder() ->addSpanProcessor($spanProcessor) ->setResource($resource) ->setSampler(new ParentBased(new AlwaysOnSampler())) ->build(); }
private function createExporter(): SpanExporter { $endpoint = config('otel.exporter.otlp.endpoint'); $headers = [];
if ($authHeader = config('otel.exporter.otlp.headers')) { if (str_starts_with($authHeader, 'Authorization=')) { $headers['Authorization'] = substr($authHeader, 14); } }
if ($endpoint) { $transport = (new OtlpHttpTransportFactory())->create( $endpoint . '/v1/traces', 'application/json', $headers );
return new SpanExporter($transport); }
// Fallback to console exporter for development return new SpanExporter( (new StreamTransportFactory())->create(STDOUT, 'application/json') ); }}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; }}<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;use OpenTelemetry\API\Instrumentation\CachedInstrumentation;use OpenTelemetry\API\Trace\Span;use OpenTelemetry\API\Trace\SpanKind;use OpenTelemetry\API\Trace\StatusCode;
class TelemetryService{ private CachedInstrumentation $instrumentation;
public function __construct() { $this->instrumentation = new CachedInstrumentation('lumen-service', '1.0.0'); }
public function traceOperation(string $operationName, callable $operation, array $attributes = []): mixed { $tracer = $this->instrumentation->tracer();
$span = $tracer->spanBuilder($operationName) ->setSpanKind(SpanKind::KIND_INTERNAL) ->setAttributes($attributes) ->startSpan();
$scope = $span->activate();
try { $result = $operation($span); $span->setStatus(StatusCode::STATUS_OK); return $result;
} catch (\Throwable $e) { $span->recordException($e); $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
Log::error('Operation failed', [ 'operation' => $operationName, 'error' => $e->getMessage(), 'trace_id' => $span->getContext()->getTraceId(), 'span_id' => $span->getContext()->getSpanId(), ]);
throw $e;
} finally { $span->end(); $scope->detach(); } }
public function getCurrentSpan(): ?Span { return Span::getCurrent(); }
public function addAttributes(array $attributes): void { $span = $this->getCurrentSpan(); if ($span) { $span->setAttributes($attributes); } }
public function addEvent(string $name, array $attributes = []): void { $span = $this->getCurrentSpan(); if ($span) { $span->addEvent($name, $attributes); } }
public function recordException(\Throwable $exception): void { $span = $this->getCurrentSpan(); if ($span) { $span->recordException($exception); } }}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); }}<?php
namespace App\Http\Controllers;
use App\Models\User;use App\Services\UserService;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Support\Facades\Validator;use OpenTelemetry\API\Trace\Span;
class UserController extends ApiController{ private UserService $userService;
public function __construct(TelemetryService $telemetry, UserService $userService) { parent::__construct($telemetry); $this->userService = $userService; }
public function index(Request $request): JsonResponse { return $this->telemetry->traceOperation('user_controller.index', function (Span $span) use ($request) { $page = (int) $request->get('page', 1); $perPage = (int) $request->get('per_page', 15); $search = $request->get('search');
$span->setAttributes([ 'controller' => 'UserController', 'action' => 'index', 'page' => $page, 'per_page' => $perPage, 'has_search' => !empty($search), ]);
$users = $this->userService->getUsers($page, $perPage, $search);
$span->setAttributes([ 'users.count' => count($users['data']), 'users.total' => $users['total'], ]);
return $this->successResponse($users, 'Users retrieved successfully'); }, [ 'operation' => 'list_users', 'controller' => 'UserController', ]); }
public function store(Request $request): JsonResponse { return $this->telemetry->traceOperation('user_controller.store', function (Span $span) use ($request) { $span->setAttributes([ 'controller' => 'UserController', 'action' => 'store', ]);
// Validate request $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email', 'password' => 'required|string|min:8', ]);
if ($validator->fails()) { $span->setAttributes([ 'validation.failed' => true, 'validation.errors' => array_keys($validator->errors()->toArray()), ]);
return $this->errorResponse( 'Validation failed', 422, $validator->errors()->toArray() ); }
$validatedData = $validator->validated(); $span->setAttributes(['user.email' => $validatedData['email']]);
$user = $this->userService->createUser($validatedData);
$span->setAttributes([ 'user.created' => true, 'user.id' => $user['id'], ]);
return $this->successResponse($user, 'User created successfully', 201); }, [ 'operation' => 'create_user', 'controller' => 'UserController', ]); }
public function show(int $id): JsonResponse { return $this->telemetry->traceOperation('user_controller.show', function (Span $span) use ($id) { $span->setAttributes([ 'controller' => 'UserController', 'action' => 'show', 'user.id' => $id, ]);
$user = $this->userService->getUserById($id);
if (!$user) { $span->setAttributes(['user.found' => false]); return $this->errorResponse('User not found', 404); }
$span->setAttributes([ 'user.found' => true, 'user.email' => $user['email'], ]);
return $this->successResponse($user, 'User retrieved successfully'); }, [ 'operation' => 'show_user', 'controller' => 'UserController', ]); }
public function update(Request $request, int $id): JsonResponse { return $this->telemetry->traceOperation('user_controller.update', function (Span $span) use ($request, $id) { $span->setAttributes([ 'controller' => 'UserController', 'action' => 'update', 'user.id' => $id, ]);
// Validate request $validator = Validator::make($request->all(), [ 'name' => 'sometimes|required|string|max:255', 'email' => 'sometimes|required|email|unique:users,email,' . $id, 'password' => 'sometimes|required|string|min:8', ]);
if ($validator->fails()) { $span->setAttributes([ 'validation.failed' => true, 'validation.errors' => array_keys($validator->errors()->toArray()), ]);
return $this->errorResponse( 'Validation failed', 422, $validator->errors()->toArray() ); }
$validatedData = $validator->validated(); $span->setAttributes(['fields_to_update' => array_keys($validatedData)]);
$user = $this->userService->updateUser($id, $validatedData);
if (!$user) { $span->setAttributes(['user.found' => false]); return $this->errorResponse('User not found', 404); }
$span->setAttributes([ 'user.updated' => true, 'user.email' => $user['email'], ]);
return $this->successResponse($user, 'User updated successfully'); }, [ 'operation' => 'update_user', 'controller' => 'UserController', ]); }
public function destroy(int $id): JsonResponse { return $this->telemetry->traceOperation('user_controller.destroy', function (Span $span) use ($id) { $span->setAttributes([ 'controller' => 'UserController', 'action' => 'destroy', 'user.id' => $id, ]);
$deleted = $this->userService->deleteUser($id);
if (!$deleted) { $span->setAttributes(['user.found' => false]); return $this->errorResponse('User not found', 404); }
$span->setAttributes(['user.deleted' => true]);
return $this->successResponse(null, 'User deleted successfully'); }, [ 'operation' => 'delete_user', 'controller' => 'UserController', ]); }}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 dependenciesRUN apk add --no-cache \ curl \ git \ icu-dev \ libzip-dev \ oniguruma-dev \ autoconf \ g++ \ make
# Install PHP extensionsRUN docker-php-ext-install \ pdo_mysql \ mbstring \ zip \ intl \ opcache
# Install OpenTelemetry extensionRUN pecl install opentelemetry && docker-php-ext-enable opentelemetry
# Install ComposerCOPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directoryWORKDIR /var/www/html
# Copy composer filesCOPY composer.json composer.lock ./RUN composer install --no-dev --optimize-autoloader --no-scripts
# Copy application filesCOPY . .
# Set permissionsRUN chown -R www-data:www-data /var/www/htmlRUN chmod -R 755 /var/www/html/storage
# Copy PHP configurationCOPY docker/php/php.ini /usr/local/etc/php/php.iniCOPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# Generate optimized autoloadRUN composer dump-autoload --optimize
USER www-data
EXPOSE 9000
CMD ["php-fpm"]version: "3.8"
services: app: build: . container_name: lumen-api environment: # OpenTelemetry - 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_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0
# Lumen - APP_ENV=production - APP_DEBUG=false - DB_CONNECTION=mysql - DB_HOST=mysql - DB_DATABASE=lumen_db - DB_USERNAME=lumen_user - DB_PASSWORD=lumen_password - CACHE_DRIVER=redis - REDIS_HOST=redis volumes: - ./storage/logs:/var/www/html/storage/logs depends_on: - mysql - redis
nginx: image: nginx:alpine container_name: lumen-nginx ports: - "80:80" volumes: - .:/var/www/html - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - app
mysql: image: mysql:8.0 container_name: lumen-mysql environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: lumen_db MYSQL_USER: lumen_user MYSQL_PASSWORD: lumen_password volumes: - mysql_data:/var/lib/mysql
redis: image: redis:7-alpine container_name: lumen-redis volumes: - redis_data:/data
volumes: mysql_data: redis_data:Testing the Integration
-
Start your application
# Local developmentphp -S localhost:8000 -t public# Or with Dockerdocker-compose up -
Test the endpoints
# Health checkcurl http://localhost:8000/health# API endpointscurl http://localhost:8000/api/v1/userscurl http://localhost:8000/api/v1/users/1# Create a usercurl -X POST http://localhost:8000/api/v1/users \-H "Content-Type: application/json" \-d '{"name": "Test User", "email": "test@example.com", "password": "password123"}'# Get statscurl http://localhost:8000/api/v1/stats -
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