Peter Davies

Ramblings

A Logstash grok filter for RFC 5424 syslog

The syslog filter most Logstash tutorials give you, mine included, expects the old BSD format: Jan 5 10:01:02 host program[123]: message. Applications on Cloud Foundry send their logs to a syslog drain in the newer RFC 5424 format instead, and that filter matches none of it. Everything lands in Kibana as one long message field tagged _grokparsefailure.

This is the filter I ended up with on Logstash 7. It is written for Cloud Foundry, but the first half works for any RFC 5424 source.

What an RFC 5424 line looks like

<14>1 2022-01-18T10:15:02.123456+00:00 my-org.my-space.my-app <app-guid> [APP/PROC/WEB/0] - [tags@47450 app_name="my-app" space_name="my-space"] {"level":"info","msg":"started"}

In order, separated by single spaces:

Part Example Meaning
PRI <14> facility and severity packed into one number
VERSION 1 always 1
TIMESTAMP ISO 8601 or - if unknown
HOSTNAME my-org.my-space.my-app Cloud Foundry puts org, space and app here
APP-NAME a GUID the app's GUID on Cloud Foundry
PROCID [APP/PROC/WEB/0] which component: app instance, router (RTR), staging and so on
MSGID - usually empty
STRUCTURED-DATA [tags@47450 ...] key="value" pairs in brackets, or -
MSG the rest the log line itself

Over TCP the sender may also put the message length in front of the whole thing (RFC 6587 octet counting), so a line can start with something like 187 <14>1 ....

Input

Use a plain tcp input, not the syslog input. The syslog input only understands the old format and will tag every one of these as a failure.

input {
  tcp {
    port => 5514
    type => "cf"
  }
}

The filter

filter {
  grok {
    match => {
      "message" => "^(?:%{NONNEGINT:syslog_length} )?<%{NONNEGINT:syslog_pri}>%{NONNEGINT:syslog_version} +(?:%{TIMESTAMP_ISO8601:syslog_timestamp}|-) +%{NOTSPACE:syslog_host} +%{NOTSPACE:syslog_app} +%{NOTSPACE:syslog_procid} +%{NOTSPACE:syslog_msgid} +(?:-|\[%{NOTSPACE:syslog_sd_id} %{DATA:syslog_sd}\]) ?%{GREEDYDATA:syslog_msg}"
    }
    add_field => { "received_at" => "%{@timestamp}" }
    tag_on_failure => ["_syslog5424_failure"]
  }

  if !("_syslog5424_failure" in [tags]) {
    syslog_pri {
      syslog_pri_field_name => "syslog_pri"
    }

    date {
      match => ["syslog_timestamp", "ISO8601"]
    }

    if [syslog_sd] {
      kv {
        source => "syslog_sd"
        target => "sd"
        field_split => " "
        value_split => "="
        trim_value => '"'
      }
    }

    mutate {
      rename => { "syslog_msg" => "message" }
      remove_field => ["syslog_length", "syslog_sd"]
    }
  }
}

What each part does:

Logstash has a built-in SYSLOG5424LINE pattern. I wrote my own because the built-in one does not allow for the length prefix, and I wanted control over the field names.

Splitting the Cloud Foundry host name

The HOSTNAME field is <org>.<space>.<app>. Breaking it up means you can filter Kibana by space (all of staging, say) without a wildcard search:

filter {
  if [syslog_host] {
    dissect {
      mapping => { "syslog_host" => "%{[cf][org]}.%{[cf][space]}.%{[cf][app]}" }
      tag_on_failure => ["_cf_host_failure"]
    }
  }
}

dissect is faster than grok for a fixed layout like this. It does assume none of the names contain a dot.

Router logs and application logs

The PROCID tells you which component wrote the line. The two I care about are the router, which writes an access log line per request, and the app itself.

filter {
  if [syslog_procid] =~ /^\[RTR/ {
    mutate { replace => { "type" => "router" } }
  }

  if [syslog_procid] =~ /^\[APP/ and [message] =~ /^\{/ {
    json {
      source => "message"
      target => "app"
      tag_on_failure => ["_app_json_failure"]
    }
  }
}

The router's access line is a combined-log-style request followed by a long run of key:"value" pairs (response time, app instance, x_forwarded_for and so on). Grok the request part with the usual HTTP patterns, then hand the tail to kv with value_split => ":". Convert response_time to a float with mutate, or Kibana cannot graph it.

For application logs, the json filter only runs when the message looks like a JSON object. Apps that log structured JSON get every key as a searchable field under app; plain text lines are left alone instead of collecting a parse-failure tag.

Testing it

Before restarting the service, check the config parses:

/usr/share/logstash/bin/logstash --path.settings /etc/logstash -t

Then paste a sample line into Kibana's Grok Debugger (under Dev Tools) with the pattern, and fix anything that does not match there rather than by restarting Logstash over and over.

Today

Logstash 8 turns on ECS compatibility by default, which renames several fields the inputs and syslog_pri create (host becomes an object, for one). Either set ecs_compatibility => disabled on the pipeline or move your field names to their ECS equivalents.