Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
Add Fahrschein User-Agent to Nakadi requests (#350)
Browse files Browse the repository at this point in the history
* Add User-Agent to Nakadi requests

We would like to have an easy way to get some statistics around Fahrschein usage in general, and which versions, which JDK version, and via which HTTP client implementation specificially, in order to a) justify the time we're spending on the library, b) get more insights about builder behaviour. The header should be set by default (not overridable initially).

Format

    "Fahrschein/" [FahrscheinVersion] " (" [HttpClient] "; " [Java Version] ("; " [Additional Property])* ")"

Examples

    User-Agent: Fahrschein/0.22.0 (Simple; Java11)
    User-Agent: Fahrschein/0.22.0 (ApacheHttpClient; Java8)
    User-Agent: Fahrschein/0.22.0 (JavaNet; Java17; experimental-features=a,b,c)
    User-Agent: Fahrschein/0.22.0 (Spring; Java8; Kotlin)

* Fix github workflow references in README

* Update fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java

Co-authored-by: Malte <[email protected]>

* Update fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java

Co-authored-by: Malte <[email protected]>

* Update fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java

Co-authored-by: Malte <[email protected]>

* Update fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java

Co-authored-by: Malte <[email protected]>

* Reduce logging from error to warn

* mark project.version as task input

Co-authored-by: Malte <[email protected]>
  • Loading branch information
otrosien and MALPI authored Jul 21, 2022
1 parent 83f4c3f commit b8b2828
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 4 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ Most dependencies are defined on a per-subproject level, only the versions for t
./gradlew test -Pjackson.version=2.9.0
```

The [integration tests](.github/workflows/ci.yaml) include running the build with our supported baseline dependency as well as the latest micro release of Jackson, Apache HttpClient and Spring. Please update the section in the README when bumping dependency baselines, and add this to the release notes.
The [integration tests](.github/workflows/ci.yml) include running the build with our supported baseline dependency as well as the latest micro release of Jackson, Apache HttpClient and Spring. Please update the section in the README when bumping dependency baselines, and add this to the release notes.

### End-to-End tests

Expand All @@ -353,7 +353,7 @@ open build/reports/dependency-check-report.html

### Releasing

Fahrschein uses Github Workflows to [build](.github/workflows/ci.yaml) and [publish releases](.github/workflows/maven-publish.yaml). This happens automatically whenever a new release is created in Github. After creating a release, please bump the `project.version` property in [gradle.properties](./gradle.properties).
Fahrschein uses Github Workflows to [build](.github/workflows/ci.yml) and [publish releases](.github/workflows/maven-publish.yml). This happens automatically whenever a new release is created in Github. After creating a release, please bump the `project.version` property in [gradle.properties](./gradle.properties).

If needed, you can preview the signed release artifacts in your local maven repository.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public interface Headers {
String CONTENT_TYPE = "Content-Type";
String CONTENT_ENCODING = "Content-Encoding";
String ACCEPT_ENCODING = "Accept-Encoding";
String USER_AGENT = "User-Agent";

List<String> get(String headerName);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.zalando.fahrschein.http.api;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public final class UserAgent {
private static final String PROPERTIES_FILE = "fahrschein.properties";
private static final Logger logger = LoggerFactory.getLogger(UserAgent.class);

private static final Properties fahrscheinProperties = new Properties();

static {
try (final InputStream stream =
UserAgent.class.getResourceAsStream("/" + PROPERTIES_FILE)) {
if (stream != null) {
fahrscheinProperties.load(stream);
} else {
logger.warn("Properties file not found: {}", PROPERTIES_FILE);
}
} catch (IOException e) {
logger.warn("Cannot read file: " + PROPERTIES_FILE, e);
}
}

private static final String AGENT_STR_TEMPLATE = "Fahrschein/%s (%s; Java%d)";

private final String userAgent;

public UserAgent(Class implementation) {
this.userAgent = String.format(AGENT_STR_TEMPLATE, fahrscheinVersion(), implementation.getSimpleName().replace("RequestFactory", ""), javaVersion());
}

public String userAgent() {
return userAgent;
}

static String fahrscheinVersion() {
return String.valueOf(fahrscheinProperties.get("fahrschein-version"));
}

static int javaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf(".");
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.zalando.fahrschein.http.api;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class UserAgentTest {

abstract class DummyRequestFactory implements RequestFactory {}

@Test
public void javaVersion() {
String version = System.getProperty("java.version");
int javaVersion = UserAgent.javaVersion();
if (version.startsWith("1.8")) {
assertEquals(8, javaVersion, "Java 8");
} else {
assertTrue(version.startsWith(String.valueOf(javaVersion)));
assertTrue(javaVersion > 8, "Java " + javaVersion);
}
}

@Test
public void userAgentString() {
UserAgent ua = new UserAgent(DummyRequestFactory.class);
String userAgent = ua.userAgent();
assertEquals("Fahrschein/0.1.0-SNAPSHOT (Dummy; Java" + UserAgent.javaVersion() + ")", userAgent);
}

}
2 changes: 2 additions & 0 deletions fahrschein-http-api/src/test/resources/fahrschein.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# for testing
fahrschein-version=0.1.0-SNAPSHOT
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void add(String headerName, String value) {

@Override
public void put(String headerName, String value) {
request.header(headerName, value);
request.setHeader(headerName, value);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ public static void startServer() throws IOException {
@Captor
public ArgumentCaptor<HttpExchange> exchangeCaptor;

@Test
public void testUserAgent() throws IOException {
// given
String expectedResponse = "{}";
HttpHandler spy = Mockito.spy(new SimpleRequestResponseContentHandler(expectedResponse));
server.createContext("/user-agent", spy);

// when
final RequestFactory f = defaultRequestFactory(ContentEncoding.IDENTITY);
Request r = f.createRequest(serverAddress.resolve("/user-agent"), "GET");
r.getHeaders().put("User-Agent", "Test");
Response executed = r.execute();
readStream(executed.getBody());

// then
Mockito.verify(spy).handle(exchangeCaptor.capture());
HttpExchange capturedArgument = exchangeCaptor.getValue();
assertThat("UserAgent header", capturedArgument.getRequestHeaders().get("user-agent"), equalTo(Arrays.asList("Test")));
}

@Test
public void testGzippedResponseBody() throws IOException {
// given
Expand Down
34 changes: 34 additions & 0 deletions fahrschein/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,37 @@ dependencies {
}

publishing.publications.maven.pom.description = 'A Java client for the Nakadi event bus'

def generateResourcesTask = tasks.register("generate-resources", GenerateResourcesTask) {
resourcesDir.set(layout.buildDirectory.dir("generated-resources/main"))
}

sourceSets {
main {
output.dir(generateResourcesTask)
}
}

idea {
module {
// Marks the already(!) added srcDir as "generated"
generatedSourceDirs += file('build/generated-resources/main')
}
}

abstract class GenerateResourcesTask extends DefaultTask {

@Input
String getVersion() {
return project.version
}

@OutputDirectory
abstract DirectoryProperty getResourcesDir()

@TaskAction
def generateResources() {
def generated = resourcesDir.file("fahrschein.properties").get().asFile
generated.text = "fahrschein-version=" + getVersion()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import org.zalando.fahrschein.http.api.RequestFactory;

import javax.annotation.Nullable;
import java.net.URI;

Expand Down Expand Up @@ -48,7 +49,7 @@ public NakadiClientBuilder withCursorManager(CursorManager cursorManager) {
}

static RequestFactory wrapClientHttpRequestFactory(RequestFactory delegate, @Nullable AuthorizationProvider authorizationProvider) {
RequestFactory requestFactory = new ProblemHandlingRequestFactory(delegate);
RequestFactory requestFactory = new ProblemHandlingRequestFactory(new UserAgentRequestFactory(delegate));
if (authorizationProvider != null) {
requestFactory = new AuthorizedRequestFactory(requestFactory, authorizationProvider);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.zalando.fahrschein;

import org.zalando.fahrschein.http.api.Headers;
import org.zalando.fahrschein.http.api.Request;
import org.zalando.fahrschein.http.api.RequestFactory;
import org.zalando.fahrschein.http.api.UserAgent;

import java.io.IOException;
import java.net.URI;

public class UserAgentRequestFactory implements RequestFactory {
private final RequestFactory delegate;
private final String userAgent;

UserAgentRequestFactory(final RequestFactory delegate) {
this.userAgent = new UserAgent(delegate.getClass()).userAgent();
this.delegate = delegate;
}

@Override
public Request createRequest(URI uri, String method) throws IOException {
final Request request = delegate.createRequest(uri, method);
request.getHeaders().put(Headers.USER_AGENT, userAgent);
return request;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.zalando.fahrschein;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zalando.fahrschein.domain.Partition;
Expand Down Expand Up @@ -56,6 +57,17 @@ public void setup() {
this.client = nakadiClient;
}

@Test
public void shouldAddUserAgent() throws IOException {
server.expectRequestTo("http://example.com/event-types/foobar/partitions", "GET")
.andExpectHeader("User-Agent", Matchers.startsWith("Fahrschein/"))
.andRespondWith(200, ContentType.APPLICATION_JSON, "[]")
.setup();

client.getPartitions("foobar");
server.verify();
}

@Test
public void shouldGetPartitions() throws IOException {
server.expectRequestTo("http://example.com/event-types/foobar/partitions", "GET")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.zalando.fahrschein;

import org.junit.jupiter.api.Test;
import org.zalando.fahrschein.http.api.Headers;
import org.zalando.fahrschein.http.api.HeadersImpl;
import org.zalando.fahrschein.http.api.Request;
import org.zalando.fahrschein.http.api.RequestFactory;
import org.zalando.fahrschein.http.api.Response;
import org.zalando.fahrschein.http.api.UserAgent;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class UserAgentRequestFactoryTest {
static class DummyRequestFactory implements RequestFactory {

@Override
public Request createRequest(URI uri, String method) throws IOException {
return new Request() {
private final Headers headers = new HeadersImpl();
@Override
public String getMethod() {
return method;
}

@Override
public URI getURI() {
return uri;
}

@Override
public Headers getHeaders() {
return headers;
}

@Override
public OutputStream getBody() throws IOException {
return null;
}

@Override
public Response execute() throws IOException {
return null;
}
};
}
}

@Test
public void appendUserAgentToRequest() throws IOException {
UserAgent ua = new UserAgent(DummyRequestFactory.class);
UserAgentRequestFactory rf = new UserAgentRequestFactory(new DummyRequestFactory());
Request r = rf.createRequest(URI.create("dummy://req"), "POST");
assertEquals(ua.userAgent(), r.getHeaders().getFirst("User-Agent"));
}
}

0 comments on commit b8b2828

Please sign in to comment.