Skip to content

Commit

Permalink
Add SPI tests to TCK (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
yasmin-aumeeruddy committed Jul 18, 2023
1 parent 093f1c9 commit 1163c97
Show file tree
Hide file tree
Showing 11 changed files with 935 additions and 0 deletions.
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.eclipse.microprofile.telemetry.tracing.tck.rest;

import static java.net.HttpURLConnection.HTTP_OK;
import static org.eclipse.microprofile.telemetry.tracing.tck.rest.PropagationHelper.SpanResourceClient;

import java.net.URL;

import org.eclipse.microprofile.telemetry.tracing.tck.TestLibraries;
import org.eclipse.microprofile.telemetry.tracing.tck.exporter.InMemorySpanExporter;
import org.eclipse.microprofile.telemetry.tracing.tck.exporter.InMemorySpanExporterProvider;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Scope;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider;
import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider;
import io.opentelemetry.sdk.trace.data.SpanData;
import jakarta.inject.Inject;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;

public class PropagatorSpiTest extends Arquillian {
public static final String TEST_VALUE = "test-value";

public static final String TEST_KEY = "test-key";

@Deployment
public static WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class)
.addClasses(TestPropagator.class, TestPropagatorProvider.class, InMemorySpanExporter.class,
InMemorySpanExporterProvider.class, PropagationHelper.class,
SpanResourceClient.class)
.addAsServiceProvider(ConfigurableSpanExporterProvider.class, InMemorySpanExporterProvider.class)
.addAsServiceProvider(ConfigurablePropagatorProvider.class, TestPropagatorProvider.class)
.addAsLibrary(TestLibraries.AWAITILITY_LIB)
.addAsResource(
new StringAsset("otel.sdk.disabled=false\notel.propagators=" + TestPropagatorProvider.NAME
+ "\notel.traces.exporter=in-memory"),
"META-INF/microprofile-config.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");

}

SpanResourceClient client;

@ArquillianResource
URL url;

@Inject
InMemorySpanExporter exporter;

@Inject
Baggage baggage;

@BeforeMethod
void setUp() {
// Only want to run on server
if (exporter != null) {
exporter.reset();
}
}

@Test
void testSPIPropagator() {
try (Scope s = baggage.toBuilder().put(TEST_KEY, TEST_VALUE).build().makeCurrent()) {
WebTarget target = ClientBuilder.newClient().target(url.toString()).path("baggage");
Response response = target.request().get();
Assert.assertEquals(response.getStatus(), HTTP_OK);
}

exporter.assertSpanCount(2);

SpanData server = exporter.getFirst(SpanKind.SERVER);
Assert.assertEquals(TEST_VALUE, server.getAttributes().get(AttributeKey.stringKey(TEST_KEY)));

SpanData client = exporter.getFirst(SpanKind.CLIENT);
// Check that trace context propagation worked by checking that the parent was set correctly
Assert.assertEquals(server.getParentSpanId(), client.getSpanId());;
}

@Path("/baggage")
public static class BaggageResource {
@Inject
Baggage baggage;

@Inject
private Span span;

@GET
public Response get(@Context HttpHeaders headers) {
try {
// Check the TestPropagator headers were used
Assert.assertNotNull(headers.getHeaderString(TestPropagator.TRACE_KEY));
Assert.assertNotNull(headers.getHeaderString(TestPropagator.BAGGAGE_KEY));

// Test that the default W3C headers were not used
Assert.assertNull(headers.getHeaderString("traceparent"));
Assert.assertNull(headers.getHeaderString("tracestate"));
Assert.assertNull(headers.getHeaderString("baggage"));

// Copy TEST_KEY from baggage into a span attribute
span.setAttribute(TEST_KEY, baggage.getEntryValue(TEST_KEY));
return Response.ok().build();
} catch (Throwable e) {
// An error here won't get reported back fully, so output it to the log as well
System.err.println("Baggage Resource Exception:");
e.printStackTrace();
throw e;
}
}
}

@ApplicationPath("/")
public static class RestApplication extends Application {

}
}
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.eclipse.microprofile.telemetry.tracing.tck.rest;

import static java.util.Collections.unmodifiableList;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;

import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.baggage.BaggageBuilder;
import io.opentelemetry.api.baggage.BaggageEntry;
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.api.trace.TraceFlags;
import io.opentelemetry.api.trace.TraceState;
import io.opentelemetry.api.trace.TraceStateBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;

/**
* A basic propagator for span context and baggage
* <p>
* Span information is passed in the TEST-SPAN key with format
* {@code traceId;spanId;flags;statekey1=value,statekey2=value}
* <p>
* Baggage information is passed in the TEST-BAGGAGE key with format
* {@code key,value,metadata;key2,value,metadata;key3,value,metadata;...}
* <p>
* All individual values are urlencoded to make parsing easy (don't have to worry about values containing separator
* characters)
*/
public class TestPropagator implements TextMapPropagator {

public static final String BAGGAGE_KEY = "TEST-BAGGAGE";
public static final String TRACE_KEY = "TEST-SPAN";

private static final List<String> FIELDS = unmodifiableList(Arrays.asList(BAGGAGE_KEY, TRACE_KEY));

/** {@inheritDoc} */
@Override
public Collection<String> fields() {
return FIELDS;
}

/** {@inheritDoc} */
@Override
public <C> Context extract(Context context, C carrier, TextMapGetter<C> getter) {
// extract data from carrier using getter and put it into context

String baggageString = getter.get(carrier, BAGGAGE_KEY);
if (baggageString != null && !baggageString.isEmpty()) {
Baggage baggage = deserializeBaggage(baggageString);
context = context.with(baggage);
}

String traceString = getter.get(carrier, TRACE_KEY);
if (traceString != null && !traceString.isEmpty()) {
Span span = deserializeSpan(traceString);
context = context.with(span);
}

return context;
}

/** {@inheritDoc} */
@Override
public <C> void inject(Context context, C carrier, TextMapSetter<C> setter) {
// take data from context and inject it into carrier using setter
Baggage baggage = Baggage.fromContextOrNull(context);
if (baggage != null && !baggage.isEmpty()) {
setter.set(carrier, BAGGAGE_KEY, serializeBaggage(baggage));
}

Span span = Span.fromContextOrNull(context);
if (span != null && span.getSpanContext().isValid()) {
setter.set(carrier, TRACE_KEY, serializeSpan(span.getSpanContext()));
}
}

private String serializeBaggage(Baggage baggage) {
StringBuffer baggageString = new StringBuffer();
boolean first = true;
for (Entry<String, BaggageEntry> entry : baggage.asMap().entrySet()) {
if (!first) {
baggageString.append(';');
first = false;
}
baggageString.append(encode(entry.getKey()))
.append(',')
.append(encode(entry.getValue().getValue()))
.append(',')
.append(encode(entry.getValue().getMetadata().getValue()));
}
return baggageString.toString();
}

private Baggage deserializeBaggage(String string) {
BaggageBuilder builder = Baggage.empty().toBuilder();
for (String entry : string.split(";")) {
if (entry.isEmpty()) {
continue;
}
String[] parts = entry.split(",", -1); // -1 -> keep trailing empty strings
builder.put(decode(parts[0]),
decode(parts[1]),
BaggageEntryMetadata.create(decode(parts[2])));
}
return builder.build();
}

private String serializeSpan(SpanContext span) {
StringBuffer spanString = new StringBuffer();
spanString.append(span.getTraceId())
.append(';')
.append(span.getSpanId())
.append(';')
.append(span.getTraceFlags().asHex())
.append(';');
boolean first = true;
for (Entry<String, String> entry : span.getTraceState().asMap().entrySet()) {
if (first) {
spanString.append(',');
first = false;
}

spanString.append(encode(entry.getKey()))
.append('=')
.append(encode(entry.getValue()));
}

return spanString.toString();
}

private Span deserializeSpan(String string) {
String[] parts = string.split(";", -1);
String traceId = decode(parts[0]);
String spanId = decode(parts[1]);
TraceFlags flags = TraceFlags.fromHex(decode(parts[2]), 0);

TraceStateBuilder stateBuilder = TraceState.builder();
for (String entry : parts[3].split(",")) {
if (entry.isEmpty()) {
continue;
}
String[] entryParts = entry.split("=");
stateBuilder.put(decode(entryParts[0]),
decode(entryParts[1]));
}

SpanContext spanContext = SpanContext.create(traceId, spanId, flags, stateBuilder.build());
return Span.wrap(spanContext);
}

private String encode(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}

private String decode(String s) {
return URLDecoder.decode(s, StandardCharsets.UTF_8);
}

}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.eclipse.microprofile.telemetry.tracing.tck.rest;

import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider;

public class TestPropagatorProvider implements ConfigurablePropagatorProvider {

public static final String NAME = "test-propagator";

/** {@inheritDoc} */
@Override
public String getName() {
return NAME;
}

/** {@inheritDoc} */
@Override
public TextMapPropagator getPropagator(ConfigProperties arg0) {
return new TestPropagator();
}

}

0 comments on commit 1163c97

Please sign in to comment.