From 4ea5d11651a8d8bcb28c1f7a9328b900a5450bb0 Mon Sep 17 00:00:00 2001 From: David Chan Date: Tue, 27 Aug 2024 12:43:15 -0400 Subject: [PATCH 1/2] Baseline before overlay change --- .../.classpath | 7 +- .../bnd.bnd | 5 +- .../LegacyMetricRegistryAdapter.java | 1068 +++++++++++++++++ 3 files changed, 1078 insertions(+), 2 deletions(-) create mode 100644 dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java diff --git a/dev/io.openliberty.io.smallrye.metrics/.classpath b/dev/io.openliberty.io.smallrye.metrics/.classpath index 63890cf77e2..46c7e9f763a 100644 --- a/dev/io.openliberty.io.smallrye.metrics/.classpath +++ b/dev/io.openliberty.io.smallrye.metrics/.classpath @@ -1,6 +1,11 @@ - + + + + + + diff --git a/dev/io.openliberty.io.smallrye.metrics/bnd.bnd b/dev/io.openliberty.io.smallrye.metrics/bnd.bnd index 17b8a88e4ac..a054440fdd0 100644 --- a/dev/io.openliberty.io.smallrye.metrics/bnd.bnd +++ b/dev/io.openliberty.io.smallrye.metrics/bnd.bnd @@ -33,6 +33,9 @@ WS-TraceGroup: METRICS publish.wlp.jar.suffix: lib -buildpath: \ - io.smallrye:smallrye-metrics;version=5.1.0 + io.smallrye:smallrye-metrics;version=5.1.0,\ + io.openliberty.org.eclipse.microprofile.metrics.5.0;version=latest,\ + io.openliberty.io.micrometer;version=latest,\ + io.openliberty.org.eclipse.microprofile.config.3.0;version=latest instrument.disabled: true \ No newline at end of file diff --git a/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java b/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java new file mode 100644 index 00000000000..cc259f8e8a5 --- /dev/null +++ b/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java @@ -0,0 +1,1068 @@ +package io.smallrye.metrics.legacyapi; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Gauge; +import org.eclipse.microprofile.metrics.Histogram; +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Metric; +import org.eclipse.microprofile.metrics.MetricFilter; +import org.eclipse.microprofile.metrics.MetricID; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.Tag; +import org.eclipse.microprofile.metrics.Timer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.smallrye.metrics.setup.ApplicationNameResolver; +import io.smallrye.metrics.setup.config.MetricsConfigurationManager; + +public class LegacyMetricRegistryAdapter implements MetricRegistry { + + private static final String CLASS_NAME = LegacyMetricRegistryAdapter.class.getName(); + private static final Logger LOGGER = Logger.getLogger(CLASS_NAME); + + private final String scope; + private final MeterRegistry registry; + + public static final String MP_APPLICATION_NAME_TAG = "mp_app"; + + public static final String MP_SCOPE_TAG = "mp_scope"; + + protected static final String MP_APPLICATION_NAME_VAR = "mp.metrics.appName"; + + protected static final String MP_DEFAULT_APPLICATION_NAME_VAR = "mp.metrics.defaultAppName"; + protected volatile static io.micrometer.core.instrument.Tag DEFAULT_APP_NAME_TAG = null; + + /* + * Set by user on the server-level with MP Config property mp.metrics.defaultAppName + */ + private final String defaultAppNameValue; + + private final Map constructedMeters = new ConcurrentHashMap<>(); + private final Map metadataMap = new ConcurrentHashMap<>(); + + protected final ConcurrentHashMap applicationMPConfigAppNameTagCache; + + protected final ConcurrentHashMap> applicationMap; + + protected final ApplicationNameResolver appNameResolver; + + protected final boolean isAppnameResolverPresent; + + private MemberToMetricMappings memberToMetricMappings; + + protected static io.micrometer.core.instrument.Tag[] SERVER_LEVEL_MPCONFIG_APPLICATION_NAME_TAG = null; + + protected static final String GLOBAL_TAG_MALFORMED_EXCEPTION = "Malformed list of Global Tags. Tag names " + + "must match the following regex [a-zA-Z_][a-zA-Z0-9_]*." + " Global Tag values must not be empty." + + " Global Tag values MUST escape equal signs `=` and commas `,`" + " with a backslash `\\` "; + + protected static final String GLOBAL_TAGS_VARIABLE = "mp.metrics.tags"; + + /** + * This static Tag[] represents the server level global tags retrieved from MP Config for + * mp.metrics.tags. This value will be 'null' when not initialized. If during initialization and no + * global tag has been resolved this will be to an array of size 0. Using an array of size 0 is to + * represent that an attempt on start up was made to resolve the value, but none was found. This + * prevents later instantiations of MetricRegistry to avoid attempting to resolve the MP Config + * value for the slight performance boon. + * + * This server level value will not change at all throughout the life time of the server as it is + * defined by env vars or sys props. + */ + protected static io.micrometer.core.instrument.Tag[] SERVER_LEVEL_MPCONFIG_GLOBAL_TAGS = null; + + public MeterRegistry getPrometheusMeterRegistry() { + return registry; + } + + /** + * Associates a metric's MetricID to a specific application if an application name can be resolved. + * + * @param metricDescriptor MetricDescriptor of metric + */ + public void addNameToApplicationMap(MetricDescriptor metricDescriptor) { + if (isAppnameResolverPresent) + addNameToApplicationMap(metricDescriptor.toMetricID(), appNameResolver.getApplicationName()); + } + + /** + * Associates a metric's MetricID to a specific application if an application name can be resolved. + * + * @param MetricID MetricID of metric + */ + public void addNameToApplicationMap(MetricID MetricID) { + if (isAppnameResolverPresent) + addNameToApplicationMap(MetricID, appNameResolver.getApplicationName()); + } + + /** + * Adds the MetricID to an application map given the application name. + * This map is not a complete list of metrics owned by an application, + * produced metrics are managed in the MetricsExtension + * + * @param metricID metric ID of metric that was added + * @param appName applicationName + */ + public void addNameToApplicationMap(MetricID metricID, String appName) { + final String METHOD_NAME = "addNameToApplicationMap"; + + /* + * - Base metrics (or vendor metrics) + * - Vendor does not support multi-application deployment and/or + * no AppnameResolver was provided. + */ + if (appName == null) + return; + ConcurrentLinkedQueue list = applicationMap.get(appName); + if (list == null) { + ConcurrentLinkedQueue newList = new ConcurrentLinkedQueue(); + list = applicationMap.putIfAbsent(appName, newList); + if (list == null) + list = newList; + } + list.add(metricID); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.logp(Level.FINER, CLASS_NAME, METHOD_NAME, + String.format("Mapped MetricID [id= %s] to application \"%s\"", metricID, appName)); + } + } + + public void unRegisterApplicationMetrics() { + unRegisterApplicationMetrics(appNameResolver.getApplicationName()); + } + + public void unRegisterApplicationMetrics(String appName) { + + /* + * - Base metrics (or vendor metrics) + * - Vendor does not support multi-application deployment and/or + * no AppnameResolver was provided. + */ + if (appName == null) { + return; + } + + ConcurrentLinkedQueue list = applicationMap.remove(appName); + + if (list != null) { + for (MetricID metricID : list) { + remove(metricID); + } + } + + MetricsConfigurationManager.getInstance().removeConfiguration(appName); + + } + + public LegacyMetricRegistryAdapter(String scope, MeterRegistry registry, ApplicationNameResolver appNameResolver) { + + /* + * Note: if ApplicationNameResolver is passed through as Java Reflection Proxy object, + * can only be checked if its is "null". + * Trying any other operations would lead to an Exception (i.e. equals()) + */ + if (appNameResolver == null) { + this.appNameResolver = ApplicationNameResolver.DEFAULT; + isAppnameResolverPresent = false; + } else { + this.appNameResolver = appNameResolver; + isAppnameResolverPresent = true; + } + + this.scope = scope; + this.registry = registry; + + applicationMPConfigAppNameTagCache = new ConcurrentHashMap(); + + applicationMap = new ConcurrentHashMap>(); + + defaultAppNameValue = resolveMPConfigDefaultAppNameTag(); + + resolveMPConfigGlobalTagsByServer(); + + if (scope != BASE_SCOPE && scope != VENDOR_SCOPE) { + memberToMetricMappings = new MemberToMetricMappings(); + } + } + + private synchronized io.micrometer.core.instrument.Tag[] resolveMPConfigGlobalTagsByServer() { + + final String METHOD_NAME = "resolveMPConfigGlobalTagsByServer"; + if (SERVER_LEVEL_MPCONFIG_GLOBAL_TAGS == null) { + + // Using MP Config to retreive the mp.metrics.tags Config value + Optional globalTags = ConfigProvider.getConfig().getOptionalValue(GLOBAL_TAGS_VARIABLE, + String.class); + + if (globalTags.isPresent()) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, CLASS_NAME, METHOD_NAME, String.format( + "MicroProfile Config value for \"%s\" resolved to be: %s", GLOBAL_TAGS_VARIABLE, + globalTags.get())); + } + } + + // evaluate if there exists tag values or set tag[0] to be null for no value; + SERVER_LEVEL_MPCONFIG_GLOBAL_TAGS = (globalTags.isPresent()) ? parseGlobalTags(globalTags.get()) : new io.micrometer.core.instrument.Tag[0]; + } + return (SERVER_LEVEL_MPCONFIG_GLOBAL_TAGS.length == 0) ? null : SERVER_LEVEL_MPCONFIG_GLOBAL_TAGS; + } + + /** + * This will return server level global tag i.e defined in env var or sys props + * + * Will return null if no MP Config value is set for the mp.metrics.tags on the server level + * + * @return Tag[] The server wide global tag; can return null + */ + private static io.micrometer.core.instrument.Tag[] parseGlobalTags(String globalTags) { + if (globalTags == null || globalTags.length() == 0) { + return null; + } + String[] kvPairs = globalTags.split("(? tagMap = new HashMap(); + tagMap.put(mpConfigAppTag.getKey(), mpConfigAppTag.getValue()); + + /* + * Application Metric tags are put into the map second + * this will over write any conflicting tags. This is similar + * to the old behaviour when MetricID auto-resolved MP Config tags + * it would resolve MP COnfig tags first then add application tags + */ + for (io.micrometer.core.instrument.Tag tag : tags) { + tagMap.put(tag.getKey(), tag.getValue()); + } + + Tags result = Tags.empty(); + for (Entry entry : tagMap.entrySet()) { + result = result.and(entry.getKey(), entry.getValue()); + } + + tags = result; + + } + return tags; + + } + + private String resolveMPConfigDefaultAppNameTag() { + + Optional configVal = ConfigProvider.getConfig().getOptionalValue(MP_DEFAULT_APPLICATION_NAME_VAR, + String.class); + + return (configVal.isPresent()) ? configVal.get().trim() : null; + } + + /** + * This method will retrieve cached tag values for the mp.metrics.appName or resolve it and cache it + * + * @return The application level MP Config mp.metrics.appName tag of the application; Or if it exists the server level + * value; Or null + */ + private synchronized io.micrometer.core.instrument.Tag resolveMPConfigAppNameTag() { + + String appName = appNameResolver.getApplicationName(); + + /* + * If appName is null then we aren't running in an application context. + * This is possible when resolving metrics for BASE or VENDOR. + * + * Since we're using a ConcurrentHashMap, can't store a null key and don't want + * to risk making up a key a user might use as their appName. So we'll call two methods + * that are similar. resolveAppTagByServer() will, however, store to a static array. + * + */ + io.micrometer.core.instrument.Tag tag = (appName == null) ? resolveMPConfigAppNameTagByServer() : resolveMPConfigAppNameTagByApplication(appName); + + return (tag != null) ? tag : (defaultAppNameValue != null) ? io.micrometer.core.instrument.Tag.of(MP_APPLICATION_NAME_TAG, defaultAppNameValue) : null; + } + + /** + * This will return server level application tag + * i.e defined in env var or sys props + * + * Will return null if no MP Config value is set + * for the mp.metrics.appName on the server level + * + * @return Tag The server wide application tag; can return null + */ + private synchronized io.micrometer.core.instrument.Tag resolveMPConfigAppNameTagByServer() { + if (SERVER_LEVEL_MPCONFIG_APPLICATION_NAME_TAG == null) { + SERVER_LEVEL_MPCONFIG_APPLICATION_NAME_TAG = new io.micrometer.core.instrument.Tag[1]; + + //Using MP Config to retrieve the mp.metrics.appName Config value + Optional applicationName = ConfigProvider.getConfig().getOptionalValue(MP_APPLICATION_NAME_VAR, + String.class); + + //Evaluate if there exists a tag value or set tag[0] to be null for no value; + SERVER_LEVEL_MPCONFIG_APPLICATION_NAME_TAG[0] = (applicationName.isPresent()) ? io.micrometer.core.instrument.Tag.of(MP_APPLICATION_NAME_TAG, + applicationName.get()) : null; + } + return SERVER_LEVEL_MPCONFIG_APPLICATION_NAME_TAG[0]; + } + + /** + * This will return the MP Config value for + * mp.metrics.appName for the application + * that the current TCCL is running for + * + * @param appName the application name to look up from cache + * @return Tag The mp.metrics.appName MP Config value associated to the appName; can return null if non exists + */ + private synchronized io.micrometer.core.instrument.Tag resolveMPConfigAppNameTagByApplication(String appName) { + //Return cached value + if (!applicationMPConfigAppNameTagCache.containsKey(appName)) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + //Using MP Config to retreive the mp.metrics.appName Config value + Optional applicationName = ConfigProvider.getConfig(classLoader).getOptionalValue(MP_APPLICATION_NAME_VAR, + String.class); + + /* + * Evaluate if there exists a tag value. If there is not then we must create an "invalid" Tag to represent no value + * resolved. + * This is used later to return a null value. + * This is due the use of ConcurrentHashMap and we cannot set a null key. + */ + io.micrometer.core.instrument.Tag appTag = (applicationName.isPresent()) ? io.micrometer.core.instrument.Tag.of(MP_APPLICATION_NAME_TAG, + applicationName.get()) : io.micrometer.core.instrument.Tag.of("null", + "null"); + + //Cache the value + applicationMPConfigAppNameTagCache.put(appName, appTag); + } + + //Perhaps we don't really need a concurrent hashmap.. so we can avoid this. + io.micrometer.core.instrument.Tag returnTag; + return ((returnTag = applicationMPConfigAppNameTagCache.get(appName)).getKey().equals("null")) ? null : returnTag; + } + + public LegacyMetricRegistryAdapter(String scope, MeterRegistry registry) { + this(scope, registry, ApplicationNameResolver.DEFAULT); + } + + @Override + public Counter counter(String name) { + return internalCounter(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags())); + } + + @Override + public Counter counter(String name, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalCounter(internalGetMetadata(name), + new MetricDescriptor(name, unifiedTags)); + } + + @Override + public Counter counter(MetricID metricID) { + String name = metricID.getName(); + return internalCounter(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Counter counter(Metadata metadata) { + return internalCounter(internalGetMetadata(metadata), + new MetricDescriptor(metadata.getName(), withAppTags())); + } + + @Override + public Counter counter(Metadata metadata, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalCounter(internalGetMetadata(metadata), + new MetricDescriptor(metadata.getName(), unifiedTags)); + } + + private void validateTagNamesMatch(MetricDescriptor id) { + /* + * Check that if there are metrics registered with same metric name that the tag + * set is the same. Specifically that the tag key values are the same. Values + * can differ. + */ + for (MetricDescriptor md : constructedMeters.keySet()) { + if (md.name().equals(id.name) && !id.isTagNamesMatch(md.tags())) { + throw new IllegalArgumentException(String.format( + "The set of tags names provided do not match those of an existing metric with the same name. Provided = %s Existing = %s ", + id.tags.toString(), md.tags.toString())); + } + } + } + + CounterAdapter internalCounter(MpMetadata metadata, MetricDescriptor id) { + + validateTagNamesMatch(id); + + CounterAdapter result = checkCast(CounterAdapter.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new CounterAdapter())); + addNameToApplicationMap(id); + + return result.register(metadata, id, registry, scope, resolveMPConfigGlobalTagsByServer()); + } + + /** + * This is specifically used for runtimes which may need use of a functional counter. + * For example, the runtime may want to implement a vendor specific counter metric which + * relies on values obtained from a Mbeans or MXbeans. + * + * @param object type + * @param metadata metadata of metric + * @param obj object to apply ToDoubleFunction + * @param func ToDoubleFunction + * @param tags tags of metric + * @return The functional counter + */ + public Counter counter(Metadata metadata, T obj, ToDoubleFunction func, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalCounter(internalGetMetadata(metadata), obj, func, + new MetricDescriptor(metadata.getName(), unifiedTags)); + } + + FunctionCounterAdapter internalCounter(MpMetadata metadata, T obj, ToDoubleFunction func, MetricDescriptor id) { + + validateTagNamesMatch(id); + + FunctionCounterAdapter result = checkCast(FunctionCounterAdapter.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new FunctionCounterAdapter(obj, func))); + addNameToApplicationMap(id); + return result.register(metadata, id, registry, scope, resolveMPConfigGlobalTagsByServer()); + } + + public Gauge gauge(String name, T o, ToDoubleFunction f) { + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags()), o, f); + } + + public Gauge gauge(String name, T o, ToDoubleFunction f, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, unifiedTags), o, f); + } + + @Override + public Gauge gauge(String name, T o, Function f, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, unifiedTags), o, f); + } + + @Override + public Gauge gauge(MetricID metricID, T o, Function f) { + String name = metricID.getName(); + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags()), o, f); + } + + @Override + public Gauge gauge(Metadata metadata, T o, Function f, Tag... tags) { + String name = metadata.getName(); + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalGauge(internalGetMetadata(metadata), + new MetricDescriptor(name, unifiedTags), o, f); + } + + @SuppressWarnings("unchecked") + GaugeAdapter internalGauge(MpMetadata metadata, MetricDescriptor id, T obj, ToDoubleFunction f) { + validateTagNamesMatch(id); + GaugeAdapter.DoubleFunctionGauge result = checkCast(GaugeAdapter.DoubleFunctionGauge.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new GaugeAdapter.DoubleFunctionGauge<>(obj, f))); + addNameToApplicationMap(id); + return result.register(metadata, id, registry, scope, resolveMPConfigGlobalTagsByServer()); + } + + @SuppressWarnings("unchecked") + GaugeAdapter internalGauge(MpMetadata metadata, MetricDescriptor id, T obj, Function f) { + validateTagNamesMatch(id); + GaugeAdapter.FunctionGauge result = checkCast(GaugeAdapter.FunctionGauge.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new GaugeAdapter.FunctionGauge<>(obj, f))); + addNameToApplicationMap(id); + return result.register(metadata, id, registry, scope, resolveMPConfigGlobalTagsByServer()); + } + + public Gauge gauge(String name, Supplier f) { + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags()), f); + } + + @Override + public Gauge gauge(String name, Supplier f, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, unifiedTags), f); + } + + @Override + public Gauge gauge(MetricID metricID, Supplier f) { + String name = metricID.getName(); + return internalGauge(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags()), f); + } + + @Override + public Gauge gauge(Metadata metadata, Supplier f, Tag... tags) { + String name = metadata.getName(); + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalGauge(internalGetMetadata(metadata), + new MetricDescriptor(name, unifiedTags), f); + } + + @SuppressWarnings("unchecked") + GaugeAdapter internalGauge(MpMetadata metadata, MetricDescriptor id, Supplier f) { + validateTagNamesMatch(id); + GaugeAdapter result = checkCast(GaugeAdapter.NumberSupplierGauge.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new GaugeAdapter.NumberSupplierGauge(f))); + addNameToApplicationMap(id); + return result.register(metadata, id, registry, scope, resolveMPConfigGlobalTagsByServer()); + } + + void bindAnnotatedGauge(AnnotatedGaugeAdapter adapter) { + MetricDescriptor id = new MetricDescriptor(adapter.name(), adapter.tags()); + AnnotatedGaugeAdapter oops = checkCast(AnnotatedGaugeAdapter.class, adapter.getMetadata(), + constructedMeters.putIfAbsent(id, adapter)); + if (oops == null) { + metadataMap.put(adapter.name(), adapter.getMetadata()); + adapter.register(id, registry); + } else { + throw new IllegalArgumentException(String.format("Gauge %s already exists. (existing='%s', new='%s')", + adapter.getId(), oops.getTargetName(), adapter.getTargetName())); + } + } + + @Override + public Histogram histogram(String name) { + return internalHistogram(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags())); + } + + @Override + public Histogram histogram(String name, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalHistogram(internalGetMetadata(name), + new MetricDescriptor(name, unifiedTags)); + } + + @Override + public Histogram histogram(MetricID metricID) { + String name = metricID.getName(); + return internalHistogram(internalGetMetadata(name), + new MetricDescriptor(name, withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Histogram histogram(Metadata metadata) { + return internalHistogram(internalGetMetadata(metadata), + new MetricDescriptor(metadata.getName(), withAppTags())); + } + + @Override + public Histogram histogram(Metadata metadata, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalHistogram(internalGetMetadata(metadata), + new MetricDescriptor(metadata.getName(), unifiedTags)); + } + + HistogramAdapter internalHistogram(MpMetadata metadata, MetricDescriptor id) { + validateTagNamesMatch(id); + HistogramAdapter result = checkCast(HistogramAdapter.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new HistogramAdapter())); + addNameToApplicationMap(id); + return result.register(metadata, id, scope, resolveMPConfigGlobalTagsByServer()); + } + + @Override + public Timer timer(String name) { + return internalTimer(internalGetMetadataTimers(name), + new MetricDescriptor(name, withAppTags())); + } + + @Override + public Timer timer(String name, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalTimer(internalGetMetadataTimers(name), + new MetricDescriptor(name, unifiedTags)); + } + + @Override + public Timer timer(MetricID metricID) { + String name = metricID.getName(); + return internalTimer(internalGetMetadataTimers(name), + new MetricDescriptor(name, withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Timer timer(Metadata metadata) { + return internalTimer(internalGetMetadataTimers(metadata), + new MetricDescriptor(metadata.getName(), withAppTags())); + } + + @Override + public Timer timer(Metadata metadata, Tag... tags) { + /* + * Verify tags before internalGetMetadata(). + * The call withAppTags() can throw an IAE. + * Don't want to have had created metadata + * and have it put into the map before that. + */ + Tags unifiedTags = withAppTags(tags); + return internalTimer(internalGetMetadataTimers(metadata), + new MetricDescriptor(metadata.getName(), unifiedTags)); + } + + TimerAdapter internalTimer(MpMetadata metadata, MetricDescriptor id) { + validateTagNamesMatch(id); + TimerAdapter result = checkCast(TimerAdapter.class, metadata, + constructedMeters.computeIfAbsent(id, k -> new TimerAdapter(registry))); + addNameToApplicationMap(id); + return result.register(metadata, id, scope, resolveMPConfigGlobalTagsByServer()); + } + + @Override + public Metric getMetric(MetricID metricID) { + return constructedMeters.get(new MetricDescriptor(metricID.getName(), withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public T getMetric(MetricID metricID, Class asType) { + return asType.cast(constructedMeters.get(new MetricDescriptor(metricID.getName(), withAppTags(metricID.getTagsAsArray())))); + } + + @Override + public Counter getCounter(MetricID metricID) { + return (Counter) constructedMeters.get(new MetricDescriptor(metricID.getName(), withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Gauge getGauge(MetricID metricID) { + return (Gauge) constructedMeters.get(new MetricDescriptor(metricID.getName(), withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Histogram getHistogram(MetricID metricID) { + return (Histogram) constructedMeters.get(new MetricDescriptor(metricID.getName(), withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Timer getTimer(MetricID metricID) { + return (Timer) constructedMeters.get(new MetricDescriptor(metricID.getName(), withAppTags(metricID.getTagsAsArray()))); + } + + @Override + public Metadata getMetadata(String name) { + return metadataMap.get(name); + } + + @Override + public boolean remove(String name) { + + boolean isRemoveSuccess = false; + for (Map.Entry e : constructedMeters.entrySet()) { + if (e.getKey().name().equals(name)) { + isRemoveSuccess = internalRemove(e.getKey()); + } + } + return isRemoveSuccess; + + } + + @Override + public boolean remove(MetricID metricID) { + return internalRemove(new MetricDescriptor(metricID)); + } + + @Override + public void removeMatching(MetricFilter metricFilter) { + for (Map.Entry e : constructedMeters.entrySet()) { + MetricID mid = e.getKey().toMetricID(); + if (metricFilter.matches(mid, e.getValue())) { + internalRemove(e.getKey()); + } + } + } + + boolean internalRemove(MetricDescriptor match) { + final String METHOD_NAME = "internalRemove"; + + MeterHolder holder = constructedMeters.remove(match); + + if (holder != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, CLASS_NAME, METHOD_NAME, + String.format("Removed metric with [id: %s]", match.toMetricID().toString())); + } + + io.micrometer.core.instrument.Meter meter = Metrics.globalRegistry.remove(holder.getMeter()); + if (meter != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, CLASS_NAME, METHOD_NAME, String.format("Removed from the Micrometer global registry a meter with MeterId [id= %s]", + meter.getId())); + } + } else { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, CLASS_NAME, METHOD_NAME, String.format( + "Attempted to remove a meter with the corresponding MetricID [id= %s] from the Micrometer global registry, but does not exist.", + match.toMetricID())); + } + } + + // Remove associated metadata if this is the last MP Metric left with that name + if (constructedMeters.keySet().stream().noneMatch(id -> id.name.equals(match.name))) { + metadataMap.remove(match.name); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, CLASS_NAME, METHOD_NAME, + String.format("Removed metadata for [name: %s]", match.name)); + } + } + } + return holder != null; + } + + @Override + public SortedSet getNames() { + return new TreeSet<>(metadataMap.keySet()); + } + + @Override + public SortedSet getMetricIDs() { + SortedSet out = new TreeSet<>(); + for (MetricDescriptor key : constructedMeters.keySet()) { + out.add(key.toMetricID()); + } + return out; + } + + @Override + public SortedMap getGauges() { + return getGauges(MetricFilter.ALL); + } + + @Override + public SortedMap getGauges(MetricFilter metricFilter) { + return getMetrics(Gauge.class, metricFilter); + } + + @Override + public SortedMap getCounters() { + return getCounters(MetricFilter.ALL); + } + + @Override + public SortedMap getCounters(MetricFilter metricFilter) { + return getMetrics(Counter.class, metricFilter); + } + + @Override + public SortedMap getHistograms() { + return getHistograms(MetricFilter.ALL); + } + + @Override + public SortedMap getHistograms(MetricFilter metricFilter) { + return getMetrics(Histogram.class, metricFilter); + } + + @Override + public SortedMap getTimers() { + return getTimers(MetricFilter.ALL); + } + + @Override + public SortedMap getTimers(MetricFilter metricFilter) { + return getMetrics(Timer.class, metricFilter); + } + + @Override + public SortedMap getMetrics(MetricFilter filter) { + SortedMap out = new TreeMap<>(); + for (Map.Entry e : constructedMeters.entrySet()) { + MetricID mid = e.getKey().toMetricID(); + if (filter.matches(mid, e.getValue())) { + out.put(e.getKey().toMetricID(), e.getValue()); + } + } + return out; + } + + @Override + public SortedMap getMetrics(Class ofType, MetricFilter filter) { + SortedMap out = new TreeMap<>(); + for (Map.Entry e : constructedMeters.entrySet()) { + if (ofType.isAssignableFrom(e.getValue().getClass())) { + MetricID mid = e.getKey().toMetricID(); + if (filter.matches(mid, e.getValue())) { + out.put(e.getKey().toMetricID(), (T) e.getValue()); + } + } + } + return out; + } + + @Override + public Map getMetrics() { + SortedMap out = new TreeMap<>(); + for (Map.Entry e : constructedMeters.entrySet()) { + out.put(e.getKey().toMetricID(), e.getValue()); + } + return out; + } + + @Override + public Map getMetadata() { + return Collections.unmodifiableMap(metadataMap); + } + + @Override + public String getScope() { + return scope; + } + + /** + * Must be called before any internalGetMetadata calls + * We may throw an IllegalArgumentException. So we don't + * want metadata to be registered if it was not necessary. + * + * @param tags Tags to be combined with + * @return tags combined with global tags and mp_app if available + */ + public Tags withAppTags(Tag... tags) { + + Tags out = Tags.empty(); + + if (tags != null) { + for (Tag t : tags) { + /* + * Need to check if tags being passed in are + * 'mp_scope' or 'mp_app'; throw IAE as per spec + * + * mp_scope is provided to micrometer registry + * during metric/meter registration in the adapters + * + * mp_app is resolved with the resolveMPConfigAppNameTag() + * logic + */ + if (t.getTagName().equals(MP_APPLICATION_NAME_TAG) + || t.getTagName().equals(MP_SCOPE_TAG)) { + throw new IllegalArgumentException("Can not use " + + "reserved tag names: \"mp_scope\" " + + "or \"mp_app\""); + } + out = out.and(t.getTagName(), t.getTagValue()); + } + } + + out = combineApplicationTagsWithMPConfigAppNameTag(out); + + return out; + } + + public Tag[] scopeTagsLegacy() { + return new Tag[] { new Tag("scope", this.scope) }; + } + + private MpMetadata internalGetMetadata(String name) { + + MpMetadata result = metadataMap.computeIfAbsent(name, k -> new MpMetadata(name)); + + /* + * Check that metadata of metric being registered/retrieved matches existing + * existing metadata (if it exists) + */ + if (!result.equals(MpMetadata.sanitize(new MpMetadata(name)))) { + throw new IllegalArgumentException(String.format("Existing metadata (%s) does not match with supplied metadata (%s)", + result.toString(), new MpMetadata(name).toString())); + } + + return result; + } + + /* + * Temporary work around due to https://github.com/eclipse/microprofile-metrics/issues/760 + */ + private MpMetadata internalGetMetadata(Metadata metadata) { + MpMetadata result = metadataMap.computeIfAbsent(metadata.getName(), k -> MpMetadata.sanitize(metadata)); + + /* + * Check that metadata of metric being registered/retrieved matches existing + * existing metadata (if it exists) + */ + if (!result.equals(MpMetadata.sanitize(metadata))) { + throw new IllegalArgumentException(String.format("Existing metadata (%s) does not match with supplied metadata (%s)", + result.toString(), metadata.toString())); + } + + return result; + } + + /* + * Temporary work around due to https://github.com/eclipse/microprofile-metrics/issues/760 + */ + private MpMetadata internalGetMetadataTimers(String name) { + + MpMetadata result = metadataMap.computeIfAbsent(name, k -> new MpMetadata(name)); + + /* + * Check that metadata of metric being registered/retrieved matches existing + * existing metadata (if it exists) + */ + if (!result.equalsTimers(MpMetadata.sanitize(new MpMetadata(name)))) { + throw new IllegalArgumentException(String.format("Existing metadata (%s) does not match with supplied metadata (%s)", + result.toString(), new MpMetadata(name).toString())); + } + + return result; + } + + private MpMetadata internalGetMetadataTimers(Metadata metadata) { + MpMetadata result = metadataMap.computeIfAbsent(metadata.getName(), k -> MpMetadata.sanitize(metadata)); + + /* + * Check that metadata of metric being registered/retrieved matches existing + * existing metadata (if it exists) + */ + if (!result.equalsTimers(MpMetadata.sanitize(metadata))) { + throw new IllegalArgumentException(String.format("Existing metadata (%s) does not match with supplied metadata (%s)", + result.toString(), metadata.toString())); + } + + return result; + } + + T checkCast(Class type, MpMetadata metadata, MeterHolder o) { + try { + return type.cast(o); + } catch (ClassCastException cce) { + throw new IllegalStateException(String.format("Metric (%s) already defined using a different type (%s)", + metadata.name, o.getMeter().getId().getType()), cce); + } + } + + public MemberToMetricMappings getMemberToMetricMappings() { + return memberToMetricMappings; + } + +} From 8bc72bb30d87d2bf246fba7f3064f264c698e2fb Mon Sep 17 00:00:00 2001 From: David Chan Date: Tue, 27 Aug 2024 12:48:45 -0400 Subject: [PATCH 2/2] Over lay change to LegacymetricRegistryAdapter to not use ConcurrentLinkedQueue as it caused Memory Leak --- .../bnd.bnd | 2 +- .../LegacyMetricRegistryAdapter.java | 69 ++++++++++++++----- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/dev/io.openliberty.io.smallrye.metrics/bnd.bnd b/dev/io.openliberty.io.smallrye.metrics/bnd.bnd index a054440fdd0..41c5e12d919 100644 --- a/dev/io.openliberty.io.smallrye.metrics/bnd.bnd +++ b/dev/io.openliberty.io.smallrye.metrics/bnd.bnd @@ -1,5 +1,5 @@ #******************************************************************************* -# Copyright (c) 2022, 2023 IBM Corporation and others. +# Copyright (c) 2022, 2024 IBM Corporation and others. # All rights reserved. This program and the accompanying materials # are made available under the terms of the Eclipse Public License 2.0 # which accompanies this distribution, and is available at diff --git a/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java b/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java index cc259f8e8a5..bee41a2a17e 100644 --- a/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java +++ b/dev/io.openliberty.io.smallrye.metrics/src/io/smallrye/metrics/legacyapi/LegacyMetricRegistryAdapter.java @@ -1,3 +1,21 @@ +/* + * Copyright 2020, 2024 Red Hat, Inc, and individual contributors + * + * Licensed 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. + */ +/* + * Liberty changes are enclosed by LIBERTY CHANGE START and LIBERTY CHANGE END + */ package io.smallrye.metrics.legacyapi; import java.util.Collections; @@ -5,12 +23,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToDoubleFunction; @@ -62,7 +80,12 @@ public class LegacyMetricRegistryAdapter implements MetricRegistry { protected final ConcurrentHashMap applicationMPConfigAppNameTagCache; - protected final ConcurrentHashMap> applicationMap; + // LIBERTY CHANGE START + /* + * Liberty change. Previously, the second param type of ConcurrentLinkedQueue lead to memory leak. + */ + protected final ConcurrentHashMap> applicationMap; + // LIBERTY CHANGE END protected final ApplicationNameResolver appNameResolver; @@ -133,14 +156,21 @@ public void addNameToApplicationMap(MetricID metricID, String appName) { */ if (appName == null) return; - ConcurrentLinkedQueue list = applicationMap.get(appName); - if (list == null) { - ConcurrentLinkedQueue newList = new ConcurrentLinkedQueue(); - list = applicationMap.putIfAbsent(appName, newList); - if (list == null) - list = newList; + + // LIBERTY CHANGE START + /* + * Liberty change. Previously, the second param type of ConcurrentLinkedQueue lead to memory leak. + */ + + Set set = applicationMap.get(appName); + if (set == null) { + Set newSet = Collections.newSetFromMap(new ConcurrentHashMap()); + set = applicationMap.putIfAbsent(appName, newSet); + if (set == null) + set = newSet; } - list.add(metricID); + set.add(metricID); + // LIBERTY CHANGE END if (LOGGER.isLoggable(Level.FINER)) { LOGGER.logp(Level.FINER, CLASS_NAME, METHOD_NAME, String.format("Mapped MetricID [id= %s] to application \"%s\"", metricID, appName)); @@ -161,11 +191,14 @@ public void unRegisterApplicationMetrics(String appName) { if (appName == null) { return; } - - ConcurrentLinkedQueue list = applicationMap.remove(appName); - - if (list != null) { - for (MetricID metricID : list) { + // LIBERTY CHANGE START + /* + * Liberty change. Previously, the second Parameter type of ConcurrentLinkedQueue lead to memory leak. + */ + Set set = applicationMap.remove(appName); + // LIBERTY CHANGE END + if (set != null) { + for (MetricID metricID : set) { remove(metricID); } } @@ -175,7 +208,6 @@ public void unRegisterApplicationMetrics(String appName) { } public LegacyMetricRegistryAdapter(String scope, MeterRegistry registry, ApplicationNameResolver appNameResolver) { - /* * Note: if ApplicationNameResolver is passed through as Java Reflection Proxy object, * can only be checked if its is "null". @@ -194,7 +226,12 @@ public LegacyMetricRegistryAdapter(String scope, MeterRegistry registry, Applica applicationMPConfigAppNameTagCache = new ConcurrentHashMap(); - applicationMap = new ConcurrentHashMap>(); + // LIBERTY CHANGE START + /* + * Liberty change. Previously, the second param type of ConcurrentLinkedQueue lead to memory leak. + */ + applicationMap = new ConcurrentHashMap>(); + // LIBERTY CHANGE END defaultAppNameValue = resolveMPConfigDefaultAppNameTag();