Advanced Usage
Advanced Usage
Custom Job Monitoring
Using the MonitorsJobs Trait
Add custom monitoring behavior to your jobs:
use Illuminate\Contracts\Queue\ShouldQueue;
use Cbox\LaravelQueueMonitor\Traits\MonitorsJobs;
class ProcessOrderJob implements ShouldQueue
{
use MonitorsJobs;
public function __construct(
public int $orderId
) {}
public function handle(): void
{
// Process order
}
/**
* Custom display name for monitoring
*/
public function displayName(): string
{
return "Processing Order #{$this->orderId}";
}
/**
* Tag this job for categorization
*/
public function tags(): array
{
return ['orders', 'high-priority'];
}
}
Conditional Payload Storage
Control payload storage per job:
class SensitiveDataJob implements ShouldQueue
{
use MonitorsJobs;
public function shouldStorePayload(): bool
{
// Don't store payload for sensitive data
return false;
}
}
Building Custom Dashboards
Real-Time Job Monitoring
use Cbox\LaravelQueueMonitor\Events\JobMonitorRecorded;
use Illuminate\Support\Facades\Event;
Event::listen(JobMonitorRecorded::class, function($event) {
$job = $event->jobMonitor;
// Broadcast to websocket for real-time dashboard
broadcast(new JobStatusUpdated($job));
});
Custom Alerting
use Cbox\LaravelQueueMonitor\Events\JobMonitorRecorded;
Event::listen(JobMonitorRecorded::class, function($event) {
$job = $event->jobMonitor;
if ($job->status->isFailed()) {
// Send to Slack, email, etc.
Alert::send(new JobFailedAlert($job));
}
if ($job->duration_ms > 10000) {
// Alert on slow jobs
Alert::send(new SlowJobAlert($job));
}
});
Custom Repository Implementations
Redis-Based Repository
Create a custom repository for Redis storage:
namespace App\Repositories;
use Cbox\LaravelQueueMonitor\Repositories\Contracts\JobMonitorRepositoryContract;
use Illuminate\Support\Facades\Redis;
class RedisJobMonitorRepository implements JobMonitorRepositoryContract
{
public function create(JobMonitorData $data): JobMonitor
{
// Store in Redis with TTL
Redis::setex(
"job:{$data->uuid}",
3600, // 1 hour TTL
json_encode($data->toArray())
);
// Also store in database for long-term
return JobMonitor::create($data->toArray());
}
// Implement other methods...
}
Register in config:
// config/queue-monitor.php
'repositories' => [
JobMonitorRepositoryContract::class => \App\Repositories\RedisJobMonitorRepository::class,
],
Performance Optimization
Query Optimization
Use eager loading for relationships:
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
$jobs = JobMonitor::with(['retriedFrom', 'retries', 'tagRecords'])
->failed()
->whereDate('completed_at', today())
->get();
Pagination
For large datasets:
use Cbox\LaravelQueueMonitor\DataTransferObjects\JobFilterData;
$filters = new JobFilterData(
limit: 50,
offset: 0,
sortBy: 'queued_at',
sortDirection: 'desc'
);
$jobs = QueueMonitor::getJobs($filters);
$total = $this->repository->count($filters);
// Paginate through results
for ($offset = 0; $offset < $total; $offset += 50) {
$filters = new JobFilterData(limit: 50, offset: $offset);
$batch = QueueMonitor::getJobs($filters);
// Process batch
}
Caching Statistics
Cache expensive statistics queries:
use Illuminate\Support\Facades\Cache;
$stats = Cache::remember('queue-monitor:stats', 60, function() {
return QueueMonitor::statistics();
});
Multi-Tenant Applications
Tenant-Specific Filtering
Filter jobs by tenant:
class TenantAwareJob implements ShouldQueue
{
use MonitorsJobs;
public function tags(): array
{
return [
'tenant:' . tenant()->id,
'tenant-name:' . tenant()->name,
];
}
}
// Query jobs for specific tenant
$filters = new JobFilterData(
tags: ['tenant:123']
);
$tenantJobs = QueueMonitor::getJobs($filters);
Separate Database Per Tenant
// config/queue-monitor.php
'database' => [
'connection' => tenant()->getDatabaseConnection(),
],
Custom Analytics
Performance Baselines
Track performance degradation:
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
$baseline = JobMonitor::forJobClass('App\\Jobs\\ProcessOrder')
->successful()
->whereBetween('completed_at', [
now()->subDays(30),
now()->subDays(7)
])
->avg('duration_ms');
$recent = JobMonitor::forJobClass('App\\Jobs\\ProcessOrder')
->successful()
->whereDate('completed_at', today())
->avg('duration_ms');
if ($recent > $baseline * 1.5) {
Alert::send("ProcessOrder performance degraded by 50%");
}
Failure Rate Trending
$dailyFailureRates = collect();
for ($i = 0; $i < 30; $i++) {
$date = today()->subDays($i);
$total = JobMonitor::whereDate('completed_at', $date)->count();
$failed = JobMonitor::failed()->whereDate('completed_at', $date)->count();
$failureRate = $total > 0 ? ($failed / $total) * 100 : 0;
$dailyFailureRates->push([
'date' => $date->toDateString(),
'failure_rate' => $failureRate,
]);
}
Error Handling
Custom Exception Handling
use Cbox\LaravelQueueMonitor\Exceptions\JobNotFoundException;
use Cbox\LaravelQueueMonitor\Exceptions\JobReplayException;
try {
QueueMonitor::replay($uuid);
} catch (JobNotFoundException $e) {
Log::error("Job not found for replay: {$uuid}");
return response()->json(['error' => 'Job not found'], 404);
} catch (JobReplayException $e) {
Log::error("Replay failed: {$e->getMessage()}");
return response()->json(['error' => $e->getMessage()], 422);
}
Scheduled Tasks
Automatic Pruning
// app/Console/Kernel.php (Laravel 10) or routes/console.php (Laravel 11+)
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue-monitor:prune', ['--days' => 30])
->daily()
->at('02:00');
Daily Reports
Schedule::call(function() {
$stats = QueueMonitor::statistics();
Mail::to('admin@example.com')->send(
new DailyQueueReport($stats)
);
})->dailyAt('08:00');
Integration with Monitoring Tools
Prometheus Metrics
Export metrics to Prometheus:
use Cbox\LaravelQueueMonitor\Events\JobMonitorRecorded;
Event::listen(JobMonitorRecorded::class, function($event) {
$job = $event->jobMonitor;
if ($job->isFinished()) {
Prometheus::histogram('queue_job_duration_ms', $job->duration_ms, [
'queue' => $job->queue,
'status' => $job->status->value,
]);
Prometheus::gauge('queue_job_memory_mb', $job->memory_peak_mb ?? 0, [
'job_class' => $job->job_class,
]);
}
});
DataDog Integration
use DataDog\DogStatsd;
Event::listen(JobMonitorRecorded::class, function($event) {
$job = $event->jobMonitor;
if ($job->isFinished()) {
$statsd = app(DogStatsd::class);
$statsd->timing('queue.job.duration', $job->duration_ms, [
'queue' => $job->queue,
'status' => $job->status->value,
]);
$statsd->increment('queue.job.completed', 1, [
'status' => $job->status->value,
]);
}
});
Testing Helpers
Factory States
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
// Create specific job scenarios
$slowJob = JobMonitor::factory()->slow(10000)->create();
$failedJob = JobMonitor::factory()->failed()->create();
$horizonJob = JobMonitor::factory()->horizon()->create();
$taggedJob = JobMonitor::factory()->withTags(['priority', 'email'])->create();
// Chain multiple states
$job = JobMonitor::factory()
->failed()
->horizon()
->withTags(['critical'])
->create();
Testing Job Replay
use Illuminate\Support\Facades\Queue;
test('job replay dispatches to correct queue', function() {
Queue::fake();
$job = JobMonitor::factory()->failed()->create([
'queue' => 'emails',
'connection' => 'redis',
]);
QueueMonitor::replay($job->uuid);
Queue::assertPushedOn('emails');
});
Advanced Filtering
Complex Query Building
use Carbon\Carbon;
use Cbox\LaravelQueueMonitor\DataTransferObjects\JobFilterData;
use Cbox\LaravelQueueMonitor\Enums\JobStatus;
$filters = new JobFilterData(
statuses: [JobStatus::FAILED, JobStatus::TIMEOUT],
queues: ['emails', 'notifications'],
jobClasses: [
'App\\Jobs\\SendEmail',
'App\\Jobs\\SendNotification',
],
serverNames: ['web-1', 'web-2'],
tags: ['priority'],
queuedAfter: Carbon::now()->subHours(24),
minDurationMs: 1000,
maxDurationMs: 10000,
search: 'timeout',
limit: 100,
sortBy: 'duration_ms',
sortDirection: 'desc'
);
$jobs = QueueMonitor::getJobs($filters);
Dynamic Filtering
$filters = new JobFilterData(
statuses: request('statuses') ?
array_map(fn($s) => JobStatus::from($s), request('statuses')) :
null,
queues: request('queues'),
search: request('search'),
limit: min(request('limit', 50), 1000)
);
$jobs = QueueMonitor::getJobs($filters);
Bulk Operations
Bulk Replay
$failedJobs = QueueMonitor::getFailedJobs(100);
foreach ($failedJobs as $job) {
try {
QueueMonitor::replay($job->uuid);
Log::info("Replayed: {$job->uuid}");
} catch (\Exception $e) {
Log::error("Replay failed for {$job->uuid}: {$e->getMessage()}");
}
// Rate limit to avoid overwhelming queue
usleep(100000); // 100ms delay
}
Bulk Deletion
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
// Delete all completed jobs older than 7 days
JobMonitor::successful()
->where('completed_at', '<', now()->subDays(7))
->chunk(100, function($jobs) {
foreach ($jobs as $job) {
QueueMonitor::delete($job->uuid);
}
});
Security Considerations
API Authentication
// config/queue-monitor.php
'api' => [
'middleware' => ['api', 'auth:sanctum'],
],
Payload Redaction
While the package stores full payloads by default, you can implement custom redaction:
// In your job class
use Cbox\LaravelQueueMonitor\Traits\MonitorsJobs;
class ProcessPaymentJob implements ShouldQueue
{
use MonitorsJobs;
public function __construct(
public array $paymentData
) {}
// Override serialization to redact sensitive data
public function __sleep(): array
{
// Redact credit card before serialization
$this->paymentData['card_number'] = '****';
return ['paymentData'];
}
}
IP Whitelisting
// Custom middleware
class WhitelistQueueMonitorApi
{
public function handle($request, Closure $next)
{
$allowedIps = config('queue-monitor.allowed_ips', []);
if (!empty($allowedIps) && !in_array($request->ip(), $allowedIps)) {
abort(403, 'Unauthorized IP');
}
return $next($request);
}
}
// config/queue-monitor.php
'api' => [
'middleware' => ['api', WhitelistQueueMonitorApi::class],
],
Performance at Scale
Database Partitioning
For high-volume applications, consider partitioning:
-- MySQL partition by month
ALTER TABLE queue_monitor_jobs
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p202501 VALUES LESS THAN (TO_DAYS('2025-02-01')),
PARTITION p202502 VALUES LESS THAN (TO_DAYS('2025-03-01')),
-- Add partitions monthly
);
Read Replicas
Use read replicas for analytics:
// config/queue-monitor.php
'database' => [
'connection' => app()->environment('production')
? 'mysql_analytics'
: 'mysql',
],
Async Pruning
use Cbox\LaravelQueueMonitor\Actions\Core\PruneJobsAction;
class PruneQueueMonitorJob implements ShouldQueue
{
public function handle(PruneJobsAction $action): void
{
$deleted = $action->execute(days: 30);
Log::info("Pruned {$deleted} queue monitor jobs");
}
}
// Dispatch daily
Schedule::job(new PruneQueueMonitorJob)->daily();
Custom Statistics
Building Custom Reports
use Illuminate\Support\Facades\DB;
class CustomQueueAnalytics
{
public function getHourlyJobCount(): array
{
$prefix = config('queue-monitor.database.table_prefix', 'queue_monitor_');
return DB::table($prefix.'jobs')
->select([
DB::raw('DATE_FORMAT(queued_at, "%Y-%m-%d %H:00:00") as hour'),
DB::raw('COUNT(*) as count'),
DB::raw('AVG(duration_ms) as avg_duration'),
])
->whereBetween('queued_at', [now()->subDay(), now()])
->groupBy('hour')
->orderBy('hour')
->get()
->toArray();
}
public function getTopFailingJobs(int $limit = 10): array
{
$prefix = config('queue-monitor.database.table_prefix', 'queue_monitor_');
return DB::table($prefix.'jobs')
->select([
'job_class',
DB::raw('COUNT(*) as failure_count'),
DB::raw('MAX(completed_at) as last_failure'),
])
->where('status', 'failed')
->whereDate('completed_at', '>=', now()->subDays(7))
->groupBy('job_class')
->orderByDesc('failure_count')
->limit($limit)
->get()
->toArray();
}
}
Extending the Package
Custom Actions
Create custom actions following the pattern:
namespace App\Actions;
use Cbox\LaravelQueueMonitor\Repositories\Contracts\JobMonitorRepositoryContract;
final readonly class ArchiveOldJobsAction
{
public function __construct(
private JobMonitorRepositoryContract $repository
) {}
public function execute(int $days): int
{
$jobs = JobMonitor::where('completed_at', '<', now()->subDays($days))
->get();
foreach ($jobs as $job) {
// Archive to S3 or external storage
Storage::put("archives/{$job->uuid}.json", $job->toJson());
// Delete from database
$this->repository->delete($job->uuid);
}
return $jobs->count();
}
}
Custom Events
Dispatch additional events:
namespace App\Events;
class CriticalJobFailed
{
public function __construct(
public JobMonitor $job
) {}
}
// In event listener
if ($job->job_class === 'App\\Jobs\\CriticalJob' && $job->status->isFailed()) {
event(new CriticalJobFailed($job));
}
Best Practices
1. Tag Everything
Use tags liberally for better analytics:
public function tags(): array
{
return [
'environment:' . app()->environment(),
'tenant:' . tenant()->id,
'priority:' . $this->priority,
'category:email',
];
}
2. Meaningful Display Names
public function displayName(): string
{
return "Send Welcome Email to User #{$this->userId}";
}
3. Monitor Resource Usage
// Alert on high memory usage
if ($job->memory_peak_mb > 500) {
Alert::send("High memory job: {$job->job_class}");
}
4. Regular Pruning
// Keep only 7 days of successful jobs
Schedule::command('queue-monitor:prune', [
'--days' => 7,
'--statuses' => 'completed'
])->daily();
// Keep failures for 30 days
Schedule::command('queue-monitor:prune', [
'--days' => 30,
'--statuses' => 'failed,timeout'
])->weekly();
5. Use Facade for Queries
// Good: Type-safe facade
$job = QueueMonitor::getJob($uuid);
// Avoid: Direct model queries for monitoring logic
$job = JobMonitor::where('uuid', $uuid)->first();