diff --git a/pom.xml b/pom.xml index c27cc12ea..8bdf6c842 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.39.11-alpha-130-SNAPSHOT + 5.39.9 UTF-8 diff --git a/src/main/java/com/uid2/operator/Const.java b/src/main/java/com/uid2/operator/Const.java index a30e3d3ba..4d32b9034 100644 --- a/src/main/java/com/uid2/operator/Const.java +++ b/src/main/java/com/uid2/operator/Const.java @@ -28,5 +28,6 @@ public class Config extends com.uid2.shared.Const.Config { public static final String OptOutStatusApiEnabled = "optout_status_api_enabled"; public static final String OptOutStatusMaxRequestSize = "optout_status_max_request_size"; public static final String MaxInvalidPaths = "logging_limit_max_invalid_paths_per_interval"; + public static final String MaxVersionBucketsPerSite = "logging_limit_max_version_buckets_per_site"; } } diff --git a/src/main/java/com/uid2/operator/Main.java b/src/main/java/com/uid2/operator/Main.java index dcbd5e729..dad32611d 100644 --- a/src/main/java/com/uid2/operator/Main.java +++ b/src/main/java/com/uid2/operator/Main.java @@ -365,7 +365,7 @@ private Future createAndDeployCloudSyncStoreVerticle(String name, ICloud private Future createAndDeployStatsCollector() { Promise promise = Promise.promise(); - StatsCollectorVerticle statsCollectorVerticle = new StatsCollectorVerticle(60000, config.getInteger(Const.Config.MaxInvalidPaths, 50)); + StatsCollectorVerticle statsCollectorVerticle = new StatsCollectorVerticle(60000, config.getInteger(Const.Config.MaxInvalidPaths, 50), config.getInteger(Const.Config.MaxVersionBucketsPerSite, 50)); vertx.deployVerticle(statsCollectorVerticle, promise); _statsCollectorQueue = statsCollectorVerticle; return promise.future(); diff --git a/src/main/java/com/uid2/operator/model/StatsCollectorMessageItem.java b/src/main/java/com/uid2/operator/model/StatsCollectorMessageItem.java index 409709911..bcf06a523 100644 --- a/src/main/java/com/uid2/operator/model/StatsCollectorMessageItem.java +++ b/src/main/java/com/uid2/operator/model/StatsCollectorMessageItem.java @@ -5,15 +5,18 @@ public class StatsCollectorMessageItem { private String referer; private String apiContact; private Integer siteId; + private String clientVersion; //USED by json serial - public StatsCollectorMessageItem(){} + public StatsCollectorMessageItem() { + } - public StatsCollectorMessageItem(String path, String referer, String apiContact, Integer siteId){ + public StatsCollectorMessageItem(String path, String referer, String apiContact, Integer siteId, String clientVersion) { this.path = path; this.referer = referer; this.apiContact = apiContact; this.siteId = siteId; + this.clientVersion = clientVersion; } @@ -48,4 +51,12 @@ public Integer getSiteId() { public void setSiteId(Integer siteId) { this.siteId = siteId; } + + public String getClientVersion() { + return clientVersion; + } + + public void setClientVersion(String clientVersion) { + this.clientVersion = clientVersion; + } } diff --git a/src/main/java/com/uid2/operator/monitoring/ClientVersionStatRecorder.java b/src/main/java/com/uid2/operator/monitoring/ClientVersionStatRecorder.java new file mode 100644 index 000000000..5abeb41d2 --- /dev/null +++ b/src/main/java/com/uid2/operator/monitoring/ClientVersionStatRecorder.java @@ -0,0 +1,52 @@ +package com.uid2.operator.monitoring; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class ClientVersionStatRecorder { + private static final String NOT_RECORDED = ""; + private final int siteClientBucketLimit; + private final Map> siteIdToVersionCounts = new HashMap<>(); + + public ClientVersionStatRecorder(int maxVersionBucketsPerSite) { + this.siteClientBucketLimit = maxVersionBucketsPerSite; + } + + public Stream getStatsView() { + return siteIdToVersionCounts.entrySet().stream().map(entry -> new SiteClientVersionStat(entry.getKey(), entry.getValue())); + } + + private void removeLowVersionCounts(int siteId) { + var versionCounts = siteIdToVersionCounts.get(siteId); + if (versionCounts == null) { + return; + } + + // Remove 3 items to avoid a couple of new version values from continuously evicting each other + var lowestEntries = versionCounts.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .filter(entry -> !entry.getKey().equals(NOT_RECORDED)) + .limit(3) + .toList(); + for (var entry : lowestEntries) { + var notRecordedCount = versionCounts.getOrDefault(NOT_RECORDED, 0); + versionCounts.put(NOT_RECORDED, notRecordedCount + entry.getValue()); + versionCounts.remove(entry.getKey()); + } + } + + public void add(Integer siteId, String clientVersion) { + if (siteId == null || clientVersion == null || clientVersion.isBlank()) { + return; + } + + var clientVersionCounts = siteIdToVersionCounts.computeIfAbsent(siteId, k -> new HashMap<>()); + + var count = clientVersionCounts.getOrDefault(clientVersion, 0); + if (count == 0 && clientVersionCounts.size() >= siteClientBucketLimit) { + removeLowVersionCounts(siteId); + } + clientVersionCounts.put(clientVersion, count + 1); + } +} diff --git a/src/main/java/com/uid2/operator/monitoring/ILoggedStat.java b/src/main/java/com/uid2/operator/monitoring/ILoggedStat.java new file mode 100644 index 000000000..2ea9f777f --- /dev/null +++ b/src/main/java/com/uid2/operator/monitoring/ILoggedStat.java @@ -0,0 +1,6 @@ +package com.uid2.operator.monitoring; + +public interface ILoggedStat { + public String GetLogPrefix(); + public Object GetValueToLog(); +} diff --git a/src/main/java/com/uid2/operator/monitoring/SiteClientVersionStat.java b/src/main/java/com/uid2/operator/monitoring/SiteClientVersionStat.java new file mode 100644 index 000000000..b9e953e49 --- /dev/null +++ b/src/main/java/com/uid2/operator/monitoring/SiteClientVersionStat.java @@ -0,0 +1,24 @@ +package com.uid2.operator.monitoring; + +import java.util.Map; +import java.util.Objects; + +public final class SiteClientVersionStat implements ILoggedStat { + private final Integer siteId; + private final Map versionCounts; + + public SiteClientVersionStat(Integer siteId, Map versionCounts) { + this.siteId = siteId; + this.versionCounts = versionCounts; + } + + @Override + public String GetLogPrefix() { + return "version log; siteId=%d versions=".formatted(siteId); + } + + @Override + public Object GetValueToLog() { + return versionCounts; + } +} diff --git a/src/main/java/com/uid2/operator/monitoring/StatsCollectorHandler.java b/src/main/java/com/uid2/operator/monitoring/StatsCollectorHandler.java index c0b45dce3..04a36d9c1 100644 --- a/src/main/java/com/uid2/operator/monitoring/StatsCollectorHandler.java +++ b/src/main/java/com/uid2/operator/monitoring/StatsCollectorHandler.java @@ -1,6 +1,7 @@ package com.uid2.operator.monitoring; import com.uid2.operator.model.StatsCollectorMessageItem; +import com.uid2.shared.Const; import com.uid2.shared.auth.ClientKey; import com.uid2.shared.middleware.AuthMiddleware; import io.vertx.core.Handler; @@ -33,10 +34,19 @@ private void addStatsMessageToQueue(RoutingContext routingContext) { final ClientKey clientKey = (ClientKey) AuthMiddleware.getAuthClient(routingContext); final String apiContact = clientKey == null ? null : clientKey.getContact(); final Integer siteId = clientKey == null ? null : clientKey.getSiteId(); + final String clientVersion = getClientVersion(routingContext); - final StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem(path, referer, apiContact, siteId); + final StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem(path, referer, apiContact, siteId, clientVersion); _statCollectorQueue.enqueue(vertx, messageItem); } + private String getClientVersion(RoutingContext routingContext) { + String clientVersion = routingContext.request().headers().get(Const.Http.ClientVersionHeader); + if (clientVersion == null) { + clientVersion = !routingContext.queryParam("client").isEmpty() ? routingContext.queryParam("client").get(0) : null; + } + return clientVersion; + } + } diff --git a/src/main/java/com/uid2/operator/monitoring/StatsCollectorVerticle.java b/src/main/java/com/uid2/operator/monitoring/StatsCollectorVerticle.java index c764010b0..8e49ec1f8 100644 --- a/src/main/java/com/uid2/operator/monitoring/StatsCollectorVerticle.java +++ b/src/main/java/com/uid2/operator/monitoring/StatsCollectorVerticle.java @@ -19,11 +19,14 @@ import java.time.Instant; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; public class StatsCollectorVerticle extends AbstractVerticle implements IStatsCollectorQueue { private static final Logger LOGGER = LoggerFactory.getLogger(StatsCollectorVerticle.class); private HashMap pathMap; + private final ClientVersionStatRecorder clientVersionStat; + private static final int MAX_AVAILABLE = 1000; private final int maxInvalidPaths; @@ -41,7 +44,7 @@ public class StatsCollectorVerticle extends AbstractVerticle implements IStatsCo private final ObjectMapper mapper; private final Counter queueFullCounter; - public StatsCollectorVerticle(long jsonIntervalMS, int maxInvalidPaths) { + public StatsCollectorVerticle(long jsonIntervalMS, int maxInvalidPaths, int maxVersionBucketsPerSite) { pathMap = new HashMap<>(); _statsCollectorCount = new AtomicInteger(); @@ -65,6 +68,7 @@ public StatsCollectorVerticle(long jsonIntervalMS, int maxInvalidPaths) { .register(Metrics.globalRegistry); mapper = new ObjectMapper(); + clientVersionStat = new ClientVersionStatRecorder(maxVersionBucketsPerSite); } @Override @@ -121,6 +125,7 @@ public void handleMessage(Message message) { pathMap.merge(path, endpointStat, this::mergeEndpoint); } + clientVersionStat.add(siteId, messageItem.getClientVersion()); _statsCollectorCount.decrementAndGet(); @@ -133,7 +138,7 @@ public void handleMessage(Message message) { if(pathMap.size() == this.maxInvalidPaths + validPaths.size()) { LOGGER.error("max invalid paths reached; a large number of invalid paths have been requested from authenticated participants"); } - Object[] stats = pathMap.values().toArray(); + var stats = buildStatsList(); this.jsonSerializerExecutor.executeBlocking( promise -> promise.complete(this.serializeToLogs(stats)), res -> { @@ -148,12 +153,12 @@ public void handleMessage(Message message) { } } - private Void serializeToLogs(Object[] stats) { + private Void serializeToLogs(List stats) { LOGGER.debug("Starting JSON Serialize"); - ObjectMapper mapper = new ObjectMapper(); - for (Object stat : stats) { + ObjectMapper statMapper = new ObjectMapper(); + for (var stat : stats) { try { - String jsonString = mapper.writeValueAsString(stat); + String jsonString = "%s%s".formatted(stat.GetLogPrefix(), statMapper.writeValueAsString(stat.GetValueToLog())); LOGGER.info(jsonString); } catch (JsonProcessingException e) { LOGGER.error(e.getMessage(), e); @@ -167,19 +172,11 @@ private EndpointStat mergeEndpoint(EndpointStat a, EndpointStat b) { return a; } - - public String getEndpointStats() { - Object[] stats = pathMap.values().toArray(); - StringBuilder completeStats = new StringBuilder(); - for (Object stat : stats) { - try { - String jsonString = mapper.writeValueAsString(stat); - completeStats.append(jsonString).append("\n"); - } catch (JsonProcessingException e) { - LOGGER.error(e.getMessage(), e); - } - } - return completeStats.toString(); + private List buildStatsList() { + Stream pathMapStream = pathMap.values().stream(); + Stream clientVersionStream = clientVersionStat.getStatsView(); + var stats = Stream.concat(pathMapStream, clientVersionStream); + return stats.toList(); } @Override @@ -225,7 +222,7 @@ public void merge(DomainStat d) { } } - class EndpointStat { + class EndpointStat implements ILoggedStat { private final String endpoint; private final Integer siteId; private final String apiVersion; @@ -276,6 +273,16 @@ else if(domainList.size() < MaxDomains) { domainMissedCounter.increment(); } } + + @Override + public String GetLogPrefix() { + return ""; + } + + @Override + public Object GetValueToLog() { + return this; + } } } diff --git a/src/main/java/com/uid2/operator/vertx/ClientVersionCapturingHandler.java b/src/main/java/com/uid2/operator/vertx/ClientVersionCapturingHandler.java deleted file mode 100644 index 2af4fb6b2..000000000 --- a/src/main/java/com/uid2/operator/vertx/ClientVersionCapturingHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.uid2.operator.vertx; - -import com.uid2.shared.Const; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.Metrics; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; - -public class ClientVersionCapturingHandler implements Handler { - private final Map _clientVersionCounters = new HashMap<>(); - - public ClientVersionCapturingHandler(String dir, String whitelistGlob) throws IOException { - try (DirectoryStream dirStream = Files.newDirectoryStream(Paths.get(dir), whitelistGlob)) { - dirStream.forEach(path -> { - final String version = getFileNameWithoutExtension(path); - final Counter counter = Counter - .builder("uid2.client_sdk_versions") - .description("counter for how many http requests are processed per each client sdk version") - .tags("client_version", version) - .register(Metrics.globalRegistry); - _clientVersionCounters.put(version, counter); - }); - } - } - @Override - public void handle(RoutingContext context) { - String clientVersion = context.request().headers().get(Const.Http.ClientVersionHeader); - if (clientVersion == null) { - clientVersion = !context.queryParam("client").isEmpty() ? context.queryParam("client").get(0) : null; - } - if (clientVersion != null) { - final Counter counter = _clientVersionCounters.get(clientVersion); - if (counter != null) { - counter.increment(); - } - } - context.next(); - } - - private static String getFileNameWithoutExtension(Path path) { - final String fileName = path.getFileName().toString(); - return fileName.indexOf(".") > 0 ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName; - } -} diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 245aae445..210b08571 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -218,7 +218,6 @@ private Router createRoutesSetup() throws IOException { router.allowForward(AllowForwardHeaders.X_FORWARD); router.route().handler(new RequestCapturingHandler()); - router.route().handler(new ClientVersionCapturingHandler("static/js", "*.js")); router.route().handler(CorsHandler.create() .addRelativeOrigin(".*.") .allowedMethod(io.vertx.core.http.HttpMethod.GET) diff --git a/src/test/java/com/uid2/operator/StatsCollectorVerticleTest.java b/src/test/java/com/uid2/operator/StatsCollectorVerticleTest.java index ac594c44c..d0f089da4 100644 --- a/src/test/java/com/uid2/operator/StatsCollectorVerticleTest.java +++ b/src/test/java/com/uid2/operator/StatsCollectorVerticleTest.java @@ -11,100 +11,125 @@ import io.vertx.core.Vertx; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; -import org.junit.jupiter.api.Assertions; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.LoggerFactory; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @ExtendWith(VertxExtension.class) public class StatsCollectorVerticleTest { private static final int MAX_INVALID_PATHS = 5; - private StatsCollectorVerticle verticle; + private static final int MAX_CLIENT_VERSION_BUCKETS = 8; + private static final int JSON_INTERVAL = 200; + private static final int LOG_WAIT_INTERVAL = 50; + private static final String CLIENT_VERSION = "uid2-sdk-3.0.0"; + private final ObjectMapper mapper = new ObjectMapper(); + private ListAppender logWatcher; + private Vertx vertx; @BeforeEach void deployVerticle(Vertx vertx, VertxTestContext testContext) { - verticle = new StatsCollectorVerticle(1000, MAX_INVALID_PATHS); + this.vertx = vertx; + var verticle = new StatsCollectorVerticle(JSON_INTERVAL, MAX_INVALID_PATHS, MAX_CLIENT_VERSION_BUCKETS); vertx.deployVerticle(verticle, testContext.succeeding(id -> testContext.completeNow())); + + logWatcher = new ListAppender<>(); + logWatcher.start(); + ((Logger) LoggerFactory.getLogger(StatsCollectorVerticle.class)).addAppender(logWatcher); } + @AfterEach + void cleanupLogger() { + logWatcher.stop(); + ((Logger) LoggerFactory.getLogger(StatsCollectorVerticle.class)).detachAppender(logWatcher); + } @Test void verticleDeployed(Vertx vertx, VertxTestContext testContext) { testContext.completeNow(); } - @Test - void testJSONSerializeWithV0AndV1Paths(Vertx vertx, VertxTestContext testContext) throws InterruptedException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/test", "https://test.com", "test", 1); + private List getMessages() { + return logWatcher.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.toList()); + } + private void sendStatMessage(StatsCollectorMessageItem messageItem) throws JsonProcessingException { vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - - messageItem = new StatsCollectorMessageItem("/v1/test", "https://test.com", "test", 1); + } - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); + private void triggerSerializeAndWait(VertxTestContext testContext) throws JsonProcessingException, InterruptedException { + StatsCollectorMessageItem triggerSerialize = new StatsCollectorMessageItem("/triggerSerialize", "https://test.com", "test", null, null); + sendStatMessage(triggerSerialize); + testContext.awaitCompletion(LOG_WAIT_INTERVAL, TimeUnit.MILLISECONDS); + } - testContext.awaitCompletion(2000, TimeUnit.MILLISECONDS); + @Test + void testJSONSerializeWithV0AndV1Paths(Vertx vertx, VertxTestContext testContext) throws InterruptedException, JsonProcessingException { + StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/test", "https://test.com", "test", 1, CLIENT_VERSION); + sendStatMessage(messageItem); + sendStatMessage(messageItem); + sendStatMessage(messageItem); - String expected = - "{\"endpoint\":\"test\",\"siteId\":1,\"apiVersion\":\"v1\",\"domainList\":[{\"domain\":\"test.com\",\"count\":2,\"apiContact\":\"test\"}]}\n" - + "{\"endpoint\":\"test\",\"siteId\":1,\"apiVersion\":\"v0\",\"domainList\":[{\"domain\":\"test.com\",\"count\":3,\"apiContact\":\"test\"}]}\n"; + messageItem = new StatsCollectorMessageItem("/v1/test", "https://test.com", "test", 1, CLIENT_VERSION); + sendStatMessage(messageItem); + sendStatMessage(messageItem); + waitForLogInterval(testContext); - String results = verticle.getEndpointStats(); + triggerSerializeAndWait(testContext); - Assertions.assertEquals(results, expected); + var expectedList = List.of("{\"endpoint\":\"test\",\"siteId\":1,\"apiVersion\":\"v1\",\"domainList\":[{\"domain\":\"test.com\",\"count\":2,\"apiContact\":\"test\"}]}", + "{\"endpoint\":\"test\",\"siteId\":1,\"apiVersion\":\"v0\",\"domainList\":[{\"domain\":\"test.com\",\"count\":3,\"apiContact\":\"test\"}]}"); + var messages = getMessages(); + assertThat(messages).containsAll(expectedList); testContext.completeNow(); } + private static void waitForLogInterval(VertxTestContext testContext) throws InterruptedException { + testContext.awaitCompletion(JSON_INTERVAL*2, TimeUnit.MILLISECONDS); + } + @Test void testJSONSerializeWithV2AndUnknownPaths(Vertx vertx, VertxTestContext testContext) throws InterruptedException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/v2/test", "https://test.com", "test", 1); - - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - - messageItem = new StatsCollectorMessageItem("/v2", "https://test.com", "test", 1); - - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); - - testContext.awaitCompletion(2000, TimeUnit.MILLISECONDS); - - String expected = - "{\"endpoint\":\"test\",\"siteId\":1,\"apiVersion\":\"v2\",\"domainList\":[{\"domain\":\"test.com\",\"count\":3,\"apiContact\":\"test\"}]}\n" - + "{\"endpoint\":\"v2\",\"siteId\":1,\"apiVersion\":\"unknown\",\"domainList\":[{\"domain\":\"test.com\",\"count\":2,\"apiContact\":\"test\"}]}\n"; - - String results = verticle.getEndpointStats(); - - Assertions.assertEquals(results, expected); + StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/v2/test", "https://test.com", "test", 1, CLIENT_VERSION); + sendStatMessage(messageItem); + sendStatMessage(messageItem); + sendStatMessage(messageItem); + + messageItem = new StatsCollectorMessageItem("/v2", "https://test.com", "test", 1, CLIENT_VERSION); + sendStatMessage(messageItem); + sendStatMessage(messageItem); + waitForLogInterval(testContext); + triggerSerializeAndWait(testContext); + + var expectedList = List.of("{\"endpoint\":\"test\",\"siteId\":1,\"apiVersion\":\"v2\",\"domainList\":[{\"domain\":\"test.com\",\"count\":3,\"apiContact\":\"test\"}]}", + "{\"endpoint\":\"v2\",\"siteId\":1,\"apiVersion\":\"unknown\",\"domainList\":[{\"domain\":\"test.com\",\"count\":2,\"apiContact\":\"test\"}]}"); + var messages = getMessages(); + assertThat(messages).containsAll(expectedList); testContext.completeNow(); } @Test void allValidPathsAllowed(Vertx vertx, VertxTestContext testContext) throws InterruptedException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); Set validEndpoints = Endpoints.pathSet(); - for(String endpoint : validEndpoints) { - StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem(endpoint, "https://test.com", "test", 1); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); + StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem(endpoint, "https://test.com", "test", 1, CLIENT_VERSION); + sendStatMessage(messageItem); } - testContext.awaitCompletion(2000, TimeUnit.MILLISECONDS); - - String results = verticle.getEndpointStats(); + waitForLogInterval(testContext); + triggerSerializeAndWait(testContext); + var messages = getMessages(); for(String endpoint: validEndpoints) { String withoutVersion = endpoint; if (endpoint.startsWith("/v1/") || endpoint.startsWith("/v2/")) { @@ -114,7 +139,7 @@ void allValidPathsAllowed(Vertx vertx, VertxTestContext testContext) throws Inte } String expected = "{\"endpoint\":\"" + withoutVersion + "\",\"siteId\":1,"; - Assertions.assertTrue(results.contains(expected)); + assertThat(messages).anyMatch(m -> m.contains(expected)); } testContext.completeNow(); @@ -122,37 +147,59 @@ void allValidPathsAllowed(Vertx vertx, VertxTestContext testContext) throws Inte @Test void invalidPathsLimit(Vertx vertx, VertxTestContext testContext) throws InterruptedException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - for(int i = 0; i < MAX_INVALID_PATHS + Endpoints.pathSet().size() + 5; i++) { - StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/bad" + i, "https://test.com", "test", 1); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); + StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/bad" + i, "https://test.com", "test", 1, CLIENT_VERSION); + sendStatMessage(messageItem); } - testContext.awaitCompletion(2000, TimeUnit.MILLISECONDS); - String results = verticle.getEndpointStats(); + waitForLogInterval(testContext); + triggerSerializeAndWait(testContext); + var messages = getMessages(); // MAX_INVALID_PATHS is not the hard limit. The maximum paths that can be recorded, including valid ones, is MAX_INVALID_PATHS + validPaths.size * 2 for(int i = 0; i < MAX_INVALID_PATHS + Endpoints.pathSet().size(); i++) { String expected = "{\"endpoint\":\"bad" + i + "\",\"siteId\":1,\"apiVersion\":\"v0\",\"domainList\":[{\"domain\":\"test.com\",\"count\":1,\"apiContact\":\"test\"}]}"; - Assertions.assertTrue(results.contains(expected)); + assertThat(messages).contains(expected); } for(int i = MAX_INVALID_PATHS + Endpoints.pathSet().size(); i < MAX_INVALID_PATHS + 5; i++) { String expected = "{\"endpoint\":\"bad" + i + "\",\"siteId\":1,\"apiVersion\":\"v0\",\"domainList\":[{\"domain\":\"test.com\",\"count\":1,\"apiContact\":\"test\"}]}"; - Assertions.assertFalse(results.contains(expected)); + assertThat(messages).contains(expected); } + assertThat(getMessages()).contains("max invalid paths reached; a large number of invalid paths have been requested from authenticated participants"); - ListAppender logWatcher = new ListAppender<>(); - logWatcher.start(); - ((Logger) LoggerFactory.getLogger(StatsCollectorVerticle.class)).addAppender(logWatcher); + testContext.completeNow(); + } - StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/triggerSerialize", "https://test.com", "test", 1); - vertx.eventBus().send(Const.Config.StatsCollectorEventBus, mapper.writeValueAsString(messageItem)); + @Test + void clientVersionStats(Vertx vertx, VertxTestContext testContext) throws InterruptedException, JsonProcessingException { + for(int i = 0; i < 3; i++) { + StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/test" + i, "https://test.com", "test", 1, CLIENT_VERSION + i); + sendStatMessage(messageItem); + } + for(int i = 0; i < 12; i++) { + StatsCollectorMessageItem messageItem = new StatsCollectorMessageItem("/test" + i, "https://test.com", "test", 2, CLIENT_VERSION + i); + for (int count = 0; count <= i; count++) { + sendStatMessage(messageItem); + } + } + sendStatMessage(new StatsCollectorMessageItem("/test", "https://test.com", "test", 2, CLIENT_VERSION + "single")); + sendStatMessage(new StatsCollectorMessageItem("/test", "https://test.com", "test", 2, null)); + + waitForLogInterval(testContext); + triggerSerializeAndWait(testContext); - testContext.awaitCompletion(1000, TimeUnit.MILLISECONDS); - Assertions.assertTrue(logWatcher.list.get(0).getFormattedMessage().contains("max invalid paths reached; a large number of invalid paths have been requested from authenticated participants")); + waitForLogInterval(testContext); + sendStatMessage(new StatsCollectorMessageItem("/test", "https://test.com", "test", 1, CLIENT_VERSION + 1)); + triggerSerializeAndWait(testContext); + + var expectedLogs = List.of( + "version log; siteId=1 versions={\"uid2-sdk-3.0.01\":1,\"uid2-sdk-3.0.02\":1,\"uid2-sdk-3.0.00\":1}", + "version log; siteId=1 versions={\"uid2-sdk-3.0.01\":2,\"uid2-sdk-3.0.02\":1,\"uid2-sdk-3.0.00\":1}", + "version log; siteId=2 versions={\"\":21,\"uid2-sdk-3.0.06\":7,\"uid2-sdk-3.0.07\":8,\"uid2-sdk-3.0.011\":12,\"uid2-sdk-3.0.08\":9,\"uid2-sdk-3.0.010\":11,\"uid2-sdk-3.0.09\":10,\"uid2-sdk-3.0.0single\":1}" + ); + var messages = getMessages(); + assertThat(messages).containsAll(expectedLogs); testContext.completeNow(); } - } diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 6b74a36bb..eafa14f9a 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -55,6 +55,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.slf4j.LoggerFactory; +import static org.assertj.core.api.Assertions.*; import javax.crypto.SecretKey; import java.math.BigInteger; @@ -3304,7 +3305,7 @@ void cstgDomainNameCheckFailsAndLogInvalidHttpOrigin(String httpOrigin, Vertx ve assertFalse(respJson.containsKey("body")); assertEquals("unexpected http origin", respJson.getString("message")); assertEquals("invalid_http_origin", respJson.getString("status")); - Assertions.assertTrue(logWatcher.list.get(0).getFormattedMessage().contains("InvalidHttpOriginAndAppName: site test (123): http://gototest.com")); + assertThat(logWatcher.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.toList())).contains("InvalidHttpOriginAndAppName: site test (123): http://gototest.com"); assertTokenStatusMetrics( clientSideTokenGenerateSiteId, TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, @@ -5090,74 +5091,4 @@ void secureLinkValidationFailsReturnsIdentityError(Vertx vertx, VertxTestContext testContext.completeNow(); }); } - - @ParameterizedTest // note that this test will be removed when we switch to logging versions - @ValueSource(strings = {"euid-sdk-1.0.0", "openid-sdk-1.0", "uid2-esp-0.0.1a", "uid2-sdk-0.0.1a", - "uid2-sdk-0.0.1b", "uid2-sdk-1.0.0", "uid2-sdk-2.0.0"}) - void clientVersionHeader(String clientVersion, Vertx vertx, VertxTestContext testContext) { - WebClient client = WebClient.create(vertx); - HttpRequest req = client.getAbs(getUrlForEndpoint("/any/endpoint")); - req.putHeader("X-UID2-Client-Version", clientVersion); - req.send(ar -> { - assertEquals(404, ar.result().statusCode()); - final double actual = Metrics.globalRegistry - .get("uid2.client_sdk_versions") - .tag("client_version", clientVersion) - .counter().count(); - assertEquals(1, actual); - testContext.completeNow(); - }); - } - - @ParameterizedTest // note that this test will be removed when we switch to logging versions - @ValueSource(strings = {"euid-sdk-1.0.0", "openid-sdk-1.0", "uid2-esp-0.0.1a", "uid2-sdk-0.0.1a", - "uid2-sdk-0.0.1b", "uid2-sdk-1.0.0", "uid2-sdk-2.0.0"}) - void clientVersionQueryParameter(String clientVersion, Vertx vertx, VertxTestContext testContext) { - WebClient client = WebClient.create(vertx); - HttpRequest req = client.getAbs(getUrlForEndpoint("/any/endpoint?client=" + clientVersion)); - req.send(ar -> { - assertEquals(404, ar.result().statusCode()); - final double actual = Metrics.globalRegistry - .get("uid2.client_sdk_versions") - .tag("client_version", clientVersion) - .counter().count(); - assertEquals(1, actual); - testContext.completeNow(); - }); - } - - @Test // note that this test will be removed when we switch to logging versions - void clientVersionHeaderNotFound(Vertx vertx, VertxTestContext testContext) { - WebClient client = WebClient.create(vertx); - HttpRequest req = client.getAbs(getUrlForEndpoint("/any/endpoint")); - String clientVersion = "invalid-sdk"; - req.putHeader("X-UID2-Client-Version", clientVersion); - req.send(ar -> { - assertEquals(404, ar.result().statusCode()); - assertThrows(MeterNotFoundException.class, () -> { - Metrics.globalRegistry - .get("uid2.client_sdk_versions") - .tag("client_version", clientVersion) - .counter(); - }); - testContext.completeNow(); - }); - } - - @Test // note that this test will be removed when we switch to logging versions - void clientVersionQueryParameterNotFound(Vertx vertx, VertxTestContext testContext) { - WebClient client = WebClient.create(vertx); - String clientVersion = "invalid-sdk"; - HttpRequest req = client.getAbs(getUrlForEndpoint("/any/endpoint?client=" + clientVersion)); - req.send(ar -> { - assertEquals(404, ar.result().statusCode()); - assertThrows(MeterNotFoundException.class, () -> { - Metrics.globalRegistry - .get("uid2.client_sdk_versions") - .tag("client_version", clientVersion) - .counter(); - }); - testContext.completeNow(); - }); - } }