Skip to content

Commit d55d6d9

Browse files
Merge pull request #66 from ExpediaDotCom/http-collector-client
Adding support of http collector client
2 parents 014209f + 7289e62 commit d55d6d9

File tree

5 files changed

+266
-0
lines changed

5 files changed

+266
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ hs_err_pid*
4040
# IntelliJ Idea project files
4141
.idea/
4242
*.iml
43+
*.classpath
44+
*.project
45+
*.settings

core/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
<artifactId>slf4j-api</artifactId>
5555
</dependency>
5656

57+
<dependency>
58+
<groupId>org.apache.httpcomponents</groupId>
59+
<artifactId>httpclient</artifactId>
60+
</dependency>
5761
<!-- Test dependencies -->
5862
<dependency>
5963
<groupId>junit</groupId>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2018 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.expedia.www.haystack.client.dispatchers.clients;
18+
19+
import com.expedia.www.haystack.client.Span;
20+
import com.expedia.www.haystack.client.dispatchers.formats.Format;
21+
import com.expedia.www.haystack.client.dispatchers.formats.ProtoBufFormat;
22+
import org.apache.commons.lang3.Validate;
23+
import org.apache.http.Header;
24+
import org.apache.http.client.methods.CloseableHttpResponse;
25+
import org.apache.http.client.methods.HttpPost;
26+
import org.apache.http.entity.ByteArrayEntity;
27+
import org.apache.http.impl.client.CloseableHttpClient;
28+
import org.apache.http.impl.client.HttpClients;
29+
import org.apache.http.message.BasicHeader;
30+
31+
import java.io.Closeable;
32+
import java.io.IOException;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
import java.util.stream.Collectors;
36+
37+
public class HttpCollectorClient implements Client {
38+
private final Format<com.expedia.open.tracing.Span> format;
39+
private final String endpoint;
40+
private final BasicHeader[] headers;
41+
private final CloseableHttpClient httpClient;
42+
43+
public HttpCollectorClient(final String endpoint,
44+
final Map<String, String> headers) {
45+
this(endpoint, headers, HttpClients.createDefault());
46+
}
47+
48+
public HttpCollectorClient(final String endpoint) {
49+
this(endpoint, new HashMap<>());
50+
}
51+
52+
public HttpCollectorClient(final String endpoint,
53+
final Map<String, String> headers,
54+
final CloseableHttpClient httpClient) {
55+
Validate.notEmpty(endpoint, "Haystack collector endpoint can't be empty");
56+
57+
this.format = new ProtoBufFormat();
58+
this.endpoint = endpoint;
59+
this.httpClient = httpClient;
60+
this.headers = headers.entrySet().stream()
61+
.map((entry) -> new BasicHeader(entry.getKey(), entry.getValue()))
62+
.collect(Collectors.toList())
63+
.toArray(new BasicHeader[0]);
64+
}
65+
66+
@Override
67+
public void close() throws ClientException {
68+
closeQuietly(httpClient);
69+
}
70+
71+
@Override
72+
public void flush() throws ClientException {
73+
/* no batching, no flushing */
74+
}
75+
76+
@Override
77+
public boolean send(Span span) throws ClientException {
78+
final byte[] spanBytes = format.format(span).toByteArray();
79+
80+
final HttpPost post = new HttpPost(endpoint);
81+
if (headers != null && headers.length > 0) {
82+
post.setHeaders(headers);
83+
}
84+
final ByteArrayEntity entity = new ByteArrayEntity(spanBytes);
85+
entity.setContentType("application/octet-stream");
86+
post.setEntity(entity);
87+
CloseableHttpResponse response = null;
88+
try {
89+
response = httpClient.execute(post);
90+
final int statusCode = getStatusCode(response);
91+
if (is2xx(statusCode)) {
92+
return true;
93+
}
94+
throw new ClientException(String.format("Failed sending span to http collector endpoint=%s, http status code=%d", endpoint, statusCode));
95+
} catch (IOException e) {
96+
throw new ClientException(String.format("Failed sending span to http collector endpoint=%s", endpoint), e);
97+
} finally {
98+
closeQuietly(response);
99+
}
100+
}
101+
102+
private int getStatusCode(final CloseableHttpResponse response) {
103+
return response.getStatusLine() == null ? 0 : response.getStatusLine().getStatusCode();
104+
}
105+
106+
private boolean is2xx(final int statusCode) {
107+
return statusCode >= 200 && statusCode < 300;
108+
}
109+
110+
public Header[] getHeaders() {
111+
return headers;
112+
}
113+
public String getEndpoint() {
114+
return endpoint;
115+
}
116+
117+
private void closeQuietly(final Closeable obj) {
118+
try {
119+
obj.close();
120+
} catch (Exception ex) {
121+
/* be quiet */
122+
}
123+
}
124+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2018 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package com.expedia.www.haystack.client.dispatchers.clients;
19+
20+
import com.expedia.www.haystack.client.Span;
21+
import com.expedia.www.haystack.client.SpanContext;
22+
import com.expedia.www.haystack.client.Tracer;
23+
import com.expedia.www.haystack.client.dispatchers.InMemoryDispatcher;
24+
import com.expedia.www.haystack.client.metrics.NoopMetricsRegistry;
25+
import org.apache.http.ProtocolVersion;
26+
import org.apache.http.client.methods.CloseableHttpResponse;
27+
import org.apache.http.client.methods.HttpPost;
28+
import org.apache.http.impl.client.CloseableHttpClient;
29+
import org.apache.http.message.BasicStatusLine;
30+
import org.junit.After;
31+
import org.junit.Before;
32+
import org.junit.Test;
33+
import org.mockito.ArgumentCaptor;
34+
import org.mockito.Mockito;
35+
36+
import java.io.IOException;
37+
import java.util.HashMap;
38+
import java.util.Map;
39+
import java.util.UUID;
40+
41+
import static org.junit.Assert.assertEquals;
42+
import static org.mockito.Mockito.*;
43+
44+
public class HttpCollectorClientTest {
45+
46+
private Tracer tracer;
47+
private final static String serviceName = "dummy-service";
48+
private final static UUID traceId = UUID.randomUUID();
49+
private final static UUID spanId = UUID.randomUUID();
50+
private final static SpanContext spanContext = new SpanContext(traceId, spanId, null);
51+
52+
@Before
53+
public void setup() throws Exception {
54+
NoopMetricsRegistry metrics = new NoopMetricsRegistry();
55+
tracer = new Tracer.Builder(metrics, serviceName, new InMemoryDispatcher.Builder(metrics).build()).build();
56+
}
57+
58+
@After
59+
public void teardown() throws Exception {
60+
tracer.close();
61+
}
62+
63+
@Test
64+
public void testDispatch() throws Exception {
65+
final Span span = tracer.buildSpan("happy-path").asChildOf(spanContext).start();
66+
span.finish();
67+
final ArgumentCaptor<HttpPost> httpPostCapture = ArgumentCaptor.forClass(HttpPost.class);
68+
69+
final CloseableHttpClient http = Mockito.mock(CloseableHttpClient.class);
70+
final CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class);
71+
final BasicStatusLine statusLine = new BasicStatusLine(new ProtocolVersion("v", 1, 1), 200, "");
72+
73+
when(http.execute(httpPostCapture.capture())).thenReturn(httpResponse);
74+
when(httpResponse.getStatusLine()).thenReturn(statusLine);
75+
76+
final Map<String, String> headers = new HashMap<>();
77+
headers.put("client-id", "my-client");
78+
79+
final HttpCollectorClient client = new HttpCollectorClient("http://myendpoint:8080/span", headers, http);
80+
final boolean isSuccess = client.send(span);
81+
client.close();
82+
verify(http, times(1)).execute(httpPostCapture.capture());
83+
verify(httpResponse, times(1)).close();
84+
verify(http, times(1)).close();
85+
verifyCapturedHttpPost(httpPostCapture.getValue());
86+
assertEquals(isSuccess, true);
87+
}
88+
89+
@Test(expected = ClientException.class)
90+
public void testFailedDispatch() throws Exception {
91+
final Span span = tracer.buildSpan("sad-path").start();
92+
span.finish();
93+
final ArgumentCaptor<HttpPost> httpPostCapture = ArgumentCaptor.forClass(HttpPost.class);
94+
final CloseableHttpClient http = Mockito.mock(CloseableHttpClient.class);
95+
final CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class);
96+
final BasicStatusLine statusLine = new BasicStatusLine(new ProtocolVersion("v", 1, 1), 404, "");
97+
98+
when(http.execute(httpPostCapture.capture())).thenReturn(httpResponse);
99+
when(httpResponse.getStatusLine()).thenReturn(statusLine);
100+
101+
final HttpCollectorClient client = new HttpCollectorClient("http://myendpoint:8080/span", new HashMap<>(), http);
102+
client.send(span);
103+
}
104+
105+
@Test(expected = ClientException.class)
106+
public void testAnotherFailedDispatch() throws Exception {
107+
final Span span = tracer.buildSpan("sad-path").start();
108+
span.finish();
109+
final ArgumentCaptor<HttpPost> httpPostCapture = ArgumentCaptor.forClass(HttpPost.class);
110+
final CloseableHttpClient http = Mockito.mock(CloseableHttpClient.class);
111+
when(http.execute(httpPostCapture.capture())).thenThrow(new IOException());
112+
113+
final HttpCollectorClient client = new HttpCollectorClient("http://myendpoint:8080/span", new HashMap<>(), http);
114+
client.send(span);
115+
}
116+
117+
private void verifyCapturedHttpPost(final HttpPost httpPost) throws IOException {
118+
assertEquals(httpPost.getMethod(), "POST");
119+
assertEquals(httpPost.getURI().toString(), "http://myendpoint:8080/span");
120+
assertEquals(httpPost.getEntity().getContentType().getValue(), "application/octet-stream");
121+
assertEquals(httpPost.getFirstHeader("client-id").getValue(), "my-client");
122+
com.expedia.open.tracing.Span span = com.expedia.open.tracing.Span.parseFrom(httpPost.getEntity().getContent());
123+
assertEquals(span.getServiceName(), serviceName);
124+
assertEquals(span.getOperationName(), "happy-path");
125+
assertEquals(span.getTraceId(), traceId.toString());
126+
assertEquals(span.getParentSpanId(), spanId.toString());
127+
}
128+
}

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<jaxrs.version>2.1</jaxrs.version>
5555
<micrometer.version>1.0.1</micrometer.version>
5656
<haystack-commons.version>1.0.41</haystack-commons.version>
57+
<http-client.vesion>4.5.3</http-client.vesion>
5758

5859
<!--Plugin Properties -->
5960
<maven-jacoco-plugin.version>0.7.9</maven-jacoco-plugin.version>
@@ -134,6 +135,12 @@
134135
<version>${haystack-commons.version}</version>
135136
</dependency>
136137

138+
<dependency>
139+
<groupId>org.apache.httpcomponents</groupId>
140+
<artifactId>httpclient</artifactId>
141+
<version>${http-client.vesion}</version>
142+
</dependency>
143+
137144
<dependency>
138145
<groupId>io.grpc</groupId>
139146
<artifactId>grpc-testing</artifactId>

0 commit comments

Comments
 (0)