Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logstash formatter #124

Merged
merged 8 commits into from Dec 14, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.mdown
Expand Up @@ -147,6 +147,7 @@ Formatters
- _WildfireFormatter_: Used to format log records into the Wildfire/FirePHP protocol, only useful for the FirePHPHandler.
- _ChromePHPFormatter_: Used to format log records into the ChromePHP format, only useful for the ChromePHPHandler.
- _GelfFormatter_: Used to format log records into Gelf message instances, only useful for the GelfHandler.
- _LogstashEventFormatter_: Used to format log records into [logstash](http://logstash.net/) event json, useful for any handler listed under inputs [here](http://logstash.net/docs/1.1.1/).

Processors
----------
Expand Down
93 changes: 93 additions & 0 deletions src/Monolog/Formatter/LogstashFormatter.php
@@ -0,0 +1,93 @@
<?php

/*
* This file is part of the Monolog package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Monolog\Formatter;

use Monolog\Logger;

/**
* Serializes a log message to Logstash Event Format
* @see http://logstash.net/
* @see https://github.com/logstash/logstash/blob/master/lib/logstash/event.rb
*
* @author Tim Mower <timothy.mower@gmail.com>
*/
class LogstashFormatter extends NormalizerFormatter
{
/**
* @var string the name of the system for the Logstash log message, used to fill the @source field
*/
protected $systemName;

/**
* @var string an application name for the Logstash log message, used to fill the @type field
*/
protected $applicationName;

/**
* @var string a prefix for 'extra' fields from the Monolog record (optional)
*/
protected $extraPrefix;

/**
* @var string a prefix for 'context' fields from the Monolog record (optional)
*/
protected $contextPrefix;


public function __construct($systemName = null, $applicationName = null, $extraPrefix = null, $contextPrefix = 'ctxt_')
{
//log stash requires a ISO 8601 format date
parent::__construct('c');

$this->systemName = $systemName ?: gethostname();
$this->applicationName = $applicationName;

$this->extraPrefix = $extraPrefix;
$this->contextPrefix = $contextPrefix;
}

/**
* {@inheritdoc}
*/
public function format(array $record)
{
$record = parent::format($record);
$message = array(
'@timestamp' => $record['datetime'],
'@message' => $record['message'],
'@tags' => array($record['channel']),
'@source' => $this->systemName
);

if (isset($this->applicationName)) {
$message['@type'] = $this->applicationName;
}
$message['@fields'] = array();
$message['@fields']['channel'] = $record['channel'];
$message['@fields']['level'] = $record['level'];
if (isset($record['extra']['server'])) {
$message['@source_host'] = $record['extra']['server'];
}
if (isset($record['extra']['url'])) {
$message['@source_path'] = $record['extra']['url'];
}
foreach ($record['extra'] as $key => $val) {
$message['@fields'][$this->extraPrefix . $key] = $val;
}

foreach ($record['context'] as $key => $val) {
$message['@fields'][$this->contextPrefix . $key] = $val;
}

return json_encode($message);
}
}
161 changes: 161 additions & 0 deletions tests/Monolog/Formatter/LogstashFormatterTest.php
@@ -0,0 +1,161 @@
<?php

/*
* This file is part of the Monolog package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Monolog\Formatter;

use Monolog\Logger;
use Monolog\Formatter\LogstashFormatter;

class LogstashFormatterTest extends \PHPUnit_Framework_TestCase
{

/**
* @covers Monolog\Formatter\LogstashFormatter::format
*/
public function testDefaultFormatter()
{
$formatter = new LogstashFormatter('test');
$record = array(
'level' => Logger::ERROR,
'level_name' => 'ERROR',
'channel' => 'meh',
'context' => array(),
'datetime' => new \DateTime("@0"),
'extra' => array(),
'message' => 'log',
);

$message = json_decode($formatter->format($record), true);

$this->assertEquals("1970-01-01T00:00:00+00:00", $message['@timestamp']);
$this->assertEquals('log', $message['@message']);
$this->assertEquals('meh', $message['@fields']['channel']);
$this->assertContains('meh', $message['@tags']);
$this->assertEquals(Logger::ERROR, $message['@fields']['level']);
$this->assertEquals('test', $message['@source']);

$formatter = new LogstashFormatter('mysystem');

$message = json_decode($formatter->format($record), true);

$this->assertEquals('mysystem', $message['@source']);
}

/**
* @covers Monolog\Formatter\LogstashFormatter::format
*/
public function testFormatWithFileAndLine()
{
$formatter = new LogstashFormatter('test');
$record = array(
'level' => Logger::ERROR,
'level_name' => 'ERROR',
'channel' => 'meh',
'context' => array('from' => 'logger'),
'datetime' => new \DateTime("@0"),
'extra' => array('file' => 'test', 'line' => 14),
'message' => 'log',
);

$message = json_decode($formatter->format($record), true);

$this->assertEquals('test', $message['@fields']['file']);
$this->assertEquals(14, $message['@fields']['line']);
}

/**
* @covers Monolog\Formatter\LogstashFormatter::format
*/
public function testFormatWithContext()
{
$formatter = new LogstashFormatter('test');
$record = array(
'level' => Logger::ERROR,
'level_name' => 'ERROR',
'channel' => 'meh',
'context' => array('from' => 'logger'),
'datetime' => new \DateTime("@0"),
'extra' => array('key' => 'pair'),
'message' => 'log'
);

$message = json_decode($formatter->format($record), true);


$message_array = $message['@fields'];

$this->assertArrayHasKey('ctxt_from', $message_array);
$this->assertEquals('logger', $message_array['ctxt_from']);

// Test with extraPrefix
$formatter = new LogstashFormatter('test', null, null, 'CTX');
$message = json_decode($formatter->format($record), true);


$message_array = $message['@fields'];

$this->assertArrayHasKey('CTXfrom', $message_array);
$this->assertEquals('logger', $message_array['CTXfrom']);

}

/**
* @covers Monolog\Formatter\LogstashFormatter::format
*/
public function testFormatWithExtra()
{
$formatter = new LogstashFormatter('test');
$record = array(
'level' => Logger::ERROR,
'level_name' => 'ERROR',
'channel' => 'meh',
'context' => array('from' => 'logger'),
'datetime' => new \DateTime("@0"),
'extra' => array('key' => 'pair'),
'message' => 'log'
);

$message = json_decode($formatter->format($record), true);

$message_array = $message['@fields'];

$this->assertArrayHasKey('key', $message_array);
$this->assertEquals('pair', $message_array['key']);

// Test with extraPrefix
$formatter = new LogstashFormatter('test', null, 'EXT');
$message = json_decode($formatter->format($record), true);

$message_array = $message['@fields'];

$this->assertArrayHasKey('EXTkey', $message_array);
$this->assertEquals('pair', $message_array['EXTkey']);
}

public function testFormatWithApplicationName()
{
$formatter = new LogstashFormatter('test', 'app');
$record = array(
'level' => Logger::ERROR,
'level_name' => 'ERROR',
'channel' => 'meh',
'context' => array('from' => 'logger'),
'datetime' => new \DateTime("@0"),
'extra' => array('key' => 'pair'),
'message' => 'log'
);

$message = json_decode($formatter->format($record), true);

$this->assertArrayHasKey('@type', $message);
$this->assertEquals('app', $message['@type']);
}
}