marekmiklusek/telegram-logger
marekmiklusek/telegram-logger adds a Laravel logging channel that sends application logs to Telegram chats via the Bot API. Configure the bot token, chat ID, and minimum log level to receive real-time error alerts and monitor events directly in Telegram.
The package is a Monolog handler that ships log records to a Telegram chat via the Bot API. v2.0.0 is a delivery-pipeline rewrite with no public API change, so the integration surface is still: publish config → set credentials → point a logging channel at it.
1. Check your platform first. v2 requires PHP 8.4+ and Laravel 13+. On PHP 8.2/8.3 or Laravel 12, Composer will resolve 1.x — that is expected and not a broken update. Confirm before you plan the upgrade:
php -v && php artisan --version
composer require marekmiklusek/telegram-logger:^2.0
2. Publish the config (mandatory on upgrade — the new keys live here):
php artisan vendor:publish --tag=telegram-logger-config --force
An existing published config from 1.x still works (new settings fall back to safe defaults), but you lose flood protection, redaction and throw_on_failure tuning until you republish. Republish, then diff the file against your VCS history.
3. Set credentials in .env. Every setting is now env-configurable; the minimum is the bot token and target chat:
TELEGRAM_LOGGER_BOT_TOKEN=123456:ABC...
TELEGRAM_LOGGER_CHAT_ID=-1001234567890
TELEGRAM_LOGGER_LEVEL=error
4. Wire it into config/logging.php as a dedicated channel rather than replacing your stack driver:
'telegram' => [
'driver' => 'telegram',
'level' => env('TELEGRAM_LOGGER_LEVEL', 'error'),
],
5. First use case: reporting production exceptions. In bootstrap/app.php (Laravel 11+ style) or your Handler, send to the Telegram channel explicitly so the channel only fires on real incidents:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (Throwable $e) {
Log::channel('telegram')->error($e->getMessage(), [
'exception' => $e::class,
'user_id' => auth()->id(),
'url' => request()->fullUrl(),
]);
});
})
You should see the new four-line layout in the chat:
❌ ERROR | Shop | production
Payment gateway timed out after 30s
📍 app/Http/Controllers/CheckoutController.php:87
🕑 16:41:13
{
"order_id": 4711,
"gateway": "stripe"
}
The heading carries level | app.name | environment, which is what makes a shared chat readable when several apps post into it.
Channel-per-severity, not channel-per-app. Because the level is in the heading, one bot can serve several channels at different thresholds. Keep telegram for error and add a noisier telegram-alerts channel at warning if you want a second chat or a second token.
Treat context as the payload, not an afterthought. Context is rendered as syntax-highlighted JSON, so scalar bags beat string concatenation:
Log::channel('telegram')->error('Import failed', [
'import_id' => $import->id,
'tenant' => $import->tenant->slug,
'rows_done' => $import->processed,
'exception' => $e::class, // class name is shown alongside the message
]);
Nested arrays render fine and are redacted recursively.
Redaction belongs in config, not in every call site. Put your organisation's sensitive keys in redact_keys once (password, token, authorization, and your own domain keys such as api_key, iban, card_number). Values are replaced with [REDACTED] at any nesting depth — this is the cheapest compliance win in the release, and it means developers can log a whole request payload without auditing it first.
Flood protection is your incident responder. A crash loop can turn one bug into thousands of Bot API calls. Set both knobs in production:
'dedupe_seconds' => 60, // identical message suppressed within the window
'max_per_minute' => 15, // hard ceiling on messages leaving the app
Design around them: dedupe is on identical rendered messages, so include a varying field (order id, request id) when you genuinely want every occurrence, and omit it when you want the loop collapsed. If you need a summary after suppression, log the counter from a scheduled command rather than fighting the dedupe window.
Two-tier logging for local vs production. Use throw_on_failure locally so a bad token or wrong chat id fails loudly during development, and leave it off in production where a logging failure must never take a request down:
// config/logging.php
'throw_on_failure' => env('APP_ENV') === 'local',
Route through a proxy or self-hosted Bot API with api_url — the practical reason is outbound network policy (corporate egress, a regional Bot API mirror, or a test double in CI). Point it at a local HTTP stub in your test suite to assert on the payload without touching Telegram.
Tune base_path when logs look truncated. File references are now path:line relative to the application root. If you deploy from a release directory (e.g. /var/www/releases/20240612/), set base_path so the stripped prefix matches the real document root rather than the default.
Log from jobs and commands, not from listeners that can re-enter. Because delivery now reads the API response, a failure is visible and retried-ish (see the plain-text retry), but a handler that logs inside a log listener is still a loop waiting to happen. Keep the channel out of the default stack driver so Log::error() elsewhere doesn't fan out to Telegram unless you intend it.
Audit what used to be silent. Delivery failures were previously indistinguishable from success — no response was read. After the upgrade, run a deliberate smoke test (invalid token, then valid) and confirm you now see the error path rather than an empty chat with a green dashboard.
1.x is the documented behaviour, not a resolution bug. Pin ^2.0 explicitly so the constraint is visible in review, and check composer why-not if it refuses.InvalidArgumentException on every log call in some versions — that bug is fixed — but the practical consequence is you can't trust pre-republish behaviour. --force overwrites: commit first.level now fails at boot. That is deliberate (previously it sent nothing, silently). It also means a typo in .env takes the app down on deploy rather than in a log line — validate the value in CI, and keep it on a channel rather than the default stack driver.App\Models\User case) was broken and is fixed, so re-test any hand-rolled escaping.allow_url_fopen must be enabled. It previously failed silently; it now throws with an actionable message. On hardened shared hosts, prefer setting api_url to a reachable endpoint over arguing with the flag, and confirm the function is available in the same PHP-FPM context your queue workers use.dedupe_seconds and max_per_minute are skipped by design so logging never stops — meaning a cache outage quietly disables your rate limiting. Use a shared cache store (Redis) if multiple servers log to the same chat, or the per-minute cap becomes per-server.app.name participates in the payload size. A very long application name could push a message past the limit; that is fixed, but keep APP_NAME short anyway since it appears on every single message in a shared chat.api_url and a proxy rather than patching the package.api_url at a local stub and assert the escaped body, rather than mocking the handler internals.How can I help you explore Laravel packages today?