| /** |
| * 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 org.apache.aurora.common.stats; |
| |
| import java.util.Collection; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.function.Supplier; |
| import java.util.regex.Pattern; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.cache.Cache; |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.MapMaker; |
| |
| import org.apache.aurora.common.base.MorePreconditions; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Manages {@link Stat}s that should be exported for monitoring. |
| * |
| * Statistic names may only contain {@code [A-Za-z0-9_]}, |
| * all other chars will be logged as a warning and replaced with underscore on export. |
| * |
| * @author John Sirois |
| */ |
| public class Stats { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(Stats.class); |
| private static final Pattern NOT_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_/.-]"); |
| |
| private static final ConcurrentMap<String, Stat<?>> VAR_MAP = new MapMaker().makeMap(); |
| |
| // Store stats in the order they were registered, so that derived variables are |
| // sampled after their inputs. |
| private static final Collection<RecordingStat<? extends Number>> ORDERED_NUMERIC_STATS = |
| new ConcurrentLinkedQueue<>(); |
| |
| private static final Cache<String, RecordingStat<? extends Number>> NUMERIC_STATS = |
| CacheBuilder.newBuilder().build(); |
| |
| public static String normalizeName(String name) { |
| return NOT_NAME_CHAR.matcher(name).replaceAll("_"); |
| } |
| |
| static String validateName(String name) { |
| String normalized = normalizeName(name); |
| if (!name.equals(normalized)) { |
| LOG.warn("Invalid stat name " + name + " exported as " + normalized); |
| } |
| return normalized; |
| } |
| |
| /** |
| * A {@link StatsProvider} that exports gauge-style stats to the global {@link Stat}s repository |
| * for time series tracking. |
| */ |
| public static final StatsProvider STATS_PROVIDER = new StatsProvider() { |
| private final StatsProvider untracked = new StatsProvider() { |
| @Override public AtomicLong makeCounter(String name) { |
| final AtomicLong longVar = new AtomicLong(); |
| Stats.exportStatic(new StatImpl<Long>(name) { |
| @Override public Long read() { |
| return longVar.get(); |
| } |
| }); |
| return longVar; |
| } |
| |
| @Override public <T extends Number> Stat<T> makeGauge(String name, final Supplier<T> gauge) { |
| return Stats.exportStatic(new StatImpl<T>(name) { |
| @Override public T read() { |
| return gauge.get(); |
| } |
| }); |
| } |
| |
| @Override public StatsProvider untracked() { |
| return this; |
| } |
| |
| @Override public RequestTimer makeRequestTimer(String name) { |
| // TODO(William Farner): Add support for this once a caller shows interest in using it. |
| throw new UnsupportedOperationException(); |
| } |
| }; |
| |
| @Override public <T extends Number> Stat<T> makeGauge(String name, final Supplier<T> gauge) { |
| return Stats.export(new StatImpl<T>(name) { |
| @Override public T read() { |
| return gauge.get(); |
| } |
| }); |
| } |
| |
| @Override public AtomicLong makeCounter(String name) { |
| return Stats.exportLong(name); |
| } |
| |
| @Override public StatsProvider untracked() { |
| return untracked; |
| } |
| |
| @Override public RequestTimer makeRequestTimer(String name) { |
| return new RequestStats(name); |
| } |
| }; |
| |
| /** |
| * A {@link StatRegistry} that provides stats registered with the global {@link Stat}s repository. |
| */ |
| public static final StatRegistry STAT_REGISTRY = Stats::getNumericVariables; |
| |
| private static class ExportStat implements Callable<RecordingStat<? extends Number>> { |
| private final AtomicBoolean called = new AtomicBoolean(false); |
| |
| private final RecordingStat<? extends Number> stat; |
| private final String name; |
| |
| private <T extends Number> ExportStat(String name, Stat<T> stat) { |
| this.name = name; |
| this.stat = (stat instanceof RecordingStat) |
| ? (RecordingStat<? extends Number>) stat |
| : new RecordingStatImpl<>(stat); |
| } |
| |
| @Override |
| public RecordingStat<? extends Number> call() { |
| try { |
| exportStaticInternal(name, stat); |
| ORDERED_NUMERIC_STATS.add(stat); |
| return stat; |
| } finally { |
| called.set(true); |
| } |
| } |
| } |
| |
| /** |
| * Exports a stat for tracking. |
| * if the stat provided implements the internal {@link RecordingStat} interface, it will be |
| * registered for time series collection and returned. If a {@link RecordingStat} with the same |
| * name as the provided stat has already been exported, the previously-exported stat will be |
| * returned and no additional registration will be performed. |
| * |
| * @param var The variable to export. |
| * @param <T> The value exported by the variable. |
| * @return A reference to the stat that was stored. The stat returned may not be equal to the |
| * stat provided. If a variable was already returned with the same |
| */ |
| public static <T extends Number> Stat<T> export(Stat<T> var) { |
| String validatedName = validateName(MorePreconditions.checkNotBlank(var.getName())); |
| ExportStat exportStat = new ExportStat(validatedName, var); |
| try { |
| @SuppressWarnings("unchecked") |
| Stat<T> exported = (Stat<T>) NUMERIC_STATS.get(validatedName, exportStat); |
| return exported; |
| } catch (ExecutionException e) { |
| throw new IllegalStateException( |
| "Unexpected error exporting stat " + validatedName, e.getCause()); |
| } finally { |
| if (!exportStat.called.get()) { |
| LOG.warn("Re-using already registered variable for key " + validatedName); |
| } |
| } |
| } |
| |
| /** |
| * Exports a string stat. |
| * String-based statistics will not be registered for time series collection. |
| * |
| * @param var Stat to export. |
| * @return A reference back to {@code var}, or the variable that was already registered under the |
| * same name as {@code var}. |
| */ |
| static Stat<String> exportString(Stat<String> var) { |
| return exportStatic(var); |
| } |
| |
| /** |
| * Adds a collection of stats for export. |
| * |
| * @param vars The variables to add. |
| */ |
| static void exportAll(Iterable<Stat<? extends Number>> vars) { |
| for (Stat<? extends Number> var : vars) { |
| export(var); |
| } |
| } |
| |
| /** |
| * Creates and exports an {@link AtomicInteger}. |
| * |
| * @param name The name to export the stat with. |
| * @return A reference to the {@link AtomicInteger} created. |
| */ |
| public static AtomicInteger exportInt(final String name) { |
| final AtomicInteger intVar = new AtomicInteger(0); |
| export(new SampledStat<Integer>(name, 0) { |
| @Override public Integer doSample() { return intVar.get(); } |
| }); |
| |
| return intVar; |
| } |
| |
| /** |
| * Creates and exports an {@link AtomicLong}. |
| * |
| * @param name The name to export the stat with. |
| * @return A reference to the {@link AtomicLong} created. |
| */ |
| public static AtomicLong exportLong(String name) { |
| final AtomicLong longVar = new AtomicLong(0L); |
| export(new StatImpl<Long>(name) { |
| @Override public Long read() { return longVar.get(); } |
| }); |
| |
| return longVar; |
| } |
| |
| /** |
| * Exports a 'static' statistic, which will not be registered for time series tracking. |
| * |
| * @param var Variable to statically export. |
| * @return A reference back to the provided {@link Stat}. |
| */ |
| static <T> Stat<T> exportStatic(Stat<T> var) { |
| String validatedName = validateName(MorePreconditions.checkNotBlank(var.getName())); |
| exportStaticInternal(validatedName, var); |
| return var; |
| } |
| |
| private static void exportStaticInternal(String name, Stat<?> stat) { |
| if (VAR_MAP.put(name, stat) != null) { |
| LOG.warn("Warning - exported variable collision on " + name); |
| } |
| } |
| |
| /** |
| * Fetches all registered stat. |
| * |
| * @return An iterable of all registered stats. |
| */ |
| public static Iterable<Stat<?>> getVariables() { |
| return ImmutableList.copyOf(VAR_MAP.values()); |
| } |
| |
| static Iterable<RecordingStat<? extends Number>> getNumericVariables() { |
| return ImmutableList.copyOf(ORDERED_NUMERIC_STATS); |
| } |
| |
| @VisibleForTesting |
| public static void flush() { |
| VAR_MAP.clear(); |
| ORDERED_NUMERIC_STATS.clear(); |
| NUMERIC_STATS.invalidateAll(); |
| } |
| |
| @VisibleForTesting |
| public static <T> Stat<T> getVariable(String name) { |
| MorePreconditions.checkNotBlank(name); |
| @SuppressWarnings("unchecked") |
| Stat<T> stat = (Stat<T>) VAR_MAP.get(name); |
| return stat; |
| } |
| } |