st4rgaze / laravel-telegram-logger
Secure, queued and throttled Telegram log channel for Laravel.
Package info
github.com/st4rgaze/laravel-telegram-logger
pkg:composer/st4rgaze/laravel-telegram-logger
Requires
- php: ^8.3
- illuminate/bus: ^12.0|^13.0
- illuminate/cache: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/log: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- monolog/monolog: ^3.0
- symfony/polyfill-php84: ^1.31
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.29
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-type-coverage: ^4.0
- phpstan/phpstan: ^2.1
- phpstan/phpstan-strict-rules: ^2.0
- rector/rector: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 23:37:42 UTC
README
A Telegram log channel for Laravel that is safe to leave on in production:
- Never blocks your requests. Messages are sent from a queued job by default, with retries that respect Telegram's rate limits.
- Never leaks your bot token. Not into your logs, exception messages, queue payloads, or HTTP monitoring such as Telescope, Pulse and Nightwatch.
- Never floods your chat. Identical errors are sent once per minute and each chat gets at most 20 Telegram messages a minute (a stack trace file counts as one); the next message tells you how many records were held back.
- Redacts before sending. Passwords, tokens and cookies in log context are masked, email addresses are masked in every text, stack traces never include call arguments, and database errors show their SQL with
?placeholders. - Always valid. Long messages are split on line breaks into valid HTML chunks; full stack traces arrive as an attached text file.
- Tells you what is wrong.
php artisan telegram-logger:testchecks the token, the chat and the topic and sends a test message.
Requires PHP 8.3+ and Laravel 12 or 13.
Installation
composer require st4rgaze/laravel-telegram-logger
- Create a bot with @BotFather (
/newbot) and copy its token. - Add the bot to your group (or send it
/startfor a private chat) and get the chat id, for example fromhttps://api.telegram.org/bot<token>/getUpdatesafter posting a message in the chat. - Add both to
.env:
TELEGRAM_LOGGER_BOT_TOKEN=123456789:AA... TELEGRAM_LOGGER_CHAT_ID=-1001234567890
- Add a channel to
config/logging.php:
'channels' => [ // ... 'telegram' => [ 'driver' => 'telegram', 'level' => 'error', ], ],
Add it to your stack ('channels' => ['single', 'telegram']) or set LOG_STACK=single,telegram.
- Check the setup:
php artisan telegram-logger:test
✓ Channel "telegram" uses the telegram driver
✓ Token format looks valid (123456789…xy)
✓ Bot authenticated as @your_alerts_bot
✓ Chat -1001234567890 found: "Ops" (supergroup)
✓ Test message sent
! Delivery is "queue": make sure a worker runs `queue:work`
Delivery
By default (TELEGRAM_LOGGER_DELIVERY=queue) each message is sent by a queued job chain, so a slow or unreachable Telegram never slows your application down. Run a worker for the queue you configure:
TELEGRAM_LOGGER_QUEUE_CONNECTION=redis # default: your app's default connection TELEGRAM_LOGGER_QUEUE=alerts # default: the connection's default queue
Each job makes up to three attempts: it waits retry_after when Telegram rate-limits (capped at 300 seconds), and 10 then 30 seconds after a server or network error. A message that still cannot be delivered is reported in one line to fallback_channel (or PHP's error_log()), never to Telegram again, so a Telegram outage cannot cause a logging loop.
Applications without a queue worker can send immediately:
TELEGRAM_LOGGER_DELIVERY=sync
Sync delivery waits for Telegram (2 second connect, TELEGRAM_LOGGER_TIMEOUT total, default 5) during the request that logged.
Production notes
- Use a cache and queue that do not live in your main database. With the database queue, a job pushed inside a transaction that rolls back disappears with it, and during a database outage (when you need alerts most) neither the throttle nor the queue works. Redis or SQS for the queue and Redis or Memcached for
throttle.cache_storeavoid both. The jobs are always dispatched immediately, even when the connection usesafter_commit. - Broker size limits. A queued message carries its stack trace file in the job payload. The default
trace.max_bytes(200 KB) fits SQS (256 KB); lower it for Beanstalkd (64 KB by default). - Workers and Octane. See one message per request before buffering in long-running processes.
Configuration
Publishing the config file is optional:
php artisan vendor:publish --tag=telegram-logger-config
| Key | Env | Default |
|---|---|---|
token |
TELEGRAM_LOGGER_BOT_TOKEN |
— |
chat_id |
TELEGRAM_LOGGER_CHAT_ID |
— |
message_thread_id |
TELEGRAM_LOGGER_MESSAGE_THREAD_ID |
— (forum topic id) |
api_host |
TELEGRAM_LOGGER_API_HOST |
https://api.telegram.org |
proxy |
TELEGRAM_LOGGER_PROXY |
— |
timeout |
TELEGRAM_LOGGER_TIMEOUT |
5 |
delivery |
TELEGRAM_LOGGER_DELIVERY |
queue |
queue.connection / queue.name |
TELEGRAM_LOGGER_QUEUE_CONNECTION / TELEGRAM_LOGGER_QUEUE |
app default |
throttle.enabled |
true |
|
throttle.dedupe_seconds |
60 |
|
throttle.max_per_minute |
20 |
|
throttle.cache_store |
app default | |
redact.keys |
password, current_password, new_password, _token, token, secret, authorization, cookie, api_key, x-api-key, hash, signature, … |
|
redact.patterns |
email addresses | |
redact.replacement |
[redacted] |
|
request_context.{url,method,route,user_id} |
true |
|
request_context.ip |
false |
|
trace.attach / trace.max_bytes |
true / 200000 |
|
max_chunks |
3 |
|
silent_below |
error (lower levels arrive without a notification sound) |
|
formatter |
DefaultMessageFormatter::class |
|
fallback_channel |
null (PHP error_log()) |
Invalid values (an unknown delivery, a non-numeric timeout, a broken regex, a non-https api_host…) are reported when the channel is created: Laravel falls back to its emergency logger and logs the reason. A missing token or chat id is not an error; the channel simply does nothing until both are set.
Several chats
Every key can be overridden per channel. Nested arrays are merged key by key; lists such as redact.keys are replaced.
'telegram' => [ 'driver' => 'telegram', 'level' => 'error', ], 'telegram-ops' => [ 'driver' => 'telegram', 'level' => 'warning', 'chat_id' => env('TELEGRAM_OPS_CHAT_ID'), 'message_thread_id' => env('TELEGRAM_OPS_TOPIC_ID'), 'throttle' => ['dedupe_seconds' => 300], ],
Channels built on demand with Log::build() must use sync delivery, because a queued job can only find the configuration of channels defined in config/logging.php:
Log::build(['driver' => 'telegram', 'delivery' => 'sync', 'chat_id' => $chatId])->info('Deployed');
One message per request
Wrap the handler in Monolog's BufferHandler with a tap class to send everything a request logged as a single message:
namespace App\Logging; use Illuminate\Log\Logger; use Monolog\Handler\BufferHandler; use Monolog\Handler\HandlerInterface; final class BufferTelegram { public function __invoke(Logger $logger): void { $monolog = $logger->getLogger(); $monolog->setHandlers(array_map( fn (HandlerInterface $handler): BufferHandler => new BufferHandler($handler), $monolog->getHandlers(), )); } }
'telegram' => [ 'driver' => 'telegram', 'level' => 'error', 'tap' => [App\Logging\BufferTelegram::class], ],
Under PHP-FPM the buffer is flushed at the end of the request and the records are joined into one message. Queue workers and Octane are long-running processes that only flush on shutdown, so give the buffer a limit there (new BufferHandler($handler, bufferLimit: 50, flushOnOverflow: true)) or apply the tap only to HTTP channels.
Redaction
Everything is redacted before it is formatted, queued or attached:
- Keys listed in
redact.keysare masked at any depth in context and extra, matched case-insensitively. - Patterns in
redact.patternsare masked in every text: the message, context values, exception messages and stack traces. Email addresses are masked by default. Add your own:
'redact' => [ 'patterns' => [ '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', // email (default) '/\b(\+62|0)8\d{8,11}\b/', // Indonesian mobile numbers '/\b\d{16}\b/', // 16-digit ids such as NIK ], ],
Phone and id patterns are not enabled by default because they also match order numbers and other ids you probably want to see.
- Stack traces list file, line and function for each frame, never the call arguments.
- Database errors (
Illuminate\Database\QueryException) are shown with the SQL and?placeholders, not the bound values. The database driver's own message can still quote a value (MySQL "Duplicate entry '…'", PostgreSQL "Key (…)=(…)"); add a pattern if that matters to you. - Request context shows the real path (
/orders/123), but route parameters whose names contain a redacted key stay placeholders (/reset-password/{token},/invites/{invite_token},/email/verify/7/{hash}), and the patterns apply too. The query string is never sent and the IP address is off by default.
Messages stay in the chat history and every member can read them. Keep the alert chat small and private.
Custom formatter
Implement MessageFormatter and set its class in formatter. Formatters build a TelegramText from typed segments; the package escapes, splits and wraps them, so a formatter can never send broken HTML or an oversized message. The entry you receive is already redacted.
use St4rgaze\TelegramLogger\Formatting\MessageFormatter; use St4rgaze\TelegramLogger\Formatting\TelegramText; use St4rgaze\TelegramLogger\Logging\LogEntry; final class CompactFormatter implements MessageFormatter { public function format(LogEntry $entry): TelegramText { return TelegramText::make() ->bold($entry->level->getName()) ->text(' '.$entry->message) ->line() ->code($entry->exceptions[0]->file ?? ''); } }
Available segments: text, bold, italic, code, pre, quote, line, append.
Diagnostics
php artisan telegram-logger:test # the "telegram" channel php artisan telegram-logger:test ops # another channel php artisan telegram-logger:test --queue # send the test message through the queue
The command stops at the first failing check and exits with status 1, so it can run as a deploy health check. The token is never printed in full.
Migrating from grkamil/laravel-telegram-logging
composer remove grkamil/laravel-telegram-logging && composer require st4rgaze/laravel-telegram-logger- Keep your
.env:TELEGRAM_LOGGER_BOT_TOKEN,TELEGRAM_LOGGER_CHAT_ID,TELEGRAM_LOGGER_MESSAGE_THREAD_ID,TELEGRAM_LOGGER_API_HOST,TELEGRAM_LOGGER_PROXYandTELEGRAM_LOGGER_TIMEOUTmean the same here. - In
config/logging.phpreplace'driver' => 'custom', 'via' => Logger\TelegramLogger::classwith'driver' => 'telegram'. - Run a queue worker, or set
TELEGRAM_LOGGER_DELIVERY=sync. - Blade templates (
TELEGRAM_LOGGER_TEMPLATE) are not supported; write a custom formatter instead. - Remove a published
config/telegram-logger.phpfrom the old package and publish this one.
Security
The bot token is part of every Bot API URL. This package sends requests from its own HTTP client without Laravel's HTTP events, so instrumentation never records those URLs, and it turns every transport error into an exception that contains neither the URL nor the token. Report vulnerabilities as described in SECURITY.md.
Testing
composer test
Runs Rector, Pint, PHPStan (level max with strict rules), 100% type coverage and the Pest suite with coverage.
License
MIT. See LICENSE.md.