package de.otto.edison.cache.controller; import com.github.benmanes.caffeine.cache.CaffeineSpec; import com.github.benmanes.caffeine.cache.Policy; import com.github.benmanes.caffeine.cache.stats.CacheStats; import de.otto.edison.cache.configuration.CaffeineCacheConfig; import de.otto.edison.navigation.NavBar; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.CachePublicMetrics; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import javax.annotation.PostConstruct; import java.util.*; import java.util.concurrent.TimeUnit; import static de.otto.edison.navigation.NavBarItem.bottom; import static de.otto.edison.navigation.NavBarItem.navBarItem; import static java.lang.String.valueOf; import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toList; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.GET; /** * Controller that is responsible for serving cache metrics in JSON or HTML format. * * @since 0.76.0 */ @Controller @ConditionalOnBean(CaffeineCacheConfig.class) public class CacheInfoController { @Autowired CachePublicMetrics cacheMetrics; @Autowired(required = false) List<CaffeineCacheConfig> cacheConfigs; @Autowired(required = false) List<CaffeineCache> caffeineCaches; @Autowired NavBar rightNavBar; @PostConstruct public void postConstruct() { rightNavBar.register(navBarItem(bottom(), "Cache Statistics", "/internal/cacheinfos")); if (cacheConfigs == null) { cacheConfigs = emptyList(); } if (caffeineCaches == null) { caffeineCaches = emptyList(); } } @RequestMapping(value = "/internal/cacheinfos", method = GET, produces = APPLICATION_JSON_VALUE) @ResponseBody public Map<String,CacheInfo> getCacheMetricsJson() { return enrichWithCacheSpecification(toCacheInfos(cacheMetrics)); } @RequestMapping(value = "/internal/cacheinfos", method = GET, produces = MediaType.ALL_VALUE) @ResponseBody public ModelAndView getCacheMetricsHtml() { final Map<String, CacheInfo> metrics = getCacheMetricsJson(); final List<CacheInfo> cacheInfos = metrics.values() .stream() .sorted(comparing(CacheInfo::getName)) .collect(toList()); return new ModelAndView( "internal/cacheinfos", "cacheInfos", cacheInfos); } private Map<String, CacheInfo> enrichWithCacheSpecification(final Map<String, CacheInfo> cacheInfos) { for (final String cacheName : cacheInfos.keySet()) { final Optional<CaffeineCacheConfig> cacheConfig = cacheConfigs .stream() .filter(c -> c.cacheName.equals(cacheName)) .findAny(); if (cacheConfig.isPresent()) { cacheInfos.get(cacheName).setSpecification(cacheConfig.get().toMap()); } else { Optional<CaffeineCache> cache = caffeineCaches .stream() .filter(c -> c.getName().equals(cacheName)) .findAny(); if (cache.isPresent()) { Policy<Object, Object> policy = cache.get().getNativeCache().policy(); cacheInfos.get(cacheName).setSpecification( new LinkedHashMap<String,String>() {{ policy.eviction().ifPresent(eviction -> { if (eviction.isWeighted()) { put("maximumWeight", valueOf(eviction.getMaximum())); } else { put("maximumSize", valueOf(eviction.getMaximum())); } }); put("recordStats", valueOf(policy.isRecordingStats())); policy.expireAfterAccess().ifPresent(expire -> { put("expireAfterAccess", expire.getExpiresAfter(SECONDS) + "s"); }); policy.expireAfterWrite().ifPresent(expire -> { put("expireAfterWrite", expire.getExpiresAfter(SECONDS) + "s"); }); policy.refreshAfterWrite().ifPresent(refresh -> { put("refreshAfterWrite", refresh.getExpiresAfter(SECONDS) + "s"); }); }} ); } } } return cacheInfos; } private Map<String, CacheInfo> toCacheInfos(final CachePublicMetrics cacheMetrics) { final Map<String, CacheInfo> cacheInfos = new LinkedHashMap<>(); cacheMetrics.metrics().forEach(m->{ final String name = m.getName().substring("cache.".length()); int pos = name.indexOf('.'); final String cacheName = name.substring(0, pos); final String metricName = toCamelHumps(normalizeDotsAndDashes(name, pos)) ; if (!cacheInfos.containsKey(cacheName)) { cacheInfos.put(cacheName, new CacheInfo(cacheName)); } cacheInfos.get(cacheName).setMetric(metricName, m.getValue()); }); return cacheInfos; } private String normalizeDotsAndDashes(final String name, final int pos) { return name.substring(pos+1).replace(".", "_").replace("-", "_"); } private String toCamelHumps(final String input) { final StringBuilder sb = new StringBuilder(); boolean isFirst = true; for( String word : input.split("_") ) { if (isFirst) { sb.append(word.substring(0, 1)); isFirst = false; } else { sb.append(word.substring(0, 1).toUpperCase()); } sb.append( word.substring(1).toLowerCase() ); } return sb.toString(); } }