ELK Stack β€” Dissect Filter

4 minute read

What Is Dissect?

Dissect is a delimiter-based log parser that splits a string into fields using a fixed pattern of delimiters. Unlike Grok (which uses regular expressions), Dissect is a tokenizer β€” it simply splits on exact character sequences.

This makes Dissect:

  • ⚑ 5–10x faster than Grok
  • πŸ“– Easier to read β€” no regex to decode
  • βœ… Predictable β€” no regex backtracking issues
  • ⚠️ Less flexible β€” only works when the log format has consistent delimiters

Dissect is available in:

  • Logstash β€” dissect filter plugin
  • Elasticsearch β€” dissect ingest processor

Dissect Syntax

The basic dissect pattern uses %{field_name} to define fields, separated by literal delimiter characters:

%{field1} %{field2} %{field3}

The characters between %{...} tokens are treated as literal delimiters that must appear exactly in the log.

Example:

Log: 192.168.1.50 GET /api/users 200

Pattern: %{ip} %{method} %{path} %{status}

Result: ip=192.168.1.50, method=GET, path=/api/users, status=200


Dissect Modifiers

Dissect provides modifiers to handle more complex parsing scenarios:

Modifier Syntax Description
Skip %{?skip_field} Match but discard the value
Append %{+field} Append to an existing field
Append with order %{+field/N} Append in a specific order (N = position)
Indirect %{&field} Use the value of another field as the field name
Right padding %{field->} Consume trailing whitespace/padding

Skip Modifier β€” %{?name}

Discard fields you don’t need:

Raw log:

2026-04-01 10:15:30 INFO web01 GET /api/users 200

Dissect pattern:

%{timestamp} %{+timestamp} %{level} %{?host} %{method} %{path} %{status}

The %{?host} matches the hostname but discards it.

Append Modifier β€” %{+name}

Join multiple tokens into a single field:

Raw log:

2026-04-01 10:15:30 INFO Request processed

Dissect pattern:

%{timestamp} %{+timestamp} %{level} %{message}

Parsed output:

{
  "timestamp": "2026-04-01 10:15:30",
  "level": "INFO",
  "message": "Request processed"
}

The %{timestamp} and %{+timestamp} values are joined with a space to form "2026-04-01 10:15:30".

Ordered Append β€” %{+name/N}

Control the order of appended values:

Raw log:

30 2026-04-01 10:15 INFO

Dissect pattern:

%{+timestamp/3} %{+timestamp/1} %{+timestamp/2} %{level}

Parsed output:

{
  "timestamp": "2026-04-01 10:15 30",
  "level": "INFO"
}

Tokens are joined in order: /1 β†’ /2 β†’ /3.

Indirect Modifier β€” %{&name}

Use a captured value as a field name:

Raw log:

env=production region=us-east-1 status=healthy

Dissect pattern:

%{?key1}=%{&key1} %{?key2}=%{&key2} %{?key3}=%{&key3}

Parsed output:

{
  "env": "production",
  "region": "us-east-1",
  "status": "healthy"
}

The %{?key1} captures env (and discards it as a value), then %{&key1} uses env as the field name for production.

Right Padding β€” %{field->}

Consume extra whitespace or repeated delimiters:

Raw log (variable spacing):

192.168.1.50     GET   /api/users   200

Dissect pattern:

%{ip->} %{method->} %{path->} %{status}

The -> modifier consumes all trailing whitespace after each field until the next non-whitespace delimiter is found.


Using Dissect in Logstash

Basic Dissect Filter

filter {
  dissect {
    mapping => {
      "message" => "%{timestamp} %{+timestamp} %{level} %{logger} - %{log_message}"
    }
  }
}

With Convert Datatype

filter {
  dissect {
    mapping => {
      "message" => "%{ip} - %{user} [%{timestamp}] \"%{method} %{path} HTTP/%{http_version}\" %{status} %{bytes}"
    }
    convert_datatype => {
      "status" => "int"
      "bytes" => "int"
    }
  }
}

Raw log:

192.168.1.50 - admin [01/Apr/2026:10:15:30 +0000] "GET /api/users HTTP/1.1" 200 1234

Parsed output:

{
  "ip": "192.168.1.50",
  "user": "admin",
  "timestamp": "01/Apr/2026:10:15:30 +0000",
  "method": "GET",
  "path": "/api/users",
  "http_version": "1.1",
  "status": 200,
  "bytes": 1234
}

Using Dissect in Elasticsearch Ingest Pipeline

PUT _ingest/pipeline/dissect-access-log
{
  "description": "Parse access logs with dissect",
  "processors": [
    {
      "dissect": {
        "field": "message",
        "pattern": "%{ip} - %{user} [%{timestamp}] \"%{method} %{path} HTTP/%{http_version}\" %{status} %{bytes}"
      }
    },
    {
      "convert": {
        "field": "status",
        "type": "integer"
      }
    },
    {
      "convert": {
        "field": "bytes",
        "type": "integer"
      }
    },
    {
      "date": {
        "field": "timestamp",
        "formats": ["dd/MMM/yyyy:HH:mm:ss Z"],
        "target_field": "@timestamp"
      }
    }
  ]
}

Dissect vs Grok β€” Performance Comparison

Aspect Dissect Grok
Parsing method Delimiter-based tokenizer Regular expressions
Speed ⚑ 5–10x faster 🐒 Slower (regex engine)
CPU usage Low Higher (backtracking)
Flexibility Fixed delimiters only Any regex pattern
Variable whitespace Needs -> modifier Handled by \s+
Optional fields Not supported Supported with (?:...)?
Pattern reuse Not composable Composable patterns
Failure mode Hard fail if format doesn’t match Partial matches possible

When to Use Dissect

  • βœ… Log lines have consistent delimiters (spaces, tabs, pipes, brackets)
  • βœ… High throughput β€” parsing millions of events per second
  • βœ… Format is predictable β€” no optional fields or variable formats
  • βœ… You want simpler patterns that are easy to maintain

When to Use Grok

  • βœ… Logs have variable formats or optional fields
  • βœ… You need regex matching (e.g., email addresses, UUIDs)
  • βœ… Format is semi-structured with no consistent delimiters
  • βœ… You need to reuse patterns across multiple configs

Combining Dissect + Grok

For complex logs, use dissect first (fast) to extract the major fields, then grok (slower) only on the parts that need regex:

filter {
  # Step 1: Fast dissect for the structured prefix
  dissect {
    mapping => {
      "message" => "[%{timestamp}] [%{level}] [%{service}] %{detail}"
    }
  }

  # Step 2: Grok only on the unstructured detail field
  grok {
    match => {
      "detail" => "%{WORD:method} %{URIPATH:path} %{NUMBER:status:int} %{NUMBER:duration:int}ms(?: error=%{GREEDYDATA:error})?"
    }
    tag_on_failure => ["_grok_detail_fail"]
  }
}

Raw log:

[2026-04-01T10:15:30.456Z] [ERROR] [PaymentService] POST /api/payments 500 2345ms error=InsufficientFunds: balance=0.00

Parsed output:

{
  "timestamp": "2026-04-01T10:15:30.456Z",
  "level": "ERROR",
  "service": "PaymentService",
  "method": "POST",
  "path": "/api/payments",
  "status": 500,
  "duration": 2345,
  "error": "InsufficientFunds: balance=0.00"
}

This hybrid approach gives you the speed of dissect for the structured parts and the flexibility of grok for the variable parts.


Quick Reference

# Dissect syntax
%{field}              # Capture a field
%{?skip}              # Match and discard
%{+append}            # Append to existing field
%{+append/2}          # Append with order
%{&indirect}          # Use value as field name
%{field->}            # Consume right padding

# Logstash dissect filter
filter {
  dissect {
    mapping => {
      "message" => "%{ip} %{method} %{path} %{status}"
    }
    convert_datatype => {
      "status" => "int"
    }
  }
}

# ES ingest dissect processor
{
  "dissect": {
    "field": "message",
    "pattern": "%{ip} %{method} %{path} %{status}"
  }
}

# Combining dissect + grok
dissect β†’ extract major fields (fast)
grok   β†’ parse remaining detail (flexible)

Previous: Custom Grok Patterns ← Β· Next: Multiline Logs β†’