Skip to content

Commit

Permalink
Added System.Diagnostics.DiagnosticSource instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
bastianeicher committed May 12, 2020
1 parent 00f76a3 commit 73b577a
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 47 deletions.
34 changes: 29 additions & 5 deletions src/TypedRest.OAuth/OAuthHandler.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -79,14 +81,36 @@ private async Task<string> DiscoverTokenEndpointAsync(CancellationToken cancella
return response.TokenEndpoint;
}

private static async Task<TResponse> HandleAsync<TResponse>(Func<Task<TResponse>> request)
private static async Task<TResponse> HandleAsync<TResponse>(Func<Task<TResponse>> request, [CallerMemberName] string caller = "unknown")
where TResponse : ProtocolResponse
{
var response = await request().ConfigureAwait(false);
var activity = new Activity(nameof(OAuthHandler) + "::" + caller.Substring(0, caller.Length - "Async".Length))
.AddTag("component", "TypedRest.OAuth")
.AddTag("span.kind", "client")
.Start();

if (response.Exception != null) throw response.Exception;
if (response.IsError) throw new AuthenticationException(response.Error);
return response;
try
{
var response = await request().ConfigureAwait(false);
activity.AddTag("http.url", response.HttpResponse.RequestMessage.RequestUri.ToString())
.AddTag("http.method", response.HttpResponse.RequestMessage.Method.Method)
.AddTag("http.status_code", ((int)response.HttpResponse.StatusCode).ToString());

if (response.Exception != null) throw response.Exception;
if (response.IsError) throw new AuthenticationException(response.Error);
return response;
}
catch (Exception ex)
{
activity.AddTag("error", "true")
.AddTag("error.type", ex.GetType().Name)
.AddTag("error.message", ex.Message);
throw;
}
finally
{
activity.Stop();
}
}

private async Task<HttpResponseMessage> SendAuthenticatedAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand Down
1 change: 1 addition & 0 deletions src/TypedRest.OAuth/TypedRest.OAuth.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

<ItemGroup>
<PackageReference Include="IdentityModel" Version="4.2.0" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.7.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
Expand Down
9 changes: 5 additions & 4 deletions src/TypedRest.Reactive/Endpoints/Reactive/PollingEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Net.Http;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using TypedRest.Endpoints.Generic;

Expand Down Expand Up @@ -39,9 +40,9 @@ public PollingEndpoint(IEndpoint referrer, string relativeUri, Predicate<TEntity
_endCondition = endCondition;
}

protected override async Task<HttpResponseMessage> HandleAsync(Func<Task<HttpResponseMessage>> request)
protected override async Task<HttpResponseMessage> HandleAsync(Func<Task<HttpResponseMessage>> request, [CallerMemberName] string caller = "unknown")
{
var response = await base.HandleAsync(request);
var response = await base.HandleAsync(request, caller);
PollingInterval =
response.Headers.RetryAfter?.Delta ??
(response.Headers.RetryAfter?.Date - DateTime.UtcNow) ??
Expand All @@ -52,7 +53,7 @@ protected override async Task<HttpResponseMessage> HandleAsync(Func<Task<HttpRes
public TimeSpan PollingInterval { get; set; } = TimeSpan.FromSeconds(3);

public IObservable<TEntity> GetObservable()
=> Observable.Create<TEntity>(async (observer, cancellationToken) =>
=> Observable.Create<TEntity>((observer, cancellationToken) => TracedAsync(async _ =>
{
TEntity previousEntity;
try
Expand Down Expand Up @@ -93,6 +94,6 @@ public IObservable<TEntity> GetObservable()
previousEntity = newEntity;
}
observer.OnCompleted();
});
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public StreamingCollectionEndpoint(IEndpoint referrer, string relativeUri)
{}

public IObservable<TEntity> GetObservable(long startIndex = 0)
=> Observable.Create<TEntity>(async (observer, cancellationToken) =>
=> Observable.Create<TEntity>((observer, cancellationToken) => TracedAsync(async _ =>
{
long currentStartIndex = startIndex;
while (!cancellationToken.IsCancellationRequested)
Expand Down Expand Up @@ -73,6 +73,6 @@ public IObservable<TEntity> GetObservable(long startIndex = 0)
if (response.Range?.To == null) return;
currentStartIndex = response.Range.To.Value + 1;
}
});
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ public StreamingEndpoint(IEndpoint referrer, string relativeUri, string separato
public int BufferSize { get; set; } = 64 * 1024;

public virtual IObservable<TEntity> GetObservable()
=> Observable.Create<TEntity>(async (observer, cancellationToken) =>
=> Observable.Create<TEntity>((observer, cancellationToken) => TracedAsync(async activity =>
{
activity.AddTag("http.method", HttpMethod.Get.Method);

using var response = await HttpClient.GetAsync(Uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
await ErrorHandler.HandleAsync(response);
Expand All @@ -70,12 +72,13 @@ public virtual IObservable<TEntity> GetObservable()
}
catch (Exception ex)
{
activity.AddException(ex);
observer.OnError(ex);
return;
}

observer.OnNext(entity);
}
});
}));
}
}
19 changes: 19 additions & 0 deletions src/TypedRest/ActivityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Diagnostics;

namespace TypedRest
{
/// <summary>
/// Provides extension methods for <see cref="Activity"/>.
/// </summary>
public static class ActivityExtensions
{
/// <summary>
/// Updates the <paramref name="activity"/> to have tags encoding the specified <paramref name="exception"/>.
/// </summary>
public static Activity AddException(this Activity activity, Exception exception)
=> activity.AddTag("error", "true")
.AddTag("error.type", exception.GetType().Name)
.AddTag("error.message", exception.Message);
}
}
79 changes: 72 additions & 7 deletions src/TypedRest/Endpoints/EndpointBase.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using TypedRest.Errors;
using TypedRest.Http;
Expand Down Expand Up @@ -91,19 +93,82 @@ public void SetDefaultLinkTemplate(string rel, string? href)
/// Handles various cross-cutting concerns regarding a response message such as discovering links and handling errors.
/// </summary>
/// <param name="request">A callback that performs the actual HTTP request.</param>
/// <param name="caller">The name of the method calling this method.</param>
/// <returns>The resolved <paramref name="request"/>.</returns>
protected virtual async Task<HttpResponseMessage> HandleAsync(Func<Task<HttpResponseMessage>> request)
protected virtual Task<HttpResponseMessage> HandleAsync(Func<Task<HttpResponseMessage>> request, [CallerMemberName] string caller = "unknown")
=> TracedAsync(async activity =>
{
var response = await request().NoContext();
activity.AddTag("http.method", response.RequestMessage.Method.Method)
.AddTag("http.status_code", ((int)response.StatusCode).ToString());

(_links, _linkTemplates) = await LinkHandler.HandleAsync(response).NoContext();

HandleCapabilities(response);

if (!response.IsSuccessStatusCode)
await ErrorHandler.HandleAsync(response).NoContext();

return response;
});

/// <summary>
/// Runs an operation in the context of a tracing span.
/// </summary>
/// <param name="operation">Callback to run the operation. Passes in an <see cref="Activity"/> to record additional tracing information.</param>
/// <param name="caller">The name of the method calling this method.</param>
protected async Task TracedAsync(Func<Activity, Task> operation, [CallerMemberName] string caller = "unknown")
{
var response = await request().NoContext();
var activity = StartActivity(caller);

(_links, _linkTemplates) = await LinkHandler.HandleAsync(response).NoContext();
try
{
await operation(activity).NoContext();
}
catch (Exception ex)
{
activity.AddException(ex);
throw;
}
finally
{
activity.Stop();
}
}

HandleCapabilities(response);
/// <summary>
/// Runs an operation in the context of a tracing span.
/// </summary>
/// <param name="operation">Callback to run the operation. Passes in an <see cref="Activity"/> to record additional tracing information.</param>
/// <param name="caller">The name of the method calling this method.</param>
protected async Task<T> TracedAsync<T>(Func<Activity, Task<T>> operation, [CallerMemberName] string caller = "unknown")
{
var activity = StartActivity(caller);

if (!response.IsSuccessStatusCode)
await ErrorHandler.HandleAsync(response).NoContext();
try
{
return await operation(activity).NoContext();
}
catch (Exception ex)
{
activity.AddException(ex);
throw;
}
finally
{
activity.Stop();
}
}

private Activity StartActivity(string caller)
{
if (caller.EndsWith("Async")) caller = caller.Substring(0, caller.Length - "Async".Length);

return response;
return new Activity(GetType().Name + "::" + caller)
.AddTag("component", "TypedRest")
.AddTag("span.kind", "client")
.AddTag("http.url", Uri.ToString())
.Start();
}

// NOTE: Always replace entire dictionary rather than modifying it to ensure thread-safety.
Expand Down
43 changes: 22 additions & 21 deletions src/TypedRest/Endpoints/Generic/ElementEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,29 +93,30 @@ public async Task<bool> ExistsAsync(CancellationToken cancellationToken = defaul
public virtual async Task DeleteAsync(CancellationToken cancellationToken = default)
=> await DeleteContentAsync(cancellationToken);

public async Task<TEntity?> UpdateAsync(Action<TEntity> updateAction, int maxRetries = 3, CancellationToken cancellationToken = default)
{
int retryCounter = 0;
while (true)
public Task<TEntity?> UpdateAsync(Action<TEntity> updateAction, int maxRetries = 3, CancellationToken cancellationToken = default)
=> TracedAsync(async _ =>
{
var entity = await ReadAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

updateAction(entity);
cancellationToken.ThrowIfCancellationRequested();

try
{
return await SetAsync(entity, cancellationToken);
}
catch (InvalidOperationException ex)
int retryCounter = 0;
while (true)
{
if (retryCounter++ >= maxRetries) throw;
await ex.HttpRetryDelayAsync(cancellationToken);
var entity = await ReadAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

updateAction(entity);
cancellationToken.ThrowIfCancellationRequested();

try
{
return await SetAsync(entity, cancellationToken);
}
catch (InvalidOperationException ex)
{
if (retryCounter++ >= maxRetries) throw;
await ex.HttpRetryDelayAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
}
}
}
});

#if NETSTANDARD
public async Task<TEntity?> UpdateAsync(Action<JsonPatchDocument<TEntity>> patchAction, int maxRetries = 3, CancellationToken cancellationToken = default)
Expand All @@ -126,13 +127,13 @@ public virtual async Task DeleteAsync(CancellationToken cancellationToken = defa
var patch = new JsonPatchDocument<TEntity>(new List<Operation<TEntity>>(), serializer.SerializerSettings.ContractResolver);
patchAction(patch);

var response = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethods.Patch, Uri)
var response = await TracedAsync(_ => HttpClient.SendAsync(new HttpRequestMessage(HttpMethods.Patch, Uri)
{
Content = new StringContent(JsonConvert.SerializeObject(patch))
{
Headers = {ContentType = new MediaTypeHeaderValue("application/json-patch+json")}
}
}, cancellationToken).NoContext();
}, cancellationToken)).NoContext();

if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.MethodNotAllowed)
return await UpdateAsync(patch.ApplyTo, maxRetries, cancellationToken);
Expand Down
Loading

0 comments on commit 73b577a

Please sign in to comment.