Repository: aurora Updated Branches: refs/heads/master d64e179ed -> 2e7b317be
Add webhook code that can POST events to an endpoint Bugs closed: AURORA-1683 Reviewed at https://reviews.apache.org/r/47440/ Project: http://git-wip-us.apache.org/repos/asf/aurora/repo Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/33295fa7 Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/33295fa7 Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/33295fa7 Branch: refs/heads/master Commit: 33295fa795cb3b8b39ea52f96fe76a1351283aa2 Parents: d64e179 Author: Dmitriy Shirchenko <[email protected]> Authored: Tue Jun 7 00:41:47 2016 +0200 Committer: Stephan Erb <[email protected]> Committed: Tue Jun 7 00:41:47 2016 +0200 ---------------------------------------------------------------------- RELEASE-NOTES.md | 1 + docs/README.md | 1 + docs/features/webhooks.md | 79 +++++++++++++ docs/reference/scheduler-configuration.md | 2 + .../aurora/scheduler/app/SchedulerMain.java | 5 +- .../aurora/scheduler/events/PubsubEvent.java | 6 + .../apache/aurora/scheduler/events/Webhook.java | 110 +++++++++++++++++++ .../aurora/scheduler/events/WebhookInfo.java | 82 ++++++++++++++ .../aurora/scheduler/events/WebhookModule.java | 98 +++++++++++++++++ .../org/apache/aurora/scheduler/webhook.json | 8 ++ .../aurora/scheduler/events/WebhookTest.java | 80 ++++++++++++++ 11 files changed, 471 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/RELEASE-NOTES.md ---------------------------------------------------------------------- diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 7d47cf6..65f4191 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,7 @@ `-job_update_history_pruning_threshold=1mins` and `-job_update_history_per_job_threshold=0` 5. Ensure a new snapshot is created by running `aurora_admin scheduler_snapshot <cluster>` 6. Rollback to previous version +- Adding a webhook feature which POSTs all task state changes to a user defined endpoint. ### Deprecations and removals: http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/docs/README.md ---------------------------------------------------------------------- diff --git a/docs/README.md b/docs/README.md index aea8518..7cea18f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,7 @@ Description of important Aurora features. * [Services](features/services.md) * [Service Discovery](features/service-discovery.md) * [SLA Metrics](features/sla-metrics.md) + * [Webhooks](features/webhooks.md) ## Operators For those that wish to manage and fine-tune an Aurora cluster. http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/docs/features/webhooks.md ---------------------------------------------------------------------- diff --git a/docs/features/webhooks.md b/docs/features/webhooks.md new file mode 100644 index 0000000..2ca342f --- /dev/null +++ b/docs/features/webhooks.md @@ -0,0 +1,79 @@ +Webhooks +================== + +Aurora has an optional feature which allows operator to specify a file to configure a HTTP webhook to receive task state + change events. It can be enabled with a scheduler flag eg + -webhook_config=/path/to/webhook.json + + Below is a sample configuration: + + ```json +{ + "headers": { + "Content-Type": "application/vnd.kafka.json.v1+json", + "Producer-Type": "reliable" + }, + "targetURL": "http://localhost:5000/", + "timeoutMsec": 5 +} +``` + +And an example of a response that you will get back: +```json +{ + "task": + { + "cachedHashCode":0, + "assignedTask": { + "cachedHashCode":0, + "taskId":"vagrant-test-http_example-8-a6cf7ec5-d793-49c7-b10f-0e14ab80bfff", + "task": { + "cachedHashCode":-1819348376, + "job": { + "cachedHashCode":803049425, + "role":"vagrant", + "environment":"test", + "name":"http_example" + }, + "owner": { + "cachedHashCode":226895216, + "user":"vagrant" + }, + "isService":true, + "numCpus":0.1, + "ramMb":16, + "diskMb":8, + "priority":0, + "maxTaskFailures":1, + "production":false, + "resources":[ + {"cachedHashCode":729800451,"setField":"NUM_CPUS","value":0.1}, + {"cachedHashCode":552899914,"setField":"RAM_MB","value":16}, + {"cachedHashCode":-1547868317,"setField":"DISK_MB","value":8}, + {"cachedHashCode":1957328227,"setField":"NAMED_PORT","value":"http"}, + {"cachedHashCode":1954229436,"setField":"NAMED_PORT","value":"tcp"} + ], + "constraints":[], + "requestedPorts":["http","tcp"], + "taskLinks":{"http":"http://%host%:%port:http%"}, + "contactEmail":"vagrant@localhost", + "executorConfig": { + "cachedHashCode":-1194797325, + "name":"AuroraExecutor", + "data": "{\"environment\": \"test\", \"health_check_config\": {\"initial_interval_secs\": 5.0, \"health_checker\": { \"http\": {\"expected_response_code\": 0, \"endpoint\": \"/health\", \"expected_response\": \"ok\"}}, \"max_consecutive_failures\": 0, \"timeout_secs\": 1.0, \"interval_secs\": 1.0}, \"name\": \"http_example\", \"service\": true, \"max_task_failures\": 1, \"cron_collision_policy\": \"KILL_EXISTING\", \"enable_hooks\": false, \"cluster\": \"devcluster\", \"task\": {\"processes\": [{\"daemon\": false, \"name\": \"echo_ports\", \"ephemeral\": false, \"max_failures\": 1, \"min_duration\": 5, \"cmdline\": \"echo \\\"tcp port: {{thermos.ports[tcp]}}; http port: {{thermos.ports[http]}}; alias: {{thermos.ports[alias]}}\\\"\", \"final\": false}, {\"daemon\": false, \"name\": \"stage_server\", \"ephemeral\": false, \"max_failures\": 1, \"min_duration\": 5, \"cmdline\": \"cp /vagrant/src/test/sh/org/apache/aurora/e2e/http_example.py .\", \"final\": false}, {\ "daemon\": false, \"name\": \"run_server\", \"ephemeral\": false, \"max_failures\": 1, \"min_duration\": 5, \"cmdline\": \"python http_example.py {{thermos.ports[http]}}\", \"final\": false}], \"name\": \"http_example\", \"finalization_wait\": 30, \"max_failures\": 1, \"max_concurrency\": 0, \"resources\": {\"disk\": 8388608, \"ram\": 16777216, \"cpu\": 0.1}, \"constraints\": [{\"order\": [\"echo_ports\", \"stage_server\", \"run_server\"]}]}, \"production\": false, \"role\": \"vagrant\", \"contact\": \"vagrant@localhost\", \"announce\": {\"primary_port\": \"http\", \"portmap\": {\"alias\": \"http\"}}, \"lifecycle\": {\"http\": {\"graceful_shutdown_endpoint\": \"/quitquitquit\", \"port\": \"health\", \"shutdown_endpoint\": \"/abortabortabort\"}}, \"priority\": 0}"}, + "metadata":[], + "container":{ + "cachedHashCode":-1955376216, + "setField":"MESOS", + "value":{"cachedHashCode":31}} + }, + "assignedPorts":{}, + "instanceId":8 + }, + "status":"PENDING", + "failureCount":0, + "taskEvents":[ + {"cachedHashCode":0,"timestamp":1464992060258,"status":"PENDING","scheduler":"aurora"}] + }, + "oldState":{}} +``` \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/docs/reference/scheduler-configuration.md ---------------------------------------------------------------------- diff --git a/docs/reference/scheduler-configuration.md b/docs/reference/scheduler-configuration.md index f7d676d..c0c3944 100644 --- a/docs/reference/scheduler-configuration.md +++ b/docs/reference/scheduler-configuration.md @@ -218,6 +218,8 @@ Optional flags: Whether to use the experimental database-backed task store. -viz_job_url_prefix (default ) URL prefix for job container stats. +-webhook_config [file must be readable] + File to configure a HTTP webhook to receive task state change events. -zk_chroot_path chroot path to use for the ZooKeeper connections -zk_digest_credentials http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java b/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java index 9ebfe23..8fe2b79 100644 --- a/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java +++ b/src/main/java/org/apache/aurora/scheduler/app/SchedulerMain.java @@ -52,6 +52,7 @@ import org.apache.aurora.scheduler.configuration.executor.ExecutorModule; import org.apache.aurora.scheduler.cron.quartz.CronModule; import org.apache.aurora.scheduler.discovery.FlaggedZooKeeperConfig; import org.apache.aurora.scheduler.discovery.ServiceDiscoveryModule; +import org.apache.aurora.scheduler.events.WebhookModule; import org.apache.aurora.scheduler.http.HttpService; import org.apache.aurora.scheduler.log.mesos.MesosLogStreamModule; import org.apache.aurora.scheduler.mesos.CommandLineDriverSettingsModule; @@ -198,7 +199,9 @@ public class SchedulerMain { new LibMesosLoadingModule(), new MesosLogStreamModule(FlaggedZooKeeperConfig.create()), new LogStorageModule(), - new TierModule()) + new TierModule(), + new WebhookModule() + ) .build(); flagConfiguredMain(Modules.combine(modules)); } http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/main/java/org/apache/aurora/scheduler/events/PubsubEvent.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/events/PubsubEvent.java b/src/main/java/org/apache/aurora/scheduler/events/PubsubEvent.java index 2a4c066..70b5470 100644 --- a/src/main/java/org/apache/aurora/scheduler/events/PubsubEvent.java +++ b/src/main/java/org/apache/aurora/scheduler/events/PubsubEvent.java @@ -18,6 +18,7 @@ import java.util.Set; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; +import com.google.gson.Gson; import org.apache.aurora.gen.ScheduleStatus; import org.apache.aurora.scheduler.base.TaskGroupKey; @@ -157,6 +158,11 @@ public interface PubsubEvent { .add("newState", getNewState()) .toString(); } + + public String toJson() { + return new Gson().toJson(this); + } + } /** http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/main/java/org/apache/aurora/scheduler/events/Webhook.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/events/Webhook.java b/src/main/java/org/apache/aurora/scheduler/events/Webhook.java new file mode 100644 index 0000000..e54aa19 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/events/Webhook.java @@ -0,0 +1,110 @@ +/** + * 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 org.apache.aurora.scheduler.events; + +import java.io.DataOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.Instant; + +import com.google.common.eventbus.Subscribe; + +import com.google.inject.Inject; + +import org.apache.aurora.scheduler.events.PubsubEvent.EventSubscriber; +import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Watches TaskStateChanges and send events to configured endpoint. + */ +public class Webhook implements EventSubscriber { + + private static final Logger LOG = LoggerFactory.getLogger(Webhook.class); + + private static final String CALL_METHOD = "POST"; + private final WebhookInfo webhookInfo; + + @Inject + Webhook(WebhookInfo webhookInfo) { + this.webhookInfo = webhookInfo; + LOG.debug("Webhook enabled with info" + this.webhookInfo); + } + + private HttpURLConnection initializeConnection() { + try { + final HttpURLConnection connection = (HttpURLConnection) new URL( + this.webhookInfo.getTargetURL()).openConnection(); + connection.setRequestMethod(CALL_METHOD); + connection.setConnectTimeout(this.webhookInfo.getConnectonTimeout()); + + webhookInfo.getHeaders().entrySet().forEach( + e -> connection.setRequestProperty(e.getKey(), e.getValue())); + connection.setRequestProperty("TimeStamp", Long.toString(Instant.now().toEpochMilli())); + connection.setDoOutput(true); + return connection; + } catch (Exception e) { + // Do nothing since we are just doing best-effort here. + LOG.error("Exception trying to initialize a connection:", e); + return null; + } + } + + /** + * Calls a specified endpoint with the provided string representing an internal event. + * + * @param eventJson String represenation of task state change. + */ + public void callEndpoint(String eventJson) { + HttpURLConnection connection = this.initializeConnection(); + if (connection == null) { + LOG.error("Received a null object when trying to initialize an HTTP connection"); + } else { + try { + try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { + wr.writeBytes(eventJson); + LOG.debug("Sending message " + eventJson + + " with connection info " + connection.toString() + + " with WebhookInfo " + this.webhookInfo.toString()); + } catch (Exception e) { + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + errorStream.close(); + } + LOG.error("Exception writing via HTTP connection", e); + } + // Don't care about reading input so just performing basic close() operation. + connection.getInputStream().close(); + } catch (Exception e) { + LOG.error("Exception when sending a task change event", e); + } + } + LOG.debug("Done with Webhook call"); + } + + /** + * Watches all TaskStateChanges and send them best effort to a configured endpoint. + * <p> + * This is used to expose an external event bus. + * + * @param stateChange State change notification. + */ + @Subscribe + public void taskChangedState(TaskStateChange stateChange) { + String eventJson = stateChange.toJson(); + callEndpoint(eventJson); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/main/java/org/apache/aurora/scheduler/events/WebhookInfo.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/events/WebhookInfo.java b/src/main/java/org/apache/aurora/scheduler/events/WebhookInfo.java new file mode 100644 index 0000000..e4193f7 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/events/WebhookInfo.java @@ -0,0 +1,82 @@ +/** + * 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 org.apache.aurora.scheduler.events; + +import java.util.Map; + +import com.google.common.base.MoreObjects; + +import com.google.common.collect.ImmutableMap; + +import org.codehaus.jackson.annotate.JsonCreator; +import org.codehaus.jackson.annotate.JsonProperty; + +import static java.util.Objects.requireNonNull; + +/** + * Defines configuration for Webhook. + */ +public class WebhookInfo { + private final Integer connectTimeout; + private final Map<String, String> headers; + private final String targetURL; + + /** + * Return key:value pairs of headers to set for every connection. + * + * @return Map + */ + public Map<String, String> getHeaders() { + return this.headers; + } + + /** + * Returns URL to post events to. + * + * @return String + */ + public String getTargetURL() { + return this.targetURL; + } + + /** + * Returns connection timeout to set when POSTing an event. + * + * @return Integer value. + */ + public Integer getConnectonTimeout() { + return this.connectTimeout; + } + + @JsonCreator + public WebhookInfo( + @JsonProperty("headers") Map<String, String> headers, + @JsonProperty("targetURL") String targetURL, + @JsonProperty("timeoutMsec") Integer timeout) { + + requireNonNull(targetURL); + this.headers = ImmutableMap.copyOf(headers); + this.targetURL = requireNonNull(targetURL); + this.connectTimeout = requireNonNull(timeout); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("headers", headers.toString()) + .add("targetURL", targetURL) + .add("connectTimeout", connectTimeout) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/main/java/org/apache/aurora/scheduler/events/WebhookModule.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/events/WebhookModule.java b/src/main/java/org/apache/aurora/scheduler/events/WebhookModule.java new file mode 100644 index 0000000..eaa7053 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/events/WebhookModule.java @@ -0,0 +1,98 @@ +/** + * 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 org.apache.aurora.scheduler.events; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.io.Files; +import com.google.common.io.Resources; +import com.google.inject.AbstractModule; + +import com.google.inject.Singleton; + +import org.apache.aurora.common.args.Arg; +import org.apache.aurora.common.args.CmdLine; +import org.apache.aurora.common.args.constraints.CanRead; +import org.apache.aurora.common.args.constraints.Exists; +import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Binding module for webhook management. + */ +public class WebhookModule extends AbstractModule { + + private static final Logger LOG = LoggerFactory.getLogger(WebhookModule.class); + + @VisibleForTesting + static final String WEBHOOK_CONFIG_PATH = "org/apache/aurora/scheduler/webhook.json"; + + @CmdLine(name = "webhook_config", help = "Path to webhook configuration file.") + @Exists + @CanRead + private static final Arg<File> WEBHOOK_CONFIG_FILE = Arg.create(); + + private final boolean enableWebhook; + + public WebhookModule() { + this(WEBHOOK_CONFIG_FILE.hasAppliedValue()); + } + + @VisibleForTesting + private WebhookModule(boolean enableWebhook) { + this.enableWebhook = enableWebhook; + } + + @Override + protected void configure() { + if (enableWebhook) { + bind(WebhookInfo.class).toInstance(parseWebhookConfig(readWebhookFile())); + PubsubEventModule.bindSubscriber(binder(), Webhook.class); + bind(Webhook.class).in(Singleton.class); + } + } + + @VisibleForTesting + static String readWebhookFile() { + try { + return WEBHOOK_CONFIG_FILE.hasAppliedValue() + ? Files.toString(WEBHOOK_CONFIG_FILE.get(), StandardCharsets.UTF_8) + : Resources.toString( + Webhook.class.getClassLoader().getResource(WEBHOOK_CONFIG_PATH), + StandardCharsets.UTF_8); + } catch (IOException e) { + LOG.error("Error loading webhook configuration file."); + throw Throwables.propagate(e); + } + } + + @VisibleForTesting + static WebhookInfo parseWebhookConfig(String config) { + checkArgument(!Strings.isNullOrEmpty(config), "Webhook configuration cannot be empty"); + try { + return new ObjectMapper().readValue(config, WebhookInfo.class); + } catch (IOException e) { + LOG.error("Error parsing Webhook configuration file."); + throw Throwables.propagate(e); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/main/resources/org/apache/aurora/scheduler/webhook.json ---------------------------------------------------------------------- diff --git a/src/main/resources/org/apache/aurora/scheduler/webhook.json b/src/main/resources/org/apache/aurora/scheduler/webhook.json new file mode 100644 index 0000000..0078798 --- /dev/null +++ b/src/main/resources/org/apache/aurora/scheduler/webhook.json @@ -0,0 +1,8 @@ +{ + "headers": { + "Content-Type": "application/vnd.kafka.json.v1+json", + "Producer-Type": "reliable" + }, + "targetURL": "http://localhost:5000/", + "timeoutMsec": 5 +} http://git-wip-us.apache.org/repos/asf/aurora/blob/33295fa7/src/test/java/org/apache/aurora/scheduler/events/WebhookTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/aurora/scheduler/events/WebhookTest.java b/src/test/java/org/apache/aurora/scheduler/events/WebhookTest.java new file mode 100644 index 0000000..488eefd --- /dev/null +++ b/src/test/java/org/apache/aurora/scheduler/events/WebhookTest.java @@ -0,0 +1,80 @@ +/** + * 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 org.apache.aurora.scheduler.events; + +import com.google.common.eventbus.EventBus; + +import org.apache.aurora.common.testing.easymock.EasyMockTest; + +import org.apache.aurora.scheduler.base.TaskTestUtil; +import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange; +import org.apache.aurora.scheduler.storage.entities.IScheduledTask; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class WebhookTest extends EasyMockTest { + private static final IScheduledTask TASK = TaskTestUtil.makeTask("id", TaskTestUtil.JOB); + private Webhook realWebhook; + private Webhook webhook; + private EventBus eventBus; + + @Before + public void setUp() { + webhook = createMock(Webhook.class); + eventBus = new EventBus(); + eventBus.register(webhook); + WebhookInfo webhookInfo = WebhookModule.parseWebhookConfig( + "{\"headers\": {\"Producer-Type\": \"reliable\"," + + " \"Content-Type\": \"application/vnd.kafka.json.v1+json\"}," + + " \"timeoutMsec\": 1," + + " \"targetURL\": \"http://localhost:5000/\"}" + ); + realWebhook = new Webhook(webhookInfo); + } + + private final TaskStateChange change = TaskStateChange.initialized(TASK); + private final String changeJson = TaskStateChange.initialized(TASK).toJson(); + + @Test + public void testTaskChangedState() { + webhook.taskChangedState(change); + + control.replay(); + + eventBus.post(change); + } + + @Test + public void testCallEndpoint() { + control.replay(); + + realWebhook.callEndpoint(changeJson); + } + + @Test + public void testWebhookInfo() { + WebhookInfo webhookInfo = WebhookModule.parseWebhookConfig(WebhookModule.readWebhookFile()); + assertEquals(webhookInfo.toString(), + "WebhookInfo{headers={" + + "Content-Type=application/vnd.kafka.json.v1+json, " + + "Producer-Type=reliable" + + "}, " + + "targetURL=http://localhost:5000/, " + + "connectTimeout=5" + + "}"); + control.replay(); + } +}
