-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce RegexClassifier for classifying errors via cfg (#5412)
- Loading branch information
Showing
5 changed files
with
242 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
ksqldb-engine/src/main/java/io/confluent/ksql/query/RegexClassifier.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
* Copyright 2020 Confluent Inc. | ||
* | ||
* Licensed under the Confluent Community License (the "License"; you may not use | ||
* this file except in compliance with the License. You may obtain a copy of the | ||
* License at | ||
* | ||
* http://www.confluent.io/confluent-community-license | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
* WARRANTIES OF ANY KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations under the License. | ||
*/ | ||
|
||
package io.confluent.ksql.query; | ||
|
||
import io.confluent.ksql.query.QueryError.Type; | ||
import java.util.Objects; | ||
import java.util.regex.Pattern; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* The {@code RegexClassifier} classifies errors based on regex patterns of | ||
* the stack trace. This class is intended as "last resort" so that noisy | ||
* errors can be classified without deploying a new version of code. | ||
*/ | ||
public final class RegexClassifier implements QueryErrorClassifier { | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(RegexClassifier.class); | ||
|
||
private final Pattern pattern; | ||
private final Type type; | ||
private final String queryId; | ||
|
||
/** | ||
* Specifying a RegexClassifier in a config requires specifying | ||
* {@code <TYPE> <PATTERN>}, for example {@code USER .*InvalidTopicException.*} | ||
* | ||
* @param config the configuration specifying the type and pattern | ||
* @return the classifier | ||
*/ | ||
public static QueryErrorClassifier fromConfig(final String config, final String queryId) { | ||
final String[] split = config.split("\\s", 2); | ||
if (split.length < 2) { | ||
LOG.warn("Ignoring invalid configuration for RegexClassifier: " + config); | ||
return err -> Type.UNKNOWN; | ||
} | ||
|
||
return new RegexClassifier( | ||
Type.valueOf(split[0].toUpperCase()), | ||
Pattern.compile(split[1], Pattern.DOTALL), | ||
queryId | ||
); | ||
} | ||
|
||
private RegexClassifier(final Type type, final Pattern pattern, final String queryId) { | ||
this.type = Objects.requireNonNull(type, "type"); | ||
this.pattern = Objects.requireNonNull(pattern, "pattern"); | ||
this.queryId = Objects.requireNonNull(queryId, "queryId"); | ||
} | ||
|
||
@Override | ||
public Type classify(final Throwable e) { | ||
LOG.info("Attempting to classify for {} under regex pattern {}.", queryId, pattern); | ||
|
||
Throwable error = e; | ||
do { | ||
if (matches(error)) { | ||
LOG.warn( | ||
"Classified error for queryId {} under regex pattern {} as type {}.", | ||
queryId, | ||
pattern, | ||
type); | ||
return type; | ||
} | ||
error = error.getCause(); | ||
} while (error != null); | ||
|
||
return Type.UNKNOWN; | ||
} | ||
|
||
private boolean matches(final Throwable e) { | ||
return pattern.matcher(e.getClass().getName()).matches() | ||
|| pattern.matcher(e.getMessage()).matches(); | ||
} | ||
|
||
} |
96 changes: 96 additions & 0 deletions
96
ksqldb-engine/src/test/java/io/confluent/ksql/query/RegexClassifierTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* Copyright 2020 Confluent Inc. | ||
* | ||
* Licensed under the Confluent Community License (the "License"; you may not use | ||
* this file except in compliance with the License. You may obtain a copy of the | ||
* License at | ||
* | ||
* http://www.confluent.io/confluent-community-license | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
* WARRANTIES OF ANY KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations under the License. | ||
*/ | ||
|
||
package io.confluent.ksql.query; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.mockito.Mockito.when; | ||
|
||
import io.confluent.ksql.query.QueryError.Type; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.MockitoJUnitRunner; | ||
|
||
@RunWith(MockitoJUnitRunner.class) | ||
public class RegexClassifierTest { | ||
|
||
@Mock | ||
private Throwable error; | ||
@Mock | ||
private Throwable cause; | ||
|
||
@Test | ||
public void shouldClassifyWithRegex() { | ||
// Given: | ||
final QueryErrorClassifier classifier = RegexClassifier.fromConfig("USER .*foo.*", "id"); | ||
givenMessage(error, "foo"); | ||
|
||
// When: | ||
final Type type = classifier.classify(error); | ||
|
||
// Then: | ||
assertThat(type, is(Type.USER)); | ||
} | ||
|
||
@Test | ||
public void shouldClassifyWithRegexInCause() { | ||
// Given: | ||
final QueryErrorClassifier classifier = RegexClassifier.fromConfig("USER .*foo.*", "id"); | ||
when(error.getCause()).thenReturn(cause); | ||
givenMessage(error, "bar"); | ||
givenMessage(cause, "foo"); | ||
|
||
// When: | ||
final Type type = classifier.classify(error); | ||
|
||
// Then: | ||
assertThat(type, is(Type.USER)); | ||
} | ||
|
||
|
||
@Test | ||
public void shouldClassifyAsUnknownIfBadRegex() { | ||
// Given: | ||
final QueryErrorClassifier classifier = RegexClassifier.fromConfig("USER", "id"); | ||
givenMessage(error, "foo"); | ||
|
||
// When: | ||
final Type type = classifier.classify(error); | ||
|
||
// Then: | ||
assertThat(type, is(Type.UNKNOWN)); | ||
} | ||
|
||
@Test | ||
public void shouldClassifyAsUnknown() { | ||
// Given: | ||
final QueryErrorClassifier classifier = RegexClassifier.fromConfig("USER .*foo.*", "id"); | ||
when(error.getCause()).thenReturn(cause); | ||
givenMessage(error, "bar"); | ||
givenMessage(cause, "baz"); | ||
|
||
// When: | ||
final Type type = classifier.classify(error); | ||
|
||
// Then: | ||
assertThat(type, is(Type.UNKNOWN)); | ||
} | ||
|
||
private void givenMessage(final Throwable error, final String message) { | ||
when(error.getMessage()).thenReturn(message); | ||
} | ||
} |