diff --git a/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/AbstractOTLPIndexingRestIT.java b/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/AbstractOTLPIndexingRestIT.java index 67421b9ab9ce5..6dd18d3e60b57 100644 --- a/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/AbstractOTLPIndexingRestIT.java +++ b/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/AbstractOTLPIndexingRestIT.java @@ -28,8 +28,10 @@ import org.junit.ClassRule; import java.io.IOException; +import java.util.Map; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.equalTo; public abstract class AbstractOTLPIndexingRestIT extends ESRestTestCase { @@ -131,4 +133,19 @@ protected ObjectPath search(String target) throws IOException { assertOK(response); return ObjectPath.createFromResponse(response); } + + protected ObjectPath search(String target, String body) throws IOException { + Request request = new Request("GET", target + "/_search"); + request.setJsonEntity(body); + var response = client().performRequest(request); + assertOK(response); + return ObjectPath.createFromResponse(response); + } + + protected static ObjectPath getIndexMappingPath(String target) throws IOException { + Map mappings = ObjectPath.createFromResponse(client().performRequest(new Request("GET", target + "/_mapping"))) + .evaluate(""); + assertThat(mappings, aMapWithSize(1)); + return new ObjectPath(new ObjectPath(mappings.values().iterator().next()).evaluate("mappings")); + } } diff --git a/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPLogsIndexingRestIT.java b/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPLogsIndexingRestIT.java index 1f80126311f51..9a4d071d83dd5 100644 --- a/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPLogsIndexingRestIT.java +++ b/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPLogsIndexingRestIT.java @@ -19,11 +19,14 @@ import java.io.IOException; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.api.logs.Severity.INFO; import static io.opentelemetry.api.logs.Severity.WARN; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; public class OTLPLogsIndexingRestIT extends AbstractOTLPIndexingRestIT { @@ -91,6 +94,40 @@ public void testLogWithAttributes() throws Exception { assertThat(source.evaluate("resource.attributes.service\\.name"), equalTo("elasticsearch")); } + public void testGeoLocationAttributesAreMerged() throws Exception { + logger.logRecordBuilder() + .setBody("geo log") + .setSeverity(INFO) + .setAttribute(doubleKey("client.geo.location.lon"), 143.2104) + .setAttribute(doubleKey("client.geo.location.lat"), -33.494) + .setAttribute(doubleKey("server.geo.location.lon"), 1.1) + .emit(); + indexLogs(); + + ObjectPath search = search("logs-generic.otel-default", """ + { + "fields": ["attributes.client.geo.location"] + } + """); + assertThat(search.evaluate("hits.total.value"), equalTo(1)); + var source = new ObjectPath(search.evaluate("hits.hits.0._source")); + assertThat(search.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.type"), equalTo("Point")); + assertThat( + search.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.coordinates.0").doubleValue(), + closeTo(143.2104, 0.001) + ); + assertThat( + search.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.coordinates.1").doubleValue(), + closeTo(-33.494, 0.001) + ); + assertThat( + getIndexMappingPath("logs-generic.otel-default").evaluate("properties.attributes.properties.client\\.geo\\.location.type"), + equalTo("geo_point") + ); + assertThat(source.evaluate("attributes.server\\.geo\\.location\\.lon"), equalTo(1.1)); + assertThat(source.evaluate("attributes.server\\.geo\\.location\\.lat"), nullValue()); + } + public void testDataStreamRouting() throws Exception { logger.logRecordBuilder() .setBody("routed log") diff --git a/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPTracesIndexingRestIT.java b/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPTracesIndexingRestIT.java index 014743cb8a544..fc0d1e38b4118 100644 --- a/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPTracesIndexingRestIT.java +++ b/x-pack/plugin/otel-data/src/javaRestTest/java/org/elasticsearch/xpack/oteldata/otlp/OTLPTracesIndexingRestIT.java @@ -23,6 +23,7 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.isA; @@ -78,12 +79,18 @@ public void testSpanWithAttributes() throws Exception { String spanId = span.getSpanContext().getSpanId(); span.setAttribute("http.method", "GET"); span.setAttribute("http.status_code", 404L); + span.setAttribute("client.geo.location.lon", 143.2104); + span.setAttribute("client.geo.location.lat", -33.494); span.setStatus(StatusCode.ERROR, "not found"); span.end(); indexTraces(); - ObjectPath search = search("traces-generic.otel-default"); + ObjectPath search = search("traces-generic.otel-default", """ + { + "fields": ["attributes.client.geo.location"] + } + """); assertThat(search.evaluate("hits.total.value"), equalTo(1)); var source = new ObjectPath(search.evaluate("hits.hits.0._source")); assertThat(source.evaluate("name"), equalTo("GET /orders")); @@ -95,6 +102,19 @@ public void testSpanWithAttributes() throws Exception { assertThat(source.evaluate("status.message"), equalTo("not found")); assertThat(source.evaluate("attributes.http\\.method"), equalTo("GET")); assertThat(source.evaluate("attributes.http\\.status_code"), equalTo(404)); + assertThat(search.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.type"), equalTo("Point")); + assertThat( + search.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.coordinates.0").doubleValue(), + closeTo(143.2104, 0.001) + ); + assertThat( + search.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.coordinates.1").doubleValue(), + closeTo(-33.494, 0.001) + ); + assertThat( + getIndexMappingPath("traces-generic.otel-default").evaluate("properties.attributes.properties.client\\.geo\\.location.type"), + equalTo("geo_point") + ); assertThat(source.evaluate("resource.attributes.service\\.name"), equalTo("elasticsearch")); assertThat(source.evaluate("scope.name"), equalTo(getClass().getSimpleName())); } @@ -103,7 +123,15 @@ public void testSpanEventsAreIndexedAsLogs() throws Exception { var span = tracer.spanBuilder("span-with-event").startSpan(); String traceId = span.getSpanContext().getTraceId(); String spanId = span.getSpanContext().getSpanId(); - span.addEvent("exception", Attributes.of(stringKey("event.attr.foo"), "event.attr.bar")); + span.addEvent( + "exception", + Attributes.builder() + .put(stringKey("event.attr.foo"), "event.attr.bar") + .put("client.geo.location.lon", 143.2104) + .put("client.geo.location.lat", -33.494) + .put("server.geo.location.lon", 1.1) + .build() + ); span.end(); indexTraces(); @@ -111,13 +139,31 @@ public void testSpanEventsAreIndexedAsLogs() throws Exception { ObjectPath tracesSearch = search("traces-generic.otel-default"); assertThat(tracesSearch.evaluate("hits.total.value"), equalTo(1)); - ObjectPath logsSearch = search("logs-generic.otel-default"); + ObjectPath logsSearch = search("logs-generic.otel-default", """ + { + "fields": ["attributes.client.geo.location"] + } + """); assertThat(logsSearch.evaluate("hits.total.value"), equalTo(1)); var source = new ObjectPath(logsSearch.evaluate("hits.hits.0._source")); assertThat(source.evaluate("event_name"), equalTo("exception")); assertThat(source.evaluate("trace_id"), equalTo(traceId)); assertThat(source.evaluate("span_id"), equalTo(spanId)); assertThat(source.evaluate("attributes.event\\.attr\\.foo"), equalTo("event.attr.bar")); + assertThat(logsSearch.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.type"), equalTo("Point")); + assertThat( + logsSearch.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.coordinates.0").doubleValue(), + closeTo(143.2104, 0.001) + ); + assertThat( + logsSearch.evaluate("hits.hits.0.fields.attributes\\.client\\.geo\\.location.0.coordinates.1").doubleValue(), + closeTo(-33.494, 0.001) + ); + assertThat( + getIndexMappingPath("logs-generic.otel-default").evaluate("properties.attributes.properties.client\\.geo\\.location.type"), + equalTo("geo_point") + ); + assertThat(source.evaluate("attributes.server\\.geo\\.location\\.lon"), equalTo(1.1)); assertThat(source.evaluate("attributes.event\\.name"), equalTo("exception")); assertThat(source.evaluate("data_stream.type"), equalTo("logs")); } diff --git a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilder.java b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilder.java index 35953735066d6..4a579dba96f27 100644 --- a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilder.java +++ b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilder.java @@ -65,10 +65,10 @@ public void buildLogDocument( } addSpanId(builder, logRecord.getSpanId().toByteArray()); addTraceId(builder, logRecord.getTraceId().toByteArray()); - buildResource(resource, resourceSchemaUrl, builder); + buildResourceWithGeoPoints(resource, resourceSchemaUrl, builder); buildDataStream(builder, targetIndex); - buildScope(builder, scope, scopeSchemaUrl); - buildAttributes(builder, logRecord.getAttributesList(), logRecord.getDroppedAttributesCount()); + buildScopeWithGeoPoints(builder, scope, scopeSchemaUrl); + buildAttributesWithGeoPoints(builder, logRecord.getAttributesList(), logRecord.getDroppedAttributesCount()); buildBody(builder, logRecord); builder.endObject(); } diff --git a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/MetricDocumentBuilder.java b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/MetricDocumentBuilder.java index 0dc5c871e53d1..fb32d2fe12011 100644 --- a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/MetricDocumentBuilder.java +++ b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/MetricDocumentBuilder.java @@ -51,6 +51,9 @@ public BytesRef buildMetricDocument( if (dataPointGroup.getStartTimestampUnixNano() != 0) { builder.field("start_timestamp", TimeUnit.NANOSECONDS.toMillis(dataPointGroup.getStartTimestampUnixNano())); } + // Metrics intentionally skip merging paired *.geo.location.lat/.lon into a [lon, lat] array: + // The *.geo.location dynamic template doesn't apply to metrics because geo_point isn't a supported dimension type. + // That would mean the merged value would land as a plain [lon, lat] array with no guaranteed element order. buildResource(dataPointGroup.resource(), dataPointGroup.resourceSchemaUrl(), builder); buildDataStream(builder, dataPointGroup.targetIndex()); buildScope(builder, dataPointGroup.scope(), dataPointGroup.scopeSchemaUrl()); diff --git a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilder.java b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilder.java index 86d47bbc102e8..0c5e9a69d79c4 100644 --- a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilder.java +++ b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilder.java @@ -22,7 +22,9 @@ import java.io.IOException; import java.util.HexFormat; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -42,18 +44,36 @@ public OTelDocumentBuilder(BufferedByteStringAccessor byteStringAccessor) { } protected void buildResource(Resource resource, ByteString schemaUrl, XContentBuilder builder) throws IOException { + buildResource(resource, schemaUrl, builder, false); + } + + protected void buildResourceWithGeoPoints(Resource resource, ByteString schemaUrl, XContentBuilder builder) throws IOException { + buildResource(resource, schemaUrl, builder, true); + } + + private void buildResource(Resource resource, ByteString schemaUrl, XContentBuilder builder, boolean mergeGeoLocationAttributes) + throws IOException { builder.startObject("resource"); addFieldIfNotEmpty(builder, "schema_url", schemaUrl); - buildAttributes(builder, resource.getAttributesList(), resource.getDroppedAttributesCount()); + buildAttributes(builder, resource.getAttributesList(), resource.getDroppedAttributesCount(), mergeGeoLocationAttributes); builder.endObject(); } protected void buildScope(XContentBuilder builder, InstrumentationScope scope, ByteString schemaUrl) throws IOException { + buildScope(builder, scope, schemaUrl, false); + } + + protected void buildScopeWithGeoPoints(XContentBuilder builder, InstrumentationScope scope, ByteString schemaUrl) throws IOException { + buildScope(builder, scope, schemaUrl, true); + } + + private void buildScope(XContentBuilder builder, InstrumentationScope scope, ByteString schemaUrl, boolean mergeGeoLocationAttributes) + throws IOException { builder.startObject("scope"); addFieldIfNotEmpty(builder, "schema_url", schemaUrl); addFieldIfNotEmpty(builder, "name", scope.getNameBytes()); addFieldIfNotEmpty(builder, "version", scope.getVersionBytes()); - buildAttributes(builder, scope.getAttributesList(), scope.getDroppedAttributesCount()); + buildAttributes(builder, scope.getAttributesList(), scope.getDroppedAttributesCount(), mergeGeoLocationAttributes); builder.endObject(); } @@ -91,19 +111,49 @@ protected void buildDataStream(XContentBuilder builder, TargetIndex targetIndex) builder.endObject(); } + /** + * Builds attributes as received from OTLP, except for Elastic control attributes that are filtered out. + * Geo attributes are not merged; use {@link #buildAttributesWithGeoPoints} when paired geo attributes + * should be converted into {@code geo_point} values. + */ protected void buildAttributes(XContentBuilder builder, List attributes, int droppedAttributesCount) throws IOException { + buildAttributes(builder, attributes, droppedAttributesCount, false); + } + + /** + * Builds attributes while merging paired double {@code *.geo.location.lon} and {@code *.geo.location.lat} + * values into {@code *.geo.location} arrays in {@code [lon, lat]} order. + */ + protected void buildAttributesWithGeoPoints(XContentBuilder builder, List attributes, int droppedAttributesCount) + throws IOException { + buildAttributes(builder, attributes, droppedAttributesCount, true); + } + + private void buildAttributes( + XContentBuilder builder, + List attributes, + int droppedAttributesCount, + boolean mergeGeoLocationAttributes + ) throws IOException { if (droppedAttributesCount > 0) { builder.field("dropped_attributes_count", droppedAttributesCount); } builder.startObject("attributes"); + GeoLocationAttributes geoLocationAttributes = mergeGeoLocationAttributes ? new GeoLocationAttributes() : null; for (int i = 0, size = attributes.size(); i < size; i++) { KeyValue attribute = attributes.get(i); String key = attribute.getKey(); if (isIgnoredAttribute(key) == false) { + if (geoLocationAttributes != null && geoLocationAttributes.addIfGeoLocationAttribute(attribute)) { + continue; + } builder.field(key); buildAnyValue(builder, attribute.getValue()); } } + if (geoLocationAttributes != null) { + geoLocationAttributes.write(builder); + } builder.endObject(); } @@ -166,4 +216,79 @@ private static void addHexIdIfNotEmpty(XContentBuilder builder, String fieldName builder.field(fieldName, HEX.formatHex(id)); } } + + private static class GeoLocationAttributes { + private static final String GEO_LOCATION = "geo.location"; + private static final String GEO_LOCATION_LAT = "geo.location.lat"; + private static final String GEO_LOCATION_LON = "geo.location.lon"; + private static final String NAMESPACED_GEO_LOCATION_LAT = "." + GEO_LOCATION_LAT; + private static final String NAMESPACED_GEO_LOCATION_LON = "." + GEO_LOCATION_LON; + + private Map locationsByPrefix; + + boolean addIfGeoLocationAttribute(KeyValue attribute) { + AnyValue value = attribute.getValue(); + if (value.getValueCase() != AnyValue.ValueCase.DOUBLE_VALUE) { + return false; + } + String key = attribute.getKey(); + String prefix; + boolean isLon; + if (GEO_LOCATION_LON.equals(key)) { + prefix = ""; + isLon = true; + } else if (GEO_LOCATION_LAT.equals(key)) { + prefix = ""; + isLon = false; + } else if (key.endsWith(NAMESPACED_GEO_LOCATION_LON)) { + prefix = key.substring(0, key.length() - NAMESPACED_GEO_LOCATION_LON.length()); + isLon = true; + } else if (key.endsWith(NAMESPACED_GEO_LOCATION_LAT)) { + prefix = key.substring(0, key.length() - NAMESPACED_GEO_LOCATION_LAT.length()); + isLon = false; + } else { + return false; + } + if (locationsByPrefix == null) { + locationsByPrefix = new LinkedHashMap<>(); + } + GeoLocation location = locationsByPrefix.computeIfAbsent(prefix, ignored -> new GeoLocation()); + if (isLon) { + location.lon = value.getDoubleValue(); + location.lonKey = key; + } else { + location.lat = value.getDoubleValue(); + location.latKey = key; + } + return true; + } + + void write(XContentBuilder builder) throws IOException { + if (locationsByPrefix == null) { + return; + } + for (Map.Entry entry : locationsByPrefix.entrySet()) { + String prefix = entry.getKey(); + GeoLocation location = entry.getValue(); + if (location.lonKey != null && location.latKey != null) { + builder.field(prefix.isEmpty() ? GEO_LOCATION : prefix + "." + GEO_LOCATION); + builder.startArray(); + builder.value(location.lon); + builder.value(location.lat); + builder.endArray(); + } else if (location.lonKey != null) { + builder.field(location.lonKey, location.lon); + } else { + builder.field(location.latKey, location.lat); + } + } + } + } + + private static class GeoLocation { + private double lon; + private double lat; + private String lonKey; + private String latKey; + } } diff --git a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilder.java b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilder.java index 85efc838b9e81..b31fff5c21fdb 100644 --- a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilder.java +++ b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilder.java @@ -63,10 +63,10 @@ public void buildSpanDocument( if (span.getDroppedLinksCount() > 0) { builder.field("dropped_links_count", span.getDroppedLinksCount()); } - buildResource(resource, resourceSchemaUrl, builder); + buildResourceWithGeoPoints(resource, resourceSchemaUrl, builder); buildDataStream(builder, targetIndex); - buildScope(builder, scope, scopeSchemaUrl); - buildAttributes(builder, span.getAttributesList(), span.getDroppedAttributesCount()); + buildScopeWithGeoPoints(builder, scope, scopeSchemaUrl); + buildAttributesWithGeoPoints(builder, span.getAttributesList(), span.getDroppedAttributesCount()); buildLinks(builder, span.getLinksList()); buildStatus(builder, span); builder.endObject(); @@ -83,7 +83,7 @@ private void buildLinks(XContentBuilder builder, List links) throws I addHexFieldIfNotEmpty(builder, "trace_id", link.getTraceId()); addHexFieldIfNotEmpty(builder, "span_id", link.getSpanId()); addFieldIfNotEmpty(builder, "trace_state", link.getTraceStateBytes()); - buildAttributes(builder, link.getAttributesList(), link.getDroppedAttributesCount()); + buildAttributesWithGeoPoints(builder, link.getAttributesList(), link.getDroppedAttributesCount()); builder.endObject(); } builder.endArray(); diff --git a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilder.java b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilder.java index f64c641221fa1..b712477b50edb 100644 --- a/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilder.java +++ b/x-pack/plugin/otel-data/src/main/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilder.java @@ -51,9 +51,9 @@ public void buildSpanEventDocument( addHexFieldIfNotEmpty(builder, "trace_id", span.getTraceId()); addHexFieldIfNotEmpty(builder, "span_id", span.getSpanId()); addFieldIfNotEmpty(builder, "event_name", event.getNameBytes()); - buildAttributes(builder, attributesWithEventName(event), event.getDroppedAttributesCount()); - buildResource(resource, resourceSchemaUrl, builder); - buildScope(builder, scope, scopeSchemaUrl); + buildAttributesWithGeoPoints(builder, attributesWithEventName(event), event.getDroppedAttributesCount()); + buildResourceWithGeoPoints(resource, resourceSchemaUrl, builder); + buildScopeWithGeoPoints(builder, scope, scopeSchemaUrl); builder.endObject(); } diff --git a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilderTests.java b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilderTests.java index 784835251e8b4..acc542332e278 100644 --- a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilderTests.java +++ b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/LogDocumentBuilderTests.java @@ -246,6 +246,19 @@ public void testBytesAttribute() throws IOException { assertThat(doc.evaluate("attributes.my_bytes"), equalTo(Base64.getEncoder().encodeToString(bytes))); } + public void testGeoLocationAttributesAreMerged() throws IOException { + LogRecord logRecord = LogRecord.newBuilder() + .setTimeUnixNano(1_000_000_000L) + .setBody(AnyValue.newBuilder().setStringValue("msg").build()) + .addAttributes(keyValue("client.geo.location.lon", 1.1)) + .addAttributes(keyValue("client.geo.location.lat", 2.2)) + .build(); + + ObjectPath doc = buildDocument(logRecord); + + assertThat(doc.evaluate("attributes.client\\.geo\\.location"), equalTo(List.of(1.1, 2.2))); + } + public void testSpanAndTraceIdsAreHexEncoded() throws IOException { LogRecord logRecord = LogRecord.newBuilder() .setTimeUnixNano(1_000_000_000L) diff --git a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilderTests.java b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilderTests.java new file mode 100644 index 0000000000000..cbbf7a35fb3c6 --- /dev/null +++ b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/OTelDocumentBuilderTests.java @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.oteldata.otlp.docbuilder; + +import io.opentelemetry.proto.common.v1.KeyValue; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.ObjectPath; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.oteldata.otlp.proto.BufferedByteStringAccessor; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.xpack.oteldata.otlp.OtlpUtils.keyValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class OTelDocumentBuilderTests extends ESTestCase { + + private final TestDocumentBuilder documentBuilder = new TestDocumentBuilder(); + + public void testBuildAttributesWithGeoPoints() throws IOException { + ObjectPath doc = buildDocumentWithGeoPoints( + List.of( + keyValue("geo.location.lon", 1.1), + keyValue("geo.location.lat", 2.2), + keyValue("foo.bar.geo.location.lon", 3.3), + keyValue("foo.bar.geo.location.lat", 4.4), + keyValue("a.geo.location.lon", 5.5), + keyValue("b.geo.location.lat", 6.6), + keyValue("unrelatedgeo.location.lon", 7.7), + keyValue("unrelatedgeo.location.lat", 8.8), + keyValue("d", 9.9), + keyValue("e.geo.location.lon", "foo"), + keyValue("e.geo.location.lat", "bar") + ) + ); + + assertThat(doc.evaluate("attributes.geo\\.location"), equalTo(List.of(1.1, 2.2))); + assertThat(doc.evaluate("attributes.foo\\.bar\\.geo\\.location"), equalTo(List.of(3.3, 4.4))); + assertThat(doc.evaluate("attributes.a\\.geo\\.location\\.lon"), equalTo(5.5)); + assertThat(doc.evaluate("attributes.b\\.geo\\.location\\.lat"), equalTo(6.6)); + assertThat(doc.evaluate("attributes.unrelatedgeo\\.location\\.lon"), equalTo(7.7)); + assertThat(doc.evaluate("attributes.unrelatedgeo\\.location\\.lat"), equalTo(8.8)); + assertThat(doc.evaluate("attributes.d"), equalTo(9.9)); + assertThat(doc.evaluate("attributes.e\\.geo\\.location\\.lon"), equalTo("foo")); + assertThat(doc.evaluate("attributes.e\\.geo\\.location\\.lat"), equalTo("bar")); + } + + public void testBuildAttributesDoesNotMergeGeoPointsByDefault() throws IOException { + ObjectPath doc = buildDocument(List.of(keyValue("geo.location.lon", 1.1), keyValue("geo.location.lat", 2.2))); + + assertThat(doc.evaluate("attributes.geo\\.location"), nullValue()); + assertThat(doc.evaluate("attributes.geo\\.location\\.lon"), equalTo(1.1)); + assertThat(doc.evaluate("attributes.geo\\.location\\.lat"), equalTo(2.2)); + } + + private ObjectPath buildDocument(List attributes) throws IOException { + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); + builder.startObject(); + documentBuilder.buildAttributes(builder, attributes, 0); + builder.endObject(); + return ObjectPath.createFromXContent(JsonXContent.jsonXContent, BytesReference.bytes(builder)); + } + + private ObjectPath buildDocumentWithGeoPoints(List attributes) throws IOException { + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); + builder.startObject(); + documentBuilder.buildAttributesWithGeoPoints(builder, attributes, 0); + builder.endObject(); + return ObjectPath.createFromXContent(JsonXContent.jsonXContent, BytesReference.bytes(builder)); + } + + private static class TestDocumentBuilder extends OTelDocumentBuilder { + TestDocumentBuilder() { + super(new BufferedByteStringAccessor()); + } + } +} diff --git a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilderTests.java b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilderTests.java index a3adcf4e0e7c1..7f821355641be 100644 --- a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilderTests.java +++ b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanDocumentBuilderTests.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.util.Base64; +import java.util.List; import java.util.Map; import static org.elasticsearch.xpack.oteldata.otlp.OtlpUtils.keyValue; @@ -143,6 +144,34 @@ public void testTimestampFallsBackToEndTime() throws IOException { assertThat(doc.evaluate("links"), nullValue()); } + public void testGeoLocationAttributesAreMerged() throws IOException { + Resource resource = Resource.newBuilder() + .addAttributes(keyValue("resource.geo.location.lon", 9.9)) + .addAttributes(keyValue("resource.geo.location.lat", 10.1)) + .build(); + InstrumentationScope scope = InstrumentationScope.newBuilder() + .addAttributes(keyValue("scope.geo.location.lon", 11.1)) + .addAttributes(keyValue("scope.geo.location.lat", 12.2)) + .build(); + Span span = Span.newBuilder() + .setStartTimeUnixNano(2_000_000_000L) + .addAttributes(keyValue("client.geo.location.lon", 1.1)) + .addAttributes(keyValue("client.geo.location.lat", 2.2)) + .addLinks( + Span.Link.newBuilder() + .addAttributes(keyValue("peer.geo.location.lon", 3.3)) + .addAttributes(keyValue("peer.geo.location.lat", 4.4)) + ) + .build(); + + ObjectPath doc = buildDocument(resource, scope, span); + + assertThat(doc.evaluate("attributes.client\\.geo\\.location"), equalTo(List.of(1.1, 2.2))); + assertThat(doc.evaluate("links.0.attributes.peer\\.geo\\.location"), equalTo(List.of(3.3, 4.4))); + assertThat(doc.evaluate("resource.attributes.resource\\.geo\\.location"), equalTo(List.of(9.9, 10.1))); + assertThat(doc.evaluate("scope.attributes.scope\\.geo\\.location"), equalTo(List.of(11.1, 12.2))); + } + public void testTimestampPreservesSubMillisecondPrecision() throws IOException { Span span = Span.newBuilder().setStartTimeUnixNano(1_721_314_113_467_654_123L).build(); diff --git a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilderTests.java b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilderTests.java index 3e4d27a2be2cd..c270b99a1e473 100644 --- a/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilderTests.java +++ b/x-pack/plugin/otel-data/src/test/java/org/elasticsearch/xpack/oteldata/otlp/docbuilder/SpanEventDocumentBuilderTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.xpack.oteldata.otlp.proto.BufferedByteStringAccessor; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -44,11 +45,15 @@ public void testBuildSpanEventDocument() throws IOException { Resource resource = Resource.newBuilder() .setDroppedAttributesCount(1) .addAttributes(OtlpUtils.keyValue("service.name", "checkout-service")) + .addAttributes(OtlpUtils.keyValue("resource.geo.location.lon", 9.9)) + .addAttributes(OtlpUtils.keyValue("resource.geo.location.lat", 10.1)) .build(); InstrumentationScope scope = InstrumentationScope.newBuilder() .setName("checkout-tracer") .setVersion("1.0.0") .addAttributes(OtlpUtils.keyValue("scope_attr", "value")) + .addAttributes(OtlpUtils.keyValue("scope.geo.location.lon", 11.1)) + .addAttributes(OtlpUtils.keyValue("scope.geo.location.lat", 12.2)) .build(); Span span = Span.newBuilder().setTraceId(traceId).setSpanId(spanId).build(); Span.Event event = Span.Event.newBuilder() @@ -56,6 +61,8 @@ public void testBuildSpanEventDocument() throws IOException { .setTimeUnixNano(2_000_000_000L) .setDroppedAttributesCount(3) .addAttributes(OtlpUtils.keyValue("event.attr.foo", "event.attr.bar")) + .addAttributes(OtlpUtils.keyValue("client.geo.location.lon", 1.1)) + .addAttributes(OtlpUtils.keyValue("client.geo.location.lat", 2.2)) .addAttributes(OtlpUtils.keyValue("event.name", "ignored-original-name")) .build(); @@ -70,14 +77,16 @@ public void testBuildSpanEventDocument() throws IOException { assertThat(doc.evaluate("data_stream.dataset"), equalTo("generic.otel")); assertThat(doc.evaluate("data_stream.namespace"), equalTo("default")); assertThat(doc.evaluate("attributes.event\\.attr\\.foo"), equalTo("event.attr.bar")); + assertThat(doc.evaluate("attributes.client\\.geo\\.location"), equalTo(List.of(1.1, 2.2))); assertThat(doc.evaluate("attributes.event\\.name"), equalTo("exception")); assertThat(doc.evaluate("resource.schema_url"), equalTo("https://opentelemetry.io/schemas/1.0.0")); assertThat(doc.evaluate("resource.attributes.service\\.name"), equalTo("checkout-service")); + assertThat(doc.evaluate("resource.attributes.resource\\.geo\\.location"), equalTo(List.of(9.9, 10.1))); assertThat(doc.evaluate("resource.dropped_attributes_count"), equalTo(1)); assertThat(doc.evaluate("scope.schema_url"), equalTo("https://opentelemetry.io/schemas/1.0.0")); assertThat(doc.evaluate("scope.name"), equalTo("checkout-tracer")); assertThat(doc.evaluate("scope.version"), equalTo("1.0.0")); - assertThat(doc.evaluate("scope.attributes"), equalTo(Map.of("scope_attr", "value"))); + assertThat(doc.evaluate("scope.attributes"), equalTo(Map.of("scope_attr", "value", "scope.geo.location", List.of(11.1, 12.2)))); } private ObjectPath buildDocument(Resource resource, InstrumentationScope scope, Span span, Span.Event event) throws IOException {