raifucore / laravel-telegram-logger
Send logs to Telegram
Package info
github.com/raifucore/laravel-telegram-logger
pkg:composer/raifucore/laravel-telegram-logger
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- laravel/framework: ^10.0|^11.0|^12.0
- monolog/monolog: ^3.0
README
Send Laravel logs to Telegram via the Telegram Bot API.
Contents
- Features
- Requirements
- Installation
- Quick start
- Configuration
- Usage
- Templates
- Per-level overrides
- Forum topics / targeted sending
- Queue behavior
- Message behavior
- Testing
- Troubleshooting
- Upgrading from 1.x
Features
- Custom Laravel logging channel with Monolog 3
- Queued delivery (
syncor async queue connections) - Blade templates (
standard,minimal, or your own) - Per-level overrides (token, chat id, thread id, template, options)
- Forum topic routing via a reserved log context key (
Handler::CONTEXT_KEY) - Optional HTTP proxy and configurable request timeout
- Automatic splitting of long messages into chunks of up to 4096 bytes
Requirements
- PHP
^8.1 - Laravel
^10|^11|^12 - Monolog
^3 - Guzzle
^7
Installation
composer require raifucore/laravel-telegram-logger
The service provider is auto-discovered by Laravel.
Publish config and views:
php artisan vendor:publish --provider="RaifuCore\TelegramLogger\ServiceProvider"
Or publish selectively:
php artisan vendor:publish --provider="RaifuCore\TelegramLogger\ServiceProvider" --tag=config php artisan vendor:publish --provider="RaifuCore\TelegramLogger\ServiceProvider" --tag=views
Quick start
1. Create a bot and chat
- Open @BotFather, run
/newbot, and copy the token. - Add the bot to your private chat, group, or forum group.
- For TG topics, make sure the bot can post in the target topic.
- Put the bot token and chat id into
.env(see below).
2. Configure .env
TELEGRAM_LOGGER_ENABLE=true TELEGRAM_LOGGER_BOT_TOKEN=your-bot-token TELEGRAM_LOGGER_CHAT_ID=-100xxxxxxxxxx TELEGRAM_LOGGER_MESSAGE_THREAD_ID= TELEGRAM_LOGGER_QUEUE=default TELEGRAM_LOGGER_QUEUE_CONNECTION=sync TELEGRAM_LOGGER_TIMEOUT=5 # optional TELEGRAM_LOGGER_PROXY= # optional: per-level forum topics (see below) #TELEGRAM_LOGGER_DEBUG_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_INFO_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_NOTICE_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_WARNING_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_ERROR_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_CRITICAL_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_ALERT_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_EMERGENCY_MESSAGE_THREAD_ID= # optional: named topics defined in your config, e.g. #TELEGRAM_LOGGER_TOPIC_PAYMENTS_MESSAGE_THREAD_ID= #TELEGRAM_LOGGER_TOPIC_PAYMENTS_CHAT_ID= #TELEGRAM_LOGGER_TOPIC_PAYMENTS_BOT_TOKEN=
TELEGRAM_LOGGER_ENABLE defaults to false. The package sends nothing until it is set to true.
Topic-related variables (all optional):
TELEGRAM_LOGGER_<LEVEL>_MESSAGE_THREAD_ID— the published config maps each log level (DEBUG,INFO,NOTICE,WARNING,ERROR,CRITICAL,ALERT,EMERGENCY) to its own forum topic id. Leave them empty to send everything to the default destination. See Per-level overrides.- Named topic variables (such as
TELEGRAM_LOGGER_TOPIC_PAYMENTS_MESSAGE_THREAD_ID) — only used if you define named topics inconfig/telegram_logger.php; the variable names are up to you. A topic can override the token and chat id as well, so keep the overridden key in the name (..._BOT_TOKEN,..._CHAT_ID,..._MESSAGE_THREAD_ID). See Forum topics / targeted sending.
3. Add a logging channel
In config/logging.php:
'telegram' => [ 'driver' => 'custom', 'via' => \RaifuCore\TelegramLogger\Logger::class, 'level' => 'debug', ],
The level key is required — the handler reads it directly and fails without it.
If your default channel is stack, include telegram there:
'stack' => [ 'driver' => 'stack', 'channels' => ['single', 'telegram'], ],
4. Send a test
php artisan tg-logger:test
The test command uses the default Log facade (Log::debug(), Log::info(), …), not Log::channel('telegram'). Add telegram to your default/stack channel, or the test will not reach Telegram.
Configuration
Main settings live in config/telegram_logger.php and are usually driven by .env.
| Variable | Default | Description |
|---|---|---|
TELEGRAM_LOGGER_ENABLE |
false |
Enable Telegram logging |
TELEGRAM_LOGGER_BOT_TOKEN |
— | Bot token from @BotFather |
TELEGRAM_LOGGER_CHAT_ID |
— | Target chat id |
TELEGRAM_LOGGER_MESSAGE_THREAD_ID |
— | Default forum topic id |
TELEGRAM_LOGGER_QUEUE |
default |
Queue name for delivery jobs |
TELEGRAM_LOGGER_QUEUE_CONNECTION |
sync |
Queue connection (sync, redis, …) |
TELEGRAM_LOGGER_TIMEOUT |
5 |
HTTP timeout in seconds |
TELEGRAM_LOGGER_PROXY |
— | Optional proxy URL |
Proxy examples:
tcp://host:porttcp://user:pass@host:portsocks5://user:pass@host:port
Per-level and named topic env vars are listed in Configure .env.
Telegram sendMessage options
Root and override configs accept Telegram Bot API options:
'options' => [ // 'disable_web_page_preview' => true, // 'disable_notification' => false, ],
See sendMessage.
Options are merged in this order (later wins): built-in default parse_mode=html → root options → level or topic options. So parse_mode defaults to html even when it is commented out in the config file, but you can override it via options. Keep in mind that the built-in templates rely on HTML tags.
Usage
use Illuminate\Support\Facades\Log; Log::error('Payment failed', ['order_id' => 123]); Log::channel('telegram')->warning('Telegram channel only');
The context key RaifuCore\TelegramLogger\Handler::CONTEXT_KEY (value raifu_telegram) is reserved for package options and is stripped from the rendered message. See Forum topics / targeted sending.
Templates
Built-in Blade templates:
telegram_logger::standard(default) — app name, level, env, datetime, formatted messagetelegram_logger::minimal— app name, level, formatted message
Available view variables include:
appName,appEnvlevel_name,datetime,formatted- other fields from the Monolog record (
message,context,extra, …)
To customize:
- Publish views.
- Edit or add a template under
resources/views/vendor/telegram_logger/. - Point
templateinconfig/telegram_logger.phpto your view:
'template' => 'telegram_logger::minimal',
Or at runtime:
config(['telegram_logger.template' => 'telegram_logger::minimal']);
Per-level overrides
Override root settings per PSR log level in config/telegram_logger.php:
'levels' => [ 'error' => [ 'chat_id' => env('TELEGRAM_LOGGER_ERROR_CHAT_ID'), 'message_thread_id' => env('TELEGRAM_LOGGER_ERROR_MESSAGE_THREAD_ID'), 'template' => 'telegram_logger::minimal', 'options' => [ 'disable_notification' => true, ], ], ],
Supported keys per level:
tokenchat_idmessage_thread_idtemplateoptions
Missing or empty keys fall back to the root config values, so leaving a per-level env variable empty is the same as not setting it.
Forum topics / targeted sending
Route a single log message with the reserved context key RaifuCore\TelegramLogger\Handler::CONTEXT_KEY (value raifu_telegram). Always reference the constant instead of the raw string. That key is stripped before rendering, so it never appears in the delivered text.
use Illuminate\Support\Facades\Log; use RaifuCore\TelegramLogger\Handler;
Numeric topic
Log::info('Order paid', [Handler::CONTEXT_KEY => ['topic' => 123]]);
Named topic
Define optional named topics in config:
'topics' => [ 'payments' => [ 'message_thread_id' => env('TELEGRAM_LOGGER_TOPIC_PAYMENTS_MESSAGE_THREAD_ID'), // optional: // 'token' => env('TELEGRAM_LOGGER_TOPIC_PAYMENTS_BOT_TOKEN'), // 'chat_id' => env('TELEGRAM_LOGGER_TOPIC_PAYMENTS_CHAT_ID'), // 'template' => 'telegram_logger::minimal', // 'options' => ['disable_notification' => true], // 'duplicate' => true, ], // another chat, no topics there 'audit' => [ 'chat_id' => env('TELEGRAM_LOGGER_TOPIC_AUDIT_CHAT_ID'), ], ],
Log::info('Order paid', [Handler::CONTEXT_KEY => ['topic' => 'payments']]);
Supported keys per named topic — all of them are optional:
message_thread_id,token,chat_id,template,options— fall back to root configduplicate— defaultfalse
A thread id only exists inside its own chat, so message_thread_id falls back to the root value only while the topic stays in the root chat (a topic chat_id equal to the root one still counts as the root chat). A topic pointing to another chat without its own message_thread_id posts to that chat directly, without a topic. When no thread id applies at all, the message is sent to the chat itself.
Destination rules
- No
topicin the context → send to the level-based destination (level overrides over root). - Topic without
duplicate→ send only to the topic destination. - Topic with
duplicate=true→ send to the level-based destination and the topic destination.
duplicate resolution:
- Context key
duplicatewins when provided. - For named topics, config
topics.*.duplicateis used when context omits it. - Numeric topics have no config default; without context
duplicate, only the topic is used.
When a topic is applied, topic overrides fall back to root config (token / chat_id / message_thread_id / template / options), not to level overrides. Level overrides apply only on the level-based pass. The message_thread_id fallback is skipped for a topic pointing to another chat, as described above.
Other behavior:
- Unknown named topic → no error; the message is delivered to the default destination (
chat_id/message_thread_id, with level overrides applied). - Topic missing on Telegram's side (deleted, closed, or never existed in that chat) → the message is resent to the same chat without
message_thread_id. - Invalid
topictype → fallback to the level-based destination only; the type mismatch is written to thesinglelog channel. - Identical destinations (
chat_id+message_thread_id) are deduplicated within one log write.
Queue behavior
Messages are dispatched as RaifuCore\TelegramLogger\Job.
TELEGRAM_LOGGER_QUEUE_CONNECTION=sync(default) — jobs run immediately in the same process.- Async connections (
redis,database, …) — jobs are pushed to theTELEGRAM_LOGGER_QUEUEqueue (defaultdefault). Run a queue worker for that connection/queue. - A failed HTTP request is not retried: the job is marked as failed immediately. On async connections check the
failed_jobstable. - The one exception is a missing topic (
message thread not found,TOPIC_DELETED,TOPIC_CLOSED): the same job immediately resends the message to the chat withoutmessage_thread_id, so a stale topic id never swallows a log message.
Message behavior
- Default
parse_modeishtml(can be overridden viaoptions). - Rendered text is split into chunks of up to 4096 bytes, which keeps every chunk within Telegram's 4096-character message limit.
- If no
templateis configured, a plain fallback format is used:<b>{app name}</b> ({level})followed by the formatted message. - Handler exceptions are written to
Log::channel('single')and are not sent back to Telegram, so the logger cannot recurse into itself. Thesinglechannel must exist in yourconfig/logging.php(it does in a default Laravel install).
Testing
php artisan tg-logger:test php artisan tg-logger:test --topic=payments php artisan tg-logger:test --topic=123 --duplicate
--topic accepts a named topic from config or a numeric (non-negative) message_thread_id.
--duplicate also sends the topic test message to the level-based destination.
The command sends one message per log level (8 in total) with a ~2 second pause between them to avoid Telegram rate limits, so it takes about 15 seconds to finish.
Reminder: the command uses the default logger, so telegram must be part of your default/stack channel.
Troubleshooting
| Symptom | What to check |
|---|---|
| Nothing is sent | TELEGRAM_LOGGER_ENABLE=true |
token or chat_id are not defined in single logs |
TELEGRAM_LOGGER_BOT_TOKEN / TELEGRAM_LOGGER_CHAT_ID |
| API / chat errors | Bot is added to the chat and allowed to post (including the forum topic) |
| Message lands in the default destination | Topic name missing from topics config (falls back silently) |
| Message lands in the chat instead of the topic | Topic id is stale or belongs to another chat (silent resend without it) |
| Wrong or missing topic | message_thread_id / named topic config |
| Jobs never leave the queue | Queue worker is running for the configured connection and queue |
| Test command silent | telegram is included in the default/stack channel |
Package failures are logged to the Laravel single channel (storage/logs/laravel.log by default). This requires the single channel to be present in config/logging.php — it is there in a default Laravel install, so only check this if you have removed or renamed it.
Upgrading from 1.x
- The reserved context key changed from
'telegram'toHandler::CONTEXT_KEY('raifu_telegram'). Replace['telegram' => [...]]with[\RaifuCore\TelegramLogger\Handler::CONTEXT_KEY => [...]]. A context key namedtelegramis now treated as regular log context and is rendered into the message. - Named topics no longer require
message_thread_id: it falls back to the root value while the topic stays in the root chat. A topic pointing to another chat without its own thread id posts to that chat without a topic. - A message aimed at a topic that no longer exists is no longer lost: it is resent to the same chat without
message_thread_id. - An unknown named topic no longer writes an error to the
singlechannel; the message goes to the default destination instead. - Suggested env naming for named topics:
TELEGRAM_LOGGER_TOPIC_<NAME>_MESSAGE_THREAD_ID,..._CHAT_ID,..._BOT_TOKEN. Env names are yours to choose, but the published config example uses this form.
License
MIT