grkamil/laravel-telegram-logging
Send Laravel application logs straight to Telegram with this Monolog handler. Configure a bot token and chat ID, choose log levels, and receive instant error and warning notifications in your team chat—simple setup, no extra services.
1. Install and wire up the channel. The package registers a custom Monolog channel driver named telegram. Install it, then add a channel entry to config/logging.php:
composer require grkamil/laravel-telegram-logging
// config/logging.php
'channels' => [
'telegram' => [
'driver' => 'telegram',
'token' => env('TELEGRAM_BOT_TOKEN'),
'chat_id' => env('TELEGRAM_LOG_CHAT_ID'),
'level' => env('TELEGRAM_LOG_LEVEL', 'error'),
],
],
TELEGRAM_BOT_TOKEN=123456:AA...
TELEGRAM_LOG_CHAT_ID=-1001234567890
TELEGRAM_LOG_LEVEL=error
2. Get the two credentials right before writing any code. Create the bot with @BotFather (/newbot) to get the token. Then send the bot a message (or add it to the group as a member and post something), and call https://api.telegram.org/bot<TOKEN>/getUpdates to read the chat.id. Group/supergroup IDs are negative (-100…); public channels can be addressed as @channelname. The bot must be a member of the target chat — this is the single most common "why is nothing arriving" cause.
3. First use case: production exception alerting. The highest-value use of this package is a dedicated alert channel that is not part of your normal stack, called explicitly only for things a human must see:
// bootstrap/app.php (Laravel 11+) or App\Exceptions\Handler::report()
$exceptions->report(function (Throwable $e) {
if (app()->isProduction() && $e instanceof Reportable) {
Log::channel('telegram')->critical($e->getMessage(), [
'exception' => $e,
'url' => request()?->fullUrl(),
'user_id' => auth()->id(),
]);
}
});
4. Verify from the CLI before trusting it in prod:
php artisan tinker
>>> Log::channel('telegram')->error('telegram logging smoke test');
Where to look first: config/logging.php (the channel array is the whole public API), then the package's handler class under vendor/grkamil/laravel-telegram-logging/src/ — it's a small Monolog AbstractProcessingHandler, and reading it tells you exactly which config keys your installed version honours (parse mode, templates, notification flags vary by release). Check the released tag against your Laravel/Monolog version in composer.lock — Monolog 3 (Laravel 10+) broke a lot of third-party handlers.
Dedicated channel over stack inclusion. Adding telegram to your stack channel means every debug/info line in the app becomes an HTTP request and a chat message. Treat it as an alerting transport, not a log sink — call it explicitly, or if you must stack it, give the Telegram channel its own level => critical so only severe records reach it while daily still gets everything.
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'telegram'], // telegram filtered by its own 'level'
],
Severity routing. Use warning/error for things that self-heal and critical/alert/emergency for paging-grade events. Since everything lands in one chat, the level in the message body is your only triage signal — keep the convention strict so on-call can skim.
Runtime-built channels for per-tenant or per-service routing. Log::build() lets you keep one bot token and fan out to different chats without editing config:
Log::build([
'driver' => 'telegram',
'token' => config('services.telegram.token'),
'chat_id' => $tenant->telegram_chat_id,
'level' => 'error',
])->error('Import pipeline failed', ['tenant' => $tenant->id]);
Queue it if it's in a hot path. The handler posts synchronously inside the request/job lifecycle. Wrap calls in a small queued job so a slow or unavailable Telegram API never extends your response time:
class SendTelegramAlert implements ShouldQueue
{
public function __construct(
public string $message,
public array $context = [],
public string $level = 'error',
) {}
public function handle(): void
{
Log::channel('telegram')->log($this->level, $this->message, $this->context);
}
}
SendTelegramAlert::dispatch('Payment webhook failed', ['id' => $id])->onQueue('alerts');
Deduplicate per-request and per-window. Exception storms (a dead DB, a broken cron) otherwise produce thousands of identical messages. Laravel's RateLimiter or a cache lock is enough:
$key = 'tg:'.md5($e::class.'|'.$e->getMessage());
if (RateLimiter::attempt($key, 1, fn () => null, 300)) { // once per 5 min
Log::channel('telegram')->critical($e->getMessage(), ['exception' => $e]);
}
Add ambient context once. Log::withContext([...]) applied in a middleware (app name, environment, request ID, deploy SHA) makes every Telegram message self-identifying across staging and production bots.
Use it for operational signals too, not just exceptions. Scheduler onFailure() hooks, failed queue-job failed() methods, deploy start/finish markers, and a daily heartbeat from Schedule::call() are all good fits — they're low-volume and high-signal.
Test without hitting the API. Override the channel in phpunit.xml (<env name="TELEGRAM_LOG_CHAT_ID" value=""/> plus a null driver) or Log::spy(), and assert on Log::shouldReceive('channel')->with('telegram') rather than asserting on a real HTTP call.
429 Too Many Requests, and your alerts silently stop. Keep level at error or above, dedupe, and prefer one aggregated message over N.daily/Sentry.<, &, *, _ cause 400 Bad Request and the record is lost. Plain text is safest for arbitrary exception messages; sanitise or strip markup.chat not found, bot was blocked, or 403 from Telegram usually just means nothing appears in the chat. Always keep a daily channel as the primary destination, and check storage/logs/laravel.log for handler-level errors when a smoke test "works" but production alerts never arrive. Confirm bubble/exception handling in the handler so a Telegram outage can't throw inside your request.report() paths rather than per log line.config/logging.php reading env() means php artisan config:clear (or a redeploy) after changing the token or chat ID — a stale cached config is a classic "I updated .env and it still posts to the old chat."/start first; supergroups need the bot added as a member (not just an admin invite left pending). If getUpdates is empty, the bot has never seen a message in that chat.'driver' => 'monolog', 'handler' => ..., 'handler_with' => [...], or subclass it — the channel config array is just constructor arguments, so anything the handler accepts can be injected there.chat_id.How can I help you explore Laravel packages today?