Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Duration Processor implementation #40753

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/ingest/ingest-node.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ include::processors/date-index-name.asciidoc[]
include::processors/dissect.asciidoc[]
include::processors/dot-expand.asciidoc[]
include::processors/drop.asciidoc[]
include::processors/duration.asciidoc[]
include::processors/fail.asciidoc[]
include::processors/foreach.asciidoc[]
include::processors/geoip.asciidoc[]
Expand Down
32 changes: 32 additions & 0 deletions docs/reference/ingest/processors/duration.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[[duration-processor]]
=== Duration Processor
Converts a human readable time value (e.g. 1.7s) to its value in milliseconds (e.g. 1700) or another kind of time unit.

Supported human readable units are "nanos", "micros", "ms", "s", "m", "h", "d" case insensitive. An error will occur if
the field is not a supported format or resultant value exceeds 2^63 nanoseconds.

This processor accepts fractional durations, but may lose resolution when converting fractional durations into time
units that are the same size or larger (e.g. A value of `1.2s` will return `1` when converted to `seconds` and will
return `0` when converted to `minutes`).

[[duration-options]]
.Duration Options
[options="header"]
|======
| Name | Required | Default | Description
| `field` | yes | - | The field to convert
| `target_unit` | no | `milliseconds` | The unit of time to convert the duration to. Supported values are `nanoseconds`, `microseconds`, `milliseconds`, `seconds`, `minuutes`, `hours`, `days`.
| `target_field` | no | `field` | The field to assign the converted value to, by default `field` is updated in-place
| `ignore_missing` | no | `false` | If `true` and `field` does not exist or is `null`, the processor quietly exits without modifying the document
include::common-options.asciidoc[]
|======

[source,js]
--------------------------------------------------
{
"duration": {
"field": "operation.runtime"
}
}
--------------------------------------------------
// NOTCONSOLE
104 changes: 79 additions & 25 deletions libs/core/src/main/java/org/elasticsearch/common/unit/TimeValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ public TimeValue(long millis) {
}

public TimeValue(long duration, TimeUnit timeUnit) {
if (duration > Long.MAX_VALUE / timeUnit.toNanos(1)) {
throw new IllegalArgumentException(
"Values greater than " + Long.MAX_VALUE + " nanoseconds are not supported: " + duration + unitToSuffix(timeUnit));
} else if (duration < Long.MIN_VALUE / timeUnit.toNanos(1)) {
throw new IllegalArgumentException(
"Values less than " + Long.MIN_VALUE + " nanoseconds are not supported: " + duration + unitToSuffix(timeUnit));
}
this.duration = duration;
this.timeUnit = timeUnit;
}
Expand Down Expand Up @@ -185,8 +192,9 @@ public double getDaysFrac() {
/**
* Returns a {@link String} representation of the current {@link TimeValue}.
*
* Note that this method might produce fractional time values (ex 1.6m) which cannot be
* parsed by method like {@link TimeValue#parse(String, String, String)}.
* Note that this method might produce fractional time values (ex 1.6m) which may lose
* resolution when parsed by methods like
* {@link TimeValue#parse(String, String, String, TimeUnit)}.
*/
@Override
public String toString() {
Expand Down Expand Up @@ -245,23 +253,27 @@ public String getStringRep() {
if (duration < 0) {
return Long.toString(duration);
}
switch (timeUnit) {
return duration + unitToSuffix(timeUnit);
}

private static String unitToSuffix(TimeUnit unit) {
switch (unit) {
case NANOSECONDS:
return duration + "nanos";
return "nanos";
case MICROSECONDS:
return duration + "micros";
return "micros";
case MILLISECONDS:
return duration + "ms";
return "ms";
case SECONDS:
return duration + "s";
return "s";
case MINUTES:
return duration + "m";
return "m";
case HOURS:
return duration + "h";
return "h";
case DAYS:
return duration + "d";
return "d";
default:
throw new IllegalArgumentException("unknown time unit: " + timeUnit.name());
throw new IllegalArgumentException("unknown time unit: " + unit.name());
}
}

Expand All @@ -278,20 +290,20 @@ public static TimeValue parseTimeValue(String sValue, TimeValue defaultValue, St
}
final String normalized = sValue.toLowerCase(Locale.ROOT).trim();
if (normalized.endsWith("nanos")) {
return new TimeValue(parse(sValue, normalized, "nanos"), TimeUnit.NANOSECONDS);
return parse(sValue, normalized, "nanos", TimeUnit.NANOSECONDS);
} else if (normalized.endsWith("micros")) {
return new TimeValue(parse(sValue, normalized, "micros"), TimeUnit.MICROSECONDS);
return parse(sValue, normalized, "micros", TimeUnit.MICROSECONDS);
} else if (normalized.endsWith("ms")) {
return new TimeValue(parse(sValue, normalized, "ms"), TimeUnit.MILLISECONDS);
return parse(sValue, normalized, "ms", TimeUnit.MILLISECONDS);
} else if (normalized.endsWith("s")) {
return new TimeValue(parse(sValue, normalized, "s"), TimeUnit.SECONDS);
return parse(sValue, normalized, "s", TimeUnit.SECONDS);
} else if (sValue.endsWith("m")) {
// parsing minutes should be case-sensitive as 'M' means "months", not "minutes"; this is the only special case.
return new TimeValue(parse(sValue, normalized, "m"), TimeUnit.MINUTES);
return parse(sValue, normalized, "m", TimeUnit.MINUTES);
} else if (normalized.endsWith("h")) {
return new TimeValue(parse(sValue, normalized, "h"), TimeUnit.HOURS);
return parse(sValue, normalized, "h", TimeUnit.HOURS);
} else if (normalized.endsWith("d")) {
return new TimeValue(parse(sValue, normalized, "d"), TimeUnit.DAYS);
return parse(sValue, normalized, "d", TimeUnit.DAYS);
} else if (normalized.matches("-0*1")) {
return TimeValue.MINUS_ONE;
} else if (normalized.matches("0+")) {
Expand All @@ -303,20 +315,62 @@ public static TimeValue parseTimeValue(String sValue, TimeValue defaultValue, St
}
}

private static long parse(final String initialInput, final String normalized, final String suffix) {
private static TimeValue parse(final String initialInput, final String normalized, final String suffix, TimeUnit targetUnit) {
final String s = normalized.substring(0, normalized.length() - suffix.length()).trim();
try {
return Long.parseLong(s);
} catch (final NumberFormatException e) {
if (s.contains(".")) {
try {
final double fractional = Double.parseDouble(s);
return parseFractional(initialInput, fractional, targetUnit);
} catch (final NumberFormatException e) {
throw new IllegalArgumentException("failed to parse [" + initialInput + "]", e);
}
} else {
try {
@SuppressWarnings("unused") final double ignored = Double.parseDouble(s);
throw new IllegalArgumentException("failed to parse [" + initialInput + "], fractional time values are not supported", e);
} catch (final NumberFormatException ignored) {
return new TimeValue(Long.parseLong(s), targetUnit);
} catch (final NumberFormatException e) {
throw new IllegalArgumentException("failed to parse [" + initialInput + "]", e);
}
}
}

private static TimeValue parseFractional(final String initialInput, final double fractional, TimeUnit targetUnit) {
if (TimeUnit.NANOSECONDS.equals(targetUnit)) {
throw new IllegalArgumentException("failed to parse [" + initialInput + "], fractional nanosecond values are not supported");
}
long multiplier = TimeUnit.NANOSECONDS.convert(1, targetUnit);
// check for overflow/underflow
if (fractional > 0) {
if (fractional != 0 && (Long.MAX_VALUE / (double) multiplier) < fractional) {
throw new IllegalArgumentException(
"Values greater than " + Long.MAX_VALUE + " nanoseconds are not supported: " + initialInput);
}
} else {
if (fractional != 0 && (Long.MIN_VALUE / (double) multiplier) > fractional) {
throw new IllegalArgumentException(
"Values less than " + Long.MIN_VALUE + " nanoseconds are not supported: " + initialInput);
}
}
long nanoValue = (long)(multiplier * fractional);
// right-size it - Skip days resolution since a fractional value's final unit will always be hours or lower
if (nanoValue / C5 != 0 && nanoValue % C5 == 0) {
long duration = nanoValue / C5;
return new TimeValue(duration, TimeUnit.HOURS);
} else if (nanoValue / C4 != 0 && nanoValue % C4 == 0) {
long duration = nanoValue / C4;
return new TimeValue(duration, TimeUnit.MINUTES);
} else if (nanoValue / C3 != 0 && nanoValue % C3 == 0) {
long duration = nanoValue / C3;
return new TimeValue(duration, TimeUnit.SECONDS);
} else if (nanoValue / C2 != 0 && nanoValue % C2 == 0) {
long duration = nanoValue / C2;
return new TimeValue(duration, TimeUnit.MILLISECONDS);
} else if (nanoValue / C1 != 0 && nanoValue % C1 == 0) {
long duration = nanoValue / C1;
return new TimeValue(duration, TimeUnit.MICROSECONDS);
}
return new TimeValue(nanoValue, TimeUnit.NANOSECONDS);
}

private static final long C0 = 1L;
private static final long C1 = C0 * 1000L;
private static final long C2 = C1 * 1000L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.object.HasToString.hasToString;
Expand Down Expand Up @@ -114,35 +115,99 @@ public void testParseTimeValue() {
assertTrue(TimeValue.parseTimeValue(zeros + "0", null, "test") == TimeValue.ZERO);
}

public void testParseFractional() {
assertEquals(new TimeValue(36, TimeUnit.HOURS),
TimeValue.parseTimeValue("1.5d", "test"));
assertEquals(new TimeValue(130464, TimeUnit.SECONDS),
TimeValue.parseTimeValue("1.51d", "test"));
assertEquals(new TimeValue(1584, TimeUnit.MINUTES),
TimeValue.parseTimeValue("1.1d", "test"));
assertEquals(new TimeValue(66, TimeUnit.MINUTES),
TimeValue.parseTimeValue("1.1h", "test"));
assertEquals(new TimeValue(66, TimeUnit.SECONDS),
TimeValue.parseTimeValue("1.1m", "test"));
assertEquals(new TimeValue(1100, TimeUnit.MILLISECONDS),
TimeValue.parseTimeValue("1.1s", "test"));
assertEquals(new TimeValue(1200, TimeUnit.MICROSECONDS),
TimeValue.parseTimeValue("1.2ms", "test"));
assertEquals(new TimeValue(1200, TimeUnit.NANOSECONDS),
TimeValue.parseTimeValue("1.2micros", "test"));
}

public void testFractionalToString() {
assertEquals("1.5d", TimeValue.parseTimeValue("1.5d", "test").toString());
assertEquals("1.5d", TimeValue.parseTimeValue("1.51d", "test").toString());
assertEquals("1.1d", TimeValue.parseTimeValue("1.1d", "test").toString());
assertEquals("1.1h", TimeValue.parseTimeValue("1.1h", "test").toString());
assertEquals("1.1m", TimeValue.parseTimeValue("1.1m", "test").toString());
assertEquals("1.1s", TimeValue.parseTimeValue("1.1s", "test").toString());
assertEquals("1.2ms", TimeValue.parseTimeValue("1.2ms", "test").toString());
assertEquals("1.2micros", TimeValue.parseTimeValue("1.2micros", "test").toString());
}

private static final String OVERFLOW_VALUES_NOT_SUPPORTED = "Values greater than 9223372036854775807 nanoseconds are not supported";
private static final String UNDERFLOW_VALUES_NOT_SUPPORTED = "Values less than -9223372036854775808 nanoseconds are not supported";

public void testParseValueTooLarge() {
final String longValue = "106752d";
final IllegalArgumentException longOverflow =
expectThrows(IllegalArgumentException.class, () -> TimeValue.parseTimeValue(longValue, ""));
assertThat(longOverflow, hasToString(containsString(OVERFLOW_VALUES_NOT_SUPPORTED)));
assertThat(longOverflow, hasToString(endsWith(longValue)));
}

public void testParseValueTooSmall() {
final String longValue = "-106752d";
final IllegalArgumentException longOverflow =
expectThrows(IllegalArgumentException.class, () -> TimeValue.parseTimeValue(longValue, ""));
assertThat(longOverflow, hasToString(containsString(UNDERFLOW_VALUES_NOT_SUPPORTED)));
assertThat(longOverflow, hasToString(endsWith(longValue)));
}

public void testParseFractionalValueTooLarge() {
final String fractionalValue = "106751.991167302d";
final IllegalArgumentException fractionalOverflow =
expectThrows(IllegalArgumentException.class, () -> TimeValue.parseTimeValue(fractionalValue, ""));
assertThat(fractionalOverflow, hasToString(containsString(OVERFLOW_VALUES_NOT_SUPPORTED)));
assertThat(fractionalOverflow, hasToString(endsWith(fractionalValue)));
}

public void testParseFractionalValueTooSmall() {
final String fractionalValue = "-106751.991167302d";
final IllegalArgumentException fractionalOverflow =
expectThrows(IllegalArgumentException.class, () -> TimeValue.parseTimeValue(fractionalValue, ""));
assertThat(fractionalOverflow, hasToString(containsString(UNDERFLOW_VALUES_NOT_SUPPORTED)));
assertThat(fractionalOverflow, hasToString(endsWith(fractionalValue)));
}

public void testRoundTrip() {
final String s = randomTimeValue();
assertThat(TimeValue.parseTimeValue(s, null, "test").getStringRep(), equalTo(s));
final TimeValue t = new TimeValue(randomIntBetween(1, 128), randomFrom(TimeUnit.values()));
assertThat(TimeValue.parseTimeValue(t.getStringRep(), null, "test"), equalTo(t));
}

private static final String FRACTIONAL_TIME_VALUES_ARE_NOT_SUPPORTED = "fractional time values are not supported";
private static final String FRACTIONAL_NANO_TIME_VALUES_ARE_NOT_SUPPORTED = "fractional nanosecond values are not supported";

public void testNonFractionalTimeValues() {
final String s = randomAlphaOfLength(10) + randomTimeUnit();
final IllegalArgumentException e =
expectThrows(IllegalArgumentException.class, () -> TimeValue.parseTimeValue(s, null, "test"));
assertThat(e, hasToString(containsString("failed to parse [" + s + "]")));
assertThat(e, not(hasToString(containsString(FRACTIONAL_TIME_VALUES_ARE_NOT_SUPPORTED))));
assertThat(e, not(hasToString(containsString(FRACTIONAL_NANO_TIME_VALUES_ARE_NOT_SUPPORTED))));
assertThat(e.getCause(), instanceOf(NumberFormatException.class));
}

public void testFractionalTimeValues() {
public void testFractionalNanoValues() {
double value;
do {
value = randomDouble();
} while (value == 0);
final String s = Double.toString(randomIntBetween(0, 128) + value) + randomTimeUnit();
final String s = (randomIntBetween(0, 128) + value) + "nanos";
final IllegalArgumentException e =
expectThrows(IllegalArgumentException.class, () -> TimeValue.parseTimeValue(s, null, "test"));
assertThat(e, hasToString(containsString("failed to parse [" + s + "]")));
assertThat(e, hasToString(containsString(FRACTIONAL_TIME_VALUES_ARE_NOT_SUPPORTED)));
assertThat(e.getCause(), instanceOf(NumberFormatException.class));
assertThat(e, hasToString(containsString(FRACTIONAL_NANO_TIME_VALUES_ARE_NOT_SUPPORTED)));
}

private String randomTimeUnit() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.ingest.common;

import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.ingest.ConfigurationUtils;

public final class DurationProcessor extends AbstractStringProcessor<Long> {

public static final String TYPE = "duration";
private static final String DEFAULT_UNIT = TimeUnit.MILLISECONDS.name();

private final TimeUnit targetUnit;

DurationProcessor(String tag, String field, boolean ignoreMissing, String targetField, TimeUnit targetUnit) {
super(tag, field, ignoreMissing, targetField);
this.targetUnit = targetUnit;
}

@Override
protected Long process(String value) {
try {
TimeValue timeValue = TimeValue.parseTimeValue(value, getField());
return targetUnit.convert(timeValue.duration(), timeValue.timeUnit());
} catch (IllegalArgumentException iae) {
throw new ElasticsearchParseException("failed to parse field [{}] with value [{}] as a time value", iae, getField(), value);
}
}

@Override
public String getType() {
return TYPE;
}

public static final class Factory extends AbstractStringProcessor.Factory {

protected Factory() {
super(TYPE);
}

@Override
protected DurationProcessor newProcessor(String processorTag, Map<String, Object> config, String field,
boolean ignoreMissing, String targetField) {
final String rawTimeUnit = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "target_unit", DEFAULT_UNIT);
TimeUnit toUnit = parseTimeUnit(rawTimeUnit, "target_unit");
return new DurationProcessor(processorTag, field, ignoreMissing, targetField, toUnit);
}

private static TimeUnit parseTimeUnit(String unit, String fieldName) {
try {
return TimeUnit.valueOf(unit.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException iae) {
throw new ElasticsearchParseException("failed to parse field [{}] with value [{}] as a time unit: Unrecognized time unit",
iae, fieldName, unit);
}
}
}
}
Loading