From 6a938725861f0cafbe97e5ff8c3417fe65405cca Mon Sep 17 00:00:00 2001 From: Ivan Vankovich Date: Fri, 14 Jan 2022 20:18:04 -0800 Subject: [PATCH] OpenTelemetry emitter extension (#12015) * Add OpenTelemetry emitter extension * Fix build * Fix checkstyle * Add used undeclared dependencies * Ignore unused declared dependencies --- distribution/pom.xml | 2 + .../opentelemetry-emitter/README.md | 166 +++++++++++ .../opentelemetry-emitter/pom.xml | 217 ++++++++++++++ .../DruidContextTextMapGetter.java | 61 ++++ .../opentelemetry/OpenTelemetryEmitter.java | 130 +++++++++ .../OpenTelemetryEmitterConfig.java | 27 ++ .../OpenTelemetryEmitterModule.java | 60 ++++ ...rg.apache.druid.initialization.DruidModule | 16 ++ .../OpenTelemetryEmitterTest.java | 267 ++++++++++++++++++ pom.xml | 3 +- 10 files changed, 948 insertions(+), 1 deletion(-) create mode 100644 extensions-contrib/opentelemetry-emitter/README.md create mode 100644 extensions-contrib/opentelemetry-emitter/pom.xml create mode 100644 extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/DruidContextTextMapGetter.java create mode 100644 extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitter.java create mode 100644 extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterConfig.java create mode 100644 extensions-contrib/opentelemetry-emitter/src/main/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterModule.java create mode 100755 extensions-contrib/opentelemetry-emitter/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule create mode 100644 extensions-contrib/opentelemetry-emitter/src/test/java/org/apache/druid/emitter/opentelemetry/OpenTelemetryEmitterTest.java 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 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