jordanpartridge/laravel-say-logger
A Laravel logging channel that speaks your log messages out loud using the macOS say command, so errors and warnings reach you even when you're away from the terminal. Handy for local development and debugging.
Install
composer require jordanpartridge/laravel-say-logger
Wire it into a logging stack — the package's channel is added alongside a normal file channel so you keep durable logs:
// config/logging.php
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'say'],
],
Then confirm the plumbing works before trusting it:
which say # must exist -> macOS only
say -v '?' | head # list installed voices
php artisan tinker
>>> Log::info('say logger smoke test');
Where to look first
vendor/jordanpartridge/laravel-say-logger/src/ — it's a small package; read the handler/service provider before you rely on it. You specifically want to see how it invokes say (exec, shell_exec, proc_open, Symfony Process) and whether it backgrounds the process.php artisan vendor:publish --tag=say-logger-config, otherwise check the README) — that's the voice-per-level map.config/logging.php — the channel definition is where you'll do 90% of your tuning.First real use case: not "make all logs speak." Add the say channel to a separate channel or an error-only stack so a failed queue job, a 500, or a long-running command finishing announces itself while you're in another window. That's the whole value proposition: an out-of-band alert for the one thing you care about, not narration for every Log::debug.
Keep a file channel, always. ['single', 'say'] is right; ['say'] alone means your logs only exist as audio. Never do that.
Filter by level at the channel, not in your code. Speech is a scarce resource — one utterance per second, not per log line. Configure the say channel with 'level' => 'error' (or critical) so debug/info noise never reaches the speaker. Reserve the fun voices for things you genuinely want sung at you.
Use a dedicated channel for deliberate announcements rather than routing everything:
// A long import or backfill — tell me when it's done
Log::channel('say')->info('Import finished, 12,430 rows');
// In withExceptions() — only emergencies bypass the error log
->report(function (Throwable $e) {
if ($e instanceof QueryException) {
Log::channel('say')->critical(class_basename($e));
}
})
Environment-gate it. The channel should be inert outside local. Two layers: don't include say in the stack in production config, and set any package-level enable flag from .env (SAY_LOGGER_ENABLED=false in .env.example, phpunit.xml, and CI).
Long-running processes are the sweet spot. Horizon/queue workers, artisan serve, scheduler tasks, and deploy scripts log into the void; a spoken "job failed" is a genuinely better signal than a line you'll never tail. One announcement per job failure, not per retry attempt.
Rate-limit yourself. If a loop logs 50 errors, you'll get 50 overlapping utterances. Wrap the say channel behind a small helper that dedupes identical messages within N seconds, or use the say channel only from code paths you know are single-shot.
Testing. Log::spy() / Log::shouldReceive() assertions cover your own calls; assert on the say channel name specifically. And make sure the test suite never actually speaks — that's both slow and maddening.
Extension point worth building: if your team is mixed-OS, wrap the TTS invocation yourself. A small TextToSpeech contract with a SayDriver (macOS), SpdSayDriver (Linux), and NullDriver (CI) is ~40 lines and makes the package usable rather than a local-only curiosity.
say binary ships with macOS and nothing else. On Linux, Windows, Alpine/Docker, and most CI runners the channel will either throw or silently no-op — verify which, because a throwing log handler can take down a request. Guard with PHP_OS_FAMILY === 'Darwin'.say blocks until the audio finishes unless the package backgrounds it (... &) or uses a non-blocking process. If it's a plain exec(), a five-second utterance becomes five seconds of extra response time on a web request. Test with time php artisan tinker around a single Log::error. If it blocks, only ever use it from CLI contexts.$(...), semicolons. If the handler builds a command string, a message like He said "hi"; rm -rf ~ can break it or worse. Read the source; if it doesn't use escapeshellarg or array-based proc_open, wrap/override it before pointing it at anything user-influenced.say error or fall back to the default. say -v '?' is your diagnostic, and users may need System Settings → Accessibility → Spoken Content → Manage Voices.say "hello" in a terminal first, then test the package.tail your log file into anything else, remember two channels now exist; check storage/logs is still being written before you trust the audio as confirmation that logging works.composer.json rather than floating ^1.0.composer.json require block; put it in require-dev so production images don't carry a channel that can only fail on Linux.How can I help you explore Laravel packages today?