Logstash: Comprehensive Reference Guide¶
1. Architecture¶
Logstash pipelines have three stages, all defined in a config file:
input { } # where events come from
filter { } # transform/enrich events
output { } # where events go
Events flow through in-memory queues between stages by default. For durability across restarts/crashes, enable the persistent queue (queue.type: persisted in logstash.yml) — without it, in-flight events are lost on a crash.
Key daemons/files:
| File | Purpose |
|---|---|
/etc/logstash/logstash.yml |
node-wide settings (queueing, heap-adjacent settings, monitoring) |
/etc/logstash/pipelines.yml |
defines multiple named pipelines and which config dir each uses |
/etc/logstash/conf.d/*.conf |
pipeline config fragments (Logstash concatenates all files in a dir alphabetically) |
/etc/logstash/jvm.options |
heap size, GC settings |
Because files in conf.d/ are concatenated, naming convention matters — hence the 10-, 11- prefixes in your notes (input configs low, filters middle, output high, so ordering is predictable regardless of alphabetical surprises).
2. Multiple Pipelines¶
Instead of one giant conf.d/, pipelines.yml lets you isolate workloads (e.g., separate DLQ-heavy pipeline from high-volume nginx access logs) so a slow filter in one doesn't back-pressure the other:
- pipeline.id: nginx
path.config: "/etc/logstash/conf.d/nginx/*.conf"
pipeline.workers: 4
- pipeline.id: dlq-reprocess
path.config: "/etc/logstash/conf.d/dlq/*.conf"
pipeline.workers: 2
3. Inputs¶
Common ones you'll actually use:
input {
beats {
port => 5044
}
http {
port => 8080
}
kafka {
bootstrap_servers => "kafka:9092"
topics => ["app-logs"]
}
}
Filebeat note (your nginx section is outdated): prospectors: was deprecated in Filebeat 6.x and removed in 7.x. Modern config uses filebeat.inputs::
filebeat.inputs:
- type: filestream
id: nginx-access
paths:
- /var/log/nginx/access.log
fields:
log_type: nginx-access
fields_under_root: true
document_type is also gone (removed in 6.0+, all docs go to _doc). Use fields + a Logstash conditional on that field instead, as above.
4. Grok Patterns¶
Custom pattern files (your approach is correct and still standard):
mkdir -p /etc/logstash/patterns
chown logstash: /etc/logstash/patterns/nginx
# /etc/logstash/patterns/nginx
NGUSERNAME [a-zA-Z\.\@\-\+_%]+
NGUSER %{NGUSERNAME}
NGINXACCESS %{IPORHOST:clientip} %{NGUSER:ident} %{NGUSER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{URIPATHPARAM:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response} (?:%{NUMBER:bytes}|-) (?:"(?:%{URI:referrer}|-)"|%{QS:referrer}) %{QS:agent}
filter {
if [log_type] == "nginx-access" {
grok {
patterns_dir => ["/etc/logstash/patterns"]
match => { "message" => "%{NGINXACCESS}" }
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
target => "@timestamp"
remove_field => ["timestamp"]
}
useragent {
source => "agent"
target => "user_agent"
}
geoip {
source => "clientip"
}
mutate {
convert => { "response" => "integer", "bytes" => "integer" }
}
}
}
Notes on additions vs. your original:
- date filter — without it, @timestamp is when Logstash processed the event, not when nginx logged it. For anything ILM-driven or used in incident timelines, this matters a lot.
- useragent/geoip — cheap enrichment, commonly wanted on access logs; drop if you don't need it (geoip in particular adds CPU cost per event).
- mutate { convert } — grok fields are strings by default; response/bytes should be numeric for aggregations/visualizations in Kibana.
- Test grok patterns against sample lines with the Grok Debugger or bin/logstash -e before deploying — a bad pattern silently tags events _grokparsefailure instead of erroring loudly.
5. Ruby Filter — Your DLQ Snippet, Cleaned Up¶
Your working version:
if "dlq" in [tags] {
ruby {
id => "prefix_dlq_message"
code => '
event.to_hash.each { |k,v|
event.set("[error][#{k}]",v)
event.remove(k)
}'
}
}
The real risk here (as you'd already noted from your ChatGPT sessions, correctly) is mutating event.to_hash while iterating it — to_hash in the JRuby event API returns a snapshot, so in practice this doesn't infinite-loop, but it's fragile and will silently misbehave if the internal implementation changes. Snapshot the keys first and exclude metadata fields so you don't nest @timestamp/tags/@version under [error] (which breaks index templates expecting @timestamp at the root):
if "dlq" in [tags] {
ruby {
id => "prefix_dlq_message"
code => '
preserve = ["@timestamp", "@version", "tags", "@metadata"]
event.to_hash.keys.each { |k|
next if preserve.include?(k)
event.set("[error][#{k}]", event.get(k))
event.remove(k)
}'
}
}
Better alternative — skip Ruby entirely. If the goal is just "namespace the whole payload under error," a prune + mutate { rename } chain is more idiomatic Logstash and doesn't run arbitrary code per-event:
if "dlq" in [tags] {
mutate {
rename => { "message" => "[error][message]" }
}
}
— only reach for ruby {} when you genuinely need dynamic/programmatic field manipulation that the built-in filters can't express. Ruby filters are also the easiest way to accidentally tank pipeline throughput, since the code runs per event with no batching.
Also worth knowing: Logstash has a built-in Dead Letter Queue feature (dead_letter_queue.enable: true in logstash.yml), separate from your manual "dlq" in [tags] convention. The built-in DLQ captures events that fail at the output stage (e.g., Elasticsearch mapping conflicts) and lets you reprocess them via a dead_letter_queue input later. Your tag-based approach looks like it's for filter-stage error handling instead, which the built-in DLQ doesn't cover — so keeping both patterns is reasonable, just worth being explicit in comments about which failure mode each one is for.
6. Common Filters Cheat Sheet¶
filter {
mutate {
rename => { "old_field" => "new_field" }
remove_field => ["unwanted"]
lowercase => ["some_field"]
}
date {
match => ["timestamp", "ISO8601"]
}
kv {
source => "message"
field_split => " "
}
fingerprint {
source => ["clientip", "request"]
target => "[@metadata][fingerprint]"
method => "SHA1"
}
}
7. Conditionals¶
if [type] == "nginx-access" and [response] >= 500 {
mutate { add_tag => ["nginx_5xx"] }
} else if "dlq" in [tags] {
# ...
}
[@metadata] fields are useful for values you need mid-pipeline (like a dynamic index name) but don't want indexed into Elasticsearch — they're dropped automatically before output.
8. Output¶
output {
if "nginx_5xx" in [tags] {
elasticsearch {
hosts => ["https://es-node:9200"]
index => "nginx-errors-%{+YYYY.MM.dd}"
ilm_enabled => true
ilm_rollover_alias => "nginx-errors"
ilm_policy => "nginx-errors-policy"
}
} else {
elasticsearch {
hosts => ["https://es-node:9200"]
index => "logs-generic-%{+YYYY.MM.dd}"
}
}
}
Given the ILM naming work you've been doing, worth standardizing the index => naming in these output blocks to match your retention-duration convention up front rather than backfilling aliases later.
9. Performance Tuning¶
pipeline.workers— default is CPU core count; raise for CPU-bound filters (grok, geoip), not for I/O-bound ones.pipeline.batch.size/pipeline.batch.delay— larger batches improve ES bulk throughput at the cost of latency.- Order filters cheapest-to-most-expensive, and put conditionals around expensive filters (geoip, ruby, dissect on large strings) so they only run on the events that need them.
grokis regex-based and slow relative todissect— if a log format is fixed-delimiter (not free text), usedissectfirst andgrokonly for the free-text remainder.- Persistent queue trades throughput for durability — size
queue.max_bytesbased on how much backlog you're willing to buffer during an ES outage.
10. Monitoring & Troubleshooting¶
GET _cat/pluginsand the Logstash monitoring API (http://localhost:9600/_node/stats) for pipeline health, events-per-second, and queue depth._grokparsefailure/_dateparsefailuretags — grep for these in Kibana to find silently-dropped enrichment.bin/logstash -f pipeline.conf --config.test_and_exit— validate config syntax before reload.bin/logstash -f pipeline.conf --config.reload.automatic— hot-reload during development.
11. Reference¶
- Logstash filter plugin index
- Grok Debugger
- Your original nginx/DLQ notes were pulled from a DigitalOcean tutorial and ChatGPT sessions — both technically sound as a starting point; the changes above mainly close gaps around timestamp accuracy, field typing, and the deprecated Filebeat
prospectorssyntax.