Skip to content

Commit

Permalink
feat: allow optin for stable http attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
manikmagar committed Dec 7, 2023
1 parent 5fc866f commit 5c86418
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 24 deletions.
28 changes: 28 additions & 0 deletions src/docs/asciidoc/module-config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,34 @@ To disable span generation for all processors in a specific namespace, set the `
<opentelemetry:mule-component namespace="os" name="*" />
----

===== Span Attributes

====== HTTP Attributes

HTTP Server and Client spans capture attributes marked with *Required* as a requirement level in OpenTelemetry https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md[Semantic Convention v1.20.0 (Experimental)]. Some of the recommended attributes may also be included, depending on what information is available in runtime.

Some of the attribute conventions from v1.21.0 were marked as deprecated and
OpenTelemetry Semantic Convention https://opentelemetry.io/docs/specs/semconv/http/http-spans/[v1.23.1] marked HTTP conventions as *Stable*.

As per the https://opentelemetry.io/docs/specs/semconv/http/http-spans/[guidelines] in the specification, this module continues to emit attributes as per https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md[v1.20.0]. This behavior can be changed by setting an environment variable `OTEL_SEMCONV_STABILITY_OPT_IN`. Here is a description of the valid values from specification -
[quote]
----
SHOULD introduce an environment variable `OTEL_SEMCONV_STABILITY_OPT_IN`
in the existing major version which is a comma-separated list of values.
The only values defined so far are:
- `http` - emit the new, stable HTTP and networking conventions,
and stop emitting the old experimental HTTP and networking conventions
that the instrumentation emitted previously.
- `http/dup` - emit both the old and the stable HTTP and networking conventions,
allowing for a seamless transition.
- The default behavior (in the absence of one of these values) is to continue
emitting whatever version of the old experimental HTTP and networking conventions
the instrumentation was emitting previously.
- Note: `http/dup` has higher precedence than `http` in case both values are present
----

WARNING: This environment variable is provided in current major version to allow time to migrate to stable attributes. It will be dropped in next major version.

==== Custom Tags
In addition to all the trace attributes captured by the module, it is possible to add custom tags to the current trace using an *operation* `opentelemetry:add-custom-tags`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.avioconsulting.mule.opentelemetry.internal.config.OpenTelemetryConfigWrapper;
import com.avioconsulting.mule.opentelemetry.internal.opentelemetry.metrics.MetricsInstaller;
import com.avioconsulting.mule.opentelemetry.internal.opentelemetry.sdk.SemanticAttributes;
import com.avioconsulting.mule.opentelemetry.internal.opentelemetry.sdk.SemconvStability;
import com.avioconsulting.mule.opentelemetry.internal.processor.TraceComponent;
import com.avioconsulting.mule.opentelemetry.internal.store.InMemoryTransactionStore;
import com.avioconsulting.mule.opentelemetry.internal.store.SpanMeta;
Expand Down Expand Up @@ -106,6 +107,7 @@ private OpenTelemetryConnection(OpenTelemetryConfigWrapper openTelemetryConfigWr
.build();
setupCustomMetrics(openTelemetryConfigWrapper);
transactionStore = InMemoryTransactionStore.getInstance();
SemconvStability.init();
}

private void setupCustomMetrics(OpenTelemetryConfigWrapper openTelemetryConfigWrapper) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.avioconsulting.mule.opentelemetry.internal.opentelemetry.sdk;

import java.util.HashSet;
import java.util.Set;

import static java.util.Arrays.asList;

/**
* Reads the Semantic Convention stability flag.
* Could have used
* {@link io.opentelemetry.instrumentation.api.internal.SemconvStability} but
* that is marked as internal
* so replicating it here for now.
*/
public class SemconvStability {

private static boolean emitOldHttpSemconv;
private static boolean emitStableHttpSemconv;
private static boolean emitOldJvmSemconv;
private static boolean emitStableJvmSemconv;

static {
init();
}

public static void init() {
boolean oldHttp = true;
boolean stableHttp = false;
boolean oldJvm = true;
boolean stableJvm = false;

String value = System.getProperty("otel.semconv-stability.opt-in",
System.getenv("OTEL_SEMCONV_STABILITY_OPT_IN"));

if (value != null) {
Set<String> values = new HashSet<>(asList(value.split(",")));
if (values.contains("http")) {
oldHttp = false;
stableHttp = true;
}
// no else -- technically it's possible to set "http,http/dup", in which case we
// should emit
// both sets of attributes
if (values.contains("http/dup")) {
oldHttp = true;
stableHttp = true;
}

if (values.contains("jvm")) {
oldJvm = false;
stableJvm = true;
}
if (values.contains("jvm/dup")) {
oldJvm = true;
stableJvm = true;
}
}

emitOldHttpSemconv = oldHttp;
emitStableHttpSemconv = stableHttp;
emitOldJvmSemconv = oldJvm;
emitStableJvmSemconv = stableJvm;
}

public static boolean emitOldHttpSemconv() {
return emitOldHttpSemconv;
}

public static boolean emitStableHttpSemconv() {
return emitStableHttpSemconv;
}

public static boolean emitOldJvmSemconv() {
return emitOldJvmSemconv;
}

public static boolean emitStableJvmSemconv() {
return emitStableJvmSemconv;
}

private SemconvStability() {
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.avioconsulting.mule.opentelemetry.internal.processor;

import com.avioconsulting.mule.opentelemetry.internal.connection.TraceContextHandler;
import com.avioconsulting.mule.opentelemetry.internal.opentelemetry.sdk.SemconvStability;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import org.mule.extension.http.api.HttpRequestAttributes;
Expand All @@ -24,7 +26,6 @@

import static io.opentelemetry.semconv.SemanticAttributes.*;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;

public class HttpProcessorComponent extends AbstractProcessorComponent {

Expand Down Expand Up @@ -93,11 +94,9 @@ public TraceComponent getEndTraceComponent(EnrichedServerNotification notificati
// include the HTTP Response attributes.
HttpResponseAttributes attributes = responseAttributes.getValue();
Map<String, String> tags = new HashMap<>();
tags.put(HTTP_STATUS_CODE.getKey(), Integer.toString(attributes.getStatusCode()));
addAttribute(tags, Integer.toString(attributes.getStatusCode()), HTTP_STATUS_CODE, HTTP_RESPONSE_STATUS_CODE);
addAttribute(tags, attributes.getHeaders().get("content-length"), HTTP_RESPONSE_CONTENT_LENGTH, null);
endTraceComponent.withStatsCode(getSpanStatus(false, attributes.getStatusCode()));
tags.put(
HTTP_RESPONSE_CONTENT_LENGTH.getKey(),
attributes.getHeaders().get("content-length"));
if (endTraceComponent.getTags() != null)
tags.putAll(endTraceComponent.getTags());
return endTraceComponent.withTags(tags);
Expand Down Expand Up @@ -148,13 +147,6 @@ private Map<String, String> getRequesterTags(ComponentWrapper componentWrapper)
Map<String, String> tags = new HashMap<>();
String path = componentWrapper.getParameters().get("path");
Map<String, String> connectionParameters = componentWrapper.getConfigConnectionParameters();
if (!connectionParameters.isEmpty()) {
tags.put(HTTP_SCHEME.getKey(), connectionParameters.getOrDefault("protocol", "").toLowerCase());
tags.put(HTTP_HOST.getKey(), connectionParameters.getOrDefault("host", "").concat(":")
.concat(connectionParameters.getOrDefault("port", "")));
tags.put(NET_PEER_NAME.getKey(), connectionParameters.getOrDefault("host", ""));
tags.put(NET_PEER_PORT.getKey(), connectionParameters.getOrDefault("port", ""));
}
Map<String, String> configParameters = componentWrapper.getConfigParameters();
if (!configParameters.isEmpty()) {
if (configParameters.containsKey("basePath")
Expand All @@ -163,10 +155,31 @@ private Map<String, String> getRequesterTags(ComponentWrapper componentWrapper)
}
}
tags.put(HTTP_ROUTE.getKey(), path);
tags.put(HTTP_METHOD.getKey(), componentWrapper.getParameters().get("method"));
if (!connectionParameters.isEmpty()) {
addRequestConnectionAttributes(tags, connectionParameters);
}
addAttribute(tags, componentWrapper.getParameters().get("method"), HTTP_METHOD, HTTP_REQUEST_METHOD);
return tags;
}

private void addRequestConnectionAttributes(Map<String, String> tags, Map<String, String> connectionParameters) {
String protocol = connectionParameters.getOrDefault("protocol", "").toLowerCase();
String serverPortAddress = connectionParameters.getOrDefault("host", "").concat(":")
.concat(connectionParameters.getOrDefault("port", ""));

String urlFull = String.format("%s://%s%s", protocol, serverPortAddress, tags.get(HTTP_ROUTE.getKey()));
addAttribute(tags, urlFull, HTTP_URL, URL_FULL);
addAttribute(tags, protocol, HTTP_SCHEME, URL_SCHEME);
addAttribute(tags, connectionParameters.getOrDefault("host", ""), NET_PEER_NAME, SERVER_ADDRESS);
addAttribute(tags, connectionParameters.getOrDefault("host", ""), NET_PEER_PORT, SERVER_PORT);

if (SemconvStability.emitOldHttpSemconv()) {
// Retained for backward-compatibility, this attribute was removed by Otel
// 1.13.0 Semconv
tags.put(HTTP_HOST.getKey(), serverPortAddress);
}
}

@Override
public TraceComponent getSourceStartTraceComponent(EnrichedServerNotification notification,
TraceContextHandler traceContextHandler) {
Expand Down Expand Up @@ -202,7 +215,7 @@ public TraceComponent getSourceEndTraceComponent(EnrichedServerNotification noti
statusCode = TypedValue.unwrap(httpStatus).toString();
}
TraceComponent traceComponent = getTraceComponentBuilderFor(notification);
traceComponent.withTags(singletonMap(HTTP_STATUS_CODE.getKey(), statusCode));
addAttribute(traceComponent.getTags(), statusCode, HTTP_STATUS_CODE, HTTP_RESPONSE_STATUS_CODE);
traceComponent.withStatsCode(getSpanStatus(true, Integer.parseInt(statusCode)));
return traceComponent;
}
Expand All @@ -213,19 +226,45 @@ public TraceComponent getSourceEndTraceComponent(EnrichedServerNotification noti
return null;
}

private <T> void addAttribute(Map<String, String> tags, String value, AttributeKey<T> oldAttribute,
AttributeKey<T> stableAttribute) {
if (SemconvStability.emitOldHttpSemconv()) {
tags.put(oldAttribute.getKey(), value);
}
if (SemconvStability.emitStableHttpSemconv() && stableAttribute != null) {
tags.put(stableAttribute.getKey(), value);
}
}

private Map<String, String> attributesToTags(HttpRequestAttributes attributes) {
Map<String, String> tags = new HashMap<>();
tags.put(HTTP_HOST.getKey(), attributes.getHeaders().get("host"));
tags.put(HTTP_USER_AGENT.getKey(), attributes.getHeaders().get("user-agent"));
tags.put(HTTP_METHOD.getKey(), attributes.getMethod());
tags.put(HTTP_SCHEME.getKey(), attributes.getScheme());
String hostHeader = attributes.getHeaders().get("host");
String[] hostParts = hostHeader.split(":");
String host = hostParts[0];
String hostPort = hostParts.length > 1 ? hostParts[1] : null;

addAttribute(tags, attributes.getScheme(), HTTP_SCHEME, URL_SCHEME);
addAttribute(tags, attributes.getHeaders().get("user-agent"), HTTP_USER_AGENT, USER_AGENT_ORIGINAL);
addAttribute(tags, attributes.getMethod(), HTTP_METHOD, HTTP_REQUEST_METHOD);
tags.put(HTTP_ROUTE.getKey(), attributes.getListenerPath());
tags.put(HTTP_TARGET.getKey(), attributes.getRequestPath());
tags.put(
HTTP_FLAVOR.getKey(),
attributes.getVersion().substring(attributes.getVersion().lastIndexOf("/") + 1));
// TODO: Support additional request headers to be added in
// http.request.header.<key>=<header-value> attribute.

if (SemconvStability.emitOldHttpSemconv()) {
tags.put(HTTP_HOST.getKey(), attributes.getHeaders().get("host"));
tags.put(HTTP_TARGET.getKey(), attributes.getRequestPath());
tags.put(
HTTP_FLAVOR.getKey(),
attributes.getVersion().substring(attributes.getVersion().lastIndexOf("/") + 1));
}
if (SemconvStability.emitStableHttpSemconv()) {
tags.put(SERVER_ADDRESS.getKey(), host);
if (hostPort != null)
tags.put(SERVER_PORT.getKey(), hostPort);
tags.put(URL_PATH.getKey(), attributes.getRequestPath());
if (attributes.getQueryString() != null
&& !attributes.getQueryString().isEmpty()) {
tags.put(URL_QUERY.getKey(), attributes.getQueryString());
}
}
return tags;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.avioconsulting.mule.opentelemetry;

import com.avioconsulting.mule.opentelemetry.internal.opentelemetry.sdk.test.DelegatedLoggingSpanTestExporter;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Test;

import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

public class MuleOpenTelemetryHttpOldStableAttributesTest extends AbstractMuleArtifactTraceTest {

@Override
protected void doSetUpBeforeMuleContextCreation() throws Exception {
System.setProperty("otel.semconv-stability.opt-in", "http/dup");
super.doSetUpBeforeMuleContextCreation();
}

@Override
protected void doTearDownAfterMuleContextDispose() throws Exception {
System.clearProperty("otel.semconv-stability.opt-in");
super.doTearDownAfterMuleContextDispose();
}

@Override
protected String getConfigFile() {
return "mule-opentelemetry-http.xml";
}

@Test
public void testOldAndStableHttpAttributes() throws Exception {
System.setProperty("otel.semconv-stability.opt-in", "http/dup");
sendRequest(CORRELATION_ID, "test-remote-request", 200);
await().untilAsserted(() -> assertThat(DelegatedLoggingSpanTestExporter.spanQueue)
.isNotEmpty()
.hasSize(3));
assertThat(DelegatedLoggingSpanTestExporter.spanQueue)
.filteredOnAssertions(span -> assertThat(span)
.as("Span for http:listener flow")
.extracting("spanName", "spanKind")
.containsOnly("/test-remote-request", "SERVER"))
.isNotEmpty()
.hasSize(1)
.element(0)
.extracting("attributes", as(InstanceOfAssertFactories.map(String.class, Object.class)))
.hasSizeGreaterThanOrEqualTo(13)
.containsEntry("mule.app.flow.name", "mule-opentelemetry-app-requester-remote")
.containsKey("mule.serverId")
.containsEntry("http.scheme", "http")
.containsEntry("http.method", "GET")
.containsEntry("http.route", "/test-remote-request")
.containsKey("mule.correlationId")
.containsKey("http.user_agent")
.hasEntrySatisfying("http.host",
value -> assertThat(value.toString()).startsWith("localhost:"))
.containsEntry("http.flavor", "1.1")
.containsEntry("mule.app.flow.source.name", "listener")
.containsEntry("mule.app.flow.source.namespace", "http")
.containsEntry("mule.app.flow.source.configRef", "HTTP_Listener_config")
.containsEntry("server.address", "localhost")
.containsEntry("server.port", serverPort.getValue())
.containsEntry("http.route", "/test-remote-request")
.containsEntry("http.request.method", "GET")
.containsEntry("url.path", "/test-remote-request")
.doesNotContainKey("url.query")
.containsEntry("url.scheme", "http")
.containsKey("user_agent.original")
.containsKey("mule.correlationId");
System.clearProperty("otel.semconv-stability.opt-in");
}

private void resetStaticFinalFields() {

}
}
Loading

0 comments on commit 5c86418

Please sign in to comment.