diff --git a/configuration/unique_ips_per_referrer_fast b/configuration/unique_ips_per_referrer_fast
index aa7e4074ccd..093e4d9c881 100755
--- a/configuration/unique_ips_per_referrer_fast
+++ b/configuration/unique_ips_per_referrer_fast
@@ -1,9 +1,9 @@
#!/bin/bash
# Production:
-CACHE=/var/log/old/cache/unique-ips-per-referrer
+#CACHE=/var/log/old/cache/unique-ips-per-referrer
# Test:
-#CACHE=/var/log/old/cache/unique-ips-per-referrer/test
+CACHE=/var/log/old/cache/unique-ips-per-referrer/test
VISITED_FILES=$CACHE/visited
STATS=$CACHE/stats
OUTPUT=$STATS/results
diff --git a/java/com.sap.sse.test/src/com/sap/sse/util/apachelog/ApacheLogAnalysisTest.java b/java/com.sap.sse.test/src/com/sap/sse/util/apachelog/ApacheLogAnalysisTest.java
new file mode 100755
index 00000000000..6eea3264f86
--- /dev/null
+++ b/java/com.sap.sse.test/src/com/sap/sse/util/apachelog/ApacheLogAnalysisTest.java
@@ -0,0 +1,38 @@
+package com.sap.sse.util.apachelog;
+
+import static org.junit.Assert.assertEquals;
+
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.junit.Test;
+
+public class ApacheLogAnalysisTest {
+ @Test
+ public void testParseSingleLogLine() throws ParseException {
+ final LogEntry entry = new LogEntry("ess2017.sapsailing.com 91.7.27.233 - - [15/Dec/2017:16:49:41 +0000] \"POST /gwt/service/sailing HTTP/1.1\" 200 437 \"https://ess2017.sapsailing.com/gwt/RaceBoard.html?regattaName=ESS+2017+Los+Cabos&raceName=Race+1&leaderboardName=ESS+2017+Los+Cabos&leaderboardGroupName=Extreme+Sailing+Series+2017&eventId=6eb987e5-1334-45f9-b54a-8b772a3fbb5b&mode=PLAYER\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36\"");
+ assertEquals("ess2017.sapsailing.com", entry.getHostname());
+ assertEquals("91.7.27.233", entry.getRequestorIpString());
+ assertEquals("-", entry.getRemoteLogname());
+ assertEquals("-", entry.getRemoteUser());
+ assertEquals("15/Dec/2017:16:49:41 +0000", entry.getTimestampString());
+ assertEquals("15/Dec/2017", entry.getDateString());
+ final Calendar cal = new GregorianCalendar();
+ cal.setTimeZone(TimeZone.getTimeZone("UTC"));
+ cal.set(Calendar.DAY_OF_MONTH, 15);
+ cal.set(Calendar.MONTH, 11);
+ cal.set(Calendar.YEAR, 2017);
+ cal.set(Calendar.HOUR_OF_DAY, 16);
+ cal.set(Calendar.MINUTE, 49);
+ cal.set(Calendar.SECOND, 41);
+ cal.set(Calendar.MILLISECOND, 0);
+ assertEquals(cal.getTime(), entry.getTimepoint().asDate());
+ assertEquals("POST /gwt/service/sailing HTTP/1.1", entry.getRequest());
+ assertEquals(200, entry.getHttpStatusCode());
+ assertEquals(437, entry.getSize());
+ assertEquals("https://ess2017.sapsailing.com/gwt/RaceBoard.html?regattaName=ESS+2017+Los+Cabos&raceName=Race+1&leaderboardName=ESS+2017+Los+Cabos&leaderboardGroupName=Extreme+Sailing+Series+2017&eventId=6eb987e5-1334-45f9-b54a-8b772a3fbb5b&mode=PLAYER", entry.getReferrer());
+ assertEquals("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", entry.getUserAgent());
+ }
+}
diff --git a/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java b/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java
new file mode 100755
index 00000000000..01f885db337
--- /dev/null
+++ b/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java
@@ -0,0 +1,117 @@
+package com.sap.sse.util.apachelog;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Iterator;
+
+import com.sap.sse.common.TimePoint;
+import com.sap.sse.common.Util;
+import com.sap.sse.common.impl.MillisecondsTimePoint;
+
+/**
+ * Represents a line of the form
+ *
+ * ess2017.sapsailing.com 91.7.27.233 - - [15/Dec/2017:16:49:41 +0000] "POST /gwt/service/sailing HTTP/1.1" 200 437 "https://ess2017.sapsailing.com/gwt/RaceBoard.html?regattaName=ESS+2017+Los+Cabos&raceName=Race+1&leaderboardName=ESS+2017+Los+Cabos&leaderboardGroupName=Extreme+Sailing+Series+2017&eventId=6eb987e5-1334-45f9-b54a-8b772a3fbb5b&mode=PLAYER" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
+ *
+ * with its constituents. It is parsed by splitting by space, respecting double-quoted strings which then may contain spaces.
+ *
+ * @author Axel Uhl (D043530)
+ *
+ */
+public class LogEntry {
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss ZZZZZ");
+
+ private final String hostname;
+ private final String requestorIpString;
+ private final String remoteLogname;
+ private final String remoteUser;
+ private final String timestampString;
+ private final String request;
+ private final String httpStatusCodeString;
+ private final String sizeString;
+ private final String referrer;
+ private final String userAgent;
+
+ public LogEntry(String logEntry) {
+ final Iterator i = Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases(logEntry).iterator();
+ hostname = i.next();
+ requestorIpString = i.next();
+ remoteLogname = i.next();
+ remoteUser = i.next();
+ final String dateTimeWithOpeningBracket = i.next();
+ final String timeZoneOffsetWithClosingBracket = i.next();
+ timestampString = dateTimeWithOpeningBracket.substring(1) + " " + timeZoneOffsetWithClosingBracket.substring(0, timeZoneOffsetWithClosingBracket.length()-1);
+ request = i.next();
+ httpStatusCodeString = i.next();
+ sizeString = i.next();
+ referrer = i.next();
+ userAgent = i.next();
+ }
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public String getRequestorIpString() {
+ return requestorIpString;
+ }
+
+ public InetAddress getRequestorIp() throws UnknownHostException {
+ return InetAddress.getByName(getRequestorIpString());
+ }
+
+ public String getRemoteLogname() {
+ return remoteLogname;
+ }
+
+ public String getRemoteUser() {
+ return remoteUser;
+ }
+
+ public String getTimestampString() {
+ return timestampString;
+ }
+
+ /**
+ * Returns only the date-specific portion of the {@link #getTimestampString() timestamp string}. For example,
+ * "15/Dec/2017:16:49:41 +0000" will result in "15/Dec/2017".
+ */
+ public String getDateString() {
+ return getTimestampString().substring(0, getTimestampString().indexOf(':'));
+ }
+
+ public TimePoint getTimepoint() throws ParseException {
+ return new MillisecondsTimePoint(dateFormat.parse(getTimestampString()));
+ }
+
+ public String getRequest() {
+ return request;
+ }
+
+ public String getHttpStatusCodeString() {
+ return httpStatusCodeString;
+ }
+
+ public int getHttpStatusCode() {
+ return Integer.valueOf(getHttpStatusCodeString());
+ }
+
+ public String getSizeString() {
+ return sizeString;
+ }
+
+ public long getSize() {
+ return Long.valueOf(getSizeString());
+ }
+
+ public String getReferrer() {
+ return referrer;
+ }
+
+ public String getUserAgent() {
+ return userAgent;
+ }
+
+}
diff --git a/java/com.sap.sse/src/com/sap/sse/util/apachelog/PerHostnameEntry.java b/java/com.sap.sse/src/com/sap/sse/util/apachelog/PerHostnameEntry.java
new file mode 100755
index 00000000000..2876b70a39e
--- /dev/null
+++ b/java/com.sap.sse/src/com/sap/sse/util/apachelog/PerHostnameEntry.java
@@ -0,0 +1,70 @@
+package com.sap.sse.util.apachelog;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.TimeZone;
+
+import com.sap.sse.common.Util;
+
+public class PerHostnameEntry {
+ private final static SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MMM/yyyy");
+ private final String requestorIpString;
+ private final String dateString;
+ private final String userAgent;
+ private final int year;
+ private final int month;
+
+ /**
+ * Parses a line of format "66.249.66.14 17/Dec/2017 Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
+ * assuming that IP and date and useragent are separated by spaces and the user agent string fills up the remainder of the line.
+ */
+ public PerHostnameEntry(String line) {
+ final Iterator i = Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases(line).iterator();
+ requestorIpString = i.next();
+ dateString = i.next();
+ final StringBuilder userAgentBuilder = new StringBuilder();
+ while (i.hasNext()) {
+ userAgentBuilder.append(i.next());
+ if (i.hasNext()) {
+ userAgentBuilder.append(' ');
+ }
+ }
+ userAgent = userAgentBuilder.toString();
+ final Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ int tmpYear;
+ int tmpMonth;
+ try {
+ cal.setTime(dateFormat.parse(dateString));
+ tmpYear = cal.get(Calendar.YEAR);
+ tmpMonth = cal.get(Calendar.MONTH);
+ } catch (ParseException e) {
+ tmpYear = 0;
+ tmpMonth = 0;
+ }
+ month = tmpMonth;
+ year = tmpYear;
+ }
+
+ public String getRequestorIpString() {
+ return requestorIpString;
+ }
+
+ public String getDateString() {
+ return dateString;
+ }
+
+ public String getUserAgent() {
+ return userAgent;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+}
diff --git a/java/com.sap.sse/src/com/sap/sse/util/apachelog/UniqueIPsPerReferrer.java b/java/com.sap.sse/src/com/sap/sse/util/apachelog/UniqueIPsPerReferrer.java
new file mode 100755
index 00000000000..0bf59e21d98
--- /dev/null
+++ b/java/com.sap.sse/src/com/sap/sse/util/apachelog/UniqueIPsPerReferrer.java
@@ -0,0 +1,313 @@
+package com.sap.sse.util.apachelog;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.zip.GZIPInputStream;
+
+import com.sap.sse.common.TimePoint;
+import com.sap.sse.common.impl.MillisecondsTimePoint;
+
+/**
+ * From a set of Apache log files, a set of already visited files and the pre-existing results gathered from the files
+ * already visited, computes an updated version of the stats that tell which hostname received how many unique visitors
+ * per month and per year. This is intended to replace Bash script that has horrible performance. We're hoping to speed
+ * things up considerably by using Java for this task.
+ *
+ * @author Axel Uhl (D043530)
+ *
+ */
+public class UniqueIPsPerReferrer {
+ private static final Logger logger = Logger.getLogger(UniqueIPsPerReferrer.class.getName());
+ private static final String HOSTNAME_FILE_EXTENSION = ".ips";
+ private static final String UNIQUE_SUFFIX = ".unique";
+ private final File CACHE;
+ private final File VISITED_FILES;
+ private final File STATS;
+ private final File OUTPUT;
+ private final File MONTHS;
+ private final File YEARS;
+ private final File TOTALS;
+ private final File EVENTTOTALS;
+
+ public UniqueIPsPerReferrer() {
+ this("/var/log/old/cache/unique-ips-per-referrer/test");
+ }
+
+ public UniqueIPsPerReferrer(final String cacheDir) {
+ CACHE = new File(cacheDir);
+ VISITED_FILES = new File(CACHE + "/visited");
+ STATS = new File(CACHE + "/stats");
+ OUTPUT = new File(STATS + "/results");
+ MONTHS = new File(OUTPUT + "/permonth");
+ YEARS = new File(OUTPUT + "/peryear");
+ TOTALS = new File(OUTPUT + "/totals");
+ EVENTTOTALS = new File(OUTPUT + "/eventtotals");
+ ensureDirectoriesExist();
+ }
+
+ /**
+ * The first argument is the base directory for where to place the outputs, used for {@link #CACHE}.
+ * All subsequent arguments are considered to be file names of logfiles that are supposed to be
+ * analyzed.
+ */
+ public static void main(String[] args) throws IOException, ParseException {
+ final TimePoint started = MillisecondsTimePoint.now();
+ final String[] logfilenames = new String[args.length-1];
+ System.arraycopy(args, 1, logfilenames, 0, logfilenames.length);
+ new UniqueIPsPerReferrer(args[0]).analyze(logfilenames);
+ logger.info("Finished. Took "+started.until(MillisecondsTimePoint.now()));
+ }
+
+ public void analyze(String... filenames) throws IOException, ParseException {
+ appendHitsToHostnameSpecificFiles(filenames);
+ cleanUpOldResults();
+ computePerMonthPerYearPerEventAndTotals();
+ countUniqueEventOccurrencesPerMonthAndPerYearAndOverall();
+ countTotalHits();
+ }
+
+ private void ensureDirectoriesExist() {
+ CACHE.mkdirs();
+ STATS.mkdirs();
+ OUTPUT.mkdirs();
+ MONTHS.mkdirs();
+ YEARS.mkdirs();
+ }
+
+ private Set getVisitedFileNames() throws IOException {
+ final Set result = new HashSet<>();
+ if (VISITED_FILES.exists()) {
+ final BufferedReader br = new BufferedReader(new FileReader(VISITED_FILES));
+ String visitedFileName;
+ while ((visitedFileName = br.readLine()) != null) {
+ result.add(visitedFileName);
+ }
+ br.close();
+ }
+ return result;
+ }
+
+ private void appendHitsToHostnameSpecificFiles(String... filenames) throws IOException {
+ final Set visitedFileNames = getVisitedFileNames();
+ for (final String filename : filenames) {
+ logger.info("Analyzing log file "+filename);
+ if (!visitedFileNames.contains(filename)) {
+ appendHitsToHostnameSpecificFiles(new File(filename));
+ visitedFileNames.add(filename);
+ addVisitedFile(filename);
+ }
+ }
+ }
+
+ /**
+ * Removes the interim files for per-month and per-year stats, as well as the interim
+ * files for totals and per-event totals.
+ */
+ private void cleanUpOldResults() throws IOException {
+ logger.info("Cleanign up old results");
+ deleteDirectoryContentsRecursively(MONTHS.toPath(), "....-..");
+ deleteDirectoryContentsRecursively(YEARS.toPath(), "....");
+ EVENTTOTALS.delete();
+ TOTALS.delete();
+ }
+
+ private void computePerMonthPerYearPerEventAndTotals() throws IOException, ParseException {
+ final Map monthFileWritersPerFileBaseName = new HashMap<>();
+ final Map yearFileWritersPerFileBaseName = new HashMap<>();
+ final FileWriter eventTotalsWriter = new FileWriter(EVENTTOTALS);
+ final FileWriter totalsWriter = new FileWriter(TOTALS);
+ for (final File perHostnameFile : getAllHostnameFiles()) {
+ final String hostname = perHostnameFile.getName().substring(0, perHostnameFile.getName().length()-HOSTNAME_FILE_EXTENSION.length());
+ logger.info("Extracting per-month and per-year stats for hostname "+hostname);
+ final BufferedReader r = new BufferedReader(new FileReader(perHostnameFile));
+ try {
+ String line;
+ while ((line=r.readLine()) != null) {
+ final PerHostnameEntry entry = new PerHostnameEntry(line);
+ if (entry.getYear() != 0) {
+ final String monthFileBasename = ""+entry.getYear()+"_"+entry.getMonth();
+ final FileWriter monthFileWriter = getFileWriter(monthFileBasename, monthFileWritersPerFileBaseName, this::getMonthFile);
+ final String combinedLine = hostname+" "+line+"\n";
+ monthFileWriter.write(combinedLine);
+ final String yearFileBasename = ""+entry.getYear();
+ final FileWriter yearFileWriter = getFileWriter(yearFileBasename, yearFileWritersPerFileBaseName, this::getYearFile);
+ yearFileWriter.write(combinedLine);
+ }
+ eventTotalsWriter.write(hostname+" "+line+"\n");
+ totalsWriter.write(line+"\n");
+ }
+ } finally {
+ r.close();
+ }
+ }
+ for (final FileWriter monthFileWriter : monthFileWritersPerFileBaseName.values()) {
+ monthFileWriter.close();
+ }
+ for (final FileWriter yearFileWriter : yearFileWritersPerFileBaseName.values()) {
+ yearFileWriter.close();
+ }
+ eventTotalsWriter.close();
+ totalsWriter.close();
+ }
+
+ private File getMonthFile(String monthFileBaseName) {
+ return new File(MONTHS, monthFileBaseName);
+ }
+
+ private File getYearFile(String yearFileBaseName) {
+ return new File(YEARS, yearFileBaseName);
+ }
+
+ private void countUniqueEventOccurrencesPerMonthAndPerYearAndOverall() throws IOException {
+ logger.info("Counting unique visitors per host for per-month files");
+ countUniqueVisitorsPerHostname(MONTHS.listFiles((dir, filename)->filename.matches("...._..")));
+ logger.info("Counting unique visitors per host for per-year files");
+ countUniqueVisitorsPerHostname(YEARS.listFiles((dir, filename)->filename.matches("....")));
+ logger.info("Counting unique visitors per host for event totals");
+ countUniqueVisitorsPerHostname(new File[] { EVENTTOTALS });
+ }
+
+ /**
+ * For each file in {@code files} produces a ".unique" file next to it that counts the distinct unique visitors for
+ * each hostname found in the first column of the respective file.
+ *
+ * @param files
+ * files that contain lines of the form "yes2015.sapsailing.com 66.249.66.14 17/Dec/2017 Mozilla/5.0
+ * (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" where the first element is the hostname
+ * and the rest is the "unique visitor" information consisting of the IP address, day and user agent
+ * string.
+ */
+ private void countUniqueVisitorsPerHostname(final File[] files) throws IOException {
+ for (final File file : files) {
+ final File uniqueFile = new File(file.getCanonicalFile()+UNIQUE_SUFFIX);
+ final Set uniqueLinesFromFile = readDistinctLines(file);
+ final FileWriter uniqueFileWriter = new FileWriter(uniqueFile);
+ final Map hostnameCounts = new HashMap<>();
+ for (final String uniqueLineFromMonthFile : uniqueLinesFromFile) {
+ final String hostname = uniqueLineFromMonthFile.split(" ")[0];
+ inc(hostnameCounts, hostname);
+ }
+ for (final Entry e : hostnameCounts.entrySet()) {
+ uniqueFileWriter.write(e.getKey()+" "+e.getValue()+"\n");
+ }
+ uniqueFileWriter.close();
+ }
+ }
+
+ private void inc(Map hostnameCountsForMonth, String hostname) {
+ Integer count = hostnameCountsForMonth.get(hostname);
+ count = count == null ? 1 : count+1;
+ hostnameCountsForMonth.put(hostname, count);
+ }
+
+ private void countTotalHits() throws IOException {
+ logger.info("Counting total number of unique visitors");
+ final FileWriter totalsCountWriter = new FileWriter(new File(TOTALS.getCanonicalPath()+UNIQUE_SUFFIX));
+ final Set uniqueTotalLines = readDistinctLines(TOTALS);
+ totalsCountWriter.write(uniqueTotalLines.size()+"\n");
+ totalsCountWriter.close();
+ }
+
+ private Set readDistinctLines(File fromFile) throws IOException {
+ final Set result = new HashSet<>();
+ final BufferedReader br = new BufferedReader(new FileReader(fromFile));
+ try {
+ String line;
+ while ((line = br.readLine()) != null) {
+ result.add(line);
+ }
+ } finally {
+ br.close();
+ }
+ return result;
+ }
+
+ private void deleteDirectoryContentsRecursively(final Path directory, String fileNameRegexpPattern) throws IOException {
+ Files.walkFileTree(directory, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (fileNameRegexpPattern == null || file.getName(file.getNameCount()-1).toString().matches(fileNameRegexpPattern)) {
+ Files.delete(file);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ if (!dir.equals(directory)) {
+ Files.delete(dir);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
+ private void addVisitedFile(String filename) throws IOException {
+ final FileWriter visitedFilesWriter = new FileWriter(VISITED_FILES, /* append */ true);
+ visitedFilesWriter.write(filename+"\n");
+ visitedFilesWriter.close();
+ }
+
+ private void appendHitsToHostnameSpecificFiles(File logfileWhichMayUseGzipCompression) throws IOException {
+ final Reader reader;
+ if (logfileWhichMayUseGzipCompression.getName().toLowerCase().endsWith(".gz")) {
+ reader = new InputStreamReader(new GZIPInputStream(new FileInputStream(logfileWhichMayUseGzipCompression)));
+ } else {
+ reader = new FileReader(logfileWhichMayUseGzipCompression);
+ }
+ appendHitsToHostnameSpecificFiles(reader);
+ }
+
+ private void appendHitsToHostnameSpecificFiles(Reader logfileReader) throws IOException {
+ final Map fileWritersPerHostname = new HashMap<>();
+ final BufferedReader br = new BufferedReader(logfileReader);
+ String line;
+ while ((line=br.readLine()) != null) {
+ final LogEntry entry = new LogEntry(line);
+ final String hostname = entry.getHostname();
+ final FileWriter fileWriterForHostname = getFileWriter(hostname, fileWritersPerHostname, this::getHostnameSpecificFile);
+ fileWriterForHostname.write(entry.getRequestorIpString()+" "+entry.getDateString()+" "+entry.getUserAgent()+"\n");
+ }
+ for (final FileWriter fw : fileWritersPerHostname.values()) {
+ fw.close();
+ }
+ }
+
+ private FileWriter getFileWriter(String key, Map fileWriterCache, Function fileConstructor) throws IOException {
+ final FileWriter fileWriterForHostname;
+ if (fileWriterCache.containsKey(key)) {
+ fileWriterForHostname = fileWriterCache.get(key);
+ } else {
+ fileWriterForHostname = new FileWriter(fileConstructor.apply(key), /* append */ true);
+ fileWriterCache.put(key, fileWriterForHostname);
+ }
+ return fileWriterForHostname;
+ }
+
+ private File getHostnameSpecificFile(String hostname) {
+ return new File(STATS, hostname+HOSTNAME_FILE_EXTENSION);
+ }
+
+ private File[] getAllHostnameFiles() {
+ return STATS.listFiles((dir, filename)->filename.endsWith(HOSTNAME_FILE_EXTENSION));
+ }
+}