Skip to content

[API Proposal]: Make some Telemetry helper types public #6737

@evgenyfedorov2

Description

@evgenyfedorov2

Background and motivation

LAST UPDATED: 26.08.2025

This will allow consumers to provide and register in DI custom implementations of types specified below, replacing the default ones. For example, it can be used for working with telemetry (traces, logs) in incoming and outgoing HTTP requests and redacting PII information.

I propose to make following internal types public:

  1. DownstreamDependencyMetadataManager - it will allow consumers to use that type, for example, via constructor injection. There is already a public extension method to register it in DI, but it is useless as of today - you can call the extension method, but you cannot inject anything to constructor because the class is internal.

  2. HttpRouteFormatter - currently internal, but it would be nice to have it public. + we can add a public method for registering it in DI, similar to this.

  3. HttpRouteParser - currently internal, but it would be nice to have it public. + we can add a public method for registering it in DI, similar to this.

  4. ParsedRouteSegments - this is part of the public method arguments of the classes above and has to become public as a result.

  5. HttpRouteParameter - this is part of the public method arguments of the classes above and has to become public as a result.

  6. Segment - this is part of the public method arguments of the classes above and has to become public as a result.

API Proposal

--- namespace Microsoft.Extensions.Telemetry.Internal;
+++ namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// Interface to manage dependency metadata.
/// </summary>
--- internal sealed class DownstreamDependencyMetadataManager : IDownstreamDependencyMetadataManager
+++ public abstract class DownstreamDependencyMetadataManager
{
    /// <summary>
    /// Get metadata for the specified HTTP request message.
    /// </summary>
    /// <param name="requestMessage">The HTTP request.</param>
    /// <returns>The resolved <see cref="RequestMetadata"/> if found; otherwise, <see langword="false" />.</returns>

--- public RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage);
+++ public virtual RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage);

    /// <summary>
    /// Gets request metadata for the specified HTTP web request.
    /// </summary>
    /// <param name="requestMessage">The HTTP web request.</param>
    /// <returns>The resolved <see cref="RequestMetadata"/> if found; otherwise, null.</returns>
--- public RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage);
+++ public virtual RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage);
}
namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// Formats HTTP request paths using route templates with sensitive parameters optionally redacted.
/// </summary>
--- internal sealed class HttpRouteFormatter : IHttpRouteFormatter
+++ public abstract class HttpRouteFormatter
{
    /// Formats the HTTP path using the route template with sensitive parameters redacted.
    /// </summary>
    /// <param name="httpRoute">HTTP request route template.</param>
    /// <param name="httpPath">HTTP request's absolute path.</param>
    /// <param name="redactionMode">Strategy to decide how parameters are redacted.</param>
    /// <param name="parametersToRedact">Dictionary of parameters with their data classification that needs to be redacted.</param>
    /// <returns>Formatted path with sensitive parameter values redacted.</returns>
--- public string Format(
        string httpRoute,
        string httpPath,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact);
+++ public virtual string Format(
        string httpRoute,
        string httpPath,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact);

    /// <summary>
    /// Formats the HTTP path using the route template with sensitive parameters redacted.
    /// </summary>
    /// <param name="routeSegments">HTTP request's route segments.</param>
    /// <param name="httpPath">HTTP request's absolute path.</param>
    /// <param name="redactionMode">Strategy to decide how parameters are redacted.</param>
    /// <param name="parametersToRedact">Dictionary of parameters with their data classification that needs to be redacted.</param>
    /// <returns>Returns formatted path with sensitive parameter values redacted.</returns>
--- public string Format(
        in ParsedRouteSegments routeSegments,
        string httpPath,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact);
+++ public virtual string Format(
        in ParsedRouteSegments routeSegments,
        string httpPath,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact);
}
namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// HTTP request route parser.
/// </summary>
--- internal interface IHttpRouteParser
+++ public abstract class HttpRouteParser
{
    /// Parses HTTP route and breaks it into text and parameter segments.
    /// </summary>
    /// <param name="httpRoute">HTTP request's route template.</param>
    /// <returns>Text and parameter segments of route.</returns>
--- public ParsedRouteSegments ParseRoute(string httpRoute);
+++ public virtual ParsedRouteSegments ParseRoute(string httpRoute);

    /// <summary>
    /// Extracts parameters values from the HTTP request path.
    /// </summary>
    /// <param name="httpPath">HTTP request's absolute path.</param>
    /// <param name="routeSegments">Route segments containing text and parameter segments of the route.</param>
    /// <param name="redactionMode">Strategy to decide how parameters are redacted.</param>
    /// <param name="parametersToRedact">Dictionary of parameters with their data classification that needs to be redacted.</param>
    /// <param name="httpRouteParameters">Output array where parameters will be stored. Caller must provide the array with enough capacity to hold all parameters in route segment.</param>
    /// <returns><see langword="true" /> if parameters were extracted successfully, <see langword="false" /> otherwise.</returns>
--- public bool TryExtractParameters(
        string httpPath,
        in ParsedRouteSegments routeSegments,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        ref HttpRouteParameter[] httpRouteParameters);
+++ public virtual bool TryExtractParameters(
        string httpPath,
        in ParsedRouteSegments routeSegments,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        ref HttpRouteParameter[] httpRouteParameters);
}
namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// Struct to hold the route segments created after parsing the route.
/// </summary>
--- internal readonly struct ParsedRouteSegments
+++ public readonly struct ParsedRouteSegments
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ParsedRouteSegments"/> struct.
    /// </summary>
    /// <param name="routeTemplate">Route's template.</param>
    /// <param name="segments">Array of segments.</param>
    public ParsedRouteSegments(string routeTemplate, Segment[] segments);

    /// <summary>
    /// Gets the route template.
    /// </summary>
    public string RouteTemplate { get; }

    /// <summary>
    /// Gets all segments of the route.
    /// </summary>
    public Segment[] Segments { get; }

    /// <summary>
    /// Gets the count of parameters in the route.
    /// </summary>
    public int ParameterCount { get; }
}
namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// Struct to hold metadata about a route parameter.
/// </summary>
--- internal readonly struct HttpRouteParameter
+++ public readonly struct HttpRouteParameter
{
    /// <summary>
    /// Initializes a new instance of the <see cref="HttpRouteParameter"/> struct.
    /// </summary>
    /// <param name="name">parameter name.</param>
    /// <param name="value">parameter value.</param>
    /// <param name="isRedacted">conveys if the parameter value is redacted.</param>
    public HttpRouteParameter(string name, string value, bool isRedacted);

    /// <summary>
    /// Gets parameter name.
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// Gets parameter value.
    /// </summary>
    public string Value { get; }

    /// <summary>
    /// Gets a value indicating whether the parameter value is redacted.
    /// </summary>
    public bool IsRedacted { get; }
}
namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// Struct to hold the metadata about a route's segment.
/// </summary>
--- internal readonly struct Segment
+++ public readonly struct Segment
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Segment"/> struct.
    /// </summary>
    /// <param name="start">Start index of the segment.</param>
    /// <param name="end">End index of the segment.</param>
    /// <param name="content">Actual content of the segment.</param>
    /// <param name="isParam">If the segment is a param.</param>
    /// <param name="paramName">Name of the parameter.</param>
    /// <param name="defaultValue">Default value of the parameter.</param>
    /// <param name="isCatchAll">If the segment is a catch-all parameter.</param>
    public Segment(
        int start, int end, string content, bool isParam,
        string paramName = "", string defaultValue = "", bool isCatchAll = false);

    /// <summary>
    /// Gets start index of the segment.
    /// </summary>
    public int Start { get; }

    /// <summary>
    /// Gets end index of the segment.
    /// </summary>
    public int End { get; }

    /// <summary>
    /// Gets content of the segment.
    /// </summary>
    public string Content { get; } = string.Empty;

    /// <summary>
    /// Gets a value indicating whether the segment is a parameter.
    /// </summary>
    public bool IsParam { get; }

    /// <summary>
    /// Gets a name of the parameter.
    /// </summary>
    public string ParamName { get; } = string.Empty;

    /// <summary>
    /// Gets a default value of the parameter.
    /// </summary>
    public string DefaultValue { get; } = string.Empty;

    /// <summary>
    /// Gets a value indicating whether the segment is a catch-all parameter.
    /// </summary>
    public bool IsCatchAll { get; }
}
namespace Microsoft.Extensions.Http.Diagnostics;

/// <summary>
/// Extensions for common telemetry utilities.
/// </summary>
--- internal static class TelemetryCommonExtensions
+++ public static class TelemetryCommonExtensions
{
--- public static IServiceCollection AddHttpRouteProcessor(this IServiceCollection services);
+++ public static IServiceCollection AddHttpRouteParser(this IServiceCollection services);
+++ public static IServiceCollection AddHttpRouteFormatter(this IServiceCollection services);
}

API Usage

public class MyMeteringHandler
{
  private readonly DownstreamDependencyMetadataManager _downstreamDependencyMetadataManager;
  public MyMeteringHandler(DownstreamDependencyMetadataManager downstreamDependencyMetadataManager)
  {
    _downstreamDependencyMetadataManager = downstreamDependencyMetadataManager;
  }
  public void MyMethod()
  {
    // example from this repo https://github.com/dotnet/extensions/blob/da94b6045fef85f78a5eee183579221cee684e51/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs#L151
    var requestMetadata = _downstreamDependencyMetadataManager.GetRequestMetadata(request);
  }
}
public class MyTraceRedactionProcessor
{
  private readonly HttpRouteFormatter _routeFormatter;
  private readonly HttpRouteParser _routeParser;
  public MyTraceProcessor(HttpRouteFormatter routeFormatter, HttpRouteParser routeParser)
  {
    _routeFormatter = routeFormatter;
    _routeParser = routeParser;
  }
  public string RedactHttpPath(string httpPath, IReadOnlyDictionary<string, DataClassification> parametersToRedact)
  {
    var routeTemplate = $"/api/users/{userId}"; // example of value; normally received via method arguments

    // we can either extract route segments and use them for route formatting
    var routeSegments = _routeParser.ParseRoute(routeTemplate);
    var redactedPath = _routeFormatter.Format(routeSegments, httpPath, redactionMode, parametersToRedact);
    
    // and later re-use them for something else with TryExtractParameters():
    HttpRouteParameter[] routeParameters = new[routeSegments.ParameterCount];
    if(_routeParser.TryExtractParameters(path, routeSegments, redactionMode, parametersToRedact, ref routeParameters))
    {
      // use routeParameters here
    }

    return redactedPath;
  }

  public string RedactHttpPath2(string httpPath, IReadOnlyDictionary<string, DataClassification> parametersToRedact)
  {
    var routeTemplate = $"/api/users/{userId}";

    // or we can directly format route
    // example in this repo:https://github.com/dotnet/extensions/blob/da94b6045fef85f78a5eee183579221cee684e51/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs#L170
    return _routeFormatter.Format(routeTemplate, httpPath, redactionMode, parametersToRedact);
  }
}

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Labels

api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-telemetry

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions