diff --git a/distribution/pom.xml b/distribution/pom.xml
index 405a2e3eda1..41c56dbac7b 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -614,6 +614,8 @@
org.apache.druid.extensions.contrib:gce-extensions
-c
org.apache.druid.extensions.contrib:aliyun-oss-extensions
+ -c
+ org.apache.druid.extensions.contrib:opentelemetry-emitter
diff --git a/extensions-contrib/opentelemetry-emitter/README.md b/extensions-contrib/opentelemetry-emitter/README.md
new file mode 100644
index 00000000000..f7298ea2928
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/README.md
@@ -0,0 +1,166 @@
+
+
+# OpenTelemetry Emitter
+
+The [OpenTelemetry](https://opentelemetry.io/) emitter generates OpenTelemetry Spans for queries.
+
+## How OpenTelemetry emitter works
+
+The [OpenTelemetry](https://opentelemetry.io/) emitter processes `ServiceMetricEvent` events for the `query/time`
+metric. It extracts OpenTelemetry context from
+the [query context](https://druid.apache.org/docs/latest/querying/query-context.html). To link druid spans to parent
+traces, the query context should contain at least `traceparent` key.
+See [context propagation](https://www.w3.org/TR/trace-context/) for more information. If no `traceparent` key is
+provided, then spans are created without `parentTraceId` and are not linked to the parent span. In addition, the emitter
+also adds other druid context entries to the span attributes.
+
+## Configuration
+
+### Enabling
+
+To enable the OpenTelemetry emitter, add the extension and enable the emitter in `common.runtime.properties`.
+
+Load the plugin:
+
+```
+druid.extensions.loadList=[..., "opentelemetry-emitter"]
+```
+
+Then there are 2 options:
+
+* You want to use only `opentelemetry-emitter`
+
+```
+druid.emitter=opentelemetry
+```
+
+* You want to use `opentelemetry-emitter` with other emitters
+
+```
+druid.emitter=composing
+druid.emitter.composing.emitters=[..., "opentelemetry"]
+```
+
+_*More about Druid configuration [here](https://druid.apache.org/docs/latest/configuration/index.html)._
+
+## Testing
+
+### Part 1: Run zipkin and otel-collector
+
+Create `docker-compose.yaml` in your working dir:
+
+```
+version: "2"
+services:
+
+ zipkin-all-in-one:
+ image: openzipkin/zipkin:latest
+ ports:
+ - "9411:9411"
+
+ otel-collector:
+ image: otel/opentelemetry-collector:latest
+ command: ["--config=otel-local-config.yaml", "${OTELCOL_ARGS}"]
+ volumes:
+ - ${PWD}/config.yaml:/otel-local-config.yaml
+ ports:
+ - "4317:4317"
+```
+
+Create `config.yaml` file with configuration for otel-collector:
+
+```
+version: "2"
+receivers:
+receivers:
+ otlp:
+ protocols:
+ grpc:
+
+exporters:
+ zipkin:
+ endpoint: "http://zipkin-all-in-one:9411/api/v2/spans"
+ format: proto
+
+ logging:
+
+processors:
+ batch:
+
+service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [logging, zipkin]
+```
+
+*_How to configure otel-collector you can read [here](https://opentelemetry.io/docs/collector/configuration/)._
+
+Run otel-collector and zipkin.
+
+```
+docker-compose up
+```
+
+### Part 2: Run Druid
+
+Build Druid:
+
+```
+mvn clean install -Pdist
+tar -C /tmp -xf distribution/target/apache-druid-0.21.0-bin.tar.gz
+cd /tmp/apache-druid-0.21.0
+```
+
+Edit `conf/druid/single-server/micro-quickstart/_common/common.runtime.properties` to enable the emitter (
+see `Configuration` section above).
+
+Start the quickstart with the apppropriate environment variables for opentelemetry autoconfiguration:
+
+```
+OTEL_SERVICE_NAME="org.apache.druid" OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" bin/start-micro-quickstart
+```
+
+*_More about opentelemetry
+autoconfiguration [here](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure)_
+
+Load sample data - [example](https://druid.apache.org/docs/latest/tutorials/index.html#step-4-load-data).
+
+### Part 3: Send queries
+
+Create `query.json`:
+
+```
+{
+ "query":"SELECT COUNT(*) as total FROM wiki WHERE countryName IS NOT NULL",
+ "context":{
+ "traceparent":"00-54ef39243e3feb12072e0f8a74c1d55a-ad6d5b581d7c29c1-01"
+ }
+}
+```
+
+Send query:
+
+```
+curl -XPOST -H'Content-Type: application/json' http://localhost:8888/druid/v2/sql/ -d @query.json
+```
+
+Then open `http://localhost:9411/zipkin/` and you can see there your spans.
\ No newline at end of file
diff --git a/extensions-contrib/opentelemetry-emitter/pom.xml b/extensions-contrib/opentelemetry-emitter/pom.xml
new file mode 100644
index 00000000000..8d81db58cdd
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/pom.xml
@@ -0,0 +1,217 @@
+
+
+
+
+ org.apache.druid
+ druid
+ 0.23.0-SNAPSHOT
+ ../../pom.xml
+
+ 4.0.0
+
+ org.apache.druid.extensions.contrib
+ opentelemetry-emitter
+ opentelemetry-emitter
+ Extension support for emitting OpenTelemetry spans for Druid queries
+
+
+ 1.7.0
+ 1.7.0-alpha
+
+ 30.1.1-jre
+ 1.41.0
+
+
+
+
+ io.opentelemetry
+ opentelemetry-bom
+ ${opentelemetry.version}
+ pom
+ import
+
+
+ io.opentelemetry
+ opentelemetry-bom-alpha
+ ${opentelemetry.version}-alpha
+ pom
+ import
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-bom-alpha
+ ${opentelemetry.instrumentation.version}
+ pom
+ compile
+
+
+
+
+
+ org.apache.druid
+ druid-core
+ ${project.parent.version}
+ provided
+
+
+ io.opentelemetry
+ opentelemetry-context
+
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+
+
+ io.opentelemetry
+ opentelemetry-sdk-trace
+
+
+ io.opentelemetry
+ opentelemetry-sdk-common
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk-extension-autoconfigure
+
+
+ io.grpc
+ grpc-netty-shaded
+ ${shade.grpc.version}
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+ com.google.inject
+ guice
+
+
+ com.google.guava
+ guava
+ ${shade.guava.version}
+
+
+
+ io.perfmark
+ perfmark-api
+ 0.23.0
+ runtime
+
+
+ joda-time
+ joda-time
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ provided
+
+
+ junit
+ junit
+ test
+
+
+ pl.pragmatists
+ JUnitParams
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ io.grpc:grpc-netty-shaded
+ com.google.guava:guava
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ opentelemetry-extension
+ package
+
+ shade
+
+
+
+
+
+
+
+
+ io.opentelemetry
+ io.grpc
+ com.google.guava
+
+
+
+
+ com.google.common
+ org.apache.druid.opentelemetry.shaded.com.google.common
+
+
+ io.grpc
+ org.apache.druid.opentelemetry.shaded.io.grpc
+
+ io.grpc.*
+
+
+
+ io.opentelemetry
+ org.apache.druid.opentelemetry.shaded.io.opentelemetry
+
+
+
+
+
+
+
+
+
diff --git a/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/DruidContextTextMapGetter.java b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/DruidContextTextMapGetter.java
new file mode 100644
index 00000000000..9a049ad609b
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/DruidContextTextMapGetter.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.druid.emitter.opentelemetry;
+
+import io.opentelemetry.context.propagation.TextMapGetter;
+import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Implementation of a text-based approach to read the W3C Trace Context from the metric query context.
+ * Context propagation
+ * W3C Trace Context
+ */
+public class DruidContextTextMapGetter implements TextMapGetter
+{
+
+ @SuppressWarnings("unchecked")
+ private Map getContext(ServiceMetricEvent event)
+ {
+ Object context = event.getUserDims().get("context");
+ if (context instanceof Map) {
+ return (Map) context;
+ }
+ return Collections.emptyMap();
+ }
+
+ @Nullable
+ @Override
+ public String get(ServiceMetricEvent event, String key)
+ {
+ return Optional.ofNullable(getContext(event).get(key)).map(Objects::toString).orElse(null);
+ }
+
+ @Override
+ public Iterable keys(ServiceMetricEvent event)
+ {
+ return getContext(event).keySet();
+ }
+}
diff --git a/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitter.java b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitter.java
new file mode 100644
index 00000000000..2c2691e64f0
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitter.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.druid.emitter.opentelemetry;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.java.util.emitter.core.Emitter;
+import org.apache.druid.java.util.emitter.core.Event;
+import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
+import org.joda.time.DateTime;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class OpenTelemetryEmitter implements Emitter
+{
+ static final DruidContextTextMapGetter DRUID_CONTEXT_TEXT_MAP_GETTER = new DruidContextTextMapGetter();
+ static final HashSet TRACEPARENT_PROPAGATION_FIELDS = new HashSet<>(Arrays.asList(
+ "traceparent",
+ "tracestate"
+ ));
+ private static final Logger log = new Logger(OpenTelemetryEmitter.class);
+ private final Tracer tracer;
+ private final TextMapPropagator propagator;
+
+ OpenTelemetryEmitter(OpenTelemetry openTelemetry)
+ {
+ tracer = openTelemetry.getTracer("druid-opentelemetry-extension");
+ propagator = openTelemetry.getPropagators().getTextMapPropagator();
+ }
+
+ @Override
+ public void start()
+ {
+ log.debug("Starting OpenTelemetryEmitter");
+ }
+
+ @Override
+ public void emit(Event e)
+ {
+ if (!(e instanceof ServiceMetricEvent)) {
+ return;
+ }
+ ServiceMetricEvent event = (ServiceMetricEvent) e;
+
+ // We only generate spans for the following types of events:
+ // query/time
+ if (!event.getMetric().equals("query/time")) {
+ return;
+ }
+
+ emitQueryTimeEvent(event);
+ }
+
+ private void emitQueryTimeEvent(ServiceMetricEvent event)
+ {
+ Context opentelemetryContext = propagator.extract(Context.current(), event, DRUID_CONTEXT_TEXT_MAP_GETTER);
+
+ try (Scope scope = opentelemetryContext.makeCurrent()) {
+ DateTime endTime = event.getCreatedTime();
+ DateTime startTime = endTime.minusMillis(event.getValue().intValue());
+
+ Span span = tracer.spanBuilder(event.getService())
+ .setStartTimestamp(startTime.getMillis(), TimeUnit.MILLISECONDS)
+ .startSpan();
+
+ getContext(event).entrySet()
+ .stream()
+ .filter(entry -> entry.getValue() != null)
+ .filter(entry -> !TRACEPARENT_PROPAGATION_FIELDS.contains(entry.getKey()))
+ .forEach(entry -> span.setAttribute(entry.getKey(), entry.getValue().toString()));
+
+ Object status = event.getUserDims().get("success");
+ if (status == null) {
+ span.setStatus(StatusCode.UNSET);
+ } else if (status.toString().equals("true")) {
+ span.setStatus(StatusCode.OK);
+ } else {
+ span.setStatus(StatusCode.ERROR);
+ }
+
+ span.end(endTime.getMillis(), TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private static Map getContext(ServiceMetricEvent event)
+ {
+ Object context = event.getUserDims().get("context");
+ if (context instanceof Map) {
+ return (Map) context;
+ }
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public void flush()
+ {
+ }
+
+ @Override
+ public void close()
+ {
+ }
+}
diff --git a/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterConfig.java b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterConfig.java
new file mode 100644
index 00000000000..c078eeb8b53
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterConfig.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.druid.emitter.opentelemetry;
+
+/**
+ * The placeholder for future configurations but there is no configuration yet
+ */
+public class OpenTelemetryEmitterConfig
+{
+}
diff --git a/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterModule.java b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterModule.java
new file mode 100644
index 00000000000..9e8e748a5e2
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterModule.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.druid.emitter.opentelemetry;
+
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Binder;
+import com.google.inject.Provides;
+import com.google.inject.name.Named;
+import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration;
+import org.apache.druid.guice.JsonConfigProvider;
+import org.apache.druid.guice.ManageLifecycle;
+import org.apache.druid.initialization.DruidModule;
+import org.apache.druid.java.util.emitter.core.Emitter;
+
+import java.util.Collections;
+import java.util.List;
+
+public class OpenTelemetryEmitterModule implements DruidModule
+{
+ private static final String EMITTER_TYPE = "opentelemetry";
+
+ @Override
+ public List extends Module> getJacksonModules()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void configure(Binder binder)
+ {
+ JsonConfigProvider.bind(binder, "druid.emitter." + EMITTER_TYPE, OpenTelemetryEmitterConfig.class);
+ }
+
+ @Provides
+ @ManageLifecycle
+ @Named(EMITTER_TYPE)
+ public Emitter getEmitter(OpenTelemetryEmitterConfig config, ObjectMapper mapper)
+ {
+ // It's a good practice to not set the GlobalOpenTelemetry since there's no need to do that
+ return new OpenTelemetryEmitter(OpenTelemetrySdkAutoConfiguration.initialize(false));
+ }
+}
diff --git a/extensions-contrib/opentelemetry-emitter/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule b/extensions-contrib/opentelemetry-emitter/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule
new file mode 100755
index 00000000000..e0b278dd069
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF 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.
+
+org.apache.druid.emitter.opentelemetry.OpenTelemetryEmitterModule
diff --git a/extensions-contrib/opentelemetry-emitter/src/test/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterTest.java b/extensions-contrib/opentelemetry-emitter/src/test/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterTest.java
new file mode 100644
index 00000000000..1db937498f9
--- /dev/null
+++ b/extensions-contrib/opentelemetry-emitter/src/test/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterTest.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.druid.emitter.opentelemetry;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.emitter.core.Event;
+import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
+import org.joda.time.DateTime;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class OpenTelemetryEmitterTest
+{
+ private static class NoopExporter implements SpanExporter
+ {
+ public Collection spanDataCollection;
+
+ @Override
+ public CompletableResultCode export(Collection collection)
+ {
+ this.spanDataCollection = collection;
+ return CompletableResultCode.ofSuccess();
+ }
+
+ @Override
+ public CompletableResultCode flush()
+ {
+ return CompletableResultCode.ofSuccess();
+ }
+
+ @Override
+ public CompletableResultCode shutdown()
+ {
+ return CompletableResultCode.ofSuccess();
+ }
+ }
+
+ private static final DateTime TIMESTAMP = DateTimes.of(2021, 11, 5, 1, 1);
+
+ private OpenTelemetry openTelemetry;
+ private NoopExporter noopExporter;
+ private OpenTelemetryEmitter emitter;
+
+ @Before
+ public void setup()
+ {
+ noopExporter = new NoopExporter();
+ openTelemetry = OpenTelemetrySdk.builder()
+ .setTracerProvider(SdkTracerProvider.builder()
+ .addSpanProcessor(SimpleSpanProcessor.create(
+ noopExporter))
+ .build())
+ .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
+ .build();
+ emitter = new OpenTelemetryEmitter(openTelemetry);
+ }
+
+ // Check that we don't call "emitQueryTimeEvent" method for event that is not instance of ServiceMetricEvent
+ @Test
+ public void testNoEmitNotServiceMetric()
+ {
+ final Event notServiceMetricEvent =
+ new Event()
+ {
+ @Override
+ public Map toMap()
+ {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String getFeed()
+ {
+ return null;
+ }
+ };
+
+ emitter.emit(notServiceMetricEvent);
+ Assert.assertNull(noopExporter.spanDataCollection);
+ }
+
+ // Check that we don't call "emitQueryTimeEvent" method for ServiceMetricEvent that is not "query/time" type
+ @Test
+ public void testNoEmitNotQueryTimeMetric()
+ {
+ final ServiceMetricEvent notQueryTimeMetric =
+ new ServiceMetricEvent.Builder().build(
+ TIMESTAMP,
+ "query/cache/total/hitRate",
+ 0.54
+ )
+ .build(
+ "broker",
+ "brokerHost1"
+ );
+
+ emitter.emit(notQueryTimeMetric);
+ Assert.assertNull(noopExporter.spanDataCollection);
+ }
+
+ @Test
+ public void testTraceparentId()
+ {
+ final String traceId = "00-54ef39243e3feb12072e0f8a74c1d55a-ad6d5b581d7c29c1-01";
+ final String expectedParentTraceId = "54ef39243e3feb12072e0f8a74c1d55a";
+ final String expectedParentSpanId = "ad6d5b581d7c29c1";
+ final Map context = new HashMap<>();
+ context.put("traceparent", traceId);
+
+ final String serviceName = "druid/broker";
+ final DateTime createdTime = TIMESTAMP;
+ final long metricValue = 100;
+
+ final ServiceMetricEvent queryTimeMetric =
+ new ServiceMetricEvent.Builder().setDimension("context", context)
+ .build(
+ createdTime,
+ "query/time",
+ metricValue
+ )
+ .build(
+ serviceName,
+ "host"
+ );
+
+ emitter.emit(queryTimeMetric);
+
+ Assert.assertEquals(1, noopExporter.spanDataCollection.size());
+
+ SpanData actualSpanData = noopExporter.spanDataCollection.iterator().next();
+ Assert.assertEquals(serviceName, actualSpanData.getName());
+ Assert.assertEquals((createdTime.getMillis() - metricValue) * 1_000_000, actualSpanData.getStartEpochNanos());
+ Assert.assertEquals(expectedParentTraceId, actualSpanData.getParentSpanContext().getTraceId());
+ Assert.assertEquals(expectedParentSpanId, actualSpanData.getParentSpanContext().getSpanId());
+ }
+
+ @Test
+ public void testAttributes()
+ {
+ final Map context = new HashMap<>();
+ final String expectedAttributeKey = "attribute";
+ final String expectedAttributeValue = "value";
+ context.put(expectedAttributeKey, expectedAttributeValue);
+
+ final ServiceMetricEvent queryTimeMetricWithAttributes =
+ new ServiceMetricEvent.Builder().setDimension("context", context)
+ .build(
+ TIMESTAMP,
+ "query/time",
+ 100
+ )
+ .build(
+ "druid/broker",
+ "host"
+ );
+
+ emitter.emit(queryTimeMetricWithAttributes);
+
+ SpanData actualSpanData = noopExporter.spanDataCollection.iterator().next();
+ Assert.assertEquals(1, actualSpanData.getAttributes().size());
+ Assert.assertEquals(
+ expectedAttributeValue,
+ actualSpanData.getAttributes().get(AttributeKey.stringKey(expectedAttributeKey))
+ );
+ }
+
+ @Test
+ public void testFilterNullValue()
+ {
+ final Map context = new HashMap<>();
+ context.put("attributeKey", null);
+
+ final ServiceMetricEvent queryTimeMetric =
+ new ServiceMetricEvent.Builder().setDimension("context", context)
+ .build(
+ TIMESTAMP,
+ "query/time",
+ 100
+ )
+ .build(
+ "druid/broker",
+ "host"
+ );
+
+ emitter.emit(queryTimeMetric);
+
+ SpanData actualSpanData = noopExporter.spanDataCollection.iterator().next();
+ Assert.assertEquals(0, actualSpanData.getAttributes().size());
+ }
+
+ @Test
+ public void testOkStatus()
+ {
+ final ServiceMetricEvent queryTimeMetric =
+ new ServiceMetricEvent.Builder().setDimension("success", "true")
+ .build(
+ TIMESTAMP,
+ "query/time",
+ 100
+ )
+ .build(
+ "druid/broker",
+ "host"
+ );
+
+ emitter.emit(queryTimeMetric);
+
+ SpanData actualSpanData = noopExporter.spanDataCollection.iterator().next();
+ Assert.assertEquals(StatusCode.OK, actualSpanData.getStatus().getStatusCode());
+ }
+
+ @Test
+ public void testErrorStatus()
+ {
+ final ServiceMetricEvent queryTimeMetric =
+ new ServiceMetricEvent.Builder().setDimension("success", "false")
+ .build(
+ TIMESTAMP,
+ "query/time",
+ 100
+ )
+ .build(
+ "druid/broker",
+ "host"
+ );
+
+ emitter.emit(queryTimeMetric);
+
+ SpanData actualSpanData = noopExporter.spanDataCollection.iterator().next();
+ Assert.assertEquals(StatusCode.ERROR, actualSpanData.getStatus().getStatusCode());
+ }
+}
diff --git a/pom.xml b/pom.xml
index d8d6d0e533c..abeda27edd9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,6 +198,7 @@
extensions-contrib/gce-extensions
extensions-contrib/aliyun-oss-extensions
extensions-contrib/prometheus-emitter
+ extensions-contrib/opentelemetry-emitter
distribution
@@ -1567,7 +1568,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 2.2
+ 3.2.4
org.apache.maven.plugins