[GERONIMO-6782] implement specification v3.0-RC2
diff --git a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/RegistryImpl.java b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/RegistryImpl.java
index 9aa2d29..6da5a7c 100644
--- a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/RegistryImpl.java
+++ b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/RegistryImpl.java
@@ -41,6 +41,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.function.Supplier;
 
 import static java.util.stream.Collectors.toCollection;
 import static java.util.stream.Collectors.toMap;
@@ -49,10 +51,20 @@
     private static final Tag[] NO_TAG = new Tag[0];
 
     private final Type type;
+    private final Tag[] globalTags;
     private final ConcurrentMap<MetricID, Holder<? extends Metric>> metrics = new ConcurrentHashMap<>();
 
     public RegistryImpl(final Type type) {
+        this(type, new Tag[0]);
+    }
+
+    public RegistryImpl(final Type type, final Tag[] globalTags) {
         this.type = type;
+        this.globalTags = globalTags;
+    }
+
+    public Tag[] getGlobalTags() {
+        return globalTags;
     }
 
     @Override
@@ -63,12 +75,9 @@
     @Override
     public <T extends Metric> T register(final Metadata metadata, final T metric, final Tag... tags) throws IllegalArgumentException {
         final MetricID metricID = new MetricID(metadata.getName(), tags);
-        final Holder<? extends Metric> holder = metrics.putIfAbsent(
+        final Holder<? extends Metric> present = metrics.putIfAbsent(
                 metricID, new Holder<>(metric, metadata, metricID));
-        if (holder != null && !metadata.isReusable() && !holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("'" + metadata.getName() + "' metric already exists and is not reusable");
-        }
-        return metric;
+        return present != null ? (T) present.metric : metric;
     }
 
     @Override
@@ -123,15 +132,11 @@
         Holder<? extends Metric> holder = metrics.get(metricID);
         if (holder == null) {
             holder = new Holder<>(new CounterImpl(
-                    metadata.getUnit().orElse(MetricUnits.NONE)), metadata, metricID);
+                    metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit()), enforceType(metadata, MetricType.COUNTER), metricID);
             final Holder<? extends Metric> existing = metrics.putIfAbsent(holder.metricID, holder);
             if (existing != null) {
                 holder = existing;
             }
-        } else if (!metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and is not set as reusable");
-        } else if (!holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and was not set as reusable");
         }
         if (!Counter.class.isInstance(holder.metric)) {
             throw new IllegalArgumentException(holder.metric + " is not a counter");
@@ -170,43 +175,80 @@
         Holder<? extends Metric> holder = metrics.get(metricID);
         if (holder == null) {
             holder = new Holder<>(new ConcurrentGaugeImpl(
-                    metadata.getUnit().orElse(MetricUnits.NONE)), metadata, metricID);
+                    metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit()), enforceType(metadata, MetricType.CONCURRENT_GAUGE), metricID);
             final Holder<? extends Metric> existing = metrics.putIfAbsent(holder.metricID, holder);
             if (existing != null) {
                 holder = existing;
             }
-        } else if (!metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and is not set as reusable");
-        } else if (!holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and was not set as reusable");
         }
         if (!ConcurrentGauge.class.isInstance(holder.metric)) {
-            throw new IllegalArgumentException(holder.metric + " is not a counter");
+            throw new IllegalArgumentException(holder.metric + " is not a concurrent gauge");
         }
         return ConcurrentGauge.class.cast(holder.metric);
     }
 
     @Override
+    public <T, R extends Number> Gauge<R> gauge(final String name, final T object, final Function<T, R> func, Tag... tags) {
+        return gauge(new MetricID(name, tags), () -> func.apply(object));
+    }
+
+    @Override
+    public <T, R extends Number> Gauge<R> gauge(final MetricID metricID, final T object, final Function<T, R> func) {
+        return gauge(metricID, () -> func.apply(object));
+    }
+
+    @Override
+    public <T, R extends Number> Gauge<R> gauge(final Metadata metadata, final T object, final Function<T, R> func, final Tag... tags) {
+        final MetricID metricID = new MetricID(metadata.getName(), tags);
+        Holder<? extends Metric> holder = metrics.get(metricID);
+        if (holder == null) {
+            holder = new Holder<>(new SimpleGaugeImpl<>(() -> func.apply(object)), enforceType(metadata, MetricType.GAUGE), metricID);
+            final Holder<? extends Metric> existing = metrics.putIfAbsent(holder.metricID, holder);
+            if (existing != null) {
+                holder = existing;
+            }
+        }
+        if (!Gauge.class.isInstance(holder.metric)) {
+            throw new IllegalArgumentException(holder.metric + " is not a gauge");
+        }
+        return Gauge.class.cast(holder.metric);
+    }
+
+    @Override
+    public <T extends Number> Gauge<T> gauge(final String name, final Supplier<T> supplier, final Tag... tags) {
+        return gauge(new MetricID(name, tags), supplier);
+    }
+
+    @Override
+    public <T extends Number> Gauge<T> gauge(final MetricID metricID, final Supplier<T> supplier) {
+        Holder<? extends Metric> holder = metrics.get(metricID);
+        if (holder == null) {
+            holder = new Holder<>(
+                    new SimpleGaugeImpl<>(supplier),
+                    Metadata.builder().withName(metricID.getName()).withType(MetricType.GAUGE).build(),
+                    metricID);
+            final Holder<? extends Metric> existing = metrics.putIfAbsent(holder.metricID, holder);
+            if (existing != null) {
+                holder = existing;
+            }
+        }
+        if (!Gauge.class.isInstance(holder.metric)) {
+            throw new IllegalArgumentException(holder.metric + " is not a gauge");
+        }
+        return Gauge.class.cast(holder.metric);
+    }
+
+    @Override
+    public <T extends Number> Gauge<T> gauge(final Metadata metadata, final Supplier<T> supplier, final Tag... tags) {
+        return register(metadata, new SimpleGaugeImpl<>(supplier), tags);
+    }
+
+    @Override
     public Gauge<?> getGauge(MetricID metricID) {
         return getMetric(metricID, Gauge.class);
     }
 
     @Override
-    public Gauge<?> gauge(String name, Gauge<?> gauge) {
-        return gauge(new MetricID(name), gauge);
-    }
-
-    @Override
-    public Gauge<?> gauge(final String name, final Gauge<?> gauge, final Tag...tags) {
-        return register(Metadata.builder().reusable().withName(name).withType(MetricType.GAUGE).build(), gauge, tags);
-    }
-
-    @Override
-    public Gauge<?> gauge(final MetricID metricID, final Gauge<?> gauge) {
-        return gauge(metricID.getName(), gauge, metricID.getTagsAsArray());
-    }
-
-    @Override
     public Histogram getHistogram(MetricID metricID) {
         return this.getMetric(metricID, Histogram.class);
     }
@@ -236,15 +278,11 @@
         final MetricID metricID = new MetricID(metadata.getName(), tags);
         Holder<? extends Metric> holder = metrics.get(metricID);
         if (holder == null) {
-            holder = new Holder<>(new HistogramImpl(metadata.getUnit().orElse(MetricUnits.NONE)), metadata, metricID);
+            holder = new Holder<>(new HistogramImpl(metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit()), enforceType(metadata, MetricType.HISTOGRAM), metricID);
             final Holder<? extends Metric> existing = metrics.putIfAbsent(metricID, holder);
             if (existing != null) {
                 holder = existing;
             }
-        } else if (!metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and is not set as reusable");
-        } else if (!holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and was not set as reusable");
         }
         if (!Histogram.class.isInstance(holder.metric)) {
             throw new IllegalArgumentException(holder.metric + " is not a histogram");
@@ -282,15 +320,11 @@
         final MetricID metricID = new MetricID(metadata.getName(), tags);
         Holder<? extends Metric> holder = metrics.get(metricID);
         if (holder == null) {
-            holder = new Holder<>(new MeterImpl(metadata.getUnit().orElse(MetricUnits.NONE)), metadata, metricID);
+            holder = new Holder<>(new MeterImpl(metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit()), enforceType(metadata, MetricType.METERED), metricID);
             final Holder<? extends Metric> existing = metrics.putIfAbsent(metricID, holder);
             if (existing != null) {
                 holder = existing;
             }
-        } else if (!metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and is not set as reusable");
-        } else if (!holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and was not set as reusable");
         }
         if (!Meter.class.isInstance(holder.metric)) {
             throw new IllegalArgumentException(holder.metric + " is not a meter");
@@ -314,7 +348,7 @@
     }
 
     @Override
-    public SimpleTimer simpleTimer(String name) {
+    public SimpleTimer simpleTimer(final String name) {
         return simpleTimer(new MetricID(name));
     }
 
@@ -328,15 +362,11 @@
         final MetricID metricID = new MetricID(metadata.getName(), tags);
         Holder<? extends Metric> holder = metrics.get(metricID);
         if (holder == null) {
-            holder = new Holder<>(new SimpleTimerImpl(metadata.getUnit().orElse(MetricUnits.NONE)), metadata, metricID);
+            holder = new Holder<>(new SimpleTimerImpl(metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit()), enforceType(metadata, MetricType.SIMPLE_TIMER), metricID);
             final Holder<? extends Metric> existing = metrics.putIfAbsent(metricID, holder);
             if (existing != null) {
                 holder = existing;
             }
-        } else if (!metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and is not set as reusable");
-        } else if (!holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and was not set as reusable");
         }
         if (!SimpleTimer.class.isInstance(holder.metric)) {
             throw new IllegalArgumentException(holder.metric + " is not a timer");
@@ -374,15 +404,11 @@
         final MetricID metricID = new MetricID(metadata.getName(), tags);
         Holder<? extends Metric> holder = metrics.get(metricID);
         if (holder == null) {
-            holder = new Holder<>(new TimerImpl(metadata.getUnit().orElse(MetricUnits.NONE)), metadata, metricID);
+            holder = new Holder<>(new TimerImpl(metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit()), enforceType(metadata, MetricType.TIMER), metricID);
             final Holder<? extends Metric> existing = metrics.putIfAbsent(metricID, holder);
             if (existing != null) {
                 holder = existing;
             }
-        } else if (!metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and is not set as reusable");
-        } else if (!holder.metadata.isReusable()) {
-            throw new IllegalArgumentException("Metric " + metadata.getName() + " already exists and was not set as reusable");
         }
         if (!Timer.class.isInstance(holder.metric)) {
             throw new IllegalArgumentException(holder.metric + " is not a timer");
@@ -397,9 +423,9 @@
     }
 
     @Override
-    public <T extends Metric> T getMetric(MetricID metricID, Class<T> asType) {
+    public <T extends Metric> T getMetric(final MetricID metricID, final Class<T> asType) {
         try {
-            return asType.cast(this.getMetric(metricID));
+            return asType.cast(getMetric(metricID));
         } catch (ClassCastException e) {
             throw new IllegalArgumentException(metricID + " was not of expected type " + asType, e);
         }
@@ -542,6 +568,13 @@
         return type;
     }
 
+    private Metadata enforceType(final Metadata metadata, final MetricType type) {
+        if (metadata.getTypeRaw() == null || !type.equals(metadata.getTypeRaw())) {
+            return Metadata.builder(metadata).withType(type).build();
+        }
+        return metadata;
+    }
+
     private <T extends Metric> SortedMap<MetricID, T> filterByType(final MetricFilter filter, final Class<T> type) {
         return metrics.entrySet().stream()
                 .filter(it -> type.isInstance(it.getValue().metric))
diff --git a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/SimpleGaugeImpl.java b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/SimpleGaugeImpl.java
new file mode 100644
index 0000000..61816ff
--- /dev/null
+++ b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/SimpleGaugeImpl.java
@@ -0,0 +1,34 @@
+/*
+ * 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.microprofile.metrics.common;
+
+import org.eclipse.microprofile.metrics.Gauge;
+
+import java.util.function.Supplier;
+
+public class SimpleGaugeImpl<T> implements Gauge<T> {
+    private final Supplier<T> supplier;
+
+    public SimpleGaugeImpl(final Supplier<T> supplier) {
+        this.supplier = supplier;
+    }
+
+    @Override
+    public T getValue() {
+        return supplier.get();
+    }
+}
diff --git a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java
index 6e38041..6fb088c 100644
--- a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java
+++ b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java
@@ -16,36 +16,7 @@
  */
 package org.apache.geronimo.microprofile.metrics.common.jaxrs;
 
-import static java.util.Collections.emptyMap;
-import static java.util.Collections.singletonMap;
-import static java.util.Optional.ofNullable;
-import static java.util.function.Function.identity;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toMap;
-
-import java.time.Duration;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.util.function.Function;
-import java.util.regex.Pattern;
-import java.util.stream.Stream;
-
-import javax.json.JsonValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.OPTIONS;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.SecurityContext;
-import javax.ws.rs.core.UriInfo;
-
+import org.apache.geronimo.microprofile.metrics.common.RegistryImpl;
 import org.apache.geronimo.microprofile.metrics.common.prometheus.PrometheusFormatter;
 import org.eclipse.microprofile.metrics.ConcurrentGauge;
 import org.eclipse.microprofile.metrics.Counter;
@@ -59,8 +30,37 @@
 import org.eclipse.microprofile.metrics.MetricRegistry;
 import org.eclipse.microprofile.metrics.SimpleTimer;
 import org.eclipse.microprofile.metrics.Snapshot;
+import org.eclipse.microprofile.metrics.Tag;
 import org.eclipse.microprofile.metrics.Timer;
 
+import javax.json.JsonValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import javax.ws.rs.core.UriInfo;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toMap;
+
 @Path("metrics")
 public class MetricsEndpoints {
     private final Pattern semicolon = Pattern.compile(";");
@@ -68,6 +68,7 @@
     private MetricRegistry baseRegistry;
     private MetricRegistry vendorRegistry;
     private MetricRegistry applicationRegistry;
+    private Tag[] globalTags = new Tag[0]; // ensure forgetting to call init() is tolerated for backward compatibility
 
     private SecurityValidator securityValidator = new SecurityValidator() {
         {
@@ -77,6 +78,16 @@
 
     private PrometheusFormatter prometheus = new PrometheusFormatter().enableOverriding();
 
+    protected void init() {
+        globalTags = Stream.of(baseRegistry, vendorRegistry, applicationRegistry)
+                .filter(RegistryImpl.class::isInstance)
+                .map(RegistryImpl.class::cast)
+                .findFirst()
+                .map(RegistryImpl::getGlobalTags)
+                .orElseGet(() -> new Tag[0]);
+        prometheus.withGlobalTags(globalTags);
+    }
+
     public void setBaseRegistry(final MetricRegistry baseRegistry) {
         this.baseRegistry = baseRegistry;
     }
@@ -197,7 +208,8 @@
 
     private MetricID findMetricId(final MetricRegistry metricRegistry, final Metadata value) {
         final Map<MetricID, Metric> metrics = metricRegistry.getMetrics();
-        final MetricID directKey = new MetricID(value.getName());
+        final MetricID directKey = RegistryImpl.class.isInstance(metricRegistry) && RegistryImpl.class.cast(metricRegistry).getGlobalTags().length > 0 ?
+                new MetricID(value.getName(), RegistryImpl.class.cast(metricRegistry).getGlobalTags()) : new MetricID(value.getName());
         if (metrics.containsKey(directKey)) {
             return directKey;
         }
@@ -225,7 +237,8 @@
 
     private <T> Map<String, T> singleEntry(final String id, final MetricRegistry metricRegistry,
                                            final Function<Metric, T> metricMapper) {
-        final MetricID key = new MetricID(id);
+        final MetricID key = RegistryImpl.class.isInstance(metricRegistry) && RegistryImpl.class.cast(metricRegistry).getGlobalTags().length > 0 ?
+                new MetricID(id, RegistryImpl.class.cast(metricRegistry).getGlobalTags()) : new MetricID(id);
         final Map<MetricID, Metric> metrics = metricRegistry.getMetrics();
         return ofNullable(metrics.get(key)) // try first without any tag (fast access)
                 .map(metric -> singletonMap(id + formatTags(key), metricMapper.apply(metric)))
@@ -235,7 +248,7 @@
     }
 
     private Meta mapMeta(final Metadata value, final MetricID metricID) {
-        return ofNullable(value).map(v -> new Meta(value, metricID)).orElse(null);
+        return ofNullable(value).map(v -> new Meta(value, metricID, globalTags)).orElse(null);
     }
 
     private Object map(final Metric metric) {
@@ -339,18 +352,22 @@
     }
 
     private String formatTags(final MetricID id) {
-        return id.getTags().isEmpty() ? "" : (';' + id.getTagsAsList().stream()
+        return id.getTags().isEmpty() && globalTags.length == 0 ? "" : (';' +
+                Stream.concat(id.getTagsAsList().stream(), Stream.of(globalTags))
                 .map(e -> e.getTagName() + "=" + semicolon.matcher(e.getTagValue()).replaceAll("_"))
+                .distinct()
                 .collect(joining(";")));
     }
 
     public static class Meta {
         private final Metadata value;
         private final MetricID metricID;
+        private final Tag[] globalTags;
 
-        private Meta(final Metadata value, final MetricID metricID) {
+        private Meta(final Metadata value, final MetricID metricID, final Tag[] globalTags) {
             this.value = value;
             this.metricID = metricID;
+            this.globalTags = globalTags;
         }
 
         public String getName() {
@@ -362,7 +379,7 @@
         }
 
         public String getDescription() {
-            return value.getDescription().orElse(null);
+            return value.getDescription();
         }
 
         public String getType() {
@@ -374,15 +391,15 @@
         }
 
         public String getUnit() {
-            return value.getUnit().orElse(null);
-        }
-
-        public boolean isReusable() {
-            return value.isReusable();
+            return value.getUnit();
         }
 
         public String getTags() { // not sure why tck expect it, sounds worse than native getTags for clients (array of key/values)
-            return metricID.getTags().entrySet().stream().map(e -> e.getKey() + '=' + e.getValue()).collect(joining(","));
+            return Stream.concat(
+                    metricID.getTags().entrySet().stream().map(e -> e.getKey() + '=' + e.getValue()),
+                    Stream.of(globalTags).map(e -> e.getTagName() + '=' + e.getTagValue()))
+                    .distinct()
+                    .collect(joining(","));
         }
     }
 }
diff --git a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java
index ddd0e8f..60fb345 100644
--- a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java
+++ b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java
@@ -60,6 +60,7 @@
     protected final Set<Object> validUnits;
     protected final Map<String, String> keyMapping = new HashMap<>();
     protected Predicate<String> prefixFilter = null;
+    protected Tag[] globalTags;
 
     public PrometheusFormatter() {
         validUnits = Stream.of(MetricUnits.class.getDeclaredFields())
@@ -80,6 +81,11 @@
         return this;
     }
 
+    public PrometheusFormatter withGlobalTags(final Tag[] globalTags) {
+        this.globalTags = globalTags;
+        return this;
+    }
+
     public PrometheusFormatter enableOverriding() {
         try (final InputStream source = Thread.currentThread().getContextClassLoader()
                 .getResourceAsStream("META-INF/geronimo-metrics/prometheus-mapping.properties")) {
@@ -130,7 +136,7 @@
                 })
                 .filter(it -> prefixFilter == null || prefixFilter.test(it.prometheusKey))
                 .map(entry -> {
-                    final List<Tag> tagsAsList = entry.metricID.getTagsAsList();
+                    final List<Tag> tagsAsList = getTags(entry);
                     switch (entry.metadata.getTypeRaw()) {
                         case COUNTER: {
                             String key = toPrometheusKey(entry.metadata);
@@ -181,6 +187,14 @@
                 .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append);
     }
 
+    private List<Tag> getTags(final Entry entry) {
+        return globalTags == null || globalTags.length == 0 ?
+                entry.metricID.getTagsAsList() :
+                Stream.concat(entry.metricID.getTagsAsList().stream(), Stream.of(globalTags))
+                    .distinct()
+                    .collect(toList());
+    }
+
     private StringBuilder histogram(final String registryKey, final Entry entry, final List<Tag> tagsAsList, final String keyBase, final String keyUnit, final Histogram histogram) {
         final String type = entry.metadata == null ? null : entry.metadata.getType();
         return new StringBuilder()
@@ -273,7 +287,7 @@
     }
 
     private String toUnitSuffix(final Metadata metadata, final boolean enforceValid) {
-        final String unit = enforceValid ? getValidUnit(metadata) : metadata.getUnit().orElse(MetricUnits.NONE);
+        final String unit = enforceValid ? getValidUnit(metadata) : (metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit());
         return MetricUnits.NONE.equalsIgnoreCase(unit) || (enforceValid && !validUnits.contains(unit)) ? "" : ("_" + toPrometheusUnit(unit));
     }
 
@@ -293,7 +307,7 @@
     }
 
     private String getValidUnit(final Metadata metadata) {
-        final String unit = metadata.getUnit().orElse(MetricUnits.NONE);
+        final String unit = metadata.getUnit() == null ? MetricUnits.NONE : metadata.getUnit();
         // for tck, we dont really want to prevent the user to add new units
         // we should likely just check it exists in MetricUnits constant but it is too restrictive
         if (unit.startsWith("jelly")) {
diff --git a/geronimo-metrics-extensions/geronimo-metrics-extension-common/src/main/java/org/apache/geronimo/microprofile/metrics/extension/common/MicroprofileMetricsAdapter.java b/geronimo-metrics-extensions/geronimo-metrics-extension-common/src/main/java/org/apache/geronimo/microprofile/metrics/extension/common/MicroprofileMetricsAdapter.java
index a8e46e8..6845461 100644
--- a/geronimo-metrics-extensions/geronimo-metrics-extension-common/src/main/java/org/apache/geronimo/microprofile/metrics/extension/common/MicroprofileMetricsAdapter.java
+++ b/geronimo-metrics-extensions/geronimo-metrics-extension-common/src/main/java/org/apache/geronimo/microprofile/metrics/extension/common/MicroprofileMetricsAdapter.java
@@ -40,7 +40,6 @@
                 .withDescription(def.getDescription())
                 .withType(GAUGE)
                 .withUnit(def.getUnit())
-                .reusable(true)
                 .build();
             try {
                 registry.register(metadata, (Gauge<Double>) () -> def.getEvaluator()
diff --git a/geronimo-metrics/pom.xml b/geronimo-metrics/pom.xml
index 4ea48a7..9ba3b92 100644
--- a/geronimo-metrics/pom.xml
+++ b/geronimo-metrics/pom.xml
@@ -27,8 +27,9 @@
   <name>Geronimo Metrics :: Impl</name>
 
   <properties>
-    <geronimo-metrics.Automatic-Module-Name>org.apache.geronimo.microprofile.metrics
-    </geronimo-metrics.Automatic-Module-Name>
+    <geronimo-metrics.Automatic-Module-Name>org.apache.geronimo.microprofile.metrics</geronimo-metrics.Automatic-Module-Name>
+
+    <tck.forkCount>1</tck.forkCount>
   </properties>
 
   <dependencies>
@@ -77,6 +78,18 @@
       <version>${spec.version}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>${junit.version}</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.hamcrest</groupId>
+          <artifactId>hamcrest-core</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
   </dependencies>
 
   <build>
@@ -85,7 +98,7 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
-          <forkCount>1</forkCount>
+          <forkCount>${tck.forkCount}</forkCount>
           <runOrder>alphabetical</runOrder>
           <dependenciesToScan>
             <dependency>org.eclipse.microprofile.metrics:microprofile-metrics-api-tck</dependency>
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/ConcurrentGaugeInterceptor.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/ConcurrentGaugeInterceptor.java
index fe7147c..6a6a2c3 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/ConcurrentGaugeInterceptor.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/ConcurrentGaugeInterceptor.java
@@ -104,7 +104,7 @@
                             .orElse(""));
 
             final ConcurrentGauge gauge = registry.getConcurrentGauge(
-                    new MetricID(name, concurrentGauge == null ? new Tag[0] : extension.createTags(concurrentGauge.tags())));
+                    new MetricID(name, extension.createTags(concurrentGauge == null ? new String[0] : concurrentGauge.tags())));
             if (gauge == null) {
                 throw new IllegalStateException("No counter with name [" + name + "] found in registry [" + registry + "]");
             }
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/CountedInterceptor.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/CountedInterceptor.java
index 7357071..42fbfae 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/CountedInterceptor.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/CountedInterceptor.java
@@ -99,7 +99,7 @@
                     ofNullable(extension.getAnnotation(type, Counted.class)).map(Counted::name).orElse(""));
 
             final Counter counter = registry.getCounter(
-                    new MetricID(name, counted == null ? new Tag[0] : extension.createTags(counted.tags())));
+                    new MetricID(name, extension.createTags(counted == null ? new String[0] : counted.tags())));
             if (counter == null) {
                 throw new IllegalStateException("No counter with name [" + name + "] found in registry [" + registry + "]");
             }
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MeteredInterceptor.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MeteredInterceptor.java
index bb56b77..d53fe7f 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MeteredInterceptor.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MeteredInterceptor.java
@@ -95,7 +95,7 @@
                     ofNullable(extension.getAnnotation(type, Metered.class)).map(Metered::name).orElse(""));
 
             meter = registry.getMeter(
-                    new MetricID(name, metered == null ? new Tag[0] : extension.createTags(metered.tags())));
+                    new MetricID(name, extension.createTags(metered == null ? new String[0] : metered.tags())));
             if (meter == null) {
                 throw new IllegalStateException("No meter with name [" + name + "] found in registry [" + registry + "]");
             }
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MetricsExtension.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MetricsExtension.java
index 49a843d..dc65f39 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MetricsExtension.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/MetricsExtension.java
@@ -19,6 +19,8 @@
 import org.apache.geronimo.microprofile.metrics.common.BaseMetrics;
 import org.apache.geronimo.microprofile.metrics.common.GaugeImpl;
 import org.apache.geronimo.microprofile.metrics.common.RegistryImpl;
+import org.eclipse.microprofile.config.Config;
+import org.eclipse.microprofile.config.ConfigProvider;
 import org.eclipse.microprofile.metrics.Counter;
 import org.eclipse.microprofile.metrics.Gauge;
 import org.eclipse.microprofile.metrics.Histogram;
@@ -85,17 +87,15 @@
 public class MetricsExtension implements Extension { // must not explicitly depend on jaxrs since it is dropped in nojaxrs bundle
     private static final Tag[] NO_TAG = new Tag[0];
 
-    private final MetricRegistry applicationRegistry = new RegistryImpl(MetricRegistry.Type.APPLICATION);
-    private final MetricRegistry baseRegistry = new RegistryImpl(MetricRegistry.Type.BASE);
-    private final MetricRegistry vendorRegistry = new RegistryImpl(MetricRegistry.Type.VENDOR);
+    private MetricRegistry applicationRegistry;
+    private MetricRegistry baseRegistry;
+    private MetricRegistry vendorRegistry;
 
     private final Map<MetricID, Metadata> registrations = new HashMap<>();
     private final Map<MetricID, Function<BeanManager, Gauge<?>>> gaugeFactories = new HashMap<>();
     private final Collection<Runnable> producersRegistrations = new ArrayList<>();
     private final Collection<CreationalContext<?>> creationalContexts = new ArrayList<>();
 
-    private Map<String, String> environmentalTags;
-
     void vetoEndpointIfNotActivated(@Observes final ProcessAnnotatedType<?> processAnnotatedType) {
         // default is secured so deploy
         final String name = processAnnotatedType.getAnnotatedType().getJavaClass().getName();
@@ -113,6 +113,11 @@
     }
 
     void letOtherExtensionsUseRegistries(@Observes final BeforeBeanDiscovery beforeBeanDiscovery, final BeanManager beanManager) {
+        final Tag[] globalTags = OptionalConfig.findTags();
+        applicationRegistry = new RegistryImpl(MetricRegistry.Type.APPLICATION, globalTags);
+        baseRegistry = new RegistryImpl(MetricRegistry.Type.BASE, globalTags);
+        vendorRegistry = new RegistryImpl(MetricRegistry.Type.VENDOR, globalTags);
+
         beforeBeanDiscovery.addQualifier(RegistryType.class);
         beanManager.fireEvent(applicationRegistry);
         beanManager.fireEvent(applicationRegistry, new RegistryTypeImpl(MetricRegistry.Type.APPLICATION));
@@ -125,9 +130,6 @@
             final String name = method.getAnnotated().getJavaMember().getName();
             return name.equals("name") || name.equals("tags");
         }).forEach(method -> method.remove(a -> a.annotationType() == Nonbinding.class));
-
-        // we must update @Metrics with that if it exists for the cdi resolution ot match since we make it binding
-        environmentalTags = new MetricID("foo").getTags();
     }
 
     void onMetric(@Observes final ProcessProducerField<? extends Metric, ?> processProducerField, final BeanManager beanManager) {
@@ -185,9 +187,9 @@
                     .withUnit(config.unit())
                     .build();
             final MetricID id = new MetricID(name, createTags(config.tags()));
-            addRegistration(metadata, id, true);
+            addRegistration(metadata, id);
 
-            if (!name.equals(config.name()) || !environmentalTags.isEmpty()) {
+            if (!name.equals(config.name())) {
                 final Annotation[] newQualifiers = Stream.concat(metricInjectionPointProcessEvent.getInjectionPoint().getQualifiers().stream()
                                 .filter(it -> it.annotationType() != org.eclipse.microprofile.metrics.annotation.Metric.class),
                         Stream.of(new MetricImpl(metadata, id)))
@@ -199,7 +201,7 @@
             final String name = MetricRegistry.name(injectionPoint.getMember().getDeclaringClass(), injectionPoint.getMember().getName());
             final Metadata metadata = Metadata.builder().withName(name).withType(type).build();
             final MetricID metricID = new MetricID(name);
-            addRegistration(metadata, metricID, true);
+            addRegistration(metadata, metricID);
 
             // ensure the injection uses the qualifier since we'll not register it without
             final Annotation[] newQualifiers = Stream.concat(metricInjectionPointProcessEvent.getInjectionPoint().getQualifiers().stream()
@@ -211,22 +213,6 @@
         }
     }
 
-    private void addRegistration(final Metadata metadata, final MetricID id, final boolean reusable) {
-        final Metadata existing = registrations.putIfAbsent(id, metadata);
-        if (existing != null) {
-            if (!reusable && !metadata.isReusable()) {
-                throw new IllegalArgumentException(id.getName() + " is not set as reusable but is referenced twice");
-            }
-        }
-    }
-
-    public Tag[] createTags(final String[] tags) {
-        return Stream.of(tags).filter(it -> it.contains("=")).map(it -> {
-            final int sep = it.indexOf("=");
-            return new Tag(it.substring(0, sep), it.substring(sep + 1));
-        }).toArray(Tag[]::new);
-    }
-
     void findInterceptorMetrics(@Observes @WithAnnotations({
             Counted.class,
             SimplyTimed.class,
@@ -330,6 +316,21 @@
         creationalContexts.forEach(CreationalContext::release);
     }
 
+    private void addRegistration(final Metadata metadata, final MetricID id) {
+        registrations.putIfAbsent(id, metadata);
+    }
+
+    public Tag[] createTags(final String[] tags) {
+        return toTags(tags);
+    }
+
+    private static Tag[] toTags(final String[] tags) {
+        return Stream.of(tags).filter(it -> it.contains("=")).map(it -> {
+            final int sep = it.indexOf("=");
+            return new Tag(it.substring(0, sep), it.substring(sep + 1));
+        }).toArray(Tag[]::new);
+    }
+
     private void doRegisterMetric(final AnnotatedType<?> annotatedType, final Class<?> javaClass, final AnnotatedCallable<?> method) {
         final Member javaMember = method.getJavaMember();
 
@@ -347,7 +348,7 @@
                     .withUnit(counted.unit())
                     .build();
             final MetricID metricID = new MetricID(name, createTags(counted.tags()));
-            addRegistration(metadata, metricID, counted.reusable() || !isMethod);
+            addRegistration(metadata, metricID);
         }
 
         final ConcurrentGauge concurrentGauge = ofNullable(method.getAnnotation(ConcurrentGauge.class)).orElseGet(() ->
@@ -364,7 +365,7 @@
                     .withUnit(concurrentGauge.unit())
                     .build();
             final MetricID metricID = new MetricID(name, createTags(concurrentGauge.tags()));
-            addRegistration(metadata, metricID, concurrentGauge.reusable() || !isMethod);
+            addRegistration(metadata, metricID);
         }
 
         final Timed timed = ofNullable(method.getAnnotation(Timed.class)).orElseGet(() -> getAnnotation(annotatedType, Timed.class));
@@ -380,7 +381,7 @@
                     .withUnit(timed.unit())
                     .build();
             final MetricID metricID = new MetricID(name, createTags(timed.tags()));
-            addRegistration(metadata, metricID, timed.reusable() || !isMethod);
+            addRegistration(metadata, metricID);
         }
 
         final SimplyTimed simplyTimed = ofNullable(method.getAnnotation(SimplyTimed.class)).orElseGet(() -> getAnnotation(annotatedType, SimplyTimed.class));
@@ -396,7 +397,7 @@
                     .withUnit(simplyTimed.unit())
                     .build();
             final MetricID metricID = new MetricID(name, createTags(simplyTimed.tags()));
-            addRegistration(metadata, metricID, simplyTimed.reusable() || !isMethod);
+            addRegistration(metadata, metricID);
         }
 
         final Metered metered = ofNullable(method.getAnnotation(Metered.class))
@@ -413,7 +414,7 @@
                     .withUnit(metered.unit())
                     .build();
             final MetricID metricID = new MetricID(name, createTags(metered.tags()));
-            addRegistration(metadata, metricID, metered.reusable() || !isMethod);
+            addRegistration(metadata, metricID);
         }
 
         final org.eclipse.microprofile.metrics.annotation.Gauge gauge = ofNullable(method.getAnnotation(org.eclipse.microprofile.metrics.annotation.Gauge.class))
@@ -430,7 +431,7 @@
                     .withUnit(gauge.unit())
                     .build();
             final MetricID metricID = new MetricID(name, createTags(gauge.tags()));
-            addRegistration(metadata, metricID, false);
+            addRegistration(metadata, metricID);
             gaugeFactories.put(metricID, beanManager -> {
                 final Object reference = getInstance(javaClass, beanManager);
                 final Method mtd = Method.class.cast(javaMember);
@@ -464,7 +465,7 @@
                 .withUnit(config.unit())
                 .build();
         final MetricID id = new MetricID(name, createTags(config.tags()));
-        addRegistration(metadata, id, true);
+        addRegistration(metadata, id);
         return metadata;
     }
 
@@ -521,16 +522,20 @@
                          final Annotation qualifier,
                          final Object instance,
                          final boolean addDefaultQualifier) {
-        final BeanConfigurator<Object> base = afterBeanDiscovery.addBean()
+        final BeanConfigurator<Object> configurator = afterBeanDiscovery.addBean()
                 .id(MetricsExtension.class.getName() + ":" + type.getName() + ":" + idSuffix)
                 .beanClass(type)
-                .types(type, Object.class)
                 .scope(Dependent.class) // avoid proxies, tck use assertEquals(proxy, registry.get(xxx))
                 .createWith(c -> instance);
-        if (addDefaultQualifier) {
-            base.qualifiers(qualifier, Default.Literal.INSTANCE, Any.Literal.INSTANCE);
+        if (MetricRegistry.class == type) {
+            configurator.types(MetricRegistry.class, RegistryImpl.class, Objects.class);
         } else {
-            base.qualifiers(qualifier, Any.Literal.INSTANCE);
+            configurator.types(type, Object.class);
+        }
+        if (addDefaultQualifier) {
+            configurator.qualifiers(qualifier, Default.Literal.INSTANCE, Any.Literal.INSTANCE);
+        } else {
+            configurator.qualifiers(qualifier, Any.Literal.INSTANCE);
         }
     }
 
@@ -556,6 +561,7 @@
             this.metadata = metadata;
             this.tags = metricID.getTags().entrySet().stream()
                     .map(e -> e.getKey() + "=" + e.getValue())
+                    .distinct()
                     .toArray(String[]::new);
         }
 
@@ -586,12 +592,12 @@
 
         @Override
         public String description() {
-            return metadata.getDescription().orElse("");
+            return metadata.getDescription();
         }
 
         @Override
         public String unit() {
-            return metadata.getUnit().orElse("");
+            return metadata.getUnit();
         }
     }
 
@@ -613,4 +619,21 @@
             return RegistryType.class;
         }
     }
+
+    private static final class OptionalConfig {
+        private OptionalConfig() {
+            // no-op
+        }
+
+        public static Tag[] findTags() {
+                try {
+                    final Config config = ConfigProvider.getConfig();
+                    return config.getOptionalValue("mp.metrics.tags", String.class)
+                            .map(it -> toTags(it.split(",")))
+                            .orElseGet(() -> new Tag[0]);
+                } catch (final IllegalStateException | ExceptionInInitializerError | NoClassDefFoundError t) {
+                    return new Tag[0];
+                }
+        }
+    }
 }
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/SimplyTimedInterceptor.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/SimplyTimedInterceptor.java
index b97e8d7..ecea25b 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/SimplyTimedInterceptor.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/SimplyTimedInterceptor.java
@@ -90,7 +90,7 @@
                     Modifier.isAbstract(executable.getDeclaringClass().getModifiers()) ? type.getJavaClass() : executable.getDeclaringClass(),
                     executable, timed == null ? null : timed.name(), timed != null && timed.absolute(),
                     ofNullable(extension.getAnnotation(type, SimplyTimed.class)).map(SimplyTimed::name).orElse(""));
-            timer = registry.getSimpleTimer(new MetricID(name, timed == null ? new Tag[0] : extension.createTags(timed.tags())));
+            timer = registry.getSimpleTimer(new MetricID(name, extension.createTags(timed == null ? new String[0] : timed.tags())));
             if (timer == null) {
                 throw new IllegalStateException("No timer with name [" + name + "] found in registry [" + registry + "]");
             }
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/TimedInterceptor.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/TimedInterceptor.java
index d736fc1..17b7619 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/TimedInterceptor.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/cdi/TimedInterceptor.java
@@ -91,7 +91,7 @@
                     Modifier.isAbstract(executable.getDeclaringClass().getModifiers()) ? type.getJavaClass() : executable.getDeclaringClass(),
                     executable, timed == null ? null : timed.name(), timed != null && timed.absolute(),
                     ofNullable(extension.getAnnotation(type, Timed.class)).map(Timed::name).orElse(""));
-            timer = registry.getTimer(new MetricID(name, timed == null ? new Tag[0] : extension.createTags(timed.tags())));
+            timer = registry.getTimer(new MetricID(name, extension.createTags(timed == null ? new String[0] : timed.tags())));
             if (timer == null) {
                 throw new IllegalStateException("No timer with name [" + name + "] found in registry [" + registry + "]");
             }
diff --git a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/jaxrs/CdiMetricsEndpoints.java b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/jaxrs/CdiMetricsEndpoints.java
index 62b9284..74ec9a6 100644
--- a/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/jaxrs/CdiMetricsEndpoints.java
+++ b/geronimo-metrics/src/main/java/org/apache/geronimo/microprofile/metrics/jaxrs/CdiMetricsEndpoints.java
@@ -47,5 +47,6 @@
         setApplicationRegistry(applicationRegistry);
         setBaseRegistry(baseRegistry);
         setVendorRegistry(vendorRegistry);
+        super.init();
     }
 }
diff --git a/geronimo-metrics/src/test/java/org/apache/geronimo/microprofile/metrics/test/ArquillianSetup.java b/geronimo-metrics/src/test/java/org/apache/geronimo/microprofile/metrics/test/ArquillianSetup.java
index a0b6499..e1d300d 100644
--- a/geronimo-metrics/src/test/java/org/apache/geronimo/microprofile/metrics/test/ArquillianSetup.java
+++ b/geronimo-metrics/src/test/java/org/apache/geronimo/microprofile/metrics/test/ArquillianSetup.java
@@ -17,10 +17,10 @@
 package org.apache.geronimo.microprofile.metrics.test;
 
 import org.apache.catalina.Context;
+import org.apache.geronimo.microprofile.metrics.cdi.MetricsExtension;
 import org.apache.meecrowave.Meecrowave;
 import org.apache.meecrowave.arquillian.MeecrowaveContainer;
 import org.eclipse.microprofile.metrics.MetricID;
-import org.eclipse.microprofile.metrics.Tag;
 import org.eclipse.microprofile.metrics.annotation.Metric;
 import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
 import org.jboss.arquillian.container.spi.context.annotation.ContainerScoped;
@@ -128,7 +128,9 @@
                         thread.setContextClassLoader(appClassLoaderInstanceProducer.get());
                         try {
                             final CDI<Object> cdi = CDI.current();
-                            final Annotation[] qualifiers = Stream.of(p.getAnnotations()).filter(it -> cdi.getBeanManager().isQualifier(it.annotationType())).toArray(Annotation[]::new);
+                            final Annotation[] qualifiers = Stream.of(p.getAnnotations())
+                                    .filter(it -> cdi.getBeanManager().isQualifier(it.annotationType()))
+                                    .toArray(Annotation[]::new);
                             return cdi.select(p.getType(), fixQualifiers(qualifiers)).get();
                         } catch (final RuntimeException re) {
                             re.printStackTrace(); // easier to debug when some test fail since TCK inject metrics as params
@@ -141,14 +143,17 @@
         }
 
         private Annotation[] fixQualifiers(final Annotation[] qualifiers) {
+            final MetricsExtension metricsExtension = CDI.current().select(MetricsExtension.class).get();
             return Stream.of(qualifiers)
                     .map(it -> {
                         if (Metric.class == it.annotationType()) { // we make tags and name binding so ensure it uses the right values
                             final Metric delegate = Metric.class.cast(it);
-                            return new MetricLiteral(delegate, new MetricID(delegate.name(), Stream.of(delegate.tags()).filter(tag -> tag.contains("=")).map(tag -> {
-                                final int sep = tag.indexOf("=");
-                                return new Tag(tag.substring(0, sep), tag.substring(sep + 1));
-                            }).toArray(Tag[]::new)).getTagsAsList().stream().map(t -> t.getTagName() + '=' + t.getTagValue()).toArray(String[]::new));
+                            final String[] tags = new MetricID(delegate.name(), metricsExtension.createTags(delegate.tags()))
+                                    .getTagsAsList().stream()
+                                    .map(t -> t.getTagName() + '=' + t.getTagValue())
+                                    .distinct()
+                                    .toArray(String[]::new);
+                            return new MetricLiteral(delegate, tags);
                         }
                         return it;
                     })
diff --git a/pom.xml b/pom.xml
index 4370c42..6ed5dd9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,10 +42,11 @@
   </scm>
 
   <properties>
-    <spec.version>3.0-M2</spec.version>
+    <spec.version>3.0-RC2</spec.version>
     <arquillian.version>1.1.13.Final</arquillian.version>
     <meecrowave.version>1.2.8</meecrowave.version>
     <owb.version>2.0.16</owb.version>
+    <junit.version>4.13</junit.version>
   </properties>
 
   <modules>
@@ -101,7 +102,7 @@
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
-      <version>4.12</version>
+      <version>${junit.version}</version>
       <scope>test</scope>
     </dependency>
   </dependencies>