All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micrometer.health.ServiceLevelObjective Maven / Gradle / Ivy

There is a newer version: 1.13.5
Show newest version
/*
 * Copyright 2020 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.health;

import io.micrometer.common.lang.Nullable;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.distribution.HistogramSupport;
import io.micrometer.core.instrument.distribution.ValueAtPercentile;
import io.micrometer.core.instrument.search.Search;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;

import static io.micrometer.health.QueryUtils.MAX_OR_NAN;
import static io.micrometer.health.QueryUtils.SUM_OR_NAN;

/**
 * Service level objective.
 *
 * @author Jon Schneider
 * @author Johnny Lim
 * @since 1.6.0
 */
public abstract class ServiceLevelObjective {

    private static final ThreadLocal WHOLE_OR_SHORT_DECIMAL = ThreadLocal.withInitial(() -> {
        DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(Locale.US);
        return new DecimalFormat("##0.##", otherSymbols);
    });

    private final String name;

    private final Tags tags;

    @Nullable
    private final String baseUnit;

    /**
     * Describes what it means for this service level objective to not be met.
     */
    @Nullable
    private final String failedMessage;

    private final Meter.Id id;

    protected ServiceLevelObjective(String name, Tags tags, @Nullable String baseUnit, @Nullable String failedMessage) {
        this.name = name;
        this.tags = tags;
        this.baseUnit = baseUnit;
        this.failedMessage = failedMessage;
        this.id = new Meter.Id(name, tags, baseUnit, failedMessage, Meter.Type.GAUGE);
    }

    public String getName() {
        return name;
    }

    public Iterable getTags() {
        return tags;
    }

    @Nullable
    public String getBaseUnit() {
        return baseUnit;
    }

    public Meter.Id getId() {
        return id;
    }

    @Nullable
    public String getFailedMessage() {
        return failedMessage;
    }

    public abstract Collection getRequires();

    public abstract Collection getAcceptFilters();

    public abstract void tick(MeterRegistry registry);

    public abstract boolean healthy(MeterRegistry registry);

    public static MultipleIndicator.Builder compose(String name, ServiceLevelObjective... objectives) {
        return new MultipleIndicator.Builder(name, objectives);
    }

    public static SingleIndicator.Builder build(String name) {
        return new SingleIndicator.Builder(name);
    }

    public static class SingleIndicator extends ServiceLevelObjective {

        private final NumericQuery query;

        private final Collection requires;

        private final String testDescription;

        private final Predicate test;

        protected SingleIndicator(NumericQuery query, String testDescription, Predicate test) {
            super(query.name, query.tags, query.baseUnit, query.failedMessage);
            this.query = query;
            this.requires = query.requires;
            this.testDescription = testDescription;
            this.test = test;
        }

        @Override
        public boolean healthy(MeterRegistry registry) {
            Double v = getValue(registry);
            return v.isNaN() || test.test(v);
        }

        @Override
        public void tick(MeterRegistry registry) {
            query.tick(registry);
        }

        @Override
        public Collection getRequires() {
            return requires;
        }

        @Override
        public Collection getAcceptFilters() {
            return query.acceptFilters();
        }

        public double getValue(MeterRegistry registry) {
            return query.getValue(registry);
        }

        public String getValueAsString(MeterRegistry registry) {
            double value = getValue(registry);
            return Double.isNaN(value) ? "no value available"
                    : getBaseUnit() != null && getBaseUnit().toLowerCase().contains("percent")
                            ? WHOLE_OR_SHORT_DECIMAL.get().format(value * 100) + "%"
                            : WHOLE_OR_SHORT_DECIMAL.get().format(value);
        }

        public String getTestDescription() {
            return testDescription;
        }

        static SingleIndicator testNumeric(NumericQuery query, String testDescription, Predicate test) {
            return new SingleIndicator(query, testDescription, test);
        }

        static SingleIndicator testDuration(NumericQuery query, String testDescription, Predicate test) {
            return new SingleIndicator(query, testDescription,
                    valueNanos -> valueNanos.isNaN() || test.test(Duration.ofNanos(valueNanos.longValue())));
        }

        public static class Builder {

            private final String name;

            private Tags tags = Tags.empty();

            @Nullable
            private String baseUnit;

            @Nullable
            private String failedMessage;

            private final Collection requires;

            Builder(String name) {
                this(name, null, new ArrayList<>());
            }

            Builder(String name, @Nullable String failedMessage, Collection requires) {
                this.name = name;
                this.failedMessage = failedMessage;
                this.requires = requires;
            }

            public final Builder failedMessage(@Nullable String failedMessage) {
                this.failedMessage = failedMessage;
                return this;
            }

            public final Builder requires(MeterBinder... requires) {
                Collections.addAll(this.requires, requires);
                return this;
            }

            public final Builder baseUnit(@Nullable String baseUnit) {
                this.baseUnit = baseUnit;
                return this;
            }

            /**
             * @param tags Must be an even number of arguments representing key/value
             * pairs of tags.
             * @return This builder.
             */
            public final Builder tags(String... tags) {
                return tags(Tags.of(tags));
            }

            /**
             * @param tags Tags to add to the single indicator.
             * @return The builder with added tags.
             */
            public final Builder tags(Iterable tags) {
                this.tags = this.tags.and(tags);
                return this;
            }

            /**
             * @param key The tag key.
             * @param value The tag value.
             * @return The single indicator builder with a single added tag.
             */
            public final Builder tag(String key, String value) {
                this.tags = tags.and(key, value);
                return this;
            }

            public final NumericQuery count(Function search) {
                return new Instant(name, tags, baseUnit, failedMessage, requires, search,
                        s -> s.meters().stream().map(m -> {
                            if (m instanceof Counter) {
                                return ((Counter) m).count();
                            }
                            else if (m instanceof Timer) {
                                return (double) ((Timer) m).count();
                            }
                            else if (m instanceof FunctionTimer) {
                                return ((FunctionTimer) m).count();
                            }
                            else if (m instanceof FunctionCounter) {
                                return ((FunctionCounter) m).count();
                            }
                            else if (m instanceof LongTaskTimer) {
                                return (double) ((LongTaskTimer) m).activeTasks();
                            }
                            return Double.NaN;
                        }).reduce(Double.NaN, SUM_OR_NAN));
            }

            public NumericQuery errorRatio(Function searchAll, Function searchErrors) {
                return count(searchAll.andThen(searchErrors)).dividedBy(over -> over.count(searchAll));
            }

            public final NumericQuery total(Function search) {
                return new Instant(name, tags, baseUnit, failedMessage, requires, search,
                        s -> s.meters().stream().map(m -> {
                            if (m instanceof DistributionSummary) {
                                return ((DistributionSummary) m).totalAmount();
                            }
                            else if (m instanceof Timer) {
                                return ((Timer) m).totalTime(TimeUnit.NANOSECONDS);
                            }
                            else if (m instanceof LongTaskTimer) {
                                return ((LongTaskTimer) m).duration(TimeUnit.NANOSECONDS);
                            }
                            return Double.NaN;
                        }).reduce(Double.NaN, SUM_OR_NAN));
            }

            public final NumericQuery maxPercentile(Function search, double percentile) {
                return new Instant(name, tags, baseUnit, failedMessage, requires, search,
                        s -> s.meters().stream().map(m -> {
                            if (!(m instanceof HistogramSupport)) {
                                return Double.NaN;
                            }

                            ValueAtPercentile[] valueAtPercentiles = ((HistogramSupport) m).takeSnapshot()
                                .percentileValues();
                            return Arrays.stream(valueAtPercentiles)
                                .filter(vap -> vap.percentile() == percentile)
                                .map(ValueAtPercentile::value)
                                .findAny()
                                .orElse(Double.NaN);
                        }).reduce(Double.NaN, MAX_OR_NAN));
            }

            public final NumericQuery max(Function search) {
                return new Instant(name, tags, baseUnit, failedMessage, requires, search,
                        s -> s.meters().stream().map(m -> {
                            if (m instanceof DistributionSummary) {
                                return ((DistributionSummary) m).max();
                            }
                            else if (m instanceof Timer) {
                                return ((Timer) m).max(TimeUnit.NANOSECONDS);
                            }
                            else if (m instanceof LongTaskTimer) {
                                return ((LongTaskTimer) m).max(TimeUnit.NANOSECONDS);
                            }
                            return Double.NaN;
                        }).reduce(Double.NaN, MAX_OR_NAN));
            }

            /**
             * @param search The search criteria for a {@link Gauge}.
             * @return The value of the first matching gauge time series.
             */
            public final NumericQuery value(Function search) {
                return new Instant(name, tags, baseUnit, failedMessage, requires, search,
                        s -> s.meters().stream().map(m -> {
                            if (m instanceof TimeGauge) {
                                return ((TimeGauge) m).value(TimeUnit.NANOSECONDS);
                            }
                            else if (m instanceof Gauge) {
                                return ((Gauge) m).value();
                            }
                            return Double.NaN;
                        }).filter(n -> !Double.isNaN(n)).findAny().orElse(Double.NaN));
            }

        }

        public abstract static class NumericQuery {

            protected final String name;

            private final Tags tags;

            @Nullable
            private final String baseUnit;

            @Nullable
            private final String failedMessage;

            private final Collection requires;

            NumericQuery(String name, Tags tags, @Nullable String baseUnit, @Nullable String failedMessage,
                    Collection requires) {
                this.name = name;
                this.tags = tags;
                this.baseUnit = baseUnit;
                this.failedMessage = failedMessage;
                this.requires = requires;
            }

            abstract Double getValue(MeterRegistry registry);

            private String thresholdString(double threshold) {
                return baseUnit != null && baseUnit.toLowerCase().contains("percent")
                        ? WHOLE_OR_SHORT_DECIMAL.get().format(threshold * 100) + "%"
                        : WHOLE_OR_SHORT_DECIMAL.get().format(threshold);
            }

            public final SingleIndicator isLessThan(double threshold) {
                return SingleIndicator.testNumeric(this, "<" + thresholdString(threshold), v -> v < threshold);
            }

            public final SingleIndicator isLessThanOrEqualTo(double threshold) {
                return SingleIndicator.testNumeric(this, "<=" + thresholdString(threshold), v -> v <= threshold);
            }

            public final SingleIndicator isGreaterThan(double threshold) {
                return SingleIndicator.testNumeric(this, ">" + thresholdString(threshold), v -> v > threshold);
            }

            public final SingleIndicator isGreaterThanOrEqualTo(double threshold) {
                return SingleIndicator.testNumeric(this, ">=" + thresholdString(threshold), v -> v >= threshold);
            }

            public final SingleIndicator isEqualTo(double threshold) {
                return SingleIndicator.testNumeric(this, "==" + thresholdString(threshold), v -> v == threshold);
            }

            public final SingleIndicator isLessThan(Duration threshold) {
                return SingleIndicator.testDuration(this, "<" + threshold, v -> v.compareTo(threshold) < 0);
            }

            public final SingleIndicator isLessThanOrEqualTo(Duration threshold) {
                return SingleIndicator.testDuration(this, "<=" + threshold, v -> v.compareTo(threshold) <= 0);
            }

            public final SingleIndicator isGreaterThan(Duration threshold) {
                return SingleIndicator.testDuration(this, ">" + threshold, v -> v.compareTo(threshold) > 0);
            }

            public final SingleIndicator isGreaterThanOrEqualTo(Duration threshold) {
                return SingleIndicator.testDuration(this, ">=" + threshold, v -> v.compareTo(threshold) >= 0);
            }

            public final SingleIndicator isEqualTo(Duration threshold) {
                return SingleIndicator.testDuration(this, "==" + threshold, v -> v.compareTo(threshold) == 0);
            }

            public final SingleIndicator test(String thresholdDescription, Predicate threshold) {
                return SingleIndicator.testNumeric(this, thresholdDescription, threshold);
            }

            public final SingleIndicator testDuration(String thresholdDescription, Predicate threshold) {
                return SingleIndicator.testDuration(this, thresholdDescription, threshold);
            }

            public final NumericQuery dividedBy(Function over) {
                return new ArithmeticOp(this, over.apply(new Builder(name, failedMessage, requires)),
                        (v1, v2) -> v1 / v2);
            }

            public final NumericQuery multipliedBy(Function by) {
                return new ArithmeticOp(this, by.apply(new Builder(name, failedMessage, requires)),
                        (v1, v2) -> v1 * v2);
            }

            public final NumericQuery plus(Function with) {
                return new ArithmeticOp(this, with.apply(new Builder(name, failedMessage, requires)), Double::sum);
            }

            public final NumericQuery minus(Function with) {
                return new ArithmeticOp(this, with.apply(new Builder(name, failedMessage, requires)),
                        (v1, v2) -> v1 - v2);
            }

            public final NumericQuery combineWith(Function with,
                    BinaryOperator combiner) {
                return new ArithmeticOp(this, with.apply(new Builder(name, failedMessage, requires)), combiner);
            }

            public final NumericQuery maxOver(Duration interval) {
                return new OverInterval(this, interval, vs -> vs.max().orElse(Double.NaN));
            }

            public final NumericQuery minOver(Duration interval) {
                return new OverInterval(this, interval, vs -> vs.min().orElse(Double.NaN));
            }

            public final NumericQuery sumOver(Duration interval) {
                return new OverInterval(this, interval, DoubleStream::sum);
            }

            public final NumericQuery averageOver(Duration interval) {
                return new OverInterval(this, interval, vs -> vs.average().orElse(Double.NaN));
            }

            abstract Collection acceptFilters();

            abstract void tick(MeterRegistry registry);

        }

        static class Instant extends NumericQuery {

            private static final CompositeMeterRegistry NOOP_REGISTRY = new CompositeMeterRegistry(Clock.SYSTEM);

            private final Function search;

            private final Function toValue;

            Instant(String name, Tags tags, @Nullable String baseUnit, @Nullable String failedMessage,
                    Collection requires, Function search,
                    Function toValue) {
                super(name, tags, baseUnit, failedMessage, requires);
                this.search = search;
                this.toValue = toValue;
            }

            protected Double getValue(MeterRegistry registry) {
                return toValue.apply(search.apply(Search.in(registry)));
            }

            @Override
            public Collection acceptFilters() {
                return Collections.singleton(search.apply(Search.in(NOOP_REGISTRY)).acceptFilter());
            }

            @Override
            public void tick(MeterRegistry registry) {
                // do nothing because the value is always determined from the current
                // instant
            }

        }

        static class ArithmeticOp extends NumericQuery {

            private final NumericQuery left;

            private final NumericQuery right;

            private BinaryOperator combiner;

            ArithmeticOp(NumericQuery left, NumericQuery right, BinaryOperator combiner) {
                super(left.name, left.tags, left.baseUnit, left.failedMessage, left.requires);
                this.left = left;
                this.right = right;
                this.combiner = combiner;
            }

            @Override
            protected Double getValue(MeterRegistry registry) {
                return combiner.apply(left.getValue(registry), right.getValue(registry));
            }

            @Override
            public Collection acceptFilters() {
                List filters = new ArrayList<>();
                filters.addAll(left.acceptFilters());
                filters.addAll(right.acceptFilters());
                return filters;
            }

            @Override
            public void tick(MeterRegistry registry) {
                left.tick(registry);
                right.tick(registry);
            }

        }

        static class OverInterval extends NumericQuery {

            private final Deque samples = new ConcurrentLinkedDeque<>();

            private final NumericQuery numericQuery;

            private final Duration interval;

            private final Function collector;

            OverInterval(NumericQuery q, Duration interval, Function collector) {
                super(q.name, q.tags, q.baseUnit, q.failedMessage, q.requires);
                this.numericQuery = q;
                this.interval = interval;
                this.collector = collector;
            }

            private static class Sample {

                private final long tick;

                private final double sample;

                private Sample(long tick, double sample) {
                    this.tick = tick;
                    this.sample = sample;
                }

            }

            @Override
            protected Double getValue(MeterRegistry registry) {
                return collector.apply(samples.stream().mapToDouble(s -> s.sample).filter(n -> !Double.isNaN(n)));
            }

            @Override
            public Collection acceptFilters() {
                return numericQuery.acceptFilters();
            }

            @Override
            public void tick(MeterRegistry registry) {
                long time = registry.config().clock().monotonicTime();

                Sample first = samples.peekFirst();
                if (first != null && Duration.ofNanos(time - first.tick).compareTo(interval) > 0) {
                    samples.removeFirst();
                }

                samples.addLast(new Sample(time, numericQuery.getValue(registry)));
            }

        }

    }

    public static class MultipleIndicator extends ServiceLevelObjective {

        private final ServiceLevelObjective[] objectives;

        private final BinaryOperator combiner;

        MultipleIndicator(String name, Tags tags, @Nullable String failedMessage, ServiceLevelObjective[] objectives,
                BinaryOperator combiner) {
            super(name, tags, null, failedMessage);
            this.objectives = objectives;
            this.combiner = combiner;
        }

        @Override
        public boolean healthy(MeterRegistry registry) {
            return Arrays.stream(objectives).map(o -> o.healthy(registry)).reduce(combiner).orElse(true);
        }

        @Override
        public Collection getRequires() {
            return Arrays.stream(objectives).flatMap(o -> o.getRequires().stream()).collect(Collectors.toList());
        }

        public ServiceLevelObjective[] getObjectives() {
            return objectives;
        }

        @Override
        public Collection getAcceptFilters() {
            return Arrays.stream(objectives).flatMap(o -> o.getAcceptFilters().stream()).collect(Collectors.toList());
        }

        @Override
        public void tick(MeterRegistry registry) {
            for (ServiceLevelObjective objective : objectives) {
                objective.tick(registry);
            }
        }

        public static class Builder {

            private final String name;

            private Tags tags = Tags.empty();

            private final ServiceLevelObjective[] objectives;

            @Nullable
            private String failedMessage;

            Builder(String name, ServiceLevelObjective[] objectives) {
                this.name = name;
                this.objectives = objectives;
            }

            public final Builder failedMessage(@Nullable String failedMessage) {
                this.failedMessage = failedMessage;
                return this;
            }

            /**
             * @param tags Must be an even number of arguments representing key/value
             * pairs of tags.
             * @return This builder.
             */
            public Builder tags(String... tags) {
                return tags(Tags.of(tags));
            }

            /**
             * @param tags Tags to add to the multiple indicator.
             * @return The builder with added tags.
             */
            public Builder tags(Iterable tags) {
                this.tags = this.tags.and(tags);
                return this;
            }

            /**
             * @param key The tag key.
             * @param value The tag value.
             * @return The builder with a single added tag.
             */
            public Builder tag(String key, String value) {
                this.tags = tags.and(key, value);
                return this;
            }

            public final MultipleIndicator and() {
                return new MultipleIndicator(name, tags, failedMessage, objectives, (o1, o2) -> o1 && o2);
            }

            public final MultipleIndicator or() {
                return new MultipleIndicator(name, tags, failedMessage, objectives, (o1, o2) -> o1 || o2);
            }

            /**
             * Combine {@link ServiceLevelObjective ServiceLevelObjectives} with the
             * provided {@code combiner}.
             * @param combiner combiner to combine {@link ServiceLevelObjective
             * ServiceLevelObjectives}
             * @return combined {@code MultipleIndicator}
             * @since 1.6.5
             */
            public final MultipleIndicator combine(BinaryOperator combiner) {
                return new MultipleIndicator(name, tags, failedMessage, objectives, combiner);
            }

        }

    }

    static class FilteredServiceLevelObjective extends ServiceLevelObjective {

        private final ServiceLevelObjective delegate;

        FilteredServiceLevelObjective(Meter.Id id, ServiceLevelObjective delegate) {
            super(id.getName(), Tags.of(id.getTags()), id.getBaseUnit(), id.getDescription());
            this.delegate = delegate;
        }

        @Override
        public Collection getRequires() {
            return delegate.getRequires();
        }

        @Override
        public Collection getAcceptFilters() {
            return delegate.getAcceptFilters();
        }

        @Override
        public void tick(MeterRegistry registry) {
            delegate.tick(registry);
        }

        @Override
        public boolean healthy(MeterRegistry registry) {
            return delegate.healthy(registry);
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy