GERONIMO-6737 adding TracingProxyFactory
diff --git a/README.adoc b/README.adoc index cc3ddc9..86fd096 100644 --- a/README.adoc +++ b/README.adoc
@@ -39,3 +39,8 @@ == ExecutorServices To monitor a CDI executor service, you can decorate it with `@TracedExecutorService`. + +== Extension module + +`geronimo-opentracing-extension` provides a `TracingProxyFactory` module allowing to use java proxies (on interfaces) +to add tracing to all methods of the decorated instance. \ No newline at end of file
diff --git a/geronimo-opentracing-extension/pom.xml b/geronimo-opentracing-extension/pom.xml new file mode 100644 index 0000000..2b6dc85 --- /dev/null +++ b/geronimo-opentracing-extension/pom.xml
@@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>geronimo-opentracing-parent</artifactId> + <groupId>org.apache.geronimo</groupId> + <version>1.0.3-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>geronimo-opentracing-extension</artifactId> + <name>Geronimo OpenTracing :: Common</name> + <description>Implementation environment independent (no CDI).</description> + <packaging>bundle</packaging> + + <properties> + <geronimo-opentracing.Automatic-Module-Name>org.apache.geronimo.opentracing.extension</geronimo-opentracing.Automatic-Module-Name> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jcdi_2.0_spec</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.geronimo</groupId> + <artifactId>geronimo-opentracing</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> \ No newline at end of file
diff --git a/geronimo-opentracing-extension/src/main/java/org/apache/geronimo/opentracing/extension/proxy/TracingProxyFactory.java b/geronimo-opentracing-extension/src/main/java/org/apache/geronimo/opentracing/extension/proxy/TracingProxyFactory.java new file mode 100644 index 0000000..b209ef6 --- /dev/null +++ b/geronimo-opentracing-extension/src/main/java/org/apache/geronimo/opentracing/extension/proxy/TracingProxyFactory.java
@@ -0,0 +1,134 @@ +/* + * 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.geronimo.opentracing.extension.proxy; + +import static java.util.Collections.emptyMap; +import static java.util.Optional.ofNullable; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; + +public class TracingProxyFactory { + public <T> T decorate(final Tracer tracer, final T instance) { + return decorate(tracer, instance, emptyMap()); + } + + public <T> T decorate(final Tracer tracer, final T instance, final Map<String, String> tags) { + final Class<?>[] interfaces = instance.getClass().getInterfaces(); + if (interfaces.length == 0) { + throw new IllegalArgumentException("Can't determine the API to proxy: " + instance); + } + final Class<T> mainApi = (Class<T>) interfaces[0]; + final Class<?>[] otherApis = interfaces.length == 1 ? + new Class<?>[0] : Stream.of(interfaces).skip(1).toArray(Class[]::new); + return decorate(tracer, instance, mainApi, tags, otherApis); + } + + public <T> T decorate(final Tracer tracer, + final T instance, + final Class<T> mainApi, + final Map<String, String> tags, + final Class<?>... otherApis) { + return mainApi.cast(Proxy.newProxyInstance( + ofNullable(Thread.currentThread().getContextClassLoader()).orElseGet(ClassLoader::getSystemClassLoader), + Stream.concat(Stream.of(mainApi), Stream.of(otherApis)).toArray(Class[]::new), + new TracingHandler(instance, tracer, tags))); + } + + private static class TracingHandler implements InvocationHandler, Serializable { + private final Object delegate; + private final Tracer tracer; + private final Map<String, String> tags; + + private TracingHandler(final Object delegate, final Tracer tracer, final Map<String, String> tags) { + this.delegate = delegate; + this.tracer = tracer; + this.tags = tags; + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + final Tracer.SpanBuilder builder = tracer.buildSpan(method.getDeclaringClass().getName() + "." + method.getName()); + builder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER); + builder.withTag(Tags.COMPONENT.getKey(), "proxy"); + tags.forEach(builder::withTag); + ofNullable(tracer.activeSpan()).ifPresent(builder::asChildOf); + + final Scope scope = builder.startActive(false /*just handle span inheritance for async case*/); + boolean doFinish = true; + try { + final Object result = method.invoke(delegate, args); + if (CompletionStage.class.isInstance(result)) { + doFinish = false; + final CompletionStage<?> stage = CompletionStage.class.cast(result); + return stage.handle((r, e) -> { + try { + if (e != null) { + onError(scope, e); + return rethrow(e); + } + return r; + } finally { + scope.span().finish(); + } + }); + } + return result; + } catch (final InvocationTargetException ite) { + onError(scope, ite.getTargetException()); + throw ite.getTargetException(); + } finally { + if (doFinish) { + scope.span().finish(); + } + scope.close(); + } + } + + private Object rethrow(final Throwable e) { + if (RuntimeException.class.isInstance(e)) { + throw RuntimeException.class.cast(e); + } + if (Error.class.isInstance(e)) { + throw Error.class.cast(e); + } + throw new IllegalStateException(e); + } + + private void onError(final Scope scope, final Throwable e) { + final Span span = scope.span(); + Tags.ERROR.set(span, true); + + final Map<String, Object> logs = new LinkedHashMap<>(); + logs.put("event", Tags.ERROR.getKey()); + logs.put("error.object", e); + span.log(logs); + } + } +}
diff --git a/geronimo-opentracing-extension/src/test/java/org/apache/geronimo/opentracing/extension/proxy/TracingProxyFactoryTest.java b/geronimo-opentracing-extension/src/test/java/org/apache/geronimo/opentracing/extension/proxy/TracingProxyFactoryTest.java new file mode 100644 index 0000000..e98facd --- /dev/null +++ b/geronimo-opentracing-extension/src/test/java/org/apache/geronimo/opentracing/extension/proxy/TracingProxyFactoryTest.java
@@ -0,0 +1,113 @@ +/* + * 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.geronimo.opentracing.extension.proxy; + +import static java.util.stream.Collectors.joining; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import java.util.ArrayList; +import java.util.Collection; + +import org.apache.geronimo.microprofile.opentracing.common.config.GeronimoOpenTracingConfig; +import org.apache.geronimo.microprofile.opentracing.common.impl.FinishedSpan; +import org.apache.geronimo.microprofile.opentracing.common.impl.GeronimoTracer; +import org.apache.geronimo.microprofile.opentracing.common.impl.IdGenerator; +import org.apache.geronimo.microprofile.opentracing.common.impl.ScopeManagerImpl; +import org.apache.geronimo.microprofile.opentracing.common.impl.SpanImpl; +import org.testng.annotations.Test; + +public class TracingProxyFactoryTest { + @Test + public void proxy() { + final Collection<FinishedSpan> spans = new ArrayList<>(); + final GeronimoOpenTracingConfig config = (value, def) -> def; + IdGenerator generator = new IdGenerator(); + generator.setConfig(config); + generator.init(); + final GeronimoTracer tracer = new GeronimoTracer(); + tracer.setConfig(config); + tracer.setIdGenerator(generator); + tracer.setScopeManager(new ScopeManagerImpl()); + tracer.setFinishedSpanEvent(spans::add); + tracer.init(); + + final Api wrapped = new TracingProxyFactory() + .decorate(tracer, new Api() { + @Override + public String ok() { + return "yeah"; + } + + @Override + public void error() { + throw new IllegalStateException("expected error"); + } + + @Override + public String foo(final String bar) { + return "other/" + bar; + } + }); + { + assertEquals("yeah", wrapped.ok()); + assertSpan(spans, "org.apache.geronimo.opentracing.extension.proxy.TracingProxyFactoryTest$Api.ok"); + spans.clear(); + } + { + assertEquals("other/something", wrapped.foo("something")); + assertSpan(spans, "org.apache.geronimo.opentracing.extension.proxy.TracingProxyFactoryTest$Api.foo"); + spans.clear(); + } + { + try { + wrapped.error(); + fail(); + } catch (final IllegalStateException ise) { + // no-op + } + assertSpan(spans, "org.apache.geronimo.opentracing.extension.proxy.TracingProxyFactoryTest$Api.error"); + final SpanImpl span = toSpanImpl(spans); + assertEquals(Boolean.TRUE, span.getTags().get("error")); + assertEquals( + "error.object=java.lang.IllegalStateException: expected error\nevent=error", + span.getLogs().stream() + .map(SpanImpl.Log::getFields) + .flatMap(m -> m.entrySet().stream()) + .map(it -> it.getKey() + "=" + it.getValue()) + .sorted() + .collect(joining("\n"))); + spans.clear(); + } + } + + private void assertSpan(final Collection<FinishedSpan> spans, final String operation) { + assertEquals(1, spans.size()); + final SpanImpl span = toSpanImpl(spans); + assertEquals(operation, span.getName()); + } + + private SpanImpl toSpanImpl(final Collection<FinishedSpan> spans) { + return SpanImpl.class.cast(spans.iterator().next().getSpan()); + } + + public interface Api { + String ok(); + void error(); + String foo(String bar); + } +}
diff --git a/geronimo-opentracing/pom.xml b/geronimo-opentracing/pom.xml index 15dbac0..0d752b3 100644 --- a/geronimo-opentracing/pom.xml +++ b/geronimo-opentracing/pom.xml
@@ -35,8 +35,6 @@ <dependency> <groupId>org.apache.geronimo.specs</groupId> <artifactId>geronimo-jcdi_2.0_spec</artifactId> - <version>1.0.1</version> - <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.geronimo.specs</groupId>
diff --git a/pom.xml b/pom.xml index 06d8d07..a3efbe6 100644 --- a/pom.xml +++ b/pom.xml
@@ -94,10 +94,22 @@ </dependency> </dependencies> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jcdi_2.0_spec</artifactId> + <version>1.1</version> + <scope>provided</scope> + </dependency> + </dependencies> + </dependencyManagement> + <modules> <module>geronimo-opentracing-common</module> <module>geronimo-opentracing</module> <module>geronimo-opentracing-osgi</module> + <module>geronimo-opentracing-extension</module> </modules> <build>