Skip to content

Commit

Permalink
feat(jans-auth-server): added new stat response service with test
Browse files Browse the repository at this point in the history
  • Loading branch information
yuriyz committed Feb 7, 2022
1 parent 5950a26 commit 9d60629
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 99 deletions.
Original file line number Diff line number Diff line change
@@ -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<String, StatResponse> responseCache = CacheBuilder
.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();

public StatResponse buildResponse(List<String> 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<StatEntry> 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<StatEntry> 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<StatEntry> entries, StatResponseItem responseItem) {
for (StatEntry entry : entries) {
entry.getStat().getTokenCountPerGrantType().entrySet().stream().filter(en -> en.getValue() != null).forEach(en -> {
final Map<String, Long> tokenMap = responseItem.getTokenCountPerGrantType().get(en.getKey());
if (tokenMap == null) {
responseItem.getTokenCountPerGrantType().put(en.getKey(), en.getValue());
return;
}
for (Map.Entry<String, Long> 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());
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
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;
import io.jans.as.model.error.ErrorResponseFactory;
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;

Expand All @@ -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;

Expand All @@ -61,7 +54,7 @@ public class StatWS {
private Logger log;

@Inject
private PersistenceEntryManager entryManager;
private StatResponseService statResponseService;

@Inject
private ErrorResponseFactory errorResponseFactory;
Expand All @@ -75,11 +68,6 @@ public class StatWS {
@Inject
private TokenService tokenService;

private final Cache<String, StatResponse> responseCache = CacheBuilder
.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();

public static String createOpenMetricsResponse(StatResponse statResponse) throws IOException {
Writer writer = new StringWriter();
CollectorRegistry registry = new CollectorRegistry();
Expand Down Expand Up @@ -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)) {
Expand All @@ -199,90 +187,6 @@ public Response stat(String authorization, String month, String format) {
}
}

private StatResponse buildResponse(List<String> 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<StatEntry> 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<StatEntry> entries, StatResponseItem responseItem) {
for (StatEntry entry : entries) {
entry.getStat().getTokenCountPerGrantType().entrySet().stream().filter(en -> en.getValue() != null).forEach(en -> {
final Map<String, Long> tokenMap = responseItem.getTokenCountPerGrantType().get(en.getKey());
if (tokenMap == null) {
responseItem.getTokenCountPerGrantType().put(en.getKey(), en.getValue());
return;
}
for (Map.Entry<String, Long> 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<StatEntry> 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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit 9d60629

Please sign in to comment.