├── .editorconfig ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── Atrapalo │ └── Monolog │ ├── Formatter │ └── ElasticsearchFormatter.php │ └── Handler │ └── ElasticsearchHandler.php └── tests └── Atrapalo └── Test └── Monolog ├── Formatter └── ElasticsearchFormatterTest.php └── Handler └── ElasticsearchHandlerTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /vendor/ 3 | /phpunit.xml 4 | /build/ 5 | /composer.lock -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | tests: 3 | override: 4 | - 5 | command: 'phpunit --coverage-clover=build/clover.xml' 6 | coverage: 7 | file: 'build/clover.xml' 8 | format: 'php-clover' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: php 4 | 5 | php: 6 | - 5.5 7 | - 5.6 8 | - 7 9 | 10 | services: 11 | - docker 12 | 13 | branches: 14 | only: 15 | - master 16 | 17 | before_install: 18 | - docker run -d -p "127.0.0.1:9200:9200" elasticsearch 19 | 20 | install: 21 | - composer install --prefer-source 22 | 23 | before_script: 24 | - composer self-update 25 | 26 | script: 27 | - php bin/phpunit 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at dt@atrapalo.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, ¡Thank you for your contribution! :) 4 | 5 | ## Code of conduct 6 | 7 | This project adheres to the Contributor Covenant 1.3. By participating, you are expected to uphold this code. Please report unacceptable behavior to dt@atrapalo.com. 8 | 9 | ## Contributing to the code base 10 | 11 | Here are a few rules to follow in order to ease code reviews, and discussions before maintainers accept and merge your work. 12 | 13 | Before proposing a pull request, check the following: 14 | 15 | * Your code should follow the [PSR-2 coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). You can use [php-cs-fixer](https://github.com/fabpot/PHP-CS-Fixer) to sniff your code and fix inconsistencies. 16 | * Unit tests should still pass after your patch. Run the tests on your dev server (with the simple `php bin/phpunit`) or check the continuous integration status for your pull request. 17 | * As much as possible, add unit tests for your code 18 | * You SHOULD write documentation. 19 | * Please, write [commit messages that make sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) before submitting your Pull Request. 20 | * One may ask you to [squash your commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) too. This is used to "clean" your Pull Request before merging it (we don't want commits such as `fix tests`, `fix 2`, `fix 3`, etc.). 21 | * Also, while creating your Pull Request on GitHub, you MUST write a description which gives the context and/or explains why you are creating it. 22 | 23 | Once your code is merged, it is available for free to everybody under the MIT License. Publishing your Pull Request on the this GitHub repository means that you agree with this license for your contribution. 24 | 25 | Thank you! 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ATRÁPALO, S.L. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Monolog Elasticsearch handler and formatter 2 | =========================================== 3 | 4 | This extremely simple library provide of an elasticsearch handler and formatter which makes use of the official PHP 5 | Elasticsearch client. 6 | 7 | [![Build Status](https://travis-ci.org/atrapalo/monolog-elasticsearch.svg?branch=master)](https://travis-ci.org/atrapalo/monolog-elasticsearch) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/atrapalo/monolog-elasticsearch/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/atrapalo/monolog-elasticsearch/?branch=master) 8 | 9 | ## Usage 10 | 11 | ```php 12 | pushHandler( 20 | new ElasticsearchHanler($client, ['index' => 'logs', 'type' => 'log']) 21 | ); 22 | 23 | ``` 24 | 25 | ## Installation 26 | 27 | This library can be installed through composer 28 | 29 | ```sh 30 | composer require atrapalo/monolog-elasticsearch 31 | ``` 32 | 33 | ## Requirements 34 | 35 | In order to make use of this library you will need 36 | 37 | * Monolog 38 | * An elasticsearch instance 39 | 40 | ## Contributing 41 | 42 | See CONTRIBUTING file. 43 | 44 | ## Running the Tests 45 | 46 | ```bash 47 | php bin/phpunit 48 | ``` 49 | 50 | ## Credits 51 | 52 | * Christian Soronellas 53 | 54 | ## Contributor Code of Conduct 55 | 56 | Please note that this project is released with a [Contributor Code of Conduct](http://contributor-covenant.org/). By 57 | participating in this project you agree to abide by its terms. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) file. 58 | 59 | ## License 60 | 61 | Monolog-Elasticsearch handler is released under the MIT License. See the bundled LICENSE file for details. 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atrapalo/monolog-elasticsearch", 3 | "description": "A Monolog handler and formatter that makes use of the elasticsearch/elasticsearch package", 4 | "type": "library", 5 | "keywords": ["log", "logging", "logger", "monolog", "psr3", "psr-3", "elastic", "elasticsearch"], 6 | "require": { 7 | "elasticsearch/elasticsearch": "^2.0" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^4.0", 11 | "monolog/monolog": "^1.17" 12 | }, 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Christian Soronellas", 17 | "email": "christian.soronellas@atrapalo.com" 18 | } 19 | ], 20 | "minimum-stability": "stable", 21 | "config": { 22 | "bin-dir": "bin/" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Atrapalo\\": "src/Atrapalo" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "": "tests/" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | src/ 21 | 22 | tests 23 | /path/to/file 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Atrapalo/Monolog/Formatter/ElasticsearchFormatter.php: -------------------------------------------------------------------------------- 1 | index = $index; 25 | $this->type = $type; 26 | } 27 | 28 | public function format(array $record) 29 | { 30 | $record = parent::format($record); 31 | 32 | return [ 33 | 'type' => $this->type, 34 | 'index' => $this->index, 35 | 'body' => $record 36 | ]; 37 | } 38 | 39 | public function formatBatch(array $records) 40 | { 41 | $bulk = ['body' => []]; 42 | 43 | foreach ($records as $record) { 44 | $bulk['body'][] = [ 45 | 'index' => [ 46 | '_index' => $this->index, 47 | '_type' => $this->type, 48 | ] 49 | ]; 50 | 51 | $bulk['body'][] = parent::format($record); 52 | } 53 | 54 | return $bulk; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Atrapalo/Monolog/Handler/ElasticsearchHandler.php: -------------------------------------------------------------------------------- 1 | build(); 18 | * $options = [ 19 | * 'index' => 'elastic_index_name', 20 | * 'type' => 'elastic_doc_type', 21 | * ]; 22 | * $handler = new ElasticsearchHandler($client, $options); 23 | * $log = new Logger('application'); 24 | * $log->pushHandler($handler); 25 | * 26 | * @author Christian Soronellas 27 | */ 28 | class ElasticsearchHandler extends AbstractProcessingHandler 29 | { 30 | const DEFAULT_INDEX_NAME = 'monolog'; 31 | const DEFAULT_TYPE_NAME = 'record'; 32 | 33 | /** 34 | * @var Client 35 | */ 36 | protected $client; 37 | 38 | /** 39 | * @var array Handler config options 40 | */ 41 | protected $options = []; 42 | 43 | /** 44 | * @param Client $client Elastica Client object 45 | * @param array $options Handler configuration 46 | * @param integer $level The minimum logging level at which this handler will be triggered 47 | * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not 48 | */ 49 | public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, $bubble = true) 50 | { 51 | $this->client = $client; 52 | $this->options = array_merge(['index' => static::DEFAULT_INDEX_NAME, 'type' => static::DEFAULT_TYPE_NAME], $options); 53 | 54 | parent::__construct($level, $bubble); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | protected function write(array $record) 61 | { 62 | $this->client->index($record['formatted']); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function setFormatter(FormatterInterface $formatter) 69 | { 70 | if ($formatter instanceof ElasticsearchFormatter) { 71 | return parent::setFormatter($formatter); 72 | } 73 | 74 | throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter'); 75 | } 76 | 77 | /** 78 | * Getter options 79 | * @return array 80 | */ 81 | public function getOptions() 82 | { 83 | return $this->options; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | protected function getDefaultFormatter() 90 | { 91 | return new ElasticsearchFormatter($this->options['index'], $this->options['type']); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function handleBatch(array $records) 98 | { 99 | $this->client->bulk( 100 | $this->getFormatter()->formatBatch($records) 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Atrapalo/Test/Monolog/Formatter/ElasticsearchFormatterTest.php: -------------------------------------------------------------------------------- 1 | elasticsearchFormatter = new ElasticsearchFormatter('index_name', 'type_name'); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function it_should_format_a_log_record_according_with_the_elasticsearch_client_format() 26 | { 27 | $record = [ 28 | 'level' => Logger::ERROR, 29 | 'level_name' => 'ERROR', 30 | 'channel' => 'meh', 31 | 'context' => ['foo' => 7, 'bar', 'class' => new \stdClass], 32 | 'datetime' => new DateTime("@0"), 33 | 'extra' => [], 34 | 'message' => 'log', 35 | ]; 36 | 37 | $expected = [ 38 | 'index' => 'index_name', 39 | 'type' => 'type_name', 40 | 'body' => $record 41 | ]; 42 | 43 | $expected['body']['datetime'] = '1970-01-01T00:00:00+0000'; 44 | $expected['body']['context'] = [ 45 | 'class' => '[object] (stdClass: {})', 46 | 'foo' => 7, 47 | 0 => 'bar', 48 | ]; 49 | 50 | $this->assertEquals($expected, $this->elasticsearchFormatter->format($record)); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function it_should_format_a_batch_of_log_records_according_to_the_elasticsearch_client_format() 57 | { 58 | $batch = [ 59 | [ 60 | 'level' => Logger::ERROR, 61 | 'level_name' => 'ERROR', 62 | 'channel' => 'meh', 63 | 'context' => ['foo' => 7, 'bar', 'class' => new \stdClass], 64 | 'datetime' => new DateTime("@0"), 65 | 'extra' => [], 66 | 'message' => 'log', 67 | ], 68 | [ 69 | 'level' => Logger::INFO, 70 | 'level_name' => 'INFO', 71 | 'channel' => 'hem', 72 | 'context' => ['foo' => 8, 'bar', 'class' => new \stdClass], 73 | 'datetime' => new DateTime("@0"), 74 | 'extra' => [], 75 | 'message' => 'log2', 76 | ], 77 | ]; 78 | 79 | $expected = [ 80 | 'body' => [ 81 | [ 82 | 'index' => [ 83 | '_index' => 'index_name', 84 | '_type' => 'type_name' 85 | ] 86 | ], 87 | [ 88 | 'level' => Logger::ERROR, 89 | 'level_name' => 'ERROR', 90 | 'channel' => 'meh', 91 | 'context' => ['class' => '[object] (stdClass: {})', 'foo' => 7, 0 => 'bar'], 92 | 'datetime' => '1970-01-01T00:00:00+0000', 93 | 'extra' => [], 94 | 'message' => 'log', 95 | ], 96 | [ 97 | 'index' => [ 98 | '_index' => 'index_name', 99 | '_type' => 'type_name' 100 | ] 101 | ], 102 | [ 103 | 'level' => Logger::INFO, 104 | 'level_name' => 'INFO', 105 | 'channel' => 'hem', 106 | 'context' => ['foo' => 8, 0 => 'bar', 'class' => '[object] (stdClass: {})'], 107 | 'datetime' => '1970-01-01T00:00:00+0000', 108 | 'extra' => [], 109 | 'message' => 'log2', 110 | ] 111 | ] 112 | ]; 113 | 114 | $this->assertEquals($expected, $this->elasticsearchFormatter->formatBatch($batch)); 115 | } 116 | } -------------------------------------------------------------------------------- /tests/Atrapalo/Test/Monolog/Handler/ElasticsearchHandlerTest.php: -------------------------------------------------------------------------------- 1 | prophesize(Client::class); 24 | 25 | $logger->pushHandler( 26 | new ElasticsearchHandler($elasticsearchClient->reveal()) 27 | ); 28 | 29 | $logger->info('This is a test'); 30 | 31 | $elasticsearchClient->index(Argument::type('array'))->shouldHaveBeenCalled(); 32 | } 33 | 34 | /** @test */ 35 | public function testIntegration() 36 | { 37 | $client = 38 | ClientBuilder::create() 39 | ->setHosts(['127.0.0.1:9200']) 40 | ->build() 41 | ; 42 | 43 | try { 44 | $client->ping(); 45 | } catch (NoNodesAvailableException $e) { 46 | $this->markTestSkipped('Skipped due to a missing instance of Elasticsearch'); 47 | } 48 | 49 | try { 50 | $client->indices()->delete(['index' => ElasticsearchHandler::DEFAULT_INDEX_NAME]); 51 | } catch (Missing404Exception $e) { 52 | // Noop 53 | } 54 | 55 | $logger = new Logger('application', [new ElasticsearchHandler($client)]); 56 | 57 | $timezone = new \DateTimeZone('UTC'); 58 | $datetime = 59 | DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)), $timezone) 60 | ->setTimezone($timezone) 61 | ->format(DateTime::ISO8601) 62 | ; 63 | 64 | $logger->setTimezone($timezone); 65 | $logger->info('This is a test!'); 66 | 67 | sleep(1); 68 | 69 | $results = $client->search([ 70 | 'index' => ElasticsearchHandler::DEFAULT_INDEX_NAME, 71 | 'type' => ElasticsearchHandler::DEFAULT_TYPE_NAME, 72 | 'body' => [ 73 | 'query' => [ 74 | 'match_all' => new \stdClass() 75 | ] 76 | ] 77 | ]); 78 | 79 | $this->assertGreaterThan(0, $results['hits']['total']); 80 | 81 | $expected = [ 82 | 'message' => 'This is a test!', 83 | 'context' => [], 84 | 'level' => 200, 85 | 'level_name' => 'INFO', 86 | 'channel' => 'application', 87 | 'datetime' => $datetime, 88 | 'extra' => [] 89 | ]; 90 | 91 | $this->assertEquals( 92 | $expected, 93 | $results['hits']['hits'][0]['_source'] 94 | ); 95 | } 96 | } --------------------------------------------------------------------------------