Skip to content

Commit

Permalink
Separate MeterFilter from Meters
Browse files Browse the repository at this point in the history
The MeterFilter which add the common tag application="YourApp" to all
meters is removed from the ApplicationInfoMetrics, and instead must be
configured separately when initializing your MeterRegistry in order to
ensure the filter is configured _before_ any bound Meter.
  • Loading branch information
runeflobakk committed Jun 6, 2024
1 parent 4e36d54 commit 85f1589
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 92 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.co.probablyfine</groupId>
<artifactId>java-8-matchers</artifactId>
<version>1.9</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.jar.Manifest;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static no.digipost.monitoring.micrometer.KeyValueResolver.FROM_ENVIRONMENT_VARIABLES;
import static no.digipost.monitoring.micrometer.KeyValueResolver.FROM_SYSTEM_PROPERTIES;
import static no.digipost.monitoring.micrometer.KeyValueResolver.noValue;

/**
* Adds `app.info` gauge that has several tags suitable for showing information about
Expand All @@ -40,7 +42,7 @@
*/
public class ApplicationInfoMetrics implements MeterBinder {

final Manifest manifest;
private final KeyValueResolver<String> fromRuntimeEnvironment;

/**
* This method will find your running mainclass from System.properties("sun.java.command")
Expand All @@ -52,55 +54,41 @@ public class ApplicationInfoMetrics implements MeterBinder {
* It could be that the property is missing?
*/
public ApplicationInfoMetrics() throws ClassNotFoundException {
manifest = new JarManifest();
this(null);
}

/**
* Base metrics tags of MANIFEST.MF from jar witch holds your class.
*
* @param classFromJar - Class contained in jar you want metrics from
* @param classInJar - Class contained in jar you want metrics from
*/
public ApplicationInfoMetrics(Class<?> classFromJar) {
manifest = new JarManifest(classFromJar);
public ApplicationInfoMetrics(Class<?> classInJar) {
this.fromRuntimeEnvironment = KeyValueResolver
.inOrderOfPrecedence(
FROM_SYSTEM_PROPERTIES,
FROM_ENVIRONMENT_VARIABLES,
Optional.ofNullable(classInJar)
.flatMap(JarManifest::tryResolveFromClassInJar).or(JarManifest::tryResolveAutomatically)
.map(KeyValueResolver::fromManifestMainAttributes)
.orElse(noValue()));
}


@Override
public void bindTo(MeterRegistry registry) {
fromManifestOrEnv("Implementation-Title")
.ifPresent(artifactId -> registry.config().commonTags("application", artifactId));

List<Tag> tags = new ArrayList<>();

addTagIfValuePresent(tags,"buildTime","Git-Build-Time");
addTagIfValuePresent(tags,"buildVersion","Git-Build-Version");
addTagIfValuePresent(tags,"buildNumber","Git-Commit");
addTagIfValuePresent(tags,"javaBuildVersion","Build-Jdk-Spec");

tags.add(Tag.of("javaVersion", (String) System.getProperties().get("java.version")));
List<Tag> tags = Stream.of(
fromRuntimeEnvironment.tryResolveValue("Git-Build-Time").map(buildTime -> Tag.of("buildTime", buildTime)),
fromRuntimeEnvironment.tryResolveValue("Git-Build-Version").map(buildVersion -> Tag.of("buildVersion", buildVersion)),
fromRuntimeEnvironment.tryResolveValue("Git-Commit").map(buildNumber -> Tag.of("buildNumber", buildNumber)),
fromRuntimeEnvironment.tryResolveValue("Build-Jdk-Spec").map(javaBuildVersion -> Tag.of("javaBuildVersion", javaBuildVersion)),
FROM_SYSTEM_PROPERTIES.tryResolveValue("java.version").map(javaVersion -> Tag.of("javaVersion", javaVersion)))
.flatMap(Optional::stream)
.collect(toList());

Gauge.builder("app.info", () -> 1.0d)
.description("General build and runtime information about the application. This is a static value")
.tags(tags)
.register(registry);
}

private void addTagIfValuePresent(List<Tag> tags, String tagKey, String valueName) {
fromManifestOrEnv(valueName).ifPresent(value -> tags.add(Tag.of(tagKey, value)));
}

private Optional<String> fromManifestOrEnv(String name) {
String value = environmentVariableOrSystemProperty(name);
if (value == null) {
value = manifest.getMainAttributes().getValue(name);
}
return ofNullable(value);
}

private static String environmentVariableOrSystemProperty(String name) {
String value = System.getProperty(name);
if (value == null) {
value = System.getenv(name);
}
return value;
}
}
103 changes: 83 additions & 20 deletions src/main/java/no/digipost/monitoring/micrometer/JarManifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,103 @@
*/
package no.digipost.monitoring.micrometer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.jar.Manifest;

import static java.lang.Thread.currentThread;
import static no.digipost.monitoring.micrometer.KeyValueResolver.noValue;

class JarManifest extends Manifest {

JarManifest() throws ClassNotFoundException {
this(
Class.forName(((String) System.getProperties().get("sun.java.command")).split(" ")[0], true, Thread.currentThread().getContextClassLoader())
);
private static final Logger LOG = LoggerFactory.getLogger(JarManifest.class);

static KeyValueResolver<String> tryResolveFromMainAttributes() {
return tryResolveFromMainAttributes(null);
}

static KeyValueResolver<String> tryResolveFromMainAttributes(Class<?> classInJar) {
return Optional.ofNullable(classInJar)
.flatMap(JarManifest::tryResolveFromClassInJar).or(JarManifest::tryResolveAutomatically)
.map(KeyValueResolver::fromManifestMainAttributes)
.orElse(noValue());
}

static Optional<Manifest> tryResolveAutomatically() {
return Optional.ofNullable(System.getProperty("sun.java.command"))
.map(sunJavaCommand -> sunJavaCommand.split(" ")[0])
.flatMap(className -> {
try {
return Optional.of(Class.forName(className, true, currentThread().getContextClassLoader()));
} catch (Exception e) {
LOG.info(
"Giving up resolving Manifest automatically from class name {}, because {}: {}",
className, e.getClass().getSimpleName(), e.getMessage(), e);
return Optional.empty();
}
})
.flatMap(JarManifest::tryResolveFromClassInJar);
}

static Optional<Manifest> tryResolveFromClassInJar(Class<?> classInJar) {
try {
return Optional.of(resolveFromClassInJar(classInJar));
} catch (Exception e) {
LOG.info(
"Giving up resolving Manifest from class {}, because {}: {}",
classInJar.getName(), e.getClass().getSimpleName(), e.getMessage(), e);
return Optional.empty();
}
}

JarManifest(Class<?> classFromJar) {
final String jarLocation = classFromJar.getProtectionDomain()

private static final String MANIFEST_RESOURCE_NAME = "META-INF/MANIFEST.MF";

private static final ConcurrentMap<String, Manifest> CACHED_MANIFESTS_BY_JAR_LOCATION = new ConcurrentHashMap<>();

static Manifest resolveFromClassInJar(Class<?> classInJar) {

String jarLocationForClass = classInJar.getProtectionDomain()
.getCodeSource()
.getLocation()
.toString()
.replaceAll("!/BOOT-INF/classes!/", ""); // If you have an executable jar, your main jar is exploded into a BOOT-INF-folder structure

try {
Collections.list(Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF"))
.stream()
return CACHED_MANIFESTS_BY_JAR_LOCATION.computeIfAbsent(jarLocationForClass, jarLocation -> {
LOG.debug("Trying to resolving {} for {}", MANIFEST_RESOURCE_NAME, jarLocation);
List<URL> manifestCandidates;
try {
manifestCandidates = Collections.list(currentThread().getContextClassLoader().getResources(MANIFEST_RESOURCE_NAME));
} catch (IOException e) {
throw new UncheckedIOException(
"Unable to resolve any resources with name " + MANIFEST_RESOURCE_NAME +
" because " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
}

URL manifestUrl = manifestCandidates.stream()
.filter(s -> s.toString().contains(jarLocation))
.findAny()
.ifPresent(mf -> {
try {
this.read(mf.openStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
System.err.println("Det blir ikke noe informasjon fra META-INF/MANIFEST.MF");
e.printStackTrace();
}
.orElseThrow(() -> new NoSuchElementException(
MANIFEST_RESOURCE_NAME + " expected located in " + jarLocation + ", resolved from class " + classInJar.getName()));

try (InputStream manifestStream = manifestUrl.openStream()) {
return new Manifest(manifestStream);
} catch (IOException e) {
throw new UncheckedIOException("Unable to read MANIFEST.MF from " + manifestUrl + ", resolved from class " + classInJar.getName(), e);
}
});

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (C) Posten Norge AS
*
* 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.
*/
package no.digipost.monitoring.micrometer;

import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.jar.Manifest;

@FunctionalInterface
interface KeyValueResolver<V> {

KeyValueResolver<String> FROM_ENVIRONMENT_VARIABLES = ofNullable(System::getenv);

KeyValueResolver<String> FROM_SYSTEM_PROPERTIES = ofNullable(System::getProperty);

/**
* A resolver which will never resolve any value.
*
* Prefer using {@link #noValue()}, which will provide a properly typed
* reference to this instance.
*/
KeyValueResolver<?> NO_VALUE = __ -> Optional.empty();


/**
* @return A resolver which will never resolve any value.
*/
static <V> KeyValueResolver<V> noValue() {
@SuppressWarnings("unchecked")
KeyValueResolver<V> typedResolver = (KeyValueResolver<V>) NO_VALUE;
return typedResolver;
}

static KeyValueResolver<String> fromManifestMainAttributes(Manifest manifest) {
return ofNullable(manifest.getMainAttributes()::getValue);
}


@SafeVarargs
static <V> KeyValueResolver<V> inOrderOfPrecedence(KeyValueResolver<V> ... resolvers) {
return inOrderOfPrecedence(List.of(resolvers));
}

static <V> KeyValueResolver<V> inOrderOfPrecedence(List<? extends KeyValueResolver<V>> resolvers) {
return key -> {
for (var resolver : resolvers) {
var value = resolver.tryResolveValue(key);
if (value.isPresent()) {
return value;
}
}
return Optional.empty();
};
}



static <V> KeyValueResolver<V> ofNullable(Function<String, V> nullableValueResolver) {
return of(nullableValueResolver.andThen(Optional::ofNullable));
}

static <V> KeyValueResolver<V> of(Function<? super String, Optional<V>> valueResolver) {
return key -> valueResolver.apply(key);
}


Optional<V> tryResolveValue(String key);

default <W> KeyValueResolver<W> andThen(BiFunction<? super String, ? super V, W> keyValueMapper) {
return key -> tryResolveValue(key).map(value -> keyValueMapper.apply(key, value));
}

}
Loading

0 comments on commit 85f1589

Please sign in to comment.