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}