diff --git a/CHANGELOG.md b/CHANGELOG.md index adef551524d..7b1a13d8978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +# Version 2.3.0 +- [#716](https://github.com/Microsoft/ApplicationInsights-Java/issues/716) Introduced W3C Distributed tracing protocol. + # Version 2.2.1 - Fixed [#767](https://github.com/Microsoft/ApplicationInsights-Java/issues/767). Updated gRPC dependencies which inlcudes latest netty version. - Fixed [#751](https://github.com/Microsoft/ApplicationInsights-Java/issues/751). Added support for absolute paths for log file output. diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/DefaultClassDataProvider.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/DefaultClassDataProvider.java index be61143e189..53d1cf73487 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/DefaultClassDataProvider.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/DefaultClassDataProvider.java @@ -88,7 +88,10 @@ public void setConfiguration(AgentConfiguration agentConfiguration) { if (agentConfiguration.getBuiltInConfiguration().isHttpEnabled()) { InternalLogger.INSTANCE.trace("Adding built-in HTTP instrumentation"); - new HttpClassDataProvider(classesToInstrument).add(); + HttpClassDataProvider httpClassDataProvider= new HttpClassDataProvider(classesToInstrument); + httpClassDataProvider.setIsW3CEnabled(agentConfiguration.getBuiltInConfiguration().isW3cEnabled()); + httpClassDataProvider.setIsW3CBackportEnabled(agentConfiguration.getBuiltInConfiguration().isW3CBackportEnabled()); + httpClassDataProvider.add(); } if (agentConfiguration.getBuiltInConfiguration().isRedisEnabled()) { diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/HttpClientMethodVisitor.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/HttpClientMethodVisitor.java index 36ecb24cd60..2fd4aec3758 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/HttpClientMethodVisitor.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/HttpClientMethodVisitor.java @@ -34,14 +34,20 @@ public final class HttpClientMethodVisitor extends AbstractHttpMethodVisitor { private final static String FINISH_DETECT_METHOD_NAME = "httpMethodFinished"; private final static String FINISH_METHOD_RETURN_SIGNATURE = "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IJ)V"; + private final boolean isW3CEnabled; + private final boolean isW3CBackportEnabled; public HttpClientMethodVisitor(int access, String desc, String owner, String methodName, MethodVisitor methodVisitor, - ClassToMethodTransformationData additionalData) { + ClassToMethodTransformationData additionalData, + boolean isW3CEnabled, + boolean isW3CBackportEnabled) { super(access, desc, owner, methodName, methodVisitor, additionalData); + this.isW3CEnabled = isW3CEnabled; + this.isW3CBackportEnabled = isW3CBackportEnabled; } private int deltaInNS; @@ -50,6 +56,8 @@ public HttpClientMethodVisitor(int access, private int childIdLocal; private int correlationContextLocal; private int appCorrelationId; + private int tracestate; + private int traceparent; @Override public void onMethodEnter() { @@ -57,36 +65,94 @@ public void onMethodEnter() { deltaInNS = this.newLocal(Type.LONG_TYPE); mv.visitVarInsn(LSTORE, deltaInNS); - // generate child ID - mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "generateChildDependencyId", "()Ljava/lang/String;", false); - childIdLocal = this.newLocal(Type.getType(Object.class)); - mv.visitVarInsn(ASTORE, childIdLocal); - - // retrieve correlation context - mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "retrieveCorrelationContext", "()Ljava/lang/String;", false); - correlationContextLocal = this.newLocal(Type.getType(Object.class)); - mv.visitVarInsn(ASTORE, correlationContextLocal); - - // retrieve request context - mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "retrieveApplicationCorrelationId", "()Ljava/lang/String;", false); - appCorrelationId = this.newLocal(Type.getType(Object.class)); - mv.visitVarInsn(ASTORE, appCorrelationId); - - // inject headers - mv.visitVarInsn(ALOAD, 2); - mv.visitLdcInsn("Request-Id"); - mv.visitVarInsn(ALOAD, childIdLocal); - mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + // This byte code instrumentation is responsible for injecting legacy AI correlation headers which include + // Request-Id and Request-Context and Correlation-Context. By default this headers are propagated if W3C + // is turned off. Please refer to generateChildDependencyId(), retrieveCorrelationContext(), + // retrieveApplicationCorrelationId() from TelemetryCorrelationUtils class for details on how these headers + // are created. + if (!isW3CEnabled) { + // generate child ID + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "generateChildDependencyId", "()Ljava/lang/String;", false); + childIdLocal = this.newLocal(Type.getType(Object.class)); + mv.visitVarInsn(ASTORE, childIdLocal); + + // retrieve correlation context + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "retrieveCorrelationContext", "()Ljava/lang/String;", false); + correlationContextLocal = this.newLocal(Type.getType(Object.class)); + mv.visitVarInsn(ASTORE, correlationContextLocal); + + // retrieve request context + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "retrieveApplicationCorrelationId", "()Ljava/lang/String;", false); + appCorrelationId = this.newLocal(Type.getType(Object.class)); + mv.visitVarInsn(ASTORE, appCorrelationId); + + // inject headers + // 2 because the 1 is the method being instrumented + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn("Request-Id"); + mv.visitVarInsn(ALOAD, childIdLocal); + mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn("Correlation-Context"); + mv.visitVarInsn(ALOAD, correlationContextLocal); + mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn("Request-Context"); + mv.visitVarInsn(ALOAD, appCorrelationId); + mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + } else { + // If W3C is enabled, we propagate Traceparent, Tracecontext headers + // to enable correlation. Please refer to generateChildDependencyTraceparent(), generateChildDependencyTraceparent() + // from TraceContextCorrelation class on how to generate this headers. + + // generate child Traceparent + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation", + "generateChildDependencyTraceparent", "()Ljava/lang/String;", false); + traceparent = this.newLocal(Type.getType(Object.class)); + mv.visitVarInsn(ASTORE, traceparent); + + mv.visitVarInsn(ALOAD, traceparent); + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation", + "createChildIdFromTraceparentString", "(Ljava/lang/String;)Ljava/lang/String;", false); + childIdLocal = this.newLocal(Type.getType(Object.class)); + mv.visitVarInsn(ASTORE, childIdLocal); + + // retrieve tracestate + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation", + "retriveTracestate", "()Ljava/lang/String;", false); + tracestate = this.newLocal(Type.getType(Object.class)); + mv.visitVarInsn(ASTORE, tracestate); + + // inject headers + // load 2nd variable because the 1st is the method being instrumented + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn("traceparent"); + mv.visitVarInsn(ALOAD, traceparent); + mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + + if (isW3CBackportEnabled) { + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn("Request-Id"); + mv.visitVarInsn(ALOAD, childIdLocal); + mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + } + + mv.visitVarInsn(ALOAD, tracestate); + Label nullLabel = new Label(); + mv.visitJumpInsn(IFNULL, nullLabel); + + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn("tracestate"); + mv.visitVarInsn(ALOAD, tracestate); + + mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); + + // skip adding tracestate + mv.visitLabel(nullLabel); + } - mv.visitVarInsn(ALOAD, 2); - mv.visitLdcInsn("Correlation-Context"); - mv.visitVarInsn(ALOAD, correlationContextLocal); - mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); - - mv.visitVarInsn(ALOAD, 2); - mv.visitLdcInsn("Request-Context"); - mv.visitVarInsn(ALOAD, appCorrelationId); - mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V", true); mv.visitVarInsn(ALOAD, 2); mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/http/HttpRequest", "getRequestLine", "()Lorg/apache/http/RequestLine;", true); @@ -153,11 +219,20 @@ protected void byteCodeForMethodExit(int opcode) { int headerValueLocal = this.newLocal(Type.getType(Object.class)); mv.visitVarInsn(ASTORE, headerValueLocal); - //generate target - mv.visitVarInsn(ALOAD, headerValueLocal); - mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "generateChildDependencyTarget", "(Ljava/lang/String;)Ljava/lang/String;", false); int targetLocal = this.newLocal(Type.getType(Object.class)); - mv.visitVarInsn(ASTORE, targetLocal); + if (!isW3CEnabled) { + //generate target + mv.visitVarInsn(ALOAD, headerValueLocal); + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils", "generateChildDependencyTarget", "(Ljava/lang/String;)Ljava/lang/String;", false); + mv.visitVarInsn(ASTORE, targetLocal); + } else { + //generate target + mv.visitVarInsn(ALOAD, headerValueLocal); + mv.visitMethodInsn(INVOKESTATIC, "com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation", + "generateChildDependencyTarget", "(Ljava/lang/String;)Ljava/lang/String;", false); + mv.visitVarInsn(ASTORE, targetLocal); + } + mv.visitFieldInsn(Opcodes.GETSTATIC, internalName, "INSTANCE", "L" + internalName + ";"); diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/http/HttpClassDataProvider.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/http/HttpClassDataProvider.java index c5aada00aa0..e894d4fbb14 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/http/HttpClassDataProvider.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/agent/http/HttpClassDataProvider.java @@ -63,6 +63,18 @@ public final class HttpClassDataProvider { private final static String REST_TEMPLATE_METTHOD = "doExecute"; + public static void setIsW3CEnabled(boolean isW3CEnabled) { + HttpClassDataProvider.isW3CEnabled = isW3CEnabled; + } + + private static boolean isW3CEnabled; + + private static boolean isW3CBackportEnabled; + + public static void setIsW3CBackportEnabled(boolean isW3CBackportEnabled) { + HttpClassDataProvider.isW3CBackportEnabled = isW3CBackportEnabled; + } + private final Map classesToInstrument; public HttpClassDataProvider(Map classesToInstrument) { @@ -80,7 +92,7 @@ public MethodVisitor create(MethodInstrumentationDecision decision, String methodName, MethodVisitor methodVisitor, ClassToMethodTransformationData additionalData) { - return new HttpClientMethodVisitor(access, desc, className, methodName, methodVisitor, additionalData); + return new HttpClientMethodVisitor(access, desc, className, methodName, methodVisitor, additionalData, isW3CEnabled, isW3CBackportEnabled); } }; diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfiguration.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfiguration.java index f7065d1fa3f..28ba58273db 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfiguration.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfiguration.java @@ -31,6 +31,8 @@ public class AgentBuiltInConfiguration { private final boolean enabled; private final boolean httpEnabled; + private final boolean w3cEnabled; + private final boolean isW3CBackportEnabled; private final boolean jdbcEnabled; private final boolean hibernateEnabled; private final boolean jedisEnabled; @@ -43,6 +45,8 @@ public class AgentBuiltInConfiguration { public AgentBuiltInConfiguration(boolean enabled, List simpleBuiltInClasses, boolean httpEnabled, + boolean w3cEnabled, + boolean isW3CBackportEnabled, boolean jdbcEnabled, boolean hibernateEnabled, boolean jedisEnabled, @@ -53,6 +57,8 @@ public AgentBuiltInConfiguration(boolean enabled, this.simpleBuiltInClasses = simpleBuiltInClasses; this.enabled = enabled; this.httpEnabled = httpEnabled; + this.w3cEnabled = w3cEnabled; + this.isW3CBackportEnabled = isW3CBackportEnabled; this.jdbcEnabled = jdbcEnabled; this.hibernateEnabled = hibernateEnabled; this.jmxEnabled = jmxEnabled; @@ -97,6 +103,14 @@ public boolean isJmxEnabled() { return jmxEnabled; } + public boolean isW3cEnabled() { + return w3cEnabled; + } + + public boolean isW3CBackportEnabled() { + return isW3CBackportEnabled; + } + public DataOfConfigurationForException getDataOfConfigurationForException() { return dataOfConfigurationForException; } diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfigurationBuilder.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfigurationBuilder.java index 7e8fae44d75..0a4306a3e2f 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfigurationBuilder.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/AgentBuiltInConfigurationBuilder.java @@ -23,6 +23,7 @@ import com.microsoft.applicationinsights.agent.internal.agent.ClassInstrumentationData; +import com.microsoft.applicationinsights.internal.logger.InternalLogger; import java.util.List; /** @@ -35,6 +36,8 @@ public class AgentBuiltInConfigurationBuilder { private boolean hibernateEnabled = false; private boolean jedisEnabled = false; private boolean jmxEnabled = false; + private boolean w3cEnabled = false; + private boolean isW3CBackportEnabled = true; private long jedisThresholdInMS = 10000L; private Long maxSqlQueryLimitInMS = 10000L; private DataOfConfigurationForException dataOfConfigurationForException = new DataOfConfigurationForException(); @@ -45,9 +48,14 @@ public AgentBuiltInConfiguration create() { this.dataOfConfigurationForException.setEnabled(false); } + InternalLogger.INSTANCE.trace(String.format("Outbound W3C tracing is enabled : %s", w3cEnabled)); + InternalLogger.INSTANCE.trace(String.format("Outbound W3C backport mode is enabled : %s", isW3CBackportEnabled)); + return new AgentBuiltInConfiguration(enabled, simpleBuiltInClasses, httpEnabled && enabled, + w3cEnabled && enabled, + isW3CBackportEnabled && enabled, jdbcEnabled && enabled, hibernateEnabled && enabled, jedisEnabled && enabled, @@ -62,8 +70,11 @@ public AgentBuiltInConfigurationBuilder setEnabled(boolean enabled) { return this; } - public AgentBuiltInConfigurationBuilder setHttpEnabled(boolean httpEnabled) { + public AgentBuiltInConfigurationBuilder setHttpEnabled(boolean httpEnabled, boolean w3cEnabled, + boolean isW3CBackportEnabled) { this.httpEnabled = httpEnabled; + this.w3cEnabled = w3cEnabled; + this.isW3CBackportEnabled = isW3CBackportEnabled; return this; } diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlAgentConfigurationBuilder.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlAgentConfigurationBuilder.java index f18d679c9e4..93322ebcd0d 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlAgentConfigurationBuilder.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlAgentConfigurationBuilder.java @@ -69,6 +69,8 @@ final class XmlAgentConfigurationBuilder implements AgentConfigurationBuilder { @VisibleForTesting final static String SDK_LOGGER_NUMBER_OF_TOTAL_SIZE_IN_MB = "NumberOfTotalSizeInMB"; private final static long JEDIS_ARGS_THRESHOLD_IN_MS = 10000L; + private final static String W3C_ENABLED = "W3C"; + private final static String W3C_BACKCOMPAT_PARAMETER = "enableW3CBackCompat"; private final static String EXCLUDED_PREFIXES_TAG = "ExcludedPrefixes"; private final static String FORBIDDEN_PREFIX_TAG = "Prefix"; @@ -223,7 +225,10 @@ private void setBuiltInInstrumentation(AgentConfigurationDefaultImpl agentConfig new ConfigRuntimeExceptionDataBuilder().setRuntimeExceptionData(builtInElement, builtInConfigurationBuilder); nodes = builtInElement.getElementsByTagName(HTTP_TAG); - builtInConfigurationBuilder.setHttpEnabled(XmlParserUtils.getEnabled(XmlParserUtils.getFirst(nodes), HTTP_TAG)); + Element httpElement = XmlParserUtils.getFirst(nodes); + boolean isW3CEnabled = XmlParserUtils.w3cEnabled(httpElement, W3C_ENABLED, false); + boolean isW3CBackportEnabled =XmlParserUtils.w3cEnabled(httpElement, W3C_BACKCOMPAT_PARAMETER, true); + builtInConfigurationBuilder.setHttpEnabled(XmlParserUtils.getEnabled(element, HTTP_TAG),isW3CEnabled, isW3CBackportEnabled); nodes = builtInElement.getElementsByTagName(JDBC_TAG); builtInConfigurationBuilder.setJdbcEnabled(XmlParserUtils.getEnabled(XmlParserUtils.getFirst(nodes), JDBC_TAG)); diff --git a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlParserUtils.java b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlParserUtils.java index 805abb3a22d..0b5efd1d60f 100644 --- a/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlParserUtils.java +++ b/agent/src/main/java/com/microsoft/applicationinsights/agent/internal/config/XmlParserUtils.java @@ -23,6 +23,7 @@ import com.microsoft.applicationinsights.agent.internal.common.StringUtils; import com.microsoft.applicationinsights.internal.logger.InternalLogger; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -59,6 +60,28 @@ public static boolean getEnabled(Element element, String attributeName) { return getEnabled(element, attributeName, true); } + /** + * Method to get the attribute value for W3C + * @param element + * @param attributeName + * @return boolean + */ + static boolean w3cEnabled(Element element, String attributeName, boolean defaultValue) { + try { + String strValue = element.getAttribute(attributeName); + if (!StringUtils.isNullOrEmpty(strValue)) { + boolean value = Boolean.valueOf(strValue); + return value; + } + return defaultValue; + + } catch (Exception e) { + InternalLogger.INSTANCE.error("cannot parse the correlation format, will default" + + "to AI proprietory correlation", ExceptionUtils.getStackTrace(e)); + } + return defaultValue; + } + public static boolean getEnabled(Element element, String elementName, boolean defaultValue) { if (element == null) { return true; diff --git a/azure-application-insights-spring-boot-starter/README.md b/azure-application-insights-spring-boot-starter/README.md index d71d3a08f91..8c55c668cf5 100644 --- a/azure-application-insights-spring-boot-starter/README.md +++ b/azure-application-insights-spring-boot-starter/README.md @@ -124,6 +124,9 @@ azure.application-insights.enabled=true # Enable/Disable web modules. Default value: true. azure.application-insights.web.enabled=true +# Enable/Disable W3C correlation protocol. Defaul value: false +azure.application-insights.web.w3c=true + # Logging type [console, file]. Default value: console. azure.application-insights.logger.type=console # Logging level [all, trace, info, warn, error, off]. Default value: error. diff --git a/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsProperties.java b/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsProperties.java index b8f303d1c63..293959664ed 100644 --- a/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsProperties.java +++ b/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsProperties.java @@ -407,6 +407,25 @@ static class Web { */ private boolean enabled = true; + /** + * Flag to enable/disable enableW3C headers. It is disabled by default. + */ + private boolean enableW3C = false; + + /** + * Flag to enable backward compatibility mode for W3C. By default this is + * enabled. + */ + private boolean enableW3CBackcompatMode = true; + + public boolean isEnableW3CBackcompatMode() { + return enableW3CBackcompatMode; + } + + public void setEnableW3CBackcompatMode(boolean enableW3CBackcompatMode) { + this.enableW3CBackcompatMode = enableW3CBackcompatMode; + } + public boolean isEnabled() { return enabled; } @@ -414,6 +433,14 @@ public boolean isEnabled() { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public boolean isEnableW3C() { + return enableW3C; + } + + public void setEnableW3C(boolean enableW3C) { + this.enableW3C = enableW3C; + } } public static class QuickPulse { diff --git a/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsWebModuleConfiguration.java b/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsWebModuleConfiguration.java index 98c067b25ca..bd5451dc4ef 100644 --- a/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsWebModuleConfiguration.java +++ b/azure-application-insights-spring-boot-starter/src/main/java/com/microsoft/applicationinsights/autoconfigure/ApplicationInsightsWebModuleConfiguration.java @@ -21,6 +21,7 @@ package com.microsoft.applicationinsights.autoconfigure; +import com.microsoft.applicationinsights.internal.logger.InternalLogger; import com.microsoft.applicationinsights.web.extensibility.initializers.WebOperationIdTelemetryInitializer; import com.microsoft.applicationinsights.web.extensibility.initializers.WebOperationNameTelemetryInitializer; import com.microsoft.applicationinsights.web.extensibility.initializers.WebSessionTelemetryInitializer; @@ -30,6 +31,7 @@ import com.microsoft.applicationinsights.web.extensibility.modules.WebSessionTrackingTelemetryModule; import com.microsoft.applicationinsights.web.extensibility.modules.WebUserTrackingTelemetryModule; import com.microsoft.applicationinsights.web.internal.perfcounter.WebPerformanceCounterModule; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; @@ -52,14 +54,29 @@ @ConditionalOnWebApplication class ApplicationInsightsWebModuleConfiguration { - /** + + /** + * Instance for the container of ApplicationInsights Properties + */ + private ApplicationInsightsProperties applicationInsightsProperties; + + @Autowired + public ApplicationInsightsWebModuleConfiguration(ApplicationInsightsProperties properties) { + this.applicationInsightsProperties = properties; + } + + /** * Bean for WebRequestTrackingTelemetryModule * @return instance of {@link WebRequestTrackingTelemetryModule} */ @Bean @ConditionalOnProperty(value = "azure.application-insights.default-modules.WebRequestTrackingTelemetryModule.enabled", havingValue = "true", matchIfMissing = true) WebRequestTrackingTelemetryModule webRequestTrackingTelemetryModule() { - return new WebRequestTrackingTelemetryModule(); + WebRequestTrackingTelemetryModule w = new WebRequestTrackingTelemetryModule(); + w.isW3CEnabled = applicationInsightsProperties.getWeb().isEnableW3C(); + InternalLogger.INSTANCE.trace(String.format("Inbound W3C tracing is enabled %s", w.isW3CEnabled)); + w.setEnableBackCompatibilityForW3C(applicationInsightsProperties.getWeb().isEnableW3CBackcompatMode()); + return w; } /** diff --git a/core/src/main/java/com/microsoft/applicationinsights/internal/agent/CoreAgentNotificationsHandler.java b/core/src/main/java/com/microsoft/applicationinsights/internal/agent/CoreAgentNotificationsHandler.java index 5c89aca0b6c..1e22a39eb8f 100644 --- a/core/src/main/java/com/microsoft/applicationinsights/internal/agent/CoreAgentNotificationsHandler.java +++ b/core/src/main/java/com/microsoft/applicationinsights/internal/agent/CoreAgentNotificationsHandler.java @@ -159,13 +159,15 @@ public void httpMethodFinished(String identifier, String method, String correlat telemetry.setTimestamp(dependencyStartTime); telemetry.setId(correlationId); telemetry.setResultCode(Integer.toString(result)); - telemetry.setType("HTTP"); + telemetry.setType("Http (tracked component)"); // For Backward Compatibility telemetry.getContext().getProperties().put("URI", uri); telemetry.getContext().getProperties().put("Method", method); if (target != null && !target.isEmpty()) { + // AI correlation expects target to be of this format. + target = createTarget(uriObject, target); if (telemetry.getTarget() == null) { telemetry.setTarget(target); } else { @@ -177,6 +179,22 @@ public void httpMethodFinished(String identifier, String method, String correlat telemetryClient.track(telemetry); } + /** + * This is used to create Target string to be set in the RDD Telemetry + * According to spec, we do not include port 80 and 443 in target + * @param uriObject + * @return + */ + private String createTarget(URI uriObject, String incomingTarget) { + assert uriObject != null; + String target = uriObject.getHost(); + if (uriObject.getPort() != 80 && uriObject.getPort() != 443) { + target += ":" + uriObject.getPort(); + } + target += " | " + incomingTarget; + return target; + } + @Override public void jedisMethodStarted(String name) { int index = name.lastIndexOf('#'); diff --git a/web/src/main/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModule.java b/web/src/main/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModule.java index ce6cf6469a5..fcbe4db6ae7 100644 --- a/web/src/main/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModule.java +++ b/web/src/main/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModule.java @@ -21,22 +21,24 @@ package com.microsoft.applicationinsights.web.extensibility.modules; -import java.util.Date; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import com.microsoft.applicationinsights.common.CommonUtils; -import com.microsoft.applicationinsights.web.internal.ApplicationInsightsHttpResponseWrapper; import com.microsoft.applicationinsights.TelemetryClient; import com.microsoft.applicationinsights.TelemetryConfiguration; +import com.microsoft.applicationinsights.common.CommonUtils; import com.microsoft.applicationinsights.extensibility.TelemetryModule; import com.microsoft.applicationinsights.internal.logger.InternalLogger; import com.microsoft.applicationinsights.telemetry.Duration; import com.microsoft.applicationinsights.telemetry.RequestTelemetry; +import com.microsoft.applicationinsights.web.internal.ApplicationInsightsHttpResponseWrapper; import com.microsoft.applicationinsights.web.internal.RequestTelemetryContext; import com.microsoft.applicationinsights.web.internal.ThreadContext; import com.microsoft.applicationinsights.web.internal.correlation.TelemetryCorrelationUtils; +import com.microsoft.applicationinsights.web.internal.correlation.TraceContextCorrelation; +import java.util.Date; +import java.util.Map; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * Created by yonisha on 2/2/2015. @@ -48,10 +50,57 @@ public class WebRequestTrackingTelemetryModule implements WebTelemetryModule, Te private TelemetryClient telemetryClient; private boolean isInitialized = false; + public boolean isW3CEnabled = false; + + /** + * Tag to indicate if W3C tracing protocol is enabled. + */ + private final String W3C_CONFIGURATION_PARAMETER = "W3CEnabled"; + + /** + * Tag to indicate if backward compatibility is turned ON/OFF for W3C. + * By default backward compatibility mode is turned ON. + */ + private final String W3C_BACKCOMPAT_PARAMETER = "enableW3CBackCompat"; + // endregion Members // region Public + public WebRequestTrackingTelemetryModule() { + + } + + /** + * Ctor that parses incoming configuration. + * @param configurationData + */ + public WebRequestTrackingTelemetryModule(Map configurationData) { + assert configurationData != null; + + if (configurationData.containsKey(W3C_CONFIGURATION_PARAMETER)) { + isW3CEnabled = Boolean.valueOf(configurationData.get(W3C_CONFIGURATION_PARAMETER)); + InternalLogger.INSTANCE.trace(String.format("Inbound W3C tracing mode is enabled %s", isW3CEnabled)); + } + + if (configurationData.containsKey(W3C_BACKCOMPAT_PARAMETER)) { + boolean enableBackCompatibilityForW3C = Boolean.valueOf(configurationData.get( + W3C_BACKCOMPAT_PARAMETER + )); + TraceContextCorrelation.setIsW3CBackCompatEnabled(enableBackCompatibilityForW3C); + } + + + } + + /** + * Used for SpringBoot setttings to propogate the switch for W3C to TracecontextCorrelation class + * @param enableBackCompatibilityForW3C + */ + public void setEnableBackCompatibilityForW3C(boolean enableBackCompatibilityForW3C) { + TraceContextCorrelation.setIsW3CBackCompatEnabled(enableBackCompatibilityForW3C); + } + /** * Begin request processing. * @param req The request to process @@ -94,7 +143,13 @@ public void onBeginRequest(ServletRequest req, ServletResponse res) { // Look for cross-component correlation headers and resolve correlation ID's HttpServletResponse response = (HttpServletResponse) res; - TelemetryCorrelationUtils.resolveCorrelation(request, response, telemetry); + if (isW3CEnabled) { + TraceContextCorrelation.resolveCorrelation(request, response, telemetry); + } else { + // Default correlation experience + TelemetryCorrelationUtils.resolveCorrelation(request, response, telemetry); + } + } catch (Exception e) { String moduleClassName = this.getClass().getSimpleName(); @@ -132,7 +187,11 @@ public void onEndRequest(ServletRequest req, ServletResponse res) { telemetry.setDuration(new Duration(endTime - context.getRequestStartTimeTicks())); String instrumentationKey = this.telemetryClient.getContext().getInstrumentationKey(); - TelemetryCorrelationUtils.resolveRequestSource((HttpServletRequest) req, telemetry, instrumentationKey); + if (isW3CEnabled) { + TraceContextCorrelation.resolveRequestSource((HttpServletRequest) req, telemetry, instrumentationKey); + } else { + TelemetryCorrelationUtils.resolveRequestSource((HttpServletRequest) req, telemetry, instrumentationKey); + } telemetryClient.track(telemetry); } catch (Exception e) { diff --git a/web/src/main/java/com/microsoft/applicationinsights/web/internal/RequestTelemetryContext.java b/web/src/main/java/com/microsoft/applicationinsights/web/internal/RequestTelemetryContext.java index 3f3d7753e35..b0dbe26ea2e 100644 --- a/web/src/main/java/com/microsoft/applicationinsights/web/internal/RequestTelemetryContext.java +++ b/web/src/main/java/com/microsoft/applicationinsights/web/internal/RequestTelemetryContext.java @@ -21,6 +21,7 @@ package com.microsoft.applicationinsights.web.internal; +import com.microsoft.applicationinsights.web.internal.correlation.tracecontext.Tracestate; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.http.HttpServletRequest; @@ -40,6 +41,8 @@ public class RequestTelemetryContext { private boolean isNewSession = false; private HttpServletRequest servletRequest; private final CorrelationContext correlationContext; + private Tracestate tracestate; + private int traceflag; private final AtomicInteger currentChildId = new AtomicInteger(); /** @@ -50,6 +53,15 @@ public RequestTelemetryContext(long ticks) { this(ticks, null); } + public Tracestate getTracestate() { + return tracestate; + } + + public void setTracestate( + Tracestate tracestate) { + this.tracestate = tracestate; + } + /** * Constructs new RequestTelemetryContext object. * @param ticks The time in ticks @@ -62,6 +74,14 @@ public RequestTelemetryContext(long ticks, HttpServletRequest servletRequest) { correlationContext = new CorrelationContext(); } + public int getTraceflag() { + return traceflag; + } + + public void setTraceflag(int traceflag) { + this.traceflag = traceflag; + } + /** * Gets the correlation context associated with the request * @return The correlation context map. diff --git a/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils.java b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils.java index e39867f5065..6de8c3b7a02 100644 --- a/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils.java +++ b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TelemetryCorrelationUtils.java @@ -375,7 +375,7 @@ private static Map getPropertyBag(String baggage) { } - private static String extractRootId(String parentId) { + static String extractRootId(String parentId) { // ported from .NET's System.Diagnostics.Activity.cs implementation: // https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs diff --git a/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation.java b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation.java new file mode 100644 index 00000000000..b1730392f55 --- /dev/null +++ b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelation.java @@ -0,0 +1,508 @@ +package com.microsoft.applicationinsights.web.internal.correlation; + +import com.google.common.base.Joiner; +import com.microsoft.applicationinsights.TelemetryConfiguration; +import com.microsoft.applicationinsights.internal.logger.InternalLogger; +import com.microsoft.applicationinsights.telemetry.RequestTelemetry; +import com.microsoft.applicationinsights.web.internal.RequestTelemetryContext; +import com.microsoft.applicationinsights.web.internal.ThreadContext; +import com.microsoft.applicationinsights.web.internal.correlation.tracecontext.Traceparent; +import com.microsoft.applicationinsights.web.internal.correlation.tracecontext.Tracestate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * A class that is responsible for performing correlation based on W3C protocol. + * This is a clean implementation of W3C protocol and doesn't have the backward + * compatibility with AI-RequestId protocol. + * + * @author Dhaval Doshi + */ +public class TraceContextCorrelation { + + public static final String TRACEPARENT_HEADER_NAME = "traceparent"; + public static final String TRACESTATE_HEADER_NAME = "tracestate"; + public static final String REQUEST_CONTEXT_HEADER_NAME = "Request-Context"; + public static final String AZURE_TRACEPARENT_COMPONENT_INITIAL = "az"; + public static final String REQUEST_CONTEXT_HEADER_APPID_KEY = "appId"; + + /** + * Switch to enable W3C Backward compatibility with Legacy AI Correlation. + * By default this is turned ON. + */ + private static boolean isW3CBackCompatEnabled = true; + + /** + * Private constructor as we don't expect to create an object of this class. + */ + private TraceContextCorrelation() {} + + /** + * This method is responsible to perform correlation for incoming request by populating it's + * traceId, spanId and parentId. It also stores incoming tracestate into ThreadLocal for downstream + * propagation. + * @param request + * @param response + * @param requestTelemetry + */ + public static void resolveCorrelation(HttpServletRequest request, HttpServletResponse response, + RequestTelemetry requestTelemetry) { + + try { + if (request == null) { + InternalLogger.INSTANCE.error("Failed to resolve correlation. request is null."); + return; + } + + if (response == null) { + InternalLogger.INSTANCE.error("Failed to resolve correlation. response is null."); + return; + } + + if (requestTelemetry == null) { + InternalLogger.INSTANCE.error("Failed to resolve correlation. requestTelemetry is null."); + return; + } + + Traceparent incomingTraceparent = extractIncomingTraceparent(request); + Traceparent processedTraceParent = processIncomingTraceparent(incomingTraceparent, request); + + // represents the id of the current request. + requestTelemetry.setId("|" + processedTraceParent.getTraceId() + "." + processedTraceParent.getSpanId() + + "."); + + // represents the trace-id of this distributed trace + requestTelemetry.getContext().getOperation().setId(processedTraceParent.getTraceId()); + + // assign parent id + if (incomingTraceparent != null) { + requestTelemetry.getContext().getOperation().setParentId("|" + processedTraceParent.getTraceId() + "." + + incomingTraceparent.getSpanId() + "."); + } else { + // set parentId only if not already set (legacy processing can set it) + if (requestTelemetry.getContext().getOperation().getParentId() == null) { + requestTelemetry.getContext().getOperation().setParentId(null); + } + } + + // Propagate trace-flags + ThreadContext.getRequestTelemetryContext().setTraceflag(processedTraceParent.getTraceFlags()); + + String appId = getAppId(); + + // Get Tracestate header + Tracestate tracestate = getTracestate(request, incomingTraceparent, appId); + + // add tracestate to threadlocal + ThreadContext.getRequestTelemetryContext().setTracestate(tracestate); + + // Let the callee know the caller's AppId + addTargetAppIdInResponseHeaderViaRequestContext(response); + + } catch (java.lang.Exception e) { + InternalLogger.INSTANCE.error("unable to perform correlation :%s", ExceptionUtils. + getStackTrace(e)); + } + } + + /** + * Helper method to create extract Incoming Traceparent header. This method can return null. + * @param request + * @return Incoming Traceparent + */ + private static Traceparent extractIncomingTraceparent(HttpServletRequest request) { + Traceparent incomingTraceparent = null; + + Enumeration traceparents = request.getHeaders(TRACEPARENT_HEADER_NAME); + List traceparentList = getEnumerationAsCollection(traceparents); + + // W3C spec mandates a request should exactly have 1 Traceparent header + if (traceparentList.size() != 1) { + return null; + } + + try { + incomingTraceparent = Traceparent.fromString(traceparentList.get(0)); + } catch (Exception e) { + InternalLogger.INSTANCE.error(String.format("Received invalid traceparent header with exception %s, " + + "distributed trace might be broken", ExceptionUtils.getStackTrace(e))); + } + return incomingTraceparent; + } + + /** + * This method takes incoming traceparent object and creates a new outbound traceparent object + * @param incomingTraceparent + * @return + */ + private static Traceparent processIncomingTraceparent(Traceparent incomingTraceparent, + HttpServletRequest request) { + + Traceparent processedTraceparent = null; + + // If incoming traceparent is null create a new Traceparent + if (incomingTraceparent == null) { + + // If BackCompt mode is enabled, read the Request-Id Header + if (isW3CBackCompatEnabled) { + processedTraceparent = processLegacyCorrelation(request); + } + + if (processedTraceparent == null){ + processedTraceparent = new Traceparent(); + } + + } else { + // create outbound traceparent inheriting traceId, flags from parent. + processedTraceparent = new Traceparent(0, incomingTraceparent.getTraceId(), null, + incomingTraceparent.getTraceFlags()); + } + return processedTraceparent; + } + + /** + * This method processes the legacy Request-ID header for backward compatibility. + * @param request + * @return + */ + private static Traceparent processLegacyCorrelation(HttpServletRequest request) { + + String requestId = request.getHeader(TelemetryCorrelationUtils.CORRELATION_HEADER_NAME); + + try { + if (requestId != null && !requestId.isEmpty()) { + String legacyOperationId = TelemetryCorrelationUtils.extractRootId(requestId); + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + requestTelemetry.getContext().getProperties().putIfAbsent("ai_legacyRootID", legacyOperationId); + requestTelemetry.getContext().getOperation().setParentId(requestId); + return new Traceparent(0, legacyOperationId, null, 0); + } + } catch (Exception e) { + InternalLogger.INSTANCE.error(String.format("unable to create traceparent from legacy request-id header" + + " %s", ExceptionUtils.getStackTrace(e))); + } + + return null; + } + + /** + * Helper method that extracts tracestate header from request if available and add's Azure component + * to it. If tracestate is not available, a new tracestate with Azure component is created. + * @param request + * @param incomingTraceparent + * @param appId + * @return Tracestate + */ + private static Tracestate getTracestate(HttpServletRequest request, Traceparent incomingTraceparent, String appId) { + + Tracestate tracestate= null; + + if (incomingTraceparent != null) { + Enumeration tracestates = request.getHeaders(TRACESTATE_HEADER_NAME); + List tracestateList = getEnumerationAsCollection(tracestates); + try { + //create tracestate from incoming header + tracestate = Tracestate.fromString(Joiner.on(",").join(tracestateList)); + // add appId to it if it's resolved + if (appId != null && !appId.isEmpty()) { + tracestate = new Tracestate(tracestate, AZURE_TRACEPARENT_COMPONENT_INITIAL, + appId); + } + + } catch (Exception e) { + InternalLogger.INSTANCE.error(String.format("Cannot parse incoming tracestate %s", + ExceptionUtils.getStackTrace(e))); + try { + // Pass new tracestate if received invalid tracestate + if (appId != null && !appId.isEmpty()) { + tracestate = new Tracestate(null, AZURE_TRACEPARENT_COMPONENT_INITIAL, appId); + } + } catch (Exception ex) { + InternalLogger.INSTANCE.error(String.format("Cannot create default tracestate %s", + ExceptionUtils.getStackTrace(ex))); + } + } + } else { + // pass new tracestate if incoming traceparent is empty + try { + if (appId != null && !appId.isEmpty()) { + tracestate = new Tracestate(null, AZURE_TRACEPARENT_COMPONENT_INITIAL, appId); + } + } catch (Exception e) { + InternalLogger.INSTANCE.error(String.format("cannot create default traceparent %s", + ExceptionUtils.getStackTrace(e))); + } + } + return tracestate; + } + + /** + * Returns collection from Enumeration + * @param e + * @return List of headers + */ + private static List getEnumerationAsCollection(Enumeration e) { + + List list = new ArrayList<>(); + while (e.hasMoreElements()) { + list.add(e.nextElement()); + } + return list; + } + + /** + * This adds the Request-Context in response header so that the Callee can know what is the caller's AppId. + * @param response HttpResponse object + */ + private static void addTargetAppIdInResponseHeaderViaRequestContext(HttpServletResponse response) { + + if (response.containsHeader(REQUEST_CONTEXT_HEADER_NAME)) { + return; + } + + String appId = getAppIdWithKey(); + if (appId.isEmpty()) { + return; + } + + // W3C protocol doesn't define any behavior for response headers. + // This is purely AI concept and hence we use RequestContextHeader here. + response.addHeader(REQUEST_CONTEXT_HEADER_NAME,appId); + } + + /** + * Gets AppId prefixed with key to append to Request-Context header + * @return + */ + private static String getAppIdWithKey() { + return REQUEST_CONTEXT_HEADER_APPID_KEY + "=" + getAppId(); + } + + /** + * Retrieves the appId for the current active config's instrumentation key. + */ + public static String getAppId() { + + String instrumentationKey = TelemetryConfiguration.getActive().getInstrumentationKey(); + String appId = InstrumentationKeyResolver.INSTANCE.resolveInstrumentationKey(instrumentationKey); + + //it's possible the appId returned is null (e.g. async task is still pending or has failed). In this case, just + //return and let the next request resolve the ikey. + if (appId == null) { + InternalLogger.INSTANCE.trace("Application correlation Id could not be retrieved (e.g. task may be pending or failed)"); + return ""; + } + + return appId; + } + + /** + * Resolves the source of a request based on request header information and the appId of the current + * component, which is retrieved via a query to the AppInsights service. + * @param request The servlet request. + * @param requestTelemetry The request telemetry in which source will be populated. + * @param instrumentationKey The instrumentation key for the current component. + */ + public static void resolveRequestSource(HttpServletRequest request, RequestTelemetry requestTelemetry, String instrumentationKey) { + + try { + + if (request == null) { + InternalLogger.INSTANCE.error("Failed to resolve correlation. request is null."); + return; + } + + if (instrumentationKey == null || instrumentationKey.isEmpty()) { + InternalLogger.INSTANCE.error("Failed to resolve correlation. InstrumentationKey is null or empty."); + return; + } + + if (requestTelemetry == null) { + InternalLogger.INSTANCE.error("Failed to resolve correlation. requestTelemetry is null."); + return; + } + + if (requestTelemetry.getSource() != null) { + InternalLogger.INSTANCE.trace("Skip resolving request source as it is already initialized."); + return; + } + + String tracestate = request.getHeader(TRACESTATE_HEADER_NAME); + if (tracestate == null || tracestate.isEmpty()) { + + if (isW3CBackCompatEnabled && + request.getHeader(TelemetryCorrelationUtils.REQUEST_CONTEXT_HEADER_NAME) != null) { + InternalLogger.INSTANCE.trace("Tracestate absent, In backward compatibility mode, will try to resolve " + + "request-context"); + TelemetryCorrelationUtils.resolveRequestSource(request, requestTelemetry, instrumentationKey); + return; + } + InternalLogger.INSTANCE.info("Skip resolving request source as the following header was not found: %s", + TRACESTATE_HEADER_NAME); + return; + } + + Tracestate incomingTracestate = Tracestate.fromString(tracestate); + + String source = generateSourceTargetCorrelation(instrumentationKey, + incomingTracestate.get(AZURE_TRACEPARENT_COMPONENT_INITIAL)); + + // Set the source of this request telemetry which would be equal to AppId of the caller if + // it's different from current AppId or else null. + requestTelemetry.setSource(source); + + } + catch(Exception ex) { + InternalLogger.INSTANCE.error("Failed to resolve request source. Exception information: %s", + ExceptionUtils.getStackTrace(ex)); + } + } + + + /** + * Generates the target appId to add to Outbound call + * @param requestContext + * @return + */ + public static String generateChildDependencyTarget(String requestContext) { + if (requestContext == null || requestContext.isEmpty()) { + InternalLogger.INSTANCE.trace("generateChildDependencyTarget: won't continue as requestContext is null or empty."); + return ""; + } + + String instrumentationKey = TelemetryConfiguration.getActive().getInstrumentationKey(); + if (instrumentationKey == null || instrumentationKey.isEmpty()) { + InternalLogger.INSTANCE.error("Failed to generate target correlation. InstrumentationKey is null or empty."); + return ""; + } + + // In W3C we only pass requestContext for the response. So it's expected to have only single key-value pair + String[] keyValue = requestContext.split("="); + assert keyValue.length == 2; + + String headerAppID = null; + if (keyValue[0].equals(REQUEST_CONTEXT_HEADER_APPID_KEY)) { + headerAppID = keyValue[1]; + } + + String currAppId = InstrumentationKeyResolver.INSTANCE.resolveInstrumentationKey(TelemetryConfiguration.getActive() + .getInstrumentationKey()); + + String target = resolve(headerAppID, currAppId); + if (target == null) { + InternalLogger.INSTANCE.warn("Target value is null and hence returning empty string"); + return ""; // we want an empty string instead of null so it plays nicer with bytecode injection + } + return target; + } + + /** + * Extracts the appId/roleName out of Tracestate and compares it with the current appId. It then + * generates the appropriate source or target. + */ + private static String generateSourceTargetCorrelation(String instrumentationKey, String appId) { + + assert instrumentationKey != null; + assert appId != null; + + String myAppId = InstrumentationKeyResolver.INSTANCE.resolveInstrumentationKey(instrumentationKey); + + return resolve(appId, myAppId); + } + + /** + * Resolves appId based on appId passed in header and current appId + * @param headerAppId + * @param currentAppId + * @return + */ + private static String resolve(String headerAppId, String currentAppId) { + + //it's possible the appId returned is null (e.g. async task is still pending or has failed). In this case, just + //return and let the next request resolve the ikey. + if (currentAppId == null) { + InternalLogger.INSTANCE.trace("Could not generate source/target correlation as the appId could not be resolved (e.g. task may be pending or failed)"); + return null; + } + + // if the current appId and the incoming appId are send null + String result = null; + if (headerAppId != null && !headerAppId.equals(currentAppId)) { + result = headerAppId; + } + + return result; + } + + /** + * Helper method to retrieve Tracestate from ThreadLocal + * @return + */ + public static String retriveTracestate() { + //check if context is null - no correlation will happen + if (ThreadContext.getRequestTelemetryContext() == null || ThreadContext.getRequestTelemetryContext(). + getTracestate() == null) { + InternalLogger.INSTANCE.warn("No correlation wil happen, Thread context is null"); + return null; + } + + Tracestate tracestate = ThreadContext.getRequestTelemetryContext().getTracestate(); + return tracestate.toString(); + } + + /** + * Generates child TraceParent by retrieving values from ThreadLocal. + * @return Outbound Traceparent + */ + public static String generateChildDependencyTraceparent() { + try { + + RequestTelemetryContext context = ThreadContext.getRequestTelemetryContext(); + + //check if context is null, no incoming request is present. + // This is likely worker role scenario, where a worker is trying + // to create a new outbound call, so generate a new traceparent. + if (context == null) { + return new Traceparent().toString(); + } + + RequestTelemetry requestTelemetry = context.getHttpRequestTelemetry(); + String traceId = requestTelemetry.getContext().getOperation().getId(); + + Traceparent tp = new Traceparent(0, traceId, null, context.getTraceflag()); + + // We need to propagate full blown traceparent header. + return tp.toString(); + } + catch (Exception ex) { + InternalLogger.INSTANCE.error("Failed to generate child ID. Exception information: %s", ex.toString()); + InternalLogger.INSTANCE.trace("Stack trace generated is %s", ExceptionUtils.getStackTrace(ex)); + } + + return null; + } + + /** + * This is helper method to convert traceparent (W3C) format to AI legacy format for supportability + * @param traceparent + * @return legacy format traceparent + */ + public static String createChildIdFromTraceparentString(String traceparent) { + assert traceparent != null; + + String[] traceparentArr = traceparent.split("-"); + assert traceparentArr.length == 4; + + return "|" + traceparentArr[1] + "." + traceparentArr[2] + "."; + } + + public static void setIsW3CBackCompatEnabled(boolean isW3CBackCompatEnabled) { + TraceContextCorrelation.isW3CBackCompatEnabled = isW3CBackCompatEnabled; + InternalLogger.INSTANCE.trace(String.format("W3C Backport mode enabled on Incoming side %s", + isW3CBackCompatEnabled)); + } +} diff --git a/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/Traceparent.java b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/Traceparent.java new file mode 100644 index 00000000000..0474c61f118 --- /dev/null +++ b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/Traceparent.java @@ -0,0 +1,188 @@ +package com.microsoft.applicationinsights.web.internal.correlation.tracecontext; + +import java.util.concurrent.ThreadLocalRandom; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.http.annotation.Experimental; + +/** + * This class represents the Traceparent data structure based on + * + * @author Reiley Yang + * @author Dhaval Doshi + * @see Trace Context + */ +@Experimental +public class Traceparent { + + /** + * Version number between range [0,255] inclusive + */ + @VisibleForTesting + final int version; + + /** + * 16 byte trace-id that is used to uniquely identify a distributed trace + */ + @VisibleForTesting + final String traceId; + + /** + * It is a 8 byte ID that represents the caller span + */ + @VisibleForTesting + final String spanId; + + /** + * An 8-bit field that controls tracing flags such as sampling, trace level etc. + */ + @VisibleForTesting + final int traceFlags; + + private Traceparent(int version, String traceId, String spanId, int traceFlags, boolean check) { + if (check) { + validate(version, traceId, spanId, traceFlags); + } + this.version = version; + this.traceId = traceId; + this.spanId = spanId; + this.traceFlags = traceFlags; + } + + /** + * The constructor that tries to create Traceparent Object from given version, traceId, spanID + * and traceFlags. + */ + public Traceparent(int version, String traceId, String spanId, int traceFlags) { + this(version, traceId != null ? traceId : randomHex(16), + spanId != null ? spanId : randomHex(8), + traceFlags, true); + } + + /** + * This constructor creates a new Traceparent object having new traceId. It should only be used + * if the call is the starting point of distributed trace. + */ + public Traceparent() { + this(0, randomHex(16), randomHex(8), 0, false); + } + + public String getTraceId() { + return traceId; + } + + public int getTraceFlags() { + return traceFlags; + } + + public String getSpanId() { + return spanId; + } + + /** + * Validates the given input based on W3C specifications. + */ + private static void validate(int version, String traceId, String spanId, int traceFlags) + throws IllegalArgumentException { + if (version < 0 || version > 254) { + throw new IllegalArgumentException("version must be within range [0, 255)"); + } + if (!isHex(traceId, 32)) { + throw new IllegalArgumentException("invalid traceId"); + } + if (traceId.equals("00000000000000000000000000000000")) { + throw new IllegalArgumentException("invalid traceId"); + } + if (!isHex(spanId, 16)) { + throw new IllegalArgumentException("invalid spanId"); + } + if (spanId.equals("0000000000000000")) { + throw new IllegalArgumentException("invalid spanId"); + } + if (traceFlags < 0 || traceFlags > 255) { + throw new IllegalArgumentException("traceFlags must be within range [0, 255]"); + } + } + + /** + * Converts the Traceparent object to header format Eg: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 + * + * @return traceparent + */ + @Override + public String toString() { + return String.format("%02x-%s-%s-%02x", version, traceId, spanId, traceFlags); + } + + /** + * Helper method to create a random hexadecimal string of n bytes. + * + * @return n byte hexadecimal string + */ + @VisibleForTesting + static String randomHex(int n) { + byte[] bytes = new byte[n]; + ThreadLocalRandom.current().nextBytes(bytes); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * Helper method to check if a given string of n bytes is hexadecimal + * + * @return boolean + */ + private static boolean isHex(String s, int n) { + if (s == null || s.length() == 0) { + return false; + } + if (s.length() != n) { + return false; + } + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + if ('0' <= c && c <= '9') { + continue; + } + if ('a' <= c && c <= 'f') { + continue; + } + return false; + } + return true; + } + + /** + * Converts traceparent from String to Traceparent object + * + * @return Traceparent + */ + public static Traceparent fromString(String s) { + if (s == null || s.length() == 0) { + return null; + } + String[] arr = s.split("-"); + if (arr.length < 4) { + return null; + } + if (!isHex(arr[0], 2)) { + return null; + } + if (arr[0].equals("00") && arr.length > 4) { + return null; + } + if (!isHex(arr[3], 2)) { + return null; + } + + return new Traceparent( + (Character.digit(arr[0].charAt(0), 16) << 4) + Character.digit(arr[0].charAt(1), 16), + arr[1], + arr[2], + (Character.digit(arr[3].charAt(0), 16) << 4) + Character.digit(arr[3].charAt(1), 16)); + } + +} diff --git a/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/Tracestate.java b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/Tracestate.java new file mode 100644 index 00000000000..e2ed8d9976e --- /dev/null +++ b/web/src/main/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/Tracestate.java @@ -0,0 +1,141 @@ +package com.microsoft.applicationinsights.web.internal.correlation.tracecontext; + +import java.util.LinkedHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.http.annotation.Experimental; + +/** + * Class that represents Tracestate header based on + * + * @author Reiley Yang + * @author Dhaval Doshi + * @see Trace Context + * + * Implementations can add vendor specific details here. + */ +@Experimental +public class Tracestate { + + private static String KEY_WITHOUT_VENDOR_FORMAT = "[a-z][_0-9a-z\\-\\*\\/]{0,255}"; + private static String KEY_WITH_VENDOR_FORMAT = "[a-z][_0-9a-z\\-\\*\\/]{0,240}@[a-z][_0-9a-z\\-\\*\\/]{0,13}"; + private static String KEY_FORMAT = KEY_WITHOUT_VENDOR_FORMAT + "|" + KEY_WITH_VENDOR_FORMAT; + private static String VALUE_FORMAT = "[\\x20-\\x2b\\x2d-\\x3c\\x3e-\\x7e]{0,255}[\\x21-\\x2b\\x2d-\\x3c\\x3e-\\x7e]"; + + private static Pattern KEY_VALIDATION_RE = Pattern.compile("^" + KEY_FORMAT + "$"); + private static Pattern VALUE_VALIDATION_RE = Pattern.compile("^" + VALUE_FORMAT + "$"); + + private static String DELIMITER_FORMAT = "[ \\t]*,[ \\t]*"; + private static String MEMBER_FORMAT = String.format("(%s)(=)(%s)", KEY_FORMAT, VALUE_FORMAT); + + private static Pattern DELIMITER_FORMAT_RE = Pattern.compile(DELIMITER_FORMAT); + private static Pattern MEMBER_FORMAT_RE = Pattern.compile("^" + MEMBER_FORMAT + "$"); + + private static final int MAX_KEY_VALUE_PAIRS = 32; + + /** + * Internal representation of the tracestate + */ + private LinkedHashMap internalList = new LinkedHashMap<>(MAX_KEY_VALUE_PAIRS); + + /** + * String representation of the tracestate + */ + private String internalString = null; + + /** + * Ctor that creates tracestate object from given value + */ + + public Tracestate(String input) { + if (input == null) { + throw new IllegalArgumentException("input is null"); + } + + String[] values = DELIMITER_FORMAT_RE.split(input); + for (String item : values) { + Matcher m = MEMBER_FORMAT_RE.matcher(item); + if (!m.find()) { + throw new IllegalArgumentException(String.format("invalid string %s in tracestate", item)); + } + String key = m.group(1); + String value = m.group(3); + if (internalList.get(key) != null) { + throw new IllegalArgumentException(String.format("duplicated keys %s in tracestate", key)); + } + internalList.put(key, value); + } + if (internalList.size() > MAX_KEY_VALUE_PAIRS) { + throw new IllegalArgumentException(String.format("cannot have more than %d key-value pairs", MAX_KEY_VALUE_PAIRS)); + } + internalString = toInternalString(); + } + + /** + * Ctor that creates a tracestate object from a parent one + */ + public Tracestate(Tracestate parent, String key, String value) { + if (key == null) { + throw new IllegalArgumentException("key is null"); + } + if (!KEY_VALIDATION_RE.matcher(key).find()) { + throw new IllegalArgumentException("invalid key format"); + } + if (value == null) { + throw new IllegalArgumentException("value is null"); + + } + if (!VALUE_VALIDATION_RE.matcher(value).find()) { + throw new IllegalArgumentException("invalid value format"); + } + internalList.put(key, value); + if (parent != null) { + for (String k : parent.internalList.keySet()) { + internalList.put(k, parent.internalList.get(k)); + } + internalList.put(key, value); + } + internalString = toInternalString(); + } + + public String get(String key) { + return internalList.get(key); + } + + /** + * Converts the Tracestate object to header format + * + * @return tracestate + */ + @Override + public String toString() { + return internalString; + } + + /** + * Converts Tracestate header to Object representation + * + * @return Tracestate + */ + public static Tracestate fromString(String s) { + return new Tracestate(s); + } + + private String toInternalString() { + boolean isFirst = true; + StringBuilder stringBuilder = new StringBuilder(512); + for (String key : internalList.keySet()) { + if (isFirst) { + isFirst = false; + } else { + stringBuilder.append(","); + } + stringBuilder.append(key); + stringBuilder.append("="); + stringBuilder.append(internalList.get(key)); + } + return stringBuilder.toString(); + } + +} diff --git a/web/src/test/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModuleTests.java b/web/src/test/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModuleTests.java index 30ea1ee7807..5e9b31af843 100644 --- a/web/src/test/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModuleTests.java +++ b/web/src/test/java/com/microsoft/applicationinsights/web/extensibility/modules/WebRequestTrackingTelemetryModuleTests.java @@ -21,40 +21,48 @@ package com.microsoft.applicationinsights.web.extensibility.modules; -import org.apache.http.HttpStatus; -import org.eclipse.jetty.http.HttpMethods; -import org.junit.*; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import com.microsoft.applicationinsights.web.utils.HttpHelper; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.microsoft.applicationinsights.TelemetryClient; import com.microsoft.applicationinsights.TelemetryConfiguration; +import com.microsoft.applicationinsights.extensibility.TelemetryModule; import com.microsoft.applicationinsights.extensibility.context.OperationContext; +import com.microsoft.applicationinsights.internal.util.DateTimeUtils; import com.microsoft.applicationinsights.telemetry.ExceptionTelemetry; import com.microsoft.applicationinsights.telemetry.RequestTelemetry; -import com.microsoft.applicationinsights.internal.util.DateTimeUtils; -import com.microsoft.applicationinsights.web.utils.JettyTestServer; -import com.microsoft.applicationinsights.web.utils.MockTelemetryChannel; -import com.microsoft.applicationinsights.web.utils.ServletUtils; import com.microsoft.applicationinsights.web.internal.RequestTelemetryContext; import com.microsoft.applicationinsights.web.internal.ThreadContext; -import com.microsoft.applicationinsights.web.internal.correlation.TelemetryCorrelationUtils; -import com.microsoft.applicationinsights.web.internal.correlation.TelemetryCorrelationUtilsTests; import com.microsoft.applicationinsights.web.internal.correlation.InstrumentationKeyResolver; import com.microsoft.applicationinsights.web.internal.correlation.ProfileFetcherResultTaskStatus; +import com.microsoft.applicationinsights.web.internal.correlation.TelemetryCorrelationUtils; +import com.microsoft.applicationinsights.web.internal.correlation.TelemetryCorrelationUtilsTests; +import com.microsoft.applicationinsights.web.internal.correlation.TraceContextCorrelation; +import com.microsoft.applicationinsights.web.internal.correlation.TraceContextCorrelationTests; import com.microsoft.applicationinsights.web.internal.correlation.mocks.MockProfileFetcher; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Map; +import com.microsoft.applicationinsights.web.internal.correlation.tracecontext.Traceparent; +import com.microsoft.applicationinsights.web.utils.HttpHelper; +import com.microsoft.applicationinsights.web.utils.JettyTestServer; +import com.microsoft.applicationinsights.web.utils.MockTelemetryChannel; +import com.microsoft.applicationinsights.web.utils.ServletUtils; import java.util.HashMap; import java.util.List; +import java.util.Map; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.http.HttpStatus; +import org.eclipse.jetty.http.HttpMethods; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; /** * Created by yonisha on 2/2/2015. @@ -65,6 +73,7 @@ public class WebRequestTrackingTelemetryModuleTests { private static JettyTestServer server = new JettyTestServer(); private static WebRequestTrackingTelemetryModule defaultModule; + private static WebRequestTrackingTelemetryModule currentModule; private static MockTelemetryChannel channel; private static MockProfileFetcher mockProfileFetcher; @@ -76,6 +85,7 @@ public static void classInitialize() throws Exception { // Set mock channel channel = MockTelemetryChannel.INSTANCE; + currentModule = getCurrentWebRequestTrackingModule(); TelemetryConfiguration.getActive().setChannel(channel); TelemetryConfiguration.getActive().setInstrumentationKey("SOME_INT_KEY"); } @@ -95,6 +105,13 @@ public void testInitialize() { channel.reset(); } + @After + public void testDestroy() { + currentModule.isW3CEnabled = false; + defaultModule.isW3CEnabled = false; + defaultModule.setEnableBackCompatibilityForW3C(true); + } + @AfterClass public static void classCleanup() throws Exception { server.shutdown(); @@ -132,6 +149,12 @@ public void testResponseHeaderIsSetForRequestContext() throws Exception { String requestContext = requestContextValues.get(0); Assert.assertEquals("appId=cid-v1:myId", requestContext); } + + @Test + public void testResponseHeaderIsSetForRequestContextWhenUsingW3C() throws Exception { + currentModule.isW3CEnabled = true; + testResponseHeaderIsSetForRequestContext(); + } @Test public void testOnBeginRequestCatchAllExceptions() { @@ -219,6 +242,305 @@ public void testCrossComponentCorrelationHeadersAreCaptured() { Assert.assertEquals(TelemetryCorrelationUtilsTests.getRequestSourceValue("id1", null), requestTelemetry.getSource()); } + @Test + public void testCrossComponentCorrelationHeadersAreCapturedWhenW3CTurnedOn() { + + // Turn W3C on + defaultModule.isW3CEnabled = true; + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + Traceparent tp = new Traceparent(); + + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, tp.toString()); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, TraceContextCorrelationTests.getTracestateHeaderValue("id1")); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + // verify ID's are set as expected in request telemetry + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + Assert.assertNotNull(requestTelemetry.getId()); + // spanIds are different + String[] id = requestTelemetry.getId().split("[.]"); + Assert.assertNotEquals(tp.getSpanId(), id[1]); + // traceIds are same + Assert.assertTrue(requestTelemetry.getId().startsWith(formatedID(tp.getTraceId()))); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertEquals(tp.getTraceId(), operation.getId()); + Assert.assertEquals(formatedID(tp.getTraceId() + "." + tp.getSpanId()), operation.getParentId()); + + //run onEnd + defaultModule.onEndRequest(request, null); + + //validate source + Assert.assertNotNull(requestTelemetry.getSource()); + Assert.assertEquals(TraceContextCorrelationTests.getRequestSourceValue("id1"), requestTelemetry.getSource()); + + } + + + @Test + public void testLegacyHeadersAreCapturedWhenW3CIsTurnedOn() { + // Turn W3C on + defaultModule.isW3CEnabled = true; + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + Traceparent tp = new Traceparent(); + + String incomingId = "|" + tp.getTraceId() + ".bcec871c_1."; + headers.put(TelemetryCorrelationUtils.CORRELATION_HEADER_NAME, incomingId); + headers.put(TelemetryCorrelationUtils.REQUEST_CONTEXT_HEADER_NAME, TelemetryCorrelationUtilsTests.getRequestContextHeaderValue("id1", null)); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + // verify ID's are set as expected in request telemetry + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + Assert.assertNotNull(requestTelemetry.getId()); + + Assert.assertTrue(requestTelemetry.getId().startsWith("|"+tp.getTraceId())); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertEquals(tp.getTraceId(), operation.getId()); + Assert.assertEquals(incomingId, operation.getParentId()); + + //run onEnd + defaultModule.onEndRequest(request, null); + + //validate source + Assert.assertNotNull(requestTelemetry.getSource()); + Assert.assertEquals(TraceContextCorrelationTests.getRequestSourceValue("id1"), requestTelemetry.getSource()); + + Assert.assertTrue(requestTelemetry.getContext().getProperties().containsKey("ai_legacyRootID")); + } + + @Test + public void testTraceparentIsCreatedWhenCorrelationFallsBackToRequestIdWhenIncorrectHeaderValues() { + // Turn W3C on + defaultModule.isW3CEnabled = true; + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + String incomingId = "|guid.bcec871c_1."; + headers.put(TelemetryCorrelationUtils.CORRELATION_HEADER_NAME, incomingId); + headers.put(TelemetryCorrelationUtils.REQUEST_CONTEXT_HEADER_NAME, TelemetryCorrelationUtilsTests. + getRequestContextHeaderValue("id1", null)); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + // verify ID's are set as expected in request telemetry + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + Assert.assertNotNull(requestTelemetry.getId()); + + //"guid" is invalid trace-id in W3C. A new Traceparent is created + Assert.assertFalse(requestTelemetry.getId().startsWith(incomingId.split("[.]")[0])); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertNotEquals(incomingId.split("[.]")[0], operation.getId()); + Assert.assertEquals(incomingId, operation.getParentId()); + + //run onEnd + defaultModule.onEndRequest(request, null); + + //validate source + Assert.assertNotNull(requestTelemetry.getSource()); + Assert.assertEquals(TraceContextCorrelationTests.getRequestSourceValue("id1"), requestTelemetry.getSource()); + + // validate ai_legacyRootID is set + Assert.assertTrue(requestTelemetry.getContext().getProperties().containsKey("ai_legacyRootID")); + Assert.assertEquals(requestTelemetry.getContext().getProperties().get("ai_legacyRootID"), + "guid"); + } + + @Test + public void traceParentIsCreatedInBackPortModeWhenRequestIdIsEmpty() { + // Turn W3C on + defaultModule.isW3CEnabled = true; + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + String incomingId = ""; + headers.put(TelemetryCorrelationUtils.CORRELATION_HEADER_NAME, incomingId); + headers.put(TelemetryCorrelationUtils.REQUEST_CONTEXT_HEADER_NAME, TelemetryCorrelationUtilsTests. + getRequestContextHeaderValue("id1", null)); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + // verify ID's are set as expected in request telemetry + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + Assert.assertNotNull(requestTelemetry.getId()); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertNotNull(operation.getId()); + + // request-id is empty + Assert.assertNull(operation.getParentId()); + + //run onEnd + defaultModule.onEndRequest(request, null); + + //validate source + Assert.assertNotNull(requestTelemetry.getSource()); + Assert.assertEquals(TraceContextCorrelationTests.getRequestSourceValue("id1"), requestTelemetry.getSource()); + + // No ai_legacyRootID is set as request-id is empty + Assert.assertFalse(requestTelemetry.getContext().getProperties().containsKey("ai_legacyRootID")); + + } + + @Test + public void traceParentIsCreatedInBackPortModeWhenRequestIdIsNull() { + // Turn W3C on + defaultModule.isW3CEnabled = true; + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + String incomingId = null; + headers.put(TelemetryCorrelationUtils.CORRELATION_HEADER_NAME, incomingId); + headers.put(TelemetryCorrelationUtils.REQUEST_CONTEXT_HEADER_NAME, TelemetryCorrelationUtilsTests. + getRequestContextHeaderValue("id1", null)); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + // verify ID's are set as expected in request telemetry + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + Assert.assertNotNull(requestTelemetry.getId()); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertNotNull(operation.getId()); + + // request-id is empty + Assert.assertNull(operation.getParentId()); + + //run onEnd + defaultModule.onEndRequest(request, null); + + //validate source + Assert.assertNotNull(requestTelemetry.getSource()); + Assert.assertEquals(TraceContextCorrelationTests.getRequestSourceValue("id1"), requestTelemetry.getSource()); + + // No ai_legacyRootID is set as request-id is empty + Assert.assertFalse(requestTelemetry.getContext().getProperties().containsKey("ai_legacyRootID")); + + } + + @Test + public void testLegacyHeadersAreNotCapturedWhenW3CIsTurnedOnAndBackPortSwitchIsOff() { + // Turn W3C on + defaultModule.isW3CEnabled = true; + defaultModule.setEnableBackCompatibilityForW3C(false); + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + Traceparent tp = new Traceparent(); + + String incomingId = "|" + tp.getTraceId() + ".bcec871c_1."; + headers.put(TelemetryCorrelationUtils.CORRELATION_HEADER_NAME, incomingId); + headers.put(TelemetryCorrelationUtils.REQUEST_CONTEXT_HEADER_NAME, TelemetryCorrelationUtilsTests.getRequestContextHeaderValue("id1", null)); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + // verify ID's are set as expected in request telemetry + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + Assert.assertNotNull(requestTelemetry.getId()); + + // old headers are not captured + Assert.assertFalse(requestTelemetry.getId().startsWith("|"+tp.getTraceId())); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertNotEquals(tp.getTraceId(), operation.getId()); + Assert.assertNotEquals(incomingId, operation.getParentId()); + + //run onEnd + defaultModule.onEndRequest(request, null); + + //old request-context headers are not checked + Assert.assertNull(requestTelemetry.getSource()); + + Assert.assertFalse(requestTelemetry.getContext().getProperties().containsKey("ai_legacyRootID")); + } + + + private String formatedID(String id) { + return "|" + id + "."; + } + @Test public void testTelemetryCreatedWithinRequestScopeIsRequestChild() { @@ -259,6 +581,176 @@ public void testTelemetryCreatedWithinRequestScopeIsRequestChild() { Assert.assertEquals("value2", exceptionTelemetry.getProperties().get("key2")); } + @Test + public void testTelemetryCreatedWithinRequestScopeIsRequestChildWhenW3CEnabled() { + + //turn on W3C + defaultModule.isW3CEnabled = true; + + //setup: initialize a request context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + Traceparent tp = new Traceparent(); + + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, tp.toString()); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, TraceContextCorrelationTests.getTracestateHeaderValue("id1")); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + //additional telemetry is manually tracked + TelemetryClient telemetryClient = new TelemetryClient(); + telemetryClient.trackException(new Exception()); + + List items = channel.getTelemetryItems(ExceptionTelemetry.class); + Assert.assertEquals(1, items.size()); + ExceptionTelemetry exceptionTelemetry = items.get(0); + + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + + //validate manually tracked telemetry is a child of the request telemetry + Assert.assertEquals(tp.getTraceId(), exceptionTelemetry.getContext().getOperation().getId()); + Assert.assertEquals(requestTelemetry.getId(), exceptionTelemetry.getContext().getOperation().getParentId()); + + Assert.assertNotNull(ThreadContext.getRequestTelemetryContext().getTracestate()); + Assert.assertEquals(TraceContextCorrelationTests.getTracestateHeaderValue("id2"), + ThreadContext.getRequestTelemetryContext().getTracestate().toString()); + + } + + @Test + public void testTracestateIsSetWhenHeaderIsEmpty() { + //turn on W3C + defaultModule.isW3CEnabled = true; + + //setup: initialize a request context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + Traceparent tp = new Traceparent(); + + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, tp.toString()); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, ""); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + Assert.assertNotNull(ThreadContext.getRequestTelemetryContext().getTracestate()); + Assert.assertEquals(TraceContextCorrelationTests.getTracestateHeaderValue("id2"), + ThreadContext.getRequestTelemetryContext().getTracestate().toString()); + } + + @Test + public void testNewTracestateIsCreatedWhenHeaderIsNotPresent() { + //turn on W3C + defaultModule.isW3CEnabled = true; + + //setup: initialize a request context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + Traceparent tp = new Traceparent(); + + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, tp.toString()); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + + Assert.assertNotNull(ThreadContext.getRequestTelemetryContext().getTracestate()); + Assert.assertEquals(TraceContextCorrelationTests.getTracestateHeaderValue("id2"), + ThreadContext.getRequestTelemetryContext().getTracestate().toString()); + } + + @Test + public void testTracestateIsPassedAsItIsWhenAppIdResolutionIsPending() { + //turn on W3C + defaultModule.isW3CEnabled = true; + + //setup: initialize a request context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + Traceparent tp = new Traceparent(); + + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, tp.toString()); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, TraceContextCorrelationTests.getTracestateHeaderValue("id1")); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.PENDING); + + //run + defaultModule.onBeginRequest(request, response); + + Assert.assertNotNull(ThreadContext.getRequestTelemetryContext().getTracestate()); + Assert.assertEquals(TraceContextCorrelationTests.getTracestateHeaderValue("id1"), + ThreadContext.getRequestTelemetryContext().getTracestate().toString()); + } + + @Test + public void testTracestateIsPassedAsIsWhenAppIdResolutionIsFailed() { + //turn on W3C + defaultModule.isW3CEnabled = true; + + //setup: initialize a request context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap<>(); + + Traceparent tp = new Traceparent(); + + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, tp.toString()); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, TraceContextCorrelationTests.getTracestateHeaderValue("id1")); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.FAILED); + + //run + defaultModule.onBeginRequest(request, response); + + Assert.assertNotNull(ThreadContext.getRequestTelemetryContext().getTracestate()); + Assert.assertEquals(TraceContextCorrelationTests.getTracestateHeaderValue("id1"), + ThreadContext.getRequestTelemetryContext().getTracestate().toString()); + } + @Test public void testOnEndAddsSourceFieldForRequestWithRequestContext() { @@ -416,6 +908,36 @@ public void testOnEndDoesNotOverrideSourceField() { Assert.assertEquals("myAppId", requestTelemetry.getSource()); } + @Test + public void testOnEndDoesNotOverrideSourceFieldWhenW3CEnabled() { + + // Enable W3C + defaultModule.isW3CEnabled = true; + + //setup: initialize a request telemetry context + RequestTelemetryContext context = new RequestTelemetryContext(DateTimeUtils.getDateTimeNow().getTime()); + ThreadContext.setRequestTelemetryContext(context); + RequestTelemetry requestTelemetry = ThreadContext.getRequestTelemetryContext().getHttpRequestTelemetry(); + requestTelemetry.setSource("myAppId"); + + //mock a servlet request with cross-component correlation headers + Map headers = new HashMap(); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, TraceContextCorrelationTests.getTracestateHeaderValue("id1")); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + //configure mock appId fetcher to return different appId from what's on the request header + mockProfileFetcher.setAppIdToReturn("id2"); + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + + //run + defaultModule.onBeginRequest(request, response); + defaultModule.onEndRequest(request, null); + + //validate source + Assert.assertEquals("myAppId", requestTelemetry.getSource()); + } + @Test public void testInstrumentationKeyIsResolvedDuringModuleInit() { Assert.assertEquals(0, mockProfileFetcher.callCount()); @@ -549,5 +1071,15 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return request; } + private static WebRequestTrackingTelemetryModule getCurrentWebRequestTrackingModule() { + List modules = TelemetryConfiguration.getActive().getTelemetryModules(); + for (TelemetryModule module : modules) { + if (module instanceof WebRequestTrackingTelemetryModule) { + return (WebRequestTrackingTelemetryModule) module; + } + } + return null; + } + // endregion Private methods } \ No newline at end of file diff --git a/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelationTests.java b/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelationTests.java new file mode 100644 index 00000000000..9ba368a6835 --- /dev/null +++ b/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/TraceContextCorrelationTests.java @@ -0,0 +1,256 @@ +package com.microsoft.applicationinsights.web.internal.correlation; + +import com.microsoft.applicationinsights.extensibility.context.OperationContext; +import com.microsoft.applicationinsights.telemetry.RequestTelemetry; +import com.microsoft.applicationinsights.web.internal.correlation.mocks.MockProfileFetcher; +import com.microsoft.applicationinsights.web.internal.correlation.tracecontext.Traceparent; +import com.microsoft.applicationinsights.web.utils.ServletUtils; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TraceContextCorrelationTests { + + private static MockProfileFetcher mockProfileFetcher; + + @Before + public void testInitialize() { + + // initialize mock profile fetcher (for resolving ikeys to appIds) + mockProfileFetcher = new MockProfileFetcher(); + InstrumentationKeyResolver.INSTANCE.setProfileFetcher(mockProfileFetcher); + InstrumentationKeyResolver.INSTANCE.clearCache(); + } + + @Test + public void testTraceparentAreResolved() { + + //setup + Map headers = new HashMap<>(); + Traceparent t = new Traceparent(); + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, t.toString()); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + //run + TraceContextCorrelation.resolveCorrelation(request, response, requestTelemetry); + + //validate we have generated proper ID's + Assert.assertNotNull(requestTelemetry.getId()); + + Assert.assertTrue(requestTelemetry.getId().startsWith(formatedID(t.getTraceId()))); + + //validate operation context ID's + OperationContext operation = requestTelemetry.getContext().getOperation(); + Assert.assertEquals(t.getTraceId(), operation.getId()); + Assert.assertEquals(formatedID(t.getTraceId() + "." + t.getSpanId()), operation.getParentId()); + } + + @Test + public void testCorrelationIdsAreResolvedIfNoTraceparentHeader() { + + //setup - no headers + Map headers = new HashMap<>(); + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + //run + TraceContextCorrelation.resolveCorrelation(request, response, requestTelemetry); + + //validate operation context ID's - there is no parent, so parentId should be null, traceId + // is newly generated and request.Id is based on new traceId-spanId + OperationContext operation = requestTelemetry.getContext().getOperation(); + + Assert.assertNotNull(requestTelemetry.getId()); + + // First trace will have it's own spanId also. + Assert.assertTrue(requestTelemetry.getId().startsWith(formatedID(operation.getId()))); + Assert.assertNull(operation.getParentId()); + } + + @Test + public void testCorrelationIdsAreResolvedIfTraceparentEmpty() { + + //setup - empty RequestId + Map headers = new HashMap<>(); + headers.put(TraceContextCorrelation.TRACEPARENT_HEADER_NAME, ""); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + HttpServletResponse response = (HttpServletResponse)ServletUtils.generateDummyServletResponse(); + + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + //run + TraceContextCorrelation.resolveCorrelation(request, response, requestTelemetry); + + //validate operation context ID's - there is no parent, so parentId should be null, traceId + // is newly generated and request.Id is based on new traceId-spanId + OperationContext operation = requestTelemetry.getContext().getOperation(); + + Assert.assertNotNull(requestTelemetry.getId()); + // First trace will have it's own spanId also. + Assert.assertTrue(requestTelemetry.getId().startsWith("|" + operation.getId())); + Assert.assertNull(operation.getParentId()); + } + + @Test + public void testTracestateIsResolved() { + Map headers = new HashMap<>(); + String incomingTracestate = getTracestateHeaderValue("id1"); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, incomingTracestate); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id2"); + + TraceContextCorrelation.resolveRequestSource(request, requestTelemetry, "ikey1"); + + Assert.assertEquals("cid-v1:id1", requestTelemetry.getSource()); + + } + + @Test + public void testSourceNotSetWhenIncomingAppIdInTraceStateIsSameAsCurrent() { + Map headers = new HashMap<>(); + String incomingTracestate = getTracestateHeaderValue("id1"); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, incomingTracestate); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id1"); + + TraceContextCorrelation.resolveRequestSource(request, requestTelemetry, "ikey1"); + //source and target have same appId + Assert.assertNull(requestTelemetry.getSource()); + } + + @Test + public void testTracestateIsNotResolvedWhenHeaderNotPresent() { + Map headers = new HashMap<>(); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id1"); + + TraceContextCorrelation.resolveRequestSource(request, requestTelemetry, "ikey1"); + Assert.assertNull(requestTelemetry.getSource()); + } + + @Test + public void testTracestateIsNotResolvedIfHeaderIsEmpty() { + Map headers = new HashMap<>(); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, ""); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id1"); + + TraceContextCorrelation.resolveRequestSource(request, requestTelemetry, "ikey1"); + Assert.assertNull(requestTelemetry.getSource()); + } + + @Test(expected = AssertionError.class) + public void testTraceStateIsNotResolvedIfHeaderDoesntHaveAzureComponent() { + Map headers = new HashMap<>(); + // get tracestate with non azure component + String incomingTracestate = getTracestateHeaderValue(null); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, incomingTracestate); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id1"); + + TraceContextCorrelation.resolveRequestSource(request, requestTelemetry, "ikey1"); + Assert.assertNull(requestTelemetry.getSource()); + } + + @Test + public void testTracestateIsNotResolvedWithNullIkey() { + Map headers = new HashMap<>(); + String incomingTracestate = getTracestateHeaderValue("id1"); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, incomingTracestate); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id2"); + + TraceContextCorrelation.resolveRequestSource(request, requestTelemetry, null); + + Assert.assertNull(requestTelemetry.getSource()); + } + + @Test + public void testTracestateIsNotResolvedWithNullRequestTelemetry() { + Map headers = new HashMap<>(); + String incomingTracestate = getTracestateHeaderValue("id1"); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, incomingTracestate); + + HttpServletRequest request = ServletUtils.createServletRequestWithHeaders(headers); + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id2"); + + TraceContextCorrelation.resolveRequestSource(request, null, "ikey1"); + + Assert.assertNull(requestTelemetry.getSource()); + } + + @Test + public void testTracestateIsNotResolvedWithNullRequest() { + Map headers = new HashMap<>(); + String incomingTracestate = getTracestateHeaderValue("id1"); + headers.put(TraceContextCorrelation.TRACESTATE_HEADER_NAME, incomingTracestate); + + RequestTelemetry requestTelemetry = new RequestTelemetry(); + + mockProfileFetcher.setResultStatus(ProfileFetcherResultTaskStatus.COMPLETE); + mockProfileFetcher.setAppIdToReturn("id2"); + + TraceContextCorrelation.resolveRequestSource(null, requestTelemetry, "ikey1"); + + Assert.assertNull(requestTelemetry.getSource()); + } + + public static String getTracestateHeaderValue(String appId) { + if (appId == null || appId.isEmpty()) { + return "foo=bar"; + } + return String.format("%s=cid-v1:%s", TraceContextCorrelation.AZURE_TRACEPARENT_COMPONENT_INITIAL, appId); + } + + public static String getRequestSourceValue(String appId) { + + if (appId == null) { + return "someValue"; + } + + return String.format("cid-v1:%s", appId); + } + + private String formatedID(String id) { + return "|" + id + "."; + } + +} diff --git a/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/TraceparentTests.java b/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/TraceparentTests.java new file mode 100644 index 00000000000..687364ccd80 --- /dev/null +++ b/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/TraceparentTests.java @@ -0,0 +1,116 @@ +package com.microsoft.applicationinsights.web.internal.correlation.tracecontext; + +import org.junit.Assert; +import org.junit.Test; + +public class TraceparentTests { + + @Test + public void canCreateValidTraceParentWithDefaultConstructor() { + Traceparent traceparent = new Traceparent(); + Assert.assertNotNull(traceparent.traceId); + Assert.assertNotNull(traceparent.spanId); + Assert.assertEquals(0, traceparent.version); + Assert.assertNotNull(traceparent.traceFlags); + } + + @Test + public void testTraceParentUniqueness() { + Traceparent t1 = new Traceparent(); + Traceparent t2 = new Traceparent(); + Assert.assertNotEquals(t1.traceId, t2.traceId); + Assert.assertNotEquals(t1.spanId, t2.spanId); + + // version is always 0 + Assert.assertEquals(t1.version, t2.version); + + // flags are currently 0, may change in future + Assert.assertEquals(t1.traceFlags, t2.traceFlags); + + } + + @Test + public void canCreateTraceParentWithProvidedValues() { + String traceId = Traceparent.randomHex(16); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(0, traceId, spanId, 0); + Assert.assertEquals(traceId, t1.traceId); + Assert.assertEquals(spanId, t1.spanId); + Assert.assertEquals(0, t1.version); + Assert.assertEquals(0, t1.traceFlags); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenCreatingTraceParentWithIllegalTraceId() { + String invalidTraceId = Traceparent.randomHex(32); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(0, invalidTraceId, spanId, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenCreatingTraceParentWithIllegalSpanId() { + String traceId = Traceparent.randomHex(16); + String invalidSpanId = Traceparent.randomHex(16); + Traceparent t1 = new Traceparent(0, traceId, invalidSpanId, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenVersionNumberIsOutOfRange() { + String traceId = Traceparent.randomHex(16); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(256, traceId, spanId, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenVersionNumberIsOutOfLowerRange() { + String traceId = Traceparent.randomHex(16); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(-1, traceId, spanId, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenFlagIsOutOfLowerRange() { + String traceId = Traceparent.randomHex(16); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(0, traceId, spanId, -1); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenFlagIsOutOfUpperRange() { + String traceId = Traceparent.randomHex(16); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(0, traceId, spanId, 256); + } + + @Test + public void canCreateTraceParentFromString() { + String traceId = Traceparent.randomHex(16); + String spanId = Traceparent.randomHex(8); + Traceparent t1 = new Traceparent(0, traceId, spanId, 0); + + Traceparent t2 = Traceparent.fromString(t1.toString()); + Assert.assertEquals(t1.version, t2.version); + Assert.assertEquals(t1.traceId, t2.traceId); + Assert.assertEquals(t1.spanId, t2.spanId); + Assert.assertEquals(t1.traceFlags, t2.traceFlags); + + // memory reference should be different + Assert.assertFalse(t1 == t2); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenTryingToCreateWithMalformedTraceparentString() { + String invalidTraceId = Traceparent.randomHex(32); + String invalidSpanId = Traceparent.randomHex(16); + String invalidTraceparent = String.format("%02x-%s-%s-%02x", 0, invalidTraceId, + invalidSpanId, 0); + + Traceparent t1 = Traceparent.fromString(invalidTraceparent); + } + + @Test + public void returnsNullTraceParentWhenTryingToCreateFromEmptyString() { + Traceparent t1 = Traceparent.fromString(""); + Assert.assertNull(t1); + } +} diff --git a/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/TracestateTests.java b/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/TracestateTests.java new file mode 100644 index 00000000000..2bee47bb923 --- /dev/null +++ b/web/src/test/java/com/microsoft/applicationinsights/web/internal/correlation/tracecontext/TracestateTests.java @@ -0,0 +1,24 @@ +package com.microsoft.applicationinsights.web.internal.correlation.tracecontext; + +import org.junit.Assert; +import org.junit.Test; + +public class TracestateTests { + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenTracestateIsNull() { + new Tracestate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsWhenTracestateIsEmpty() { + new Tracestate(""); + } + + @Test + public void canCreateTraceStateWithString() { + String tracestate = "az=cid-v1:120"; + Tracestate t1 = new Tracestate(tracestate); + Assert.assertEquals(tracestate, t1.toString()); + } +} diff --git a/web/src/test/java/com/microsoft/applicationinsights/web/utils/ServletUtils.java b/web/src/test/java/com/microsoft/applicationinsights/web/utils/ServletUtils.java index d755880413d..200c958d26f 100644 --- a/web/src/test/java/com/microsoft/applicationinsights/web/utils/ServletUtils.java +++ b/web/src/test/java/com/microsoft/applicationinsights/web/utils/ServletUtils.java @@ -24,6 +24,10 @@ import com.microsoft.applicationinsights.internal.logger.InternalLogger; import com.microsoft.applicationinsights.web.internal.WebModulesContainer; import com.microsoft.applicationinsights.web.internal.correlation.TelemetryCorrelationUtils; +import com.microsoft.applicationinsights.web.internal.correlation.TraceContextCorrelation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import org.apache.commons.lang3.exception.ExceptionUtils; import javax.servlet.Filter; @@ -97,6 +101,26 @@ public static HttpServletRequest createServletRequestWithHeaders(Map