// Copyright 2017 JanusGraph Authors // // 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.janusgraph.diskstorage.util; import java.io.IOException; import java.util.List; import java.util.Map; import org.janusgraph.diskstorage.BackendException; import org.janusgraph.diskstorage.Entry; import org.janusgraph.diskstorage.EntryList; import org.janusgraph.diskstorage.keycolumnvalue.*; import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.google.common.base.Preconditions; import org.janusgraph.diskstorage.StaticBuffer; import org.janusgraph.util.stats.MetricManager; /** * This class instruments an arbitrary KeyColumnValueStore backend with Metrics. * The cumulative runtime of, number of invocations of, and number of exceptions * thrown by each interface method are instrumented with Metrics (using Timer, * Counter, and Counter again, respectively). The Metric names are generated by * calling {@link MetricRegistry#name(backendClass, methodName, identifier)}, * where methodName is the exact name of the method including capitalization, * and identifier is "time", "calls", or "exceptions". * <p/> * In addition to the three standard metrics, {@code getSlice} and * {@code getKeys} have some additional metrics related to their return values. * {@code getSlice} carries metrics with the identifiers "entries-returned" and * "entries-histogram". The first is a counter of total Entry objects returned. * The second is a histogram of the size of Entry lists returned. * {@code getKeys} returns a {@link RecordIterator} that manages metrics for its * methods. * <p/> * This implementation does not catch any exceptions. Exceptions emitted by the * backend store implementation are guaranteed to pass through this * implementation's methods. * <p/> * The implementation includes repeated {@code try...catch} boilerplate that * could be reduced by using reflection to determine the method name and by * delegating Metrics object handling to a common helper that takes a Callable * closure, but I'm not sure that the extra complexity and potential performance * hit is worth it. * * @author Dan LaRocque <dalaro@hopcount.org> */ public class MetricInstrumentedStore implements KeyColumnValueStore { private final KeyColumnValueStore backend; private static final Logger log = LoggerFactory.getLogger(MetricInstrumentedStore.class); public static final String M_CONTAINS_KEY = "containsKey"; public static final String M_GET_SLICE = "getSlice"; public static final String M_MUTATE = "mutate"; public static final String M_ACQUIRE_LOCK = "acquireLock"; public static final String M_GET_KEYS = "getKeys"; public static final String M_GET_PART = "getLocalKeyPartition"; public static final String M_CLOSE = "close"; public static final List<String> OPERATION_NAMES = ImmutableList.of(M_CONTAINS_KEY,M_GET_SLICE,M_MUTATE,M_ACQUIRE_LOCK,M_GET_KEYS); public static final String M_CALLS = "calls"; public static final String M_TIME = "time"; public static final String M_EXCEPTIONS = "exceptions"; public static final String M_ENTRIES_COUNT = "entries-returned"; public static final String M_ENTRIES_HISTO = "entries-histogram"; public static final List<String> EVENT_NAMES = ImmutableList.of(M_CALLS,M_TIME,M_EXCEPTIONS,M_ENTRIES_COUNT,M_ENTRIES_HISTO); public static final String M_ITERATOR = "iterator"; private final String metricsStoreName; public MetricInstrumentedStore(KeyColumnValueStore backend, String metricsStoreName) { this.backend = backend; this.metricsStoreName = metricsStoreName; log.debug("Wrapped Metrics named \"{}\" around store {}", metricsStoreName, backend); } @Override public EntryList getSlice(final KeySliceQuery query, final StoreTransaction txh) throws BackendException { return runWithMetrics(txh, metricsStoreName, M_GET_SLICE, new StorageCallable<EntryList>() { public EntryList call() throws BackendException { EntryList result = backend.getSlice(query, txh); recordSliceMetrics(txh, result); return result; } } ); } @Override public Map<StaticBuffer,EntryList> getSlice(final List<StaticBuffer> keys, final SliceQuery query, final StoreTransaction txh) throws BackendException { return runWithMetrics(txh, metricsStoreName, M_GET_SLICE, new StorageCallable<Map<StaticBuffer,EntryList>>() { public Map<StaticBuffer,EntryList> call() throws BackendException { Map<StaticBuffer,EntryList> results = backend.getSlice(keys, query, txh); for (EntryList result : results.values()) { recordSliceMetrics(txh, result); } return results; } } ); } @Override public void mutate(final StaticBuffer key, final List<Entry> additions, final List<StaticBuffer> deletions, final StoreTransaction txh) throws BackendException { runWithMetrics(txh, metricsStoreName, M_MUTATE, new StorageCallable<Void>() { public Void call() throws BackendException { backend.mutate(key, additions, deletions, txh); return null; } } ); } @Override public void acquireLock(final StaticBuffer key, final StaticBuffer column, final StaticBuffer expectedValue, final StoreTransaction txh) throws BackendException { runWithMetrics(txh, metricsStoreName, M_ACQUIRE_LOCK, new StorageCallable<Void>() { public Void call() throws BackendException { backend.acquireLock(key, column, expectedValue, txh); return null; } } ); } @Override public KeyIterator getKeys(final KeyRangeQuery query, final StoreTransaction txh) throws BackendException { return runWithMetrics(txh, metricsStoreName, M_GET_KEYS, new StorageCallable<KeyIterator>() { public KeyIterator call() throws BackendException { KeyIterator ki = backend.getKeys(query, txh); if (txh.getConfiguration().hasGroupName()) { return MetricInstrumentedIterator.of(ki,txh.getConfiguration().getGroupName(),metricsStoreName,M_GET_KEYS,M_ITERATOR); } else { return ki; } } } ); } @Override public KeyIterator getKeys(final SliceQuery query, final StoreTransaction txh) throws BackendException { return runWithMetrics(txh, metricsStoreName, M_GET_KEYS, new StorageCallable<KeyIterator>() { public KeyIterator call() throws BackendException { KeyIterator ki = backend.getKeys(query, txh); if (txh.getConfiguration().hasGroupName()) { return MetricInstrumentedIterator.of(ki,txh.getConfiguration().getGroupName(),metricsStoreName,M_GET_KEYS,M_ITERATOR); } else { return ki; } } } ); } @Override public String getName() { return backend.getName(); } @Override public void close() throws BackendException { backend.close(); } private void recordSliceMetrics(StoreTransaction txh, List<Entry> row) { if (!txh.getConfiguration().hasGroupName()) return; String p = txh.getConfiguration().getGroupName(); final MetricManager mgr = MetricManager.INSTANCE; mgr.getCounter(p, metricsStoreName, M_GET_SLICE, M_ENTRIES_COUNT).inc(row.size()); mgr.getHistogram(p, metricsStoreName, M_GET_SLICE, M_ENTRIES_HISTO).update(row.size()); } static <T> T runWithMetrics(StoreTransaction txh, String storeName, String name, StorageCallable<T> impl) throws BackendException { if (!txh.getConfiguration().hasGroupName()) { return impl.call(); } String prefix = txh.getConfiguration().getGroupName(); Preconditions.checkNotNull(name); Preconditions.checkNotNull(impl); final MetricManager mgr = MetricManager.INSTANCE; mgr.getCounter(prefix, storeName, name, M_CALLS).inc(); final Timer.Context tc = mgr.getTimer(prefix, storeName, name, M_TIME).time(); try { return impl.call(); } catch (BackendException e) { mgr.getCounter(prefix, storeName, name, M_EXCEPTIONS).inc(); throw e; } catch (RuntimeException e) { mgr.getCounter(prefix, storeName, name, M_EXCEPTIONS).inc(); throw e; } finally { tc.stop(); } } static <T> T runWithMetrics(String prefix, String storeName, String name, IOCallable<T> impl) throws IOException { if (null == prefix) { return impl.call(); } Preconditions.checkNotNull(name); Preconditions.checkNotNull(impl); final MetricManager mgr = MetricManager.INSTANCE; mgr.getCounter(prefix, storeName, name, M_CALLS).inc(); final Timer.Context tc = mgr.getTimer(prefix, storeName, name, M_TIME).time(); try { return impl.call(); } catch (IOException e) { mgr.getCounter(prefix, storeName, name, M_EXCEPTIONS).inc(); throw e; } finally { tc.stop(); } } static <T> T runWithMetrics(String prefix, String storeName, String name, UncheckedCallable<T> impl) { if (null == prefix) { return impl.call(); } Preconditions.checkNotNull(name); Preconditions.checkNotNull(impl); final MetricManager mgr = MetricManager.INSTANCE; mgr.getCounter(prefix, storeName, name, M_CALLS).inc(); final Timer.Context tc = mgr.getTimer(prefix, storeName, name, M_TIME).time(); try { return impl.call(); } catch (RuntimeException e) { mgr.getCounter(prefix, storeName, name, M_EXCEPTIONS).inc(); throw e; } finally { tc.stop(); } } }