Skip to content

Commit

Permalink
Update to latest (#4029)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Taillefer <mataille@microsoft.com>
  • Loading branch information
geeknoid and Martin Taillefer authored Jun 2, 2023
1 parent a5738f6 commit fc19f16
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.AmbientMetadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Telemetry;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Telemetry.Latency;
Expand All @@ -25,18 +27,22 @@ internal sealed class RequestLatencyTelemetryMiddleware : IMiddleware
private static readonly ObjectPool<CancellationTokenSource> _cancellationTokenSourcePool = PoolFactory.CreateCancellationTokenSourcePool();

private readonly TimeSpan _exportTimeout;

private readonly string _applicationName;
private readonly ILatencyDataExporter[] _latencyDataExporters;

/// <summary>
/// Initializes a new instance of the <see cref="RequestLatencyTelemetryMiddleware"/> class.
/// </summary>
/// <param name="options">An instance of <see cref="RequestLatencyTelemetryOptions"/>.</param>
/// <param name="latencyDataExporters">The list of exporters for latency data.</param>
public RequestLatencyTelemetryMiddleware(IOptions<RequestLatencyTelemetryOptions> options, IEnumerable<ILatencyDataExporter> latencyDataExporters)
public RequestLatencyTelemetryMiddleware(
IOptions<RequestLatencyTelemetryOptions> options,
IEnumerable<ILatencyDataExporter> latencyDataExporters,
IOptions<ApplicationMetadata>? appMetdata = null)
{
_exportTimeout = options.Value.LatencyDataExportTimeout;
_latencyDataExporters = latencyDataExporters.ToArray();
_applicationName = string.Empty;

if (appMetdata != null)
{
_applicationName = appMetdata.Value.ApplicationName;
}
}

/// <summary>
Expand All @@ -49,6 +55,16 @@ public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var latencyContext = context.RequestServices.GetRequiredService<ILatencyContext>();

if (!string.IsNullOrEmpty(_applicationName))
{
context.Response.OnStarting(ctx =>
{
var httpContext = (HttpContext)ctx;
httpContext.Response.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, _applicationName);
return Task.CompletedTask;
}, context);
}

context.Response.OnCompleted(async l =>
{
var latencyContext = l as ILatencyContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -18,7 +19,8 @@ namespace Microsoft.Extensions.Telemetry.Logging;
/// OpenTelemetry Logger provider class.
/// </summary>
[ProviderAlias("R9")]
internal sealed class LoggerProvider : BaseProvider, ILoggerProvider, ISupportExternalScope
[Experimental]
public sealed class LoggerProvider : BaseProvider, ILoggerProvider, ISupportExternalScope
{
private const int ProcessorShutdownGracePeriodInMs = 5000;
private readonly ConcurrentDictionary<string, Logger> _loggers = new();
Expand All @@ -31,7 +33,7 @@ internal sealed class LoggerProvider : BaseProvider, ILoggerProvider, ISupportEx
/// <param name="loggingOptions">Logger options.</param>
/// <param name="enrichers">Collection of enrichers.</param>
/// <param name="processors">Collection of processors.</param>
public LoggerProvider(
internal LoggerProvider(
IOptions<LoggingOptions> loggingOptions,
IEnumerable<ILogEnricher> enrichers,
IEnumerable<BaseProcessor<LogRecord>> processors)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options.Validation;
using Microsoft.Extensions.Telemetry.Enrichment;
using Microsoft.Shared.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Logs;
Expand All @@ -29,7 +31,7 @@ public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder build
_ = Throw.IfNull(builder);
_ = Throw.IfNull(section);

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, LoggerProvider>());
builder.Services.TryAddLoggerProvider();
_ = builder.Services.AddValidatedOptions<LoggingOptions, LoggingOptionsValidator>().Bind(section);

return builder;
Expand All @@ -46,7 +48,8 @@ public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder build
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configure);

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, LoggerProvider>());
builder.Services.TryAddLoggerProvider();

_ = builder.Services.AddValidatedOptions<LoggingOptions, LoggingOptionsValidator>().Configure(configure);

return builder;
Expand Down Expand Up @@ -90,4 +93,13 @@ public static ILoggingBuilder AddProcessor<T>(this ILoggingBuilder builder)

return builder;
}

private static void TryAddLoggerProvider(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, LoggerProvider>(
sp => new LoggerProvider(
sp.GetRequiredService<IOptions<LoggingOptions>>(),
sp.GetServices<ILogEnricher>(),
sp.GetServices<BaseProcessor<LogRecord>>())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ public DownstreamDependencyMetadataManager(IEnumerable<IDownstreamDependencyMeta
return null;
}

string dependencyName = GetHostDependencyName(requestMessage.RequestUri.Host);
return GetRequestMetadataInternal(requestMessage.Method.Method, requestMessage.RequestUri.AbsolutePath, dependencyName);
var hostMetadata = GetHostMetadata(requestMessage.RequestUri.Host);
return GetRequestMetadataInternal(requestMessage.Method.Method, requestMessage.RequestUri.AbsolutePath, hostMetadata);
}
catch (Exception)
{
Expand All @@ -65,8 +65,8 @@ public DownstreamDependencyMetadataManager(IEnumerable<IDownstreamDependencyMeta
#pragma warning disable CA1031 // Do not catch general exception types
try
{
string dependencyName = GetHostDependencyName(requestMessage.RequestUri.Host);
return GetRequestMetadataInternal(requestMessage.Method, requestMessage.RequestUri.AbsolutePath, dependencyName);
var hostMetadata = GetHostMetadata(requestMessage.RequestUri.Host);
return GetRequestMetadataInternal(requestMessage.Method, requestMessage.RequestUri.AbsolutePath, hostMetadata);
}
catch (Exception)
{
Expand Down Expand Up @@ -290,18 +290,21 @@ private void AddHostnameToTrie(string hostNameSuffix, string dependencyName)
}

trieCurrent.DependencyName = dependencyName;
trieCurrent.RequestMetadata.DependencyName = dependencyName;
trieCurrent.RequestMetadata.MethodType = string.Empty;
}

private string GetHostDependencyName(string host)
private HostSuffixTrieNode? GetHostMetadata(string host)
{
HostSuffixTrieNode? hostMetadataNode = null;
string dependencyName = string.Empty;
var trieCurrent = _hostSuffixTrieRoot;
for (int i = host.Length - 1; i >= 0; i--)
{
char ch = host[i];
if (ch >= Constants.ASCIICharCount)
{
return string.Empty;
return null;
}

ch = _toUpper[ch];
Expand All @@ -313,20 +316,25 @@ private string GetHostDependencyName(string host)
trieCurrent = trieCurrent.Nodes[ch];
if (!string.IsNullOrEmpty(trieCurrent.DependencyName))
{
dependencyName = trieCurrent.DependencyName;
hostMetadataNode = trieCurrent;
}
}

return dependencyName;
return hostMetadataNode;
}

private RequestMetadata? GetRequestMetadataInternal(string httpMethod, string requestPath, string dependencyName)
private RequestMetadata? GetRequestMetadataInternal(string httpMethod, string requestPath, HostSuffixTrieNode? hostMetadata)
{
if (!_frozenProcessedMetadataMap.TryGetValue(dependencyName, out var routeMetadataTrieRoot))
if (hostMetadata == null)
{
return null;
}

if (!_frozenProcessedMetadataMap.TryGetValue(hostMetadata.DependencyName, out var routeMetadataTrieRoot))
{
return hostMetadata.RequestMetadata;
}

var trieCurrent = routeMetadataTrieRoot.Nodes[0];
var lastStartNode = trieCurrent;
var requestPathEndIndex = requestPath.Length;
Expand Down Expand Up @@ -387,12 +395,16 @@ private string GetHostDependencyName(string host)
var childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot);
if (childNode == null)
{
return null;
// Return the default request metadata for the host which
// contains only the dependency name, but no other route/request info.
return hostMetadata.RequestMetadata;
}

trieCurrent = childNode;
}

return trieCurrent.RequestMetadataEntryIndex == -1 ? null : routeMetadataTrieRoot.RequestMetadatas[trieCurrent.RequestMetadataEntryIndex];
return trieCurrent.RequestMetadataEntryIndex == -1 ?
hostMetadata.RequestMetadata : // Return the default request metadata for this host if no matching route is found.
routeMetadataTrieRoot.RequestMetadatas[trieCurrent.RequestMetadataEntryIndex];
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Http.Telemetry;

namespace Microsoft.Extensions.Telemetry;
internal sealed class HostSuffixTrieNode
{
private const int ASCIICharCount = 128;

public string DependencyName { get; set; } = string.Empty;

public RequestMetadata RequestMetadata { get; } = new RequestMetadata();

public HostSuffixTrieNode[] Nodes { get; } = new HostSuffixTrieNode[ASCIICharCount];
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Telemetry.Internal;
using Microsoft.Extensions.AmbientMetadata;
using Microsoft.Extensions.Http.Telemetry;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Telemetry.Latency;
using Moq;
Expand All @@ -19,22 +21,81 @@ public class RequestLatencyTelemetryMiddlewareTest
{
[Fact]
public async Task RequestLatency_GivenContext_InvokesOperations()
{
var ex1 = new TestExporter();
var ex2 = new TestExporter();
string serverName = "AppServer";
var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), new List<ILatencyDataExporter> { ex1, ex2 },
Options.Create(new ApplicationMetadata { ApplicationName = serverName }));

var lc = GetMockLatencyContext();
var httpContextMock = GetHttpContext(lc.Object);
var fakeHttpResponseFeature = new FakeHttpResponseFeature();
httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);

var nextInvoked = false;
await m.InvokeAsync(httpContextMock, (_) =>
{
nextInvoked = true;
return Task.CompletedTask;
});
await fakeHttpResponseFeature.StartAsync();
lc.Verify(c => c.Freeze());
var header = httpContextMock.Response.Headers[TelemetryConstants.ServerApplicationNameHeader];
Assert.NotEmpty(header);
Assert.Equal(serverName, header[0]);
Assert.True(nextInvoked);
Assert.True(ex1.Invoked == 1);
Assert.True(ex2.Invoked == 1);
}

[Fact]
public async Task RequestLatency_WithoutServiceMetadata_InvokesOperations()
{
var ex1 = new TestExporter();
var ex2 = new TestExporter();
var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), new List<ILatencyDataExporter> { ex1, ex2 });

var lc = GetMockLatencyContext();
var httpContextMock = GetHttpContext(lc.Object);
var fakeHttpResponseFeature = new FakeHttpResponseFeature();
httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);

var nextInvoked = false;
await m.InvokeAsync(httpContextMock, (_) =>
{
nextInvoked = true;
return Task.CompletedTask;
});
await fakeHttpResponseFeature.StartAsync();
lc.Verify(c => c.Freeze());
Assert.False(httpContextMock.Response.Headers.TryGetValue(TelemetryConstants.ServerApplicationNameHeader, out var val));
Assert.True(nextInvoked);
Assert.True(ex1.Invoked == 1);
Assert.True(ex2.Invoked == 1);
}

[Fact]
public async Task RequestLatency_NoServiceData_DoesNotAddHeader()
{
var ex1 = new TestExporter();
var ex2 = new TestExporter();
var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), new List<ILatencyDataExporter> { ex1, ex2 }, Options.Create(new ApplicationMetadata()));

var lc = GetMockLatencyContext();
var httpContextMock = GetHttpContext(lc.Object);
var fakeHttpResponseFeature = new FakeHttpResponseFeature();
httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);

var nextInvoked = false;
await m.InvokeAsync(httpContextMock, (_) =>
{
nextInvoked = true;
return Task.CompletedTask;
});
await fakeHttpResponseFeature.StartAsync();
lc.Verify(c => c.Freeze());
Assert.False(httpContextMock.Response.Headers.TryGetValue(TelemetryConstants.ServerApplicationNameHeader, out var val));
Assert.True(nextInvoked);
Assert.True(ex1.Invoked == 1);
Assert.True(ex2.Invoked == 1);
Expand All @@ -45,7 +106,7 @@ public async Task RequestLatency_NoExporter()
{
var lc = GetMockLatencyContext();
var httpContextMock = GetHttpContext(lc.Object);
var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), Array.Empty<ILatencyDataExporter>());
var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), Array.Empty<ILatencyDataExporter>(), Options.Create(new ApplicationMetadata()));

var nextInvoked = false;
await m.InvokeAsync(httpContextMock, (_) =>
Expand All @@ -66,7 +127,8 @@ public async Task RequestLatency_GivenTimeout_PassedToExport()

var m = new RequestLatencyTelemetryMiddleware(
Options.Create(new RequestLatencyTelemetryOptions { LatencyDataExportTimeout = exportTimeout }),
new List<ILatencyDataExporter> { ex1 });
new List<ILatencyDataExporter> { ex1 },
Options.Create(new ApplicationMetadata()));

var lc = GetMockLatencyContext();
var httpContextMock = GetHttpContext(lc.Object);
Expand All @@ -83,6 +145,34 @@ await m.InvokeAsync(httpContextMock, (_) =>
Assert.True(nextInvoked);
}

private sealed class FakeHttpResponseFeature : HttpResponseFeature
{
private Func<Task> _responseStartingAsync =
static () => Task.CompletedTask;

public override void OnStarting(Func<object, Task> callback, object state)
{
ChainCallback(callback, state);
}

public override void OnCompleted(Func<object, Task> callback, object state)
{
ChainCallback(callback, state);
}

private void ChainCallback(Func<object, Task> callback, object state)
{
var prior = _responseStartingAsync;
_responseStartingAsync = async () =>
{
await prior();
await callback(state);
};
}

public async Task StartAsync() => await _responseStartingAsync();
}

private static HttpContext GetHttpContext(ILatencyContext latencyContext)
{
var httpContextMock = new DefaultHttpContext();
Expand Down
Loading

0 comments on commit fc19f16

Please sign in to comment.