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,
],
Per-Attempt Lookup
A broker can redeliver a slow job to a second worker while the first is still running
it, so two attempts of one job can report at the same time. To write each outcome to
the right attempt's row, the recorders look the row up by job key and attempt number
through JobAttemptRepositoryContract. Implement it next to
JobMonitorRepositoryContract:
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
use Cbox\LaravelQueueMonitor\Repositories\Contracts\JobAttemptRepositoryContract;
use Cbox\LaravelQueueMonitor\Repositories\Contracts\JobMonitorRepositoryContract;
class RedisJobMonitorRepository implements JobAttemptRepositoryContract, JobMonitorRepositoryContract
{
public function findAttemptByJobId(int|string $jobId, int $attempt): ?JobMonitor
{
return JobMonitor::where('job_id', (string) $jobId)
->where('attempt', $attempt)
->orderByDesc('created_at')
->first();
}
// ...
}
It is optional. A repository that implements only JobMonitorRepositoryContract keeps
working, and outcomes go to the job's latest attempt, as in earlier versions. See
Job Lifecycle and Attempts.
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
// routes/console.php
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('[email protected]')->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), config('queue-monitor.api.max_limit', 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
When payload storage is enabled, 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 Cbox\LaravelQueueMonitor\Enums\JobStatus;
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
class CustomQueueAnalytics
{
public function getHourlyJobCount(): array
{
return JobMonitor::query()
->whereBetween('queued_at', [now()->subDay(), now()])
->get(['queued_at', 'duration_ms'])
->groupBy(fn (JobMonitor $job): string => $job->queued_at->format('Y-m-d H:00'))
->map(fn ($jobs, string $hour): array => [
'hour' => $hour,
'count' => $jobs->count(),
'avg_duration' => $jobs->avg('duration_ms'),
])
->values()
->toArray();
}
public function getTopFailingJobs(int $limit = 10): array
{
return JobMonitor::query()
->select('job_class')
->selectRaw('COUNT(*) as failure_count')
->selectRaw('MAX(completed_at) as last_failure')
->where('status', JobStatus::FAILED->value)
->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();