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}