Skip to content

Commit b3124f7

Browse files
authored
Add time range bucketing attribute to APM shard search latency metrics (elastic#135524)
We recently added relevant attributes to the existing shard search latency metrics (elastic#134798). This commit introduces an additional attribute that analyzes the parsed time range filter against the @timestamp field and reports back whether it is within the last 15 minutes, last hour, last 12 hours, last day, last three days, last seven days, or last 14 days.
1 parent 30253d9 commit b3124f7

File tree

8 files changed

+188
-43
lines changed

8 files changed

+188
-43
lines changed

docs/changelog/135524.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 135524
2+
summary: Add time range bucketing attribute to APM shard search latency metrics
3+
area: Search
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/action/search/SearchRequestAttributesExtractor.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,18 @@ private SearchRequestAttributesExtractor() {}
4747
* Introspects the provided search request and extracts metadata from it about some of its characteristics.
4848
*/
4949
public static Map<String, Object> extractAttributes(SearchRequest searchRequest, String[] localIndices) {
50-
return extractAttributes(searchRequest.source(), searchRequest.scroll(), localIndices);
50+
return extractAttributes(searchRequest.source(), searchRequest.scroll(), null, -1, localIndices);
5151
}
5252

5353
/**
5454
* Introspects the provided shard search request and extracts metadata from it about some of its characteristics.
5555
*/
56-
public static Map<String, Object> extractAttributes(ShardSearchRequest shardSearchRequest) {
56+
public static Map<String, Object> extractAttributes(ShardSearchRequest shardSearchRequest, Long rangeTimestampFrom, long nowInMillis) {
5757
Map<String, Object> attributes = extractAttributes(
5858
shardSearchRequest.source(),
5959
shardSearchRequest.scroll(),
60+
rangeTimestampFrom,
61+
nowInMillis,
6062
shardSearchRequest.shardId().getIndexName()
6163
);
6264
boolean isSystem = ((EsExecutors.EsThread) Thread.currentThread()).isSystem();
@@ -67,6 +69,8 @@ public static Map<String, Object> extractAttributes(ShardSearchRequest shardSear
6769
private static Map<String, Object> extractAttributes(
6870
SearchSourceBuilder searchSourceBuilder,
6971
TimeValue scroll,
72+
Long rangeTimestampFrom,
73+
long nowInMillis,
7074
String... localIndices
7175
) {
7276
String target = extractIndices(localIndices);
@@ -77,7 +81,7 @@ private static Map<String, Object> extractAttributes(
7781
}
7882

7983
if (searchSourceBuilder == null) {
80-
return buildAttributesMap(target, ScoreSortBuilder.NAME, HITS_ONLY, false, false, false, pitOrScroll);
84+
return buildAttributesMap(target, ScoreSortBuilder.NAME, HITS_ONLY, false, false, false, pitOrScroll, null);
8185
}
8286

8387
if (searchSourceBuilder.pointInTimeBuilder() != null) {
@@ -103,14 +107,19 @@ private static Map<String, Object> extractAttributes(
103107
}
104108

105109
final boolean hasKnn = searchSourceBuilder.knnSearch().isEmpty() == false || queryMetadataBuilder.knnQuery;
110+
String timestampRangeFilter = null;
111+
if (rangeTimestampFrom != null) {
112+
timestampRangeFilter = introspectTimeRange(rangeTimestampFrom, nowInMillis);
113+
}
106114
return buildAttributesMap(
107115
target,
108116
primarySort,
109117
queryType,
110118
hasKnn,
111119
queryMetadataBuilder.rangeOnTimestamp,
112120
queryMetadataBuilder.rangeOnEventIngested,
113-
pitOrScroll
121+
pitOrScroll,
122+
timestampRangeFilter
114123
);
115124
}
116125

@@ -121,7 +130,8 @@ private static Map<String, Object> buildAttributesMap(
121130
boolean knn,
122131
boolean rangeOnTimestamp,
123132
boolean rangeOnEventIngested,
124-
String pitOrScroll
133+
String pitOrScroll,
134+
String timestampRangeFilter
125135
) {
126136
Map<String, Object> attributes = new HashMap<>(5, 1.0f);
127137
attributes.put(TARGET_ATTRIBUTE, target);
@@ -139,6 +149,9 @@ private static Map<String, Object> buildAttributesMap(
139149
if (rangeOnEventIngested) {
140150
attributes.put(RANGE_EVENT_INGESTED_ATTRIBUTE, rangeOnEventIngested);
141151
}
152+
if (timestampRangeFilter != null) {
153+
attributes.put(TIMESTAMP_RANGE_FILTER_ATTRIBUTE, timestampRangeFilter);
154+
}
142155
return attributes;
143156
}
144157

@@ -155,6 +168,7 @@ private static final class QueryMetadataBuilder {
155168
static final String KNN_ATTRIBUTE = "knn";
156169
static final String RANGE_TIMESTAMP_ATTRIBUTE = "range_timestamp";
157170
static final String RANGE_EVENT_INGESTED_ATTRIBUTE = "range_event_ingested";
171+
static final String TIMESTAMP_RANGE_FILTER_ATTRIBUTE = "timestamp_range_filter";
158172

159173
private static final String TARGET_KIBANA = ".kibana";
160174
private static final String TARGET_ML = ".ml";
@@ -310,4 +324,31 @@ private static void introspectQueryBuilder(QueryBuilder queryBuilder, QueryMetad
310324
default:
311325
}
312326
}
327+
328+
private enum TimeRangeBucket {
329+
FifteenMinutes(TimeValue.timeValueMinutes(15).getMillis(), "15_minutes"),
330+
OneHour(TimeValue.timeValueHours(1).getMillis(), "1_hour"),
331+
TwelveHours(TimeValue.timeValueHours(12).getMillis(), "12_hours"),
332+
OneDay(TimeValue.timeValueDays(1).getMillis(), "1_day"),
333+
ThreeDays(TimeValue.timeValueDays(3).getMillis(), "3_days"),
334+
SevenDays(TimeValue.timeValueDays(7).getMillis(), "7_days"),
335+
FourteenDays(TimeValue.timeValueDays(14).getMillis(), "14_days");
336+
337+
private final long millis;
338+
private final String bucketName;
339+
340+
TimeRangeBucket(long millis, String bucketName) {
341+
this.millis = millis;
342+
this.bucketName = bucketName;
343+
}
344+
}
345+
346+
static String introspectTimeRange(long timeRangeFrom, long nowInMillis) {
347+
for (TimeRangeBucket value : TimeRangeBucket.values()) {
348+
if (timeRangeFrom >= nowInMillis - value.millis) {
349+
return value.bucketName;
350+
}
351+
}
352+
return "older_than_14_days";
353+
}
313354
}

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.lucene.search.IndexSortSortedNumericDocValuesRangeQuery;
2626
import org.apache.lucene.search.Query;
2727
import org.elasticsearch.ElasticsearchParseException;
28+
import org.elasticsearch.cluster.metadata.DataStream;
2829
import org.elasticsearch.common.geo.ShapeRelation;
2930
import org.elasticsearch.common.logging.DeprecationCategory;
3031
import org.elasticsearch.common.logging.DeprecationLogger;
@@ -747,33 +748,34 @@ public Query rangeQuery(
747748
if (relation == ShapeRelation.DISJOINT) {
748749
throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] does not support DISJOINT ranges");
749750
}
750-
DateMathParser parser;
751-
if (forcedDateParser == null) {
752-
if (lowerTerm instanceof Number || upperTerm instanceof Number) {
753-
// force epoch_millis
754-
parser = EPOCH_MILLIS_PARSER;
755-
} else {
756-
parser = dateMathParser;
757-
}
758-
} else {
759-
parser = forcedDateParser;
760-
}
761-
return dateRangeQuery(lowerTerm, upperTerm, includeLower, includeUpper, timeZone, parser, context, resolution, (l, u) -> {
762-
Query query;
763-
if (isIndexed()) {
764-
query = LongPoint.newRangeQuery(name(), l, u);
765-
if (hasDocValues()) {
766-
Query dvQuery = SortedNumericDocValuesField.newSlowRangeQuery(name(), l, u);
767-
query = new IndexOrDocValuesQuery(query, dvQuery);
751+
DateMathParser parser = resolveDateMathParser(forcedDateParser, lowerTerm, upperTerm);
752+
return dateRangeQuery(
753+
lowerTerm,
754+
upperTerm,
755+
includeLower,
756+
includeUpper,
757+
timeZone,
758+
parser,
759+
context,
760+
resolution,
761+
name(),
762+
(l, u) -> {
763+
Query query;
764+
if (isIndexed()) {
765+
query = LongPoint.newRangeQuery(name(), l, u);
766+
if (hasDocValues()) {
767+
Query dvQuery = SortedNumericDocValuesField.newSlowRangeQuery(name(), l, u);
768+
query = new IndexOrDocValuesQuery(query, dvQuery);
769+
}
770+
} else {
771+
query = SortedNumericDocValuesField.newSlowRangeQuery(name(), l, u);
768772
}
769-
} else {
770-
query = SortedNumericDocValuesField.newSlowRangeQuery(name(), l, u);
771-
}
772-
if (hasDocValues() && context.indexSortedOnField(name())) {
773-
query = new IndexSortSortedNumericDocValuesRangeQuery(name(), l, u, query);
773+
if (hasDocValues() && context.indexSortedOnField(name())) {
774+
query = new IndexSortSortedNumericDocValuesRangeQuery(name(), l, u, query);
775+
}
776+
return query;
774777
}
775-
return query;
776-
});
778+
);
777779
}
778780

779781
public static Query dateRangeQuery(
@@ -785,6 +787,7 @@ public static Query dateRangeQuery(
785787
DateMathParser parser,
786788
SearchExecutionContext context,
787789
Resolution resolution,
790+
String fieldName,
788791
BiFunction<Long, Long, Query> builder
789792
) {
790793
return handleNow(context, nowSupplier -> {
@@ -796,6 +799,9 @@ public static Query dateRangeQuery(
796799
if (includeLower == false) {
797800
++l;
798801
}
802+
if (fieldName.equals(DataStream.TIMESTAMP_FIELD_NAME)) {
803+
context.setRangeTimestampFrom(l);
804+
}
799805
}
800806
if (upperTerm == null) {
801807
u = Long.MAX_VALUE;
@@ -951,6 +957,17 @@ public Relation isFieldWithinQuery(
951957
return isFieldWithinQuery(minValue, maxValue, from, to, includeLower, includeUpper, timeZone, dateParser, context);
952958
}
953959

960+
public DateMathParser resolveDateMathParser(DateMathParser dateParser, Object from, Object to) {
961+
if (dateParser == null) {
962+
if (from instanceof Number || to instanceof Number) {
963+
// force epoch_millis
964+
return EPOCH_MILLIS_PARSER;
965+
}
966+
return this.dateMathParser;
967+
}
968+
return dateParser;
969+
}
970+
954971
public Relation isFieldWithinQuery(
955972
long minValue,
956973
long maxValue,
@@ -962,14 +979,7 @@ public Relation isFieldWithinQuery(
962979
DateMathParser dateParser,
963980
QueryRewriteContext context
964981
) {
965-
if (dateParser == null) {
966-
if (from instanceof Number || to instanceof Number) {
967-
// force epoch_millis
968-
dateParser = EPOCH_MILLIS_PARSER;
969-
} else {
970-
dateParser = this.dateMathParser;
971-
}
972-
}
982+
dateParser = resolveDateMathParser(dateParser, from, to);
973983

974984
long fromInclusive = Long.MIN_VALUE;
975985
if (from != null) {

server/src/main/java/org/elasticsearch/index/mapper/DateScriptFieldType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ public Query rangeQuery(
297297
parser,
298298
context,
299299
DateFieldMapper.Resolution.MILLISECONDS,
300+
name(),
300301
(l, u) -> new LongScriptFieldRangeQuery(script, leafFactory(context)::newInstance, name(), l, u)
301302
);
302303
}

server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public class SearchExecutionContext extends QueryRewriteContext {
109109
private final Integer requestSize;
110110
private final MapperMetrics mapperMetrics;
111111

112+
private Long rangeTimestampFrom;
113+
112114
/**
113115
* Build a {@linkplain SearchExecutionContext}.
114116
*/
@@ -300,6 +302,7 @@ private void reset() {
300302
this.lookup = null;
301303
this.namedQueries.clear();
302304
this.nestedScope = new NestedScope();
305+
303306
}
304307

305308
// Set alias filter, so it can be applied for queries that need it (e.g. knn query)
@@ -742,4 +745,22 @@ public void setRewriteToNamedQueries() {
742745
public boolean rewriteToNamedQuery() {
743746
return rewriteToNamedQueries;
744747
}
748+
749+
/**
750+
* Returns the minimum lower bound across the time ranges filters against the @timestamp field included in the query
751+
*/
752+
public Long getRangeTimestampFrom() {
753+
return rangeTimestampFrom;
754+
}
755+
756+
/**
757+
* Records the lower bound of a time range filter against the @timestamp field included in the query. For telemetry purposes.
758+
*/
759+
public void setRangeTimestampFrom(Long rangeTimestampFrom) {
760+
if (this.rangeTimestampFrom == null) {
761+
this.rangeTimestampFrom = rangeTimestampFrom;
762+
} else {
763+
this.rangeTimestampFrom = Math.min(rangeTimestampFrom, this.rangeTimestampFrom);
764+
}
765+
}
745766
}

server/src/main/java/org/elasticsearch/index/search/stats/ShardSearchPhaseAPMMetrics.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package org.elasticsearch.index.search.stats;
1111

1212
import org.elasticsearch.action.search.SearchRequestAttributesExtractor;
13+
import org.elasticsearch.index.query.SearchExecutionContext;
1314
import org.elasticsearch.index.shard.SearchOperationListener;
1415
import org.elasticsearch.search.internal.SearchContext;
1516
import org.elasticsearch.search.internal.ShardSearchRequest;
@@ -42,16 +43,29 @@ public ShardSearchPhaseAPMMetrics(MeterRegistry meterRegistry) {
4243

4344
@Override
4445
public void onQueryPhase(SearchContext searchContext, long tookInNanos) {
45-
recordPhaseLatency(queryPhaseMetric, tookInNanos, searchContext.request());
46+
SearchExecutionContext searchExecutionContext = searchContext.getSearchExecutionContext();
47+
Long rangeTimestampFrom = searchExecutionContext.getRangeTimestampFrom();
48+
recordPhaseLatency(queryPhaseMetric, tookInNanos, searchContext.request(), rangeTimestampFrom);
4649
}
4750

4851
@Override
4952
public void onFetchPhase(SearchContext searchContext, long tookInNanos) {
50-
recordPhaseLatency(fetchPhaseMetric, tookInNanos, searchContext.request());
53+
SearchExecutionContext searchExecutionContext = searchContext.getSearchExecutionContext();
54+
Long rangeTimestampFrom = searchExecutionContext.getRangeTimestampFrom();
55+
recordPhaseLatency(fetchPhaseMetric, tookInNanos, searchContext.request(), rangeTimestampFrom);
5156
}
5257

53-
private static void recordPhaseLatency(LongHistogram histogramMetric, long tookInNanos, ShardSearchRequest request) {
54-
Map<String, Object> attributes = SearchRequestAttributesExtractor.extractAttributes(request);
58+
private static void recordPhaseLatency(
59+
LongHistogram histogramMetric,
60+
long tookInNanos,
61+
ShardSearchRequest request,
62+
Long rangeTimestampFrom
63+
) {
64+
Map<String, Object> attributes = SearchRequestAttributesExtractor.extractAttributes(
65+
request,
66+
rangeTimestampFrom,
67+
request.nowInMillis()
68+
);
5569
histogramMetric.record(TimeUnit.NANOSECONDS.toMillis(tookInNanos), attributes);
5670
}
5771
}

server/src/test/java/org/elasticsearch/action/search/SearchRequestAttributesExtractorTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,56 @@ public void testDepthLimit() {
361361
assertAttributes(stringObjectMap, "user", "_score", "hits_only", false, false, false, null);
362362
}
363363
}
364+
365+
public void testIntrospectTimeRange() {
366+
long nowInMillis = System.currentTimeMillis();
367+
assertEquals("15_minutes", SearchRequestAttributesExtractor.introspectTimeRange(nowInMillis, nowInMillis));
368+
369+
long fifteenMinutesAgo = nowInMillis - (15 * 60 * 1000);
370+
assertEquals(
371+
"15_minutes",
372+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(fifteenMinutesAgo, nowInMillis), nowInMillis)
373+
);
374+
375+
long oneHourAgo = nowInMillis - (60 * 60 * 1000);
376+
assertEquals(
377+
"1_hour",
378+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(oneHourAgo, fifteenMinutesAgo), nowInMillis)
379+
);
380+
381+
long twelveHoursAgo = nowInMillis - (12 * 60 * 60 * 1000);
382+
assertEquals(
383+
"12_hours",
384+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(twelveHoursAgo, oneHourAgo), nowInMillis)
385+
);
386+
387+
long oneDayAgo = nowInMillis - (24 * 60 * 60 * 1000);
388+
assertEquals(
389+
"1_day",
390+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(oneDayAgo, twelveHoursAgo), nowInMillis)
391+
);
392+
393+
long threeDaysAgo = nowInMillis - (3 * 24 * 60 * 60 * 1000);
394+
assertEquals(
395+
"3_days",
396+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(threeDaysAgo, oneDayAgo), nowInMillis)
397+
);
398+
399+
long sevenDaysAgo = nowInMillis - (7 * 24 * 60 * 60 * 1000);
400+
assertEquals(
401+
"7_days",
402+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(sevenDaysAgo, threeDaysAgo), nowInMillis)
403+
);
404+
405+
long fourteenDaysAgo = nowInMillis - (14 * 24 * 60 * 60 * 1000);
406+
assertEquals(
407+
"14_days",
408+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(fourteenDaysAgo, sevenDaysAgo), nowInMillis)
409+
);
410+
411+
assertEquals(
412+
"older_than_14_days",
413+
SearchRequestAttributesExtractor.introspectTimeRange(randomLongBetween(0, fourteenDaysAgo), nowInMillis)
414+
);
415+
}
364416
}

0 commit comments

Comments
 (0)