Skip to content
This repository has been archived by the owner on Mar 21, 2023. It is now read-only.

Commit

Permalink
Add key-value parsing function (#77)
Browse files Browse the repository at this point in the history
Fixes #38
  • Loading branch information
kroepke authored and bernd committed Aug 8, 2016
1 parent c051191 commit 812b245
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ default void preprocessArgs(FunctionArgs args) {
if (value != null) {
//noinspection unchecked
final ParameterDescriptor<Object, Object> param = (ParameterDescriptor<Object, Object>) args.param(name);
if (param == null) {
throw new IllegalStateException("Unknown parameter " + name + "! Cannot continue.");
}
args.setPreComputedValue(name, param.transform().apply(value));
}
} catch (Exception exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.graylog.plugins.pipelineprocessor.functions.strings.Concat;
import org.graylog.plugins.pipelineprocessor.functions.strings.Contains;
import org.graylog.plugins.pipelineprocessor.functions.strings.GrokMatch;
import org.graylog.plugins.pipelineprocessor.functions.strings.KeyValue;
import org.graylog.plugins.pipelineprocessor.functions.strings.Lowercase;
import org.graylog.plugins.pipelineprocessor.functions.strings.RegexMatch;
import org.graylog.plugins.pipelineprocessor.functions.strings.Substring;
Expand Down Expand Up @@ -103,6 +104,7 @@ protected void configure() {
addMessageProcessorFunction(Uncapitalize.NAME, Uncapitalize.class);
addMessageProcessorFunction(Uppercase.NAME, Uppercase.class);
addMessageProcessorFunction(Concat.NAME, Concat.class);
addMessageProcessorFunction(KeyValue.NAME, KeyValue.class);

// json
addMessageProcessorFunction(JsonParse.NAME, JsonParse.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* This file is part of Graylog Pipeline Processor.
*
* Graylog Pipeline Processor is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog Pipeline Processor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog Pipeline Processor. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog.plugins.pipelineprocessor.functions.strings;

import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.inject.TypeLiteral;
import org.graylog.plugins.pipelineprocessor.EvaluationContext;
import org.graylog.plugins.pipelineprocessor.ast.functions.AbstractFunction;
import org.graylog.plugins.pipelineprocessor.ast.functions.FunctionArgs;
import org.graylog.plugins.pipelineprocessor.ast.functions.FunctionDescriptor;
import org.graylog.plugins.pipelineprocessor.ast.functions.ParameterDescriptor;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

import static org.graylog.plugins.pipelineprocessor.ast.functions.ParameterDescriptor.bool;
import static org.graylog.plugins.pipelineprocessor.ast.functions.ParameterDescriptor.string;

public class KeyValue extends AbstractFunction<Map<String, String>> {

public static final String NAME = "key_value";
private final ParameterDescriptor<String, String> valueParam;
private final ParameterDescriptor<String, CharMatcher> splitParam;
private final ParameterDescriptor<String, CharMatcher> valueSplitParam;
private final ParameterDescriptor<Boolean, Boolean> ignoreEmptyValuesParam;
private final ParameterDescriptor<Boolean, Boolean> allowDupeKeysParam;
private final ParameterDescriptor<String, String> duplicateHandlingParam;
private final ParameterDescriptor<String, CharMatcher> trimCharactersParam;
private final ParameterDescriptor<String, CharMatcher> trimValueCharactersParam;

public KeyValue() {
valueParam = string("value").build();
splitParam = string("delimiters", CharMatcher.class).transform(CharMatcher::anyOf).optional().build();
valueSplitParam = string("kv_delimiters", CharMatcher.class).transform(CharMatcher::anyOf).optional().build();

ignoreEmptyValuesParam = bool("ignore_empty_values").optional().build();
allowDupeKeysParam = bool("allow_dup_keys").optional().build();
duplicateHandlingParam = string("handle_dup_keys").optional().build();
trimCharactersParam = string("trim_key_chars", CharMatcher.class)
.transform(CharMatcher::anyOf)
.optional()
.build();
trimValueCharactersParam = string("trim_value_chars", CharMatcher.class)
.transform(CharMatcher::anyOf)
.optional()
.build();
}

@Override
public Map<String, String> evaluate(FunctionArgs args, EvaluationContext context) {
final String value = valueParam.required(args, context);
if (Strings.isNullOrEmpty(value)) {
return null;
}
final CharMatcher kvPairsMatcher = splitParam.optional(args, context).orElse(CharMatcher.whitespace());
final CharMatcher kvDelimMatcher = valueSplitParam.optional(args, context).orElse(CharMatcher.anyOf("="));

Splitter outerSplitter = Splitter.on(kvPairsMatcher)
.omitEmptyStrings()
.trimResults();

final Splitter entrySplitter = Splitter.on(kvDelimMatcher)
.omitEmptyStrings()
.trimResults();
return new MapSplitter(outerSplitter,
entrySplitter,
ignoreEmptyValuesParam.optional(args, context).orElse(true),
trimCharactersParam.optional(args, context).orElse(CharMatcher.none()),
trimValueCharactersParam.optional(args, context).orElse(CharMatcher.none()),
allowDupeKeysParam.optional(args, context).orElse(true),
duplicateHandlingParam.optional(args, context).orElse("take_first"))
.split(value);
}

@Override
public FunctionDescriptor<Map<String, String>> descriptor() {
//noinspection unchecked
return FunctionDescriptor.<Map<String, String>>builder()
.name(NAME)
.returnType((Class<? extends Map<String, String>>) new TypeLiteral<Map<String, String>>() {}.getRawType())
.params(valueParam,
splitParam,
valueSplitParam,
ignoreEmptyValuesParam,
allowDupeKeysParam,
duplicateHandlingParam,
trimCharactersParam,
trimValueCharactersParam
)
.build();
}


private static class MapSplitter {

private final Splitter outerSplitter;
private final Splitter entrySplitter;
private final boolean ignoreEmptyValues;
private final CharMatcher keyTrimMatcher;
private final CharMatcher valueTrimMatcher;
private final Boolean allowDupeKeys;
private final String duplicateHandling;

MapSplitter(Splitter outerSplitter,
Splitter entrySplitter,
boolean ignoreEmptyValues,
CharMatcher keyTrimMatcher,
CharMatcher valueTrimMatcher,
Boolean allowDupeKeys,
String duplicateHandling) {
this.outerSplitter = outerSplitter;
this.entrySplitter = entrySplitter;
this.ignoreEmptyValues = ignoreEmptyValues;
this.keyTrimMatcher = keyTrimMatcher;
this.valueTrimMatcher = valueTrimMatcher;
this.allowDupeKeys = allowDupeKeys;
this.duplicateHandling = duplicateHandling;
}


public Map<String, String> split(CharSequence sequence) {
final Map<String, String> map = new LinkedHashMap<>();

for (String entry : outerSplitter.split(sequence)) {
boolean concat = false;
Iterator<String> entryFields = entrySplitter.split(entry).iterator();

if (!entryFields.hasNext()) {
continue;
}
String key = entryFields.next();
key = keyTrimMatcher.trimFrom(key);
if (map.containsKey(key)) {
if (!allowDupeKeys) {
throw new IllegalArgumentException("Duplicate key " + key + " is not allowed in key_value function.");
}
switch (Strings.nullToEmpty(duplicateHandling).toLowerCase(Locale.ENGLISH)) {
case "take_first":
// ignore this value
continue;
case "take_last":
// simply reset the entry
break;
default:
concat = true;
}
}

if (entryFields.hasNext()) {
String value = entryFields.next();
value = valueTrimMatcher.trimFrom(value);
// already have a value, concating old+delim+new
if (concat) {
value = map.get(key) + duplicateHandling + value;
}
map.put(key, value);
} else if (!ignoreEmptyValues) {
throw new IllegalArgumentException("Missing value for key " + key);
}

}
return Collections.unmodifiableMap(map);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.graylog.plugins.pipelineprocessor.functions.strings.Concat;
import org.graylog.plugins.pipelineprocessor.functions.strings.Contains;
import org.graylog.plugins.pipelineprocessor.functions.strings.GrokMatch;
import org.graylog.plugins.pipelineprocessor.functions.strings.KeyValue;
import org.graylog.plugins.pipelineprocessor.functions.strings.Lowercase;
import org.graylog.plugins.pipelineprocessor.functions.strings.RegexMatch;
import org.graylog.plugins.pipelineprocessor.functions.strings.Substring;
Expand Down Expand Up @@ -149,6 +150,7 @@ public static void registerFunctions() {
functions.put(Swapcase.NAME, new Swapcase());
functions.put(Uncapitalize.NAME, new Uncapitalize());
functions.put(Uppercase.NAME, new Uppercase());
functions.put(KeyValue.NAME, new KeyValue());

final ObjectMapper objectMapper = new ObjectMapperProvider().get();
functions.put(JsonParse.NAME, new JsonParse(objectMapper));
Expand Down Expand Up @@ -310,9 +312,9 @@ public void strings() {
assertThat(actionsTriggered.get()).isTrue();
assertThat(message).isNotNull();
assertThat(message.getField("has_xyz")).isInstanceOf(Boolean.class);
assertThat((boolean)message.getField("has_xyz")).isFalse();
assertThat((boolean) message.getField("has_xyz")).isFalse();
assertThat(message.getField("string_literal")).isInstanceOf(String.class);
assertThat((String)message.getField("string_literal")).isEqualTo("abcd\\.e\tfg\u03a9\363");
assertThat((String) message.getField("string_literal")).isEqualTo("abcd\\.e\tfg\u03a9\363");
}

@Test
Expand Down Expand Up @@ -381,9 +383,11 @@ public void urls() {
assertThat(message.getField("user_info")).isEqualTo("admin:s3cr31");
assertThat(message.getField("host")).isEqualTo("some.host.with.lots.of.subdomains.com");
assertThat(message.getField("port")).isEqualTo(9999);
assertThat(message.getField("file")).isEqualTo("/path1/path2/three?q1=something&with_spaces=hello%20graylog&equal=can=containanotherone");
assertThat(message.getField("file")).isEqualTo(
"/path1/path2/three?q1=something&with_spaces=hello%20graylog&equal=can=containanotherone");
assertThat(message.getField("fragment")).isEqualTo("anchorstuff");
assertThat(message.getField("query")).isEqualTo("q1=something&with_spaces=hello%20graylog&equal=can=containanotherone");
assertThat(message.getField("query")).isEqualTo(
"q1=something&with_spaces=hello%20graylog&equal=can=containanotherone");
assertThat(message.getField("q1")).isEqualTo("something");
assertThat(message.getField("with_spaces")).isEqualTo("hello graylog");
assertThat(message.getField("equal")).isEqualTo("can=containanotherone");
Expand Down Expand Up @@ -523,6 +527,37 @@ public void fieldPrefixSuffix() {
assertThat(message.getField("pre_field2")).isEqualTo("8");
assertThat(message.getField("field1_suff")).isEqualTo("9");
assertThat(message.getField("field2_suff")).isEqualTo("10");
}

public void keyValue() {
final Rule rule = parser.parseRule(ruleForTest(), true);

final EvaluationContext context = contextForRuleEval(rule, new Message("", "", Tools.nowUTC()));

assertThat(context).isNotNull();
assertThat(context.evaluationErrors()).isEmpty();
final Message message = context.currentMessage();
assertThat(message).isNotNull();


assertThat(message.getField("a")).isEqualTo("1,4");
assertThat(message.getField("b")).isEqualTo("2");
assertThat(message.getField("c")).isEqualTo("3");
assertThat(message.getField("d")).isEqualTo("44");
assertThat(message.getField("e")).isEqualTo("4");
assertThat(message.getField("f")).isEqualTo("1");
assertThat(message.getField("g")).isEqualTo("3");
assertThat(message.hasField("h")).isFalse();

assertThat(message.getField("dup_first")).isEqualTo("1");
assertThat(message.getField("dup_last")).isEqualTo("2");
}

@Test
public void keyValueFailure() {
final Rule rule = parser.parseRule(ruleForTest(), true);
final EvaluationContext context = contextForRuleEval(rule, new Message("", "", Tools.nowUTC()));

assertThat(context.hasEvaluationErrors()).isTrue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
rule "kv"
when true
then
set_fields(key_value(
value: "a='1' <b>=2 \n 'c'=3 [d]=44 a=4 \"e\"=4 [f=1][[g]:3] h=",
delimiters: " \t\n\r[",
kv_delimiters: "=:",
ignore_empty_values: true,
trim_key_chars: "\"[]<>'",
trim_value_chars: "']",
allow_dup_keys: true, // the default
handle_dup_keys: "," // meaning concat, default "take_first"
));

set_fields(key_value(
value: "dup_first=1 dup_first=2",
handle_dup_keys: "take_first"
));
set_fields(key_value(
value: "dup_last=1 dup_last=2",
handle_dup_keys: "take_last"
));
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
rule "kv"
when true
then
set_fields(key_value(
value: "dup_first=1 dup_first=2",
allow_dup_keys: false
));
set_fields(key_value(
value: "dup_last=",
ignore_empty_values: false
));
end

0 comments on commit 812b245

Please sign in to comment.