Modern Laravel 12 application with automatic OpenTelemetry instrumentation for distributed tracing, metrics, and logs. Demonstrates JWT authentication, PostgreSQL integration, and Base14 Scout observability platform integration.
| Component | Version | EOL Status | Notes |
|---|---|---|---|
| PHP | 8.5 | Active | Current stable |
| Laravel | 12.x | Active | Latest LTS |
| PostgreSQL | 18 | Active | Latest stable |
| OpenTelemetry SDK | 1.6+ | N/A | Current |
- HTTP requests and responses (method, route, status)
- PostgreSQL database queries (Eloquent ORM)
- Redis connection and basic operations (get, set, delete, exists)
- Cache operations
- External HTTP calls (Guzzle/PSR-18)
- Distributed trace propagation (W3C Trace Context)
- Traces: All article operations (list, create, show, update, delete, favorite, unfavorite, feed)
- Attributes:
user.id,article.id,article.slug,db.operation,db.table,error.type - Logs: Structured JSON logs with trace correlation (
trace_id,span_id) - Metrics: Application counters (users, articles, comments)
- Error Handling: Centralized exception handler with span recording and error type classification
- Queue worker shares service name with web app for unified distributed tracing
- Web and worker distinguished via
service.instance.roleattribute (weborworker) - Trace context propagation with span linking from HTTP request to background job
- Job attributes:
job.name,job.queue,job.attempt,job.parent_trace_id - Worker traces include nested SQL spans for database operations
- See
app/Jobs/ProcessArticleJob.phpfor implementation
Note: Redis auto-instrumentation (
mismatch/opentelemetry-auto-redis) covers connection and basic operations (GET, SET, DELETE, EXISTS, SCAN). Queue-specific operations (LPUSH, BRPOP, EVAL) used by Laravel's Redis queue driver are not instrumented. Queue job execution is still traced via Laravel's job instrumentation withmessaging.system=redisattribute.
- Atomic Counter Updates:
favorites_countuses database-level increment/decrement - Rate Limiting: 60 req/min for API, 10 req/min for auth endpoints
- Security Headers: X-Content-Type-Options, X-Frame-Options, HSTS, etc.
- Graceful Shutdown: Telemetry flush on SIGTERM/SIGINT signals
| Component | Version | Purpose |
|---|---|---|
| Laravel | 12.x | Web framework |
| PHP | 8.5 | Runtime |
| PostgreSQL | 18 | Database |
| tymon/jwt-auth | 2.0 | JWT authentication |
| open-telemetry/sdk | 1.6+ | Telemetry SDK |
| open-telemetry/exporter-otlp | 1.3+ | OTLP exporter |
| open-telemetry/opentelemetry-auto-laravel | 1.2+ | Auto-instrumentation |
| mismatch/opentelemetry-auto-redis | 0.3+ | Redis auto-instrumentation |
| OTel Collector | 0.144.0 | Telemetry pipeline |
- Docker & Docker Compose
- Base14 Scout Account (setup guide)
- PHP 8.5+ and Composer (for local development only)
git clone https://github.com/base-14/examples.git
cd examples/php/php85-laravel12-postgresexport SCOUT_ENDPOINT=https://your-tenant.base14.io:4318
export SCOUT_CLIENT_ID=your_client_id
export SCOUT_CLIENT_SECRET=your_client_secret
export SCOUT_TOKEN_URL=https://your-tenant.base14.io/oauth/tokendocker compose up --builddocker exec laravel-app php artisan migrate
# Or with seed data (3 users, 5 articles, 7 tags, 5 comments)
docker exec laravel-app php artisan migrate:fresh --seedSeed credentials: alice@example.com, bob@example.com, charlie@example.com
(password: password)
./scripts/test-api.sh./scripts/verify-scout.sh- Login: Navigate to
https://your-tenant.base14.io - Find Service: Traces → Select
php-laravel12-postgres-otel - Explore: Click any trace to see distributed view
Scout provides:
- Distributed trace visualization across all services
- Span attributes and events
- Correlated logs with trace IDs
- Performance metrics and anomaly detection
| Variable | Required | Description |
|---|---|---|
SCOUT_ENDPOINT |
Yes | Base14 Scout OTLP endpoint |
SCOUT_CLIENT_ID |
Yes | OAuth2 client ID |
SCOUT_CLIENT_SECRET |
Yes | OAuth2 client secret |
SCOUT_TOKEN_URL |
Yes | OAuth2 token endpoint |
| Variable | Default | Description |
|---|---|---|
OTEL_SERVICE_NAME |
php-laravel12-postgres-otel |
Service identifier |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://otel-collector:4318 |
Collector endpoint |
OTEL_EXPORTER_OTLP_PROTOCOL |
http/protobuf |
Export protocol |
OTEL_PHP_AUTOLOAD_ENABLED |
true |
Enable auto-instrumentation |
DB_CONNECTION |
pgsql |
Database driver |
JWT_SECRET |
- | JWT signing key |
Automatically included in all telemetry:
service.name=php-laravel12-postgres-otel
telemetry.sdk.name=opentelemetry
telemetry.sdk.language=php
deployment.environment=development| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/health |
Health check (database, redis) | No |
| GET | /api/metrics |
Prometheus-compatible metrics | No |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/register |
User registration | No |
| POST | /api/login |
User authentication | No |
| POST | /api/logout |
User logout | Yes |
| GET | /api/user |
Get current user | Yes |
| POST | /api/refresh |
Refresh JWT token | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/articles |
List articles (paginated) | No |
| POST | /api/articles |
Create article | Yes |
| GET | /api/articles/{id} |
Get single article | No |
| PUT | /api/articles/{id} |
Update article | Yes |
| DELETE | /api/articles/{id} |
Delete article | Yes |
| GET | /api/articles/feed |
Personalized feed | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/articles/{id}/favorite |
Favorite article | Yes |
| DELETE | /api/articles/{id}/favorite |
Unfavorite article | Yes |
| GET | /api/articles/{id}/comments |
List comments | Yes |
| POST | /api/articles/{id}/comments |
Add comment | Yes |
| DELETE | /api/articles/{id}/comments/{cid} |
Delete comment | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/tags |
List all tags | No |
# Health check
curl http://localhost:8000/api/health
# Register user
curl -X POST http://localhost:8000/api/register \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name":"Test User","email":"test@example.com","password":"password123"}'
# Login
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
# Create article (with token)
curl -X POST http://localhost:8000/api/articles \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"title":"My Article","description":"Short desc","body":"Content","tagList":["php","laravel"]}'
# Get metrics
curl http://localhost:8000/api/metricsHTTP spans include:
http.method- GET, POST, PUT, DELETEhttp.route- Route pattern (e.g.,/api/articles/{id})http.status_code- Response statushttp.url- Full request URL
Database spans include:
db.system-postgresqldb.statement- SQL querydb.operation- SELECT, INSERT, UPDATE, DELETE
Custom business spans include:
user.id- Authenticated userarticle.id- Article identifierarticle.slug- URL slugservice.instance.role-weborworker(distinguishes app from queue worker)
Structured JSON format with trace correlation:
{
"timestamp": "2025-01-15T10:30:45Z",
"level": "info",
"message": "Article created",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"user.id": "user-123",
"article.id": "article-456"
}Prometheus-compatible metrics at /api/metrics:
app_users_total- Total registered usersapp_articles_total- Total articlesapp_comments_total- Total commentsapp_database_up- Database connection statusapp_info- Application version info
{
"require": {
"open-telemetry/sdk": "^1.6",
"open-telemetry/exporter-otlp": "^1.3",
"open-telemetry/opentelemetry-auto-laravel": "^1.2",
"open-telemetry/opentelemetry-auto-psr18": "^1.1"
}
}See app/Http/Controllers/Api/ArticleController.php for implementation:
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;
$tracer = Globals::tracerProvider()->getTracer('laravel-app');
$span = $tracer->spanBuilder('article.create')->startSpan();
try {
$span->setAttribute('user.id', $user->id);
$span->setAttribute('article.title', $request->title);
$article = Article::create([...]);
$span->setAttribute('article.id', $article->id);
$span->setStatus(StatusCode::STATUS_OK);
return $article;
} catch (\Exception $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
}-- Users
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
bio TEXT,
image VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- Articles
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
author_id BIGINT REFERENCES users(id),
slug VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
body TEXT NOT NULL,
favorites_count INTEGER DEFAULT 0,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- Tags (many-to-many with articles)
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL
);
-- Favorites (many-to-many users <-> articles)
CREATE TABLE article_user (
article_id BIGINT REFERENCES articles(id),
user_id BIGINT REFERENCES users(id),
PRIMARY KEY (article_id, user_id)
);composer install
cp .env.example .env
php artisan key:generate
php artisan jwt:secret
php artisan migrate
php artisan serve# Start services
docker compose up --build
# View logs
docker compose logs -f app
docker compose logs -f worker
docker compose logs -f otel-collector
# Laravel shell
docker exec -it laravel-app bash
# Database access
docker exec -it laravel-postgres psql -U laravel -d laravel
# Check queue jobs
docker exec laravel-app php artisan queue:monitor
# Stop services
docker compose down
# Stop and remove volumes
docker compose down -v| Service | URL/Command | Purpose |
|---|---|---|
| Application | http://localhost:8000 | Laravel API |
| Queue Worker | docker compose logs -f worker |
Background job processing |
| OTel Collector Health | http://localhost:13133 | Collector status |
| Collector zPages | http://localhost:55679/debug/tracez | Trace debugging |
- Check collector logs:
docker compose logs otel-collector - Verify Scout credentials are set correctly
- Ensure
OTEL_PHP_AUTOLOAD_ENABLED=true - Check OpenTelemetry extension:
php -m | grep opentelemetry
- Generate JWT secret:
docker exec laravel-app php artisan jwt:secret - Always include header:
-H "Accept: application/json" - Check token expiry (default: 60 minutes)
- Verify containers are running:
docker compose ps - Check migrations:
docker exec laravel-app php artisan migrate:status - Test connection:
docker exec laravel-app php artisan tinkerthenDB::connection()->getPdo()
- Verify all Scout credentials are exported
- Check token URL is accessible
- Review collector logs for OAuth errors