From 31e2e4cda58e276b49befe42c1b7ec3eb48f3753 Mon Sep 17 00:00:00 2001 From: "david.krzystek" Date: Mon, 13 Jul 2020 10:36:48 +0200 Subject: [PATCH] Splunk Custom Steps --- jbehave-support-core-test/README.md | 2 +- .../core/test/app/SplunkController.java | 75 ++++++++++ .../src/main/resources/application.yml | 4 + .../main/resources/splunk/login-response.xml | 6 + .../splunk/search-data-response.json | 36 +++++ .../splunk/search-no-data-response.json | 7 + .../resources/splunk/server-info-response.xml | 129 ++++++++++++++++ jbehave-support-core/README.md | 1 + jbehave-support-core/docs/Splunk.md | 90 ++++++++++++ jbehave-support-core/pom.xml | 5 + .../core/JBehaveDefaultConfig.java | 10 +- .../splunk/OneShotSearchSplunkClient.java | 139 ++++++++++++++++++ .../core/internal/splunk/SplunkArgNames.java | 14 ++ .../internal/splunk/SplunkEventFields.java | 15 ++ .../internal/splunk/SplunkOutputModes.java | 34 +++++ .../core/splunk/SplunkClient.java | 59 ++++++++ .../core/splunk/SplunkConfig.java | 47 ++++++ .../core/splunk/SplunkSearchResultEntry.java | 28 ++++ .../core/splunk/SplunkSteps.java | 91 ++++++++++++ .../org/jbehavesupport/core/TestConfig.groovy | 16 ++ .../test/sample/SampleStoriesIT.java | 3 +- .../jbehavesupport/test/sample/Splunk.story | 28 ++++ .../test/support/TestConfig.java | 47 ++++-- .../src/test/resources/test.yml | 12 +- pom.xml | 14 ++ 25 files changed, 891 insertions(+), 21 deletions(-) create mode 100644 jbehave-support-core-test/jbehave-support-core-test-app/src/main/java/org/jbehavesupport/core/test/app/SplunkController.java create mode 100644 jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/login-response.xml create mode 100644 jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-data-response.json create mode 100644 jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-no-data-response.json create mode 100644 jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/server-info-response.xml create mode 100644 jbehave-support-core/docs/Splunk.md create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/OneShotSearchSplunkClient.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkArgNames.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkEventFields.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkOutputModes.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkClient.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkConfig.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSearchResultEntry.java create mode 100644 jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSteps.java create mode 100644 jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/Splunk.story diff --git a/jbehave-support-core-test/README.md b/jbehave-support-core-test/README.md index f4167162..f7b28595 100644 --- a/jbehave-support-core-test/README.md +++ b/jbehave-support-core-test/README.md @@ -21,7 +21,7 @@ Application exposes H2 database: We are running database in server mode without storage (in memory mode). -Application also expose webservice endpoint for webservice testing. Endpoint url is: http://localhost:8080/services/ +Application also expose webservice endpoint for webservice testing. Endpoint url is: http://localhost:8080/ws/ ## run test application locally You can run test application locally: diff --git a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/java/org/jbehavesupport/core/test/app/SplunkController.java b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/java/org/jbehavesupport/core/test/app/SplunkController.java new file mode 100644 index 00000000..9d68cd83 --- /dev/null +++ b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/java/org/jbehavesupport/core/test/app/SplunkController.java @@ -0,0 +1,75 @@ +package org.jbehavesupport.core.test.app; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Map; + +import static java.util.Objects.nonNull; + +@RestController +@RequestMapping("/services") +public class SplunkController { + + public static final String CONTENT_KEY_SEARCH = "search"; + public static final String SEARCH_NO_DATA = "no data"; + + @Value("classpath:splunk/server-info-response.xml") + Resource mockedServerInfoResponse; + + @Value("classpath:splunk/login-response.xml") + Resource mockedLoginResponse; + + @Value("classpath:splunk/search-data-response.json") + Resource mockedSearchDataResponse; + + @Value("classpath:splunk/search-no-data-response.json") + Resource mockedSearchNoDataResponse; + + @PostMapping(value = "/auth/login", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.TEXT_XML_VALUE) + public ResponseEntity login(@RequestParam Map body) { + return new ResponseEntity<>(resourceToString(mockedLoginResponse), HttpStatus.OK); + } + + @GetMapping(value = "/server/info", + produces = MediaType.TEXT_XML_VALUE) + public ResponseEntity info() { + return new ResponseEntity<>(resourceToString(mockedServerInfoResponse), HttpStatus.OK); + } + + @PostMapping(value = "/search/jobs", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity search(@RequestParam Map body) { + ResponseEntity response = new ResponseEntity<>(resourceToString(mockedSearchDataResponse), HttpStatus.OK); + if (nonNull(body) && shouldNotReturnSearchDataMatch(body)) { + response = new ResponseEntity<>(resourceToString(mockedSearchNoDataResponse), HttpStatus.OK); + } + return response; + } + + private boolean shouldNotReturnSearchDataMatch(Map body) { + return body.getOrDefault(CONTENT_KEY_SEARCH, "").contains(SEARCH_NO_DATA); + } + + private String resourceToString(Resource resource) { + try { + return StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset()); + } catch (IOException e) { + throw new IllegalStateException("Unable to read resource " + resource.getFilename()); + } + } +} diff --git a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/application.yml b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/application.yml index 34314d50..0cc436d6 100644 --- a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/application.yml +++ b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/application.yml @@ -1,3 +1,5 @@ + + spring: datasource: port: 11112 @@ -9,6 +11,8 @@ spring: user: name: sa password: sa + webservices: + path: /ws server: port: 11110 diff --git a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/login-response.xml b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/login-response.xml new file mode 100644 index 00000000..9ee359cf --- /dev/null +++ b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/login-response.xml @@ -0,0 +1,6 @@ + + HRr5SWhzWRAmmzkF10zqr1nAehUya6vnOex5GO^nupzJsxMdlIEvkIwdxlBbg66NmXq30D8VktgwtD1q5zRRVtdCl1uqdiOTL7oLDsX4RwRuqusapFBjs8Y + + + + \ No newline at end of file diff --git a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-data-response.json b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-data-response.json new file mode 100644 index 00000000..525fcd6f --- /dev/null +++ b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-data-response.json @@ -0,0 +1,36 @@ +{ + "preview": false, + "init_offset": 0, + "messages": [], + "fields": [ + { + "name": "host" + }, + { + "name": "namespace" + }, + { + "name": "index" + }, + { + "name": "_time" + }, + { + "name": "message" + }, + { + "name": "level" + } + ], + "results": [ + { + "host": "a8210fc9865a", + "namespace": "in00a1-hi-am", + "index": "main", + "_time": "2020-07-10T07:40:26.631+00:00", + "message": "response for https://am.in00a1.cz.infra/cabus-am/rest/openapi/v2/account/persons/147814/accountBalance: 200 OK with headers [Date:\"Fri, 10 Jul 2020 07:39:50 GMT\", Content-Type:\"application/json;charset=UTF-8\", Transfer-Encoding:\"chunked\", Connection:\"keep-alive\", Vary:\"Accept-Encoding\", X-B3-TraceId:\"30a3d60bd3d698eae25eaf5afe3e1df5\", X-B3-SpanId:\"48a4bc7562a346eb\", X-B3-ParentSpanId:\"1017258853b1a1d7\", X-B3-Sampled:\"1\", Cache-Control:\"no-cache, no-store, max-age=0, must-revalidate\", Pragma:\"no-cache\", Expires:\"0\", Strict-Transport-Security:\"max-age=15724800; includeSubDomains\", X-XSS-Protection:\"1; mode=block\", X-Frame-Options:\"DENY\", X-Content-Type-Options:\"nosniff\"]", + "level": "DEBUG" + } + ], + "highlighted": {} +} \ No newline at end of file diff --git a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-no-data-response.json b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-no-data-response.json new file mode 100644 index 00000000..bd9a03f3 --- /dev/null +++ b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/search-no-data-response.json @@ -0,0 +1,7 @@ +{ + "preview": false, + "init_offset": 0, + "post_process_count": 0, + "messages": [], + "results": [] +} \ No newline at end of file diff --git a/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/server-info-response.xml b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/server-info-response.xml new file mode 100644 index 00000000..1aeba32c --- /dev/null +++ b/jbehave-support-core-test/jbehave-support-core-test-app/src/main/resources/splunk/server-info-response.xml @@ -0,0 +1,129 @@ + + + + server-info + https://localhost:8089/services/server/info + 2020-07-18T09:19:35+00:00 + + + Splunk + + 1 + 30 + 0 + + + server-info + https://localhost:8089/services/server/info/server-info + 1970-01-01T00:00:00+00:00 + + + system + + + + + Trial + Production + + + + + + + 0 + + + add_on + + + + + + + report + 6F416E61-B40E-461C-A782-CBC186E98133 + 200 + + + external_results_provider + + + + + ab7a85abaa98 + x86_64 + 0 + + + + 1 + 1 + 0 + system + + + + + * + + + + + + + + 0 + system + + + 0 + 5634CE98-7B24-43C0-9A97-00B36F905DE4 + green + 1 + a8210fc9865a + a8210fc9865a + a8210fc9865a + 0 + 0 + 1 + ready + + + 5C52DA5145AD67B8188604C49962D12F2C3B2CF1B82A6878E46F68CA2812807B + + + c31f28d42cbf21ca3563f0244d70d594 + OK + + + Splunk Enterprise Splunk Analytics for Hadoop Download Trial + + + 5634CE98-7B24-43C0-9A97-00B36F905DE4 + self + 4294967295 + normal + 10 + 10 + #56~18.04.1-Ubuntu SMP Wed Jun 24 16:17:03 UTC 2020 + Linux + Linux + 5.3.0-62-generic + 23003 + enterprise + 1 + a8210fc9865a + + + indexer + license_master + + + 1595056837 + C79F504C1A3CB7BEE6C5EE03E10BFDF783961A0DE0E4B0AF6F4D75607F96D04C + 8.0.4.1 + + + + \ No newline at end of file diff --git a/jbehave-support-core/README.md b/jbehave-support-core/README.md index eefa24d9..a62bb76e 100644 --- a/jbehave-support-core/README.md +++ b/jbehave-support-core/README.md @@ -11,6 +11,7 @@ Core jBehave support. - [Web service steps](docs/Web-service.md) - [SQL steps](docs/Sql-steps.md) - [SSH steps](docs/Ssh.md) + - [Splunk steps](docs/Splunk.md) - [JMS steps](docs/Jms.md) - [Health check steps](docs/Health-checks.md) - [Reporting](docs/Reporting.md) diff --git a/jbehave-support-core/docs/Splunk.md b/jbehave-support-core/docs/Splunk.md new file mode 100644 index 00000000..e8b2cdb3 --- /dev/null +++ b/jbehave-support-core/docs/Splunk.md @@ -0,0 +1,90 @@ +[Contents](../README.md) + +## Splunk steps + +### Configuration +All the search queries are handled by a bean of `SplunkClient` type. + +It is configured with a few properties that will be set to a bean of `SplunkConfig` type. + +The properties are following: + +| name | value | note | +|--------------------------------|----------------------------------------|---------------------------------------------------------| +| splunk.host | host with running Splunk instance | | +| splunk.port | 8089 | | +| splunk.scheme | http or https | Use "https" for EIT/Local Docker Splunk instance | +| splunk.sslSecurityProtocol | TLSv1_2 | Used when splunk.scheme="https" | +| splunk.credentials.username | technical user username | Used for test purposes when you dont have authToken | +| splunk.credentials.password | technical user password | Used for test purposes when you dont have authToken | +| splunk.credentials.token | Bearer | Prefer this to username/password | + + +Example of configuration working with local Docker Splunk instance: +``` +splunk: + host: localhost + port: 8089 + scheme: https + sslSecurityProtocol: TLSv1_2 + credentials: + token: Bearer +``` + +Example of configuration working with Splunk endpoints mocked by JBehaveSupport Core Test Application: +``` +splunk: + host: localhost + port: 11110 + scheme: http + credentials: + username: admin + password: password +``` + + +### Available steps +There are two steps available for sending requests to Splunk Search API using following input: +* Splunk search query +* Splunk search query within a given timeframe + +#### Splunk Query +The following step sends a request without a time range. +``` +When the Splunk search query is performed: +search index="am-index" namespace="*" level="*" message="*response*" traceId="{CP:TRACEID_VARIABLE}" | tail 1 +``` + +#### Splunk Query with Time Range +The following step will send a request including time range. + +``` +When the Splunk search query is performed within [2020-07-10T00:00:00.000+02:00] and [2020-07-20T23:59:59.000+02:00]: +search index="am-index" namespace="*" level="*" message="*response*" traceId="{CP:TRACEID_VARIABLE}" | tail 1 +``` + +Please note that the both boundaries can leverage context variables as well as it is available in the query. +``` +When the Splunk search query is performed within [{CP:EARLIEST_TIME_VARIABLE}] and [{CP:LATEST_TIME_VARIABLE}]: +search index="am-index" namespace="*" level="*" message="*response*" traceId="{CP:TRACEID_VARIABLE}" | tail 1 +``` + +#### Verification Steps +Search results can be verified with the help of two more steps. + +##### Search Result Size +You can verify that Splunk returned particular number of rows +``` +Then the Splunk search result set has 1 row(s) +``` + +You can also verify that all returned rows match verifier rules. +``` +Then the Splunk search result match these rules: +| data | verifier | +| ^.*200 OK with headers.*$ | REGEX_MATCH | +| X-B3-TraceId:"30a3d60bd3d698eae25eaf5afe3e1df5" | CONTAINS | +| does not contain | NOT_CONTAINS | +| | NOT_NULL | +| no equality | NE | +``` \ No newline at end of file diff --git a/jbehave-support-core/pom.xml b/jbehave-support-core/pom.xml index c27934f3..4c503c69 100644 --- a/jbehave-support-core/pom.xml +++ b/jbehave-support-core/pom.xml @@ -330,6 +330,11 @@ com.sun.activation jakarta.activation + + com.splunk + splunk + true + diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/JBehaveDefaultConfig.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/JBehaveDefaultConfig.java index 9ce9e9e8..9fa8ac37 100644 --- a/jbehave-support-core/src/main/java/org/jbehavesupport/core/JBehaveDefaultConfig.java +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/JBehaveDefaultConfig.java @@ -16,9 +16,11 @@ import org.jbehavesupport.core.internal.web.waitcondition.WebWaitConditionResolverImpl; import org.jbehavesupport.core.internal.web.webdriver.WebDriverDelegatingInterceptor; import org.jbehavesupport.core.internal.web.webdriver.WebDriverFactoryResolverImpl; +import org.jbehavesupport.core.internal.splunk.OneShotSearchSplunkClient; +import org.jbehavesupport.core.splunk.SplunkClient; +import org.jbehavesupport.core.splunk.SplunkConfig; import org.jbehavesupport.core.ssh.SshHandler; import org.jbehavesupport.core.support.TimeFacade; - import org.jbehavesupport.core.verification.Verifier; import org.jbehavesupport.core.verification.VerifierResolver; import org.jbehavesupport.core.web.ByFactory; @@ -149,4 +151,10 @@ public WebDriver webDriver(WebDriverFactoryResolver webDriverFactoryResolver) { return (WebDriver) proxyFactory.getProxy(); } + @Bean + @ConditionalOnMissingBean(SplunkClient.class) + public SplunkClient splunkClient(SplunkConfig config) { + return new OneShotSearchSplunkClient(config); + } + } diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/OneShotSearchSplunkClient.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/OneShotSearchSplunkClient.java new file mode 100644 index 00000000..53207392 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/OneShotSearchSplunkClient.java @@ -0,0 +1,139 @@ +package org.jbehavesupport.core.internal.splunk; + +import com.splunk.Args; +import com.splunk.ResultsReader; +import com.splunk.Service; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.jbehavesupport.core.splunk.SplunkSearchResultEntry; +import org.jbehavesupport.core.splunk.SplunkClient; +import org.jbehavesupport.core.splunk.SplunkConfig; + +import java.io.IOException; +import java.io.InputStream; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; +import static org.jbehavesupport.core.internal.splunk.SplunkArgNames.EARLIEST_TIME; +import static org.jbehavesupport.core.internal.splunk.SplunkArgNames.LATEST_TIME; +import static org.jbehavesupport.core.internal.splunk.SplunkArgNames.OUTPUT_MODE; +import static org.jbehavesupport.core.internal.splunk.SplunkOutputModes.JSON; + + +/** + * This a class wraps Splunk Java SDK and its "one shot search" API which allows our Splunk steps to communicate with Splunk Search Heads. + *

+ * Prior to your testing, verify that you have a valid Splunk account and you know hostname and port. + *

+ * You can use username/password or auth token if you generated one. All the configuration elements should go to your application yaml file. + * + *

+ * Example: + * splunk: + * host: + * port: + * scheme: https + * credentials: + * username: admin + * password: password + * or + * token: Bearer + */ + +@RequiredArgsConstructor +@Slf4j +public class OneShotSearchSplunkClient implements SplunkClient { + + @NonNull + private final SplunkConfig config; + private Service service; + + /** + * {@inheritDoc} + */ + @Override + public List search(String query, String earliestTime, String latestTime, SplunkOutputModes splunkOutputMode) { + validateTimes(earliestTime, latestTime); + try { + return processSearchResult(getSplunkService().oneshotSearch( + query, + toArgs( + Arrays.asList( + Pair.of(EARLIEST_TIME, earliestTime), + Pair.of(LATEST_TIME, latestTime), + Pair.of(OUTPUT_MODE, splunkOutputMode.getModeName()) + ) + ) + ), splunkOutputMode); + } catch (IOException e) { + throw new IllegalArgumentException("Splunk search failed", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List search(String query) { + return search(query, null, null); + } + + /** + * {@inheritDoc} + */ + @Override + public List search(String query, String earliestTime, String latestTime) { + return search(query, earliestTime, latestTime, JSON); + } + + private void validateTimes(String earliestTimeStr, String latestTimeStr) { + ZonedDateTime earliestTime = nonNull(earliestTimeStr) ? ZonedDateTime.parse(earliestTimeStr) : null; + ZonedDateTime latestTime = nonNull(latestTimeStr) ? ZonedDateTime.parse(latestTimeStr) : null; + + if ((nonNull(earliestTime) && nonNull(latestTime)) && latestTime.isBefore(earliestTime)) { + throw new IllegalArgumentException("Latest time cannot be before earliest time"); + } + } + + private Service getSplunkService() { + if (isNull(service)) { + if (nonNull(config.getUsername())) { + // username/password based service + service = Service.connect(config.toServiceArguments()); + } else { + // auth token based service + service = new Service(config.toServiceArguments()); + } + } + return service; + } + + private List processSearchResult(InputStream searchResultStream, SplunkOutputModes splunkOutputMode) throws IOException { + ResultsReader resultReader = null; + try { + resultReader = splunkOutputMode.createReaderFrom(searchResultStream); + return StreamSupport.stream(resultReader.spliterator(), false) + .map(SplunkSearchResultEntry::new) + .collect(Collectors.toList()); + } finally { + if (!isNull(resultReader)) { + resultReader.close(); + } + } + } + + private Args toArgs(List> argList) { + return Args.create( + argList.stream() + .filter(entry -> !isNull(entry.getValue())) + .collect(Collectors.toMap(pair -> pair.getKey().getArgName(), Pair::getValue)) + ); + } +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkArgNames.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkArgNames.java new file mode 100644 index 00000000..5111be93 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkArgNames.java @@ -0,0 +1,14 @@ +package org.jbehavesupport.core.internal.splunk; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum SplunkArgNames { + EARLIEST_TIME("earliest_time"), + LATEST_TIME("latest_time"), + OUTPUT_MODE("output_mode"); + + private String argName; +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkEventFields.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkEventFields.java new file mode 100644 index 00000000..9ee0aa76 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkEventFields.java @@ -0,0 +1,15 @@ +package org.jbehavesupport.core.internal.splunk; + +import lombok.Getter; +import lombok.experimental.UtilityClass; + +@UtilityClass +@Getter +public class SplunkEventFields { + public final String TIME = "_time"; + public final String LEVEL = "level"; + public final String MESSAGE = "message"; + public final String NAMESPACE = "namespace"; + public final String INDEX = "index"; + public final String HOST = "host"; +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkOutputModes.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkOutputModes.java new file mode 100644 index 00000000..e5f365e5 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/internal/splunk/SplunkOutputModes.java @@ -0,0 +1,34 @@ +package org.jbehavesupport.core.internal.splunk; + +import com.splunk.ResultsReader; +import com.splunk.ResultsReaderCsv; +import com.splunk.ResultsReaderJson; +import com.splunk.ResultsReaderXml; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.IOException; +import java.io.InputStream; + +@AllArgsConstructor +@Getter +public enum SplunkOutputModes { + XML("xml") { + public ResultsReader createReaderFrom(InputStream searchResultStream) throws IOException { + return new ResultsReaderXml(searchResultStream); + } + }, + JSON("json") { + public ResultsReader createReaderFrom(InputStream searchResultStream) throws IOException { + return new ResultsReaderJson(searchResultStream); + } + }, + CSV("csv") { + public ResultsReader createReaderFrom(InputStream searchResultStream) throws IOException { + return new ResultsReaderCsv(searchResultStream); + } + }; + + private String modeName; + public abstract ResultsReader createReaderFrom(InputStream searchResultStream) throws IOException; +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkClient.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkClient.java new file mode 100644 index 00000000..bec9f855 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkClient.java @@ -0,0 +1,59 @@ +package org.jbehavesupport.core.splunk; + +import org.jbehavesupport.core.internal.splunk.SplunkOutputModes; + +import java.util.List; + + +/** + * This an interface exposing search entry points for Splunk Java SDK. + *

+ * Prior to your testing, verify that you have a valid Splunk account and you know hostname and port. + *

+ * You can use username/password or auth token if you generated one. All the configuration elements should go to your application yaml file. + * + *

+ * Example: + * splunk: + * host: + * port: + * scheme: https + * credentials: + * username: admin + * password: password + * or + * token: Bearer + */ + + +public interface SplunkClient { + + /** + * This method invokes Splunk search. + * + * @param query Splunk search query + * @param earliestTime earliest date of the search frame + * @param latestTime latest date of the search frame + * @param splunkOutputMode Splunk output mode (e.g. "xml", "json", "csv") + * @return Splunk resultset matching the search query + */ + List search(String query, String earliestTime, String latestTime, SplunkOutputModes splunkOutputMode); + + /** + * This method invokes Splunk search. + * + * @param query Splunk search query + * @param earliestTime earliest date of the search frame + * @param latestTime latest date of the search frame + * @return Splunk resultset matching the search query + */ + List search(String query, String earliestTime, String latestTime); + + /** + * This method invokes Splunk search. + * + * @param query Splunk search query + * @return Splunk resultset matching the search query + */ + List search(String query); +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkConfig.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkConfig.java new file mode 100644 index 00000000..c35214e7 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkConfig.java @@ -0,0 +1,47 @@ +package org.jbehavesupport.core.splunk; + +import com.splunk.SSLSecurityProtocol; +import com.splunk.ServiceArgs; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Data +@Builder +@AllArgsConstructor +@Component +public class SplunkConfig { + public static final String HTTPS_SCHEME = "https"; + + @NonNull + private String host; + @NonNull + private int port; + @NonNull + private String scheme; + private String username; + private String password; + private String token; + private SSLSecurityProtocol sslSecurityProtocol; + + public ServiceArgs toServiceArguments() { + ServiceArgs args = new ServiceArgs(); + args.setUsername(username); + args.setPassword(password); + args.setToken(token); + args.setHost(host); + args.setPort(port); + args.setScheme(scheme); + if (HTTPS_SCHEME.equals(scheme)) { + if (Objects.isNull(sslSecurityProtocol)) { + throw new IllegalArgumentException("Configuration option 'splunk.sslSecurityProtocol' is required when 'splunk.schema' is 'https'"); + } + args.setSSLSecurityProtocol(sslSecurityProtocol); + } + return args; + } +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSearchResultEntry.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSearchResultEntry.java new file mode 100644 index 00000000..de777bf1 --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSearchResultEntry.java @@ -0,0 +1,28 @@ +package org.jbehavesupport.core.splunk; + +import com.splunk.Event; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jbehavesupport.core.internal.splunk.SplunkEventFields; + +@Getter +@EqualsAndHashCode +@ToString +public class SplunkSearchResultEntry { + private String time; + private String level; + private String message; + private String namespace; + private String index; + private String host; + + public SplunkSearchResultEntry(Event event) { + this.time = event.get(SplunkEventFields.TIME); + this.level = event.get(SplunkEventFields.LEVEL); + this.message = event.get(SplunkEventFields.MESSAGE); + this.namespace = event.get(SplunkEventFields.NAMESPACE); + this.index = event.get(SplunkEventFields.INDEX); + this.host = event.get(SplunkEventFields.HOST); + } +} diff --git a/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSteps.java b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSteps.java new file mode 100644 index 00000000..a44e07af --- /dev/null +++ b/jbehave-support-core/src/main/java/org/jbehavesupport/core/splunk/SplunkSteps.java @@ -0,0 +1,91 @@ +package org.jbehavesupport.core.splunk; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.annotations.When; +import org.jbehave.core.model.ExamplesTable; +import org.jbehavesupport.core.TestContext; +import org.jbehavesupport.core.expression.ExpressionEvaluatingParameter; +import org.jbehavesupport.core.internal.parameterconverters.ExamplesEvaluationTableConverter; +import org.jbehavesupport.core.verification.VerifierResolver; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jbehavesupport.core.internal.ExampleTableConstraints.DATA; +import static org.jbehavesupport.core.internal.ExampleTableConstraints.VERIFIER; +import static org.jbehavesupport.core.internal.ExamplesTableUtil.convertTable; +import static org.springframework.util.Assert.isTrue; +import static org.springframework.util.Assert.notNull; + +@Slf4j +@Component +@RequiredArgsConstructor +public final class SplunkSteps { + private static final String SPLUNK_RESULT_KEY = "splunk_search_result"; + + private final SplunkClient splunkClient; + private final TestContext testContext; + private final ExamplesEvaluationTableConverter examplesTableFactory; + + @NonNull + private VerifierResolver verifierResolver; + + @Given("the Splunk search query is performed:$splunkQuery") + @When("the Splunk search query is performed:$splunkQuery") + public void executeSplunkSearchQuery(ExpressionEvaluatingParameter splunkQueryExpression) { + testContext.put(SPLUNK_RESULT_KEY, splunkClient.search(splunkQueryExpression.getValue())); + } + + @Given("the Splunk search query is performed within [$earliestTime] and [$latestTime]:$splunkQuery") + @When("the Splunk search query is performed within [$earliestTime] and [$latestTime]:$splunkQuery") + public void executeSplunkSearchQuery(ExpressionEvaluatingParameter earliestTimeExpression, ExpressionEvaluatingParameter latestTimeExpression, ExpressionEvaluatingParameter splunkQueryExpression) { + testContext.put(SPLUNK_RESULT_KEY, splunkClient.search(splunkQueryExpression.getValue(), earliestTimeExpression.getValue(), latestTimeExpression.getValue())); + } + + @Then("the Splunk search result set has $rowCount row(s)") + public void compareSplunkSearchResultSetRowCount(ExpressionEvaluatingParameter rowCount) { + assertThat(getSplunkSearchResult()) + .as("returned Splunk search result set row count does not match expected count") + .hasSize(rowCount.getValue()); + } + + @Then("the Splunk search result match these rules:$rulesData") + public void matchSplunkSearchResultAgainstRules(String verifiersData) { + compareExampleTableWithListOfSplunkSearchResultEntries(examplesTableFactory.convertValue(verifiersData, null)); + } + + private void compareExampleTableWithListOfSplunkSearchResultEntries(ExamplesTable verifiersData) { + List splunkSearchData = getSplunkSearchResult(); + notNull(verifiersData, "verifier meta data can't be null"); + notNull(splunkSearchData, "search result set data can't be null"); + isTrue(verifiersData.getHeaders().size() == 2 && verifiersData.getHeaders().contains(VERIFIER), "searchData must have one search data column and a verifier)"); + + assertThat(splunkSearchData) + .as("no Splunk search query match") + .isNotEmpty(); + verifySearchResultEntries(verifiersData, splunkSearchData); + } + + private void verifySearchResultEntries(ExamplesTable verifiersData, List splunkSearchData) { + convertTable(verifiersData).forEach(row -> + assertThat(splunkSearchData) + .as("all found Splunk search result data meet all verifier definitions") + .allSatisfy( + splunkSearchResultEntry -> { + String verifierName = row.get(VERIFIER); + String verifierData = row.get(DATA); + verifierResolver.getVerifierByName(verifierName).verify(splunkSearchResultEntry.getMessage(), verifierData); + } + ) + ); + } + + private List getSplunkSearchResult() { + return testContext.get(SPLUNK_RESULT_KEY); + } +} diff --git a/jbehave-support-core/src/test/groovy/org/jbehavesupport/core/TestConfig.groovy b/jbehave-support-core/src/test/groovy/org/jbehavesupport/core/TestConfig.groovy index 85d797c1..a3ee7abb 100644 --- a/jbehave-support-core/src/test/groovy/org/jbehavesupport/core/TestConfig.groovy +++ b/jbehave-support-core/src/test/groovy/org/jbehavesupport/core/TestConfig.groovy @@ -1,5 +1,6 @@ package org.jbehavesupport.core +import com.splunk.SSLSecurityProtocol import org.jbehave.core.configuration.MostUsefulConfiguration import org.jbehavesupport.core.healthcheck.HealthCheck import org.jbehavesupport.core.healthcheck.HealthCheckSteps @@ -11,6 +12,7 @@ import org.jbehavesupport.core.internal.web.WebScreenshotCreator import org.jbehavesupport.core.internal.web.by.XpathByFactory import org.jbehavesupport.core.rest.RestServiceHandler import org.jbehavesupport.core.internal.web.webdriver.WebDriverDelegatingInterceptor +import org.jbehavesupport.core.splunk.SplunkConfig import org.jbehavesupport.core.ssh.RollingLogResolver import org.jbehavesupport.core.ssh.SimpleRollingLogResolver import org.jbehavesupport.core.ssh.SshLog @@ -36,6 +38,7 @@ import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration import org.springframework.core.env.Environment +import static java.util.Objects.nonNull import static org.mockito.Mockito.when import java.time.ZonedDateTime @@ -165,4 +168,17 @@ class TestConfig { RestServiceHandler testRestServiceHandler() { return new RestServiceHandler(env.getProperty("rest.url")) } + + @Bean + SplunkConfig splunkConfig() throws IOException { + return SplunkConfig.builder() + .host(env.getProperty("splunk.host")) + .port(Integer.parseInt(env.getProperty("splunk.port"))) + .scheme(env.getProperty("splunk.scheme")) + .sslSecurityProtocol(nonNull(env.getProperty("splunk.sslSecurityProtocol")) ? SSLSecurityProtocol.valueOf(env.getProperty("splunk.sslSecurityProtocol")) : null) + .username(env.getProperty("splunk.credentials.username")) + .password(env.getProperty("splunk.credentials.password")) + .token(env.getProperty("splunk.credentials.token")) + .build(); + } } diff --git a/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/SampleStoriesIT.java b/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/SampleStoriesIT.java index 2c050e0f..fff1a7f7 100644 --- a/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/SampleStoriesIT.java +++ b/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/SampleStoriesIT.java @@ -27,7 +27,8 @@ protected List storyPaths() { "org/jbehavesupport/test/sample/Ssh.story", "org/jbehavesupport/test/sample/HealthCheck.story", "org/jbehavesupport/test/sample/Jms.story", - "org/jbehavesupport/test/sample/Verification.story" + "org/jbehavesupport/test/sample/Verification.story", + "org/jbehavesupport/test/sample/Splunk.story" ); } } diff --git a/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/Splunk.story b/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/Splunk.story new file mode 100644 index 00000000..3b15b4ec --- /dev/null +++ b/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/sample/Splunk.story @@ -0,0 +1,28 @@ +Scenario: Test data are present in Splunk search result +Given the value [30a3d60bd3d698eae25eaf5afe3e1df5] is saved as [TRACEID_VARIABLE] +When the Splunk search query is performed: +search index="main" namespace="*" level="*" message="*response*" traceId="{CP:TRACEID_VARIABLE}" | tail 1 +Then the Splunk search result set has 1 row(s) +Then the Splunk search result match these rules: +| data | verifier | +| ^.*200 OK with headers.*$ | REGEX_MATCH | +| X-B3-TraceId:"30a3d60bd3d698eae25eaf5afe3e1df5" | CONTAINS | +| does not contain | NOT_CONTAINS | +| | NOT_NULL | +| no equality | NE | + +Scenario: Test data are present in Splunk search result within given time range +Given the value [30a3d60bd3d698eae25eaf5afe3e1df5] is saved as [TRACEID_VARIABLE] +And the value [2020-07-10T00:00:00.000+02:00] is saved as [EARLIEST_TIME_VARIABLE] +And the value [2020-07-20T23:59:59.000+02:00] is saved as [LATEST_TIME_VARIABLE] +When the Splunk search query is performed within [{CP:EARLIEST_TIME_VARIABLE}] and [{CP:LATEST_TIME_VARIABLE}]: +search index="main" namespace="*" level="*" message="*response*" traceId="{CP:TRACEID_VARIABLE}" | tail 1 +Then the Splunk search result set has 1 row(s) +Then the Splunk search result match these rules: +| data | verifier | +| ^.*200 OK with headers.*$ | REGEX_MATCH | + +Scenario: Test data are not present in Splunk search result +Given the Splunk search query is performed: +search index="main" namespace="no data" message="*" +Then the Splunk search result set has 0 row(s) \ No newline at end of file diff --git a/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/support/TestConfig.java b/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/support/TestConfig.java index 2cf1769a..82ccd927 100644 --- a/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/support/TestConfig.java +++ b/jbehave-support-core/src/test/groovy/org/jbehavesupport/test/support/TestConfig.java @@ -1,20 +1,6 @@ package org.jbehavesupport.test.support; -import static java.lang.Integer.parseInt; -import static java.util.Collections.singletonMap; -import static org.jbehavesupport.core.ssh.SshSetting.builder; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.ZonedDateTime; - -import javax.jms.ConnectionFactory; -import javax.sql.DataSource; - +import com.splunk.SSLSecurityProtocol; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.activemq.ActiveMQConnectionFactory; @@ -37,6 +23,7 @@ import org.jbehavesupport.core.report.extension.WsXmlReporterExtension; import org.jbehavesupport.core.rest.RestServiceHandler; import org.jbehavesupport.core.rest.RestTemplateConfigurer; +import org.jbehavesupport.core.splunk.SplunkConfig; import org.jbehavesupport.core.ssh.RollingLogResolver; import org.jbehavesupport.core.ssh.SimpleRollingLogResolver; import org.jbehavesupport.core.ssh.SshHandler; @@ -61,6 +48,21 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.jms.core.JmsTemplate; +import javax.jms.ConnectionFactory; +import javax.sql.DataSource; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.ZonedDateTime; + +import static java.lang.Integer.parseInt; +import static java.util.Collections.singletonMap; +import static java.util.Objects.nonNull; +import static org.jbehavesupport.core.ssh.SshSetting.builder; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + @Configuration @ComponentScan @RequiredArgsConstructor @@ -132,7 +134,7 @@ public TestContextXmlReporterExtension testContextXmlReporterExtension(TestConte } @Bean - public ServerLogXmlReporterExtension serverLogXmlReporterExtension(ConfigurableListableBeanFactory beanFactory, SshHandler sshHandler,TestContext testContext, FileNameResolver fileNameResolver) { + public ServerLogXmlReporterExtension serverLogXmlReporterExtension(ConfigurableListableBeanFactory beanFactory, SshHandler sshHandler, TestContext testContext, FileNameResolver fileNameResolver) { return new ServerLogXmlReporterExtension(testContext, fileNameResolver, sshHandler, beanFactory); } @@ -331,4 +333,17 @@ private RemoteWebDriver getBrowserStackWebDriver(DesiredCapabilities capabilitie driver.manage().window().maximize(); return driver; } + + @Bean + SplunkConfig splunkConfig() throws IOException { + return SplunkConfig.builder() + .host(env.getProperty("splunk.host")) + .port(Integer.parseInt(env.getProperty("splunk.port"))) + .scheme(env.getProperty("splunk.scheme")) + .sslSecurityProtocol(nonNull(env.getProperty("splunk.sslSecurityProtocol")) ? SSLSecurityProtocol.valueOf(env.getProperty("splunk.sslSecurityProtocol")) : null) + .username(env.getProperty("splunk.credentials.username")) + .password(env.getProperty("splunk.credentials.password")) + .token(env.getProperty("splunk.credentials.token")) + .build(); + } } diff --git a/jbehave-support-core/src/test/resources/test.yml b/jbehave-support-core/src/test/resources/test.yml index fd06b1f0..d3ff2826 100644 --- a/jbehave-support-core/src/test/resources/test.yml +++ b/jbehave-support-core/src/test/resources/test.yml @@ -2,7 +2,7 @@ environmentInfo: name: jbehave-support-test-environment ws: - url: http://localhost:11110/services/ + url: http://localhost:11110/ws/ rest: url: http://localhost:11110/rest/ @@ -35,4 +35,12 @@ jms: browser-stack: username: browser-stack-username key: browser-stack-key - url: https://${browser-stack.username}:${browser-stack.key}@hub-cloud.browserstack.com/wd/hub \ No newline at end of file + url: https://${browser-stack.username}:${browser-stack.key}@hub-cloud.browserstack.com/wd/hub + +splunk: + host: localhost + port: 11110 + scheme: http + credentials: + username: admin + password: password \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9179ac62..4e1ae77b 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,7 @@ 1.4.1 3.7.0.1746 3.3.2 + 1.6.5.0 @@ -204,6 +205,14 @@ + + + splunk-artifactory + Splunk Releases + https://splunk.jfrog.io/splunk/ext-releases-local + + + @@ -455,6 +464,11 @@ browserstack-local-java ${version.browserstack-local-java} + + com.splunk + splunk + ${version.splunk} +