diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index ab659d44af..d1bf85005a 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.diffplug.spotless' spotless { java { googleJavaFormat() - licenseHeaderFile rootProject.file('gradle/enforcement/spotless.license.java'), '(package|import|public)' + licenseHeaderFile rootProject.file('gradle/enforcement/spotless.license.java'), '(package|import|public|// Includes work from:)' target 'src/**/*.java' } groovy { diff --git a/instrumentation/apache-camel-2.20/apache-camel-2.20.gradle b/instrumentation/apache-camel-2.20/apache-camel-2.20.gradle new file mode 100644 index 0000000000..c7081b901f --- /dev/null +++ b/instrumentation/apache-camel-2.20/apache-camel-2.20.gradle @@ -0,0 +1,35 @@ +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.camel" + module = "camel-core" + versions = "[2.20.1,3)" + } +} + +dependencies { + library group: 'org.apache.camel', name: 'camel-core', version: '2.20.1' + + testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-2.0') + testImplementation project(':instrumentation:servlet:servlet-3.0') + + testImplementation group: 'org.spockframework', name: 'spock-spring', version: "$versions.spock" + + testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '1.5.17.RELEASE' + testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter', version: '1.5.17.RELEASE' + + testImplementation group: 'org.apache.camel', name: 'camel-spring-boot-starter', version: '2.20.1' + testImplementation group: 'org.apache.camel', name: 'camel-jetty-starter', version: '2.20.1' + testImplementation group: 'org.apache.camel', name: 'camel-http-starter', version: '2.20.1' + testImplementation group: 'org.apache.camel', name: 'camel-jaxb-starter', version: '2.20.1' + testImplementation group: 'org.apache.camel', name: 'camel-undertow', version: '2.20.1' + + testImplementation 'javax.xml.bind:jaxb-api:2.3.1' + + latestDepTestLibrary group: 'org.apache.camel', name: 'camel-core', version: '2.+' +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ActiveSpanManager.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ActiveSpanManager.java new file mode 100644 index 0000000000..ed80d8e321 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ActiveSpanManager.java @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import org.apache.camel.Exchange; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Utility class for managing active spans as a stack associated with an exchange. */ +class ActiveSpanManager { + + private static final String ACTIVE_SPAN_PROPERTY = "OpenTelemetry.activeSpan"; + + private static final Logger LOG = LoggerFactory.getLogger(ActiveSpanManager.class); + + private ActiveSpanManager() {} + + public static Span getSpan(Exchange exchange) { + SpanWithScope spanWithScope = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class); + if (spanWithScope != null) { + return spanWithScope.getSpan(); + } + return null; + } + + /** + * This method activates the supplied span for the supplied exchange. If an existing span is found + * for the exchange, this will be pushed onto a stack. + * + * @param exchange The exchange + * @param span The span + */ + public static void activate(Exchange exchange, Span span) { + + SpanWithScope parent = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class); + SpanWithScope spanWithScope = SpanWithScope.activate(span, parent); + exchange.setProperty(ACTIVE_SPAN_PROPERTY, spanWithScope); + if (LOG.isTraceEnabled()) { + LOG.trace("Activated a span: " + spanWithScope); + } + } + + /** + * This method deactivates an existing active span associated with the supplied exchange. Once + * deactivated, if a parent span is found associated with the stack for the exchange, it will be + * restored as the current span for that exchange. + * + * @param exchange The exchange + */ + public static void deactivate(Exchange exchange) { + + SpanWithScope spanWithScope = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class); + + if (spanWithScope != null) { + spanWithScope.deactivate(); + exchange.setProperty(ACTIVE_SPAN_PROPERTY, spanWithScope.getParent()); + if (LOG.isTraceEnabled()) { + LOG.trace("Deactivated span: " + spanWithScope); + } + } + } + + public static class SpanWithScope { + @Nullable private final SpanWithScope parent; + private final Span span; + private final Scope scope; + + public SpanWithScope(SpanWithScope parent, Span span, Scope scope) { + this.parent = parent; + this.span = span; + this.scope = scope; + } + + public static SpanWithScope activate(Span span, SpanWithScope parent) { + Scope scope = CamelTracer.TRACER.startScope(span); + return new SpanWithScope(parent, span, scope); + } + + public SpanWithScope getParent() { + return parent; + } + + public Span getSpan() { + return span; + } + + public void deactivate() { + span.end(); + scope.close(); + } + + @Override + public String toString() { + return "SpanWithScope [span=" + span + ", scope=" + scope + "]"; + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelContextInstrumentation.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelContextInstrumentation.java new file mode 100644 index 0000000000..c9602ad69d --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelContextInstrumentation.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.Instrumenter; +import java.util.Collections; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.camel.CamelContext; + +@AutoService(Instrumenter.class) +public class CamelContextInstrumentation extends Instrumenter.Default { + + public CamelContextInstrumentation() { + super("apachecamel", "apache-camel"); + } + + @Override + public ElementMatcher classLoaderMatcher() { + // Optimization for expensive typeMatcher. + return hasClassesNamed("org.apache.camel.CamelContext"); + } + + @Override + public ElementMatcher typeMatcher() { + + return not(isAbstract()).and(implementsInterface(named("org.apache.camel.CamelContext"))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection", + "io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.BaseSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.DbSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.MessagingSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.HttpSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.InternalSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.KafkaSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.LogSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.RestSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.TimerSpanDecorator", + "io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.DecoratorRegistry", + "io.opentelemetry.javaagent.instrumentation.apachecamel.ActiveSpanManager", + "io.opentelemetry.javaagent.instrumentation.apachecamel.ActiveSpanManager$SpanWithScope", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelPropagationUtil", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelPropagationUtil$MapGetter", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelPropagationUtil$MapSetter", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelTracer", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelEventNotifier", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelRoutePolicy", + "io.opentelemetry.javaagent.instrumentation.apachecamel.CamelTracingService" + }; + } + + @Override + public Map, String> transformers() { + + return Collections.singletonMap( + named("start").and(isPublic()).and(takesArguments(0)), + CamelContextInstrumentation.class.getName() + "$ContextAdvice"); + } + + public static class ContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onContextStart(@Advice.This final CamelContext context) throws Exception { + + if (context.hasService(CamelTracingService.class) == null) { + // start this service eager so we init before Camel is starting up + context.addService(new CamelTracingService(context), true, true); + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelDirection.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelDirection.java new file mode 100644 index 0000000000..a18b1777dc --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelDirection.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +public enum CamelDirection { + INBOUND, + OUTBOUND; +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelEventNotifier.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelEventNotifier.java new file mode 100644 index 0000000000..668756d501 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelEventNotifier.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.util.EventObject; +import org.apache.camel.management.event.ExchangeSendingEvent; +import org.apache.camel.management.event.ExchangeSentEvent; +import org.apache.camel.support.EventNotifierSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CamelEventNotifier extends EventNotifierSupport { + + private static final Logger LOG = LoggerFactory.getLogger(CamelEventNotifier.class); + + @Override + public void notify(EventObject event) { + + try { + if (event instanceof ExchangeSendingEvent) { + onExchangeSending((ExchangeSendingEvent) event); + } else if (event instanceof ExchangeSentEvent) { + onExchangeSent((ExchangeSentEvent) event); + } + } catch (Throwable t) { + LOG.warn("Failed to capture tracing data", t); + } + } + + /** Camel about to send (outbound). */ + private void onExchangeSending(ExchangeSendingEvent ese) { + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(ese.getEndpoint()); + if (!sd.shouldStartNewSpan()) { + return; + } + + String name = + sd.getOperationName(ese.getExchange(), ese.getEndpoint(), CamelDirection.OUTBOUND); + Span span = CamelTracer.TRACER.startSpan(name, sd.getInitiatorSpanKind()); + sd.pre(span, ese.getExchange(), ese.getEndpoint(), CamelDirection.OUTBOUND); + CamelPropagationUtil.injectParent(Context.current(), ese.getExchange().getIn().getHeaders()); + ActiveSpanManager.activate(ese.getExchange(), span); + + if (LOG.isTraceEnabled()) { + LOG.trace("[Exchange sending] Initiator span started " + span); + } + } + + /** Camel finished sending (outbound). Finish span and remove it from CAMEL holder. */ + private void onExchangeSent(ExchangeSentEvent event) { + ExchangeSentEvent ese = event; + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(ese.getEndpoint()); + if (!sd.shouldStartNewSpan()) { + return; + } + + Span span = ActiveSpanManager.getSpan(ese.getExchange()); + if (span != null) { + if (LOG.isTraceEnabled()) { + LOG.trace("[Exchange sent] Initiator span finished " + span); + } + sd.post(span, ese.getExchange(), ese.getEndpoint()); + ActiveSpanManager.deactivate(ese.getExchange()); + } else { + LOG.warn("Could not find managed span for exchange " + ese.getExchange()); + } + } + + @Override + public boolean isEnabled(EventObject event) { + return event instanceof ExchangeSendingEvent || event instanceof ExchangeSentEvent; + } + + @Override + public String toString() { + return "OpenTelemetryCamelEventNotifier"; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtil.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtil.java new file mode 100644 index 0000000000..90c2de0c07 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator.Getter; +import io.opentelemetry.context.propagation.TextMapPropagator.Setter; +import java.util.Map; + +final class CamelPropagationUtil { + + private CamelPropagationUtil() {} + + static Context extractParent(final Map exchangeHeaders) { + return OpenTelemetry.getGlobalPropagators() + .getTextMapPropagator() + .extract(Context.current(), exchangeHeaders, MapGetter.INSTANCE); + } + + static void injectParent(Context context, final Map exchangeHeaders) { + OpenTelemetry.getGlobalPropagators() + .getTextMapPropagator() + .inject(context, exchangeHeaders, MapSetter.INSTANCE); + } + + private static class MapGetter implements Getter> { + + private static final MapGetter INSTANCE = new MapGetter(); + + @Override + public String get(Map map, String s) { + return (map.containsKey(s) ? map.get(s).toString() : null); + } + } + + private static class MapSetter implements Setter> { + + private static final MapSetter INSTANCE = new MapSetter(); + + @Override + public void set(Map carrier, String key, String value) { + // Camel keys are internal ones + if (!key.startsWith("Camel")) { + carrier.put(key, value); + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelRoutePolicy.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelRoutePolicy.java new file mode 100644 index 0000000000..f69e44eea2 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelRoutePolicy.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import org.apache.camel.Exchange; +import org.apache.camel.Route; +import org.apache.camel.support.RoutePolicySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CamelRoutePolicy extends RoutePolicySupport { + + private static final Logger LOG = LoggerFactory.getLogger(CamelRoutePolicy.class); + + private Span spanOnExchangeBegin(Route route, Exchange exchange, SpanDecorator sd) { + Span activeSpan = CamelTracer.TRACER.getCurrentSpan(); + String name = sd.getOperationName(exchange, route.getEndpoint(), CamelDirection.INBOUND); + Span.Builder builder = CamelTracer.TRACER.spanBuilder(name); + if (!activeSpan.getSpanContext().isValid()) { + // root operation, set kind, otherwise - INTERNAL + builder.setSpanKind(sd.getReceiverSpanKind()); + Context parentContext = CamelPropagationUtil.extractParent(exchange.getIn().getHeaders()); + if (parentContext != null) { + builder.setParent(parentContext); + } + } + return builder.startSpan(); + } + + /** + * Route exchange started, ie request could have been already captured by upper layer + * instrumentation. + */ + @Override + public void onExchangeBegin(Route route, Exchange exchange) { + try { + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(route.getEndpoint()); + Span span = spanOnExchangeBegin(route, exchange, sd); + sd.pre(span, exchange, route.getEndpoint(), CamelDirection.INBOUND); + ActiveSpanManager.activate(exchange, span); + if (LOG.isTraceEnabled()) { + LOG.trace("[Route start] Receiver span started " + span); + } + } catch (Throwable t) { + LOG.warn("Failed to capture tracing data", t); + } + } + + /** Route exchange done. Get active CAMEL span, finish, remove from CAMEL holder. */ + @Override + public void onExchangeDone(Route route, Exchange exchange) { + try { + Span span = ActiveSpanManager.getSpan(exchange); + if (span != null) { + if (LOG.isTraceEnabled()) { + LOG.trace("[Route finished] Receiver span finished " + span); + } + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(route.getEndpoint()); + sd.post(span, exchange, route.getEndpoint()); + ActiveSpanManager.deactivate(exchange); + } else { + LOG.warn("Could not find managed span for exchange=" + exchange); + } + } catch (Throwable t) { + LOG.warn("Failed to capture tracing data", t); + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracer.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracer.java new file mode 100644 index 0000000000..9806cb290c --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracer.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.DecoratorRegistry; +import org.apache.camel.Endpoint; +import org.apache.camel.util.StringHelper; + +class CamelTracer extends BaseTracer { + + public static final CamelTracer TRACER = new CamelTracer(); + + private final DecoratorRegistry registry = new DecoratorRegistry(); + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.auto.apache-camel-2.20"; + } + + public Span.Builder spanBuilder(String name) { + return tracer.spanBuilder(name); + } + + public SpanDecorator getSpanDecorator(Endpoint endpoint) { + + String component = ""; + String uri = endpoint.getEndpointUri(); + String splitURI[] = StringHelper.splitOnCharacter(uri, ":", 2); + if (splitURI[1] != null) { + component = splitURI[0]; + } + return registry.forComponent(component); + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracingService.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracingService.java new file mode 100644 index 0000000000..0bbbbe1651 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracingService.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel; + +import org.apache.camel.CamelContext; +import org.apache.camel.StaticService; +import org.apache.camel.model.RouteDefinition; +import org.apache.camel.spi.RoutePolicy; +import org.apache.camel.spi.RoutePolicyFactory; +import org.apache.camel.support.ServiceSupport; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.ServiceHelper; + +public class CamelTracingService extends ServiceSupport + implements RoutePolicyFactory, StaticService { + + private final CamelContext camelContext; + private final CamelEventNotifier eventNotifier = new CamelEventNotifier(); + private final CamelRoutePolicy routePolicy = new CamelRoutePolicy(); + + public CamelTracingService(CamelContext camelContext) { + ObjectHelper.notNull(camelContext, "CamelContext", this); + this.camelContext = camelContext; + } + + @Override + protected void doStart() throws Exception { + camelContext.getManagementStrategy().addEventNotifier(eventNotifier); + if (!camelContext.getRoutePolicyFactories().contains(this)) { + camelContext.addRoutePolicyFactory(this); + } + + ServiceHelper.startServices(eventNotifier); + } + + @Override + protected void doStop() throws Exception { + // stop event notifier + camelContext.getManagementStrategy().removeEventNotifier(eventNotifier); + ServiceHelper.stopService(eventNotifier); + + // remove route policy + camelContext.getRoutePolicyFactories().remove(this); + } + + @Override + public RoutePolicy createRoutePolicy( + CamelContext camelContext, String routeId, RouteDefinition route) { + return routePolicy; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SpanDecorator.java new file mode 100644 index 0000000000..838d3e4a80 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SpanDecorator.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +/** This interface represents a decorator specific to the component/endpoint being instrumented. */ +public interface SpanDecorator { + + /** + * This method indicates whether the component associated with the SpanDecorator should result in + * a new span being created. + * + * @return Whether a new span should be created + */ + boolean shouldStartNewSpan(); + + /** + * This method returns the operation name to use with the Span representing this exchange and + * endpoint. + * + * @param exchange The exchange + * @param endpoint The endpoint + * @return The operation name + */ + String getOperationName(Exchange exchange, Endpoint endpoint, CamelDirection camelDirection); + + /** + * This method adds appropriate details (tags/logs) to the supplied span based on the pre + * processing of the exchange. + * + * @param span The span + * @param exchange The exchange + * @param endpoint The endpoint + */ + void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection); + + /** + * This method adds appropriate details (tags/logs) to the supplied span based on the post + * processing of the exchange. + * + * @param span The span + * @param exchange The exchange + * @param endpoint The endpoint + */ + void post(Span span, Exchange exchange, Endpoint endpoint); + + /** + * This method returns the 'span.kind' value for use when the component is initiating a + * communication. + * + * @return The kind + */ + Span.Kind getInitiatorSpanKind(); + + /** + * This method returns the 'span.kind' value for use when the component is receiving a + * communication. + * + * @return The kind + */ + Span.Kind getReceiverSpanKind(); +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/BaseSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/BaseSpanDecorator.java new file mode 100644 index 0000000000..66469aab0e --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/BaseSpanDecorator.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Span.Kind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.util.StringHelper; +import org.apache.camel.util.URISupport; + +/** An abstract base implementation of the {@link SpanDecorator} interface. */ +class BaseSpanDecorator implements SpanDecorator { + + static final String DEFAULT_OPERATION_NAME = "CamelOperation"; + + /** + * This method removes the scheme, any leading slash characters and options from the supplied URI. + * This is intended to extract a meaningful name from the URI that can be used in situations, such + * as the operation name. + * + * @param endpoint The endpoint + * @return The stripped value from the URI + */ + public static String stripSchemeAndOptions(Endpoint endpoint) { + int start = endpoint.getEndpointUri().indexOf(':'); + start++; + // Remove any leading '/' + while (endpoint.getEndpointUri().charAt(start) == '/') { + start++; + } + int end = endpoint.getEndpointUri().indexOf('?'); + return end == -1 + ? endpoint.getEndpointUri().substring(start) + : endpoint.getEndpointUri().substring(start, end); + } + + public static Map toQueryParameters(String uri) { + int index = uri.indexOf('?'); + if (index != -1) { + String queryString = uri.substring(index + 1); + Map map = new HashMap<>(); + for (String param : queryString.split("&")) { + String[] parts = param.split("="); + if (parts.length == 2) { + map.put(parts[0], parts[1]); + } + } + return map; + } + return Collections.emptyMap(); + } + + @Override + public boolean shouldStartNewSpan() { + return true; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + String[] splitURI = StringHelper.splitOnCharacter(endpoint.getEndpointUri(), ":", 2); + return (splitURI.length > 0 ? splitURI[0] : DEFAULT_OPERATION_NAME); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + span.setAttribute("camel.uri", URISupport.sanitizeUri(endpoint.getEndpointUri())); + } + + @Override + public void post(Span span, Exchange exchange, Endpoint endpoint) { + if (exchange.isFailed()) { + span.setStatus(StatusCode.ERROR); + if (exchange.getException() != null) { + span.recordException(exchange.getException()); + } + } + } + + @Override + public Span.Kind getInitiatorSpanKind() { + return Kind.CLIENT; + } + + @Override + public Span.Kind getReceiverSpanKind() { + return Kind.SERVER; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DbSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DbSpanDecorator.java new file mode 100644 index 0000000000..2ef0085e04 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DbSpanDecorator.java @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import java.net.URI; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class DbSpanDecorator extends BaseSpanDecorator { + + private final String component; + private final String system; + + DbSpanDecorator(String component, String system) { + this.component = component; + this.system = system; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + + switch (component) { + case "mongodb": + case "elasticsearch": + { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + if (queryParameters.containsKey("operation")) { + return queryParameters.get("operation"); + } + } + } + return super.getOperationName(exchange, endpoint, camelDirection); + } + + private String getStatement(Exchange exchange, Endpoint endpoint) { + switch (component) { + case "mongodb": + { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + return queryParameters.toString(); + } + case "cql": + { + Object cql = exchange.getIn().getHeader("CamelCqlQuery"); + if (cql != null) { + return cql.toString(); + } else { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + if (queryParameters.containsKey("cql")) { + return queryParameters.get("cql"); + } + } + } + case "jdbc": + { + Object body = exchange.getIn().getBody(); + if (body instanceof String) { + return (String) body; + } + } + case "sql": + { + Object sqlquery = exchange.getIn().getHeader("CamelSqlQuery"); + if (sqlquery instanceof String) { + return (String) sqlquery; + } + } + } + return null; + } + + private String getDbName(Endpoint endpoint) { + switch (component) { + case "mongodb": + { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + return queryParameters.get("database"); + } + case "cql": + { + URI uri = URI.create(endpoint.getEndpointUri()); + if (uri.getPath() != null && uri.getPath().length() > 0) { + // Strip leading '/' from path + return uri.getPath().substring(1); + } + } + case "elasticsearch": + { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + if (queryParameters.containsKey("indexName")) { + return queryParameters.get("indexName"); + } + } + } + return null; + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + span.setAttribute(SemanticAttributes.DB_SYSTEM, system); + String statement = getStatement(exchange, endpoint); + if (statement != null) { + span.setAttribute(SemanticAttributes.DB_STATEMENT, statement); + } + String dbName = getDbName(endpoint); + if (dbName != null) { + span.setAttribute(SemanticAttributes.DB_NAME, dbName); + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DecoratorRegistry.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DecoratorRegistry.java new file mode 100644 index 0000000000..69d0e0a90b --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DecoratorRegistry.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator; +import io.opentelemetry.javaagent.instrumentation.api.db.DbSystem; +import java.util.HashMap; +import java.util.Map; + +public class DecoratorRegistry { + + private static final SpanDecorator DEFAULT = new BaseSpanDecorator(); + private static final Map DECORATORS = loadDecorators(); + + private static Map loadDecorators() { + + Map result = new HashMap<>(); + result.put("ahc", new HttpSpanDecorator()); + result.put("ampq", new MessagingSpanDecorator("ampq")); + result.put("aws-sns", new MessagingSpanDecorator("aws-sns")); + result.put("aws-sqs", new MessagingSpanDecorator("aws-sqs")); + result.put("cometd", new MessagingSpanDecorator("cometd")); + result.put("cometds", new MessagingSpanDecorator("cometds")); + result.put("cql", new DbSpanDecorator("cql", DbSystem.CASSANDRA)); + result.put("direct", new InternalSpanDecorator()); + result.put("direct-vm", new InternalSpanDecorator()); + result.put("disruptor", new InternalSpanDecorator()); + result.put("disruptor-vm", new InternalSpanDecorator()); + result.put("elasticsearch", new DbSpanDecorator("elasticsearch", "elasticsearch")); + result.put("http4", new HttpSpanDecorator()); + result.put("http", new HttpSpanDecorator()); + result.put("ironmq", new MessagingSpanDecorator("ironmq")); + result.put("jdbc", new DbSpanDecorator("jdbc", DbSystem.OTHER_SQL)); + result.put("jetty", new HttpSpanDecorator()); + result.put("jms", new MessagingSpanDecorator("jms")); + result.put("kafka", new KafkaSpanDecorator()); + result.put("log", new LogSpanDecorator()); + result.put("mongodb", new DbSpanDecorator("mongodb", DbSystem.MONGODB)); + result.put("mqtt", new MessagingSpanDecorator("mqtt")); + result.put("netty-http4", new HttpSpanDecorator()); + result.put("netty-http", new HttpSpanDecorator()); + result.put("paho", new MessagingSpanDecorator("paho")); + result.put("rabbitmq", new MessagingSpanDecorator("rabbitmq")); + result.put("restlet", new HttpSpanDecorator()); + result.put("rest", new RestSpanDecorator()); + result.put("seda", new InternalSpanDecorator()); + result.put("servlet", new HttpSpanDecorator()); + result.put("sjms", new MessagingSpanDecorator("sjms")); + result.put("sql", new DbSpanDecorator("sql", DbSystem.OTHER_SQL)); + result.put("stomp", new MessagingSpanDecorator("stomp")); + result.put("timer", new TimerSpanDecorator()); + result.put("undertow", new HttpSpanDecorator()); + result.put("vm", new InternalSpanDecorator()); + return result; + } + + public SpanDecorator forComponent(final String component) { + + return DECORATORS.getOrDefault(component, DEFAULT); + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java new file mode 100644 index 0000000000..6f5315d5a9 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.attributes.SemanticAttributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import java.net.MalformedURLException; +import java.net.URL; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.checkerframework.checker.nullness.qual.Nullable; + +class HttpSpanDecorator extends BaseSpanDecorator { + + private static final String POST_METHOD = "POST"; + private static final String GET_METHOD = "GET"; + + protected static String getHttpMethod(Exchange exchange, Endpoint endpoint) { + // 1. Use method provided in header. + Object method = exchange.getIn().getHeader(Exchange.HTTP_METHOD); + if (method instanceof String) { + return (String) method; + } + + // 2. GET if query string is provided in header. + if (exchange.getIn().getHeader(Exchange.HTTP_QUERY) != null) { + return GET_METHOD; + } + + // 3. GET if endpoint is configured with a query string. + if (endpoint.getEndpointUri().indexOf('?') != -1) { + return GET_METHOD; + } + + // 4. POST if there is data to send (body is not null). + if (exchange.getIn().getBody() != null) { + return POST_METHOD; + } + + // 5. GET otherwise. + return GET_METHOD; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + // Based on HTTP component documentation: + String spanName = null; + if (shouldSetPathAsName(camelDirection)) { + spanName = getPath(exchange, endpoint); + } + return (spanName == null ? getHttpMethod(exchange, endpoint) : spanName); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + String httpUrl = getHttpURL(exchange, endpoint); + if (httpUrl != null) { + span.setAttribute(SemanticAttributes.HTTP_URL, httpUrl); + } + + span.setAttribute(SemanticAttributes.HTTP_METHOD, getHttpMethod(exchange, endpoint)); + + Span serverSpan = Context.current().get(BaseTracer.CONTEXT_SERVER_SPAN_KEY); + if (shouldUpdateServerSpanName(serverSpan, camelDirection)) { + updateServerSpanName(serverSpan, exchange, endpoint); + } + } + + private boolean shouldSetPathAsName(CamelDirection camelDirection) { + return CamelDirection.INBOUND.equals(camelDirection); + } + + @Nullable + protected String getPath(Exchange exchange, Endpoint endpoint) { + + String httpUrl = getHttpURL(exchange, endpoint); + try { + URL url = new URL(httpUrl); + return url.getPath(); + } catch (MalformedURLException e) { + return null; + } + } + + private boolean shouldUpdateServerSpanName(Span serverSpan, CamelDirection camelDirection) { + return (serverSpan != null && shouldSetPathAsName(camelDirection)); + } + + private void updateServerSpanName(Span serverSpan, Exchange exchange, Endpoint endpoint) { + String path = getPath(exchange, endpoint); + if (path != null) { + serverSpan.updateName(path); + } + } + + protected String getHttpURL(Exchange exchange, Endpoint endpoint) { + Object url = exchange.getIn().getHeader(Exchange.HTTP_URL); + if (url instanceof String) { + return (String) url; + } else { + Object uri = exchange.getIn().getHeader(Exchange.HTTP_URI); + if (uri instanceof String) { + return (String) uri; + } else { + // Try to obtain from endpoint + int index = endpoint.getEndpointUri().lastIndexOf("http:"); + if (index != -1) { + return endpoint.getEndpointUri().substring(index); + } + } + } + return null; + } + + @Override + public void post(Span span, Exchange exchange, Endpoint endpoint) { + super.post(span, exchange, endpoint); + + if (exchange.hasOut()) { + Object responseCode = exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE); + if (responseCode instanceof Integer) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, (Integer) responseCode); + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/InternalSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/InternalSpanDecorator.java new file mode 100644 index 0000000000..195a458832 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/InternalSpanDecorator.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span.Kind; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class InternalSpanDecorator extends BaseSpanDecorator { + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + // Internal communications use descriptive names, so suitable + // as an operation name, but need to strip the scheme and any options + return stripSchemeAndOptions(endpoint); + } + + @Override + public boolean shouldStartNewSpan() { + return false; + } + + @Override + public Kind getReceiverSpanKind() { + return Kind.INTERNAL; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java new file mode 100644 index 0000000000..abdf0b250a --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class KafkaSpanDecorator extends MessagingSpanDecorator { + + private static final String PARTITION_KEY = "kafka.PARTITION_KEY"; + private static final String PARTITION = "kafka.PARTITION"; + private static final String KEY = "kafka.KEY"; + private static final String TOPIC = "kafka.TOPIC"; + private static final String OFFSET = "kafka.OFFSET"; + + public KafkaSpanDecorator() { + super("kafka"); + } + + @Override + public String getDestination(Exchange exchange, Endpoint endpoint) { + String topic = (String) exchange.getIn().getHeader(TOPIC); + if (topic == null) { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + topic = queryParameters.get("topic"); + } + return topic != null ? topic : super.getDestination(exchange, endpoint); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + span.setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process"); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic"); + + String partition = getValue(exchange, PARTITION, Integer.class); + if (partition != null) { + span.setAttribute("partition", partition); + } + + String partitionKey = (String) exchange.getIn().getHeader(PARTITION_KEY); + if (partitionKey != null) { + span.setAttribute("partitionKey", partitionKey); + } + + String key = (String) exchange.getIn().getHeader(KEY); + if (key != null) { + span.setAttribute("key", key); + } + + String offset = getValue(exchange, OFFSET, Long.class); + if (offset != null) { + span.setAttribute("offset", offset); + } + } + + /** + * Extracts header value from the exchange for given header + * + * @param exchange the {@link Exchange} + * @param header the header name + * @param type the class type of the exchange header + * @return + */ + private String getValue(final Exchange exchange, final String header, Class type) { + T value = exchange.getIn().getHeader(header, type); + return value != null ? String.valueOf(value) : exchange.getIn().getHeader(header, String.class); + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/LogSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/LogSpanDecorator.java new file mode 100644 index 0000000000..ddf60ea6d4 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/LogSpanDecorator.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +class LogSpanDecorator extends BaseSpanDecorator { + + @Override + public boolean shouldStartNewSpan() { + return false; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java new file mode 100644 index 0000000000..5947f0f767 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Span.Kind; +import io.opentelemetry.api.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import java.net.URI; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class MessagingSpanDecorator extends BaseSpanDecorator { + + private final String component; + + public MessagingSpanDecorator(String component) { + this.component = component; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + + switch (component) { + case "mqtt": + return stripSchemeAndOptions(endpoint); + } + return getDestination(exchange, endpoint); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, getDestination(exchange, endpoint)); + + String messageId = getMessageId(exchange); + if (messageId != null) { + span.setAttribute(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId); + } + } + + /** + * This method identifies the destination from the supplied exchange and/or endpoint. + * + * @param exchange The exchange + * @param endpoint The endpoint + * @return The message bus destination + */ + protected String getDestination(Exchange exchange, Endpoint endpoint) { + switch (component) { + case "cometds": + case "cometd": + return URI.create(endpoint.getEndpointUri()).getPath().substring(1); + case "rabbitmq": + return (String) exchange.getIn().getHeader("rabbitmq.EXCHANGE_NAME"); + case "stomp": + { + String destination = stripSchemeAndOptions(endpoint); + if (destination.startsWith("queue:")) { + destination = destination.substring("queue:".length()); + } + return destination; + } + case "mqtt": + { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + return (queryParameters.containsKey("subscribeTopicNames") + ? queryParameters.get("subscribeTopicNames") + : queryParameters.get("publishTopicName")); + } + } + return stripSchemeAndOptions(endpoint); + } + + @Override + public Span.Kind getInitiatorSpanKind() { + return Kind.PRODUCER; + } + + @Override + public Span.Kind getReceiverSpanKind() { + return Kind.CONSUMER; + } + + /** + * This method identifies the message id for the messaging exchange. + * + * @return The message id, or null if no id exists for the exchange + */ + protected String getMessageId(Exchange exchange) { + switch (component) { + case "aws-sns": + return (String) exchange.getIn().getHeader("CamelAwsSnsMessageId"); + case "aws-sqs": + return (String) exchange.getIn().getHeader("CamelAwsSqsMessageId"); + case "ironmq": + return (String) exchange.getIn().getHeader("CamelIronMQMessageId"); + case "jms": + return (String) exchange.getIn().getHeader("JMSMessageID"); + } + return null; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/RestSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/RestSpanDecorator.java new file mode 100644 index 0000000000..8b2f278efe --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/RestSpanDecorator.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class RestSpanDecorator extends HttpSpanDecorator { + + private static final Logger LOG = LoggerFactory.getLogger(RestSpanDecorator.class); + + @Override + protected String getPath(Exchange exchange, Endpoint endpoint) { + String endpointUri = endpoint.getEndpointUri(); + // Obtain the 'path' part of the URI format: rest://method:path[:uriTemplate]?[options] + String path = null; + int index = endpointUri.indexOf(':'); + if (index != -1) { + index = endpointUri.indexOf(':', index + 1); + if (index != -1) { + path = endpointUri.substring(index + 1); + index = path.indexOf('?'); + if (index != -1) { + path = path.substring(0, index); + } + path = path.replaceAll(":", ""); + try { + path = URLDecoder.decode(path, "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.debug("Failed to decode URL path '" + path + "', ignoring exception", e); + } + } + } + return path; + } +} diff --git a/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/TimerSpanDecorator.java b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/TimerSpanDecorator.java new file mode 100644 index 0000000000..402fbb7d61 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/TimerSpanDecorator.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * 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 io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class TimerSpanDecorator extends BaseSpanDecorator { + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + Object name = exchange.getProperty(Exchange.TIMER_NAME); + if (name instanceof String) { + return (String) name; + } + + return super.getOperationName(exchange, endpoint, camelDirection); + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/DirectCamelTest.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/DirectCamelTest.groovy new file mode 100644 index 0000000000..371db0c7ce --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/DirectCamelTest.groovy @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.Span.Kind.INTERNAL + +import io.opentelemetry.instrumentation.test.AgentTestRunner +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class DirectCamelTest extends AgentTestRunner { + + @Shared + ConfigurableApplicationContext server + + def setupSpec() { + def app = new SpringApplication(DirectConfig) + server = app.run() + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "simple direct to a single services"() { + setup: + def camelContext = server.getBean(CamelContext) + ProducerTemplate template = camelContext.createProducerTemplate() + + when: + template.sendBody("direct:input", "Example request") + + then: + assertTraces(1) { + trace(0, 2) { + def parent = it + it.span(0) { + name "input" + kind INTERNAL + hasNoParent() + attributes { + "camel.uri" "direct://input" + } + } + it.span(1) { + name "receiver" + kind INTERNAL + parentSpanId parent.span(0).spanId + attributes { + "camel.uri" "direct://receiver" + } + } + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/DirectConfig.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/DirectConfig.groovy new file mode 100644 index 0000000000..f10867085d --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/DirectConfig.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class DirectConfig { + + @Bean + RouteBuilder receiverRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:receiver") + .log(LoggingLevel.INFO, "test","RECEIVER got: \${body}") + .delay(simple("2000")) + .setBody(constant("result")) + } + } + } + + @Bean + RouteBuilder clientRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test","SENDING request \${body}") + .to("direct:receiver") + .log(LoggingLevel.INFO, "test","RECEIVED response \${body}") + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/MulticastConfig.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/MulticastConfig.groovy new file mode 100644 index 0000000000..39e3f76bbc --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/MulticastConfig.groovy @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class MulticastConfig { + + @Bean + RouteBuilder firstServiceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:first") + .log(LoggingLevel.INFO, "test","FIRST request: \${body}") + .delay(simple("1000")) + .setBody(constant("first")) + } + } + } + + @Bean + RouteBuilder secondServiceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:second") + .log(LoggingLevel.INFO, "test","SECOND request: \${body}") + .delay(simple("2000")) + .setBody(constant("second")) + } + } + } + + @Bean + RouteBuilder clientServiceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test","SENDING request \${body}") + .multicast() + .parallelProcessing() + .to("direct:first", "direct:second") + .end() + .log(LoggingLevel.INFO, "test","RECEIVED response \${body}") + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/MulticastDirectCamelTest.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/MulticastDirectCamelTest.groovy new file mode 100644 index 0000000000..1e2b555394 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/MulticastDirectCamelTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.Span.Kind.INTERNAL + +import io.opentelemetry.instrumentation.test.AgentTestRunner +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class MulticastDirectCamelTest extends AgentTestRunner { + + @Shared + ConfigurableApplicationContext server + + def setupSpec() { + def app = new SpringApplication(MulticastConfig) + server = app.run() + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "parallel multicast to two child services"() { + setup: + def camelContext = server.getBean(CamelContext) + ProducerTemplate template = camelContext.createProducerTemplate() + + when: + template.sendBody("direct:input", "Example request") + + then: + assertTraces(1) { + trace(0, 3) { + def parent = it + it.span(0) { + name "input" + kind INTERNAL + hasNoParent() + attributes { + "camel.uri" "direct://input" + } + } + it.span(1) { + name "second" + kind INTERNAL + parentSpanId parent.span(0).spanId + attributes { + "camel.uri" "direct://second" + } + } + it.span(2) { + name "first" + kind INTERNAL + parentSpanId parent.span(0).spanId + attributes { + "camel.uri" "direct://first" + } + } + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/RestCamelTest.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/RestCamelTest.groovy new file mode 100644 index 0000000000..35bd772208 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/RestCamelTest.groovy @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.Span.Kind.CLIENT +import static io.opentelemetry.api.trace.Span.Kind.INTERNAL +import static io.opentelemetry.api.trace.Span.Kind.SERVER + +import com.google.common.collect.ImmutableMap +import io.opentelemetry.instrumentation.test.AgentTestRunner +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.api.trace.attributes.SemanticAttributes +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class RestCamelTest extends AgentTestRunner { + + @Shared + ConfigurableApplicationContext server + @Shared + int port + + def setupSpec() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + port = PortUtils.randomOpenPort() + def app = new SpringApplication(RestConfig) + app.setDefaultProperties(ImmutableMap.of("restServer.port", port)) + server = app.run() + println getClass().name + " http server started at: http://localhost:$port/" + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "rest component - server and client call with jetty backend"() { + setup: + def camelContext = server.getBean(CamelContext) + ProducerTemplate template = camelContext.createProducerTemplate() + + when: + // run client and server in separate threads to simulate "real" rest client/server call + new Thread(new Runnable() { + @Override + void run() { + template.sendBodyAndHeaders("direct:start", null, ImmutableMap.of("module", "firstModule", "unitId", "unitOne")) + } + } + ).start() + + then: + assertTraces(1) { + trace(0, 5) { + it.span(0) { + name "start" + kind INTERNAL + attributes { + "camel.uri" "direct://start" + } + } + it.span(1) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "camel.uri" "rest://get:api/%7Bmodule%7D/unit/%7BunitId%7D" + } + } + it.span(2) { + name "/api/{module}/unit/{unitId}" + kind SERVER + attributes { + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$port/api/firstModule/unit/unitOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$SemanticAttributes.HTTP_CLIENT_IP.key" "127.0.0.1" + "$SemanticAttributes.HTTP_USER_AGENT.key" "Jetty/9.3.21.v20170918" + "$SemanticAttributes.HTTP_FLAVOR.key" "HTTP/1.1" + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_PORT.key" Long + } + } + it.span(3) { + name "/api/{module}/unit/{unitId}" + kind INTERNAL + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$port/api/firstModule/unit/unitOne" + "camel.uri" String + } + } + it.span(4) { + name "moduleUnit" + kind INTERNAL + attributes { + "camel.uri" "direct://moduleUnit" + } + } + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/RestConfig.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/RestConfig.groovy new file mode 100644 index 0000000000..3e381b8fb3 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/RestConfig.groovy @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.rest.RestBindingMode +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class RestConfig { + + @Bean + RouteBuilder routes() { + return new RouteBuilder() { + @Override + void configure() throws Exception { + + restConfiguration() + .component("jetty") + .bindingMode(RestBindingMode.auto) + .host("localhost") + .port("{{restServer.port}}") + + rest("/api") + .get("/{module}/unit/{unitId}") + .to("direct:moduleUnit") + + from("direct:moduleUnit") + .transform().simple("\${header.unitId} of \${header.module}") + + // producer - client route + from("direct:start") + .log(LoggingLevel.INFO, "test","SENDING request") + .to("rest:get:api/{module}/unit/{unitId}") + .log(LoggingLevel.INFO, "test","RECEIVED response: '\${body}'") + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/SingleServiceCamelTest.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/SingleServiceCamelTest.groovy new file mode 100644 index 0000000000..0f67ee172c --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/SingleServiceCamelTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.Span.Kind.SERVER + +import com.google.common.collect.ImmutableMap +import io.opentelemetry.instrumentation.test.AgentTestRunner +import io.opentelemetry.instrumentation.test.utils.OkHttpUtils +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.api.trace.attributes.SemanticAttributes +import okhttp3.FormBody +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class SingleServiceCamelTest extends AgentTestRunner { + + @Shared + ConfigurableApplicationContext server + @Shared + OkHttpClient client = OkHttpUtils.client() + @Shared + int port + @Shared + URI address + + def setupSpec() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + port = PortUtils.randomOpenPort() + address = new URI("http://localhost:$port/") + def app = new SpringApplication(SingleServiceConfig) + app.setDefaultProperties(ImmutableMap.of("camelService.port", port)) + server = app.run() + println getClass().name + " http server started at: http://localhost:$port/" + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "single camel service span"() { + setup: + def requestUrl = address.resolve("/camelService") + def url = HttpUrl.get(requestUrl) + def request = new Request.Builder() + .url(url) + .method("POST", + new FormBody.Builder().add("", "testContent").build()) + .build() + + when: + client.newCall(request).execute() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + kind SERVER + name "/camelService" + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "${address.resolve("/camelService")}" + "camel.uri" "${address.resolve("/camelService")}".replace("localhost", "0.0.0.0") + } + } + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/SingleServiceConfig.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/SingleServiceConfig.groovy new file mode 100644 index 0000000000..97f7386c24 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/SingleServiceConfig.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class SingleServiceConfig { + + @Bean + RouteBuilder serviceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + + from("undertow:http://0.0.0.0:{{camelService.port}}/camelService") + .routeId("camelService") + .streamCaching() + .log("CamelService request: \${body}") + .delay(simple("\${random(1000, 2000)}")) + .transform(simple("CamelService-\${body}")) + .log(LoggingLevel.INFO, "test", "CamelService response: \${body}") + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/TwoServicesConfig.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/TwoServicesConfig.groovy new file mode 100644 index 0000000000..67f706ca57 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/TwoServicesConfig.groovy @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class TwoServicesConfig { + + @Bean + RouteBuilder serviceOneRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + + from("undertow:http://0.0.0.0:{{service.one.port}}/serviceOne") + .routeId("serviceOne") + .streamCaching() + .removeHeaders("CamelHttp*") + .log("Service One request: \${body}") + .delay(simple("\${random(1000,2000)}")) + .transform(simple("Service-One-\${body}")) + .to("http://0.0.0.0:{{service.two.port}}/serviceTwo") + .log("Service One response: \${body}") + } + } + } + + @Bean + RouteBuilder serviceTwoRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + + from("jetty:http://0.0.0.0:{{service.two.port}}/serviceTwo?arg=value") + .routeId("serviceTwo") + .streamCaching() + .log("Service Two request: \${body}") + .delay(simple("\${random(1000, 2000)}")) + .transform(simple("Service-Two-\${body}")) + .log("Service Two response: \${body}") + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/groovy/test/TwoServicesWithDirectClientCamelTest.groovy b/instrumentation/apache-camel-2.20/src/test/groovy/test/TwoServicesWithDirectClientCamelTest.groovy new file mode 100644 index 0000000000..2888df8a28 --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/groovy/test/TwoServicesWithDirectClientCamelTest.groovy @@ -0,0 +1,171 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.Span.Kind.CLIENT +import static io.opentelemetry.api.trace.Span.Kind.INTERNAL +import static io.opentelemetry.api.trace.Span.Kind.SERVER + +import com.google.common.collect.ImmutableMap +import io.opentelemetry.instrumentation.test.AgentTestRunner +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.api.trace.attributes.SemanticAttributes +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.impl.DefaultCamelContext +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class TwoServicesWithDirectClientCamelTest extends AgentTestRunner { + + @Shared + int portOne + @Shared + int portTwo + @Shared + ConfigurableApplicationContext server + @Shared + CamelContext clientContext + + def setupSpec() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + portOne = PortUtils.randomOpenPort() + portTwo = PortUtils.randomOpenPort() + def app = new SpringApplication(TwoServicesConfig) + app.setDefaultProperties(ImmutableMap.of("service.one.port", portOne, "service.two.port", portTwo)) + server = app.run() + } + + def createAndStartClient() { + clientContext = new DefaultCamelContext() + clientContext.addRoutes(new RouteBuilder() { + void configure() { + from("direct:input") + .log("SENT Client request") + .to("http://localhost:$portOne/serviceOne") + .log("RECEIVED Client response") + } + }) + clientContext.start() + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "two camel service spans"() { + setup: + createAndStartClient() + ProducerTemplate template = clientContext.createProducerTemplate() + + when: + template.sendBody("direct:input", "Example request") + + then: + assertTraces(1) { + trace(0, 8) { + it.span(0) { + name "input" + kind INTERNAL + attributes { + "camel.uri" "direct://input" + } + } + it.span(1) { + name "POST" + kind CLIENT + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "camel.uri" "http://localhost:$portOne/serviceOne" + } + } + it.span(2) { + name "HTTP POST" + kind CLIENT + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" portOne + "$SemanticAttributes.NET_TRANSPORT.key" "IP.TCP" + "$SemanticAttributes.HTTP_FLAVOR.key" "1.1" + } + } + it.span(3) { + name "/serviceOne" + kind SERVER + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "camel.uri" "http://0.0.0.0:$portOne/serviceOne" + } + } + it.span(4) { + name "POST" + kind CLIENT + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "camel.uri" "http://0.0.0.0:$portTwo/serviceTwo" + } + } + it.span(5) { + name "HTTP POST" + kind CLIENT + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$SemanticAttributes.NET_PEER_NAME.key" "0.0.0.0" + "$SemanticAttributes.NET_PEER_PORT.key" portTwo + "$SemanticAttributes.NET_TRANSPORT.key" "IP.TCP" + "$SemanticAttributes.HTTP_FLAVOR.key" "1.1" + "$SemanticAttributes.HTTP_USER_AGENT.key" "Jakarta Commons-HttpClient/3.1" + } + } + it.span(6) { + name "/serviceTwo" + kind SERVER + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo" + "$SemanticAttributes.NET_PEER_PORT.key" Number + "$SemanticAttributes.NET_PEER_IP.key" InetAddress.getLocalHost().getHostAddress().toString() + "$SemanticAttributes.HTTP_USER_AGENT.key" "Jakarta Commons-HttpClient/3.1" + "$SemanticAttributes.HTTP_FLAVOR.key" "HTTP/1.1" + "$SemanticAttributes.HTTP_CLIENT_IP.key" InetAddress.getLocalHost().getHostAddress().toString() + + } + } + it.span(7) { + name "/serviceTwo" + kind INTERNAL + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo" + "camel.uri" "jetty:http://0.0.0.0:$portTwo/serviceTwo?arg=value" + } + } + } + } + } +} diff --git a/instrumentation/apache-camel-2.20/src/test/resources/logback.xml b/instrumentation/apache-camel-2.20/src/test/resources/logback.xml new file mode 100644 index 0000000000..8aab504caf --- /dev/null +++ b/instrumentation/apache-camel-2.20/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 4fb41830ba..0d179297ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -55,6 +55,7 @@ include ':smoke-tests' include ':instrumentation:akka-context-propagation-2.5' include ':instrumentation:akka-http-10.0' +include ':instrumentation:apache-camel-2.20' include ':instrumentation:apache-httpasyncclient-4.0' include ':instrumentation:apache-httpclient:apache-httpclient-2.0' include ':instrumentation:apache-httpclient:apache-httpclient-4.0'