Skip to content

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%");
}
$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();