Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Meter.Cache for dynamic meter registration #4092

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -18,6 +18,7 @@
import io.micrometer.common.lang.Nullable;

import java.util.Collections;
import java.util.function.Function;

/**
* Counters monitor monotonically increasing values. Counters may never be reset to a
Expand Down Expand Up @@ -128,6 +129,14 @@ public Builder baseUnit(@Nullable String unit) {
* @return A new or existing counter.
*/
public Counter register(MeterRegistry registry) {
return register(registry, tags);
}

public <K> Meter.Provider<K, Counter> register(MeterRegistry registry, Function<K, Tags> provider) {
return new Meter.Cache<>(key -> register(registry, tags.and(provider.apply(key))));
}

private Counter register(MeterRegistry registry, Tags tags) {
return registry.counter(new Meter.Id(name, tags, baseUnit, description, Type.COUNTER));
}

Expand Down
Expand Up @@ -24,6 +24,7 @@
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
* Track the sample distribution of events. An example would be the response sizes for
Expand Down Expand Up @@ -395,6 +396,14 @@ public Builder scale(double scale) {
* @return A new or existing distribution summary.
*/
public DistributionSummary register(MeterRegistry registry) {
return register(registry, tags);
}

public <K> Meter.Provider<K, DistributionSummary> register(MeterRegistry registry, Function<K, Tags> provider) {
return new Meter.Cache<>(key -> register(registry, tags.and(provider.apply(key))));
}

private DistributionSummary register(MeterRegistry registry, Tags tags) {
return registry.summary(new Meter.Id(name, tags, baseUnit, description, Type.DISTRIBUTION_SUMMARY),
distributionConfigBuilder.build(), scale);
}
Expand Down
Expand Up @@ -20,6 +20,7 @@
import io.micrometer.core.instrument.distribution.HistogramGauges;

import java.util.Collections;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;

Expand Down Expand Up @@ -192,6 +193,19 @@ public Builder<T> strongReference(boolean strong) {
* @return A new or existing gauge.
*/
public Gauge register(MeterRegistry registry) {
return register(registry, obj, tags);
}

public <K> Meter.Provider<K, T> register(MeterRegistry registry, Supplier<T> generator,
Function<K, Tags> provider) {
return new Meter.Cache<>(key -> {
final T obj = generator.get();
register(registry, obj, tags.and(provider.apply(key)));
return obj;
});
}

private Gauge register(MeterRegistry registry, T obj, Tags tags) {
return registry.gauge(new Meter.Id(name, tags, baseUnit, description, Type.GAUGE, syntheticAssociation),
obj, strongReference ? new StrongReferenceGaugeFunction<>(obj, f) : f);
}
Expand Down
Expand Up @@ -23,7 +23,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -483,4 +485,27 @@ public Meter register(MeterRegistry registry) {
default void close() {
}

interface Provider<K, V> {

V get(@Nullable K key);

}

class Cache<K, V> implements Provider<K, V> {

private final Map<K, V> cache = new ConcurrentHashMap<>();

private final Function<K, V> mappingFunction;

public Cache(Function<K, V> mappingFunction) {
this.mappingFunction = mappingFunction;
}

@Override
public V get(@Nullable K key) {
return cache.computeIfAbsent(key, mappingFunction);
}

}

}
Expand Up @@ -29,6 +29,7 @@
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.function.DoubleSupplier;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
Expand Down Expand Up @@ -439,6 +440,14 @@ public Builder description(String description) {
* @return A new or existing timer.
*/
public Timer register(MeterRegistry registry) {
return register(registry, tags);
}

public <K> Meter.Provider<K, Timer> register(MeterRegistry registry, Function<K, Tags> provider) {
return new Meter.Cache<>(key -> register(registry, tags.and(provider.apply(key))));
}

private Timer register(MeterRegistry registry, Tags tags) {
// the base unit for a timer will be determined by the monitoring system
// implementation
return registry.timer(new Meter.Id(name, tags, null, description, Type.TIMER),
Expand Down
@@ -0,0 +1,167 @@
/*
* Copyright 2023 VMware, Inc.
*
* 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 io.micrometer.core.instrument;

import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link Meter.Cache}.
*
* @author qweek
*/
public class MeterCacheTest {

private MeterRegistry registry = new SimpleMeterRegistry();

@Test
void provideCounter() {
Meter.Provider<String, Counter> counterProvider = Counter.builder("my.counter")
.register(registry, value -> value == null ? Tags.empty() : Tags.of("my.key", value));

assertThat(registry.getMeters()).hasSize(0);

Counter firstCounter = counterProvider.get("first.value");
firstCounter.increment();

assertThat(registry.find("my.counter").counter()).isEqualTo(firstCounter);
assertThat(firstCounter.count()).isEqualTo(1);

Counter sameCounter = counterProvider.get("first.value");
sameCounter.increment();

assertThat(registry.getMeters()).hasSize(1);
assertThat(sameCounter.count()).isEqualTo(2);

Counter secondCounter = counterProvider.get("second.value");
secondCounter.increment();

assertThat(registry.getMeters()).hasSize(2);
assertThat(secondCounter.count()).isEqualTo(1);
}

@Test
void provideTimer() {
Meter.Provider<String, Timer> timerProvider = Timer.builder("my.timer")
.register(registry, value -> value == null ? Tags.empty() : Tags.of("my.key", value));

assertThat(registry.getMeters()).hasSize(0);

Timer firstTimer = timerProvider.get("first.value");
firstTimer.record(1, TimeUnit.NANOSECONDS);

assertThat(registry.find("my.timer").timer()).isEqualTo(firstTimer);
assertThat(firstTimer.count()).isEqualTo(1);

Timer sameTimer = timerProvider.get("first.value");
sameTimer.record(2, TimeUnit.NANOSECONDS);

assertThat(registry.getMeters()).hasSize(1);
assertThat(sameTimer.count()).isEqualTo(2);

Timer secondTimer = timerProvider.get("second.value");
secondTimer.record(4, TimeUnit.NANOSECONDS);

assertThat(registry.getMeters()).hasSize(2);
assertThat(secondTimer.count()).isEqualTo(1);
}

@Test
void provideSummary() {
Meter.Provider<String, DistributionSummary> summaryProvider = DistributionSummary.builder("my.summary")
.register(registry, value -> value == null ? Tags.empty() : Tags.of("my.key", value));

assertThat(registry.getMeters()).hasSize(0);

DistributionSummary firstSummary = summaryProvider.get("first.value");
firstSummary.record(1);

assertThat(registry.find("my.summary").summary()).isEqualTo(firstSummary);
assertThat(firstSummary.count()).isEqualTo(1);

DistributionSummary sameSummary = summaryProvider.get("first.value");
sameSummary.record(2);

assertThat(registry.getMeters()).hasSize(1);
assertThat(sameSummary.count()).isEqualTo(2);

DistributionSummary secondSummary = summaryProvider.get("second.value");
secondSummary.record(4);

assertThat(registry.getMeters()).hasSize(2);
assertThat(secondSummary.count()).isEqualTo(1);
}

@Test
void provideGauge() {
Meter.Provider<String, AtomicInteger> gaugeProvider = Gauge
.builder("my.gauge", null, AtomicInteger::doubleValue)
.register(registry, AtomicInteger::new, value -> value == null ? Tags.empty() : Tags.of("my.key", value));

assertThat(registry.getMeters()).hasSize(0);

AtomicInteger firstGauge = gaugeProvider.get("first.value");
firstGauge.set(1);

assertThat(firstGauge.get()).isEqualTo(1);

AtomicInteger sameGauge = gaugeProvider.get("first.value");

assertThat(registry.getMeters()).hasSize(1);
assertThat(sameGauge).isEqualTo(firstGauge);

AtomicInteger secondGauge = gaugeProvider.get("second.value");

assertThat(registry.getMeters()).hasSize(2);
assertThat(secondGauge).isNotEqualTo(firstGauge);
}

@Test
void provideCounterWithoutDuplicateTags() {
Meter.Provider<String, Counter> counterProvider = Counter.builder("my.counter")
.tag("my.default.tag", "my.default.value")
.register(registry, value -> value == null ? Tags.empty() : Tags.of("my.key", value));

assertThat(registry.getMeters()).hasSize(0);

Counter firstCounter = counterProvider.get("first.value");
firstCounter.increment();

assertThat(firstCounter.getId().getTags()).hasSize(2);
assertThat(firstCounter.getId().getTags()).contains(Tag.of("my.default.tag", "my.default.value"),
Tag.of("my.key", "first.value"));

Counter sameCounter = counterProvider.get("first.value");
sameCounter.increment();

assertThat(sameCounter.getId().getTags()).hasSize(2);
assertThat(sameCounter.getId().getTags()).contains(Tag.of("my.default.tag", "my.default.value"),
Tag.of("my.key", "first.value"));

Counter secondCounter = counterProvider.get("second.value");
secondCounter.increment();

assertThat(secondCounter.getId().getTags()).hasSize(2);
assertThat(secondCounter.getId().getTags()).contains(Tag.of("my.default.tag", "my.default.value"),
Tag.of("my.key", "second.value"));
}

}