Skip to content

Commit

Permalink
Merge pull request #34 from digipost/separate-appinfo-and-application…
Browse files Browse the repository at this point in the history
…-tag

Separate MeterFilter from Meters
  • Loading branch information
runeflobakk authored Jun 11, 2024
2 parents 4e36d54 + bd48a53 commit c62ce4a
Show file tree
Hide file tree
Showing 11 changed files with 517 additions and 131 deletions.
113 changes: 84 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,47 @@
![](https://github.com/digipost/digipost-micrometer-prometheus/workflows/Build%20and%20deploy/badge.svg)
[![License](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/digipost/digipost-micrometer-prometheus/blob/main/LICENCE)


# digipost-micrometer-prometheus


## Common application tag MeterFilter

To include the **name of your application in all reported metrics**, make sure to configure this as
early as possible before any meters are bound to your `MeterRegistry`. E.g. configure this where you _create_ your registry:

```java
MeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
MeterFilters.tryIncludeApplicationNameCommonTag().ifPresentOrElse(
config()::meterFilter,
() -> LOG.warn("Unable to include application common tag in MeterRegistry"));
```

This will try to determine the name of your application by resolving the value of the `Implementation-Title` key in the
`MANIFEST.MF` file of the JAR file containing the class which started your application.
You also have the option to provide a class yourself instead of relying on this being automatically discovered. The class should
be located in the JAR which also contains the `MANIFEST.MF` which contains the `Implementation-Title` you would like to use as
your application name.

The example above logs a warning should this discovery mechanism fail to resolve your application name. You may choose to handle
this in any way depending on your preference, e.g. throw an exception instead of just logging.

You can also skip all this automatic discovery, and just **supply the name of your application** when configuring the filter:

```java
MeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
meterRegistry.config().meterFilter(MeterFilters.includeApplicationNameCommonTag("my-application"))
```

See PR #34 for more examples on how to configure the filter.




## Micrometer metrics

Usage in a `MeterRegistry`:
Usage with a `MeterRegistry`:

```java
new ApplicationInfoMetrics().bindTo(this);
```
Expand All @@ -17,25 +53,30 @@ This is what is expected to exist in the manifest or as key value environment va

```
Build-Jdk-Spec: 12
Implementation-Title: my-application
Git-Build-Time: 2019-12-19T22:52:05+0100
Git-Build-Version: 1.2.3
Git-Commit: ffb9099
```

This will create this metric in Prometheus running java 11:

```
# HELP app_info General build and runtime information about the application. This is a static value
# TYPE app_info gauge
app_info{application="my-application",buildNumber="ffb9099",buildTime="2019-12-19T22:52:05+0100",buildVersion="1.2.3",javaBuildVersion="12",javaVersion="11",} 1.0
app_info{application="my-application",buildNumber="ffb9099",buildTime="2019-12-19T22:52:05+0100",buildVersion="1.2.3",javaBuildVersion="12",javaVersion="11"} 1.0
```

(Note, `application="my-application"` will only be included if you configured the "Common application tag MeterFilter" described previously.)

The following metric will be created if no values are present in the manifest or environment variables:

```
# HELP app_info General build and runtime information about the application. This is a static value
# TYPE app_info gauge
app_info{javaVersion="11",} 1.0
```
app_info{javaVersion="11"} 1.0
```



## Simple Prometheus server

Expand All @@ -45,71 +86,85 @@ To start the server you need your instance of `PrometheusMeterRegistry` and a po

```java
new SimplePrometheusServer(LOG::info)
.startMetricsServer(
prometheusRegistry, 9610
);
```
.startMetricsServer(prometheusRegistry, 9610);
```




## TimedThirdPartyCall

With `TimedThirdPartyCall` you can wrap your code to get metrics on the call with extended funtionality on top of what
With `TimedThirdPartyCall` you can wrap your code to get metrics on the call with extended funtionality on top of what
micrometer Timed gives you.


An example:

```java
final BiFunction<MyResponse, Optional<RuntimeException>, AppStatus> warnOnSituation = (response, possibleException) -> possibleException.isPresent() || "ERROR_SITUATION".equals(response.data) ? AppStatus.WARN : AppStatus.OK;
BiFunction<MyResponse, Optional<RuntimeException>, AppStatus> warnOnSituation =
(response, possibleException) -> possibleException.isPresent() || "ERROR_SITUATION".equals(response.data) ? AppStatus.WARN : AppStatus.OK;

final TimedThirdPartyCall<MyResponse> getStuff = TimedThirdPartyCallDescriptor.create("ExternalService", "getStuff", prometheusRegistry)
TimedThirdPartyCall<MyResponse> getStuff = TimedThirdPartyCallDescriptor
.create("ExternalService", "getStuff", prometheusRegistry)
.callResponseStatus(warnOnSituation);

getStuff.call(() -> new MyResponse("ERROR_SITUATION"));
```
```

This will produce a number of metrics:

```
app_third_party_call_total{name="ExternalService_getStuff", status="OK"} 0.0
app_third_party_call_total{name="ExternalService_getStuff", status="WARN"} 1.0
app_third_party_call_total{name="ExternalService_getStuff", status="FAILED"} 0.0
app_third_party_call_seconds_count{name="ExternalService_getStuff",} 1.0
app_third_party_call_seconds_sum{name="ExternalService_getStuff",} 6.6018E-5
app_third_party_call_seconds_max{name="ExternalService_getStuff",} 6.6018E-5
app_third_party_call_seconds_count{name="ExternalService_getStuff"} 1.0
app_third_party_call_seconds_sum{name="ExternalService_getStuff"} 6.6018E-5
app_third_party_call_seconds_max{name="ExternalService_getStuff"} 6.6018E-5
```

The idea is that Timed only count exections overall. What we want in addition is finer granularity to create better alerts
in our alerting rig. By specifying a function by witch we say OK/WARN/FAILED we can exclude error-situations
in our alerting rig. By specifying a function by witch we say OK/WARN/FAILED we can exclude error-situations
that we want to igore from alerts reacting to `FAILED` or a percentage of `FAILED/TOTAL`.

You can also use simple exception-mapper-function for a boolean OK/FAILED:

```java
TimedThirdPartyCall<String> getStuff = TimedThirdPartyCallDescriptor.create("ExternalService", "getStuff", prometheusRegistry)
.exceptionAsFailure();
TimedThirdPartyCall<String> getStuff = TimedThirdPartyCallDescriptor
.create("ExternalService", "getStuff", prometheusRegistry)
.exceptionAsFailure();

String result = getStuff.call(() -> "OK");
```

For timing `void` functions you can use `NoResultTimedThirdPartyCall`,
acquired by invoking the `noResult()` method:

```java
NoResultTimedThirdPartyCall voidFunction = TimedThirdPartyCallDescriptor.create("ExternalService", "voidFunction", prometheusRegistry)
NoResultTimedThirdPartyCall voidFunction = TimedThirdPartyCallDescriptor
.create("ExternalService", "voidFunction", prometheusRegistry)
.noResult() // allows timing void function calls
.exceptionAsFailure();

voidFunction.call(() -> {});
```

You can also defined percentiles (default 0.5, 0.95, 0.99):

```java
TimedThirdPartyCallDescriptor.create("ExternalService", "getStuff", prometheusRegistry)
TimedThirdPartyCallDescriptor
.create("ExternalService", "getStuff", prometheusRegistry)
.callResponseStatus(warnOnSituation, 0.95, 0.99);
```



## MetricsUpdater

Often you want to have metrics that might be slow to get. Examples of this is count rows in a Postgres-database or
maybe stats from a keystore. Typically you want to have som kind of worker threat that updates this
Often you want to have metrics that might be slow to get. Examples of this is count rows in a Postgres-database or
maybe stats from a keystore. Typically you want to have som kind of worker threat that updates this
value on a regular basis. But how do you know that your worker thread is not stuck?
For this you can use the `MetricsUpdater` class. Create an instance of it and specify the number of threads you want. Now
registrer a runnable at an interval.
For this you can use the `MetricsUpdater` class. Create an instance of it and specify the number of threads you want. Now
registrer a runnable at an interval.

```java
metricsUpdater.registerAsyncUpdate("count-table", Duration.ofMinutes(10), () -> {
Expand All @@ -130,7 +185,7 @@ You can the alert if this is stale:
Sometimes you want to have metrics for some event that happens in your application. And sometimes you want som kind of
alert or warning when they occur at a given rate. This implementation is a way to achieve that in a generic way.

Your application need to implement the interface `AppBusinessEvent`. We usually do that with an enum so that we have
Your application need to implement the interface `AppBusinessEvent`. We usually do that with an enum so that we have
easy access to the instance of the event. You can se a complete implementation of this in `AppBusinessEventLoggerTest`.
You can also use the interface `AppSensorEvent` to add a multiplier score (severity) to an event.

Expand All @@ -141,10 +196,10 @@ eventLogger.log(MyBusinessEvents.VIOLATION_WITH_WARN);

This should produce a prometheus scrape output like this:
```
# HELP app_business_events_1min_warn_thresholds
# HELP app_business_events_1min_warn_thresholds
# TYPE app_business_events_1min_warn_thresholds gauge
app_business_events_1min_warn_thresholds{name="VIOLATION_WITH_WARN",} 5.0
# HELP app_business_events_total
# HELP app_business_events_total
# TYPE app_business_events_total counter
app_business_events_total{name="VIOLATION_WITH_WARN",} 1.0
```
Expand Down Expand Up @@ -215,7 +270,7 @@ This will produce the following metrics during prometheus scraping, in addition
# TYPE logger_events_1min_threshold gauge
logger_events_5min_threshold{application="my-application",level="warn",logger="ROOT",} 10.0
logger_events_5min_threshold{application="my-application",level="error",logger="ROOT",} 5.0
```
```

These metrics can be used for alerting in combination with the metrics above. Prometheus expression:
```
Expand Down
14 changes: 13 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-bom</artifactId>
<version>1.13.0</version>
<version>1.13.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down 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;
}
}
Loading

0 comments on commit c62ce4a

Please sign in to comment.