001package io.prometheus.client.spring.web;
002
003import io.prometheus.client.Summary;
004import org.aspectj.lang.ProceedingJoinPoint;
005import org.aspectj.lang.annotation.Around;
006import org.aspectj.lang.annotation.Aspect;
007import org.aspectj.lang.annotation.Pointcut;
008import org.aspectj.lang.reflect.MethodSignature;
009import org.springframework.context.annotation.Scope;
010import org.springframework.core.annotation.AnnotationUtils;
011import org.springframework.util.ReflectionUtils;
012import org.springframework.web.bind.annotation.ControllerAdvice;
013
014import java.lang.reflect.Method;
015import java.util.HashMap;
016import java.util.concurrent.locks.Lock;
017import java.util.concurrent.locks.ReadWriteLock;
018import java.util.concurrent.locks.ReentrantReadWriteLock;
019
020/**
021 * This class automatically times (via aspectj) the execution of annotated methods, if it's been enabled via {@link EnablePrometheusTiming},
022 * for methods annotated with {@link PrometheusTimeMethod}
023 *
024 * @author Andrew Stuart
025 */
026@Aspect("pertarget(io.prometheus.client.spring.web.MethodTimer.timeable())")
027@Scope("prototype")
028@ControllerAdvice
029public class MethodTimer {
030    private final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
031    private final HashMap<String, Summary> summaries = new HashMap<String, Summary>();
032
033    @Pointcut("@annotation(io.prometheus.client.spring.web.PrometheusTimeMethod)")
034    public void annotatedMethod() {}
035
036    @Pointcut("annotatedMethod()")
037    public void timeable() {}
038
039    private PrometheusTimeMethod getAnnotation(ProceedingJoinPoint pjp) throws NoSuchMethodException {
040        assert(pjp.getSignature() instanceof MethodSignature);
041        MethodSignature signature = (MethodSignature) pjp.getSignature();
042
043        PrometheusTimeMethod annot = AnnotationUtils.findAnnotation(pjp.getTarget().getClass(), PrometheusTimeMethod.class);
044        if (annot != null) {
045            return annot;
046        }
047
048        // When target is an AOP interface proxy but annotation is on class method (rather than Interface method).
049        final String name = signature.getName();
050        final Class[] parameterTypes = signature.getParameterTypes();
051        Method method = ReflectionUtils.findMethod(pjp.getTarget().getClass(), name, parameterTypes);
052        return AnnotationUtils.findAnnotation(method, PrometheusTimeMethod.class);
053    }
054
055    private Summary ensureSummary(ProceedingJoinPoint pjp, String key) throws IllegalStateException {
056        PrometheusTimeMethod annot;
057        try {
058            annot = getAnnotation(pjp);
059        } catch (NoSuchMethodException e) {
060            throw new IllegalStateException("Annotation could not be found for pjp \"" + pjp.toShortString() +"\"", e);
061        } catch (NullPointerException e) {
062            throw new IllegalStateException("Annotation could not be found for pjp \"" + pjp.toShortString() +"\"", e);
063        }
064
065        assert(annot != null);
066
067        Summary summary;
068
069        // We use a writeLock here to guarantee no concurrent reads.
070        final Lock writeLock = summaryLock.writeLock();
071        writeLock.lock();
072        try {
073            // Check one last time with full mutual exclusion in case multiple readers got null before creation.
074            summary = summaries.get(key);
075            if (summary != null) {
076                return summary;
077            }
078
079            // Now we know for sure that we have never before registered.
080            summary = Summary.build()
081                    .name(annot.name())
082                    .help(annot.help())
083                    .register();
084
085            // Even a rehash of the underlying table will not cause issues as we mutually exclude readers while we
086            // perform our updates.
087            summaries.put(key, summary);
088
089            return summary;
090        } finally {
091            writeLock.unlock();
092        }
093    }
094
095    @Around("timeable()")
096    public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable {
097        String key = pjp.getSignature().toLongString();
098
099        Summary summary;
100        final Lock r = summaryLock.readLock();
101        r.lock();
102        try {
103            summary = summaries.get(key);
104        } finally {
105            r.unlock();
106        }
107
108        if (summary == null) {
109            summary = ensureSummary(pjp, key);
110        }
111
112        final Summary.Timer t = summary.startTimer();
113
114        try {
115            return pjp.proceed();
116        } finally {
117            t.observeDuration();
118        }
119    }
120}