logstash icon indicating copy to clipboard operation
logstash copied to clipboard

Implements safe evaluation of conditional expressions, logging the error without killing the pipeline

Open andsel opened this issue 1 year ago • 5 comments

Release notes

Fix if statement expression evaluation against runtime errors that could crash the pipeline, just logging the offending event and continuing with the next in the batch.

What does this PR do?

Translates the org.jruby.exceptions.TypeError, IllegalArgumentException, org.jruby.exceptions.ArgumentError that could happen during EventCodition evaluation into a custom ConditionalEvaluationError which bubbles up on AST tree nodes. It's catched in the SplitDataset node. Updates the generation of the SplitDataset so that the execution of filterEvents method inside the compute body is try-catch guarded and defer the execution to an instance of AbstractPipelineExt.ConditionalEvaluationListener to handle such error. In this particular case the error management consist in just logging the offending Event.

Why is it important/What is the impact to the user?

This PR protects the if statements against expression evaluation errors, cancel the event under processing and log it. This avoids to crash the pipeline which encounter a runtime error during event condition evaluation, permitting to debug the root cause reporting the offending event and removing from the current processing batch.

Checklist

  • [x] My code follows the style guidelines of this project
  • [x] I have commented my code, particularly in hard-to-understand areas
  • [x] I have made corresponding changes to the documentation
  • ~~[ ] I have made corresponding change to the default configuration files (and/or docker env variables)~~
  • [x] I have added tests that prove my fix is effective or that my feature works

Author's Checklist

  • [x] add test case for #15091

How to test this PR locally

  1. start with small pipeline like
input {
    stdin { codec => json_lines }
}
filter {
  if [path][to][value] > 100 {
    mutate { add_tag => "hit" }
  }
}
output {
  stdout { 
    codec => rubydebug
  }
}

and use the data samples from #16007 2. tinker with ifs also in the output, something like:

input {
    stdin { codec => json_lines }
}

filter {
  if [path][to][value] > 100 {
    mutate { add_tag => "hit" }
  } else {
    mutate { add_tag => "miss" }
  }
  mutate { add_tag => "after"}
}

output {
   if [path][to][value] > 100 {
        stdout {
            codec => rubydebug
        }
   }
}
  1. then verify with batches containing ok and ko data. Add the following input for example:
file {
    path => "/tmp/pipeline_conditional_test_fixture.json"
    sincedb_path => "/tmp/logstash_andsel/sincedb"
    mode => "read"
    file_completed_action => "log"
    file_completed_log_path => "/tmp/processed.log"

    codec => json
}

Using a json file like:

{"path":{"to":{"value":101}}}
{"path":{"to":{"value":102}}}
{"path":{"to":{"value":"101"}}}
{"path":{"to":{"value":103}}}
  1. whatever other test comes to your mind :-)

Related issues

  • Relates #16007
  • Closes #15091

Use cases

Screenshots

Logs

andsel avatar Jul 12 '24 09:07 andsel

I believe we're in a good state in regards to catching the error situations but I'd like to improve the wording and content of the errors presented to the user, so there's less "noise". Currently with this PR we show:

[2024-08-14T17:10:47,532][WARN ][org.logstash.execution.AbstractPipelineExt][main] Error in condition evaluation with error org.logstash.config.ir.compiler.EventCondition$Compiler$UnexpectedTypeException: Unexpected input type combination left <no-class>:<null-value>, right class org.jruby.RubyFixnum:100

But if we do:

diff --git a/logstash-core/src/main/java/org/logstash/config/ir/compiler/EventCondition.java b/logstash-core/src/main/java/org/logstash/config/ir/compiler/EventCondition.java
index ab3c867f4..8d5493c9f 100644
--- a/logstash-core/src/main/java/org/logstash/config/ir/compiler/EventCondition.java
+++ b/logstash-core/src/main/java/org/logstash/config/ir/compiler/EventCondition.java
@@ -685,19 +685,19 @@ public interface EventCondition {
             UnexpectedTypeException(final Expression left, final Expression right) {
                 super(
                         String.format(
-                                "Unexpected input types left: %s, right: %s", getUnexpectedTypeDetails(left), getUnexpectedTypeDetails(right)
+                                "Unexpected conditional input combination of \'%s\' (left) and \'%s\' (right)", getUnexpectedTypeDetails(left), getUnexpectedTypeDetails(right)
                         )
                 );
             }
 
             UnexpectedTypeException(final Object inner) {
-                super(String.format("Unexpected input type %s", getUnexpectedTypeDetails(inner)));
+                super(String.format("Unexpected conditional input type \'%s\'", getUnexpectedTypeDetails(inner)));
             }
 
             UnexpectedTypeException(final Object left, final Object right) {
                 super(
                         String.format(
-                                "Unexpected input type combination left %s, right %s", getUnexpectedTypeDetails(left), getUnexpectedTypeDetails(right)
+                                "Unexpected conditional input combination of \'%s\' (left) and \'%s\' (right)", getUnexpectedTypeDetails(left), getUnexpectedTypeDetails(right)
                         )
                 );
             }
diff --git a/logstash-core/src/main/java/org/logstash/execution/AbstractPipelineExt.java b/logstash-core/src/main/java/org/logstash/execution/AbstractPipelineExt.java
index 07c324a3b..0ac584f15 100644
--- a/logstash-core/src/main/java/org/logstash/execution/AbstractPipelineExt.java
+++ b/logstash-core/src/main/java/org/logstash/execution/AbstractPipelineExt.java
@@ -179,8 +179,8 @@ public class AbstractPipelineExt extends RubyBasicObject {
     public final class LogErrorEvaluationListener implements ConditionalEvaluationListener {
         @Override
         public void notify(ConditionalEvaluationError err) {
-            lastErrorEvaluationReceived = err.getMessage();
-            LOGGER.warn("Error in condition evaluation with error {}", lastErrorEvaluationReceived);
+            lastErrorEvaluationReceived = err.getCause().getMessage();
+            LOGGER.warn("{}. Event was dropped, enable debug logging to see the event's payload.", lastErrorEvaluationReceived);
             LOGGER.debug("Event generating the fault: {}", err.failedEvent().toMap().toString());
 
             // logs the exception at debug level

Then we get:

[2024-08-14T17:06:44,651][WARN ][org.logstash.execution.AbstractPipelineExt][main] Unexpected conditional input combination of '<no-class>:<null-value>' (left) and 'class org.jruby.RubyFixnum:100' (right). Event was dropped, enable debug logging to see the event's payload.

I'm not sure if this is an over optimization for this particular kind of exception that impacts others, but I think we should aim at something similar to this.

jsvd avatar Aug 14 '24 16:08 jsvd

Finally the biggest elephant in the room is not being able to point to right conditional had the issue. Did you investigate on the effort required to add that? It's not needed for this PR but should be a follow up issue.

jsvd avatar Aug 14 '24 16:08 jsvd

Thanks @jsvd for picking up the 🐘 , follow a description of how we can do it, renouncing to little performance (I don't have any data about it, should we observe it?) in compilation.

Short explanation

The IfVertex instance which is used to compile the EventCondition contains the source's metadata that could be used by the filterEvent once the event condition fails to evaluate, but EventCondition-s are cached respect to their Ruby string representation.

Code details

EventCondition compilation is done starting from IfVertex: https://github.com/elastic/logstash/blob/d4519711a692f290c0d9e87401fb5d679f9385ff/logstash-core/src/main/java/org/logstash/config/ir/CompiledPipeline.java#L546

IfVertex exposes source metadata: https://github.com/elastic/logstash/blob/d4519711a692f290c0d9e87401fb5d679f9385ff/logstash-core/src/main/java/org/logstash/config/ir/graph/Vertex.java#L242 which contains the line https://github.com/elastic/logstash/blob/d4519711a692f290c0d9e87401fb5d679f9385ff/logstash-core/src/main/java/org/logstash/common/SourceWithMetadata.java#L52 and file source.

However when an EventCondition is compiled, the compiler uses a cache for precomputed equal conditions. https://github.com/elastic/logstash/blob/d4519711a692f290c0d9e87401fb5d679f9385ff/logstash-core/src/main/java/org/logstash/config/ir/compiler/EventCondition.java#L113

The has a form as (event.getField('[path][to][value]')>'100'), so if same pipeline uses same condition in different locations we can't link the condition with source reference (line, column, file).

If we accept to be little bit slower in compilation, renouncing to the cache, the code reference can be then inserted into the EventCondition, and when the condition eventually fail to evaluate in https://github.com/elastic/logstash/blob/d4519711a692f290c0d9e87401fb5d679f9385ff/logstash-core/src/main/java/org/logstash/config/ir/compiler/Utils.java#L47

andsel avatar Aug 19 '24 10:08 andsel

:green_heart: Build Succeeded

History

  • :green_heart: Build #1406 succeeded f60892c5a029753dd61300c74dabe91157d14e6b
  • :green_heart: Build #1398 succeeded e2f3197881bfb39591e23730480bd089ffa8e098
  • :broken_heart: Build #1397 failed ea5d0949260f11184c807d635099e6158169c865
  • :green_heart: Build #1395 succeeded 135ae08c94b54ceaf44d4063e86950ba36e1d463
  • :green_heart: Build #1392 succeeded 01fcab7c176a70009005903393de17fc190bf3a1
  • :green_heart: Build #1391 succeeded beb47ad2820bbf51c3a46470be615db10aaf6fe5

cc @andsel

elasticmachine avatar Sep 05 '24 08:09 elasticmachine

Hi @andsel

In which logstash version would this fix be released?

Thanks Paras

paulparas avatar Oct 30 '24 07:10 paulparas

Hi @paulparas thank's for pointing it out, it's going to be released in 8.16.0

andsel avatar Oct 30 '24 07:10 andsel