Skip to content

Commit

Permalink
Stats reporting in docker compose rule (#319)
Browse files Browse the repository at this point in the history
* Copy over report api

* Rename to event-api

* Copy over code

* Hook up before/after/setDescription

* Delete unnecessary file

* Use static shutdown reporter

* Get something that seems to kind of post webhooks

* Remove https for now

* Parse and use reporting config file

* Add parseable version for user agent

* Remove enhanced-docker-compose-rule

* Better name for webhook poster

* Add some tests for config

* Actually get http json poster working

* Wiremock test for http code

* Attempt to create integration test

* Revert "Attempt to create integration test"

This reverts commit 1915eb2.

* Thread saftey lol

* Add generated changelog entries

* Helpful comment

* Checkstyle

* Test $VERSION behaviour

* Make fake reporter rather than fake poster

* Generalize out which env vars you want

* Tests for PatternCollection

* Revert "Revert "Attempt to create integration test""

This reverts commit f9526d0.

* Fix integ test by manually triggering shutdown

* Use empty test description

* Remove needless keystores dep

* Use optional flatMap

* Delete JsonPoster

* spotless

* Remove meaningless name

* Make test work by running out of process

* Always run runRecorder.after()

* Set connect timeout to be 1 sec

* Better conjure def

* Get rid of $VERSION

* Put report API version in report itselft

* NoOpReporter

* Grab circle envvars by default and remove config option

* Autorelease 1.1.0-rc1

* Revert "Autorelease 1.1.0-rc1"

This reverts commit 7abd657.
  • Loading branch information
CRogers authored Aug 8, 2019
1 parent d3ad2cb commit c08c286
Show file tree
Hide file tree
Showing 25 changed files with 1,155 additions and 10 deletions.
6 changes: 6 additions & 0 deletions changelog/@unreleased/pr-319.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: feature
feature:
description: Reporting stats to a http endpoint is possible by creating a `.docker-compose-rule.yml`
config file in the root of your project
links:
- https://github.com/palantir/docker-compose-rule/pull/319
4 changes: 4 additions & 0 deletions docker-compose-rule-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ dependencies {
compile 'org.apache.commons:commons-lang3:3.0'
compile 'org.hamcrest:hamcrest-core'
compile 'org.slf4j:slf4j-api'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
implementation 'com.palantir.conjure.java.runtime:conjure-java-jackson-serialization'
implementation 'one.util:streamex'

testCompile 'com.github.stefanbirkner:system-rules'
testCompile 'junit:junit'
testCompile 'org.assertj:assertj-core'
testCompile 'org.mockito:mockito-core'
testCompile 'org.slf4j:slf4j-simple'
testImplementation 'com.github.tomakehurst:wiremock'

integrationTestCompile project.sourceSets.test.output
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.docker.compose.reporting;

import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.status;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.palantir.docker.compose.DockerComposeManager;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

public class ReportingIntegrationTest {
@Rule
public final WireMockRule wireMockRule = new WireMockRule();

@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();

// Run by test
public static void main(String... args) throws IOException, InterruptedException {
String testCodeWorkingDir = args[0];

DockerComposeManager dockerComposeManager = new DockerComposeManager.Builder()
.file(Paths.get(testCodeWorkingDir, "src/test/resources/no-healthcheck.yaml")
.toAbsolutePath()
.toString())
.build();

try {
dockerComposeManager.before();
} finally {
dockerComposeManager.after();
}
}

@Test
public void a_report_of_the_right_format_is_posted() throws IOException, InterruptedException {
File config = temporaryFolder.newFile(".docker-compose-rule.yml");
Files.write(config.toPath(), ImmutableList.of(
"reporting:",
" url: http://localhost:" + wireMockRule.port() + "/some/path"
), StandardCharsets.UTF_8);

wireMockRule.stubFor(post("/some/path").willReturn(status(200)));

// We start this in a new java subprocess, as the *intentional* use of shutdown hooks and reading config files
// from the current working directory makes it very hard to run in process, since other instantiations of
// DockerComposeManger may have already set statics
Process process = new ProcessBuilder()
.command("java",
"-classpath",
System.getProperty("java.class.path"),
ReportingIntegrationTest.class.getCanonicalName(),
System.getProperty("user.dir"))
.directory(temporaryFolder.getRoot())
.start();

process.waitFor(30, TimeUnit.SECONDS);

String stdout = streamToString(process.getInputStream());
System.out.println("stdout: " + stdout);
System.out.println("stderr: " + streamToString(process.getErrorStream()));

wireMockRule.verify(postRequestedFor(urlPathEqualTo("/some/path")));
}

private String streamToString(InputStream inputStream) throws IOException {
return CharStreams.toString(new InputStreamReader(inputStream, UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@

@Target({ElementType.PACKAGE, ElementType.TYPE})
@Style(depluralize = true, strictBuilder = true, overshadowImplementation = true)
@interface CustomImmutablesStyle {}
public @interface CustomImmutablesStyle {}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import com.palantir.docker.compose.logging.FileLogCollector;
import com.palantir.docker.compose.logging.LogCollector;
import com.palantir.docker.compose.logging.LogDirectory;
import com.palantir.docker.compose.report.TestDescription;
import com.palantir.docker.compose.reporting.RunRecorder;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
Expand All @@ -73,6 +75,8 @@ public abstract class DockerComposeManager {
public static final Duration DEFAULT_TIMEOUT = Duration.standardMinutes(2);
public static final int DEFAULT_RETRY_ATTEMPTS = 2;

private final RunRecorder runRecorder = RunRecorder.defaults();

public DockerPort hostNetworkedPort(int port) {
return new DockerPort(machine().getIp(), port, port);
}
Expand Down Expand Up @@ -161,12 +165,22 @@ protected LogCollector logCollector() {

@Value.Derived
protected EventEmitter emitEventsFor() {
return new EventEmitter(eventConsumers());
List<EventConsumer> eventConsumers =
Stream.concat(Stream.of(runRecorder), eventConsumers().stream())
.collect(Collectors.toList());

return new EventEmitter(eventConsumers);
}

protected void setDescription(TestDescription testDescription) {
runRecorder.setDescription(testDescription);
}

public void before() throws IOException, InterruptedException {
log.debug("Starting docker-compose cluster");

runRecorder.before(() -> dockerCompose().config());

pullBuildAndUp();

emitEventsFor().waitingForServices(this::waitForServices);
Expand Down Expand Up @@ -251,7 +265,10 @@ public void after() {
shutdownStrategy().shutdown(this.dockerCompose(), this.docker()));
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Error cleaning up docker compose cluster", e);
} finally {
runRecorder.after();
}

}

public String exec(DockerComposeExecOption options, String containerName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.docker.compose.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.google.common.annotations.VisibleForTesting;
import com.palantir.docker.compose.CustomImmutablesStyle;
import com.palantir.docker.compose.reporting.ReportingConfig;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import one.util.streamex.StreamEx;
import org.immutables.value.Value;

@Value.Immutable
@CustomImmutablesStyle
@JsonDeserialize(as = ImmutableDockerComposeRuleConfig.class)
public abstract class DockerComposeRuleConfig {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory())
.registerModule(new Jdk8Module())
.registerModule(new GuavaModule());
public static final String CONFIG_FILENAME = ".docker-compose-rule.yml";

public abstract Optional<ReportingConfig> reporting();

public static class Builder extends ImmutableDockerComposeRuleConfig.Builder {}

public static Builder builder() {
return new Builder();
}

public static Optional<DockerComposeRuleConfig> findAutomatically() {
return findAutomaticallyFrom(new File("."));
}

@VisibleForTesting
static Optional<DockerComposeRuleConfig> findAutomaticallyFrom(File startDir) {
// If current dir is /foo/bar/baz, we search for:
// /foo/bar/baz/.docker-compose-rule.yml
// /foo/bar/.docker-compose-rule.yml
// /foo/.docker-compose-rule.yml
// /.docker-compose-rule.yml
Optional<File> configFile = dirAndParents(startDir)
.map(dir -> new File(dir, CONFIG_FILENAME))
.findFirst(File::exists);

return configFile.map(config -> {
try {
return OBJECT_MAPPER.readValue(config, DockerComposeRuleConfig.class);
} catch (IOException e) {
throw new RuntimeException("Couldn't deserialize config file", e);
}
});
}

private static StreamEx<File> dirAndParents(File startDir) {
return StreamEx.of(Stream.generate(new Supplier<Optional<File>>() {
private Optional<File> dir = Optional.of(startDir.getAbsoluteFile());

@Override
public Optional<File> get() {
Optional<File> toReturn = dir;
dir = dir.flatMap(directory -> Optional.ofNullable(directory.getParentFile()));
return toReturn;
}
}))
.takeWhile(Optional::isPresent)
.map(Optional::get);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.docker.compose.reporting;

import java.io.PrintWriter;
import java.io.StringWriter;

final class ExceptionUtils {
private ExceptionUtils() {}

public static String exceptionToString(Exception exception) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
exception.printStackTrace(printWriter);
return stringWriter.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.docker.compose.reporting;

import com.google.common.io.CharStreams;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class HttpJsonPoster {
private static final Logger log = LoggerFactory.getLogger(HttpJsonPoster.class);

private final ReportingConfig reportingConfig;

HttpJsonPoster(ReportingConfig reportingConfig) {
this.reportingConfig = reportingConfig;
}

public void post(String json) {
try {
URL url = new URL(reportingConfig.url());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setConnectTimeout(1_000);
connection.setReadTimeout(10_000);

connection.setRequestMethod("POST");
connection.setInstanceFollowRedirects(true);
connection.setRequestProperty("Content-Type", "application/json");

String version = Optional.ofNullable(this.getClass().getPackage().getImplementationVersion())
.orElse("0.0.0");
connection.setRequestProperty("User-Agent", "docker-compose-rule/" + version);

connection.setDoOutput(true);
PrintWriter body = new PrintWriter(connection.getOutputStream());
body.println(json);
body.close();

connection.connect();

int status = connection.getResponseCode();

if (status >= 400) {
String error = CharStreams.toString(new InputStreamReader(
connection.getErrorStream(), StandardCharsets.UTF_8));

throw new RuntimeException("Posting json failed. Error is: " + error);
}

connection.disconnect();
} catch (Exception e) {
log.error("Failed to post report", e);
}
}
}
Loading

0 comments on commit c08c286

Please sign in to comment.