Skip to content

Commit

Permalink
change how/where httpexchange is changed to httptrace, more name upda…
Browse files Browse the repository at this point in the history
…tes, filter headers during return instead of capture
  • Loading branch information
TimHess committed Sep 16, 2024
1 parent 45969bc commit 27ff773
Show file tree
Hide file tree
Showing 22 changed files with 317 additions and 343 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,7 @@ internal Task<SecurityResult> GetPermissionsAsync(HttpContext context)
}
else
{
string? endpointPath = endpointOptions.Path;

if (endpointOptions.Id == "httpexchanges")
{
endpointPath = "httptrace";
}

if (requestPath.StartsWithSegments(basePath + endpointPath))
if (requestPath.StartsWithSegments(basePath + endpointOptions.Path))
{
return endpointOptions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using Microsoft.Extensions.Configuration;
using Microsoft.Net.Http.Headers;
using Steeltoe.Common;
using Steeltoe.Management.Endpoint.Configuration;

namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;
Expand Down Expand Up @@ -81,28 +82,26 @@ public override void Configure(HttpExchangesEndpointOptions options)
// It's not possible to distinguish between null and an empty list in configuration.
// See https://github.com/dotnet/extensions/issues/1341.
// As a workaround, we interpret a single empty string element to clear the defaults.
if (options.RequestHeaders.Count == 0)
if (options.RequestHeaders.Count == 0 || !string.IsNullOrEmpty(options.RequestHeaders[0]))
{
foreach (string defaultKey in DefaultAllowedRequestHeaders)
{
options.RequestHeaders.Add(defaultKey);
}
}
else if (options.RequestHeaders is [""])
{
options.RequestHeaders.Clear();
}

if (options.ResponseHeaders.Count == 0)
if (options.ResponseHeaders.Count == 0 || !string.IsNullOrEmpty(options.ResponseHeaders[0]))
{
foreach (string defaultKey in DefaultAllowedResponseHeaders)
{
options.ResponseHeaders.Add(defaultKey);
}
}
else if (options.ResponseHeaders is [""])

// this should only be needed until Apps Manager supports /httpexchanges
if (Platform.IsCloudFoundry)
{
options.ResponseHeaders.Clear();
options.Id = "httptrace";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static IServiceCollection AddHttpExchangesActuator(this IServiceCollectio
public static IServiceCollection AddHttpExchangesActuator(this IServiceCollection services, Action<HttpExchangesEndpointOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);

services.TryAddSingleton<IDiagnosticsManager, DiagnosticsManager>();
services.AddHostedService<DiagnosticsService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;
public sealed class HttpExchange
{
public long Timestamp { get; }
public TracePrincipal? Principal { get; }
public TraceSession? Session { get; }
public TraceRequest Request { get; }
public TraceResponse Response { get; }
public HttpExchangePrincipal? Principal { get; }
public HttpExchangeSession? Session { get; }
public HttpExchangeRequest Request { get; }
public HttpExchangeResponse Response { get; }
public long TimeTaken { get; }

public HttpExchange(TraceRequest request, TraceResponse response, long timestamp, TracePrincipal? principal, TraceSession? session, double timeTaken)
public HttpExchange(HttpExchangeRequest request, HttpExchangeResponse response, long timestamp, HttpExchangePrincipal? principal, HttpExchangeSession? session, double timeTaken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;

public sealed class TracePrincipal
public sealed class HttpExchangePrincipal
{
public string Name { get; }

public TracePrincipal(string name)
public HttpExchangePrincipal(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;

public sealed class TraceRequest
public sealed class HttpExchangeRequest
{
public string Method { get; }
public string Uri { get; }
public IDictionary<string, IList<string?>> Headers { get; }
public IDictionary<string, IList<string>> Headers { get; }
public string? RemoteAddress { get; }

public TraceRequest(string method, string uri, IDictionary<string, IList<string?>> headers, string? remoteAddress)
public HttpExchangeRequest(string method, string uri, IDictionary<string, IList<string>> headers, string? remoteAddress)
{
ArgumentException.ThrowIfNullOrWhiteSpace(method);
ArgumentException.ThrowIfNullOrWhiteSpace(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;

public sealed class TraceResponse
public sealed class HttpExchangeResponse
{
public int Status { get; }
public IDictionary<string, IList<string?>> Headers { get; }
public IDictionary<string, IList<string>> Headers { get; }

public TraceResponse(int status, IDictionary<string, IList<string?>> headers)
public HttpExchangeResponse(int status, IDictionary<string, IList<string>> headers)
{
ArgumentNullException.ThrowIfNull(headers);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;

public sealed class TraceSession
public sealed class HttpExchangeSession
{
public string Id { get; }

public TraceSession(string id)
public HttpExchangeSession(string id)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ public HttpExchangesDiagnosticObserver(IOptionsMonitor<HttpExchangesEndpointOpti

public HttpExchangesResult GetHttpExchanges()
{
return new HttpExchangesResult(Queue.ToList());
HttpExchange[] recentExchanges = Queue.ToArray();
foreach (HttpExchange exchange in recentExchanges)
{
FilterHeaders(exchange.Request.Headers, _optionsMonitor.CurrentValue.RequestHeaders);
FilterHeaders(exchange.Response.Headers, _optionsMonitor.CurrentValue.ResponseHeaders);
}

return new HttpExchangesResult(recentExchanges);
}

public override void ProcessEvent(string eventName, object? value)
Expand All @@ -63,62 +70,62 @@ public override void ProcessEvent(string eventName, object? value)

if (context != null)
{
RecordHttpTrace(current, context);
RecordHttpExchange(current, context);
}
}

private HttpExchange GetHttpExchange(HttpContext context, TimeSpan duration)
{
HttpExchangesEndpointOptions options = _optionsMonitor.CurrentValue;
string requestUri = GetRequestUri(context.Request, options.AddPathInfo);
string requestUri = GetRequestUri(context.Request, options.IncludePathInfo);

if (options.AddQueryString)
if (options.IncludeQueryString)
{
requestUri += context.Request.QueryString.Value;
}

string? remoteAddress = null;

if (options.AddRemoteAddress)
if (options.IncludeRemoteAddress)
{
remoteAddress = GetRemoteAddress(context);
}

Dictionary<string, IList<string?>> requestHeaders = [];
Dictionary<string, IList<string>> requestHeaders = [];

if (options.AddRequestHeaders)
if (options.IncludeRequestHeaders)
{
requestHeaders = GetHeaders(context.Request.Headers, options.RequestHeaders);
requestHeaders = GetHeaders(context.Request.Headers);
}

var traceRequest = new TraceRequest(context.Request.Method, requestUri, requestHeaders, remoteAddress);
var traceRequest = new HttpExchangeRequest(context.Request.Method, requestUri, requestHeaders, remoteAddress);

Dictionary<string, IList<string?>> responseHeaders = [];
Dictionary<string, IList<string>> responseHeaders = [];

if (options.AddResponseHeaders)
if (options.IncludeResponseHeaders)
{
responseHeaders = GetHeaders(context.Response.Headers, options.ResponseHeaders);
responseHeaders = GetHeaders(context.Response.Headers);
}

var traceResponse = new TraceResponse(context.Response.StatusCode, responseHeaders);
var traceResponse = new HttpExchangeResponse(context.Response.StatusCode, responseHeaders);

string? userName = null;

if (options.AddUserPrincipal)
if (options.IncludeUserPrincipal)
{
userName = GetUserPrincipal(context);
}

TracePrincipal? principal = userName == null ? null : new TracePrincipal(userName);
HttpExchangePrincipal? principal = userName == null ? null : new HttpExchangePrincipal(userName);

string? sessionId = null;

if (options.AddSessionId)
if (options.IncludeSessionId)
{
sessionId = GetSessionId(context);
}

TraceSession? session = sessionId == null ? null : new TraceSession(sessionId);
HttpExchangeSession? session = sessionId == null ? null : new HttpExchangeSession(sessionId);

long timestamp = GetJavaTime(DateTime.UtcNow.Ticks);

Expand All @@ -130,26 +137,34 @@ private HttpExchange GetHttpExchange(HttpContext context, TimeSpan duration)
return GetPropertyOrDefault<HttpContext>(instance, "HttpContext");
}

internal static Dictionary<string, IList<string?>> GetHeaders(IHeaderDictionary headers, IList<string> allowedHeaders)
internal static Dictionary<string, IList<string>> GetHeaders(IHeaderDictionary headers)
{
var result = new Dictionary<string, IList<string?>>();
var result = new Dictionary<string, IList<string>>();

foreach (KeyValuePair<string, StringValues> pair in headers)
foreach ((string key, StringValues value) in headers)
{
#pragma warning disable S4040 // Strings should be normalized to uppercase
result.Add(pair.Key.ToLowerInvariant(), HeaderShouldBeRedacted(pair.Key, allowedHeaders) ? new StringValues(Redacted) : pair.Value);
result.Add(key.ToLowerInvariant(), value);
#pragma warning restore S4040 // Strings should be normalized to uppercase
}

return result;
}

private static void FilterHeaders(IDictionary<string, IList<string>> headers, IList<string> allowedHeaders)
{
foreach (KeyValuePair<string, IList<string>> header in headers.Where(header => HeaderShouldBeRedacted(header.Key, allowedHeaders)))
{
headers[header.Key] = [Redacted];
}
}

private static bool HeaderShouldBeRedacted(string currentHeader, IList<string> allowedHeaders)
{
return !allowedHeaders.Contains(currentHeader) || (allowedHeaders.Count == 1 && string.IsNullOrEmpty(allowedHeaders[0]));
return allowedHeaders is not [""] && !allowedHeaders.Any(header => header.Equals(currentHeader, StringComparison.OrdinalIgnoreCase));
}

private void RecordHttpTrace(Activity current, HttpContext context)
internal void RecordHttpExchange(Activity current, HttpContext context)
{
ArgumentNullException.ThrowIfNull(current);
ArgumentNullException.ThrowIfNull(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ internal sealed class HttpExchangesEndpointHandler : IHttpExchangesEndpointHandl
private readonly IHttpExchangesRepository _httpExchangeRepository;
private readonly ILogger<HttpExchangesEndpointHandler> _logger;

public MediaTypeVersion Version { get; set; } = MediaTypeVersion.V2;

public EndpointOptions Options => _optionsMonitor.CurrentValue;

public HttpExchangesEndpointHandler(IOptionsMonitor<HttpExchangesEndpointOptions> optionsMonitor, IHttpExchangesRepository httpExchangeRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,50 @@ public sealed class HttpExchangesEndpointOptions : EndpointOptions
/// not be logged unless logs are secure and access controlled and the privacy impact assessed.
/// </para>
/// </summary>
public bool AddRequestHeaders { get; set; } = true;
public bool IncludeRequestHeaders { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether HTTP headers from the response should be included in traces.
/// <para>
/// If a response header is not present in the <see cref="ResponseHeaders" />, the header name will be logged with a redacted value.
/// </para>
/// </summary>
public bool AddResponseHeaders { get; set; } = true;
public bool IncludeResponseHeaders { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the request path should be included in traces.
/// </summary>
public bool AddPathInfo { get; set; } = true;
public bool IncludePathInfo { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the name of the user principal making the request should be included in traces.
/// </summary>
public bool AddUserPrincipal { get; set; }
public bool IncludeUserPrincipal { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the request querystring should be included in traces.
/// </summary>
public bool AddQueryString { get; set; }
public bool IncludeQueryString { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the IP address of the request's sender should be included in traces.
/// </summary>
public bool AddRemoteAddress { get; set; }
public bool IncludeRemoteAddress { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the user's session ID should be included in traces.
/// </summary>
public bool AddSessionId { get; set; }
public bool IncludeSessionId { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the time taken to process the request should be included in traces.
/// </summary>
public bool AddTimeTaken { get; set; } = true;
public bool IncludeTimeTaken { get; set; } = true;

/// <summary>
/// Gets request header values that are allowed to be logged.
/// <para>
/// If a request header is not present in the <see cref="RequestHeaders" />, the header name will be logged with a redacted value. Request headers can
/// If a request header is not present in the <see cref="HttpExchangesEndpointOptions.RequestHeaders" />, the header name will be logged with a redacted value. Request headers can
/// contain authentication tokens, or private information which may have regulatory concerns under GDPR and other laws. Arbitrary request headers should
/// not be logged unless logs are secure and access controlled and the privacy impact assessed.
/// </para>
Expand All @@ -74,7 +74,7 @@ public sealed class HttpExchangesEndpointOptions : EndpointOptions
/// <summary>
/// Gets response header values that are allowed to be logged.
/// <para>
/// If a response header is not present in the <see cref="ResponseHeaders" />, the header name will be logged with a redacted value.
/// If a response header is not present in the <see cref="HttpExchangesEndpointOptions.ResponseHeaders" />, the header name will be logged with a redacted value.
/// </para>
/// </summary>
public IList<string> ResponseHeaders { get; private set; } = new List<string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Steeltoe.Common;

namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges;
Expand All @@ -11,7 +10,6 @@ public sealed class HttpExchangesResult
{
internal MediaTypeVersion CurrentVersion { get; set; }

[JsonPropertyName("traces")]
public IList<HttpExchange> Exchanges { get; }

public HttpExchangesResult(IList<HttpExchange> exchanges)
Expand Down
Loading

0 comments on commit 27ff773

Please sign in to comment.