├── .gitignore ├── composer.json ├── Readme.md └── src └── LogstashFormatter.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happyr/monolog-logstash-formatter", 3 | "type": "library", 4 | "description": "Monolog Logstash formatter", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Tobias Nyholm", 9 | "email": "tobias@happyr.com" 10 | } 11 | ], 12 | "repositories": [ 13 | { "type": "composer", "url": "https://packages.happyr.io/" } 14 | ], 15 | "require": { 16 | "php": ">=8.1", 17 | "monolog/monolog": "^3.2" 18 | }, 19 | "autoload": { 20 | "psr-4": { "Happyr\\MonologLogstashFormatter\\": "src/" } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Logstash formatter for Monolog v3 2 | 3 | 4 | ```yaml 5 | # config/prod/monolog.yaml 6 | monolog: 7 | handlers: 8 | filter_for_errors: 9 | type: fingers_crossed 10 | action_level: warning 11 | handler: cloudwatch 12 | buffer_size: 100 13 | excluded_http_codes: [] 14 | 15 | cloudwatch: 16 | type: stream 17 | path: 'php://stderr' 18 | formatter: 'app.monolog.formatter.logstash' 19 | level: info 20 | 21 | services: 22 | app.monolog.formatter.logstash: 23 | class: Happyr\MonologLogstashFormatter\LogstashFormatter 24 | arguments: 25 | - 'app.example.com' 26 | 27 | monolog.processor.uid: 28 | class: Monolog\Processor\UidProcessor 29 | autoconfigure: true 30 | tags: 31 | - { name: monolog.processor, handler: cloudwatch } 32 | ``` 33 | -------------------------------------------------------------------------------- /src/LogstashFormatter.php: -------------------------------------------------------------------------------- 1 | 12 | * @author Tobias Nyholm 13 | */ 14 | final class LogstashFormatter extends NormalizerFormatter 15 | { 16 | /** 17 | * @var string the name of the system for the Logstash log message, used to fill the @source field 18 | */ 19 | private $systemName; 20 | 21 | /** 22 | * @var string an application name for the Logstash log message, used to fill the @type field 23 | */ 24 | private $applicationName; 25 | 26 | /** 27 | * @var string the key for 'extra' fields from the Monolog record 28 | */ 29 | private $extraKey; 30 | 31 | /** 32 | * @var string the key for 'context' fields from the Monolog record 33 | */ 34 | private $contextKey; 35 | 36 | /** 37 | * @param string $applicationName The application that sends the data, used as the "type" field of logstash 38 | * @param string|null $systemName The system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine 39 | * @param string $extraKey The key for extra keys inside logstash "fields", defaults to extra 40 | * @param string $contextKey The key for context keys inside logstash "fields", defaults to context 41 | */ 42 | public function __construct(string $applicationName, ?string $systemName = null, string $extraKey = 'extra', string $contextKey = 'context') 43 | { 44 | // logstash requires a ISO 8601 format date with optional millisecond precision. 45 | parent::__construct('Y-m-d\TH:i:s.uP'); 46 | 47 | $this->systemName = null === $systemName ? \gethostname() : $systemName; 48 | $this->applicationName = $applicationName; 49 | $this->extraKey = $extraKey; 50 | $this->contextKey = $contextKey; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function format(LogRecord $record): string 57 | { 58 | $record = parent::format($record); 59 | 60 | if (empty($record['datetime'])) { 61 | $record['datetime'] = \gmdate('c'); 62 | } 63 | $message = [ 64 | '@timestamp' => $record['datetime'], 65 | '@version' => 1, 66 | 'host' => $this->systemName, 67 | ]; 68 | if (isset($record['message'])) { 69 | $message['message'] = $record['message']; 70 | } 71 | if (isset($record['channel'])) { 72 | $message['type'] = $record['channel']; 73 | $message['channel'] = $record['channel']; 74 | } 75 | if (isset($record['level_name'])) { 76 | $message['level'] = $record['level_name']; 77 | } 78 | if (isset($record['level'])) { 79 | $message['monolog_level'] = $record['level']; 80 | } 81 | if ($this->applicationName) { 82 | $message['type'] = $this->applicationName; 83 | } 84 | if (!empty($record['extra'])) { 85 | $message[$this->extraKey] = $record['extra']; 86 | } 87 | if (!empty($record['context'])) { 88 | $message[$this->contextKey] = $record['context']; 89 | } 90 | 91 | // TO support Funcitonbeat 8.x 92 | if (isset($message[$this->contextKey]['message']) && is_array($message[$this->contextKey]['message'])) { 93 | $message[$this->contextKey]['message'] = json_encode($message[$this->contextKey]['message']); 94 | } 95 | 96 | $json = $this->toJson($message); 97 | 98 | // 8000 is the limit by PHP-FPM 99 | if (\mb_strlen($json) > 8000 && isset($message['context']['exception']['previous'])) { 100 | $message['context']['exception']['previous'] = ['removed' => 'Message too long']; 101 | $json = $this->toJson($message); 102 | } 103 | 104 | // 8000 is the limit by PHP-FPM 105 | if (\mb_strlen($json) > 8000 && isset($message['context']['exception'])) { 106 | if ($message['context']['exception'] instanceof \Throwable) { 107 | $message['context']['exception_class'] = get_class($message['context']['exception']); 108 | } 109 | unset($message['context']['exception']); 110 | $json = $this->toJson($message); 111 | } 112 | 113 | return $json."\n"; 114 | } 115 | } 116 | --------------------------------------------------------------------------------