-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Service level objective meter registry (fixes #2055)
- Loading branch information
Jon Schneider
committed
May 4, 2020
1 parent
4765490
commit 8285634
Showing
16 changed files
with
1,738 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
dependencies { | ||
api project(':micrometer-core') | ||
|
||
implementation 'org.slf4j:slf4j-api' | ||
|
||
testImplementation project(':micrometer-test') | ||
} |
38 changes: 38 additions & 0 deletions
38
...entations/micrometer-registry-health/src/main/java/io/micrometer/health/HealthConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** | ||
* Copyright 2020 VMware, Inc. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.health; | ||
|
||
import io.micrometer.core.instrument.config.MeterRegistryConfig; | ||
|
||
import java.time.Duration; | ||
|
||
import static io.micrometer.core.instrument.config.validate.PropertyValidator.getDuration; | ||
|
||
public interface HealthConfig extends MeterRegistryConfig { | ||
HealthConfig DEFAULT = key -> null; | ||
|
||
@Override | ||
default String prefix() { | ||
return "health"; | ||
} | ||
|
||
/** | ||
* @return The step size to use. | ||
*/ | ||
default Duration step() { | ||
return getDuration(this, "step").orElse(Duration.ofSeconds(10)); | ||
} | ||
} |
209 changes: 209 additions & 0 deletions
209
...ns/micrometer-registry-health/src/main/java/io/micrometer/health/HealthMeterRegistry.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
/** | ||
* Copyright 2020 VMware, Inc. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.health; | ||
|
||
import io.micrometer.core.annotation.Incubating; | ||
import io.micrometer.core.instrument.Clock; | ||
import io.micrometer.core.instrument.Meter; | ||
import io.micrometer.core.instrument.binder.MeterBinder; | ||
import io.micrometer.core.instrument.config.MeterFilter; | ||
import io.micrometer.core.instrument.config.MeterFilterReply; | ||
import io.micrometer.core.instrument.simple.CountingMode; | ||
import io.micrometer.core.instrument.simple.SimpleConfig; | ||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; | ||
import io.micrometer.core.instrument.util.NamedThreadFactory; | ||
import io.micrometer.core.lang.Nullable; | ||
|
||
import java.time.Duration; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.ScheduledExecutorService; | ||
import java.util.concurrent.ThreadFactory; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* Configured with a set of queries, provides an overall health indicator given a set of | ||
* service level objectives. | ||
* <p> | ||
* For efficiency, this registry automatically denies all metrics that aren't part of the definition of | ||
* a service level objective. | ||
* <p> | ||
* Service level objectives can specify one or more {@link MeterBinder} that they require to be registered | ||
* in order to perform their tests. These are automatically bound at construction time. | ||
* | ||
* @author Jon Schneider | ||
* @since 1.6.0 | ||
*/ | ||
@Incubating(since = "1.6.0") | ||
public class HealthMeterRegistry extends SimpleMeterRegistry { | ||
private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("health-metrics-ticker"); | ||
|
||
private final HealthConfig config; | ||
private final Collection<ServiceLevelObjective> serviceLevelObjectives; | ||
private final Collection<MeterFilter> serviceLevelObjectiveFilters; | ||
|
||
@Nullable | ||
private ScheduledExecutorService scheduledExecutorService; | ||
|
||
protected HealthMeterRegistry(HealthConfig config, Collection<ServiceLevelObjective> serviceLevelObjectives, | ||
Collection<MeterFilter> serviceLevelObjectiveFilters, | ||
Clock clock, ThreadFactory threadFactory) { | ||
super(new SimpleConfig() { | ||
@Override | ||
public String get(String key) { | ||
return null; | ||
} | ||
|
||
@Override | ||
public Duration step() { | ||
return config.step(); | ||
} | ||
|
||
@Override | ||
public final CountingMode mode() { | ||
return CountingMode.STEP; | ||
} | ||
}, clock); | ||
|
||
config.requireValid(); | ||
|
||
this.config = config; | ||
this.serviceLevelObjectives = serviceLevelObjectives; | ||
this.serviceLevelObjectiveFilters = serviceLevelObjectiveFilters; | ||
|
||
for (ServiceLevelObjective slo : serviceLevelObjectives) { | ||
for (MeterFilter filter : slo.getAcceptFilters()) { | ||
config().meterFilter(filter); | ||
} | ||
} | ||
|
||
// deny all metrics that aren't specifically indicators used to measure SLOs | ||
config().meterFilter(MeterFilter.deny()); | ||
|
||
// do this after the deny filter is set, because maybe only a portion of the metrics a binder registers are needed | ||
// for the SLOs that require the binder | ||
for (ServiceLevelObjective slo : serviceLevelObjectives) { | ||
for (MeterBinder require : slo.getRequires()) { | ||
require.bindTo(this); | ||
} | ||
} | ||
|
||
start(threadFactory); | ||
} | ||
|
||
@Override | ||
protected TimeUnit getBaseTimeUnit() { | ||
return TimeUnit.NANOSECONDS; | ||
} | ||
|
||
public static Builder builder(HealthConfig config) { | ||
return new Builder(config); | ||
} | ||
|
||
public static class Builder { | ||
private final HealthConfig config; | ||
private final Collection<ServiceLevelObjective> serviceLevelObjectives = new ArrayList<>(); | ||
private final Collection<MeterFilter> serviceLevelObjectiveFilters = new ArrayList<>(); | ||
private Clock clock = Clock.SYSTEM; | ||
private ThreadFactory threadFactory = DEFAULT_THREAD_FACTORY; | ||
|
||
Builder(HealthConfig config) { | ||
this.config = config; | ||
} | ||
|
||
public Builder clock(Clock clock) { | ||
this.clock = clock; | ||
return this; | ||
} | ||
|
||
public Builder threadFactory(ThreadFactory threadFactory) { | ||
this.threadFactory = threadFactory; | ||
return this; | ||
} | ||
|
||
public Builder serviceLevelObjectives(ServiceLevelObjective... slos) { | ||
this.serviceLevelObjectives.addAll(Arrays.asList(slos)); | ||
return this; | ||
} | ||
|
||
public Builder serviceLevelObjectiveFilter(MeterFilter filter) { | ||
this.serviceLevelObjectiveFilters.add(filter); | ||
return this; | ||
} | ||
|
||
public HealthMeterRegistry build() { | ||
return new HealthMeterRegistry(config, serviceLevelObjectives, serviceLevelObjectiveFilters, clock, threadFactory); | ||
} | ||
} | ||
|
||
void tick() { | ||
serviceLevelObjectives.forEach(slo -> slo.tick(this)); | ||
} | ||
|
||
public Collection<ServiceLevelObjective> getServiceLevelObjectives() { | ||
return serviceLevelObjectives.stream() | ||
.filter(slo -> accept(slo.getId())) | ||
.map(slo -> | ||
serviceLevelObjectiveFilters.stream() | ||
.reduce( | ||
slo, | ||
(filtered, filter) -> new ServiceLevelObjective.FilteredServiceLevelObjective( | ||
filter.map(filtered.getId()), | ||
filtered | ||
), | ||
(obj1, obj2) -> obj2 | ||
) | ||
) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private boolean accept(Meter.Id id) { | ||
for (MeterFilter filter : serviceLevelObjectiveFilters) { | ||
MeterFilterReply reply = filter.accept(id); | ||
if (reply == MeterFilterReply.DENY) { | ||
return false; | ||
} else if (reply == MeterFilterReply.ACCEPT) { | ||
return true; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
public void start(ThreadFactory threadFactory) { | ||
if (scheduledExecutorService != null) | ||
stop(); | ||
|
||
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(threadFactory); | ||
scheduledExecutorService.scheduleAtFixedRate(this::tick, config.step() | ||
.toMillis(), config.step().toMillis(), TimeUnit.MILLISECONDS); | ||
} | ||
|
||
public void stop() { | ||
if (scheduledExecutorService != null) { | ||
scheduledExecutorService.shutdown(); | ||
scheduledExecutorService = null; | ||
} | ||
} | ||
|
||
@Override | ||
public void close() { | ||
stop(); | ||
super.close(); | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
...ementations/micrometer-registry-health/src/main/java/io/micrometer/health/QueryUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/** | ||
* Copyright 2020 VMware, Inc. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.health; | ||
|
||
import java.util.function.BinaryOperator; | ||
|
||
class QueryUtils { | ||
public static final BinaryOperator<Double> SUM_OR_NAN = (v1, v2) -> { | ||
if (Double.isNaN(v1)) { | ||
if (Double.isNaN(v2)) { | ||
return Double.NaN; | ||
} | ||
return v2; | ||
} else if (Double.isNaN(v2)) { | ||
return v1; | ||
} | ||
return v1 + v2; | ||
}; | ||
|
||
public static final BinaryOperator<Double> MAX_OR_NAN = (v1, v2) -> { | ||
if (Double.isNaN(v1)) { | ||
if (Double.isNaN(v2)) { | ||
return Double.NaN; | ||
} | ||
return v2; | ||
} else if (Double.isNaN(v2)) { | ||
return v1; | ||
} | ||
return Math.max(v1, v2); | ||
}; | ||
|
||
private QueryUtils() { | ||
} | ||
} |
Oops, something went wrong.