diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java index 3119ecd2fe5..ca9c004d286 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java @@ -27,6 +27,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; +import datadog.trace.bootstrap.instrumentation.api.InferredProxyContext; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.bootstrap.instrumentation.api.TagContext; @@ -128,6 +129,7 @@ public Context extract(REQUEST_CARRIER carrier) { public AgentSpan startSpan( String instrumentationName, REQUEST_CARRIER carrier, AgentSpanContext.Extracted context) { + AgentSpan span = tracer() .startSpan(instrumentationName, spanName(), callIGCallbackStart(context)) @@ -144,9 +146,70 @@ public AgentSpan startSpan( } public AgentSpan startSpan(REQUEST_CARRIER carrier, Context context) { + if (Config.get().isTraceInferredProxyServicesEnabled()) { + InferredProxyContext inferredContext = InferredProxyContext.fromContext(context); + if (inferredContext != null && inferredContext.validContext()) { + return startInferredProxySpan("http-server", carrier, inferredContext, context); + } + } return startSpan("http-server", carrier, getExtractedSpanContext(context)); } + public AgentSpan startInferredProxySpan( + String instrumentationName, + REQUEST_CARRIER carrier, + InferredProxyContext inferredContext, + Context context) { + + AgentSpanContext.Extracted extractedCtx = getExtractedSpanContext(context); + + long startTimeMicros; + try { + startTimeMicros = Long.parseLong(inferredContext.getStartTime()) * 1000; + } catch (NumberFormatException nfe) { + return startSpan(instrumentationName, carrier, extractedCtx); + } + + AgentSpan inferredProxySpan = + tracer() + .startSpan( + "inferred_proxy", + inferredContext.getProxyName(), + callIGCallbackStart(extractedCtx), + startTimeMicros); + + // enrich the inferred proxy span with APIGW metadata + inferredProxySpan.setTag(Tags.COMPONENT, inferredContext.getComponentName()); + inferredProxySpan.setTag( + DDTags.RESOURCE_NAME, inferredContext.getHttpMethod() + " " + inferredContext.getPath()); + inferredProxySpan.setTag(DDTags.SERVICE_NAME, inferredContext.getDomainName()); + inferredProxySpan.setTag(DDTags.SPAN_TYPE, "web"); + inferredProxySpan.setTag(Tags.HTTP_METHOD, inferredContext.getHttpMethod()); + inferredProxySpan.setTag( + Tags.HTTP_URL, inferredContext.getDomainName() + inferredContext.getPath()); + inferredProxySpan.setTag("stage", inferredContext.getStage()); + inferredProxySpan.setTag("_dd.inferred_span", 1); + + AgentSpan serverSpan = + tracer() + .startSpan(instrumentationName, spanName(), inferredProxySpan.context()) + .setMeasured(true); + + serverSpan.setServiceName(Config.get().getServiceName()); + + // run the same IG/header logic we normally execute in startSpan(..) + Flow flow = callIGCallbackRequestHeaders(serverSpan, carrier); + if (flow.getAction() instanceof Flow.Action.RequestBlockingAction) { + serverSpan.setRequestBlockingAction((Flow.Action.RequestBlockingAction) flow.getAction()); + } + AgentPropagation.ContextVisitor getter = getter(); + if (carrier != null && getter != null) { + tracer().getDataStreamsMonitoring().setCheckpoint(serverSpan, forHttpServer()); + } + + return new InferredProxySpanGroupDecorator(inferredProxySpan, serverSpan); + } + public AgentSpanContext.Extracted getExtractedSpanContext(Context context) { AgentSpan extractedSpan = AgentSpan.fromContext(context); return extractedSpan == null ? null : (AgentSpanContext.Extracted) extractedSpan.context(); diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/InferredProxySpanGroupDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/InferredProxySpanGroupDecorator.java new file mode 100644 index 00000000000..fa4e2665ce5 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/InferredProxySpanGroupDecorator.java @@ -0,0 +1,389 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import datadog.trace.api.DDTraceId; +import datadog.trace.api.TraceConfig; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.interceptor.MutableSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import java.util.Map; + +public class InferredProxySpanGroupDecorator implements AgentSpan { + private final AgentSpan inferredProxySpan; + private final AgentSpan serverSpan; + + InferredProxySpanGroupDecorator(AgentSpan inferredProxySpan, AgentSpan serverSpan) { + this.inferredProxySpan = inferredProxySpan; + this.serverSpan = serverSpan; + } + + @Override + public DDTraceId getTraceId() { + return serverSpan.getTraceId(); + } + + @Override + public long getSpanId() { + return serverSpan.getSpanId(); + } + + @Override + public AgentSpan setTag(String key, boolean value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setTag(String key, int value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setTag(String key, long value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setTag(String key, double value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setTag(String key, String value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setTag(String key, CharSequence value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setTag(String key, Object value) { + return serverSpan.setTag(key, value); + } + + /** + * @param map + * @return + */ + @Override + public AgentSpan setAllTags(Map map) { + return null; + } + + @Override + public AgentSpan setTag(String key, Number value) { + return serverSpan.setTag(key, value); + } + + @Override + public AgentSpan setMetric(CharSequence key, int value) { + return serverSpan.setMetric(key, value); + } + + @Override + public AgentSpan setMetric(CharSequence key, long value) { + return serverSpan.setMetric(key, value); + } + + @Override + public AgentSpan setMetric(CharSequence key, double value) { + return serverSpan.setMetric(key, value); + } + + @Override + public AgentSpan setSpanType(CharSequence type) { + return serverSpan.setSpanType(type); + } + + @Override + public Object getTag(String key) { + return serverSpan.getTag(key); + } + + @Override + public AgentSpan setError(boolean error) { + serverSpan.setError(error); + if (inferredProxySpan != null) { + inferredProxySpan.setError(error); + } + return this; + } + + @Override + public AgentSpan setError(boolean error, byte priority) { + serverSpan.setError(error, priority); + if (inferredProxySpan != null) { + inferredProxySpan.setError(error, priority); + } + return this; + } + + @Override + public AgentSpan setMeasured(boolean measured) { + return serverSpan.setMeasured(measured); + } + + @Override + public AgentSpan setErrorMessage(String errorMessage) { + return serverSpan.setErrorMessage(errorMessage); + } + + @Override + public AgentSpan addThrowable(Throwable throwable) { + serverSpan.addThrowable(throwable); + if (inferredProxySpan != null) { + inferredProxySpan.addThrowable(throwable); + } + return this; + } + + @Override + public AgentSpan addThrowable(Throwable throwable, byte errorPriority) { + serverSpan.addThrowable(throwable, errorPriority); + if (inferredProxySpan != null) { + inferredProxySpan.addThrowable(throwable, errorPriority); + } + return this; + } + + @Override + public AgentSpan getLocalRootSpan() { + return serverSpan.getLocalRootSpan(); + } + + @Override + public boolean isSameTrace(AgentSpan otherSpan) { + return serverSpan.isSameTrace(otherSpan); + } + + @Override + public AgentSpanContext context() { + return serverSpan.context(); + } + + @Override + public String getBaggageItem(String key) { + return serverSpan.getBaggageItem(key); + } + + @Override + public AgentSpan setBaggageItem(String key, String value) { + return serverSpan.setBaggageItem(key, value); + } + + @Override + public AgentSpan setHttpStatusCode(int statusCode) { + serverSpan.setHttpStatusCode(statusCode); + if (inferredProxySpan != null) { + inferredProxySpan.setHttpStatusCode(statusCode); + } + return this; + } + + @Override + public short getHttpStatusCode() { + return serverSpan.getHttpStatusCode(); + } + + @Override + public void finish() { + serverSpan.finish(); + if (inferredProxySpan != null) { + inferredProxySpan.finish(); + } + } + + @Override + public void finish(long finishMicros) { + serverSpan.finish(finishMicros); + if (inferredProxySpan != null) { + inferredProxySpan.finish(finishMicros); + } + } + + @Override + public void finishWithDuration(long durationNanos) { + serverSpan.finishWithDuration(durationNanos); + if (inferredProxySpan != null) { + inferredProxySpan.finishWithDuration(durationNanos); + } + } + + @Override + public void beginEndToEnd() { + serverSpan.beginEndToEnd(); + } + + @Override + public void finishWithEndToEnd() { + serverSpan.finishWithEndToEnd(); + if (inferredProxySpan != null) { + inferredProxySpan.finishWithEndToEnd(); + } + } + + @Override + public boolean phasedFinish() { + final boolean ret = serverSpan.phasedFinish(); + if (inferredProxySpan != null) { + inferredProxySpan.phasedFinish(); + } + return ret; + } + + @Override + public void publish() { + serverSpan.publish(); + } + + @Override + public CharSequence getSpanName() { + return serverSpan.getSpanName(); + } + + @Override + public void setSpanName(CharSequence spanName) { + serverSpan.setSpanName(spanName); + } + + @Deprecated + @Override + public boolean hasResourceName() { + return serverSpan.hasResourceName(); + } + + @Override + public byte getResourceNamePriority() { + return serverSpan.getResourceNamePriority(); + } + + @Override + public AgentSpan setResourceName(CharSequence resourceName) { + return serverSpan.setResourceName(resourceName); + } + + @Override + public AgentSpan setResourceName(CharSequence resourceName, byte priority) { + return serverSpan.setResourceName(resourceName, priority); + } + + @Override + public RequestContext getRequestContext() { + return serverSpan.getRequestContext(); + } + + @Override + public Integer forceSamplingDecision() { + return serverSpan.forceSamplingDecision(); + } + + @Override + public AgentSpan setSamplingPriority(int newPriority, int samplingMechanism) { + return serverSpan.setSamplingPriority(newPriority, samplingMechanism); + } + + @Override + public TraceConfig traceConfig() { + return serverSpan.traceConfig(); + } + + @Override + public void addLink(AgentSpanLink link) { + serverSpan.addLink(link); + } + + @Override + public AgentSpan setMetaStruct(String field, Object value) { + return serverSpan.setMetaStruct(field, value); + } + + @Override + public boolean isOutbound() { + return serverSpan.isOutbound(); + } + + @Override + public AgentSpan asAgentSpan() { + return serverSpan.asAgentSpan(); + } + + @Override + public long getStartTime() { + return serverSpan.getStartTime(); + } + + @Override + public long getDurationNano() { + return serverSpan.getDurationNano(); + } + + @Override + public CharSequence getOperationName() { + return serverSpan.getOperationName(); + } + + @Override + public MutableSpan setOperationName(CharSequence serviceName) { + return serverSpan.setOperationName(serviceName); + } + + @Override + public String getServiceName() { + return serverSpan.getServiceName(); + } + + @Override + public MutableSpan setServiceName(String serviceName) { + return serverSpan.setServiceName(serviceName); + } + + @Override + public CharSequence getResourceName() { + return serverSpan.getResourceName(); + } + + @Override + public Integer getSamplingPriority() { + return serverSpan.getSamplingPriority(); + } + + @Deprecated + @Override + public MutableSpan setSamplingPriority(int newPriority) { + return serverSpan.setSamplingPriority(newPriority); + } + + @Override + public String getSpanType() { + return serverSpan.getSpanType(); + } + + @Override + public Map getTags() { + return serverSpan.getTags(); + } + + @Override + public boolean isError() { + return serverSpan.isError(); + } + + @Deprecated + @Override + public MutableSpan getRootSpan() { + return serverSpan.getRootSpan(); + } + + @Override + public void setRequestBlockingAction(Flow.Action.RequestBlockingAction rba) { + serverSpan.setRequestBlockingAction(rba); + } + + @Override + public Flow.Action.RequestBlockingAction getRequestBlockingAction() { + return serverSpan.getRequestBlockingAction(); + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index ab052ca6707..2623eb9eb11 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -84,6 +84,7 @@ public final class ConfigDefaults { new LinkedHashSet<>(asList(PropagationStyle.DATADOG)); static final int DEFAULT_TRACE_BAGGAGE_MAX_ITEMS = 64; static final int DEFAULT_TRACE_BAGGAGE_MAX_BYTES = 8192; + static final boolean DEFAULT_TRACE_INFERRED_PROXY_SERVICES_ENABLED = false; static final boolean DEFAULT_JMX_FETCH_ENABLED = true; static final boolean DEFAULT_TRACE_AGENT_V05_ENABLED = false; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java index d817c88666e..5bc49039407 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java @@ -99,6 +99,9 @@ public final class TracerConfig { public static final String TRACE_BAGGAGE_MAX_ITEMS = "trace.baggage.max.items"; public static final String TRACE_BAGGAGE_MAX_BYTES = "trace.baggage.max.bytes"; + public static final String TRACE_INFERRED_PROXY_SERVICES_ENABLED = + "trace.inferred.proxy.services.enabled"; + public static final String ENABLE_TRACE_AGENT_V05 = "trace.agent.v0.5.enabled"; public static final String CLIENT_IP_ENABLED = "trace.client-ip.enabled"; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 498c8a9c27d..42fd3c8ca47 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -7,6 +7,7 @@ import static datadog.trace.api.TracePropagationBehaviorExtract.IGNORE; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.BAGGAGE_CONCERN; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.DSM_CONCERN; +import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.INFERRED_PROXY_CONCERN; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.TRACING_CONCERN; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.XRAY_TRACING_CONCERN; import static datadog.trace.common.metrics.MetricsAggregatorFactory.createMetricsAggregator; @@ -77,6 +78,7 @@ import datadog.trace.common.writer.WriterFactory; import datadog.trace.common.writer.ddintake.DDIntakeTraceInterceptor; import datadog.trace.context.TraceScope; +import datadog.trace.core.apigw.InferredProxyPropagator; import datadog.trace.core.baggage.BaggagePropagator; import datadog.trace.core.datastreams.DataStreamsMonitoring; import datadog.trace.core.datastreams.DefaultDataStreamsMonitoring; @@ -727,6 +729,9 @@ private CoreTracer( && config.getTracePropagationBehaviorExtract() != IGNORE) { Propagators.register(BAGGAGE_CONCERN, new BaggagePropagator(config)); } + if (config.isTraceInferredProxyServicesEnabled()) { + Propagators.register(INFERRED_PROXY_CONCERN, new InferredProxyPropagator()); + } this.tagInterceptor = null == tagInterceptor ? new TagInterceptor(new RuleFlags(config)) : tagInterceptor; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/apigw/InferredProxyPropagator.java b/dd-trace-core/src/main/java/datadog/trace/core/apigw/InferredProxyPropagator.java new file mode 100644 index 00000000000..76fc8fcc9e7 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/apigw/InferredProxyPropagator.java @@ -0,0 +1,116 @@ +package datadog.trace.core.apigw; + +import datadog.context.Context; +import datadog.context.propagation.CarrierSetter; +import datadog.context.propagation.CarrierVisitor; +import datadog.context.propagation.Propagator; +import datadog.trace.bootstrap.instrumentation.api.InferredProxyContext; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InferredProxyPropagator implements Propagator { + private static final Logger log = LoggerFactory.getLogger(InferredProxyPropagator.class); + static final String INFERRED_PROXY_KEY = "x-dd-proxy"; + static final String REQUEST_TIME_KEY = "x-dd-proxy-request-time-ms"; + static final String DOMAIN_NAME_KEY = "x-dd-proxy-domain-name"; + static final String HTTP_METHOD_KEY = "x-dd-proxy-httpmethod"; + static final String PATH_KEY = "x-dd-proxy-path"; + static final String STAGE_KEY = "x-dd-proxy-stage"; + + // Supported proxies mapping (header value -> canonical component name) + static final Map SUPPORTED_PROXIES; + + static { + SUPPORTED_PROXIES = new HashMap<>(); + SUPPORTED_PROXIES.put("aws-apigateway", "aws.apigateway"); + } + + /** + * METHOD STUB: InferredProxy is currently not meant to be injected to downstream services + * + * @param context the context containing the values to be injected. + * @param carrier the instance that will receive the key/value pairs to propagate. + * @param setter the callback to set key/value pairs into the carrier. + */ + @Override + public void inject(Context context, C carrier, CarrierSetter setter) {} + + /** + * Extracts an InferredProxyContext from un upstream service and stores it as part of the Context + * object + * + * @param context the base context to store the extracted values on top, use {@link + * Context#root()} for a default base context. + * @param carrier the instance to fetch the propagated key/value pairs from. + * @param visitor the callback to walk over the carrier and extract its key/value pais. + * @return A context with the extracted values on top of the given base context. + */ + @Override + public Context extract(Context context, C carrier, CarrierVisitor visitor) { + if (context == null || carrier == null || visitor == null) { + return context; + } + InferredProxyContextExtractor extractor = new InferredProxyContextExtractor(); + visitor.forEachKeyValue(carrier, extractor); + + InferredProxyContext extractedContext = extractor.extractedContext; + // Mandatory headers for APIGW + if (extractedContext == null || !extractedContext.validContext()) { + return context; + } + return context.with(extractedContext); + } + + public static class InferredProxyContextExtractor implements BiConsumer { + private InferredProxyContext extractedContext; + + InferredProxyContextExtractor() {} + + /** + * Performs this operation on the given arguments. + * + * @param key the first input argument from an http header + * @param value the second input argument from an http header + */ + @Override + public void accept(String key, String value) { + if (key == null || key.isEmpty() || !key.startsWith(INFERRED_PROXY_KEY)) { + return; + } + + if (extractedContext == null) { + extractedContext = new InferredProxyContext(); + } + + switch (key) { + case INFERRED_PROXY_KEY: + if (SUPPORTED_PROXIES.containsKey(value)) { + extractedContext.setProxyName(SUPPORTED_PROXIES.get(value)); + extractedContext.setComponentName(value); + } + break; + case REQUEST_TIME_KEY: + extractedContext.setStartTime(value); + break; + case DOMAIN_NAME_KEY: + extractedContext.setDomainName(value); + break; + case HTTP_METHOD_KEY: + extractedContext.setHttpMethod(value); + break; + case PATH_KEY: + extractedContext.setPath(value); + break; + case STAGE_KEY: + extractedContext.setStage(value); + break; + default: + log.info( + "Extracting Inferred Proxy header that doesn't match accepted Inferred Proxy keys"); + } + } + } +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/apigw/InferredProxyPropagatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/apigw/InferredProxyPropagatorTest.groovy new file mode 100644 index 00000000000..4eb1d45b842 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/apigw/InferredProxyPropagatorTest.groovy @@ -0,0 +1,89 @@ +package datadog.trace.core.apigw + +import datadog.context.Context +import datadog.trace.bootstrap.instrumentation.api.ContextVisitors +import datadog.trace.bootstrap.instrumentation.api.InferredProxyContext +import datadog.trace.core.test.DDCoreSpecification + +class InferredProxyPropagatorTest extends DDCoreSpecification { + + def propagator = new InferredProxyPropagator() + + def "extract inferred proxy http headers"() { + setup: + def headers = [ + (InferredProxyPropagator.INFERRED_PROXY_KEY): "aws-apigateway", + (InferredProxyPropagator.REQUEST_TIME_KEY): "1672531200000", + (InferredProxyPropagator.DOMAIN_NAME_KEY): "example.com", + (InferredProxyPropagator.HTTP_METHOD_KEY): "GET", + (InferredProxyPropagator.PATH_KEY): "/test/path", + (InferredProxyPropagator.STAGE_KEY): "prod" + ] + + when: + final Context context = propagator.extract(Context.root(), headers, ContextVisitors.stringValuesMap()) + final InferredProxyContext inferredProxyContext = context.get(InferredProxyContext.CONTEXT_KEY) + + then: + inferredProxyContext != null + inferredProxyContext.getProxyName() == "aws.apigateway" + inferredProxyContext.getComponentName() == "aws-apigateway" + Long.parseLong(inferredProxyContext.getStartTime()) == 1672531200000L + inferredProxyContext.getDomainName() == "example.com" + inferredProxyContext.getHttpMethod() == "GET" + inferredProxyContext.getPath() == "/test/path" + inferredProxyContext.getStage() == "prod" + } + + def "extract with missing mandatory headers should not create context"() { + setup: + def headers = [ + // Missing InferredProxyPropagator.INFERRED_PROXY_KEY + (InferredProxyPropagator.REQUEST_TIME_KEY): "1672531200000", + (InferredProxyPropagator.DOMAIN_NAME_KEY): "example.com", + (InferredProxyPropagator.HTTP_METHOD_KEY): "GET", + (InferredProxyPropagator.PATH_KEY): "/test/path", + (InferredProxyPropagator.STAGE_KEY): "prod" + ] + + when: + final Context context = propagator.extract(Context.root(), headers, ContextVisitors.stringValuesMap()) + final InferredProxyContext inferredProxyContext = context.get(InferredProxyContext.CONTEXT_KEY) + + then: + inferredProxyContext == null + } + + def "extract with unsupported proxy should not create context"() { + setup: + def headers = [ + (InferredProxyPropagator.INFERRED_PROXY_KEY): "unsupported-proxy", + (InferredProxyPropagator.REQUEST_TIME_KEY): "1672531200000", + (InferredProxyPropagator.DOMAIN_NAME_KEY): "example.com", + (InferredProxyPropagator.HTTP_METHOD_KEY): "GET", + (InferredProxyPropagator.PATH_KEY): "/test/path", + (InferredProxyPropagator.STAGE_KEY): "prod" + ] + + when: + final Context context = propagator.extract(Context.root(), headers, ContextVisitors.stringValuesMap()) + final InferredProxyContext inferredProxyContext = context.get(InferredProxyContext.CONTEXT_KEY) + + then: + inferredProxyContext == null + } + + def "extract with only proxy key should not create context"() { + setup: + def headers = [ + (InferredProxyPropagator.INFERRED_PROXY_KEY): "aws-apigateway" + ] + + when: + final Context context = propagator.extract(Context.root(), headers, ContextVisitors.stringValuesMap()) + final InferredProxyContext inferredProxyContext = context.get(InferredProxyContext.CONTEXT_KEY) + + then: + inferredProxyContext == null + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index dc1b70139d1..f33e21d4cb3 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -154,6 +154,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_CLOUD_PAYLOAD_TAGGING_SERVICES; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_EXPERIMENTAL_FEATURES_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_HTTP_RESOURCE_REMOVE_TRAILING_SLASH; +import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_INFERRED_PROXY_SERVICES_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_KEEP_LATENCY_THRESHOLD_MS; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_LONG_RUNNING_FLUSH_INTERVAL; @@ -597,6 +598,7 @@ import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_RESOURCE_REMOVE_TRAILING_SLASH; import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_SERVER_ERROR_STATUSES; import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING; +import static datadog.trace.api.config.TracerConfig.TRACE_INFERRED_PROXY_SERVICES_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_KEEP_LATENCY_THRESHOLD_MS; import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_FLUSH_INTERVAL; @@ -823,6 +825,7 @@ public static String getHostName() { private final boolean tracePropagationExtractFirst; private final int traceBaggageMaxItems; private final int traceBaggageMaxBytes; + private final boolean traceInferredProxyServicesEnabled; private final int clockSyncPeriod; private final boolean logsInjectionEnabled; @@ -1717,6 +1720,10 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins configProvider.getBoolean( TRACE_PROPAGATION_EXTRACT_FIRST, DEFAULT_TRACE_PROPAGATION_EXTRACT_FIRST); + traceInferredProxyServicesEnabled = + configProvider.getBoolean( + TRACE_INFERRED_PROXY_SERVICES_ENABLED, DEFAULT_TRACE_INFERRED_PROXY_SERVICES_ENABLED); + clockSyncPeriod = configProvider.getInteger(CLOCK_SYNC_PERIOD, DEFAULT_CLOCK_SYNC_PERIOD); if (experimentalFeaturesEnabled.contains( @@ -3115,6 +3122,10 @@ public int getTraceBaggageMaxBytes() { return traceBaggageMaxBytes; } + public boolean isTraceInferredProxyServicesEnabled() { + return traceInferredProxyServicesEnabled; + } + public int getClockSyncPeriod() { return clockSyncPeriod; } @@ -5373,6 +5384,8 @@ public String toString() { + tracePropagationBehaviorExtract + ", tracePropagationExtractFirst=" + tracePropagationExtractFirst + + ", traceInferredProxyServicesEnabled=" + + traceInferredProxyServicesEnabled + ", clockSyncPeriod=" + clockSyncPeriod + ", jmxFetchEnabled=" diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java index 3dea68cd5b2..a8e27b3dc44 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java @@ -15,6 +15,7 @@ public final class AgentPropagation { public static final Concern TRACING_CONCERN = named("tracing"); public static final Concern BAGGAGE_CONCERN = named("baggage"); + public static final Concern INFERRED_PROXY_CONCERN = named("inferred_proxy"); public static final Concern XRAY_TRACING_CONCERN = named("tracing-xray"); // TODO DSM propagator should run after the other propagators as it stores the pathway context diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InferredProxyContext.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InferredProxyContext.java new file mode 100644 index 00000000000..f4c5891aa42 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InferredProxyContext.java @@ -0,0 +1,98 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import datadog.context.Context; +import datadog.context.ContextKey; +import datadog.context.ImplicitContextKeyed; +import java.util.Objects; +import java.util.stream.Stream; + +public class InferredProxyContext implements ImplicitContextKeyed { + public static final ContextKey CONTEXT_KEY = + ContextKey.named("inferred-proxy-key"); + private String componentName; + private String proxyName; + private String startTime; + private String domainName; + private String httpMethod; + private String path; + private String stage; + + public static InferredProxyContext fromContext(Context context) { + return context.get(CONTEXT_KEY); + } + + public InferredProxyContext() {} + + public void setComponentName(String name) { + this.componentName = name; + } + + public String getComponentName() { + return this.componentName; + } + + public void setProxyName(String name) { + this.proxyName = name; + } + + public String getProxyName() { + return this.proxyName; + } + + public void setStartTime(String time) { + this.startTime = time; + } + + public String getStartTime() { + return this.startTime; + } + + public void setDomainName(String name) { + this.domainName = name; + } + + public String getDomainName() { + return this.domainName; + } + + public void setHttpMethod(String httpMethod) { + this.httpMethod = httpMethod; + } + + public String getHttpMethod() { + return this.httpMethod; + } + + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return this.path; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public String getStage() { + return this.stage; + } + + public boolean validContext() { + return Stream.of(proxyName, componentName, startTime, domainName, httpMethod, path, stage) + .allMatch(Objects::nonNull); + } + + /** + * Creates a new context with this value under its chosen key. + * + * @param context the context to copy the original values from. + * @return the new context with the implicitly keyed value. + * @see Context#with(ImplicitContextKeyed) + */ + @Override + public Context storeInto(Context context) { + return context.with(CONTEXT_KEY, this); + } +} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/InferredProxyContextTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/InferredProxyContextTest.groovy new file mode 100644 index 00000000000..cc4e063eef9 --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/InferredProxyContextTest.groovy @@ -0,0 +1,123 @@ +package datadog.trace.bootstrap.instrumentation.api + +import datadog.context.Context +import spock.lang.Specification + +class InferredProxyContextTest extends Specification { + + def "test context validity"() { + setup: + def context = new InferredProxyContext() + + expect: + !context.validContext() + + when: + context.setProxyName("aws.apigateway") + context.setComponentName("aws-apigateway") + context.setStartTime("123") + context.setDomainName("example.com") + context.setHttpMethod("GET") + context.setPath("/foo") + + then: + !context.validContext() + + when: + context.setStage("prod") + + then: + context.validContext() + } + + def "test fromContext and storeInto"() { + setup: + def inferredProxyContext = new InferredProxyContext() + inferredProxyContext.setProxyName("aws.apigateway") + inferredProxyContext.setComponentName("aws-apigateway") + inferredProxyContext.setStartTime("123") + inferredProxyContext.setDomainName("example.com") + inferredProxyContext.setHttpMethod("GET") + inferredProxyContext.setPath("/foo") + inferredProxyContext.setStage("prod") + + when: + def context = inferredProxyContext.storeInto(Context.root()) + def extractedContext = InferredProxyContext.fromContext(context) + + then: + extractedContext == inferredProxyContext + extractedContext.getProxyName() == "aws.apigateway" + extractedContext.getComponentName() == "aws-apigateway" + } + + def "test fromContext with no inferred proxy context"() { + when: + def extractedContext = InferredProxyContext.fromContext(Context.root()) + + then: + extractedContext == null + } + + def "test getters and setters"() { + setup: + def context = new InferredProxyContext() + + when: + context.setProxyName("aws.apigateway") + context.setComponentName("aws-apigateway") + context.setStartTime("123") + context.setDomainName("example.com") + context.setHttpMethod("GET") + context.setPath("/foo") + context.setStage("prod") + + then: + context.getProxyName() == "aws.apigateway" + context.getComponentName() == "aws-apigateway" + context.getStartTime() == "123" + context.getDomainName() == "example.com" + context.getHttpMethod() == "GET" + context.getPath() == "/foo" + context.getStage() == "prod" + } + + def "test context validity for each field"() { + given: + def context = new InferredProxyContext() + + when: + if (proxy) { + context.setProxyName("proxy") + context.setComponentName("proxy-header") + } + if (startTime) { + context.setStartTime("123") + } + if (domainName) { + context.setDomainName("domain") + } + if (httpMethod) { + context.setHttpMethod("GET") + } + if (path) { + context.setPath("/path") + } + if (stage) { + context.setStage("prod") + } + + then: + context.validContext() == expected + + where: + proxy | startTime | domainName | httpMethod | path | stage | expected + false | true | true | true | true | true | false + true | false | true | true | true | true | false + true | true | false | true | true | true | false + true | true | true | false | true | true | false + true | true | true | true | false | true | false + true | true | true | true | true | false | false + true | true | true | true | true | true | true + } +}