generated from arcus-azure/arcus.github.template
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: track http request data in http trigger (#448)
* feat: track http request data in http trigger * pr-fix: better defect detection * pr-fix: timeout * Update docs/preview/03-Features/writing-different-telemetry-types.md Co-authored-by: Frederik Gheysels <frederik.gheysels@telenet.be> * pr-fix: more stable metric telemetry unit tests Co-authored-by: Frederik Gheysels <frederik.gheysels@telenet.be>
- Loading branch information
1 parent
68bd459
commit e192925
Showing
11 changed files
with
721 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
...cus.Observability.Telemetry.AzureFunctions/Extensions/ILoggerHttpRequestDataExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Net; | ||
using Arcus.Observability.Telemetry.Core; | ||
using Arcus.Observability.Telemetry.Core.Logging; | ||
using GuardNet; | ||
#if NET6_0 | ||
using Microsoft.Azure.Functions.Worker.Http; | ||
#endif | ||
|
||
// ReSharper disable once CheckNamespace | ||
namespace Microsoft.Extensions.Logging | ||
{ | ||
#if NET6_0 | ||
/// <summary> | ||
/// Telemetry extensions on the <see cref="ILogger"/> instance to write Application Insights compatible log messages. | ||
/// </summary> | ||
// ReSharper disable once InconsistentNaming | ||
public static class ILoggerRequestExtensions | ||
{ | ||
/// <summary> | ||
/// Logs an HTTP request. | ||
/// </summary> | ||
/// <param name="logger">The logger to track the telemetry.</param> | ||
/// <param name="request">The incoming HTTP request that was processed.</param> | ||
/// <param name="responseStatusCode">The HTTP status code returned by the service.</param> | ||
/// <param name="measurement">The instance to measure the duration of the HTTP request.</param> | ||
/// <param name="context">The context that provides more insights on the tracked HTTP request.</param> | ||
/// <exception cref="ArgumentNullException"> | ||
/// Thrown when the <paramref name="logger"/>, <paramref name="request"/>, or the <paramref name="measurement"/> is <c>null</c>. | ||
/// </exception> | ||
public static void LogRequest( | ||
this ILogger logger, | ||
HttpRequestData request, | ||
HttpStatusCode responseStatusCode, | ||
DurationMeasurement measurement, | ||
Dictionary<string, object> context = null) | ||
{ | ||
Guard.NotNull(logger, nameof(logger), "Requires a logger instance to track telemetry"); | ||
Guard.NotNull(request, nameof(request), "Requires a HTTP request instance to track a HTTP request"); | ||
Guard.NotNull(measurement, nameof(measurement), "Requires an measurement instance to time the duration of the HTTP request"); | ||
|
||
LogRequest(logger, request, responseStatusCode, measurement.StartTime, measurement.Elapsed, context); | ||
} | ||
|
||
/// <summary> | ||
/// Logs an HTTP request. | ||
/// </summary> | ||
/// <param name="logger">The logger to track the telemetry.</param> | ||
/// <param name="request">The incoming HTTP request that was processed.</param> | ||
/// <param name="responseStatusCode">The HTTP status code returned by the service.</param> | ||
/// <param name="startTime">The time when the HTTP request was received.</param> | ||
/// <param name="duration">The duration of the HTTP request processing operation.</param> | ||
/// <param name="context">The context that provides more insights on the tracked HTTP request.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="logger"/> or the <paramref name="request"/> is <c>null</c>.</exception> | ||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="duration"/> is a negative time range.</exception> | ||
public static void LogRequest( | ||
this ILogger logger, | ||
HttpRequestData request, | ||
HttpStatusCode responseStatusCode, | ||
DateTimeOffset startTime, | ||
TimeSpan duration, | ||
Dictionary<string, object> context = null) | ||
{ | ||
Guard.NotNull(logger, nameof(logger), "Requires a logger instance to track telemetry"); | ||
Guard.NotNull(request, nameof(request), "Requires a HTTP request instance to track a HTTP request"); | ||
Guard.NotLessThan(duration, TimeSpan.Zero, nameof(duration), "Requires a positive time duration of the request operation"); | ||
|
||
LogRequest(logger, request, responseStatusCode, operationName: null, startTime, duration, context); | ||
} | ||
|
||
/// <summary> | ||
/// Logs an HTTP request. | ||
/// </summary> | ||
/// <param name="logger">The logger to track the telemetry.</param> | ||
/// <param name="request">The incoming HTTP request that was processed.</param> | ||
/// <param name="responseStatusCode">The HTTP status code returned by the service.</param> | ||
/// <param name="operationName">The name of the operation of the request.</param> | ||
/// <param name="measurement">The instance to measure the duration of the HTTP request.</param> | ||
/// <param name="context">The context that provides more insights on the tracked HTTP request.</param> | ||
/// <exception cref="ArgumentNullException"> | ||
/// Thrown when the <paramref name="logger"/>, <paramref name="request"/>, or the <paramref name="measurement"/> is <c>null</c>. | ||
/// </exception> | ||
public static void LogRequest( | ||
this ILogger logger, | ||
HttpRequestData request, | ||
HttpStatusCode responseStatusCode, | ||
string operationName, | ||
DurationMeasurement measurement, | ||
Dictionary<string, object> context = null) | ||
{ | ||
Guard.NotNull(logger, nameof(logger), "Requires a logger instance to track telemetry"); | ||
Guard.NotNull(request, nameof(request), "Requires a HTTP request instance to track a HTTP request"); | ||
Guard.NotNull(measurement, nameof(measurement), "Requires an measurement instance to time the duration of the HTTP request"); | ||
|
||
LogRequest(logger, request, responseStatusCode, operationName, measurement.StartTime, measurement.Elapsed, context); | ||
} | ||
|
||
/// <summary> | ||
/// Logs an HTTP request. | ||
/// </summary> | ||
/// <param name="logger">The logger to track the telemetry.</param> | ||
/// <param name="request">The incoming HTTP request that was processed.</param> | ||
/// <param name="responseStatusCode">The HTTP status code returned by the service.</param> | ||
/// <param name="operationName">The name of the operation of the request.</param> | ||
/// <param name="startTime">The time when the HTTP request was received.</param> | ||
/// <param name="duration">The duration of the HTTP request processing operation.</param> | ||
/// <param name="context">The context that provides more insights on the tracked HTTP request.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="logger"/> or the <paramref name="request"/> is <c>null</c>.</exception> | ||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="duration"/> is a negative time range.</exception> | ||
public static void LogRequest( | ||
this ILogger logger, | ||
HttpRequestData request, | ||
HttpStatusCode responseStatusCode, | ||
string operationName, | ||
DateTimeOffset startTime, | ||
TimeSpan duration, | ||
Dictionary<string, object> context = null) | ||
{ | ||
Guard.NotNull(logger, nameof(logger), "Requires a logger instance to track telemetry"); | ||
Guard.NotNull(request, nameof(request), "Requires a HTTP request instance to track a HTTP request"); | ||
Guard.NotLessThan(duration, TimeSpan.Zero, nameof(duration), "Requires a positive time duration of the request operation"); | ||
|
||
context = context ?? new Dictionary<string, object>(); | ||
|
||
logger.LogWarning(MessageFormats.RequestFormat, | ||
RequestLogEntry.CreateForHttpRequest(request.Method, | ||
request.Url.Scheme, | ||
request.Url.Host, | ||
request.Url.AbsolutePath, | ||
operationName, | ||
(int) responseStatusCode, | ||
startTime, | ||
duration, | ||
context)); | ||
} | ||
} | ||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
...Observability.Tests.Integration/Serilog/Sinks/ApplicationInsights/HttpRequestDataTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using Arcus.Observability.Correlation; | ||
using Microsoft.Azure.ApplicationInsights.Query.Models; | ||
using Microsoft.Azure.Functions.Worker; | ||
using Microsoft.Azure.Functions.Worker.Http; | ||
using Microsoft.Extensions.Logging; | ||
using Moq; | ||
using Serilog; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace Arcus.Observability.Tests.Integration.Serilog.Sinks.ApplicationInsights | ||
{ | ||
public class HttpRequestDataTests : ApplicationInsightsSinkTests | ||
{ | ||
public HttpRequestDataTests(ITestOutputHelper outputWriter) : base(outputWriter) | ||
{ | ||
} | ||
|
||
[Fact] | ||
public async Task LogRequest_SinksToApplicationInsightsWithCorrelation_ResultsInRequestTelemetry() | ||
{ | ||
// Arrange | ||
var correlation = new CorrelationInfo($"operation-{Guid.NewGuid()}", $"transaction-{Guid.NewGuid()}", $"parent-{Guid.NewGuid()}"); | ||
var accessor = new DefaultCorrelationInfoAccessor(); | ||
accessor.SetCorrelationInfo(correlation); | ||
LoggerConfiguration.Enrich.WithCorrelationInfo(accessor); | ||
|
||
var operationName = "sampleoperation"; | ||
HttpMethod httpMethod = GenerateHttpMethod(); | ||
var requestUri = new Uri(BogusGenerator.Internet.UrlWithPath()); | ||
HttpRequestData request = CreateStubRequest(httpMethod, requestUri); | ||
var statusCode = BogusGenerator.PickRandom<HttpStatusCode>(); | ||
|
||
TimeSpan duration = BogusGenerator.Date.Timespan(); | ||
DateTimeOffset startTime = DateTimeOffset.Now; | ||
Dictionary<string, object> telemetryContext = CreateTestTelemetryContext(); | ||
|
||
// Act | ||
Logger.LogRequest(request, statusCode, operationName, startTime, duration, telemetryContext); | ||
|
||
// Assert | ||
await RetryAssertUntilTelemetryShouldBeAvailableAsync(async client => | ||
{ | ||
EventsRequestResult[] results = await client.GetRequestsAsync(); | ||
AssertX.Any(results, result => | ||
{ | ||
Assert.Equal($"{requestUri.Scheme}://{requestUri.Host}{requestUri.AbsolutePath}", result.Request.Url); | ||
Assert.Equal(((int) statusCode).ToString(), result.Request.ResultCode); | ||
Assert.Equal($"{httpMethod.Method} {operationName}", result.Operation.Name); | ||
Assert.Equal(correlation.OperationId, result.Request.Id); | ||
Assert.Equal(correlation.TransactionId, result.Operation.Id); | ||
Assert.Equal(correlation.OperationParentId, result.Operation.ParentId); | ||
}); | ||
}); | ||
} | ||
|
||
private HttpMethod GenerateHttpMethod() | ||
{ | ||
return BogusGenerator.PickRandom( | ||
HttpMethod.Get, | ||
HttpMethod.Delete, | ||
HttpMethod.Head, | ||
HttpMethod.Options, | ||
HttpMethod.Patch, | ||
HttpMethod.Post, | ||
HttpMethod.Put, | ||
HttpMethod.Trace); | ||
} | ||
|
||
private static HttpRequestData CreateStubRequest(HttpMethod method, Uri requestUri) | ||
{ | ||
var context = Mock.Of<FunctionContext>(); | ||
|
||
var stub = new Mock<HttpRequestData>(context); | ||
stub.Setup(s => s.Method).Returns(method.Method); | ||
stub.Setup(s => s.Url).Returns(requestUri); | ||
|
||
return stub.Object; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.