|
18 | 18 | using Datadog.Trace.Headers; |
19 | 19 | using Datadog.Trace.Iast; |
20 | 20 | using Datadog.Trace.Logging; |
| 21 | +using Datadog.Trace.OpenTelemetry; |
21 | 22 | using Datadog.Trace.Propagators; |
22 | 23 | using Datadog.Trace.Sampling; |
23 | 24 | using Datadog.Trace.Tagging; |
@@ -82,8 +83,54 @@ public void Dispose() |
82 | 83 | { |
83 | 84 | } |
84 | 85 |
|
| 86 | + /// <summary> |
| 87 | + /// Runs the AppSec and IAST request hooks against the span tracking the request. Shared by the |
| 88 | + /// usual path and by a transferred request, which reuses the span of the request it came from. |
| 89 | + /// </summary> |
| 90 | + private static void ReportToSecurityAndIast(Scope scope, HttpContext httpContext, HttpRequest httpRequest) |
| 91 | + { |
| 92 | + var security = Security.Instance; |
| 93 | + if (security.AppsecEnabled) |
| 94 | + { |
| 95 | + var securityCoordinator = SecurityCoordinator.Get(security, scope.Span, httpContext); |
| 96 | + securityCoordinator.Reporter.ReportWafInitInfoOnce(security.WafInitResult); |
| 97 | + |
| 98 | + // request args |
| 99 | + var args = securityCoordinator.GetBasicRequestArgsForWaf(); |
| 100 | + |
| 101 | + // body args |
| 102 | + if (httpRequest.ContentType?.IndexOf("application/x-www-form-urlencoded", StringComparison.InvariantCultureIgnoreCase) >= 0) |
| 103 | + { |
| 104 | + var bodyArgs = securityCoordinator.GetBodyFromRequest(); |
| 105 | + if (bodyArgs is not null) |
| 106 | + { |
| 107 | + args.Add(AddressesConstants.RequestBody, bodyArgs); |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + securityCoordinator.BlockAndReport(args, isInHttpTracingModule: true); |
| 112 | + } |
| 113 | + |
| 114 | + var iastInstance = Iast.Iast.Instance; |
| 115 | + if (iastInstance.Settings.Enabled && iastInstance.OverheadController.AcquireRequest()) |
| 116 | + { |
| 117 | + var traceContext = scope.Span?.Context?.TraceContext; |
| 118 | + traceContext?.EnableIastInRequest(); |
| 119 | + traceContext?.IastRequestContext?.AddRequestData(httpRequest); |
| 120 | + } |
| 121 | + } |
| 122 | + |
85 | 123 | private static string BuildResourceName(Tracer tracer, HttpRequest httpRequest) |
86 | 124 | { |
| 125 | + if (tracer.Settings.OtelSemanticsEnabled) |
| 126 | + { |
| 127 | + // The OpenTelemetry HTTP span specification requires the span name to be |
| 128 | + // "{method} {http.route}", or just "{method}" when no route is available. Falling back |
| 129 | + // to the URI path is explicitly not allowed, as it makes the name high-cardinality. |
| 130 | + // The route is added later by the MVC / Web API integrations, if the request matched one. |
| 131 | + return HttpSemanticConventions.GetServerResourceNameFromRawMethod(httpRequest.HttpMethod, route: null); |
| 132 | + } |
| 133 | + |
87 | 134 | var url = tracer.Settings.BypassHttpRequestUrlCachingEnabled |
88 | 135 | ? RequestDataHelper.BuildUrl(httpRequest) |
89 | 136 | : RequestDataHelper.GetUrl(httpRequest); |
@@ -180,21 +227,68 @@ private void OnBeginRequest(object sender, EventArgs eventArgs) |
180 | 227 | } |
181 | 228 | } |
182 | 229 |
|
183 | | - string host = requestHeaders.Get("Host"); |
| 230 | + var otelSemanticsEnabled = tracer.Settings.OtelSemanticsEnabled; |
| 231 | + |
| 232 | + // HttpServerUtility.TransferRequest re-runs the pipeline with a fresh HttpContext but |
| 233 | + // the same ExecutionContext, so the span the original request started is still active. |
| 234 | + // The OpenTelemetry conventions describe a single HTTP server span per inbound request, |
| 235 | + // so track the transferred request against that span rather than nesting a second |
| 236 | + // server span inside it. This pipeline still produces the response the client sees, and |
| 237 | + // its EndRequest runs first, so it is the one that stamps the status code onto the span. |
| 238 | + var reusedScope = otelSemanticsEnabled ? HttpSemanticConventions.GetActiveHttpServerScope(tracer) : null; |
| 239 | + |
| 240 | + if (reusedScope is not null) |
| 241 | + { |
| 242 | + httpContext.Items[_httpContextScopeKey] = new ScopeContainer(reusedScope, proxyScope: null, ownsScope: false); |
| 243 | + shouldDisposeScope = false; |
| 244 | + ReportToSecurityAndIast(reusedScope, httpContext, httpRequest); |
| 245 | + return; |
| 246 | + } |
| 247 | + |
| 248 | + var hostHeader = requestHeaders.Get("Host"); |
184 | 249 | var userAgent = requestHeaders.Get(HttpHeaderNames.UserAgent); |
185 | | - string httpMethod = httpRequest.HttpMethod.ToUpperInvariant(); |
186 | | - var url = httpContext.Request.GetUrlForSpan(tracer.TracerManager.QueryStringManager, tracer.Settings.BypassHttpRequestUrlCachingEnabled); |
187 | 250 | var tags = new WebTags(); |
188 | 251 | // FIXME: InstrumentationName should be added to InstrumentationTags |
189 | 252 | tags.SetTag("component", "aspnet"); |
| 253 | + |
| 254 | + string host; |
| 255 | + string url; |
| 256 | + string httpMethod; |
| 257 | + |
| 258 | + if (otelSemanticsEnabled) |
| 259 | + { |
| 260 | + // OpenTelemetry reports the Host header as server.address/server.port and the URL as |
| 261 | + // url.scheme/url.path/url.query, so neither the Host header nor the formatted absolute |
| 262 | + // URL is used as-is. |
| 263 | + host = null; |
| 264 | + url = null; |
| 265 | + |
| 266 | + var requestUri = tracer.Settings.BypassHttpRequestUrlCachingEnabled |
| 267 | + ? RequestDataHelper.BuildUrl(httpRequest) |
| 268 | + : RequestDataHelper.GetUrl(httpRequest); |
| 269 | + |
| 270 | + httpMethod = HttpSemanticConventions.SetHttpServerRequestValues( |
| 271 | + tags, |
| 272 | + httpRequest.HttpMethod, |
| 273 | + requestUri, |
| 274 | + hostHeader, |
| 275 | + tracer.TracerManager.QueryStringManager); |
| 276 | + } |
| 277 | + else |
| 278 | + { |
| 279 | + host = hostHeader; |
| 280 | + url = httpContext.Request.GetUrlForSpan(tracer.TracerManager.QueryStringManager, tracer.Settings.BypassHttpRequestUrlCachingEnabled); |
| 281 | + httpMethod = httpRequest.HttpMethod.ToUpperInvariant(); |
| 282 | + } |
| 283 | + |
190 | 284 | scope = tracer.StartActiveInternal(_requestOperationName, extractedContext.SpanContext, tags: tags); |
191 | 285 | // Attempt to set Resource Name to something that will be close to what is expected |
192 | 286 | // Note: we will go and re-do it in OnEndRequest, but doing it here will allow for resource-based sampling |
193 | 287 | // this likely won't be perfect - but we need something to try and allow resource-based sampling to function |
194 | 288 | var resourceName = tracer.CurrentTraceSettings.HasResourceBasedSamplingRule |
195 | 289 | ? BuildResourceName(tracer, httpRequest) |
196 | 290 | : null; |
197 | | - scope.Span.DecorateWebServerSpan(resourceName: resourceName, httpMethod, host, url, userAgent, tags); |
| 291 | + scope.Span.DecorateWebServerSpan(resourceName: resourceName, httpMethod, host, url, userAgent, tags, otelSemanticsEnabled); |
198 | 292 | tracer.TracerManager.SpanContextPropagator.AddHeadersToSpanAsTags(scope.Span, headers, tracer.CurrentTraceSettings.Settings.HeaderTags, defaultTagPrefix: SpanContextPropagator.HttpRequestHeadersTagPrefix); |
199 | 293 | tracer.TracerManager.SpanContextPropagator.AddSecurityTestingHeadersAsTags(scope.Span, headers); |
200 | 294 | if (inferredProxyScope?.Span is { } proxySpan) |
@@ -237,35 +331,7 @@ private void OnBeginRequest(object sender, EventArgs eventArgs) |
237 | 331 |
|
238 | 332 | tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId); |
239 | 333 |
|
240 | | - var security = Security.Instance; |
241 | | - if (security.AppsecEnabled) |
242 | | - { |
243 | | - var securityCoordinator = SecurityCoordinator.Get(security, scope.Span, httpContext); |
244 | | - securityCoordinator.Reporter.ReportWafInitInfoOnce(security.WafInitResult); |
245 | | - |
246 | | - // request args |
247 | | - var args = securityCoordinator.GetBasicRequestArgsForWaf(); |
248 | | - |
249 | | - // body args |
250 | | - if (httpRequest.ContentType?.IndexOf("application/x-www-form-urlencoded", StringComparison.InvariantCultureIgnoreCase) >= 0) |
251 | | - { |
252 | | - var bodyArgs = securityCoordinator.GetBodyFromRequest(); |
253 | | - if (bodyArgs is not null) |
254 | | - { |
255 | | - args.Add(AddressesConstants.RequestBody, bodyArgs); |
256 | | - } |
257 | | - } |
258 | | - |
259 | | - securityCoordinator.BlockAndReport(args, isInHttpTracingModule: true); |
260 | | - } |
261 | | - |
262 | | - var iastInstance = Iast.Iast.Instance; |
263 | | - if (iastInstance.Settings.Enabled && iastInstance.OverheadController.AcquireRequest()) |
264 | | - { |
265 | | - var traceContext = scope.Span?.Context?.TraceContext; |
266 | | - traceContext?.EnableIastInRequest(); |
267 | | - traceContext?.IastRequestContext?.AddRequestData(httpRequest); |
268 | | - } |
| 334 | + ReportToSecurityAndIast(scope, httpContext, httpRequest); |
269 | 335 | } |
270 | 336 | catch (Exception ex) |
271 | 337 | { |
@@ -427,31 +493,40 @@ private void OnEndRequest(object sender, EventArgs eventArgs) |
427 | 493 | AddHeaderTagsFromHttpResponse(app.Context, proxyScope); |
428 | 494 | } |
429 | 495 |
|
430 | | - if (app.Context.Items[SharedItems.HttpContextPropagatedResourceNameKey] is string resourceName |
431 | | - && !string.IsNullOrEmpty(resourceName)) |
| 496 | + // A transferred request must not rename the span of the request that |
| 497 | + // transferred to it: the name describes the request the client made. |
| 498 | + if (container.OwnsScope) |
432 | 499 | { |
433 | | - currentSpan.ResourceName = resourceName; |
434 | | - } |
435 | | - else |
436 | | - { |
437 | | - currentSpan.ResourceName = BuildResourceName(tracer, app.Request); |
| 500 | + if (app.Context.Items[SharedItems.HttpContextPropagatedResourceNameKey] is string resourceName |
| 501 | + && !string.IsNullOrEmpty(resourceName)) |
| 502 | + { |
| 503 | + currentSpan.ResourceName = resourceName; |
| 504 | + } |
| 505 | + else |
| 506 | + { |
| 507 | + currentSpan.ResourceName = BuildResourceName(tracer, app.Request); |
| 508 | + } |
438 | 509 | } |
439 | 510 | } |
440 | 511 | finally |
441 | 512 | { |
442 | | - try |
| 513 | + if (container.OwnsScope) |
443 | 514 | { |
444 | | - if (scope.Span.ResourceName is null) |
| 515 | + try |
445 | 516 | { |
446 | | - scope.Span.ResourceName = BuildResourceName(tracer, app.Request); |
| 517 | + if (scope.Span.ResourceName is null) |
| 518 | + { |
| 519 | + scope.Span.ResourceName = BuildResourceName(tracer, app.Request); |
| 520 | + } |
447 | 521 | } |
448 | | - } |
449 | | - catch (Exception ex) |
450 | | - { |
451 | | - Log.Debug(ex, "Unable to set fallback resource name."); |
| 522 | + catch (Exception ex) |
| 523 | + { |
| 524 | + Log.Debug(ex, "Unable to set fallback resource name."); |
| 525 | + } |
| 526 | + |
| 527 | + scope.Dispose(); |
452 | 528 | } |
453 | 529 |
|
454 | | - scope.Dispose(); |
455 | 530 | proxyScope?.Dispose(); |
456 | 531 | // Clear the context to make sure another TracingHttpModule doesn't try to close the same scope |
457 | 532 | TryClearContext(app.Context); |
@@ -536,14 +611,23 @@ private void TryClearContext(HttpContext context) |
536 | 611 | /// </summary> |
537 | 612 | internal sealed class ScopeContainer |
538 | 613 | { |
539 | | - public ScopeContainer(Scope scope, Scope proxyScope = null) |
| 614 | + public ScopeContainer(Scope scope, Scope proxyScope = null, bool ownsScope = true) |
540 | 615 | { |
541 | 616 | Scope = scope; |
542 | 617 | ProxyScope = proxyScope; |
| 618 | + OwnsScope = ownsScope; |
543 | 619 | } |
544 | 620 |
|
545 | 621 | public Scope Scope { get; } |
546 | 622 |
|
| 623 | + /// <summary> |
| 624 | + /// Gets a value indicating whether this request started the scope, and is therefore the |
| 625 | + /// one that names and finishes it. <c>false</c> for a request produced by |
| 626 | + /// <see cref="HttpServerUtility.TransferRequest(string)"/> under OpenTelemetry semantics, |
| 627 | + /// where the span belongs to the request that transferred to this one. |
| 628 | + /// </summary> |
| 629 | + public bool OwnsScope { get; } |
| 630 | + |
547 | 631 | /// <summary> |
548 | 632 | /// Gets the inferred proxy scope. Only present when inferred proxy spans are enabled |
549 | 633 | /// AND necessary proxy headers were present. |
|
0 commit comments