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