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>