/** * 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.scheduler.storage.db; import java.util.Properties; import java.util.function.Function; import javax.annotation.Nonnull; import javax.inject.Inject; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import org.apache.aurora.common.stats.SlidingStats; import org.apache.aurora.common.util.Clock; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Objects.requireNonNull; /** * A Mybatis Executor invocation interceptor that exports timing information for update and query * mapped statements. * * Currently intercepting the following invocations: * 1. update(MappedStatement ms, Object parameter) * 2. query(MappedStatement ms, Object parameter, RowBounds rowBounds, * ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) * 3. query(MappedStatement ms, Object parameter, RowBounds rowBounds, * ResultHandler resultHandler) * 4. queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) * * more signatures can be added from: org.apache.ibatis.executors */ @Intercepts({ @Signature( type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class}) }) public class InstrumentingInterceptor implements Interceptor { private static final String INVALID_INVOCATION_METRIC_NAME = "invalid_invocations"; private static final String STATS_NAME_PREFIX = "mybatis."; private static final Logger LOG = LoggerFactory.getLogger(InstrumentingInterceptor.class); private final Clock clock; private final LoadingCache<String, SlidingStats> stats; @Inject public InstrumentingInterceptor(Clock clock) { this(clock, (String name) -> new SlidingStats(name, "nanos")); } @VisibleForTesting public InstrumentingInterceptor(Clock clock, Function<String, SlidingStats> statsFactory) { this.clock = requireNonNull(clock); this.stats = CacheBuilder.newBuilder().build(new CacheLoader<String, SlidingStats>() { @Override public SlidingStats load(String statsName) { return statsFactory.apply(STATS_NAME_PREFIX + statsName); } }); } private String generateStatsName(Invocation invocation) { if (firstArgumentIsMappedStatement(invocation)) { MappedStatement statement = (MappedStatement) invocation.getArgs()[0]; return statement.getId(); } LOG.warn("Received invocation for unknown or invalid target. Invocation target: {}. " + "Invocation method: {}. Using metric name '{}' instead.", invocation.getTarget(), invocation.getMethod(), INVALID_INVOCATION_METRIC_NAME); return INVALID_INVOCATION_METRIC_NAME; } private boolean firstArgumentIsMappedStatement(Invocation invocation) { return invocation != null && invocation.getArgs() != null && invocation.getArgs()[0] instanceof MappedStatement; } @Override public Object intercept(@Nonnull Invocation invocation) throws Throwable { long start = clock.nowNanos(); try { return invocation.proceed(); } finally { String statsName = generateStatsName(invocation); SlidingStats stat = stats.get(statsName); stat.accumulate(clock.nowNanos() - start); } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // intentionally left empty as instructed in http://www.mybatis.org/mybatis-3/configuration.html } }