Skip to content

Add request tracing for content type #646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4a7d9dc
add initial content type pattern
amerjusupovic Apr 7, 2025
8c59edb
format fix
amerjusupovic Apr 7, 2025
8faa194
fix comment
amerjusupovic Apr 7, 2025
54a6b1c
update to account for chat completion vs ai profiles
amerjusupovic Apr 11, 2025
92377e3
in progress fix adapter to use existing requesttracingoptions
amerjusupovic Apr 11, 2025
dd1d538
use content type tracing to pass to requesttracingoptions
amerjusupovic Apr 11, 2025
51ecabf
fix comments and naming
amerjusupovic Apr 11, 2025
a75e9ac
remove unneeded file
amerjusupovic Apr 11, 2025
93a5259
add check for request tracing enabled
amerjusupovic Apr 14, 2025
140503f
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Dotn…
amerjusupovic Apr 14, 2025
99d0132
check content type in preparedata
amerjusupovic Apr 14, 2025
5d98e66
remove errors
amerjusupovic Apr 14, 2025
0b11741
fix spacing
amerjusupovic Apr 14, 2025
52942b7
fix test
amerjusupovic Apr 15, 2025
f036f0b
remove unused usings, add back catch for .net framework
amerjusupovic Apr 15, 2025
4ee9199
fix parsing
amerjusupovic Apr 15, 2025
5b06d28
rename constants
amerjusupovic Apr 15, 2025
012256a
fix indent
amerjusupovic Apr 15, 2025
e2cfb46
update for PR comments
amerjusupovic Apr 16, 2025
f483ca7
PR comments, update if conditions
amerjusupovic Apr 16, 2025
ed90256
add isjson check
amerjusupovic Apr 16, 2025
2e0a547
update isjson extension
amerjusupovic Apr 16, 2025
f70f91a
Merge branch 'main' into ajusupovic/contenttype-tracing
amerjusupovic Apr 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ internal IEnumerable<IKeyValueAdapter> Adapters
internal bool IsKeyVaultRefreshConfigured { get; private set; } = false;

/// <summary>
/// Indicates all types of feature filters used by the application.
/// Indicates all feature flag features used by the application.
/// </summary>
internal FeatureFlagTracing FeatureFlagTracing { get; set; } = new FeatureFlagTracing();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,9 +597,21 @@ private async Task<Dictionary<string, string>> PrepareData(Dictionary<string, Co
// Reset old feature flag tracing in order to track the information present in the current response from server.
_options.FeatureFlagTracing.ResetFeatureFlagTracing();

// Reset old request tracing values for content type
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.ResetAiConfigurationTracing();
}

foreach (KeyValuePair<string, ConfigurationSetting> kvp in data)
{
IEnumerable<KeyValuePair<string, string>> keyValuePairs = null;

if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType);
}

keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false);

foreach (KeyValuePair<string, string> kv in keyValuePairs)
Expand Down Expand Up @@ -636,7 +648,7 @@ private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellation
{
IEnumerable<ConfigurationClient> clients = _configClientManager.GetClients();

if (_requestTracingOptions != null)
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.ReplicaCount = clients.Count() - 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal class RequestTracingConstants
public const string ReplicaCountKey = "ReplicaCount";
public const string FeaturesKey = "Features";
public const string LoadBalancingEnabledTag = "LB";
public const string AIConfigurationTag = "AI";
public const string AIChatCompletionConfigurationTag = "AICC";

public const string SignalRUsedTag = "SignalR";
public const string FailoverRequestTag = "Failover";
public const string PushRefreshTag = "PushRefresh";
Expand All @@ -54,5 +57,8 @@ internal class RequestTracingConstants
public const string SignalRAssemblyName = "Microsoft.AspNetCore.SignalR";

public const string Delimiter = "+";

public const string AIMimeProfile = "https://azconfig.io/mime-profiles/ai";
public const string AIChatCompletionMimeProfile = "https://azconfig.io/mime-profiles/ai/chat-completion";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Linq;
using System;
using System.Net.Mime;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using System.Collections.Generic;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
internal static class ContentTypeExtensions
{
private static readonly IEnumerable<string> ExcludedJsonContentTypes = new[]
{
FeatureManagementConstants.ContentType,
KeyVaultConstants.ContentType
};

public static bool IsAi(this ContentType contentType)
{
return contentType != null &&
contentType.IsJson() &&
contentType.Parameters.ContainsKey("profile") &&
!string.IsNullOrEmpty(contentType.Parameters["profile"]) &&
contentType.Parameters["profile"].StartsWith(RequestTracingConstants.AIMimeProfile);
}

public static bool IsAiChatCompletion(this ContentType contentType)
{
return contentType != null &&
contentType.IsJson() &&
contentType.Parameters.ContainsKey("profile") &&
!string.IsNullOrEmpty(contentType.Parameters["profile"]) &&
contentType.Parameters["profile"].StartsWith(RequestTracingConstants.AIChatCompletionMimeProfile);
}

public static bool IsJson(this ContentType contentType)
{
if (contentType == null)
{
return false;
}

string acceptedMainType = "application";
string acceptedSubType = "json";
string mediaType = contentType.MediaType;

if (!ExcludedJsonContentTypes.Contains(mediaType, StringComparer.OrdinalIgnoreCase))
{
ReadOnlySpan<char> mediaTypeSpan = mediaType.AsSpan();

// Since contentType has been validated using System.Net.Mime.ContentType,
// mediaType will always have exactly 2 parts after splitting on '/'
int slashIndex = mediaTypeSpan.IndexOf('/');

if (mediaTypeSpan.Slice(0, slashIndex).Equals(acceptedMainType.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> subTypeSpan = mediaTypeSpan.Slice(slashIndex + 1);

while (!subTypeSpan.IsEmpty)
{
int plusIndex = subTypeSpan.IndexOf('+');

ReadOnlySpan<char> currentSubType = plusIndex == -1 ? subTypeSpan : subTypeSpan.Slice(0, plusIndex);

if (currentSubType.Equals(acceptedSubType.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return true;
}

subTypeSpan = plusIndex == -1 ? ReadOnlySpan<char>.Empty : subTypeSpan.Slice(plusIndex + 1);
}
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;
using System.Net.Mime;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
internal static class StringExtensions
{
public static bool TryParseContentType(this string contentTypeString, out ContentType contentType)
{
contentType = null;

if (string.IsNullOrWhiteSpace(contentTypeString))
{
return false;
}

try
{
contentType = new ContentType(contentTypeString.Trim());

return true;
}
catch (FormatException)
{
return false;
}
catch (IndexOutOfRangeException)
{
// Bug in System.Net.Mime.ContentType throws this if contentType is "xyz/"
// https://github.com/dotnet/runtime/issues/39337
return false;
}
}

public static string NormalizeNull(this string s)
{
return s == LabelFilter.Null ? null : s;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
// Licensed under the MIT license.
//
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading;
Expand All @@ -16,12 +15,6 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
internal class JsonKeyValueAdapter : IKeyValueAdapter
{
private static readonly IEnumerable<string> ExcludedJsonContentTypes = new[]
{
FeatureManagementConstants.ContentType,
KeyVaultConstants.ContentType
};

public Task<IEnumerable<KeyValuePair<string, string>>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken)
{
if (setting == null)
Expand Down Expand Up @@ -58,38 +51,9 @@ public bool CanProcess(ConfigurationSetting setting)
return false;
}

string acceptedMainType = "application";
string acceptedSubType = "json";
string mediaType;

try
if (setting.ContentType.TryParseContentType(out ContentType contentType))
{
mediaType = new ContentType(setting.ContentType.Trim()).MediaType;
}
catch (FormatException)
{
return false;
}
catch (IndexOutOfRangeException)
{
// Bug in System.Net.Mime.ContentType throws this if contentType is "xyz/"
// https://github.com/dotnet/runtime/issues/39337
return false;
}

if (!ExcludedJsonContentTypes.Contains(mediaType, StringComparer.OrdinalIgnoreCase))
{
// Since contentType has been validated using System.Net.Mime.ContentType,
// mediaType will always have exactly 2 parts after splitting on '/'
string[] types = mediaType.Split('/');
if (string.Equals(types[0], acceptedMainType, StringComparison.OrdinalIgnoreCase))
{
string[] subTypes = types[1].Split('+');
if (subTypes.Contains(acceptedSubType, StringComparer.OrdinalIgnoreCase))
{
return true;
}
}
return contentType.IsJson();
}

return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using System.Net.Mime;
using System.Text;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
Expand Down Expand Up @@ -68,13 +70,57 @@ internal class RequestTracingOptions
/// </summary>
public bool IsPushRefreshUsed { get; set; } = false;

/// <summary>
/// Flag to indicate whether any key-value uses the json content type and contains
/// a parameter indicating an AI profile.
/// </summary>
public bool UsesAIConfiguration { get; set; } = false;

/// <summary>
/// Flag to indicate whether any key-value uses the json content type and contains
/// a parameter indicating an AI chat completion profile.
/// </summary>
public bool UsesAIChatCompletionConfiguration { get; set; } = false;

/// <summary>
/// Resets the AI configuration tracing flags.
/// </summary>
public void ResetAiConfigurationTracing()
{
UsesAIConfiguration = false;
UsesAIChatCompletionConfiguration = false;
}

/// <summary>
/// Updates AI configuration tracing flags based on the provided content type.
/// </summary>
/// <param name="contentTypeString">The content type to analyze.</param>
public void UpdateAiConfigurationTracing(string contentTypeString)
{
if (!UsesAIChatCompletionConfiguration &&
!string.IsNullOrWhiteSpace(contentTypeString) &&
contentTypeString.TryParseContentType(out ContentType contentType) &&
contentType.IsAi())
{
UsesAIConfiguration = true;

if (contentType.IsAiChatCompletion())
{
UsesAIChatCompletionConfiguration = true;
}
}
}

/// <summary>
/// Checks whether any tracing feature is used.
/// </summary>
/// <returns>true if any tracing feature is used, otherwise false.</returns>
public bool UsesAnyTracingFeature()
{
return IsLoadBalancingEnabled || IsSignalRUsed;
return IsLoadBalancingEnabled ||
IsSignalRUsed ||
UsesAIConfiguration ||
UsesAIChatCompletionConfiguration;
}

/// <summary>
Expand Down Expand Up @@ -105,6 +151,26 @@ public string CreateFeaturesString()
sb.Append(RequestTracingConstants.SignalRUsedTag);
}

if (UsesAIConfiguration)
{
if (sb.Length > 0)
{
sb.Append(RequestTracingConstants.Delimiter);
}

sb.Append(RequestTracingConstants.AIConfigurationTag);
}

if (UsesAIChatCompletionConfiguration)
{
if (sb.Length > 0)
{
sb.Append(RequestTracingConstants.Delimiter);
}

sb.Append(RequestTracingConstants.AIChatCompletionConfigurationTag);
}

return sb.ToString();
}
}
Expand Down