diff --git a/CHANGELOG.md b/CHANGELOG.md index acdf0218..226f2997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,19 @@ Version template: # Alfred Telemetry Changelog +## [0.1.2] - UNRELEASED +### Added +* MeterRegistryCustomizer to support complex customization of MeterRegistries + +## [0.1.1] - 2019-08-13 + +### Fixed +* MeterFilters are applied after metrics are possibly already exposed to the global MeterRegistry + +### Changed +* Changed default logging level to INFO +* Disable Cache metrics by default + ## [0.1.0] - 2019-07-11 Initial, early access release including: @@ -33,13 +46,4 @@ if available on the classpath. - The possibility to customize meters with custom `MeterFilter` implementations. - Integration with the out of the box Alfresco metrics included since 6.1. - Limited support for Care4Alf compatible metrics. -- ... - -## [0.1.1] - 2019-08-13 - -### Fixed -* MeterFilters are applied after metrics are possibly already exposed to the global MeterRegistry - -### Changed -* Changed default logging level to INFO -* Disable Cache metrics by default \ No newline at end of file +- ... \ No newline at end of file diff --git a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/MeterRegistryCustomizer.java b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/MeterRegistryCustomizer.java new file mode 100644 index 00000000..353af5d6 --- /dev/null +++ b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/MeterRegistryCustomizer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * 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 + * + * https://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 eu.xenit.alfred.telemetry; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; + +/** + * Exact copy of Spring Boot MeterRegistryCustomizer: https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizer.java + *

+ * Callback interface that can be used to customize auto-configured {@link MeterRegistry + * MeterRegistries}. + *

+ * Customizers are guaranteed to be applied before any {@link Meter} is registered with + * the registry. + * + * @author Jon Schneider + * @param the registry type to customize + * @since 0.1.1 + */ +@FunctionalInterface +public interface MeterRegistryCustomizer { + + /** + * Customize the given {@code registry}. + * @param registry the registry to customize + */ + void customize(T registry); + +} \ No newline at end of file diff --git a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/MeterBinderRegistrar.java b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/MeterBinderRegistrar.java index 55824385..91457a54 100644 --- a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/MeterBinderRegistrar.java +++ b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/MeterBinderRegistrar.java @@ -3,7 +3,6 @@ import com.google.common.base.CaseFormat; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; -import io.micrometer.core.instrument.config.MeterFilter; import java.util.ArrayList; import java.util.List; import java.util.Properties; diff --git a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/registry/RegistryRegistrar.java b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/registry/RegistryRegistrar.java index 6e66e199..aeeee5a8 100644 --- a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/registry/RegistryRegistrar.java +++ b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/registry/RegistryRegistrar.java @@ -1,6 +1,8 @@ package eu.xenit.alfred.telemetry.registry; +import eu.xenit.alfred.telemetry.MeterRegistryCustomizer; +import eu.xenit.alfred.telemetry.util.LambdaSafe; import eu.xenit.alfred.telemetry.util.VersionUtil; import eu.xenit.alfred.telemetry.util.VersionUtil.Version; import io.micrometer.core.instrument.Counter; @@ -63,6 +65,7 @@ private void processRegistryFactoryWrapper(RegistryFactoryWrapper factoryWrapper } final MeterRegistry registry = factoryWrapper.getRegistryFactory().createRegistry(); + this.customize(registry); this.addFilters(registry); globalMeterRegistry.add(registry); @@ -71,6 +74,14 @@ private void processRegistryFactoryWrapper(RegistryFactoryWrapper factoryWrapper this.incrementRegistryCounter(); } + @SuppressWarnings("unchecked") + private void customize(final MeterRegistry registry) { + LambdaSafe.callbacks(MeterRegistryCustomizer.class, ctx.getBeansOfType(MeterRegistryCustomizer.class).values(), + registry) + .withLogger(this.getClass()) + .invoke((c) -> c.customize(registry)); + } + private void addFilters(final MeterRegistry registry) { ctx.getBeansOfType(MeterFilter.class).values().forEach(registry.config()::meterFilter); } diff --git a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/util/LambdaSafe.java b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/util/LambdaSafe.java new file mode 100644 index 00000000..45215461 --- /dev/null +++ b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/util/LambdaSafe.java @@ -0,0 +1,429 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * 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 + * + * https://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 eu.xenit.alfred.telemetry.util; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Exact copy of the Spring Boot LambdaSafe class. https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java + *

+ * Utility that can be used to invoke lambdas in a safe way. Primarily designed to help support generically typed + * callbacks where {@link ClassCastException class cast exceptions} need to be dealt with due to class erasure. + * + * @author Phillip Webb + * @since 0.1.1 + */ +public final class LambdaSafe { + + private static final Method CLASS_GET_MODULE; + + private static final Method MODULE_GET_NAME; + + static { + CLASS_GET_MODULE = ReflectionUtils.findMethod(Class.class, "getModule"); + MODULE_GET_NAME = (CLASS_GET_MODULE != null) + ? ReflectionUtils.findMethod(CLASS_GET_MODULE.getReturnType(), "getName") : null; + } + + private LambdaSafe() { + } + + /** + * Start a call to a single callback instance, dealing with common generic type concerns and exceptions. + * + * @param callbackType the callback type (a {@link FunctionalInterface functional interface}) + * @param callbackInstance the callback instance (may be a lambda) + * @param argument the primary argument passed to the callback + * @param additionalArguments any additional arguments passed to the callback + * @param the callback type + * @param the primary argument type + * @return a {@link Callback} instance that can be invoked. + */ + public static Callback callback(Class callbackType, C callbackInstance, A argument, + Object... additionalArguments) { + Assert.notNull(callbackType, "CallbackType must not be null"); + Assert.notNull(callbackInstance, "CallbackInstance must not be null"); + return new Callback<>(callbackType, callbackInstance, argument, additionalArguments); + } + + /** + * Start a call to callback instances, dealing with common generic type concerns and exceptions. + * + * @param callbackType the callback type (a {@link FunctionalInterface functional interface}) + * @param callbackInstances the callback instances (elements may be lambdas) + * @param argument the primary argument passed to the callbacks + * @param additionalArguments any additional arguments passed to the callbacks + * @param the callback type + * @param the primary argument type + * @return a {@link Callbacks} instance that can be invoked. + */ + public static Callbacks callbacks(Class callbackType, Collection callbackInstances, + A argument, Object... additionalArguments) { + Assert.notNull(callbackType, "CallbackType must not be null"); + Assert.notNull(callbackInstances, "CallbackInstances must not be null"); + return new Callbacks<>(callbackType, callbackInstances, argument, additionalArguments); + } + + /** + * Abstract base class for lambda safe callbacks. + * + * @param the callback type + * @param the primary argument type + * @param the self class reference + */ + protected abstract static class LambdaSafeCallback> { + + private final Class callbackType; + + private final A argument; + + private final Object[] additionalArguments; + + private Log logger; + + private Filter filter = new GenericTypeFilter<>(); + + LambdaSafeCallback(Class callbackType, A argument, Object[] additionalArguments) { + this.callbackType = callbackType; + this.argument = argument; + this.additionalArguments = additionalArguments; + this.logger = LogFactory.getLog(callbackType); + } + + /** + * Use the specified logger source to report any lambda failures. + * + * @param loggerSource the logger source to use + * @return this instance + */ + public SELF withLogger(Class loggerSource) { + return withLogger(LogFactory.getLog(loggerSource)); + } + + /** + * Use the specified logger to report any lambda failures. + * + * @param logger the logger to use + * @return this instance + */ + public SELF withLogger(Log logger) { + Assert.notNull(logger, "Logger must not be null"); + this.logger = logger; + return self(); + } + + /** + * Use a specific filter to determine when a callback should apply. If no explicit filter is set filter will be + * attempted using the generic type on the callback type. + * + * @param filter the filter to use + * @return this instance + */ + SELF withFilter(Filter filter) { + Assert.notNull(filter, "Filter must not be null"); + this.filter = filter; + return self(); + } + + protected final InvocationResult invoke(C callbackInstance, Supplier supplier) { + if (this.filter.match(this.callbackType, callbackInstance, this.argument, this.additionalArguments)) { + try { + return InvocationResult.of(supplier.get()); + } catch (ClassCastException ex) { + if (!isLambdaGenericProblem(ex)) { + throw ex; + } + logNonMatchingType(callbackInstance, ex); + } + } + return InvocationResult.noResult(); + } + + private boolean isLambdaGenericProblem(ClassCastException ex) { + return (ex.getMessage() == null || startsWithArgumentClassName(ex.getMessage())); + } + + private boolean startsWithArgumentClassName(String message) { + Predicate startsWith = (argument) -> startsWithArgumentClassName(message, argument); + return startsWith.test(this.argument) || Stream.of(this.additionalArguments).anyMatch(startsWith); + } + + private boolean startsWithArgumentClassName(String message, Object argument) { + if (argument == null) { + return false; + } + Class argumentType = argument.getClass(); + // On Java 8, the message starts with the class name: "java.lang.String cannot + // be cast..." + if (message.startsWith(argumentType.getName())) { + return true; + } + // On Java 11, the message starts with "class ..." a.k.a. Class.toString() + if (message.startsWith(argumentType.toString())) { + return true; + } + // On Java 9, the message used to contain the module name: + // "java.base/java.lang.String cannot be cast..." + int moduleSeparatorIndex = message.indexOf('/'); + if (moduleSeparatorIndex != -1 && message.startsWith(argumentType.getName(), moduleSeparatorIndex + 1)) { + return true; + } + if (CLASS_GET_MODULE != null) { + Object module = ReflectionUtils.invokeMethod(CLASS_GET_MODULE, argumentType); + Object moduleName = ReflectionUtils.invokeMethod(MODULE_GET_NAME, module); + return message.startsWith(moduleName + "/" + argumentType.getName()); + } + return false; + } + + private void logNonMatchingType(C callback, ClassCastException ex) { + if (this.logger.isDebugEnabled()) { + Class expectedType = ResolvableType.forClass(this.callbackType).resolveGeneric(); + String expectedTypeName = (expectedType != null) ? ClassUtils.getShortName(expectedType) + " type" + : "type"; + String message = "Non-matching " + expectedTypeName + " for callback " + + ClassUtils.getShortName(this.callbackType) + ": " + callback; + this.logger.debug(message, ex); + } + } + + @SuppressWarnings("unchecked") + private SELF self() { + return (SELF) this; + } + + } + + /** + * Represents a single callback that can be invoked in a lambda safe way. + * + * @param the callback type + * @param the primary argument type + */ + public static final class Callback extends LambdaSafeCallback> { + + private final C callbackInstance; + + private Callback(Class callbackType, C callbackInstance, A argument, Object[] additionalArguments) { + super(callbackType, argument, additionalArguments); + this.callbackInstance = callbackInstance; + } + + /** + * Invoke the callback instance where the callback method returns void. + * + * @param invoker the invoker used to invoke the callback + */ + public void invoke(Consumer invoker) { + invoke(this.callbackInstance, () -> { + invoker.accept(this.callbackInstance); + return null; + }); + } + + /** + * Invoke the callback instance where the callback method returns a result. + * + * @param invoker the invoker used to invoke the callback + * @param the result type + * @return the result of the invocation (may be {@link InvocationResult#noResult} if the callback was not + * invoked) + */ + public InvocationResult invokeAnd(Function invoker) { + return invoke(this.callbackInstance, () -> invoker.apply(this.callbackInstance)); + } + + } + + /** + * Represents a collection of callbacks that can be invoked in a lambda safe way. + * + * @param the callback type + * @param the primary argument type + */ + public static final class Callbacks extends LambdaSafeCallback> { + + private final Collection callbackInstances; + + private Callbacks(Class callbackType, Collection callbackInstances, A argument, + Object[] additionalArguments) { + super(callbackType, argument, additionalArguments); + this.callbackInstances = callbackInstances; + } + + /** + * Invoke the callback instances where the callback method returns void. + * + * @param invoker the invoker used to invoke the callback + */ + public void invoke(Consumer invoker) { + this.callbackInstances.forEach((callbackInstance) -> { + invoke(callbackInstance, () -> { + invoker.accept(callbackInstance); + return null; + }); + }); + } + + /** + * Invoke the callback instances where the callback method returns a result. + * + * @param invoker the invoker used to invoke the callback + * @param the result type + * @return the results of the invocation (may be an empty stream if no callbacks could be called) + */ + public Stream invokeAnd(Function invoker) { + Function> mapper = (callbackInstance) -> invoke(callbackInstance, + () -> invoker.apply(callbackInstance)); + return this.callbackInstances.stream().map(mapper).filter(InvocationResult::hasResult) + .map(InvocationResult::get); + } + + } + + /** + * A filter that can be used to restrict when a callback is used. + * + * @param the callback type + * @param the primary argument type + */ + @FunctionalInterface + interface Filter { + + /** + * Determine if the given callback matches and should be invoked. + * + * @param callbackType the callback type (the functional interface) + * @param callbackInstance the callback instance (the implementation) + * @param argument the primary argument + * @param additionalArguments any additional arguments + * @return if the callback matches and should be invoked + */ + boolean match(Class callbackType, C callbackInstance, A argument, Object[] additionalArguments); + + /** + * Return a {@link Filter} that allows all callbacks to be invoked. + * + * @param the callback type + * @param the primary argument type + * @return an "allow all" filter + */ + static Filter allowAll() { + return (callbackType, callbackInstance, argument, additionalArguments) -> true; + } + + } + + /** + * {@link Filter} that matches when the callback has a single generic and primary argument is an instance of it. + */ + private static class GenericTypeFilter implements Filter { + + @Override + public boolean match(Class callbackType, C callbackInstance, A argument, Object[] additionalArguments) { + ResolvableType type = ResolvableType.forClass(callbackType, callbackInstance.getClass()); + if (type.getGenerics().length == 1 && type.resolveGeneric() != null) { + return type.resolveGeneric().isInstance(argument); + } + + return true; + } + + } + + /** + * The result of a callback which may be a value, {@code null} or absent entirely if the callback wasn't suitable. + * Similar in design to {@link Optional} but allows for {@code null} as a valid value. + * + * @param the result type + */ + public static final class InvocationResult { + + private static final InvocationResult NONE = new InvocationResult<>(null); + + private final R value; + + private InvocationResult(R value) { + this.value = value; + } + + /** + * Return true if a result in present. + * + * @return if a result is present + */ + public boolean hasResult() { + return this != NONE; + } + + /** + * Return the result of the invocation or {@code null} if the callback wasn't suitable. + * + * @return the result of the invocation or {@code null} + */ + public R get() { + return this.value; + } + + /** + * Return the result of the invocation or the given fallback if the callback wasn't suitable. + * + * @param fallback the fallback to use when there is no result + * @return the result of the invocation or the fallback + */ + public R get(R fallback) { + return (this != NONE) ? this.value : fallback; + } + + /** + * Create a new {@link InvocationResult} instance with the specified value. + * + * @param value the value (may be {@code null}) + * @param the result type + * @return an {@link InvocationResult} + */ + public static InvocationResult of(R value) { + return new InvocationResult<>(value); + } + + /** + * Return an {@link InvocationResult} instance representing no result. + * + * @param the result type + * @return an {@link InvocationResult} + */ + @SuppressWarnings("unchecked") + public static InvocationResult noResult() { + return (InvocationResult) NONE; + } + + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7d14619e..072a7c05 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ subprojects { group = 'eu.xenit.alfred.telemetry' - version = '0.1.1' + version = '0.1.2' boolean isRelease = System.env.BRANCH_NAME?.startsWith("release") if (!isRelease)