diff --git a/bot/src/main/java/org/openjdk/skara/bot/BotRunner.java b/bot/src/main/java/org/openjdk/skara/bot/BotRunner.java index f114620fa..998c1ffc1 100644 --- a/bot/src/main/java/org/openjdk/skara/bot/BotRunner.java +++ b/bot/src/main/java/org/openjdk/skara/bot/BotRunner.java @@ -22,6 +22,7 @@ */ package org.openjdk.skara.bot; +import java.util.concurrent.atomic.AtomicInteger; import org.openjdk.skara.json.JSONValue; import java.io.IOException; @@ -49,6 +50,8 @@ enum TaskPhases { END } + private AtomicInteger workIdCounter = new AtomicInteger(); + private class RunnableWorkItem implements Runnable { private final WorkItem item; @@ -73,16 +76,19 @@ public void run() { scratchPath = scratchPaths.removeFirst(); } - log.log(Level.FINE, "Executing item " + item + " on repository " + scratchPath, TaskPhases.BEGIN); Collection followUpItems = null; - try { - followUpItems = item.run(scratchPath); - } catch (RuntimeException e) { - log.severe("Exception during item execution (" + item + "): " + e.getMessage()); - item.handleRuntimeException(e); - log.throwing(item.toString(), "run", e); - } finally { - log.log(Level.FINE, "Item " + item + " is now done", TaskPhases.END); + try (var __ = new LogContext(Map.of("work_item", item.toString(), + "work_id", String.valueOf(workIdCounter.incrementAndGet())))) { + log.log(Level.FINE, "Executing item " + item + " on repository " + scratchPath, TaskPhases.BEGIN); + try { + followUpItems = item.run(scratchPath); + } catch (RuntimeException e) { + log.severe("Exception during item execution (" + item + "): " + e.getMessage()); + item.handleRuntimeException(e); + log.throwing(item.toString(), "run", e); + } finally { + log.log(Level.FINE, "Item " + item + " is now done", TaskPhases.END); + } } if (followUpItems != null) { followUpItems.forEach(BotRunner.this::submitOrSchedule); @@ -215,19 +221,21 @@ public BotRunner(BotRunnerConfiguration config, List bots) { } private void checkPeriodicItems() { - log.log(Level.FINE, "Starting of checking for periodic items", TaskPhases.BEGIN); - try { - for (var bot : bots) { - var items = bot.getPeriodicItems(); - for (var item : items) { - submitOrSchedule(item); + try (var __ = new LogContext("work_id", String.valueOf(workIdCounter.incrementAndGet()))) { + log.log(Level.FINE, "Starting of checking for periodic items", TaskPhases.BEGIN); + try { + for (var bot : bots) { + var items = bot.getPeriodicItems(); + for (var item : items) { + submitOrSchedule(item); + } } + } catch (RuntimeException e) { + log.severe("Exception during periodic item checking: " + e.getMessage()); + log.throwing("BotRunner", "checkPeriodicItems", e); + } finally { + log.log(Level.FINE, "Done checking periodic items", TaskPhases.END); } - } catch (RuntimeException e) { - log.severe("Exception during periodic item checking: " + e.getMessage()); - log.throwing("BotRunner", "checkPeriodicItems", e); - } finally { - log.log(Level.FINE, "Done checking periodic items", TaskPhases.END); } } @@ -248,20 +256,22 @@ private void itemWatchdog() { } private void processRestRequest(JSONValue request) { - log.log(Level.FINE, "Starting processing of incoming rest request", TaskPhases.BEGIN); - log.fine("Request: " + request); - try { - for (var bot : bots) { - var items = bot.processWebHook(request); - for (var item : items) { - submitOrSchedule(item); + try (var __ = new LogContext("work_id", String.valueOf(workIdCounter.incrementAndGet()))) { + log.log(Level.FINE, "Starting processing of incoming rest request", TaskPhases.BEGIN); + log.fine("Request: " + request); + try { + for (var bot : bots) { + var items = bot.processWebHook(request); + for (var item : items) { + submitOrSchedule(item); + } } + } catch (RuntimeException e) { + log.severe("Exception during rest request processing: " + e.getMessage()); + log.throwing("BotRunner", "processRestRequest", e); + } finally { + log.log(Level.FINE, "Done processing incoming rest request", TaskPhases.END); } - } catch (RuntimeException e) { - log.severe("Exception during rest request processing: " + e.getMessage()); - log.throwing("BotRunner", "processRestRequest", e); - } finally { - log.log(Level.FINE, "Done processing incoming rest request", TaskPhases.END); } } diff --git a/bot/src/main/java/org/openjdk/skara/bot/LogContext.java b/bot/src/main/java/org/openjdk/skara/bot/LogContext.java new file mode 100644 index 000000000..1898aa25f --- /dev/null +++ b/bot/src/main/java/org/openjdk/skara/bot/LogContext.java @@ -0,0 +1,50 @@ +package org.openjdk.skara.bot; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * A LogContext is used to temporarily add extra log metadata in the current thread. + * It should be initiated with a try-with-resources construct. The variable itself + * is never used, we only want the controlled automatic close at the end of the try + * block. Typically name the variable __. Example: + * + * try (var __ = new LogContext("foo", "bar")) { + * // some code that logs stuff + * } + */ +public class LogContext implements AutoCloseable { + private static final Logger log = Logger.getLogger("org.openjdk.skara.bot"); + private final Map context = new HashMap<>(); + + public LogContext(String key, String value) { + this.init(Map.of(key, value)); + } + + public LogContext(Map ctx) { + this.init(ctx); + } + + private void init(Map newContext) { + for (var entry : newContext.entrySet()) { + String currentValue = LogContextMap.get(entry.getKey()); + if (currentValue != null) { + if (!currentValue.equals(entry.getValue())) { + log.severe("Tried to override the current LogContext value: " + currentValue + + " for " + entry.getKey() + " with a different value: " + entry.getValue()); + } + } else { + this.context.put(entry.getKey(), entry.getValue()); + LogContextMap.put(entry.getKey(), entry.getValue()); + } + } + + } + + public void close() { + this.context.forEach((key, value) -> { + LogContextMap.remove(key); + }); + } +} diff --git a/bot/src/main/java/org/openjdk/skara/bot/LogContextMap.java b/bot/src/main/java/org/openjdk/skara/bot/LogContextMap.java new file mode 100644 index 000000000..4c92d91f5 --- /dev/null +++ b/bot/src/main/java/org/openjdk/skara/bot/LogContextMap.java @@ -0,0 +1,48 @@ +package org.openjdk.skara.bot; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This class holds a static thread local hashmap to store temporary log + * metadata which our custom StreamHandlers can pick up and include in log + * messages. + */ +public class LogContextMap { + + private static final ThreadLocal> threadContextMap = new ThreadLocal<>(); + + public static void put(String key, String value) { + if (threadContextMap.get() == null) { + threadContextMap.set(new HashMap<>()); + } + var map = threadContextMap.get(); + map.put(key, value); + } + + public static String get(String key) { + if (threadContextMap.get() != null) { + return threadContextMap.get().get(key); + } else { + return null; + } + } + + public static String remove(String key) { + if (threadContextMap.get() != null) { + return threadContextMap.get().remove(key); + } else { + return null; + } + } + + public static Set> entrySet() { + if (threadContextMap.get() != null) { + return threadContextMap.get().entrySet(); + } else { + return Collections.emptySet(); + } + } +} diff --git a/bot/src/test/java/org/openjdk/skara/bot/LogContextTests.java b/bot/src/test/java/org/openjdk/skara/bot/LogContextTests.java new file mode 100644 index 000000000..4a04b63b1 --- /dev/null +++ b/bot/src/test/java/org/openjdk/skara/bot/LogContextTests.java @@ -0,0 +1,19 @@ +package org.openjdk.skara.bot; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class LogContextTests { + + @Test + public void simple() { + String key = "keyname"; + assertNull(LogContextMap.get(key), "Key " + key + " already present in context"); + try (var __ = new LogContext(key, "value")) { + assertEquals("value", LogContextMap.get(key), "Context property not set"); + } + assertNull(LogContextMap.get(key), "Context property not removed"); + } +} diff --git a/bots/cli/src/main/java/module-info.java b/bots/cli/src/main/java/module-info.java index c9243b354..62a24d24d 100644 --- a/bots/cli/src/main/java/module-info.java +++ b/bots/cli/src/main/java/module-info.java @@ -33,6 +33,7 @@ requires org.openjdk.skara.network; requires org.openjdk.skara.version; + requires java.net.http; requires java.sql; exports org.openjdk.skara.bots.cli; diff --git a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java index 3336a6570..f375527dd 100644 --- a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java +++ b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java @@ -22,6 +22,9 @@ */ package org.openjdk.skara.bots.cli; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import org.openjdk.skara.args.*; import org.openjdk.skara.bot.*; import org.openjdk.skara.network.URIBuilder; @@ -41,6 +44,7 @@ public class BotLauncher { private static Logger log; + private static final Instant START_TIME = Instant.now(); private static void applyLogging(JSONObject config) { LogManager.getLogManager().reset(); @@ -81,11 +85,7 @@ private static void applyLogging(JSONObject config) { if (config.get("log").asObject().contains("logstash")) { var logstashConf = config.get("log").get("logstash").asObject(); var level = Level.parse(logstashConf.get("level").asString()); - var maxRecords = 100; - if (logstashConf.contains("maxrecords")) { - maxRecords = logstashConf.get("maxrecords").asInt(); - } - var handler = new BotLogstashHandler(URIBuilder.base(logstashConf.get("endpoint").asString()).build(), maxRecords); + var handler = new BotLogstashHandler(URIBuilder.base(logstashConf.get("endpoint").asString()).build()); if (logstashConf.contains("fields")) { for (var field : logstashConf.get("fields").asArray()) { if (field.asObject().contains("pattern")) { @@ -99,6 +99,10 @@ private static void applyLogging(JSONObject config) { } } handler.setLevel(level); + var dateTimeFormatter = DateTimeFormatter.ISO_INSTANT + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + handler.addExtraField("instance_start_time", dateTimeFormatter.format(START_TIME)); log.addHandler(handler); } } diff --git a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java index 2b49d729e..b029ab3ec 100644 --- a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java +++ b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java @@ -22,25 +22,28 @@ */ package org.openjdk.skara.bots.cli; -import org.openjdk.skara.bot.BotTaskAggregationHandler; -import org.openjdk.skara.network.RestRequest; +import org.openjdk.skara.bot.LogContextMap; import org.openjdk.skara.json.JSON; -import java.io.*; import java.net.URI; +import java.net.http.*; import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.Future; import java.util.logging.*; import java.util.regex.Pattern; -import java.util.stream.Collectors; -public class BotLogstashHandler extends BotTaskAggregationHandler { - private final RestRequest endpoint; +/** + * Handles logging to logstash. Be careful not to call anything that creates new + * log records from this class as that can cause infinite recursion. + */ +public class BotLogstashHandler extends StreamHandler { + private final URI endpoint; + private final HttpClient httpClient; private final DateTimeFormatter dateTimeFormatter; - private final int maxRecords; - private final Logger log = Logger.getLogger("org.openjdk.skara.bots.cli"); - + // Optionally store all futures for testing purposes + private Collection>> futures; private static class ExtraField { String name; @@ -50,9 +53,12 @@ private static class ExtraField { private final List extraFields; - BotLogstashHandler(URI endpoint, int maxRecords) { - this.endpoint = new RestRequest(endpoint); - this.maxRecords = maxRecords; + BotLogstashHandler(URI endpoint) { + this.endpoint = endpoint; + this.httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(30)) + .build(); dateTimeFormatter = DateTimeFormatter.ISO_INSTANT .withLocale(Locale.getDefault()) .withZone(ZoneId.systemDefault()); @@ -74,46 +80,30 @@ void addExtraField(String name, String value, String pattern) { } private void publishToLogstash(Instant time, Level level, String message, Map extraFields) { - try { - var query = JSON.object(); - query.put("@timestamp", dateTimeFormatter.format(time)); - query.put("level", level.getName()); - query.put("level_value", level.intValue()); - query.put("message", message); - - for (var extraField : extraFields.entrySet()) { - query.put(extraField.getKey(), extraField.getValue()); - } - - endpoint.post("/") - .body(query) - .executeUnparsed(); - } catch (RuntimeException | IOException e) { - log.warning("Exception during logstash publishing: " + e.getMessage()); - log.throwing("BotSlackHandler", "publish", e); + var query = JSON.object(); + query.put("@timestamp", dateTimeFormatter.format(time)); + query.put("level", level.getName()); + query.put("level_value", level.intValue()); + query.put("message", message); + + for (var entry : LogContextMap.entrySet()) { + query.put(entry.getKey(), entry.getValue()); } - } - - private String formatDuration(Duration duration) { - return String.format("[%02d:%02d]", duration.toMinutes(), duration.toSeconds() % 60); - } - private String formatRecord(Instant base, LogRecord record) { - var writer = new StringWriter(); - var printer = new PrintWriter(writer); - - printer.print(formatDuration(Duration.between(base, record.getInstant()))); - printer.print("["); - printer.print(record.getLevel().getName()); - printer.print("] "); - printer.print(record.getMessage()); - - var exception = record.getThrown(); - if (exception != null) { - exception.printStackTrace(printer); + for (var extraField : extraFields.entrySet()) { + query.put(extraField.getKey(), extraField.getValue()); } - return writer.toString().stripTrailing(); + var httpRequest = HttpRequest.newBuilder() + .uri(endpoint) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(query.toString())) + .build(); + var future = httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding()); + // Save futures in optional collection when running tests. + if (futures != null) { + futures.add(future); + } } private Map getExtraFields(LogRecord record) { @@ -132,60 +122,15 @@ private Map getExtraFields(LogRecord record) { return ret; } - // Remove every entry below minLevel - private List filterRecords(List records, Level minLevel) { - return records.stream() - .filter(entry -> entry.getLevel().intValue() >= minLevel.intValue()) - .collect(Collectors.toList()); - } - @Override - public void publishAggregated(List task) { - var maxLevel = task.stream() - .max(Comparator.comparingInt(r -> r.getLevel().intValue())) - .map(LogRecord::getLevel) - .orElseThrow(); - if (maxLevel.intValue() < getLevel().intValue()) { - return; - } - - var start = task.get(0).getInstant(); - - // For duplicate keys, the first value seen is retained - var concatenatedFields = task.stream() - .map(this::getExtraFields) - .flatMap(extra -> extra.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, - (value1, value2) -> value1)); - - // First try to accommodate size limit by filtering out low level logging - if (task.size() > maxRecords) { - task = filterRecords(task, Level.FINER); - } - if (task.size() > maxRecords) { - task = filterRecords(task, Level.FINE); - } - - // If there's still too many lines, strip out the middle - if (task.size() > maxRecords) { - var beginning = task.subList(0, maxRecords / 2); - var end = task.subList(task.size() - maxRecords / 2, task.size()); - task = beginning; - task.addAll(end); - } - - var concatenatedMessage = task.stream() - .map(record -> formatRecord(start, record)) - .collect(Collectors.joining("\n")); - - publishToLogstash(start, maxLevel, concatenatedMessage, concatenatedFields); - } - - @Override - public void publishSingle(LogRecord record) { + public void publish(LogRecord record) { if (record.getLevel().intValue() < getLevel().intValue()) { return; } publishToLogstash(record.getInstant(), record.getLevel(), record.getMessage(), getExtraFields(record)); } + + void setFuturesCollection(Collection>> futures) { + this.futures = futures; + } } diff --git a/bots/cli/src/test/java/org/openjdk/skara/bots/cli/BotLogstashHandlerTests.java b/bots/cli/src/test/java/org/openjdk/skara/bots/cli/BotLogstashHandlerTests.java index a8c974229..a775611ac 100644 --- a/bots/cli/src/test/java/org/openjdk/skara/bots/cli/BotLogstashHandlerTests.java +++ b/bots/cli/src/test/java/org/openjdk/skara/bots/cli/BotLogstashHandlerTests.java @@ -22,22 +22,35 @@ */ package org.openjdk.skara.bots.cli; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Test; import java.io.IOException; -import java.util.logging.*; -import java.util.stream.Collectors; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.LogRecord; import static org.junit.jupiter.api.Assertions.*; class BotLogstashHandlerTests { + @Test - void simple() throws IOException { + void simple() throws IOException, ExecutionException, InterruptedException { try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 100); + var handler = new BotLogstashHandler(receiver.getEndpoint()); + var futures = new ArrayList>>(); + handler.setFuturesCollection(futures); + var record = new LogRecord(Level.INFO, "Hello"); handler.publish(record); + for (Future> future : futures) { + future.get(); + } + var requests = receiver.getRequests(); assertEquals(1, requests.size(), requests.toString()); assertTrue(requests.get(0).get("message").asString().contains("Hello")); @@ -46,9 +59,11 @@ void simple() throws IOException { } @Test - void simpleTask() throws IOException { + void simpleTask() throws IOException, ExecutionException, InterruptedException { try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 100); + var handler = new BotLogstashHandler(receiver.getEndpoint()); + var futures = new ArrayList>>(); + handler.setFuturesCollection(futures); LoggingBot.runOnce(handler, log -> { log.warning("Hello"); @@ -56,27 +71,45 @@ void simpleTask() throws IOException { log.warning("Bye"); }); + for (Future> future : futures) { + future.get(); + } + var requests = receiver.getRequests(); - assertEquals(1, requests.size(), requests.toString()); + // The async message sending means we may get results in any order. Sort on the + // timestamp to get the actual order. + requests.sort(Comparator.comparing(r -> r.get("@timestamp").toString())); + + assertEquals(3, requests.size(), requests.toString()); assertEquals(Level.WARNING.getName(), requests.get(0).get("level").asString()); assertEquals(Level.WARNING.intValue(), requests.get(0).get("level_value").asInt()); - assertTrue(requests.get(0).get("message").asString().contains("Hello")); - assertTrue(requests.get(0).get("message").asString().contains("Warning")); - assertTrue(requests.get(0).get("message").asString().contains("Bye")); - assertTrue(requests.get(0).get("message").asString().contains(Level.WARNING.toString())); + assertEquals("Hello", requests.get(0).get("message").asString()); + assertEquals("Warning!", requests.get(1).get("message").asString()); + assertEquals("Bye", requests.get(2).get("message").asString()); + assertEquals(Level.WARNING.toString(), requests.get(0).get("level").asString()); + assertNotNull(requests.get(0).get("work_id"), "work_id not set"); + assertTrue(requests.get(0).get("work_item").asString().contains("LoggingBot@"), + "work_item has bad value " + requests.get(0).get("work_item").asString()); } } @Test - void extraField() throws IOException { + void extraField() throws IOException, ExecutionException, InterruptedException { try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 100); + var handler = new BotLogstashHandler(receiver.getEndpoint()); + var futures = new ArrayList>>(); + handler.setFuturesCollection(futures); + handler.addExtraField("mandatory", "value"); handler.addExtraField("optional1", "$1", "^H(ello)$"); handler.addExtraField("optional2", "$1", "^(Not found)$"); var record = new LogRecord(Level.INFO, "Hello"); handler.publish(record); + for (Future> future : futures) { + future.get(); + } + var requests = receiver.getRequests(); assertEquals(1, requests.size(), requests.toString()); assertEquals("value", requests.get(0).get("mandatory").asString()); @@ -86,9 +119,12 @@ void extraField() throws IOException { } @Test - void extraFieldTask() throws IOException { + void extraFieldTask() throws IOException, ExecutionException, InterruptedException { try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 100); + var handler = new BotLogstashHandler(receiver.getEndpoint()); + var futures = new ArrayList>>(); + handler.setFuturesCollection(futures); + handler.addExtraField("mandatory", "value"); handler.addExtraField("optional1", "$1", "^H(ello)$"); handler.addExtraField("optional2", "$1", "^(Not found)$"); @@ -101,120 +137,20 @@ void extraFieldTask() throws IOException { log.warning("Bye"); }); + for (Future> future : futures) { + future.get(); + } + var requests = receiver.getRequests(); - assertEquals(1, requests.size(), requests.toString()); + // The async message sending means we may get results in any order. Sort on the + // timestamp to get the actual order. + requests.sort(Comparator.comparing(r -> r.get("@timestamp").toString())); + + assertEquals(3, requests.size(), requests.toString()); assertEquals("value", requests.get(0).get("mandatory").asString()); assertEquals("ello", requests.get(0).get("optional1").asString()); assertFalse(requests.get(0).contains("optional2")); - assertEquals("ye", requests.get(0).get("optional3").asString()); - assertTrue(requests.get(0).get("greedy").asString().contains("Executing item")); - } - } - - @Test - void filterLowLevels() throws IOException { - try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 10); - - LoggingBot.runOnce(handler, Level.FINER, log -> { - for (int i = 0; i < 5; ++i) { - log.fine("Fine nr " + i); - } - for (int i = 0; i < 5; ++i) { - log.finer("Finer nr " + i); - } - }); - - var requests = receiver.getRequests(); - var aggregatedLines = requests.stream() - .filter(request -> request.get("message").asString().contains("Executing item")) - .findAny() - .orElseThrow() - .get("message") - .asString() - .lines() - .collect(Collectors.toList()); - - var fineLines = aggregatedLines.stream() - .filter(line -> line.contains("Fine nr")) - .collect(Collectors.toList()); - var finerLines = aggregatedLines.stream() - .filter(line -> line.contains("Finer nr")) - .collect(Collectors.toList()); - assertEquals(5, fineLines.size(), aggregatedLines.toString()); - assertEquals(0, finerLines.size(), aggregatedLines.toString()); - } - } - - @Test - void filterLowestLevels() throws IOException { - try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 15); - - LoggingBot.runOnce(handler, Level.FINER, log -> { - for (int i = 0; i < 5; ++i) { - log.fine("Fine nr " + i); - } - for (int i = 0; i < 5; ++i) { - log.finer("Finer nr " + i); - } - for (int i = 0; i < 5; ++i) { - log.finest("Finest nr " + i); - } - }); - - var requests = receiver.getRequests(); - var aggregatedLines = requests.stream() - .filter(request -> request.get("message").asString().contains("Executing item")) - .findAny() - .orElseThrow() - .get("message") - .asString() - .lines() - .collect(Collectors.toList()); - - var fineLines = aggregatedLines.stream() - .filter(line -> line.contains("Fine nr")) - .collect(Collectors.toList()); - var finerLines = aggregatedLines.stream() - .filter(line -> line.contains("Finer nr")) - .collect(Collectors.toList()); - var finestLines = aggregatedLines.stream() - .filter(line -> line.contains("Finest nr")) - .collect(Collectors.toList()); - assertEquals(5, fineLines.size(), aggregatedLines.toString()); - assertEquals(5, finerLines.size(), aggregatedLines.toString()); - assertEquals(0, finestLines.size(), aggregatedLines.toString()); - } - } - - @Test - void filterMiddle() throws IOException { - try (var receiver = new RestReceiver()) { - var handler = new BotLogstashHandler(receiver.getEndpoint(), 100); - - LoggingBot.runOnce(handler, Level.FINER, log -> { - for (int i = 0; i < 100; ++i) { - log.fine("Start nr " + i); - } - for (int i = 0; i < 100; ++i) { - log.fine("Middle nr " + i); - } - for (int i = 0; i < 100; ++i) { - log.fine("End nr " + i); - } - }); - - var requests = receiver.getRequests(); - var aggregatedLines = requests.stream() - .filter(request -> request.get("message").asString().contains("Executing item")) - .findAny() - .orElseThrow() - .get("message") - .asString(); - assertTrue(aggregatedLines.contains("Start nr")); - assertFalse(aggregatedLines.contains("Middle nr")); - assertTrue(aggregatedLines.contains("End nr")); + assertEquals("ye", requests.get(2).get("optional3").asString()); } } }