From c98b8ce5656774573a7484fb7a4d0394953b7c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Z=C3=B6llner?= Date: Wed, 10 Jul 2024 17:40:15 +0200 Subject: [PATCH] log mock invocations and request order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andreas Zöllner --- .../microcks/domain/InvocationLogEntry.java | 125 ++++++++++++++++++ .../io/github/microcks/domain/Operation.java | 9 ++ .../microcks/event/MockInvocationEvent.java | 9 +- .../listener/InvocationAdvancedMetrics.java | 51 +++++++ .../repository/InvocationLogRepository.java | 37 ++++++ .../InvocationLogRepositoryImpl.java | 100 ++++++++++++++ .../util/metadata/MetadataExtractor.java | 3 + .../web/DynamicMockRestController.java | 2 +- .../microcks/web/GraphQLController.java | 2 +- .../microcks/web/GrpcServerCallHandler.java | 2 +- .../io/github/microcks/web/LogController.java | 52 ++++++++ .../microcks/web/MockControllerCommons.java | 20 ++- .../github/microcks/web/RestController.java | 3 +- .../github/microcks/web/SoapController.java | 2 +- .../resources/config/application.properties | 1 + .../listener/DailyStatisticsFeederTest.java | 2 +- 16 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 commons/model/src/main/java/io/github/microcks/domain/InvocationLogEntry.java create mode 100644 webapp/src/main/java/io/github/microcks/listener/InvocationAdvancedMetrics.java create mode 100644 webapp/src/main/java/io/github/microcks/repository/InvocationLogRepository.java create mode 100644 webapp/src/main/java/io/github/microcks/repository/InvocationLogRepositoryImpl.java create mode 100644 webapp/src/main/java/io/github/microcks/web/LogController.java diff --git a/commons/model/src/main/java/io/github/microcks/domain/InvocationLogEntry.java b/commons/model/src/main/java/io/github/microcks/domain/InvocationLogEntry.java new file mode 100644 index 000000000..4ac003226 --- /dev/null +++ b/commons/model/src/main/java/io/github/microcks/domain/InvocationLogEntry.java @@ -0,0 +1,125 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +@Document("invocations") +public class InvocationLogEntry { + @Id + private Long timestampEpoch; + private String serviceName; + /** + * + */ + private String serviceVersion; + /** + * + */ + private String mockResponse; + /** + * + */ + private Date invocationTimestamp; + /** + * + */ + private long duration; + private String source; + private String requestId; + + public InvocationLogEntry(Long timestampEpoch, String serviceName, String serviceVersion, String mockResponse, + Date invocationTimestamp, long duration, String source, String requestId) { + this.timestampEpoch = timestampEpoch; + this.serviceName = serviceName; + this.serviceVersion = serviceVersion; + this.mockResponse = mockResponse; + this.invocationTimestamp = invocationTimestamp; + this.duration = duration; + this.source = source; + this.requestId = requestId; + } + + public InvocationLogEntry() { + } + + public Long getTimestampEpoch() { + return timestampEpoch; + } + + public void setTimestampEpoch(Long timestampEpoch) { + this.timestampEpoch = timestampEpoch; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceVersion() { + return serviceVersion; + } + + public void setServiceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + } + + public String getMockResponse() { + return mockResponse; + } + + public void setMockResponse(String mockResponse) { + this.mockResponse = mockResponse; + } + + public Date getInvocationTimestamp() { + return invocationTimestamp; + } + + public void setInvocationTimestamp(Date invocationTimestamp) { + this.invocationTimestamp = invocationTimestamp; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } +} diff --git a/commons/model/src/main/java/io/github/microcks/domain/Operation.java b/commons/model/src/main/java/io/github/microcks/domain/Operation.java index 9ffaaf368..f18ea1536 100644 --- a/commons/model/src/main/java/io/github/microcks/domain/Operation.java +++ b/commons/model/src/main/java/io/github/microcks/domain/Operation.java @@ -43,6 +43,7 @@ public class Operation { private Set resourcePaths; private List parameterConstraints; + private String idPath; public String getName() { return name; @@ -162,4 +163,12 @@ public void addParameterConstraint(ParameterConstraint constraint) { } parameterConstraints.add(constraint); } + + public void setIdPath(String idPath) { + this.idPath = idPath; + } + + public String getIdPath() { + return idPath; + } } diff --git a/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java b/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java index f0dd288e1..27fcb2b0f 100644 --- a/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java +++ b/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java @@ -35,6 +35,8 @@ public class MockInvocationEvent extends ApplicationEvent { private final Date invocationTimestamp; /** */ private final long duration; + /** */ + private final String requestId; /** * Create a new mock invocation event. @@ -46,13 +48,14 @@ public class MockInvocationEvent extends ApplicationEvent { * @param duration Duration of invocation */ public MockInvocationEvent(Object source, String serviceName, String serviceVersion, String mockResponse, - Date invocationTimestamp, long duration) { + Date invocationTimestamp, long duration, String requestId) { super(source); this.serviceName = serviceName; this.serviceVersion = serviceVersion; this.mockResponse = mockResponse; this.invocationTimestamp = invocationTimestamp; this.duration = duration; + this.requestId = requestId; } public String getServiceName() { @@ -74,4 +77,8 @@ public Date getInvocationTimestamp() { public long getDuration() { return duration; } + + public String getRequestId() { + return requestId; + } } diff --git a/webapp/src/main/java/io/github/microcks/listener/InvocationAdvancedMetrics.java b/webapp/src/main/java/io/github/microcks/listener/InvocationAdvancedMetrics.java new file mode 100644 index 000000000..10b044d61 --- /dev/null +++ b/webapp/src/main/java/io/github/microcks/listener/InvocationAdvancedMetrics.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.domain.InvocationLogEntry; +import io.github.microcks.event.MockInvocationEvent; +import io.github.microcks.repository.InvocationLogRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +/** + * Log invocations to database, listens to MockInvocationEvent, properties mocks.enable-invocation-logs and + * mocks.enable-invocation-stats need to be enabled to use this. + */ +@Component +@ConditionalOnProperty(name = "mocks.enable-invocation-logs", havingValue = "true") +public class InvocationAdvancedMetrics implements ApplicationListener { + + final InvocationLogRepository repo; + + public InvocationAdvancedMetrics(InvocationLogRepository repo) { + this.repo = repo; + } + + @Override + public void onApplicationEvent(@NonNull MockInvocationEvent event) { + repo.insert(new InvocationLogEntry(event.getTimestamp(), event.getServiceName(), event.getServiceVersion(), + event.getMockResponse(), event.getInvocationTimestamp(), event.getDuration(), + event.getSource().getClass().getSimpleName(), event.getRequestId())); + } + + @Override + public boolean supportsAsyncExecution() { + return ApplicationListener.super.supportsAsyncExecution(); + } +} diff --git a/webapp/src/main/java/io/github/microcks/repository/InvocationLogRepository.java b/webapp/src/main/java/io/github/microcks/repository/InvocationLogRepository.java new file mode 100644 index 000000000..a0033e270 --- /dev/null +++ b/webapp/src/main/java/io/github/microcks/repository/InvocationLogRepository.java @@ -0,0 +1,37 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.InvocationLogEntry; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.repository.NoRepositoryBean; +import java.util.List; + +/** + * Repository for InvocationLogEntries + */ +@NoRepositoryBean +public interface InvocationLogRepository extends MongoRepository { + + /** + * find the latest invocation log entries from database + * @param service Service to query + * @param version Version of the service to query + * @param limit maximum number of entries + * @return List of log entries ordered by newest entry first + */ + List findLastEntriesByServiceName(String service, String version, int limit); +} diff --git a/webapp/src/main/java/io/github/microcks/repository/InvocationLogRepositoryImpl.java b/webapp/src/main/java/io/github/microcks/repository/InvocationLogRepositoryImpl.java new file mode 100644 index 000000000..f41706abc --- /dev/null +++ b/webapp/src/main/java/io/github/microcks/repository/InvocationLogRepositoryImpl.java @@ -0,0 +1,100 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.InvocationLogEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * Implementation for InvocationLogRepository + */ +@Repository +public class InvocationLogRepositoryImpl extends SimpleMongoRepository + implements InvocationLogRepository { + + @Autowired + private MongoTemplate mongoTemplate; + + public InvocationLogRepositoryImpl(MongoOperations mongoOperations) { + super(new MongoEntityInformation<>() { + @Override + @NonNull + public Class getJavaType() { + return InvocationLogEntry.class; + } + + @Override + public boolean isNew(@NonNull InvocationLogEntry entity) { + return false; + } + + @Override + public Long getId(@NonNull InvocationLogEntry entity) { + return entity.getInvocationTimestamp().toInstant().toEpochMilli(); + } + + @Override + @NonNull + public Class getIdType() { + return Long.class; + } + + @Override + @NonNull + public String getCollectionName() { + return "invocations"; + } + + @Override + @NonNull + public String getIdAttribute() { + return "invocationTimestamp"; + } + + @Override + public Collation getCollation() { + return null; + } + }, mongoOperations); + } + + /** + * find the latest invocation log entries from database + * @param service Service to query + * @param version Version of the service to query + * @param limit maximum number of entries + * @return List of log entries ordered by newest entry first + */ + @Override + public List findLastEntriesByServiceName(String service, String version, int limit) { + Query query = new Query().addCriteria(Criteria.where("serviceName").is(service)) + .addCriteria(Criteria.where("serviceVersion").is(version)) + .with(Sort.by(Sort.Direction.DESC, "invocationTimestamp")).limit(limit); + return mongoTemplate.find(query, InvocationLogEntry.class); + } +} diff --git a/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java b/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java index c97377f4f..9c527049c 100644 --- a/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java +++ b/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java @@ -61,5 +61,8 @@ public static void completeOperationProperties(Operation operation, JsonNode nod if (node.has("dispatcherRules")) { operation.setDispatcherRules(node.path("dispatcherRules").asText()); } + if (node.has("requestIdPath")) { + operation.setIdPath(node.path("requestIdPath").asText()); + } } } diff --git a/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java b/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java index bdd050039..7a5c1b702 100644 --- a/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java +++ b/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java @@ -324,7 +324,7 @@ private void waitForDelay(Long since, Long delay, MockContext mockContext) { if (enableInvocationStats) { MockInvocationEvent event = new MockInvocationEvent(this, mockContext.service.getName(), mockContext.service.getVersion(), "DynamicMockRestController", new Date(since), - since - System.currentTimeMillis()); + since - System.currentTimeMillis(), ""); applicationContext.publishEvent(event); log.debug("Mock invocation event has been published"); } diff --git a/webapp/src/main/java/io/github/microcks/web/GraphQLController.java b/webapp/src/main/java/io/github/microcks/web/GraphQLController.java index 601e8e173..1adf8bfc8 100644 --- a/webapp/src/main/java/io/github/microcks/web/GraphQLController.java +++ b/webapp/src/main/java/io/github/microcks/web/GraphQLController.java @@ -263,7 +263,7 @@ public ResponseEntity execute(@PathVariable("service") String serviceName, // Publish an invocation event before returning if enabled. if (Boolean.TRUE.equals(enableInvocationStats)) { MockControllerCommons.publishMockInvocation(applicationContext, this, service, - graphqlResponses.get(0).getResponse(), startTime); + graphqlResponses.get(0).getResponse(), startTime, ""); } String responseContent = null; diff --git a/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java b/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java index abd6855b9..3636a09d2 100644 --- a/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java +++ b/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java @@ -319,7 +319,7 @@ private void manageResponseTransmission(StreamObserver streamObserver, S // Publish an invocation event before returning if enabled. if (Boolean.TRUE.equals(enableInvocationStats)) { - MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime); + MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime, ""); } // Send the output message and complete the stream. diff --git a/webapp/src/main/java/io/github/microcks/web/LogController.java b/webapp/src/main/java/io/github/microcks/web/LogController.java new file mode 100644 index 000000000..a80c4d112 --- /dev/null +++ b/webapp/src/main/java/io/github/microcks/web/LogController.java @@ -0,0 +1,52 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.InvocationLogEntry; +import io.github.microcks.repository.InvocationLogRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Comparator; +import java.util.List; + +/** + * Endpoint to for accessing the log of the last mock invocations. + */ +@RestController +@ConditionalOnProperty(name = "mocks.enable-invocation-logs", havingValue = "true") +@RequestMapping("/api") +public class LogController { + + @Autowired + private InvocationLogRepository invocationLogRepository; + + /** + * get the latest invocations from database + * @param service Service to be fetched + * @param version Version of the service to be fetched + * @param limit maximum number of latest results + * @return list of results ordered by newest entry first + */ + @GetMapping(value = "/log/invocations/{service}/{version}") + public List logInvocations(@PathVariable("service") String service, + @PathVariable("version") String version, @RequestParam(value = "limit", defaultValue = "10") int limit) { + return invocationLogRepository.findLastEntriesByServiceName(service, version, limit).stream() + .sorted(Comparator.comparing(InvocationLogEntry::getInvocationTimestamp).reversed()).toList(); + } +} diff --git a/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java b/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java index 5e0f4a365..63a4ebd18 100644 --- a/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java +++ b/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java @@ -323,17 +323,19 @@ public static void waitForDelay(Long startTime, Long delay) { /** * Publish a mock invocation event on Spring ApplicationContext internal bus. + * * @param applicationContext The context to use for publication * @param eventSource The source of this event * @param service The mocked Service that was invoked * @param response The response it has been dispatched to * @param startTime The start time of the invocation + * @param id The rendered ID to be saved for this request */ public static void publishMockInvocation(ApplicationContext applicationContext, Object eventSource, Service service, - Response response, Long startTime) { + Response response, Long startTime, String id) { // Publish an invocation event before returning. MockInvocationEvent event = new MockInvocationEvent(eventSource, service.getName(), service.getVersion(), - response.getName(), new Date(startTime), startTime - System.currentTimeMillis()); + response.getName(), new Date(startTime), startTime - System.currentTimeMillis(), id); applicationContext.publishEvent(event); log.debug("Mock invocation event has been published"); } @@ -353,4 +355,18 @@ public static String extractResourcePath(HttpServletRequest request, String serv } return resourcePath; } + + /** + * extract and render a request identifier based on supplied operation metadata. + * @param requestBody Request Body to be used as input + * @param requestResourcePath Request Resource Path to be used as parameter + * @param request Request conext + * @param idString Templated string for rendering the id + * @return rendered request id as string + */ + public static String extractId(String requestBody, String requestResourcePath, HttpServletRequest request, + String idString) { + return unguardedRenderResponseContent(buildEvaluableRequest(requestBody, requestResourcePath, request), + Collections.emptyMap(), TemplateEngineFactory.getTemplateEngine(), idString); + } } diff --git a/webapp/src/main/java/io/github/microcks/web/RestController.java b/webapp/src/main/java/io/github/microcks/web/RestController.java index 4208d95d0..184a2bb14 100644 --- a/webapp/src/main/java/io/github/microcks/web/RestController.java +++ b/webapp/src/main/java/io/github/microcks/web/RestController.java @@ -277,7 +277,8 @@ public ResponseEntity execute(@PathVariable("service") String serviceNam // Publish an invocation event before returning if enabled. if (Boolean.TRUE.equals(enableInvocationStats)) { - MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime); + String id = MockControllerCommons.extractId(body, resourcePath, request, operation.getIdPath()); + MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime, id); } // Return response content or just headers. diff --git a/webapp/src/main/java/io/github/microcks/web/SoapController.java b/webapp/src/main/java/io/github/microcks/web/SoapController.java index c525f5bbc..7283bd6ce 100644 --- a/webapp/src/main/java/io/github/microcks/web/SoapController.java +++ b/webapp/src/main/java/io/github/microcks/web/SoapController.java @@ -297,7 +297,7 @@ public ResponseEntity execute(@PathVariable("service") String serviceName, // Publish an invocation event before returning if enabled. if (Boolean.TRUE.equals(enableInvocationStats)) { - MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime); + MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime, ""); } if (response.isFault()) { diff --git a/webapp/src/main/resources/config/application.properties b/webapp/src/main/resources/config/application.properties index 3f97d1f03..86c89d3f0 100644 --- a/webapp/src/main/resources/config/application.properties +++ b/webapp/src/main/resources/config/application.properties @@ -28,6 +28,7 @@ validation.resourceUrl=http://localhost:8080/api/resources/ services.update.interval=${SERVICES_UPDATE_INTERVAL:0 0 0/2 * * *} mocks.enable-invocation-stats=${ENABLE_INVOCATION_STATS:true} +mocks.enable-invocation-logs=${ENABLE_INVOCATION_LOGS:true} mocks.rest.enable-cors-policy=${ENABLE_CORS_POLICY:true} mocks.rest.cors.allowedOrigins=${CORS_REST_ALLOWED_ORIGINS:*} mocks.rest.cors.allowCredentials=${CORS_REST_ALLOW_CREDENTIALS:false} diff --git a/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java b/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java index a8353308e..262bec846 100644 --- a/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java +++ b/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java @@ -51,7 +51,7 @@ class DailyStatisticsFeederTest { void testOnApplicationEvent() { Calendar today = Calendar.getInstance(); MockInvocationEvent event = new MockInvocationEvent(this, "TestService1", "1.0", "123456789", today.getTime(), - 100); + 100, ""); // Fire event a first time. feeder.onApplicationEvent(event);