Description
Background and motivation
The System.ClientModel library provides building blocks for .NET clients that call cloud services. For background, this package has been reviewed in the following previous issues: #94126 | #97711 | #104617 | #106197 | #111046
These additional APIs are adding extension methods for client library authors to add distributed tracing to their libraries built on System.ClientModel. Client libraries have the following spans:
- HTTP spans - these are provided by HttpClient, we will not be duplicating these in System.ClientModel
- Spans for each call to public APIs that send HTTP requests - this proposal addresses this area
The proposed API is simply adding extension methods to Activity and ActivitySource for library authors to use to create distributed tracing spans in each public method call. These methods provide common base functionality that is applicable to all client libraries. Library authors can then use the returned Activity directly to add any additional functionality to meet their own needs.
API Proposal
namespace System.ClientModel.Primitives
{
+ public static partial class ActivityExtensions
+ {
+ public static System.Diagnostics.Activity MarkFailed(this System.Diagnostics.Activity activity, System.Exception? exception) { throw null; }
+ public static System.Diagnostics.Activity? StartClientActivity(this System.Diagnostics.ActivitySource activitySource, System.ClientModel.Primitives.ClientPipelineOptions options, string name, System.Diagnostics.ActivityKind kind = System.Diagnostics.ActivityKind.Internal, System.Diagnostics.ActivityContext parentContext = default(System.Diagnostics.ActivityContext), System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, object?>>? tags = null) { throw null; }
+ }
...
public partial class ClientPipelineOptions
{
public ClientPipelineOptions() { }
public System.ClientModel.Primitives.ClientLoggingOptions? ClientLoggingOptions { get { throw null; } set { } }
+ public bool? EnableDistributedTracing { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelinePolicy? MessageLoggingPolicy { get { throw null; } set { } }
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelinePolicy? RetryPolicy { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelineTransport? Transport { get { throw null; } set { } }
public void AddPolicy(System.ClientModel.Primitives.PipelinePolicy policy, System.ClientModel.Primitives.PipelinePosition position) { }
protected void AssertNotFrozen() { }
public virtual void Freeze() { }
}
...
}
...
API Usage
public class SampleClient
{
private readonly Uri _endpoint;
private readonly ApiKeyCredential _credential;
private readonly ClientPipeline _pipeline;
private readonly SampleClientOptions _sampleClientOptions;
// Each client should have a static ActivitySource named after the full name
// of the client.
// Tracing should start as experimental.
internal static ActivitySource ActivitySource { get; } = new($"Experimental.{typeof(MapsClient).FullName!}");
public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default)
{
options ??= new SampleClientOptions();
_sampleClientOptions = options;
_endpoint = endpoint;
_credential = credential;
ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential);
_pipeline = ClientPipeline.Create(options,
perCallPolicies: ReadOnlySpan<PipelinePolicy>.Empty,
perTryPolicies: new PipelinePolicy[] { authenticationPolicy },
beforeTransportPolicies: ReadOnlySpan<PipelinePolicy>.Empty);
}
public ClientResult<SampleResource> UpdateResource(SampleResource resource)
{
// Attempt to create and start an Activity for this operation.
// StartClientActivity does nothing and returns null if distributed tracing was
// disabled by the consuming application or if there are not any active listeners.
using Activity? activity = ActivitySource.StartClientActivity(_sampleClientOptions, $"{nameof(SampleClient)}.{nameof(UpdateResource)}");
try
{
using PipelineMessage message = _pipeline.CreateMessage();
PipelineRequest request = message.Request;
request.Method = "PATCH";
request.Uri = new Uri($"https://www.example.com/update?id={resource.Id}");
request.Headers.Add("Accept", "application/json");
request.Content = BinaryContent.Create(resource);
_pipeline.Send(message);
PipelineResponse response = message.Response!;
if (response.IsError)
{
throw new ClientResultException(response);
}
SampleResource updated = ModelReaderWriter.Read<SampleResource>(response.Content)!;
return ClientResult.FromValue(updated, response);
}
catch (Exception ex)
{
// Catch any exceptions and update the activity. Then re-throw the exception.
activity?.MarkFailed(ex);
throw;
}
}
}
Alternative Designs
No response
Risks
No response