diff --git a/pom.xml b/pom.xml
index f44b0e08a..607ae5618 100644
--- a/pom.xml
+++ b/pom.xml
@@ -296,6 +296,7 @@
false
/tmp/atlas/audit/audit.log
+ /tmp/atlas/audit/audit-%d{yyyy-MM-dd}-%i.log
/tmp/atlas/audit/audit-extra.log
diff --git a/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java b/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java
new file mode 100644
index 000000000..b2fe8552e
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java
@@ -0,0 +1,255 @@
+package org.ohdsi.webapi.statistic.controller;
+
+import com.opencsv.CSVWriter;
+
+import org.ohdsi.webapi.statistic.dto.AccessTrendDto;
+import org.ohdsi.webapi.statistic.dto.AccessTrendsDto;
+import org.ohdsi.webapi.statistic.dto.EndpointDto;
+import org.ohdsi.webapi.statistic.dto.SourceExecutionDto;
+import org.ohdsi.webapi.statistic.dto.SourceExecutionsDto;
+import org.ohdsi.webapi.statistic.service.StatisticService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Controller;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.io.ByteArrayOutputStream;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Controller
+@Path("/statistic/")
+@ConditionalOnProperty(value = "audit.trail.enabled", havingValue = "true")
+public class StatisticController {
+ private StatisticService service;
+
+ public enum ResponseFormat {
+ CSV, JSON
+ }
+
+ private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER = new ArrayList() {{
+ add(new String[]{"Date", "Source", "Execution Type"});
+ }};
+
+ private static final List ACCESS_TRENDS_CSV_RESULT_HEADER = new ArrayList() {{
+ add(new String[]{"Date", "Endpoint", "UserID"});
+ }};
+
+ public StatisticController(StatisticService service) {
+ this.service = service;
+ }
+
+ /**
+ * Returns execution statistics
+ * @param executionStatisticsRequest - filter settings for statistics
+ */
+ @POST
+ @Path("/executions")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response executionStatistics(ExecutionStatisticsRequest executionStatisticsRequest) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ boolean showUserInformation = executionStatisticsRequest.isShowUserInformation();
+
+ SourceExecutionsDto sourceExecutions = service.getSourceExecutions(LocalDate.parse(executionStatisticsRequest.getStartDate(), formatter),
+ LocalDate.parse(executionStatisticsRequest.getEndDate(), formatter), executionStatisticsRequest.getSourceKey(), showUserInformation);
+
+ if (ResponseFormat.CSV.equals(executionStatisticsRequest.getResponseFormat())) {
+ return prepareExecutionResultResponse(sourceExecutions.getExecutions(), "execution_statistics.zip", showUserInformation);
+ } else {
+ return Response.ok(sourceExecutions).build();
+ }
+ }
+
+ /**
+ * Returns access trends statistics
+ * @param accessTrendsStatisticsRequest - filter settings for statistics
+ */
+ @POST
+ @Path("/accesstrends")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response accessStatistics(AccessTrendsStatisticsRequest accessTrendsStatisticsRequest) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ boolean showUserInformation = accessTrendsStatisticsRequest.isShowUserInformation();
+
+ AccessTrendsDto trends = service.getAccessTrends(LocalDate.parse(accessTrendsStatisticsRequest.getStartDate(), formatter),
+ LocalDate.parse(accessTrendsStatisticsRequest.getEndDate(), formatter), accessTrendsStatisticsRequest.getEndpoints(), showUserInformation);
+
+ if (ResponseFormat.CSV.equals(accessTrendsStatisticsRequest.getResponseFormat())) {
+ return prepareAccessTrendsResponse(trends.getTrends(), "execution_trends.zip", showUserInformation);
+ } else {
+ return Response.ok(trends).build();
+ }
+ }
+
+ private Response prepareExecutionResultResponse(List executions, String filename, boolean showUserInformation) {
+ updateExecutionStatisticsHeader(showUserInformation);
+ List data = executions.stream()
+ .map(execution -> showUserInformation
+ ? new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName(), execution.getUserID()}
+ : new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName()}
+ )
+ .collect(Collectors.toList());
+ return prepareResponse(data, filename, EXECUTION_STATISTICS_CSV_RESULT_HEADER);
+ }
+
+ private Response prepareAccessTrendsResponse(List trends, String filename, boolean showUserInformation) {
+ updateAccessTrendsHeader(showUserInformation);
+ List data = trends.stream()
+ .map(trend -> showUserInformation
+ ? new String[]{trend.getExecutionDate().toString(), trend.getEndpointName(), trend.getUserID()}
+ : new String[]{trend.getExecutionDate().toString(), trend.getEndpointName()}
+ )
+ .collect(Collectors.toList());
+ return prepareResponse(data, filename, ACCESS_TRENDS_CSV_RESULT_HEADER);
+ }
+
+ private Response prepareResponse(List data, String filename, List header) {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ StringWriter sw = new StringWriter();
+ CSVWriter csvWriter = new CSVWriter(sw, ',', CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER);
+ csvWriter.writeAll(header);
+ csvWriter.writeAll(data);
+ csvWriter.flush();
+ baos.write(sw.getBuffer().toString().getBytes());
+
+ return Response
+ .ok(baos)
+ .type(MediaType.APPLICATION_OCTET_STREAM)
+ .header("Content-Disposition", String.format("attachment; filename=\"%s\"", filename))
+ .build();
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private void updateExecutionStatisticsHeader(boolean showUserInformation) {
+ EXECUTION_STATISTICS_CSV_RESULT_HEADER.clear();
+ if (showUserInformation) {
+ EXECUTION_STATISTICS_CSV_RESULT_HEADER.add(new String[]{"Date", "Source", "Execution Type", "User ID"});
+ } else {
+ EXECUTION_STATISTICS_CSV_RESULT_HEADER.add(new String[]{"Date", "Source", "Execution Type"});
+ }
+ }
+
+ private void updateAccessTrendsHeader(boolean showUserInformation) {
+ ACCESS_TRENDS_CSV_RESULT_HEADER.clear();
+ if (showUserInformation) {
+ ACCESS_TRENDS_CSV_RESULT_HEADER.add(new String[]{"Date", "Endpoint", "UserID"});
+ } else {
+ ACCESS_TRENDS_CSV_RESULT_HEADER.add(new String[]{"Date", "Endpoint"});
+ }
+ }
+
+ public static final class ExecutionStatisticsRequest {
+ // Format - yyyy-MM-dd
+ String startDate;
+ // Format - yyyy-MM-dd
+ String endDate;
+ String sourceKey;
+ ResponseFormat responseFormat;
+ boolean showUserInformation;
+
+ public String getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(String startDate) {
+ this.startDate = startDate;
+ }
+
+ public String getEndDate() {
+ return endDate;
+ }
+
+ public void setEndDate(String endDate) {
+ this.endDate = endDate;
+ }
+
+ public String getSourceKey() {
+ return sourceKey;
+ }
+
+ public void setSourceKey(String sourceKey) {
+ this.sourceKey = sourceKey;
+ }
+
+ public ResponseFormat getResponseFormat() {
+ return responseFormat;
+ }
+
+ public void setResponseFormat(ResponseFormat responseFormat) {
+ this.responseFormat = responseFormat;
+ }
+
+ public boolean isShowUserInformation() {
+ return showUserInformation;
+ }
+
+ public void setShowUserInformation(boolean showUserInformation) {
+ this.showUserInformation = showUserInformation;
+ }
+ }
+
+ public static final class AccessTrendsStatisticsRequest {
+ // Format - yyyy-MM-dd
+ String startDate;
+ // Format - yyyy-MM-dd
+ String endDate;
+ // Key - method (POST, GET)
+ // Value - endpoint ("{}" can be used as a placeholder, will be converted to ".*" in regular expression)
+ List endpoints;
+ ResponseFormat responseFormat;
+ boolean showUserInformation;
+
+ public String getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(String startDate) {
+ this.startDate = startDate;
+ }
+
+ public String getEndDate() {
+ return endDate;
+ }
+
+ public void setEndDate(String endDate) {
+ this.endDate = endDate;
+ }
+
+ public List getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(List endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ public ResponseFormat getResponseFormat() {
+ return responseFormat;
+ }
+
+ public void setResponseFormat(ResponseFormat responseFormat) {
+ this.responseFormat = responseFormat;
+ }
+
+ public boolean isShowUserInformation() {
+ return showUserInformation;
+ }
+
+ public void setShowUserInformation(boolean showUserInformation) {
+ this.showUserInformation = showUserInformation;
+ }
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/statistic/dto/AccessTrendDto.java b/src/main/java/org/ohdsi/webapi/statistic/dto/AccessTrendDto.java
new file mode 100644
index 000000000..4a3511373
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/dto/AccessTrendDto.java
@@ -0,0 +1,37 @@
+package org.ohdsi.webapi.statistic.dto;
+
+public class AccessTrendDto {
+ private String endpointName;
+ private String executionDate;
+ private String userID;
+
+ public AccessTrendDto(String endpointName, String executionDate, String userID) {
+ this.endpointName = endpointName;
+ this.executionDate = executionDate;
+ this.userID = userID;
+ }
+
+ public String getEndpointName() {
+ return endpointName;
+ }
+
+ public void setEndpointName(String endpointName) {
+ this.endpointName = endpointName;
+ }
+
+ public String getExecutionDate() {
+ return executionDate;
+ }
+
+ public void setExecutionDate(String executionDate) {
+ this.executionDate = executionDate;
+ }
+
+ public String getUserID() {
+ return userID;
+ }
+
+ public void setUserID(String userID) {
+ this.userID = userID;
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/statistic/dto/AccessTrendsDto.java b/src/main/java/org/ohdsi/webapi/statistic/dto/AccessTrendsDto.java
new file mode 100644
index 000000000..7c80e8c80
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/dto/AccessTrendsDto.java
@@ -0,0 +1,16 @@
+package org.ohdsi.webapi.statistic.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccessTrendsDto {
+ private List trends = new ArrayList<>();
+
+ public AccessTrendsDto(List trends) {
+ this.trends = trends;
+ }
+
+ public List getTrends() {
+ return trends;
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/statistic/dto/EndpointDto.java b/src/main/java/org/ohdsi/webapi/statistic/dto/EndpointDto.java
new file mode 100644
index 000000000..5fe302953
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/dto/EndpointDto.java
@@ -0,0 +1,32 @@
+package org.ohdsi.webapi.statistic.dto;
+
+public class EndpointDto {
+ String method;
+ String urlPattern;
+ String userId;
+
+ public String getMethod() {
+ return method;
+ }
+
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ public String getUrlPattern() {
+ return urlPattern;
+ }
+
+ public void setUrlPattern(String urlPattern) {
+ this.urlPattern = urlPattern;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+}
+
diff --git a/src/main/java/org/ohdsi/webapi/statistic/dto/SourceExecutionDto.java b/src/main/java/org/ohdsi/webapi/statistic/dto/SourceExecutionDto.java
new file mode 100644
index 000000000..bfda776db
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/dto/SourceExecutionDto.java
@@ -0,0 +1,50 @@
+package org.ohdsi.webapi.statistic.dto;
+
+import java.time.Instant;
+import java.time.LocalDate;
+
+public class SourceExecutionDto {
+ private String sourceName;
+ private String executionName;
+ private String executionDate;
+ private String userID;
+
+ public SourceExecutionDto(String sourceName, String executionName, String executionDate, String userID) {
+ this.sourceName = sourceName;
+ this.executionName = executionName;
+ this.executionDate = executionDate;
+ this.userID = userID;
+ }
+
+ public String getSourceName() {
+ return sourceName;
+ }
+
+ public void setSourceName(String sourceName) {
+ this.sourceName = sourceName;
+ }
+
+ public String getExecutionName() {
+ return executionName;
+ }
+
+ public void setExecutionName(String executionName) {
+ this.executionName = executionName;
+ }
+
+ public String getExecutionDate() {
+ return executionDate;
+ }
+
+ public void setExecutionDate(String executionDate) {
+ this.executionDate = executionDate;
+ }
+
+ public String getUserID() {
+ return userID;
+ }
+
+ public void setUserID(String userID) {
+ this.userID = userID;
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/statistic/dto/SourceExecutionsDto.java b/src/main/java/org/ohdsi/webapi/statistic/dto/SourceExecutionsDto.java
new file mode 100644
index 000000000..e9d48b15b
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/dto/SourceExecutionsDto.java
@@ -0,0 +1,16 @@
+package org.ohdsi.webapi.statistic.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SourceExecutionsDto {
+ private List executions = new ArrayList<>();
+
+ public SourceExecutionsDto(List executions) {
+ this.executions = executions;
+ }
+
+ public List getExecutions() {
+ return executions;
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java b/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java
new file mode 100644
index 000000000..7bb00cfef
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java
@@ -0,0 +1,230 @@
+package org.ohdsi.webapi.statistic.service;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.ohdsi.webapi.statistic.dto.AccessTrendDto;
+import org.ohdsi.webapi.statistic.dto.AccessTrendsDto;
+import org.ohdsi.webapi.statistic.dto.EndpointDto;
+import org.ohdsi.webapi.statistic.dto.SourceExecutionDto;
+import org.ohdsi.webapi.statistic.dto.SourceExecutionsDto;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Service
+@ConditionalOnProperty(value = "audit.trail.enabled", havingValue = "true")
+public class StatisticService {
+ protected final Logger LOG = LoggerFactory.getLogger(getClass());
+
+ @Value("${audit.trail.log.file}")
+ // TODO remove value
+ private String absoluteLogFileName = "/tmp/atlas/audit/audit.log";
+
+ private String logFileName;
+
+ @Value("${audit.trail.log.file.pattern}")
+ // TODO remove value
+ private String absoluteLogFileNamePattern = "/tmp/atlas/audit/audit-%d{yyyy-MM-dd}-%i.log";
+
+ private String logFileNamePattern;
+
+ private SimpleDateFormat logFileDateFormat;
+
+ private int logFileDateStart;
+
+ private int logFileDateEnd;
+
+ // Some execution can have duplicate logs with different parameters
+ // Duplicate log entries can exist because sometimes ccontroller methods are called from other controller methods
+ // These regular expressions let us to choose only needed log entries
+ private static final Pattern COHORT_GENERATION_REGEXP =
+ Pattern.compile("^.*(\\d{4}-\\d{2}-\\d{2})T\\d{2}:\\d{2}:\\d{2}.*-\\s-\\s-\\s([\\w-]+)\\s.*GET\\s/WebAPI/cohortdefinition/\\d+/generate/(.+)\\s-\\s.*status::String,startDate::Date,endDate::Date.*$");
+
+ private static final Pattern CHARACTERIZATION_GENERATION_REGEXP =
+ Pattern.compile("^.*(\\d{4}-\\d{2}-\\d{2})T\\d{2}:\\d{2}:\\d{2}.*-\\s-\\s-\\s([\\w-]+)\\s.*POST\\s/WebAPI/cohort-characterization/\\d+/generation/(.+)\\s-\\s.*status::String,startDate::Date,endDate::Date.*$");
+
+ private static final Pattern PATHWAY_GENERATION_REGEXP =
+ Pattern.compile("^.*(\\d{4}-\\d{2}-\\d{2})T\\d{2}:\\d{2}:\\d{2}.*-\\s-\\s-\\s([\\w-]+)\\s.*POST\\s/WebAPI/pathway-analysis/\\d+/generation/(.+)\\s-\\s.*status::String,startDate::Date,endDate::Date.*$");
+
+ private static final Pattern IR_GENERATION_REGEXP =
+ Pattern.compile("^.*(\\d{4}-\\d{2}-\\d{2})T\\d{2}:\\d{2}:\\d{2}.*-\\s-\\s-\\s([\\w-]+)\\s.*GET\\s/WebAPI/ir/\\d+/execute/(.+)\\s-\\s.*status::String,startDate::Date,endDate::Date.*$");
+
+ private static final Pattern PLE_GENERATION_REGEXP =
+ Pattern.compile("^.*(\\d{4}-\\d{2}-\\d{2})T\\d{2}:\\d{2}:\\d{2}.*-\\s-\\s-\\s([\\w-]+)\\s.*POST\\s/WebAPI/estimation/\\d+/generation/(.+)\\s-\\s.*status::String,startDate::Date,endDate::Date.*$");
+
+ private static final Pattern PLP_GENERATION_REGEXP =
+ Pattern.compile("^.*(\\d{4}-\\d{2}-\\d{2})T\\d{2}:\\d{2}:\\d{2}.*-\\s-\\s-\\s([\\w-]+)\\s.*POST\\s/WebAPI/prediction/\\d+/generation/(.+)\\s-\\s.*status::String,startDate::Date,endDate::Date.*$");
+
+ private static final String ENDPOINT_REGEXP =
+ "^.*(\\d{4}-\\d{2}-\\d{2})T(\\d{2}:\\d{2}:\\d{2}).*-\\s-\\s-\\s([\\w-]+)\\s.*-\\s({METHOD_PLACEHOLDER}\\s.*{ENDPOINT_PLACEHOLDER})\\s-.*$";
+
+ private static final String COHORT_GENERATION_NAME = "Cohort Generation";
+
+ private static final String CHARACTERIZATION_GENERATION_NAME = "Characterization Generation";
+
+ private static final String PATHWAY_GENERATION_NAME = "Pathway Generation";
+
+ private static final String IR_GENERATION_NAME = "Incidence Rates Generation";
+
+ private static final String PLE_GENERATION_NAME = "Estimation Generation";
+
+ private static final String PLP_GENERATION_NAME = "Prediction Generation";
+
+ private static final Map patternMap = new HashMap<>();
+
+ static {
+ patternMap.put(COHORT_GENERATION_NAME, COHORT_GENERATION_REGEXP);
+ patternMap.put(CHARACTERIZATION_GENERATION_NAME, CHARACTERIZATION_GENERATION_REGEXP);
+ patternMap.put(PATHWAY_GENERATION_NAME, PATHWAY_GENERATION_REGEXP);
+ patternMap.put(IR_GENERATION_NAME, IR_GENERATION_REGEXP);
+ patternMap.put(PLE_GENERATION_NAME, PLE_GENERATION_REGEXP);
+ patternMap.put(PLP_GENERATION_NAME, PLP_GENERATION_REGEXP);
+ }
+
+ public StatisticService() {
+ logFileName = new File(absoluteLogFileName).getName();
+ logFileNamePattern = new File(absoluteLogFileNamePattern).getName();
+
+ // Pattern contains "%d{yyyy-MM-dd}". "%d" will not be contained in real log file name
+ int placeHolderPrefixLength = 3;
+ logFileDateStart = logFileNamePattern.indexOf("{") - placeHolderPrefixLength + 1;
+ logFileDateEnd = logFileNamePattern.indexOf("}") - placeHolderPrefixLength;
+ String dateString = logFileNamePattern.substring(logFileDateStart + placeHolderPrefixLength,
+ logFileDateEnd + placeHolderPrefixLength);
+ logFileDateFormat = new SimpleDateFormat(dateString);
+ }
+
+ public SourceExecutionsDto getSourceExecutions(LocalDate startDate, LocalDate endDate, String sourceKey, boolean showUserInformation) {
+ Set paths = getLogPaths(startDate, endDate);
+ List executions = paths.stream()
+ .flatMap(path -> extractSourceExecutions(path, sourceKey, showUserInformation).stream())
+ .collect(Collectors.toList());
+ return new SourceExecutionsDto(executions);
+ }
+
+ public AccessTrendsDto getAccessTrends(LocalDate startDate, LocalDate endDate, List endpoints, boolean showUserInformation) {
+ Set paths = getLogPaths(startDate, endDate);
+ List trends = paths.stream()
+ .flatMap(path -> extractAccessTrends(path, endpoints, showUserInformation).stream())
+ .collect(Collectors.toList());
+ return new AccessTrendsDto(trends);
+ }
+
+ private List extractSourceExecutions(Path path, String sourceKey, boolean showUserInformation) {
+ try (Stream stream = Files.lines(path)) {
+ return stream
+ .map(str -> getMatchedExecution(str, sourceKey, showUserInformation))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ LOG.error("Error parsing log file {}. {}", path.getFileName(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List extractAccessTrends(Path path, List endpoints, boolean showUserInformation) {
+ List patterns = endpoints.stream()
+ .map(endpointPair -> {
+ String method = endpointPair.getMethod();
+
+ String endpoint = endpointPair.getUrlPattern().replaceAll("\\{\\}", ".*");
+ String regexpStr = ENDPOINT_REGEXP.replace("{METHOD_PLACEHOLDER}", method);
+ regexpStr = regexpStr.replace("{ENDPOINT_PLACEHOLDER}", endpoint);
+
+ return Pattern.compile(regexpStr);
+ })
+
+ .collect(Collectors.toList());
+ try (Stream stream = Files.lines(path)) {
+ return stream
+ .map(str -> {
+ return patterns.stream()
+ .map(pattern -> pattern.matcher(str))
+ .filter(Matcher::matches)
+ .map(matcher -> new AccessTrendDto(matcher.group(4), matcher.group(1), showUserInformation ? matcher.group(3) : null))
+ .findFirst();
+ })
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ LOG.error("Error parsing log file {}. {}", path.getFileName(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Optional getMatchedExecution(String str, String sourceKey, boolean showUserInformation) {
+ return patternMap.entrySet().stream()
+ .map(entry -> new ImmutablePair<>(entry.getKey(), entry.getValue().matcher(str)))
+ .filter(pair -> pair.getValue().matches())
+ .filter(pair -> sourceKey == null || (sourceKey != null && sourceKey.equals(pair.getValue().group(3))))
+ .map(pair -> new SourceExecutionDto(pair.getValue().group(3), pair.getKey(), pair.getValue().group(1), showUserInformation ? pair.getValue().group(2) : null))
+ .findFirst();
+ }
+
+ private Set getLogPaths(LocalDate startDate, LocalDate endDate) {
+ String folderPath = new File(absoluteLogFileName).getParentFile().getAbsolutePath();
+ try (Stream stream = Files.list(Paths.get(folderPath))) {
+ return stream
+ .filter(file -> !Files.isDirectory(file))
+ .filter(this::isValidLogFile)
+ .filter(file -> isLogInDateRange(file, startDate, endDate))
+ .map(Path::toAbsolutePath)
+ .collect(Collectors.toSet());
+ } catch (IOException e) {
+ LOG.error("Error getting list of log files", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean isValidLogFile(Path path) {
+ return path.getFileName().toString().endsWith(".log");
+ }
+
+ private boolean isLogInDateRange(Path path, LocalDate startDate, LocalDate endDate) {
+ if (startDate == null && endDate == null) {
+ return true;
+ }
+ LocalDate logDate = getFileDate(path.getFileName());
+ if ((startDate != null && logDate.isBefore(startDate))
+ || (endDate != null && logDate.isAfter(endDate))) {
+ return false;
+ }
+ return true;
+ }
+
+ private LocalDate getFileDate(Path path) {
+ String fileName = path.toString();
+ if (logFileName.equals(fileName)) {
+ return LocalDate.now();
+ }
+ try {
+ String dateStr = fileName.substring(logFileDateStart, logFileDateEnd);
+ return logFileDateFormat.parse(dateStr).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+ } catch (ParseException | IndexOutOfBoundsException e) {
+ // If we cannot check the date of a file, then assume that it is a file for the current date
+ return LocalDate.now();
+ }
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index cd1afb201..fef844aaa 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -277,4 +277,5 @@ versioning.maxAttempt=${versioning.maxAttempt}
#Audit trail
audit.trail.enabled=${audit.trail.enabled}
audit.trail.log.file=${audit.trail.log.file}
+audit.trail.log.file.pattern=${audit.trail.log.file.pattern}
audit.trail.log.extraFile=${audit.trail.log.extraFile}
diff --git a/src/main/resources/db/migration/postgresql/V2.13.1.20231002112232__source_execution_satatistics.sql b/src/main/resources/db/migration/postgresql/V2.13.1.20231002112232__source_execution_satatistics.sql
new file mode 100644
index 000000000..6f26e7822
--- /dev/null
+++ b/src/main/resources/db/migration/postgresql/V2.13.1.20231002112232__source_execution_satatistics.sql
@@ -0,0 +1,15 @@
+INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES
+ (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'statistic:executions:get', 'Source execution statistics permission');
+
+INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES
+ (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'statistic:accesstrends:post', 'Access trends statistics permission');
+
+INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id)
+SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id
+FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr
+WHERE sp.value IN ('statistic:executions:get') AND sr.name IN ('admin');
+
+INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id)
+SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id
+FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr
+WHERE sp.value IN ('statistic:accesstrends:post') AND sr.name IN ('admin');
\ No newline at end of file
diff --git a/src/main/resources/db/migration/postgresql/V2.14.0.20231206190600__fixing_statistics_enpoint_permission_method.sql b/src/main/resources/db/migration/postgresql/V2.14.0.20231206190600__fixing_statistics_enpoint_permission_method.sql
new file mode 100644
index 000000000..df5fe8b3c
--- /dev/null
+++ b/src/main/resources/db/migration/postgresql/V2.14.0.20231206190600__fixing_statistics_enpoint_permission_method.sql
@@ -0,0 +1,2 @@
+UPDATE ${ohdsiSchema}.sec_permission SET value='statistic:executions:post'
+WHERE value='statistic:executions:get';
\ No newline at end of file
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
index 011405298..6e5b5674e 100644
--- a/src/main/resources/log4j2.xml
+++ b/src/main/resources/log4j2.xml
@@ -2,7 +2,8 @@
${bundle:application:audit.trail.log.extraFile}
- ${bundle:application:audit.trail.log.file}
+ ${bundle:application:audit.trail.log.file}
+ ${bundle:application:audit.trail.log.file.pattern}
${bundle:application:logging.level.org.apache.shiro}
${bundle:application:logging.level.org.ohdsi}
${bundle:application:logging.level.org.pac4j}
@@ -12,7 +13,7 @@
+ filePattern="${audit.trail.log.file.pattern}">
%m%n