Laravel
Learn how to integrate OpenTelemetry with Laravel applications and send telemetry data to Last9
This guide shows you how to instrument your Laravel application with OpenTelemetry and send traces, metrics, and logs to Last9.
Prerequisites
- PHP 8.1 or later
- Laravel 9.x or Laravel 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-laravel \open-telemetry/opentelemetry-auto-psr18 \open-telemetry/opentelemetry-auto-io \php-http/guzzle7-adapter -
Install additional instrumentation packages (optional)
composer require \open-telemetry/opentelemetry-auto-pdo \open-telemetry/opentelemetry-auto-slim \monolog/monolog \predis/predis
Configuration
Environment Variables
# ApplicationAPP_NAME=Laravel-AppAPP_ENV=productionAPP_DEBUG=false
# OpenTelemetry ConfigurationOTEL_PHP_AUTOLOAD_ENABLED=trueOTEL_SERVICE_NAME=laravel-appOTEL_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=laravel_dbDB_USERNAME=userDB_PASSWORD=password
# RedisREDIS_HOST=127.0.0.1REDIS_PASSWORD=nullREDIS_PORT=6379
# QueueQUEUE_CONNECTION=redis<?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', 'laravel-app')),
'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', 'laravel-app')), 'service.version' => env('OTEL_SERVICE_VERSION', '1.0.0'), 'deployment.environment' => env('APP_ENV', 'production'), 'framework.name' => 'laravel', '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'),];Auto-Instrumentation Setup
<?php
use Illuminate\Foundation\Application;use Illuminate\Foundation\Configuration\Exceptions;use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // Add OpenTelemetry middleware $middleware->web(append: [ \App\Http\Middleware\OpenTelemetryMiddleware::class, ]);
$middleware->api(append: [ \App\Http\Middleware\OpenTelemetryMiddleware::class, ]); }) ->withExceptions(function (Exceptions $exceptions) { // Exception handling will be instrumented automatically }) ->create();<?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(): void { $this->app->singleton(TracerInterface::class, function ($app) { return $this->createTracer(); });
$this->app->singleton('otel.instrumentation', function ($app) { return new CachedInstrumentation('laravel-app', '1.0.0'); }); }
public function boot(): void { if (!config('otel.enabled', true)) { return; }
$this->initializeOpenTelemetry(); }
private function createTracer(): TracerInterface { return Globals::tracerProvider()->getTracer( config('otel.service_name', 'laravel-app'), '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); } else { // Parse other headers if needed parse_str(str_replace(';', '&', $authHeader), $headers); } }
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') ); }}Custom Middleware and Services
OpenTelemetry Middleware
<?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;use Symfony\Component\HttpFoundation\Response;
class OpenTelemetryMiddleware{ private CachedInstrumentation $instrumentation;
public function __construct() { $this->instrumentation = new CachedInstrumentation('laravel-middleware', '1.0.0'); }
public function handle(Request $request, Closure $next): Response { $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->route()?->uri() ?? 'unknown', TraceAttributes::USER_AGENT_ORIGINAL => $request->userAgent(), TraceAttributes::CLIENT_ADDRESS => $request->ip(), 'laravel.route.name' => $request->route()?->getName(), 'laravel.route.action' => $request->route()?->getActionName(), 'request.body.size' => $request->server('CONTENT_LENGTH', 0), ]) ->startSpan();
$scope = $span->activate(); $startTime = hrtime(true);
try { // Store span in request 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; // Convert to milliseconds
$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(); $route = $request->route()?->uri() ?? $request->getPathInfo();
return "{$method} {$route}"; }}<?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('laravel-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\Http\Requests\StoreUserRequest;use App\Http\Requests\UpdateUserRequest;use App\Http\Resources\UserResource;use App\Models\User;use App\Services\TelemetryService;use App\Services\UserService;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class UserController extends Controller{ public function __construct( private UserService $userService, private TelemetryService $telemetry ) {}
public function index(Request $request): AnonymousResourceCollection { return $this->telemetry->traceOperation('user_controller.index', function (Span $span) use ($request) { $perPage = $request->get('per_page', 15); $search = $request->get('search');
$span->setAttributes([ 'controller' => 'UserController', 'action' => 'index', 'per_page' => $perPage, 'has_search' => !empty($search), ]);
$users = $this->userService->getUsers($perPage, $search);
$span->setAttributes([ 'users.count' => $users->count(), 'users.total' => $users->total(), ]);
return UserResource::collection($users); }, [ 'operation' => 'list_users', 'controller' => 'UserController', ]); }
public function store(StoreUserRequest $request): JsonResponse { return $this->telemetry->traceOperation('user_controller.store', function (Span $span) use ($request) { $validatedData = $request->validated();
$span->setAttributes([ 'controller' => 'UserController', 'action' => 'store', 'user.email' => $validatedData['email'], ]);
$user = $this->userService->createUser($validatedData);
$span->setAttributes([ 'user.created' => true, 'user.id' => $user->id, ]);
return response()->json([ 'message' => 'User created successfully', 'data' => new UserResource($user), ], 201); }, [ 'operation' => 'create_user', 'controller' => 'UserController', ]); }
public function show(User $user): JsonResponse { return $this->telemetry->traceOperation('user_controller.show', function (Span $span) use ($user) { $span->setAttributes([ 'controller' => 'UserController', 'action' => 'show', 'user.id' => $user->id, 'user.email' => $user->email, ]);
// Load relationships with tracing $userWithRelations = $this->userService->getUserWithDetails($user->id);
$span->setAttributes([ 'user.has_profile' => $userWithRelations->profile !== null, 'user.posts_count' => $userWithRelations->posts_count ?? 0, ]);
return response()->json([ 'data' => new UserResource($userWithRelations), ]); }, [ 'operation' => 'show_user', 'controller' => 'UserController', ]); }
public function update(UpdateUserRequest $request, User $user): JsonResponse { return $this->telemetry->traceOperation('user_controller.update', function (Span $span) use ($request, $user) { $validatedData = $request->validated();
$span->setAttributes([ 'controller' => 'UserController', 'action' => 'update', 'user.id' => $user->id, 'user.email' => $user->email, ]);
$updatedUser = $this->userService->updateUser($user, $validatedData);
$span->setAttributes([ 'user.updated' => true, 'fields_updated' => array_keys($validatedData), ]);
return response()->json([ 'message' => 'User updated successfully', 'data' => new UserResource($updatedUser), ]); }, [ 'operation' => 'update_user', 'controller' => 'UserController', ]); }
public function destroy(User $user): JsonResponse { return $this->telemetry->traceOperation('user_controller.destroy', function (Span $span) use ($user) { $span->setAttributes([ 'controller' => 'UserController', 'action' => 'destroy', 'user.id' => $user->id, ]);
$this->userService->deleteUser($user);
$span->setAttributes([ 'user.deleted' => true, ]);
return response()->json([ 'message' => 'User deleted successfully', ]); }, [ 'operation' => 'delete_user', 'controller' => 'UserController', ]); }}Service Layer Integration
<?php
namespace App\Services;
use App\Models\User;use Illuminate\Contracts\Pagination\LengthAwarePaginator;use Illuminate\Support\Facades\Cache;use Illuminate\Support\Facades\DB;use Illuminate\Support\Facades\Hash;use Illuminate\Support\Facades\Mail;
class UserService{ public function __construct( private TelemetryService $telemetry ) {}
public function getUsers(int $perPage = 15, ?string $search = null): LengthAwarePaginator { return $this->telemetry->traceOperation('user_service.get_users', function (Span $span) use ($perPage, $search) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'getUsers', '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]); }
$users = $query->paginate($perPage);
$span->setAttributes([ 'results.count' => $users->count(), 'results.total' => $users->total(), 'results.pages' => $users->lastPage(), ]);
return $users; }); }
public function getUserWithDetails(int $userId): User { return $this->telemetry->traceOperation('user_service.get_user_with_details', function (Span $span) use ($userId) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'getUserWithDetails', 'user.id' => $userId, ]);
// Try cache first $cacheKey = "user_details_{$userId}";
return Cache::remember($cacheKey, 300, function () use ($userId, $span) { $span->addEvent('cache_miss', ['cache_key' => "user_details_{$userId}"]);
$user = User::with(['profile', 'posts' => function ($query) { $query->latest()->take(5); }]) ->withCount('posts') ->findOrFail($userId);
$span->setAttributes([ 'user.found' => true, 'user.has_profile' => $user->profile !== null, 'user.posts_count' => $user->posts_count, 'cache.stored' => true, ]);
return $user; }); }); }
public function createUser(array $data): User { 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);
$span->setAttributes([ 'user.created' => true, 'user.id' => $user->id, ]);
// Send welcome email (async) $this->sendWelcomeEmail($user);
// Clear relevant caches $this->clearUserCaches();
$span->addEvent('transaction_completed');
return $user; }); }); }
public function updateUser(User $user, array $data): User { return $this->telemetry->traceOperation('user_service.update_user', function (Span $span) use ($user, $data) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'updateUser', 'user.id' => $user->id, 'fields_to_update' => array_keys($data), ]);
return DB::transaction(function () use ($user, $data, $span) { $span->addEvent('transaction_started');
// Hash password if provided if (isset($data['password'])) { $data['password'] = Hash::make($data['password']); }
$user->update($data);
$span->setAttributes([ 'user.updated' => true, ]);
// Clear cache for this user $this->clearUserCache($user->id);
$span->addEvent('transaction_completed');
return $user->refresh(); }); }); }
public function deleteUser(User $user): bool { return $this->telemetry->traceOperation('user_service.delete_user', function (Span $span) use ($user) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'deleteUser', 'user.id' => $user->id, ]);
return DB::transaction(function () use ($user, $span) { $span->addEvent('transaction_started');
// Soft delete or hard delete based on business logic $deleted = $user->delete();
$span->setAttributes([ 'user.deleted' => $deleted, 'deletion_type' => 'soft_delete', ]);
// Clear cache $this->clearUserCache($user->id);
$span->addEvent('transaction_completed');
return $deleted; }); }); }
private function sendWelcomeEmail(User $user): void { $this->telemetry->traceOperation('user_service.send_welcome_email', function (Span $span) use ($user) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'sendWelcomeEmail', 'user.id' => $user->id, 'user.email' => $user->email, ]);
try { // Queue welcome email (this will be traced by Laravel's queue instrumentation) Mail::to($user->email)->queue(new \App\Mail\WelcomeEmail($user));
$span->setAttributes([ 'email.queued' => true, 'email.type' => 'welcome', ]);
} catch (\Exception $e) { $span->recordException($e); $span->setStatus(StatusCode::STATUS_ERROR, 'Failed to queue welcome email');
// Log but don't fail user creation Log::error('Failed to queue welcome email', [ 'user_id' => $user->id, 'error' => $e->getMessage(), ]); } }); }
private function clearUserCache(int $userId): void { $this->telemetry->traceOperation('user_service.clear_user_cache', function (Span $span) use ($userId) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'clearUserCache', 'user.id' => $userId, ]);
$keys = [ "user_details_{$userId}", "user_posts_{$userId}", "user_profile_{$userId}", ];
foreach ($keys as $key) { Cache::forget($key); }
$span->setAttributes([ 'cache.keys_cleared' => count($keys), ]); }); }
private function clearUserCaches(): void { $this->telemetry->traceOperation('user_service.clear_user_caches', function (Span $span) { $span->setAttributes([ 'service' => 'UserService', 'operation' => 'clearUserCaches', ]);
// Clear user list cache Cache::tags(['users'])->flush();
$span->setAttributes([ 'cache.tags_cleared' => ['users'], ]); }); }}Database Integration
Laravel’s Eloquent ORM is automatically instrumented when you install the OpenTelemetry auto-instrumentation packages. However, you can add custom attributes:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Foundation\Auth\User as Authenticatable;use Illuminate\Notifications\Notifiable;use OpenTelemetry\API\Trace\Span;
class User extends Authenticatable{ use HasFactory, Notifiable;
protected $fillable = [ 'name', 'email', 'password', ];
protected $hidden = [ 'password', 'remember_token', ];
protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; }
// Automatically add tracing attributes to model events protected static function booted(): void { static::created(function (User $user) { $span = Span::getCurrent(); if ($span) { $span->addEvent('user.created', [ 'user.id' => $user->id, 'user.email' => $user->email, ]); } });
static::updated(function (User $user) { $span = Span::getCurrent(); if ($span) { $span->addEvent('user.updated', [ 'user.id' => $user->id, 'changes' => array_keys($user->getChanges()), ]); } });
static::deleted(function (User $user) { $span = Span::getCurrent(); if ($span) { $span->addEvent('user.deleted', [ 'user.id' => $user->id, ]); } }); }
public function profile() { return $this->hasOne(UserProfile::class); }
public function posts() { return $this->hasMany(Post::class); }}Queue Integration
<?php
namespace App\Jobs;
use App\Models\User;use App\Services\TelemetryService;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Illuminate\Support\Facades\Mail;
class ProcessUserRegistration implements ShouldQueue{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct( private User $user ) {}
public function handle(TelemetryService $telemetry): void { $telemetry->traceOperation('process_user_registration', function (Span $span) { $span->setAttributes([ 'job' => 'ProcessUserRegistration', 'user.id' => $this->user->id, 'user.email' => $this->user->email, 'queue.name' => $this->queue ?? 'default', ]);
// Send welcome email $telemetry->traceOperation('send_welcome_email', function (Span $emailSpan) { $emailSpan->setAttributes([ 'email.type' => 'welcome', 'email.recipient' => $this->user->email, ]);
Mail::to($this->user)->send(new \App\Mail\WelcomeEmail($this->user));
$emailSpan->setAttributes([ 'email.sent' => true, ]); });
// Create user profile $telemetry->traceOperation('create_user_profile', function (Span $profileSpan) { $profile = $this->user->profile()->create([ 'bio' => '', 'avatar' => null, ]);
$profileSpan->setAttributes([ 'profile.created' => true, 'profile.id' => $profile->id, ]); });
$span->setAttributes([ 'registration.processed' => true, ]); }, [ 'job.class' => static::class, 'job.queue' => $this->queue ?? 'default', ]); }
public function failed(\Throwable $exception): void { $telemetry = app(TelemetryService::class);
$telemetry->recordException($exception); $telemetry->addAttributes([ 'job.failed' => true, 'job.class' => static::class, 'user.id' => $this->user->id, ]); }}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
# Run Laravel optimizationsRUN php artisan config:cache && \ php artisan route:cache && \ php artisan view:cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"]version: "3.8"
services: app: build: . container_name: laravel-app environment: # OpenTelemetry - OTEL_PHP_AUTOLOAD_ENABLED=true - OTEL_SERVICE_NAME=laravel-app - 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
# Laravel - APP_ENV=production - APP_DEBUG=false - DB_CONNECTION=mysql - DB_HOST=mysql - DB_DATABASE=laravel_db - DB_USERNAME=laravel_user - DB_PASSWORD=laravel_password - REDIS_HOST=redis volumes: - ./storage/logs:/var/www/html/storage/logs depends_on: - mysql - redis
nginx: image: nginx:alpine container_name: laravel-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: laravel-mysql environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: laravel_db MYSQL_USER: laravel_user MYSQL_PASSWORD: laravel_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql
redis: image: redis:7-alpine container_name: laravel-redis ports: - "6379:6379" volumes: - redis_data:/data
volumes: mysql_data: redis_data:Kubernetes Deployment
apiVersion: apps/v1kind: Deploymentmetadata: name: laravel-app labels: app: laravel-appspec: replicas: 3 selector: matchLabels: app: laravel-app template: metadata: labels: app: laravel-app spec: containers: - name: laravel-app image: your-registry/laravel-app:latest ports: - containerPort: 9000 env: - name: OTEL_PHP_AUTOLOAD_ENABLED value: "true" - name: OTEL_SERVICE_NAME value: "laravel-app" - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "$last9_otlp_endpoint" - name: OTEL_EXPORTER_OTLP_HEADERS value: "Authorization=$last9_otlp_auth_header" - name: OTEL_RESOURCE_ATTRIBUTES value: "deployment.environment=production,service.version=1.0.0,k8s.cluster.name=production" - name: APP_ENV value: "production" - name: DB_HOST valueFrom: secretKeyRef: name: laravel-secrets key: db-host - name: DB_PASSWORD valueFrom: secretKeyRef: name: laravel-secrets key: db-password livenessProbe: tcpSocket: port: 9000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: tcpSocket: port: 9000 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m"---apiVersion: v1kind: Servicemetadata: name: laravel-app-servicespec: selector: app: laravel-app ports: - protocol: TCP port: 9000 targetPort: 9000Testing the Integration
-
Start your application
# Local developmentphp artisan serve# Or with Dockerdocker-compose up -
Test the endpoints
# API endpointscurl http://localhost:8000/api/userscurl http://localhost:8000/api/users/1# Create a usercurl -X POST http://localhost:8000/api/users \-H "Content-Type: application/json" \-d '{"name": "Test User", "email": "test@example.com", "password": "password123"}' -
View telemetry in Last9
Check your Last9 dashboard for:
- HTTP request traces with Laravel-specific attributes
- Eloquent ORM database operation spans
- Queue job execution traces
- Cache operation spans
- Custom business logic spans and events