Laravel and Logstash
As we get larger clients, our need to not be cowboying our monitoring / alerting is increasing. In our scenario we are injesting logs via Logstash and sending them all to an AWS Elasticsearch instance, and if it is of severity ERROR we send it to AWS Simple Noticiation Service (which people or services can subscribe to) as well as send them to PagerDuty.
Input
For each of our services we have an input config which basically says ‘consume this file patter, call it a laravel file, and add its stack name to the event.’
<pre lang="">input {
file {
path => "/storage/logs/laravel-*.log"
start_position => "beginning"
type => "laravel"
codec => multiline {
pattern => "^\[%{TIMESTAMP_ISO8601}\] "
negate => true
what => previous
auto_flush_interval => 10
}
add_field => {"stack" => "tether"}
}
}
Filter
Since its a type laravel file, we pull out the environment its running in, and log severity, plus grab the ip of the instance, build the SNS message subject and make sure the event timestamp is the one in the log, not the time logstash touched the event. (Without that last step, you end up with > 1MM entries for a single day the first time you run things.)
<pre lang="">filter {
# Laravel log files
if [type] == "laravel" {
grok {
match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] %{DATA:env}\.%{DATA:severity}: %{GREEDYDATA:message}" }
}
ruby {
code => "event.set('ip', `ip a s eth0 | awk \'/inet / {print$2}\'`)"
}
mutate {
add_field => { "sns_subject" => "%{stack} Alert (%{env} - %{ip})" }
}
date {
match => [ "timestamp", "yyyy-MM-dd HH:mm:ss" ]
target => "@timestamp"
}
}
}
Output
And then we pump it around where it needs to be.
If you are upgrading ES from 5.x to 6.x you need to have the template_overwrite setting else the new schema doesn’t get imported and there was some important changes that were made. The scope stuff is for Puppet to do replacements. And there is a but in 6.4.0 of the amazon_es plugin around template_overwrite…
<pre lang="">output {
amazon_es {
hosts => [""]
region => "us-west-2"
index => "logstash--%{+YYYY.MM.dd}"
template => "/etc/logstash/templates/elasticsearch-template-es6x.json"
template_overwrite => true
}
}
<pre lang="">output {
if [severity] == "ERROR" {
sns {
arn => "arn:aws:sns:us-west-2:xxxxxxxxx:-errors"
region => 'us-west-2'
}
}
}
I’m not quite happy with our Pageruty setup as the de-duping is running at an instance level right now. Ideally, it would have the reason for the exception as well but that’s a task for another day.
<pre lang="">output {
if [severity] == "ERROR" {
pagerduty {
event_type => "trigger"
description => "%{stack} - %{ip}"
details => {
timestamp => "%{@timestamp}"
message => "%{message}"
}
service_key => ""
incident_key => "logstash/%{stack}/%{ip}"
}
}
}
For the really curious, here is my Puppet stuff for all this. Every machine which has Laravel services has the first manifest, but there are some environments which have multiple services on them which is why the input file lives at the service level.
modules/profiles/manifests/laravel.pp
<pre lang=""> class { 'logstash':
version => '1:6.3.2-1',
}
$es_host = hiera('elasticsearch')
logstash::configfile { 'filter_laravel':
template => 'logstash/filter_laravel.erb'
}
logstash::configfile { 'output_es':
template => 'logstash/output_es_cluster.erb'
}
if $environment == 'sales' or $environment == 'production' {
logstash::configfile { 'output_sns':
template => 'logstash/output_sns.erb'
}
$pagerduty = lookup('pagerduty')
logstash::configfile { 'output_pagerduty':
template => 'logstash/output_pagerduty.erb'
}
}
unless $environment == 'development' {
file { [ '/etc/logstash/templates' ]:
ensure => 'directory',
group => 'root',
owner => 'root',
mode => 'u=rwx,go+rx'
}
file { [ '/etc/logstash/templates/elasticsearch-template-es6x.json' ]:
ensure => 'present',
group => 'root',
owner => 'root',
mode => 'u=rwx,go+rx',
source => 'puppet:///modules/logstash/elasticsearch-template-es6x.json',
require => Class['Logstash']
}
logstash::plugin { 'logstash-output-amazon_es':
source => 'puppet:///modules/logstash/logstash-output-amazon_es-6.4.1-java.gem',
ensure => '6.4.1'
}
}
modules/profiles/manifests/
<pre lang=""> logstash::configfile { 'input_tether':
template => 'logstash/input_tether.erb'
}
The next thing I need to work on is consuming the ES data back into our app so we don’t have to log into Kibana or the individual machines to see the log information. I think every view-your-logs solution I’ve seen for Laravel has been based around reading the actual logs on disk which doesn’t work in a clustered environment or where you have multiple services controlled by a hub one.