├── .gitignore ├── .php_cs ├── .travis.yml ├── phpunit.xml.dist ├── LICENSE ├── composer.json ├── src └── Mero │ └── Monolog │ ├── Formatter │ └── HtmlFormatter.php │ └── Handler │ └── TelegramHandler.php ├── tests └── Mero │ └── Monolog │ └── Handler │ ├── TestCase.php │ └── TelegramHandlerTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /.php_cs.cache 4 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in(__DIR__) 6 | ; 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRules([ 10 | '@PSR2' => true, 11 | 'array_syntax' => ['syntax' => 'short'], 12 | ]) 13 | ->setFinder($finder) 14 | ; 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache 8 | 9 | php: 10 | - 5.6 11 | - 7.0 12 | - 7.1 13 | - 7.2 14 | - nightly 15 | 16 | matrix: 17 | fast_finish: true 18 | allow_failures: 19 | - php: nightly 20 | 21 | before_install: 22 | - composer self-update 23 | 24 | install: 25 | - composer install --prefer-dist 26 | 27 | script: 28 | - composer test 29 | 30 | after_script: 31 | - php ./vendor/bin/coveralls -v 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ./tests/Mero/Monolog/ 10 | 11 | 12 | 13 | 14 | ./src/Mero/Monolog/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Rafael Mello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mero/telegram-handler", 3 | "description": "Monolog handler to send log by Telegram", 4 | "keywords": [ 5 | "telegram", 6 | "monolog", 7 | "logging", 8 | "log" 9 | ], 10 | "type": "library", 11 | "license": "MIT", 12 | "support": { 13 | "issues": "https://github.com/merorafael/telegram-handler/issues", 14 | "source": "https://github.com/merorafael/telegram-handler" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Rafael Mello", 19 | "email": "merorafael@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "ext-curl": "*", 24 | "php": ">=5.6 || >=7.0 || >=8.0", 25 | "monolog/monolog": "^1.20" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^5.6", 29 | "satooshi/php-coveralls": "~1.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Mero\\Monolog\\": "src/Mero/Monolog" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Mero\\Monolog\\": "tests/Mero/Monolog" 39 | } 40 | }, 41 | "suggest": { 42 | "symfony/yaml": "Needed to use HtmlFormatter feature" 43 | }, 44 | "extra": { 45 | "branch-alias": { 46 | "dev-master": "0.4.x-dev" 47 | } 48 | }, 49 | "scripts": { 50 | "test": [ 51 | "phpunit" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Mero/Monolog/Formatter/HtmlFormatter.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @see https://core.telegram.org/bots/api#formatting-options 14 | */ 15 | class HtmlFormatter extends NormalizerFormatter 16 | { 17 | /** 18 | * @inheritDoc 19 | */ 20 | public function __construct($dateFormat = null) 21 | { 22 | parent::__construct($dateFormat); 23 | } 24 | 25 | /** 26 | * Formats a log record. 27 | * 28 | * @param array $record A record to format 29 | * @return mixed The formatted record 30 | */ 31 | public function format(array $record) 32 | { 33 | $output = "{$record['level_name']}".PHP_EOL; 34 | $output .= "Message: {$record['message']}".PHP_EOL; 35 | $output .= "Time: {$record['datetime']->format($this->dateFormat)}".PHP_EOL; 36 | $output .= "Channel: {$record['channel']}".PHP_EOL; 37 | 38 | if ($record['context']) { 39 | $output .= PHP_EOL; 40 | $output .= "[context]".PHP_EOL; 41 | $output .= Yaml::dump($record['context']); 42 | } 43 | if ($record['extra']) { 44 | $output .= PHP_EOL; 45 | $output .= "[context]".PHP_EOL; 46 | $output .= Yaml::dump($record['extra']); 47 | } 48 | 49 | return $output; 50 | } 51 | 52 | /** 53 | * Formats a set of log records. 54 | * 55 | * @param array $records A set of records to format 56 | * @return mixed The formatted set of records 57 | */ 58 | public function formatBatch(array $records) 59 | { 60 | $message = ''; 61 | foreach ($records as $record) { 62 | $message .= $this->format($record); 63 | } 64 | 65 | return $message; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Mero/Monolog/Handler/TestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Mero\Monolog\Handler; 13 | 14 | use Monolog\Logger; 15 | use PHPUnit\Framework\TestCase as PHPUnitTestCase; 16 | 17 | class TestCase extends PHPUnitTestCase 18 | { 19 | /** 20 | * @return array Record 21 | */ 22 | protected function getRecord($level = Logger::WARNING, $message = 'test', $context = []) 23 | { 24 | return [ 25 | 'message' => $message, 26 | 'context' => $context, 27 | 'level' => $level, 28 | 'level_name' => Logger::getLevelName($level), 29 | 'channel' => 'test', 30 | 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))), 31 | 'extra' => [], 32 | ]; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | protected function getMultipleRecords() 39 | { 40 | return [ 41 | $this->getRecord(Logger::DEBUG, 'debug message 1'), 42 | $this->getRecord(Logger::DEBUG, 'debug message 2'), 43 | $this->getRecord(Logger::INFO, 'information'), 44 | $this->getRecord(Logger::WARNING, 'warning'), 45 | $this->getRecord(Logger::ERROR, 'error'), 46 | ]; 47 | } 48 | 49 | /** 50 | * @return Monolog\Formatter\FormatterInterface 51 | */ 52 | protected function getIdentityFormatter() 53 | { 54 | $formatter = $this 55 | ->getMockBuilder('Monolog\\Formatter\\FormatterInterface') 56 | ->getMock(); 57 | $formatter->expects($this->any()) 58 | ->method('format') 59 | ->will($this->returnCallback(function ($record) { 60 | return $record['message']; 61 | })); 62 | 63 | return $formatter; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Mero/Monolog/Handler/TelegramHandlerTest.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @see https://core.telegram.org/bots/api 13 | */ 14 | class TelegramHandlerTest extends TestCase 15 | { 16 | /** 17 | * @var resource 18 | */ 19 | private $res; 20 | 21 | /** 22 | * @var TelegramHandler 23 | */ 24 | private $handler; 25 | 26 | public function setUp() 27 | { 28 | if (!extension_loaded('curl')) { 29 | $this->markTestSkipped('This test requires curl to run'); 30 | } 31 | $this->handler = new TelegramHandler('myToken', 'myChat', Logger::DEBUG, true); 32 | } 33 | 34 | public function testCreateHandler() 35 | { 36 | $this->assertInstanceOf(TelegramHandler::class, $this->handler); 37 | } 38 | 39 | public function testWriteHeader() 40 | { 41 | $class = new \ReflectionClass(TelegramHandler::class); 42 | $method = $class->getMethod('buildHeader'); 43 | $method->setAccessible(true); 44 | 45 | $header = $method->invoke($this->handler, 'test'); 46 | 47 | $this->assertContains('Content-Type: application/json', $header); 48 | $this->assertContains('Content-Length: 4', $header); 49 | } 50 | 51 | public function testWriteContent() 52 | { 53 | $this->handler->setFormatter($this->getIdentityFormatter()); 54 | 55 | $class = new \ReflectionClass(TelegramHandler::class); 56 | $method = $class->getMethod('buildContent'); 57 | $method->setAccessible(true); 58 | 59 | $content = $method->invoke($this->handler, ['formatted' => 'test1']); 60 | 61 | $this->assertRegexp('/{"chat_id":"myChat","text":"test1"}$/', $content); 62 | } 63 | 64 | public function testWriteContentWithTelegramFormatter() 65 | { 66 | $this->handler->setFormatter(new HtmlFormatter()); 67 | 68 | $class = new \ReflectionClass(TelegramHandler::class); 69 | $method = $class->getMethod('buildContent'); 70 | $method->setAccessible(true); 71 | 72 | $content = $method->invoke($this->handler, ['formatted' => 'test1']); 73 | 74 | $this->assertRegexp('/{"chat_id":"myChat","text":"test1","parse_mode":"HTML"}$/', $content); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Mero/Monolog/Handler/TelegramHandler.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * @see https://core.telegram.org/bots/api 17 | */ 18 | class TelegramHandler extends AbstractProcessingHandler 19 | { 20 | /** 21 | * @var string Telegram API token 22 | */ 23 | private $token; 24 | 25 | /** 26 | * @var int Chat identifier 27 | */ 28 | private $chatId; 29 | 30 | /** 31 | * @var int Request timeout 32 | */ 33 | private $timeout; 34 | 35 | /** 36 | * @param string $token Telegram API token 37 | * @param int $chatId Chat identifier 38 | * @param int $level The minimum logging level at which this handler will be triggered 39 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not 40 | * 41 | * @throws MissingExtensionException If the PHP cURL extension is not loaded 42 | */ 43 | public function __construct( 44 | $token, 45 | $chatId, 46 | $level = Logger::CRITICAL, 47 | $bubble = true 48 | ) { 49 | if (!extension_loaded('curl')) { 50 | throw new MissingExtensionException('The cURL PHP extension is required to use the TelegramHandler'); 51 | } 52 | 53 | $this->token = $token; 54 | $this->chatId = $chatId; 55 | $this->timeout = 0; 56 | 57 | parent::__construct($level, $bubble); 58 | } 59 | 60 | /** 61 | * Define a timeout to Telegram send message request. 62 | * 63 | * @param int $timeout Request timeout 64 | * 65 | * @return TelegramHandler 66 | */ 67 | public function setTimeout($timeout) 68 | { 69 | $this->timeout = $timeout; 70 | return $this; 71 | } 72 | 73 | /** 74 | * Builds the header of the API Call. 75 | * 76 | * @param string $content 77 | * 78 | * @return array 79 | */ 80 | protected function buildHeader($content) 81 | { 82 | return [ 83 | 'Content-Type: application/json', 84 | 'Content-Length: '.strlen($content), 85 | ]; 86 | } 87 | 88 | /** 89 | * Builds the body of API call. 90 | * 91 | * @param array $record 92 | * 93 | * @return string 94 | */ 95 | protected function buildContent(array $record) 96 | { 97 | $content = [ 98 | 'chat_id' => $this->chatId, 99 | 'text' => $record['formatted'], 100 | ]; 101 | 102 | if ($this->formatter instanceof HtmlFormatter) { 103 | $content['parse_mode'] = 'HTML'; 104 | } 105 | 106 | return json_encode($content); 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | * 112 | * @param array $record 113 | */ 114 | protected function write(array $record) 115 | { 116 | $content = $this->buildContent($record); 117 | 118 | $ch = curl_init(); 119 | 120 | curl_setopt($ch, CURLOPT_HTTPHEADER, $this->buildHeader($content)); 121 | curl_setopt($ch, CURLOPT_URL, sprintf('https://api.telegram.org/bot%s/sendMessage', $this->token)); 122 | curl_setopt($ch, CURLOPT_POST, true); 123 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 124 | curl_setopt($ch, CURLOPT_POSTFIELDS, $content); 125 | curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); 126 | 127 | Curl\Util::execute($ch); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TelegramHandler 2 | =============== 3 | 4 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d7f41933-3e48-4c2d-befc-35aba76bf0ef/mini.png)](https://insight.sensiolabs.com/projects/d7f41933-3e48-4c2d-befc-35aba76bf0ef) 5 | [![Build Status](https://travis-ci.org/merorafael/telegram-handler.svg?branch=master)](https://travis-ci.org/merorafael/telegram-handler) 6 | [![Coverage Status](https://coveralls.io/repos/github/merorafael/telegram-handler/badge.svg?branch=master)](https://coveralls.io/github/merorafael/telegram-handler?branch=master) 7 | [![Latest Stable Version](https://poser.pugx.org/mero/telegram-handler/v/stable.svg)](https://packagist.org/packages/mero/telegram-handler) 8 | [![Total Downloads](https://poser.pugx.org/mero/telegram-handler/downloads.svg)](https://packagist.org/packages/mero/telegram-handler) 9 | [![License](https://poser.pugx.org/mero/telegram-handler/license.svg)](https://packagist.org/packages/mero/telegram-handler) 10 | 11 | Monolog handler to send log by Telegram. 12 | 13 | Requirements 14 | ------------ 15 | 16 | - PHP 5.6 or above 17 | - cURL extension 18 | 19 | Instalation with composer 20 | ------------------------- 21 | 22 | 1. Open your project directory; 23 | 2. Run `composer require mero/telegram-handler` to add `TelegramHandler` in your project vendor; 24 | 3. Add `symfony/yaml` dependency if you need use the `\Mero\Monolog\Formatter\HtmlFormatter`. 25 | 26 | Declaring handler object 27 | ------------------------ 28 | 29 | To declare this handler, you need to know the bot token and the chat identifier(chat_id) to 30 | which the log will be sent. 31 | 32 | ```php 33 | // ... 34 | $handler = new \Mero\Monolog\Handler\TelegramHandler('', , ); 35 | // ... 36 | ``` 37 | 38 | **Example:** 39 | 40 | ```php 41 | setFormatter(new \Mero\Monolog\Formatter\HtmlFormatter()); 51 | $handler->setTimeout(30); 52 | $log->pushHandler($handler); 53 | 54 | $log->debug('Message log'); 55 | ``` 56 | 57 | The above example is using HtmlFormatter for Telegram API. This feature is added on 0.3.0 release and 58 | you can use declaring handler formatter to use `\Mero\Monolog\Formatter\HtmlFormatter` class. 59 | 60 | You can set the timeout for Telegram request using `setTimeout` method, implemented on `TelegramHandler`. This feature is implemented on 0.4.0 release and this use is not required. 61 | 62 | Creating a bot 63 | -------------- 64 | 65 | To use this handler, you need to create your bot on telegram and receive the Bot API access token. 66 | To do this, start a conversation with **@BotFather**. 67 | 68 | **Conversation example:** 69 | 70 | In the example below, I'm talking to **@BotFather**. to create a bot named "Cronus Bot" with user "@cronus_bot". 71 | 72 | ``` 73 | Me: /newbot 74 | --- 75 | @BotFather: Alright, a new bot. How are we going to call it? Please choose a name for your bot. 76 | --- 77 | Me: Cronus Bot 78 | --- 79 | @BotFather: Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: 80 | TetrisBot or tetris_bot. 81 | --- 82 | Me: cronus_bot 83 | --- 84 | @BotFather: Done! Congratulations on your new bot. You will find it at telegram.me/cronus_bot. You can now add a 85 | description, about section and profile picture for your bot, see /help for a list of commands. By the way, when 86 | you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure 87 | the bot is fully operational before you do this. 88 | 89 | Use this token to access the HTTP API: 90 | 000000000:XXXXX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx 91 | 92 | For a description of the Bot API, see this page: https://core.telegram.org/bots/api 93 | ``` 94 | 95 | Give a chat identifier 96 | ---------------------- 97 | 98 | To retrieve the chat_id in which the log will be sent, the recipient user will first need a conversation with 99 | the bot. After the conversation has started, make the request below to know the chat_id of that conversation. 100 | 101 | **URL:** https://api.telegram.org/bot_token_/getUpdates 102 | 103 | **Example:** 104 | 105 | ``` 106 | Request 107 | ------- 108 | POST https://api.telegram.org/bot000000000:XXXXX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx/getUpdates 109 | 110 | Response 111 | -------- 112 | { 113 | "ok": true, 114 | "result": [ 115 | { 116 | "update_id": 141444845, 117 | "message": { 118 | "message_id": 111, 119 | "from": { 120 | "id": 111111111, 121 | "first_name": "Rafael", 122 | "last_name": "Mello", 123 | "username": "merorafael" 124 | }, 125 | "chat": { 126 | "id": 111111111, 127 | "first_name": "Rafael", 128 | "last_name": "Mello", 129 | "username": "merorafael", 130 | "type": "private" 131 | }, 132 | "date": 1480701504, 133 | "text": "test" 134 | } 135 | } 136 | ] 137 | } 138 | ``` 139 | 140 | In the above request, the chat_id is represented by the number "111111111". 141 | --------------------------------------------------------------------------------