maherelgamil/periscope
Periscope is a Laravel package that adds visibility into your application's activity, helping you monitor and inspect runtime events through a simple, developer-friendly interface.
Periscope is a long-running Laravel daemon that monitors queue activity by listening to Laravel's queue events (JobQueued, JobProcessing, JobProcessed, JobFailed, and friends) rather than inspecting driver internals. That single architectural choice is why it works against redis, database, sqs, beanstalkd, and sync alike — the events fire regardless of driver. Horizon reads Redis structures directly; Periscope doesn't. Keep that in mind from the first minute, because it determines where the package is useful and where it isn't.
Install and pin. composer require maherelgamil/periscope and pin the minor ("0.5.*" or an exact 0.5.3). At v0.5.x the public surface is pre-1.0 and the release notes already show the shutdown path being reworked once.
Check ext-pcntl before anything else. php -m | grep pcntl in the runtime image, not your laptop. v0.5.3 replaced a usleep() master loop with pcntl_sigtimedwait(), which makes pcntl load-bearing. pcntl is CLI-only and does not exist on Windows or in FPM. Official php:* images include it; slimmed Alpine and --disable-pcntl builds do not.
Publish and read the config. Run php artisan vendor:publish and open the generated config file. Treat it as the authoritative description of what the daemon watches and how often it ticks — more authoritative than the README, which lags at this version.
Run it in the foreground first. php artisan periscope:start, then php artisan periscope:start --help to see the flags your installed version actually has. Press Ctrl+C and confirm the process exits immediately — that is precisely what v0.5.3 fixed, and it is the cheapest smoke test you'll ever write.
First real use case. Open a terminal next to your app, run the daemon, and dispatch a job on a connection Horizon doesn't cover — a database or sqs queue in staging. Watch it appear, complete, and fail. That single exercise tells you more about fit than any blog post, because it proves the event-driven telemetry works on the driver you actually use.
One warning before you start. The package's description.md text describes roles and permissions — that is stale/incorrect metadata for this package. The release notes, the assessments, and the agent guide all describe a queue monitor. Trust the config file and the actual command surface.
Treat it as an operational sidecar, not a dependency. Nothing in your request path should import Periscope classes. The daemon runs beside the app, not inside it. If you find yourself wanting programmatic hooks, look for package events first and subclass internals last.
Complement Horizon rather than assume it replaces it. The honest framing: Horizon is excellent and deeply integrated if you're Redis-only. Periscope earns its place when you have database, sqs, or beanstalkd queues — or a mix — where Horizon gives you nothing. Running both, scoped per-connection, is a legitimate pattern; just make sure they aren't both installing signal handlers in the same container (see Gotchas).
Supervisor-managed daemon. Put it under Supervisor or systemd, not a backgrounded shell. Keep numprocs=1 — the release notes describe a "master" that owns child processes, so a second instance duplicates work and races over whatever it records. Typical Supervisor unit:
[program:periscope]
command=php /var/www/artisan periscope:start
numprocs=1
autostart=true
autorestart=true
stopsignal=TERM
stopwaitsecs=30
Set stopwaitsecs / systemd's TimeoutStopSec larger than your configured tick interval so the master has room to drain children before the supervisor escalates to SIGKILL.
Container and Kubernetes integration. The master must be PID 1 or must receive forwarded signals. Use init: true in Compose, or an exec-form entrypoint (exec php artisan periscope:start) so SIGTERM reaches PHP instead of dying in the shell. In Kubernetes, ensure terminationGracePeriodSeconds comfortably exceeds your worst-case shutdown. With the old usleep() behaviour shutdown was ragged; with pcntl_sigtimedwait() it's predictable, which is what rolling deploys actually need.
Restart on every deploy. A long-lived process holds your old code in memory. Add supervisorctl restart periscope:* (or the equivalent) to your deploy script. "The fix didn't work" is overwhelmingly this. Same rule for config/periscope.php and .env changes: restart, then confirm in the log that the new values took effect.
Scripted verification. Don't rely on a human pressing Ctrl+C. In CI or staging, timeout -s INT 10 php artisan periscope:start is a one-liner that fails the build if shutdown regresses. Complement it with ps -ef --forest to confirm no orphaned children remain.
Local development. Run it in a terminal or as a dedicated Sail/Compose service alongside the queue worker. Keep it out of the request cycle entirely. The foreground verbose output is your primary debugging surface; reproduce the problem there before touching a process manager.
pcntl is now load-bearing and fails late. pcntl_sigtimedwait() requires the extension, and it's unavailable on Windows and in web SAPIs. If the manifest doesn't declare ext-pcntl as a hard requirement, installs succeed on unsupported platforms and die at command start — the worst possible failure timing. Check the runtime image, not the dev box.
v0.5.3 is a semantic change, not a no-op patch. Shutdown is now immediate rather than "up to one sleep interval later." If you tuned stopwaitsecs around the old lag, retune it. Immediate SIGINT also gives in-flight child work less implicit breathing room — verify the master still drains or kills children cleanly under your workload.
Ctrl+C hanging was never a busy loop. It was the master sitting in usleep(), which isn't interruptible the way you'd hope. Do not "fix" a recurrence by shrinking the sleep interval — that trades CPU wakeups for the same bug. Confirm you're actually on ≥ 0.5.3.
Multi-replica duplication. There's no evidence of leader election or locking. A naive multi-replica rollout can produce multiple masters doing duplicate collection work. Verify idempotency of whatever it writes before scaling horizontally, or pin it to a single replica.
Signal-handler collisions. If Horizon, Octane, or a queue worker shares the container, more than one process may install handlers. The failure mode is "shutdown hangs" or "children stop getting reaped" — not a clean exception. Isolate the daemon in its own process group where possible.
Orphaned children on hard kill. SIGKILL on the master can leave child processes the supervisor doesn't own. ps -ef --forest after every restart you do by hand, and instrument for orphans in production.
Memory creep. Long-running Laravel processes accumulate state. Watch RSS over days. If the package offers no max-requests or max-lifetime option, schedule a periodic restart via the supervisor — for most teams this is the pragmatic answer.
Debug empty recordings at the worker layer, not the master. If the daemon exits cleanly but records nothing, suspect child workers or the event listener registration rather than the master loop. Run foreground with verbose output and reproduce.
Pre-1.0 churn. 0.5.x gives no semver guarantee that 0.6.0 won't rename a command or config key. Pin the minor, read release notes before bumping, and keep the foreground smoke test in CI so lifecycle regressions are caught before a supervisor silently SIGKILLs them.
Extension points. Prefer the published config and any package events/hooks over subclassing internals — especially for shutdown behaviour. The signal path changed once already and is the most likely thing to change again. Also, if you're considering adoption, ask the maintainer directly: is ext-pcntl declared as a hard requirement, does the master forward SIGTERM to children, and is there a test covering the signal path? Those three answers determine your real operational cost, far more than the install command does.
How can I help you explore Laravel packages today?