HttpWebRequest ActivitySource (#694)

* Updated HttpWebRequestDiagnosticSource to use the new .NET 5 ActivitySource API.

* Code review part 1 and string allocation reduction.

* Code review part 2 added ExceptionInitializingInstrumentation event.

* Code review feedback 3.

* Code review feedback 4.

* Updated unit tests and some code review feedback.

* Updated to JaegerActivityExporter since it was just merged.

* Refactored http tag value caching so that it can be shared in dependencies project.

* Code review.

* Noticed a couple comments needed to be updated.

* Added Activity.IsAllDataRequested logic.

* Code review.

* Reverted change to OpenTelemetrySdk.

Co-authored-by: Cijo Thomas <cithomas@microsoft.com>
This commit is contained in:
Mikel Blanchard 2020-06-01 17:28:30 -07:00 committed by GitHub
parent dcedf02ec5
commit 96e1eb9f30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 991 additions and 975 deletions

View File

@ -1,7 +1,9 @@
using System.Web;
using System;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;
using OpenTelemetry.Trace;
using OpenTelemetry.Trace.Configuration;
namespace OpenTelemetry.Exporter.AspNet
@ -9,6 +11,7 @@ namespace OpenTelemetry.Exporter.AspNet
public class WebApiApplication : HttpApplication
{
private TracerFactory tracerFactory;
private IDisposable openTelemetry;
protected void Application_Start()
{
@ -24,6 +27,16 @@ namespace OpenTelemetry.Exporter.AspNet
.AddDependencyInstrumentation();
});
TracerFactoryBase.SetDefault(this.tracerFactory);
this.openTelemetry = OpenTelemetrySdk.EnableOpenTelemetry(
(builder) => builder.AddDependencyInstrumentation()
.UseJaegerActivityExporter(c =>
{
c.AgentHost = "localhost";
c.AgentPort = 6831;
}));
GlobalConfiguration.Configure(WebApiConfig.Register);
AreaRegistration.RegisterAllAreas();
@ -33,6 +46,7 @@ namespace OpenTelemetry.Exporter.AspNet
protected void Application_End()
{
this.tracerFactory?.Dispose();
this.openTelemetry?.Dispose();
}
}
}

View File

@ -27,6 +27,10 @@
<assemblyIdentity name="System.Memory" publicKeyToken="CC7B13FFCD2DDD51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.1.1" newVersion="4.0.1.1"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="CC7B13FFCD2DDD51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -25,8 +25,14 @@ namespace OpenTelemetry.Trace
public const string ComponentKey = "component";
public const string PeerServiceKey = "peer.service";
public const string StatusCodeKey = "ot.status_code";
public const string StatusDescriptionKey = "ot.status_description";
public const string HttpMethodKey = "http.method";
public const string HttpSchemeKey = "http.scheme";
public const string HttpTargetKey = "http.target";
public const string HttpStatusCodeKey = "http.status_code";
public const string HttpStatusTextKey = "http.status_text";
public const string HttpUserAgentKey = "http.user_agent";
public const string HttpPathKey = "http.path";
public const string HttpHostKey = "http.host";

View File

@ -112,6 +112,23 @@ namespace OpenTelemetry.Trace
return span;
}
/// <summary>
/// Helper method that populates span properties from host and port
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/2316771e7e0ca3bfe9b2286d13e3a41ded6b8858/specification/data-http.md.
/// </summary>
/// <param name="span">Span to fill out.</param>
/// <param name="hostAndPort">Host and port value.</param>
/// <returns>Span with populated host properties.</returns>
public static TelemetrySpan PutHttpHostAttribute(this TelemetrySpan span, string hostAndPort)
{
if (!string.IsNullOrEmpty(hostAndPort))
{
span.SetAttribute(SpanAttributeConstants.HttpHostKey, hostAndPort);
}
return span;
}
/// <summary>
/// Helper method that populates span properties from route
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/2316771e7e0ca3bfe9b2286d13e3a41ded6b8858/specification/data-http.md.
@ -171,46 +188,7 @@ namespace OpenTelemetry.Trace
{
span.PutHttpStatusCodeAttribute(statusCode);
var newStatus = Status.Unknown;
if (statusCode >= 200 && statusCode <= 399)
{
newStatus = Status.Ok;
}
else if (statusCode == 400)
{
newStatus = Status.InvalidArgument;
}
else if (statusCode == 401)
{
newStatus = Status.Unauthenticated;
}
else if (statusCode == 403)
{
newStatus = Status.PermissionDenied;
}
else if (statusCode == 404)
{
newStatus = Status.NotFound;
}
else if (statusCode == 429)
{
newStatus = Status.ResourceExhausted;
}
else if (statusCode == 501)
{
newStatus = Status.Unimplemented;
}
else if (statusCode == 503)
{
newStatus = Status.Unavailable;
}
else if (statusCode == 504)
{
newStatus = Status.DeadlineExceeded;
}
span.Status = newStatus.WithDescription(reasonPhrase);
span.Status = SpanHelper.ResolveSpanStatusForHttpStatusCode(statusCode).WithDescription(reasonPhrase);
return span;
}

View File

@ -0,0 +1,111 @@
// <copyright file="SpanHelper.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Collections.Generic;
namespace OpenTelemetry.Trace
{
/// <summary>
/// A collection of helper methods to be used when building spans.
/// </summary>
public static class SpanHelper
{
private static readonly Dictionary<StatusCanonicalCode, string> StatusCanonicalCodeToStringCache = new Dictionary<StatusCanonicalCode, string>()
{
[StatusCanonicalCode.Ok] = StatusCanonicalCode.Ok.ToString(),
[StatusCanonicalCode.Cancelled] = StatusCanonicalCode.Cancelled.ToString(),
[StatusCanonicalCode.Unknown] = StatusCanonicalCode.Unknown.ToString(),
[StatusCanonicalCode.InvalidArgument] = StatusCanonicalCode.InvalidArgument.ToString(),
[StatusCanonicalCode.DeadlineExceeded] = StatusCanonicalCode.DeadlineExceeded.ToString(),
[StatusCanonicalCode.NotFound] = StatusCanonicalCode.NotFound.ToString(),
[StatusCanonicalCode.AlreadyExists] = StatusCanonicalCode.AlreadyExists.ToString(),
[StatusCanonicalCode.PermissionDenied] = StatusCanonicalCode.PermissionDenied.ToString(),
[StatusCanonicalCode.ResourceExhausted] = StatusCanonicalCode.ResourceExhausted.ToString(),
[StatusCanonicalCode.FailedPrecondition] = StatusCanonicalCode.FailedPrecondition.ToString(),
[StatusCanonicalCode.Aborted] = StatusCanonicalCode.Aborted.ToString(),
[StatusCanonicalCode.OutOfRange] = StatusCanonicalCode.OutOfRange.ToString(),
[StatusCanonicalCode.Unimplemented] = StatusCanonicalCode.Unimplemented.ToString(),
[StatusCanonicalCode.Internal] = StatusCanonicalCode.Internal.ToString(),
[StatusCanonicalCode.Unavailable] = StatusCanonicalCode.Unavailable.ToString(),
[StatusCanonicalCode.DataLoss] = StatusCanonicalCode.DataLoss.ToString(),
[StatusCanonicalCode.Unauthenticated] = StatusCanonicalCode.Unauthenticated.ToString(),
};
/// <summary>
/// Helper method that returns the string version of a <see cref="StatusCanonicalCode"/> using a cache to save on allocations.
/// </summary>
/// <param name="statusCanonicalCode"><see cref="StatusCanonicalCode"/>.</param>
/// <returns>String version of the supplied <see cref="StatusCanonicalCode"/>.</returns>
public static string GetCachedCanonicalCodeString(StatusCanonicalCode statusCanonicalCode)
{
if (!StatusCanonicalCodeToStringCache.TryGetValue(statusCanonicalCode, out string canonicalCode))
{
return statusCanonicalCode.ToString();
}
return canonicalCode;
}
/// <summary>
/// Helper method that populates span properties from http status code according
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/2316771e7e0ca3bfe9b2286d13e3a41ded6b8858/specification/data-http.md.
/// </summary>
/// <param name="httpStatusCode">Http status code.</param>
/// <returns>Resolved span <see cref="Status"/> for the Http status code.</returns>
public static Status ResolveSpanStatusForHttpStatusCode(int httpStatusCode)
{
var newStatus = Status.Unknown;
if (httpStatusCode >= 200 && httpStatusCode <= 399)
{
newStatus = Status.Ok;
}
else if (httpStatusCode == 400)
{
newStatus = Status.InvalidArgument;
}
else if (httpStatusCode == 401)
{
newStatus = Status.Unauthenticated;
}
else if (httpStatusCode == 403)
{
newStatus = Status.PermissionDenied;
}
else if (httpStatusCode == 404)
{
newStatus = Status.NotFound;
}
else if (httpStatusCode == 429)
{
newStatus = Status.ResourceExhausted;
}
else if (httpStatusCode == 501)
{
newStatus = Status.Unimplemented;
}
else if (httpStatusCode == 503)
{
newStatus = Status.Unavailable;
}
else if (httpStatusCode == 504)
{
newStatus = Status.DeadlineExceeded;
}
return newStatus;
}
}
}

View File

@ -25,9 +25,6 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation
{
internal static class JaegerConversionExtensions
{
private const string StatusCode = "ot.status_code";
private const string StatusDescription = "ot.status_description";
private const int DaysPerYear = 365;
// Number of days in 4 years
@ -56,8 +53,6 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation
["db.instance"] = 4, // peer.service for Redis.
};
private static readonly Dictionary<StatusCanonicalCode, string> CanonicalCodeDictionary = new Dictionary<StatusCanonicalCode, string>();
private static readonly DictionaryEnumerator<string, object, TagState>.ForEachDelegate ProcessAttributeRef = ProcessAttribute;
private static readonly DictionaryEnumerator<string, object, TagState>.ForEachDelegate ProcessLibraryAttributeRef = ProcessLibraryAttribute;
private static readonly ListEnumerator<Link, PooledListState<JaegerSpanRef>>.ForEachDelegate ProcessLinkRef = ProcessLink;
@ -127,17 +122,11 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation
if (status.IsValid)
{
if (!CanonicalCodeDictionary.TryGetValue(status.CanonicalCode, out string statusCode))
{
statusCode = status.CanonicalCode.ToString();
CanonicalCodeDictionary.Add(status.CanonicalCode, statusCode);
}
PooledList<JaegerTag>.Add(ref jaegerTags.Tags, new JaegerTag(StatusCode, JaegerTagType.STRING, vStr: statusCode));
PooledList<JaegerTag>.Add(ref jaegerTags.Tags, new JaegerTag(SpanAttributeConstants.StatusCodeKey, JaegerTagType.STRING, vStr: SpanHelper.GetCachedCanonicalCodeString(status.CanonicalCode)));
if (status.Description != null)
{
PooledList<JaegerTag>.Add(ref jaegerTags.Tags, new JaegerTag(StatusDescription, JaegerTagType.STRING, vStr: status.Description));
PooledList<JaegerTag>.Add(ref jaegerTags.Tags, new JaegerTag(SpanAttributeConstants.StatusDescriptionKey, JaegerTagType.STRING, vStr: status.Description));
}
}

View File

@ -26,9 +26,6 @@ namespace OpenTelemetry.Exporter.Zipkin.Implementation
{
internal static class ZipkinConversionExtensions
{
private const string StatusCode = "ot.status_code";
private const string StatusDescription = "ot.status_description";
private static readonly Dictionary<string, int> RemoteEndpointServiceNameKeyResolutionDictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
[SpanAttributeConstants.PeerServiceKey] = 0, // RemoteEndpoint.ServiceName primary.
@ -41,7 +38,6 @@ namespace OpenTelemetry.Exporter.Zipkin.Implementation
private static readonly ConcurrentDictionary<string, ZipkinEndpoint> LocalEndpointCache = new ConcurrentDictionary<string, ZipkinEndpoint>();
private static readonly ConcurrentDictionary<string, ZipkinEndpoint> RemoteEndpointCache = new ConcurrentDictionary<string, ZipkinEndpoint>();
private static readonly ConcurrentDictionary<StatusCanonicalCode, string> CanonicalCodeCache = new ConcurrentDictionary<StatusCanonicalCode, string>();
private static readonly DictionaryEnumerator<string, object, AttributeEnumerationState>.ForEachDelegate ProcessAttributesRef = ProcessAttributes;
private static readonly DictionaryEnumerator<string, object, AttributeEnumerationState>.ForEachDelegate ProcessLibraryResourcesRef = ProcessLibraryResources;
@ -96,17 +92,11 @@ namespace OpenTelemetry.Exporter.Zipkin.Implementation
if (status.IsValid)
{
if (!CanonicalCodeCache.TryGetValue(status.CanonicalCode, out string canonicalCode))
{
canonicalCode = status.CanonicalCode.ToString();
CanonicalCodeCache.TryAdd(status.CanonicalCode, canonicalCode);
}
PooledList<KeyValuePair<string, string>>.Add(ref attributeEnumerationState.Tags, new KeyValuePair<string, string>(StatusCode, canonicalCode));
PooledList<KeyValuePair<string, string>>.Add(ref attributeEnumerationState.Tags, new KeyValuePair<string, string>(SpanAttributeConstants.StatusCodeKey, SpanHelper.GetCachedCanonicalCodeString(status.CanonicalCode)));
if (status.Description != null)
{
PooledList<KeyValuePair<string, string>>.Add(ref attributeEnumerationState.Tags, new KeyValuePair<string, string>(StatusDescription, status.Description));
PooledList<KeyValuePair<string, string>>.Add(ref attributeEnumerationState.Tags, new KeyValuePair<string, string>(SpanAttributeConstants.StatusDescriptionKey, status.Description));
}
}

View File

@ -1,23 +0,0 @@
// <copyright file="Constants.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
namespace OpenTelemetry.Instrumentation.Dependencies
{
internal static class Constants
{
public const string HttpSpanPrefix = "HTTP ";
}
}

View File

@ -42,13 +42,11 @@ namespace OpenTelemetry.Instrumentation.Dependencies
var assemblyVersion = typeof(DependenciesInstrumentation).Assembly.GetName().Version;
var httpClientListener = new HttpClientInstrumentation(tracerFactory.GetTracer(nameof(HttpClientInstrumentation), "semver:" + assemblyVersion), httpOptions ?? new HttpClientInstrumentationOptions());
var httpWebRequestInstrumentation = new HttpWebRequestInstrumentation(tracerFactory.GetTracer(nameof(HttpWebRequestInstrumentation), "semver:" + assemblyVersion), httpOptions ?? new HttpClientInstrumentationOptions());
var azureClientsListener = new AzureClientsInstrumentation(tracerFactory.GetTracer(nameof(AzureClientsInstrumentation), "semver:" + assemblyVersion));
var azurePipelineListener = new AzurePipelineInstrumentation(tracerFactory.GetTracer(nameof(AzurePipelineInstrumentation), "semver:" + assemblyVersion));
var sqlClientListener = new SqlClientInstrumentation(tracerFactory.GetTracer(nameof(SqlClientInstrumentation), "semver:" + assemblyVersion), sqlOptions ?? new SqlClientInstrumentationOptions());
this.instrumentations.Add(httpClientListener);
this.instrumentations.Add(httpWebRequestInstrumentation);
this.instrumentations.Add(azureClientsListener);
this.instrumentations.Add(azurePipelineListener);
this.instrumentations.Add(sqlClientListener);

View File

@ -14,10 +14,8 @@
// limitations under the License.
// </copyright>
using System;
using System.Net;
using System.Net.Http;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Instrumentation.Dependencies.Implementation;
namespace OpenTelemetry.Instrumentation.Dependencies
{
@ -101,18 +99,6 @@ namespace OpenTelemetry.Instrumentation.Dependencies
return true;
}
}
#if NET461
else if (activityName == HttpWebRequestDiagnosticSource.ActivityName)
{
if (arg1 is HttpWebRequest request &&
request.RequestUri != null &&
request.Method == "POST")
{
requestUri = request.RequestUri;
return true;
}
}
#endif
requestUri = null;
return false;

View File

@ -1,63 +0,0 @@
// <copyright file="HttpWebRequestInstrumentation.net461.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System;
using OpenTelemetry.Instrumentation.Dependencies.Implementation;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Instrumentation.Dependencies
{
/// <summary>
/// Dependencies instrumentation.
/// </summary>
public class HttpWebRequestInstrumentation : IDisposable
{
#if NET461
private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
#endif
/// <summary>
/// Initializes a new instance of the <see cref="HttpWebRequestInstrumentation"/> class.
/// </summary>
/// <param name="tracer">Tracer to record traced with.</param>
public HttpWebRequestInstrumentation(Tracer tracer)
: this(tracer, new HttpClientInstrumentationOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpWebRequestInstrumentation"/> class.
/// </summary>
/// <param name="tracer">Tracer to record traced with.</param>
/// <param name="options">Configuration options for HttpWebRequest instrumentation.</param>
public HttpWebRequestInstrumentation(Tracer tracer, HttpClientInstrumentationOptions options)
{
#if NET461
GC.KeepAlive(HttpWebRequestDiagnosticSource.Instance);
this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpWebRequestDiagnosticListener(tracer, options), options.EventFilter);
this.diagnosticSourceSubscriber.Subscribe();
#endif
}
/// <inheritdoc/>
public void Dispose()
{
#if NET461
this.diagnosticSourceSubscriber?.Dispose();
#endif
}
}
}

View File

@ -73,24 +73,24 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
return;
}
this.Tracer.StartActiveSpanFromActivity(Constants.HttpSpanPrefix + request.Method, activity, SpanKind.Client, out var span);
this.Tracer.StartActiveSpanFromActivity(HttpTagHelper.GetOperationNameForHttpMethod(request.Method), activity, SpanKind.Client, out var span);
if (span.IsRecording)
{
span.PutComponentAttribute("http");
span.PutHttpMethodAttribute(request.Method.ToString());
span.PutHttpHostAttribute(request.RequestUri.Host, request.RequestUri.Port);
span.PutHttpMethodAttribute(HttpTagHelper.GetNameForHttpMethod(request.Method));
span.PutHttpHostAttribute(HttpTagHelper.GetHostTagValueFromRequestUri(request.RequestUri));
span.PutHttpRawUrlAttribute(request.RequestUri.OriginalString);
if (this.options.SetHttpFlavor)
{
span.PutHttpFlavorAttribute(request.Version.ToString());
span.PutHttpFlavorAttribute(HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version));
}
}
if (!(this.httpClientSupportsW3C && this.options.TextFormat is TraceContextFormat))
{
this.options.TextFormat.Inject<HttpRequestMessage>(span.Context, request, (r, k, v) => r.Headers.Add(k, v));
this.options.TextFormat.Inject(span.Context, request, (r, k, v) => r.Headers.Add(k, v));
}
}

View File

@ -0,0 +1,117 @@
// <copyright file="HttpTagHelper.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
{
/// <summary>
/// A collection of helper methods to be used when building Http spans.
/// </summary>
public static class HttpTagHelper
{
private static readonly ConcurrentDictionary<string, string> MethodOperationNameCache = new ConcurrentDictionary<string, string>();
private static readonly ConcurrentDictionary<HttpMethod, string> HttpMethodOperationNameCache = new ConcurrentDictionary<HttpMethod, string>();
private static readonly ConcurrentDictionary<HttpMethod, string> HttpMethodNameCache = new ConcurrentDictionary<HttpMethod, string>();
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<int, string>> HostAndPortToStringCache = new ConcurrentDictionary<string, ConcurrentDictionary<int, string>>();
private static readonly ConcurrentDictionary<Version, string> ProtocolVersionToStringCache = new ConcurrentDictionary<Version, string>();
private static readonly ConcurrentDictionary<HttpStatusCode, string> StatusCodeToStringCache = new ConcurrentDictionary<HttpStatusCode, string>();
private static readonly Func<string, string> ConvertMethodToOperationNameRef = ConvertMethodToOperationName;
private static readonly Func<HttpMethod, string> ConvertHttpMethodToOperationNameRef = ConvertHttpMethodToOperationName;
private static readonly Func<HttpMethod, string> ConvertHttpMethodToNameRef = ConvertHttpMethodToName;
private static readonly Func<Version, string> ConvertConvertProtcolVersionToStringRef = ConvertProtcolVersionToString;
private static readonly Func<HttpStatusCode, string> ConvertHttpStatusCodeToStringRef = ConvertHttpStatusCodeToString;
/// <summary>
/// Gets the OpenTelemetry standard operation name for a span based on its Http method.
/// </summary>
/// <param name="method">Http method.</param>
/// <returns>Span operation name.</returns>
public static string GetOperationNameForHttpMethod(string method) => MethodOperationNameCache.GetOrAdd(method, ConvertMethodToOperationNameRef);
/// <summary>
/// Gets the OpenTelemetry standard operation name for a span based on its <see cref="HttpMethod"/>.
/// </summary>
/// <param name="method"><see cref="HttpMethod"/>.</param>
/// <returns>Span operation name.</returns>
public static string GetOperationNameForHttpMethod(HttpMethod method) => HttpMethodOperationNameCache.GetOrAdd(method, ConvertHttpMethodToOperationNameRef);
/// <summary>
/// Gets the OpenTelemetry standard method name for a span based on its <see cref="HttpMethod"/>.
/// </summary>
/// <param name="method"><see cref="HttpMethod"/>.</param>
/// <returns>Span method name.</returns>
public static string GetNameForHttpMethod(HttpMethod method) => HttpMethodNameCache.GetOrAdd(method, ConvertHttpMethodToNameRef);
/// <summary>
/// Gets the OpenTelemetry standard version tag value for a span based on its protocol <see cref="Version"/>.
/// </summary>
/// <param name="protocolVersion"><see cref="Version"/>.</param>
/// <returns>Span flavor value.</returns>
public static string GetFlavorTagValueFromProtocolVersion(Version protocolVersion) => ProtocolVersionToStringCache.GetOrAdd(protocolVersion, ConvertConvertProtcolVersionToStringRef);
/// <summary>
/// Gets the OpenTelemetry standard status code tag value for a span based on its protocol <see cref="HttpStatusCode"/>.
/// </summary>
/// <param name="statusCode"><see cref="HttpStatusCode"/>.</param>
/// <returns>Span status code value.</returns>
public static string GetStatusCodeTagValueFromHttpStatusCode(HttpStatusCode statusCode) => StatusCodeToStringCache.GetOrAdd(statusCode, ConvertHttpStatusCodeToStringRef);
/// <summary>
/// Gets the OpenTelemetry standard host tag value for a span based on its request <see cref="Uri"/>.
/// </summary>
/// <param name="requestUri"><see cref="Uri"/>.</param>
/// <returns>Span host value.</returns>
public static string GetHostTagValueFromRequestUri(Uri requestUri)
{
string host = requestUri.Host;
if (requestUri.IsDefaultPort)
{
return host;
}
int port = requestUri.Port;
if (!HostAndPortToStringCache.TryGetValue(host, out ConcurrentDictionary<int, string> portCache))
{
portCache = new ConcurrentDictionary<int, string>();
HostAndPortToStringCache.TryAdd(host, portCache);
}
if (!portCache.TryGetValue(port, out string hostTagValue))
{
hostTagValue = $"{requestUri.Host}:{requestUri.Port}";
portCache.TryAdd(port, hostTagValue);
}
return hostTagValue;
}
private static string ConvertMethodToOperationName(string method) => $"HTTP {method}";
private static string ConvertHttpMethodToOperationName(HttpMethod method) => $"HTTP {method}";
private static string ConvertHttpMethodToName(HttpMethod method) => method.ToString();
private static string ConvertHttpStatusCodeToString(HttpStatusCode statusCode) => ((int)statusCode).ToString();
private static string ConvertProtcolVersionToString(Version protocolVersion) => protocolVersion.ToString();
}
}

View File

@ -1,4 +1,4 @@
// <copyright file="HttpWebRequestDiagnosticSource.net461.cs" company="OpenTelemetry Authors">
// <copyright file="HttpWebRequestActivitySource.net461.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -21,7 +21,9 @@ using System.Diagnostics;
using System.Net;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
{
@ -29,24 +31,23 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
/// Hooks into the <see cref="HttpWebRequest"/> class reflectively and writes diagnostic events as requests are processed.
/// </summary>
/// <remarks>
/// Created from the System.Diagnostics.DiagnosticSource.HttpHandlerDiagnosticListener class which has some bugs and feature gaps.
/// Inspired from the System.Diagnostics.DiagnosticSource.HttpHandlerDiagnosticListener class which has some bugs and feature gaps.
/// See https://github.com/dotnet/runtime/pull/33732 for details.
/// </remarks>
internal sealed class HttpWebRequestDiagnosticSource : DiagnosticListener
internal sealed class HttpWebRequestActivitySource
{
internal const string DiagnosticListenerName = "HttpWebRequestDiagnosticListener";
internal const string ActivityName = DiagnosticListenerName + ".HttpRequestOut";
internal const string RequestStartName = ActivityName + ".Start";
internal const string RequestStopName = ActivityName + ".Stop";
internal const string RequestExceptionName = ActivityName + ".Exception";
internal const string ActivitySourceName = "HttpWebRequest";
internal const string ActivityName = ActivitySourceName + ".HttpRequestOut";
internal static readonly HttpWebRequestDiagnosticSource Instance = new HttpWebRequestDiagnosticSource();
internal static readonly HttpWebRequestActivitySource Instance = new HttpWebRequestActivitySource();
private const string InitializationFailed = DiagnosticListenerName + ".InitializationFailed";
private const string CorrelationContextHeaderName = "Correlation-Context";
private const string TraceParentHeaderName = "traceparent";
private const string TraceStateHeaderName = "tracestate";
private static readonly Version Version = typeof(HttpWebRequestActivitySource).Assembly.GetName().Version;
private static readonly ActivitySource WebRequestActivitySource = new ActivitySource(ActivitySourceName, Version.ToString());
// Fields for reflection
private static FieldInfo connectionGroupListField;
private static Type connectionGroupType;
@ -75,35 +76,115 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
private static Func<HttpWebResponse, bool> isWebSocketResponseAccessor;
private static Func<HttpWebResponse, string> connectionGroupNameAccessor;
// Fields for controlling initialization of the HttpWebRequestDiagnosticSource singleton
private bool initialized = false;
private HttpWebRequestDiagnosticSource()
: base(DiagnosticListenerName)
internal HttpWebRequestActivitySource()
{
try
{
PrepareReflectionObjects();
PerformInjection();
}
catch (Exception ex)
{
// If anything went wrong, just no-op. Write an event so at least we can find out.
InstrumentationEventSource.Log.ExceptionInitializingInstrumentation(typeof(HttpWebRequestActivitySource).FullName, ex);
}
}
public override IDisposable Subscribe(IObserver<KeyValuePair<string, object>> observer, Predicate<string> isEnabled)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, Activity activity)
{
IDisposable result = base.Subscribe(observer, isEnabled);
this.Initialize();
return result;
activity.DisplayName = HttpTagHelper.GetOperationNameForHttpMethod(request.Method);
InstrumentRequest(request, activity);
activity.SetCustomProperty("HttpWebRequest.Request", request);
if (activity.IsAllDataRequested)
{
activity.AddTag(SpanAttributeConstants.ComponentKey, "http");
activity.AddTag(SpanAttributeConstants.HttpMethodKey, request.Method);
activity.AddTag(SpanAttributeConstants.HttpHostKey, HttpTagHelper.GetHostTagValueFromRequestUri(request.RequestUri));
activity.AddTag(SpanAttributeConstants.HttpUrlKey, request.RequestUri.OriginalString);
activity.AddTag(SpanAttributeConstants.HttpFlavorKey, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.ProtocolVersion));
}
}
public override IDisposable Subscribe(IObserver<KeyValuePair<string, object>> observer, Func<string, object, object, bool> isEnabled)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddResponseTags(HttpWebResponse response, Activity activity)
{
IDisposable result = base.Subscribe(observer, isEnabled);
this.Initialize();
return result;
activity.SetCustomProperty("HttpWebRequest.Response", response);
if (activity.IsAllDataRequested)
{
activity.AddTag(SpanAttributeConstants.HttpStatusCodeKey, HttpTagHelper.GetStatusCodeTagValueFromHttpStatusCode(response.StatusCode));
Status status = SpanHelper.ResolveSpanStatusForHttpStatusCode((int)response.StatusCode);
activity.AddTag(SpanAttributeConstants.StatusCodeKey, SpanHelper.GetCachedCanonicalCodeString(status.CanonicalCode));
activity.AddTag(SpanAttributeConstants.StatusDescriptionKey, response.StatusDescription);
}
}
public override IDisposable Subscribe(IObserver<KeyValuePair<string, object>> observer)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddExceptionTags(Exception exception, Activity activity)
{
IDisposable result = base.Subscribe(observer);
this.Initialize();
return result;
activity.SetCustomProperty("HttpWebRequest.Exception", exception);
if (!activity.IsAllDataRequested)
{
return;
}
Status status;
if (exception is WebException wexc)
{
if (wexc.Response is HttpWebResponse response)
{
activity.AddTag(SpanAttributeConstants.HttpStatusCodeKey, HttpTagHelper.GetStatusCodeTagValueFromHttpStatusCode(response.StatusCode));
status = SpanHelper.ResolveSpanStatusForHttpStatusCode((int)response.StatusCode).WithDescription(response.StatusDescription);
}
else
{
switch (wexc.Status)
{
case WebExceptionStatus.Timeout:
status = Status.DeadlineExceeded;
break;
case WebExceptionStatus.NameResolutionFailure:
status = Status.InvalidArgument.WithDescription(exception.Message);
break;
case WebExceptionStatus.SendFailure:
case WebExceptionStatus.ConnectFailure:
case WebExceptionStatus.SecureChannelFailure:
case WebExceptionStatus.TrustFailure:
status = Status.FailedPrecondition.WithDescription(exception.Message);
break;
case WebExceptionStatus.ServerProtocolViolation:
status = Status.Unimplemented.WithDescription(exception.Message);
break;
case WebExceptionStatus.RequestCanceled:
status = Status.Cancelled;
break;
case WebExceptionStatus.MessageLengthLimitExceeded:
status = Status.ResourceExhausted.WithDescription(exception.Message);
break;
default:
status = Status.Unknown.WithDescription(exception.Message);
break;
}
}
}
else
{
status = Status.Unknown.WithDescription(exception.Message);
}
activity.AddTag(SpanAttributeConstants.StatusCodeKey, SpanHelper.GetCachedCanonicalCodeString(status.CanonicalCode));
activity.AddTag(SpanAttributeConstants.StatusDescriptionKey, status.Description);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InstrumentRequest(HttpWebRequest request, Activity activity)
{
// do not inject header if it was injected already
@ -139,12 +220,53 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsRequestInstrumented(HttpWebRequest request)
=> request.Headers.Get(TraceParentHeaderName) != null;
private static void ProcessRequest(HttpWebRequest request)
{
if (!WebRequestActivitySource.HasListeners() || IsRequestInstrumented(request))
{
// No subscribers to the ActivitySource or this request was instrumented by previous
// ProcessRequest, such is the case with redirect responses where the same request is sent again.
return;
}
var activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client);
if (activity == null)
{
// There is a listener but it decided not to sample the current request.
return;
}
IAsyncResult asyncContext = writeAResultAccessor(request);
if (asyncContext != null)
{
// Flow here is for [Begin]GetRequestStream[Async].
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext));
asyncCallbackModifier(asyncContext, callback.AsyncCallback);
}
else
{
// Flow here is for [Begin]GetResponse[Async] without a prior call to [Begin]GetRequestStream[Async].
asyncContext = readAResultAccessor(request);
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext));
asyncCallbackModifier(asyncContext, callback.AsyncCallback);
}
AddRequestTagsAndInstrumentRequest(request, activity);
}
private static void HookOrProcessResult(HttpWebRequest request)
{
IAsyncResult writeAsyncContext = writeAResultAccessor(request);
if (writeAsyncContext == null || !(asyncCallbackAccessor(writeAsyncContext)?.Target is AsyncCallbackWrapper writeAsyncContextCallback))
{
// If we already hooked into the read result during RaiseRequestEvent or we hooked up after the fact already we don't need to do anything here.
// If we already hooked into the read result during ProcessRequest or we hooked up after the fact already we don't need to do anything here.
return;
}
@ -163,7 +285,7 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
if (endCalledAccessor.Invoke(readAsyncContext) || readAsyncContext.CompletedSynchronously)
{
// We need to process the result directly because the read callback has already fired. Force a copy because response has likely already been disposed.
ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Request, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true);
ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true);
return;
}
@ -172,16 +294,20 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
asyncCallbackModifier(readAsyncContext, callback.AsyncCallback);
}
private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, HttpWebRequest request, Activity activity, object result, bool forceResponseCopy)
private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy)
{
// We could be executing on a different thread now so set the activity.
Activity.Current = activity;
Debug.Assert(Activity.Current == null || Activity.Current == activity, "There was an unexpected active Activity on the result thread.");
if (Activity.Current == null)
{
Activity.Current = activity;
}
try
{
if (result is Exception ex)
{
Instance.RaiseExceptionEvent(request, ex);
AddExceptionTags(ex, activity);
}
else
{
@ -202,11 +328,11 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
isWebSocketResponseAccessor(response), connectionGroupNameAccessor(response),
});
Instance.RaiseResponseEvent(request, responseCopy);
AddResponseTags(responseCopy, activity);
}
else
{
Instance.RaiseResponseEvent(request, response);
AddResponseTags(response, activity);
}
}
}
@ -456,97 +582,6 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
return (Func<object[], T>)setterMethod.CreateDelegate(typeof(Func<object[], T>));
}
/// <summary>
/// Initializes all the reflection objects it will ever need. Reflection is costly, but it's better to take
/// this one time performance hit than to get it multiple times later, or do it lazily and have to worry about
/// threading issues. If Initialize has been called before, it will not doing anything.
/// </summary>
private void Initialize()
{
lock (this)
{
if (!this.initialized)
{
try
{
// This flag makes sure we only do this once. Even if we failed to initialize in an
// earlier time, we should not retry because this initialization is not cheap and
// the likelihood it will succeed the second time is very small.
this.initialized = true;
PrepareReflectionObjects();
PerformInjection();
}
catch (Exception ex)
{
// If anything went wrong, just no-op. Write an event so at least we can find out.
this.Write(InitializationFailed, new { Exception = ex });
}
}
}
}
private void RaiseRequestEvent(HttpWebRequest request)
{
if (this.IsRequestInstrumented(request))
{
// This request was instrumented by previous RaiseRequestEvent, such is the case with redirect responses where the same request is sent again.
return;
}
if (this.IsEnabled(ActivityName, request))
{
// We don't call StartActivity here because it will fire into user code before the headers are added.
var activity = new Activity(ActivityName);
activity.Start();
IAsyncResult asyncContext = readAResultAccessor(request);
if (asyncContext != null)
{
// Flow here is for [Begin]GetResponse[Async] without a prior call to [Begin]GetRequestStream[Async].
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext));
asyncCallbackModifier(asyncContext, callback.AsyncCallback);
}
else
{
// Flow here is for [Begin]GetRequestStream[Async].
asyncContext = writeAResultAccessor(request);
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext));
asyncCallbackModifier(asyncContext, callback.AsyncCallback);
}
InstrumentRequest(request, activity);
// Only send start event to users who subscribed for it, but start activity anyway
if (this.IsEnabled(RequestStartName))
{
this.Write(activity.OperationName + ".Start", new { Request = request });
}
}
}
private void RaiseResponseEvent(HttpWebRequest request, HttpWebResponse response)
{
if (this.IsEnabled(RequestStopName))
{
this.Write(RequestStopName, new { Request = request, Response = response });
}
}
private void RaiseExceptionEvent(HttpWebRequest request, Exception exception)
{
if (this.IsEnabled(RequestExceptionName))
{
this.Write(RequestExceptionName, new { Request = request, Exception = exception });
}
}
private bool IsRequestInstrumented(HttpWebRequest request)
=> request.Headers.Get(TraceParentHeaderName) != null;
private class HashtableWrapper : Hashtable, IEnumerable
{
private readonly Hashtable table;
@ -957,7 +992,7 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
if (value is HttpWebRequest request)
{
Instance.RaiseRequestEvent(request);
ProcessRequest(request);
}
return index;
@ -1011,7 +1046,7 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
object result = resultAccessor(asyncResult);
if (result is Exception || result is HttpWebResponse)
{
ProcessResult(asyncResult, this.OriginalCallback, this.Request, this.Activity, result, false);
ProcessResult(asyncResult, this.OriginalCallback, this.Activity, result, false);
}
this.OriginalCallback?.Invoke(asyncResult);

View File

@ -1,163 +0,0 @@
// <copyright file="HttpWebRequestDiagnosticListener.net461.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#if NET461
using System;
using System.Diagnostics;
using System.Net;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Instrumentation.Dependencies.Implementation
{
internal class HttpWebRequestDiagnosticListener : ListenerHandler
{
private readonly PropertyFetcher startRequestFetcher = new PropertyFetcher("Request");
private readonly PropertyFetcher stopResponseFetcher = new PropertyFetcher("Response");
private readonly PropertyFetcher stopExceptionFetcher = new PropertyFetcher("Exception");
private readonly HttpClientInstrumentationOptions options;
public HttpWebRequestDiagnosticListener(Tracer tracer, HttpClientInstrumentationOptions options)
: base(HttpWebRequestDiagnosticSource.DiagnosticListenerName, tracer)
{
this.options = options;
}
public override void OnStartActivity(Activity activity, object payload)
{
const string EventNameSuffix = ".OnStartActivity";
if (!(this.startRequestFetcher.Fetch(payload) is HttpWebRequest request))
{
InstrumentationEventSource.Log.NullPayload(nameof(HttpWebRequestDiagnosticListener) + EventNameSuffix);
return;
}
this.Tracer.StartActiveSpanFromActivity(Constants.HttpSpanPrefix + request.Method, activity, SpanKind.Client, out var span);
if (span.IsRecording)
{
span.PutComponentAttribute("http");
span.PutHttpMethodAttribute(request.Method);
span.PutHttpHostAttribute(request.RequestUri.Host, request.RequestUri.Port);
span.PutHttpRawUrlAttribute(request.RequestUri.OriginalString);
if (this.options.SetHttpFlavor)
{
span.PutHttpFlavorAttribute(request.ProtocolVersion.ToString());
}
}
if (!(this.options.TextFormat is TraceContextFormat))
{
this.options.TextFormat.Inject<HttpWebRequest>(span.Context, request, (r, k, v) => r.Headers.Add(k, v));
}
}
public override void OnStopActivity(Activity activity, object payload)
{
const string EventNameSuffix = ".OnStopActivity";
var span = this.Tracer.CurrentSpan;
try
{
if (span == null || !span.Context.IsValid)
{
InstrumentationEventSource.Log.NullOrBlankSpan(nameof(HttpWebRequestDiagnosticListener) + EventNameSuffix);
return;
}
if (span.IsRecording)
{
if (this.stopResponseFetcher.Fetch(payload) is HttpWebResponse response)
{
span.PutHttpStatusCode((int)response.StatusCode, response.StatusDescription);
}
}
}
finally
{
span?.End();
}
}
public override void OnException(Activity activity, object payload)
{
const string EventNameSuffix = ".OnException";
var span = this.Tracer.CurrentSpan;
try
{
if (span == null || !span.Context.IsValid)
{
InstrumentationEventSource.Log.NullOrBlankSpan(nameof(HttpWebRequestDiagnosticListener) + EventNameSuffix);
return;
}
if (span.IsRecording)
{
if (!(this.stopExceptionFetcher.Fetch(payload) is Exception exc))
{
InstrumentationEventSource.Log.NullPayload(nameof(HttpWebRequestDiagnosticListener) + EventNameSuffix);
return;
}
ProcessException(span, exc);
}
}
finally
{
span?.End();
}
}
private static void ProcessException(TelemetrySpan span, Exception exception)
{
if (exception is WebException wexc)
{
if (wexc.Response is HttpWebResponse response)
{
span.PutHttpStatusCode((int)response.StatusCode, response.StatusDescription);
return;
}
switch (wexc.Status)
{
case WebExceptionStatus.Timeout:
span.Status = Status.DeadlineExceeded;
return;
case WebExceptionStatus.NameResolutionFailure:
span.Status = Status.InvalidArgument.WithDescription(exception.Message);
return;
case WebExceptionStatus.SendFailure:
case WebExceptionStatus.ConnectFailure:
case WebExceptionStatus.SecureChannelFailure:
case WebExceptionStatus.TrustFailure:
span.Status = Status.FailedPrecondition.WithDescription(exception.Message);
return;
case WebExceptionStatus.ServerProtocolViolation:
span.Status = Status.Unimplemented.WithDescription(exception.Message);
return;
case WebExceptionStatus.RequestCanceled:
span.Status = Status.Cancelled;
return;
case WebExceptionStatus.MessageLengthLimitExceeded:
span.Status = Status.ResourceExhausted.WithDescription(exception.Message);
return;
}
}
span.Status = Status.Unknown.WithDescription(exception.Message);
}
}
}
#endif

View File

@ -0,0 +1,66 @@
// <copyright file="OpenTelemetryBuilderExtensions.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System;
using OpenTelemetry.Instrumentation.Dependencies.Implementation;
namespace OpenTelemetry.Trace.Configuration
{
/// <summary>
/// Extension methods to simplify registering of dependency instrumentation.
/// </summary>
public static class OpenTelemetryBuilderExtensions
{
/// <summary>
/// Enables the outgoing requests automatic data collection for all supported activity sources.
/// </summary>
/// <param name="builder"><see cref="OpenTelemetryBuilder"/> being configured.</param>
/// <returns>The instance of <see cref="OpenTelemetryBuilder"/> to chain the calls.</returns>
public static OpenTelemetryBuilder AddDependencyInstrumentation(this OpenTelemetryBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
#if NET461
builder.AddHttpWebRequestDependencyInstrumentation();
#endif
return builder;
}
#if NET461
/// <summary>
/// Enables the outgoing requests automatic data collection for .NET Framework HttpWebRequest activity source.
/// </summary>
/// <param name="builder"><see cref="OpenTelemetryBuilder"/> being configured.</param>
/// <returns>The instance of <see cref="OpenTelemetryBuilder"/> to chain the calls.</returns>
public static OpenTelemetryBuilder AddHttpWebRequestDependencyInstrumentation(this OpenTelemetryBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
GC.KeepAlive(HttpWebRequestActivitySource.Instance);
builder.AddActivitySource(HttpWebRequestActivitySource.ActivitySourceName);
return builder;
}
#endif
}
}

View File

@ -40,7 +40,6 @@ namespace OpenTelemetry.Trace.Configuration
.AddInstrumentation((t) => new AzureClientsInstrumentation(t))
.AddInstrumentation((t) => new AzurePipelineInstrumentation(t))
.AddInstrumentation((t) => new HttpClientInstrumentation(t))
.AddInstrumentation((t) => new HttpWebRequestInstrumentation(t))
.AddInstrumentation((t) => new SqlClientInstrumentation(t));
}
@ -71,7 +70,6 @@ namespace OpenTelemetry.Trace.Configuration
.AddInstrumentation((t) => new AzureClientsInstrumentation(t))
.AddInstrumentation((t) => new AzurePipelineInstrumentation(t))
.AddInstrumentation((t) => new HttpClientInstrumentation(t, httpOptions))
.AddInstrumentation((t) => new HttpWebRequestInstrumentation(t, httpOptions))
.AddInstrumentation((t) => new SqlClientInstrumentation(t, sqlOptions));
}
}

View File

@ -85,6 +85,21 @@ namespace OpenTelemetry.Instrumentation
this.WriteEvent(6, eventName);
}
[NonEvent]
public void ExceptionInitializingInstrumentation(string instrumentationType, Exception ex)
{
if (this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.ExceptionInitializingInstrumentation(instrumentationType, ToInvariantString(ex));
}
}
[Event(7, Message = "Error initializing instrumentation type {0}. Exception : {1}", Level = EventLevel.Error)]
public void ExceptionInitializingInstrumentation(string instrumentationType, string ex)
{
this.WriteEvent(7, instrumentationType, ex);
}
/// <summary>
/// Returns a culture-independent string representation of the given <paramref name="exception"/> object,
/// appropriate for diagnostics tracing.

View File

@ -33,10 +33,11 @@ namespace OpenTelemetry.Trace.Configuration
/// Enables OpenTelemetry.
/// </summary>
/// <param name="configureOpenTelemetryBuilder">Function that configures OpenTelemetryBuilder.</param>
/// <returns><see cref="IDisposable"/> to be disposed on application shutdown.</returns>
/// <remarks>
/// Basic implementation only. Most logic from TracerBuilder will be ported here.
/// </remarks>
public static void EnableOpenTelemetry(Action<OpenTelemetryBuilder> configureOpenTelemetryBuilder)
public static IDisposable EnableOpenTelemetry(Action<OpenTelemetryBuilder> configureOpenTelemetryBuilder)
{
var openTelemetryBuilder = new OpenTelemetryBuilder();
configureOpenTelemetryBuilder(openTelemetryBuilder);
@ -66,7 +67,7 @@ namespace OpenTelemetry.Trace.Configuration
// Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to
// or not
ShouldListenTo = (activitySource) => openTelemetryBuilder.ActivitySourceNames.Contains(activitySource.Name.ToUpperInvariant()),
ShouldListenTo = (activitySource) => openTelemetryBuilder.ActivitySourceNames?.Contains(activitySource.Name.ToUpperInvariant()) ?? false,
// The following parameter is not used now.
GetRequestedDataUsingParentId = (ref ActivityCreationOptions<string> options) => ActivityDataRequest.AllData,
@ -86,7 +87,7 @@ namespace OpenTelemetry.Trace.Configuration
var shouldSample = sampler.ShouldSample(
options.Parent,
options.Parent.TraceId,
default(ActivitySpanId), // Passing default SpanId here. The actual SpanId is not known before actual Activity creation
spanId: default, // Passing default SpanId here. The actual SpanId is not known before actual Activity creation
options.Name,
options.Kind,
options.Tags,
@ -105,6 +106,8 @@ namespace OpenTelemetry.Trace.Configuration
};
ActivitySource.AddActivityListener(listener);
return listener;
}
}
}

View File

@ -62,9 +62,12 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
[Fact]
public async Task HttpDependenciesInstrumentationInjectsHeadersAsync()
{
var spanProcessor = new Mock<SpanProcessor>();
var tracer = TracerFactory.Create(b => b.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
.GetTracer(null);
var activityProcessor = new Mock<ActivityProcessor>();
using var shutdownSignal = OpenTelemetrySdk.EnableOpenTelemetry(b =>
{
b.SetProcessorPipeline(c => c.AddProcessor(ap => activityProcessor.Object));
b.AddHttpWebRequestDependencyInstrumentation();
});
var request = (HttpWebRequest)WebRequest.Create(this.url);
@ -76,28 +79,26 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
using (new HttpWebRequestInstrumentation(tracer, new HttpClientInstrumentationOptions()))
{
using var response = await request.GetResponseAsync();
}
using var response = await request.GetResponseAsync();
Assert.Equal(2, spanProcessor.Invocations.Count); // begin and end was called
var span = (SpanData)spanProcessor.Invocations[1].Arguments[0];
Assert.Equal(2, activityProcessor.Invocations.Count); // begin and end was called
var activity = (Activity)activityProcessor.Invocations[1].Arguments[0];
Assert.Equal(parent.TraceId, span.Context.TraceId);
Assert.Equal(parent.SpanId, span.ParentSpanId);
Assert.NotEqual(parent.SpanId, span.Context.SpanId);
Assert.NotEqual(default, span.Context.SpanId);
Assert.Equal(parent.TraceId, activity.Context.TraceId);
Assert.Equal(parent.SpanId, activity.ParentSpanId);
Assert.NotEqual(parent.SpanId, activity.Context.SpanId);
Assert.NotEqual(default, activity.Context.SpanId);
string traceparent = request.Headers.Get("traceparent");
string tracestate = request.Headers.Get("tracestate");
Assert.Equal($"00-{span.Context.TraceId}-{span.Context.SpanId}-01", traceparent);
Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparent);
Assert.Equal("k1=v1,k2=v2", tracestate);
parent.Stop();
}
/* TBD: ActivitySource doesn't support custom format TraceIds.
[Fact]
public async Task HttpDependenciesInstrumentationInjectsHeadersAsync_CustomFormat()
{
@ -109,10 +110,12 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
action(message, "custom_tracestate", Activity.Current.TraceStateString);
});
var spanProcessor = new Mock<SpanProcessor>();
var tracer = TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
.GetTracer(null);
var activityProcessor = new Mock<ActivityProcessor>();
using var shutdownSignal = OpenTelemetrySdk.EnableOpenTelemetry(b =>
{
b.SetProcessorPipeline(c => c.AddProcessor(ap => activityProcessor.Object));
b.AddHttpWebRequestDependencyInstrumentation();
});
var request = (HttpWebRequest)WebRequest.Create(this.url);
@ -124,72 +127,35 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
using (new HttpWebRequestInstrumentation(tracer, new HttpClientInstrumentationOptions { TextFormat = textFormat.Object }))
{
using var response = await request.GetResponseAsync();
}
using var response = await request.GetResponseAsync();
Assert.Equal(2, spanProcessor.Invocations.Count); // begin and end was called
Assert.Equal(2, activityProcessor.Invocations.Count); // begin and end was called
var span = (SpanData)spanProcessor.Invocations[1].Arguments[0];
var activity = (Activity)activityProcessor.Invocations[1].Arguments[0];
Assert.Equal(parent.TraceId, span.Context.TraceId);
Assert.Equal(parent.SpanId, span.ParentSpanId);
Assert.NotEqual(parent.SpanId, span.Context.SpanId);
Assert.NotEqual(default, span.Context.SpanId);
Assert.Equal(parent.TraceId, activity.Context.TraceId);
Assert.Equal(parent.SpanId, activity.ParentSpanId);
Assert.NotEqual(parent.SpanId, activity.Context.SpanId);
Assert.NotEqual(default, activity.Context.SpanId);
string traceparent = request.Headers.Get("custom_traceparent");
string tracestate = request.Headers.Get("custom_tracestate");
Assert.Equal($"00/{span.Context.TraceId}/{span.Context.SpanId}/01", traceparent);
Assert.Equal($"00/{activity.Context.TraceId}/{activity.Context.SpanId}/01", traceparent);
Assert.Equal("k1=v1,k2=v2", tracestate);
parent.Stop();
}
[Fact]
public async Task HttpDependenciesInstrumentation_AddViaFactory_HttpInstrumentation_CollectsSpans()
{
var spanProcessor = new Mock<SpanProcessor>();
using (TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object))
.AddInstrumentation(t => new HttpWebRequestInstrumentation(t))))
{
using var c = new HttpClient();
await c.GetAsync(this.url);
}
Assert.Single(spanProcessor.Invocations.Where(i => i.Method.Name == "OnStart"));
Assert.Single(spanProcessor.Invocations.Where(i => i.Method.Name == "OnEnd"));
Assert.IsType<SpanData>(spanProcessor.Invocations[1].Arguments[0]);
}
[Fact]
public async Task HttpDependenciesInstrumentation_AddViaFactory_DependencyInstrumentation_CollectsSpans()
{
var spanProcessor = new Mock<SpanProcessor>();
using (TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object))
.AddDependencyInstrumentation()))
{
using var c = new HttpClient();
await c.GetAsync(this.url);
}
Assert.Single(spanProcessor.Invocations.Where(i => i.Method.Name == "OnStart"));
Assert.Single(spanProcessor.Invocations.Where(i => i.Method.Name == "OnEnd"));
Assert.IsType<SpanData>(spanProcessor.Invocations[1].Arguments[0]);
}
}*/
[Fact]
public async Task HttpDependenciesInstrumentationBacksOffIfAlreadyInstrumented()
{
var spanProcessor = new Mock<SpanProcessor>();
var tracer = TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
.GetTracer(null);
var activityProcessor = new Mock<ActivityProcessor>();
using var shutdownSignal = OpenTelemetrySdk.EnableOpenTelemetry(b =>
{
b.SetProcessorPipeline(c => c.AddProcessor(ap => activityProcessor.Object));
b.AddHttpWebRequestDependencyInstrumentation();
});
var request = new HttpRequestMessage
{
@ -199,13 +165,18 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01");
using (new HttpWebRequestInstrumentation(tracer, new HttpClientInstrumentationOptions()))
using (var activityListener = new ActivityListener
{
ShouldListenTo = (activitySource) => activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName,
})
{
ActivitySource.AddActivityListener(activityListener);
using var c = new HttpClient();
await c.SendAsync(request);
}
Assert.Equal(0, spanProcessor.Invocations.Count);
Assert.Equal(0, activityProcessor.Invocations.Count);
}
[Fact]
@ -218,12 +189,17 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
.GetTracer(null);
var options = new HttpClientInstrumentationOptions((activityName, arg1, _)
=> !(activityName == HttpWebRequestDiagnosticSource.ActivityName &&
=> !(activityName == HttpWebRequestActivitySource.ActivityName &&
arg1 is HttpWebRequest request &&
request.RequestUri.OriginalString.Contains(this.url)));
using (new HttpWebRequestInstrumentation(tracer, options))
using (var activityListener = new ActivityListener
{
ShouldListenTo = (activitySource) => activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName,
})
{
ActivitySource.AddActivityListener(activityListener);
using var c = new HttpClient();
await c.GetAsync(this.url);
}

View File

@ -16,6 +16,7 @@
#if NET461
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
@ -46,74 +47,67 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
out var host,
out var port);
var spanProcessor = new Mock<SpanProcessor>();
var tracer = TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
.GetTracer(null);
var activityProcessor = new Mock<ActivityProcessor>();
using var shutdownSignal = OpenTelemetrySdk.EnableOpenTelemetry(b =>
{
b.SetProcessorPipeline(c => c.AddProcessor(ap => activityProcessor.Object));
b.AddHttpWebRequestDependencyInstrumentation();
});
tc.Url = HttpTestData.NormalizeValues(tc.Url, host, port);
using (new HttpWebRequestInstrumentation(tracer, new HttpClientInstrumentationOptions() { SetHttpFlavor = tc.SetHttpFlavor }))
try
{
try
var request = (HttpWebRequest)WebRequest.Create(tc.Url);
request.Method = tc.Method;
if (tc.Headers != null)
{
var request = (HttpWebRequest)WebRequest.Create(tc.Url);
request.Method = tc.Method;
if (tc.Headers != null)
foreach (var header in tc.Headers)
{
foreach (var header in tc.Headers)
{
request.Headers.Add(header.Key, header.Value);
}
request.Headers.Add(header.Key, header.Value);
}
request.ContentLength = 0;
using var response = (HttpWebResponse)request.GetResponse();
new StreamReader(response.GetResponseStream()).ReadToEnd();
}
catch (Exception)
{
//test case can intentionally send request that will result in exception
}
request.ContentLength = 0;
using var response = (HttpWebResponse)request.GetResponse();
new StreamReader(response.GetResponseStream()).ReadToEnd();
}
catch (Exception)
{
//test case can intentionally send request that will result in exception
}
Assert.Equal(2, spanProcessor.Invocations.Count); // begin and end was called
var span = (SpanData)spanProcessor.Invocations[1].Arguments[0];
Assert.Equal(2, activityProcessor.Invocations.Count); // begin and end was called
var activity = (Activity)activityProcessor.Invocations[1].Arguments[0];
Assert.Equal(tc.SpanName, span.Name);
Assert.Equal(tc.SpanKind, span.Kind.ToString());
Assert.Equal(tc.SpanName, activity.DisplayName);
Assert.Equal(tc.SpanKind, activity.Kind.ToString());
var d = new Dictionary<StatusCanonicalCode, string>()
var d = new Dictionary<string, string>()
{
{ StatusCanonicalCode.Ok, "OK"},
{ StatusCanonicalCode.Cancelled, "CANCELLED"},
{ StatusCanonicalCode.Unknown, "UNKNOWN"},
{ StatusCanonicalCode.InvalidArgument, "INVALID_ARGUMENT"},
{ StatusCanonicalCode.DeadlineExceeded, "DEADLINE_EXCEEDED"},
{ StatusCanonicalCode.NotFound, "NOT_FOUND"},
{ StatusCanonicalCode.AlreadyExists, "ALREADY_EXISTS"},
{ StatusCanonicalCode.PermissionDenied, "PERMISSION_DENIED"},
{ StatusCanonicalCode.ResourceExhausted, "RESOURCE_EXHAUSTED"},
{ StatusCanonicalCode.FailedPrecondition, "FAILED_PRECONDITION"},
{ StatusCanonicalCode.Aborted, "ABORTED"},
{ StatusCanonicalCode.OutOfRange, "OUT_OF_RANGE"},
{ StatusCanonicalCode.Unimplemented, "UNIMPLEMENTED"},
{ StatusCanonicalCode.Internal, "INTERNAL"},
{ StatusCanonicalCode.Unavailable, "UNAVAILABLE"},
{ StatusCanonicalCode.DataLoss, "DATA_LOSS"},
{ StatusCanonicalCode.Unauthenticated, "UNAUTHENTICATED"},
{ StatusCanonicalCode.Ok.ToString(), "OK"},
{ StatusCanonicalCode.Cancelled.ToString(), "CANCELLED"},
{ StatusCanonicalCode.Unknown.ToString(), "UNKNOWN"},
{ StatusCanonicalCode.InvalidArgument.ToString(), "INVALID_ARGUMENT"},
{ StatusCanonicalCode.DeadlineExceeded.ToString(), "DEADLINE_EXCEEDED"},
{ StatusCanonicalCode.NotFound.ToString(), "NOT_FOUND"},
{ StatusCanonicalCode.AlreadyExists.ToString(), "ALREADY_EXISTS"},
{ StatusCanonicalCode.PermissionDenied.ToString(), "PERMISSION_DENIED"},
{ StatusCanonicalCode.ResourceExhausted.ToString(), "RESOURCE_EXHAUSTED"},
{ StatusCanonicalCode.FailedPrecondition.ToString(), "FAILED_PRECONDITION"},
{ StatusCanonicalCode.Aborted.ToString(), "ABORTED"},
{ StatusCanonicalCode.OutOfRange.ToString(), "OUT_OF_RANGE"},
{ StatusCanonicalCode.Unimplemented.ToString(), "UNIMPLEMENTED"},
{ StatusCanonicalCode.Internal.ToString(), "INTERNAL"},
{ StatusCanonicalCode.Unavailable.ToString(), "UNAVAILABLE"},
{ StatusCanonicalCode.DataLoss.ToString(), "DATA_LOSS"},
{ StatusCanonicalCode.Unauthenticated.ToString(), "UNAUTHENTICATED"},
};
Assert.Equal(tc.SpanStatus, d[span.Status.CanonicalCode]);
if (tc.SpanStatusHasDescription.HasValue)
Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(span.Status.Description));
var normalizedAttributes = span.Attributes.ToDictionary(
x => x.Key,
x => x.Value.ToString());
tc.SpanAttributes = tc.SpanAttributes.ToDictionary(
x => x.Key,
x =>
@ -123,7 +117,35 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
return HttpTestData.NormalizeValues(x.Value, host, port);
});
Assert.Equal(tc.SpanAttributes, normalizedAttributes);
foreach (KeyValuePair<string, string> tag in activity.Tags)
{
if (!tc.SpanAttributes.TryGetValue(tag.Key, out string value))
{
if (tag.Key == "http.flavor")
{
// http.flavor is optional in .NET Core instrumentation but there is no way to pass that option to the new ActivitySource model so it always shows up here.
if (tc.SetHttpFlavor)
{
Assert.Equal(value, tag.Value);
}
continue;
}
if (tag.Key == SpanAttributeConstants.StatusCodeKey)
{
Assert.Equal(tc.SpanStatus, d[tag.Value]);
continue;
}
if (tag.Key == SpanAttributeConstants.StatusDescriptionKey)
{
if (tc.SpanStatusHasDescription.HasValue)
Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(tag.Value));
continue;
}
Assert.True(false, $"Tag {tag.Key} was not found in test data.");
}
Assert.Equal(value, tag.Value);
}
}
[Fact]