From 9d60629d44967e541d963195fe43e1846dfa6279 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 7 Feb 2022 11:13:59 +0200 Subject: [PATCH] feat(jans-auth-server): added new stat response service with test https://github.com/JanssenProject/jans/issues/317 --- .../service/stat/StatResponseService.java | 131 ++++++++++++++++++ .../io/jans/as/server/ws/rs/stat/StatWS.java | 102 +------------- .../service/stat/StatResponseServiceTest.java | 41 ++++++ 3 files changed, 175 insertions(+), 99 deletions(-) create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/stat/StatResponseService.java create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/service/stat/StatResponseServiceTest.java diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/stat/StatResponseService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/stat/StatResponseService.java new file mode 100644 index 00000000000..a462dfa0d53 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/stat/StatResponseService.java @@ -0,0 +1,131 @@ +package io.jans.as.server.service.stat; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import io.jans.as.common.model.stat.StatEntry; +import io.jans.as.server.ws.rs.stat.StatResponse; +import io.jans.as.server.ws.rs.stat.StatResponseItem; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.search.filter.Filter; +import net.agkn.hll.HLL; +import org.slf4j.Logger; + +import javax.ejb.DependsOn; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static io.jans.as.model.util.Util.escapeLog; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +@DependsOn("appInitializer") +@Named +public class StatResponseService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private StatService statService; + + private final Cache responseCache = CacheBuilder + .newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); + + public StatResponse buildResponse(List months) { + final String cacheKey = months.toString(); + final StatResponse cachedResponse = responseCache.getIfPresent(cacheKey); + if (cachedResponse != null) { + return cachedResponse; + } + + StatResponse response = new StatResponse(); + for (String month : months) { + final StatResponseItem responseItem = buildItem(month); + if (responseItem != null) { + response.getResponse().put(month, responseItem); + } + } + + responseCache.put(cacheKey, response); + return response; + } + + private StatResponseItem buildItem(String month) { + try { + String monthlyDn = String.format("ou=%s,%s", escapeLog(month), statService.getBaseDn()); + + final List entries = entryManager.findEntries(monthlyDn, StatEntry.class, Filter.createPresenceFilter("jansId")); + if (entries == null || entries.isEmpty()) { + log.trace("Can't find stat entries for month: {}", monthlyDn); + return null; + } + + final StatResponseItem responseItem = new StatResponseItem(); + responseItem.setMonthlyActiveUsers(userCardinality(entries)); + responseItem.setMonth(month); + + unionTokenMapIntoResponseItem(entries, responseItem); + + return responseItem; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + + private long userCardinality(List entries) { + HLL hll = decodeHll(entries.get(0)); + + // Union hll + if (entries.size() > 1) { + for (int i = 1; i < entries.size(); i++) { + hll.union(decodeHll(entries.get(i))); + } + } + return hll.cardinality(); + } + + private HLL decodeHll(StatEntry entry) { + try { + return HLL.fromBytes(Base64.getDecoder().decode(entry.getUserHllData())); + } catch (Exception e) { + log.error("Failed to decode HLL data, entry dn: {}, data: {}", entry.getDn(), entry.getUserHllData()); + return statService.newHll(); + } + } + + + private void unionTokenMapIntoResponseItem(List entries, StatResponseItem responseItem) { + for (StatEntry entry : entries) { + entry.getStat().getTokenCountPerGrantType().entrySet().stream().filter(en -> en.getValue() != null).forEach(en -> { + final Map tokenMap = responseItem.getTokenCountPerGrantType().get(en.getKey()); + if (tokenMap == null) { + responseItem.getTokenCountPerGrantType().put(en.getKey(), en.getValue()); + return; + } + for (Map.Entry tokenEntry : en.getValue().entrySet()) { + final Long counter = tokenMap.get(tokenEntry.getKey()); + if (counter == null) { + tokenMap.put(tokenEntry.getKey(), tokenEntry.getValue()); + continue; + } + + tokenMap.put(tokenEntry.getKey(), counter + tokenEntry.getValue()); + } + }); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/ws/rs/stat/StatWS.java b/jans-auth-server/server/src/main/java/io/jans/as/server/ws/rs/stat/StatWS.java index 059eeb93274..3579915a988 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/ws/rs/stat/StatWS.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/ws/rs/stat/StatWS.java @@ -1,8 +1,5 @@ package io.jans.as.server.ws.rs.stat; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import io.jans.as.common.model.stat.StatEntry; import io.jans.as.model.common.ComponentType; import io.jans.as.model.config.Constants; import io.jans.as.model.configuration.AppConfiguration; @@ -10,15 +7,13 @@ import io.jans.as.model.token.TokenErrorResponseType; import io.jans.as.server.model.common.AbstractToken; import io.jans.as.server.model.common.AuthorizationGrant; +import io.jans.as.server.service.stat.StatResponseService; import io.jans.as.server.service.stat.StatService; import io.jans.as.server.service.token.TokenService; import io.jans.as.server.util.ServerUtil; -import io.jans.orm.PersistenceEntryManager; -import io.jans.orm.search.filter.Filter; import io.prometheus.client.CollectorRegistry; import io.prometheus.client.Counter; import io.prometheus.client.exporter.common.TextFormat; -import net.agkn.hll.HLL; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -38,10 +33,8 @@ import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import static io.jans.as.model.util.Util.escapeLog; @@ -61,7 +54,7 @@ public class StatWS { private Logger log; @Inject - private PersistenceEntryManager entryManager; + private StatResponseService statResponseService; @Inject private ErrorResponseFactory errorResponseFactory; @@ -75,11 +68,6 @@ public class StatWS { @Inject private TokenService tokenService; - private final Cache responseCache = CacheBuilder - .newBuilder() - .expireAfterWrite(1, TimeUnit.HOURS) - .build(); - public static String createOpenMetricsResponse(StatResponse statResponse) throws IOException { Writer writer = new StringWriter(); CollectorRegistry registry = new CollectorRegistry(); @@ -176,7 +164,7 @@ public Response stat(String authorization, String month, String format) { try { if (log.isTraceEnabled()) log.trace("Recognized months: {}", escapeLog(months)); - final StatResponse statResponse = buildResponse(months); + final StatResponse statResponse = statResponseService.buildResponse(months); final String responseAsStr; if ("openmetrics".equalsIgnoreCase(format)) { @@ -199,90 +187,6 @@ public Response stat(String authorization, String month, String format) { } } - private StatResponse buildResponse(List months) { - final String cacheKey = months.toString(); - final StatResponse cachedResponse = responseCache.getIfPresent(cacheKey); - if (cachedResponse != null) { - return cachedResponse; - } - - StatResponse response = new StatResponse(); - for (String month : months) { - final StatResponseItem responseItem = buildItem(month); - if (responseItem != null) { - response.getResponse().put(month, responseItem); - } - } - - responseCache.put(cacheKey, response); - return response; - } - - private StatResponseItem buildItem(String month) { - try { - String monthlyDn = String.format("ou=%s,%s", escapeLog(month), statService.getBaseDn()); - - final List entries = entryManager.findEntries(monthlyDn, StatEntry.class, Filter.createPresenceFilter("jansId")); - if (entries == null || entries.isEmpty()) { - log.trace("Can't find stat entries for month: {}", monthlyDn); - return null; - } - - final StatResponseItem responseItem = new StatResponseItem(); - responseItem.setMonthlyActiveUsers(userCardinality(entries)); - responseItem.setMonth(month); - - unionTokenMapIntoResponseItem(entries, responseItem); - - return responseItem; - } catch (Exception e) { - log.error(e.getMessage(), e); - return null; - } - } - - private void unionTokenMapIntoResponseItem(List entries, StatResponseItem responseItem) { - for (StatEntry entry : entries) { - entry.getStat().getTokenCountPerGrantType().entrySet().stream().filter(en -> en.getValue() != null).forEach(en -> { - final Map tokenMap = responseItem.getTokenCountPerGrantType().get(en.getKey()); - if (tokenMap == null) { - responseItem.getTokenCountPerGrantType().put(en.getKey(), en.getValue()); - return; - } - for (Map.Entry tokenEntry : en.getValue().entrySet()) { - final Long counter = tokenMap.get(tokenEntry.getKey()); - if (counter == null) { - tokenMap.put(tokenEntry.getKey(), tokenEntry.getValue()); - continue; - } - - tokenMap.put(tokenEntry.getKey(), counter + tokenEntry.getValue()); - } - }); - } - } - - private long userCardinality(List entries) { - HLL hll = decodeHll(entries.get(0)); - - // Union hll - if (entries.size() > 1) { - for (int i = 1; i < entries.size(); i++) { - hll.union(decodeHll(entries.get(i))); - } - } - return hll.cardinality(); - } - - private HLL decodeHll(StatEntry entry) { - try { - return HLL.fromBytes(Base64.getDecoder().decode(entry.getUserHllData())); - } catch (Exception e) { - log.error("Failed to decode HLL data, entry dn: {}, data: {}", entry.getDn(), entry.getUserHllData()); - return statService.newHll(); - } - } - private void validateAuthorization(String authorization) { log.trace("Validating authorization: {}", authorization); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/service/stat/StatResponseServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/service/stat/StatResponseServiceTest.java new file mode 100644 index 00000000000..38db4db4470 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/service/stat/StatResponseServiceTest.java @@ -0,0 +1,41 @@ +package io.jans.as.server.service.stat; + +import io.jans.orm.PersistenceEntryManager; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Yuriy Zabrovarnyy + */ +@Listeners(MockitoTestNGListener.class) +public class StatResponseServiceTest { + + @InjectMocks + private StatResponseService statResponseService; + + @Mock + private PersistenceEntryManager entryManager; + + @Test + public void buildResponse_whenCalled_shouldInvokeEntityManagerOneTimeBecauseSecondTimeResponseMustBeCached() { + when(entryManager.findEntries(any(), any(), any())).thenReturn(new ArrayList<>()); + + statResponseService.buildResponse(Collections.singletonList("01")); + statResponseService.buildResponse(Collections.singletonList("01")); + statResponseService.buildResponse(Collections.singletonList("01")); + + // must be called exactly 1 time, all further calls should use cached response + verify(entryManager, times(1)).findEntries(any(), any(), any()); + } +}