-
Notifications
You must be signed in to change notification settings - Fork 283
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
[bug] Current AspNetCore integration does not correctly track client context when behind proxies that use X-Forwarded-*
headers
#2231
Comments
Hi @DrEsteban, http vs httpsCan you test this with the ConsoleExporter and share the results? x-forwarded-* headersAs a workaround, you can manually set these using the Enrich methods.
|
Hey @TimothyMothra! I've gone ahead and updated my post with a small comment in the repro steps explaining where the Regarding the ConsoleExporter test, and your proposed workaround: Here is a configuration to repro locally: services.AddOpenTelemetry().WithTracing().WithLogging().WithMetrics();
services.ConfigureOpenTelemetryTracerProvider(b =>
b.AddConsoleExporter()
.AddAspNetCoreInstrumentation(o => o.EnrichWithHttpRequest = (a, context) => a.SetTag("new_scheme", context.Scheme)));
services.Configure<ForwardedHeadersOptions>(o =>
{
// Trust forwarded headers from any IP (not secure)
o.KnownNetworks.Add(IPNetwork.Parse("0.0.0.0/0"));
o.AllowedHosts = ["*"];
o.RequireHeaderSymmetry = false;
o.ForwardedHeaders = ForwardedHeaders.All;
});
var app = builder.Build();
app.UseForwardedHeaders();
app.Use((context, next) =>
{
context.RequestServices.GetRequiredService<ILogger<Program>>().LogInformation($"Request from {context.Request.Scheme}");
return next(context);
}); I then issue a request with: And here is the console output: ConsoleExporter output
Notice my LogRecord, which pulls directly from The only way I've found to truly workaround this issue is to introduce a custom Processor: MyHttpTraceActivityProcessor.cspublic class MyHttpTraceActivityProcessor(IHttpContextAccessor _httpContextAccessor) : BaseProcessor<Activity>
{
public override void OnEnd(Activity activity)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return;
}
// Fix Forwarded Headers...
const string ClientIP = "client.address";
const string Scheme = "url.scheme";
const string Host = "server.address";
const string Port = "server.port";
const string Path = "url.path";
var request = context.Request;
var connection = context.Connection;
var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/";
SetIpAddressTagIfDifferent(activity, ClientIP, connection.RemoteIpAddress?.ToString());
SetStringTagIfDifferent(activity, Scheme, request.Scheme);
SetStringTagIfDifferent(activity, Host, request.Host.Host);
SetIntTagIfDifferent(activity, Port, request.Host.Port);
SetStringTagIfDifferent(activity, Path, path);
}
private static void SetIpAddressTagIfDifferent(Activity activity, string key, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
object? currentTag = activity.GetTagItem(key);
if (currentTag == null) // If null, set
{
activity.SetTag(key, value);
}
else if (currentTag is string currentStr)
{
if (currentStr != value) // Only set if different
{
activity.SetTag(key, value);
}
}
else if (currentTag is IPAddress currentAddr)
{
if (currentAddr.ToString() != value) // Only set if different
{
activity.SetTag(key, value);
}
}
else // Unrecognized existing type
{
activity.SetTag(key + ".on-end-value", value);
activity.SetTag(key + ".type", currentTag.GetType().FullName);
}
}
private static void SetStringTagIfDifferent(Activity activity, string key, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
object? currentTag = activity.GetTagItem(key);
if (currentTag == null) // If null, set
{
activity.SetTag(key, value);
}
else if (currentTag is string currentStr)
{
if (currentStr != value) // Only set if different
{
activity.SetTag(key, value);
}
}
else // Unrecognized existing type
{
activity.SetTag(key + ".on-end-value", value);
activity.SetTag(key + ".type", currentTag.GetType().FullName);
}
}
private static void SetIntTagIfDifferent(Activity activity, string key, int? valueNullable)
{
if (!valueNullable.HasValue)
{
return;
}
int value = valueNullable.Value;
object? currentTag = activity.GetTagItem(key);
if (currentTag == null) // If null, set
{
activity.SetTag(key, value);
}
else if (currentTag is int currentInt)
{
if (currentInt != value) // Only set if different
{
activity.SetTag(key, value);
}
}
else if (currentTag is string currentString)
{
if (currentString != value.ToString()) // Only set if different
{
activity.SetTag(key, value);
}
}
else // Unrecognized existing type
{
activity.SetTag(key + ".on-end-value", value);
activity.SetTag(key + ".type", currentTag.GetType().FullName);
}
}
} (That's probably a little too verbose & complex for a workaround 😆 But I wanted the solution to be generic and safe lol. Hopefully it conveys the idea!) |
X-Forwarded-*
headersX-Forwarded-*
headers (e.g. X-Forwarded-For
, X-Forwarded-Proto
, X-Forwarded-Host
, X-Forwarded-Prefix
)
X-Forwarded-*
headers (e.g. X-Forwarded-For
, X-Forwarded-Proto
, X-Forwarded-Host
, X-Forwarded-Prefix
)X-Forwarded-*
headers
Thank you for the extra information! With regard to the X-Forwarded- headers,the OpenTelemetry specification doesn't make comprehensive recommendations on how to handle these. The few parts where they are mentioned seem to suggest that they are opt-in (specification link). Since it's not required by the spec, I wouldn't expect to see any changes here in the near future. Your custom Processor looks good and I think this would be the recommended workaround. With regard to http vs https,I can see the ConsoleExporter is showing |
@DrEsteban one more question, what version of .NET are you using? Starting with .NET 8, the native libraries for AspNetCore produce their own Activities. |
@TimothyMothra I'm on .NET 8.0! (And I personally think it's very cool the AspNetCore libraries have adopted Activities in this way 🙂 It's a neat way to handle library instrumentation.) From the spec:Your comment from above:
The specification does seem to call the concept out, at least X-Forwarded-Host, saying instrumentations "SHOULD" make best effort to use the correct value based on the decision tree above. When you say it's not required by the spec, is that because of the "should" terminology? While I understand it doesn't say "must", it does seem to strongly suggest grabbing the correct value(s) - even providing an ordered priority list on where the values should come from. So, if the SDK is going to try to set these properties at all, it feels like both the spec AND user expectation are pointing to a change that needs to be made to the current behavior. (At least in my opinion/interpretation! 🙂) Of course another option would be to stop setting these properties entirely, since they are optional, but IMO I think it's a valuable behavior. It's just, if they're going to be set, they should ideally be set correctly! (To spec)
Thanks for your attention to this issue! |
The spec you've pasted here seems pretty unambiguous - according to that snip, there is alignment between the spec and our expectations; @TimothyMothra are you referring to some other part of the spec or a different spec? |
Component
OpenTelemetry.Instrumentation.AspNetCore
Package Version
Runtime Version
net8.0
Description
Because the current AspNetCore integration sets all request telemetry in the OnStart() method, it does not work correctly in situations where the webapp is receiving traffic behind a proxy where
X-Forwarded-*
-type headers are being used to forward the original ClientIP, Scheme, Path, etc.ASP.NET includes middleware that by default will recognize:
X-Forwarded-For
(original clientIP)X-Forwarded-Host
(original Host header)X-Forwarded-Proto
(HTTP vs HTTPS)X-Forwarded-Prefix
(original base path - for proxies that rewrite paths when forwarding to backend)In my case, I am running an ASP.NET8.0 app in Azure WebApps for Linux, which terminates TLS and forwards client context via headers like
X-Forwarded-For
,X-Forwarded-Proto
, etc. I am using the Forwarded Headers Middleware to propagate those headers to the HttpContext, (which I've verified is working), but the OpenTelemetry framework is still using the original values when publishing telemetry.Compare this with the native Application Insights SDK. If I configure OpenTelemetry and Application Insights side-by-side, sending to the same Application Insights resource, here are the request logs I see on the Azure portal:
As expected, there are 2 copies of each request because I'm double-logging traces with 2 frameworks. However, the OpenTelemetry version has not correctly recognized that the original client connection was over HTTPS, whereas the ApplicationInsights one has.
I believe this is because these values change after the "Activity" is started, once the Forwarded Headers Middleware kicks in. But by that time the Activity has already been created with the original values. Therefore, it would seem the only way to support that scenario would be to set/reset these values from the HttpContext in the
OnStop()
method as well. (Or by some other fancier means of tying into ASP.NET Core's middleware pipeline...)Steps to Reproduce
Azure.Monitor.OpenTelemetry.AspNetCore
OpenTelemetry distro and the nativeMicrosoft.ApplicationInsights.AspNetCore
SDK:e.g.
APPLICATIONINSIGHTS_CONNECTION_STRING
appropriatelyX-Forwarded-*
headers when it terminates TLS and forwards the requests to your apprequests
tableExpected Result
I expect to see both log messages are mostly identical, at least w.r.t. basic properties like request URL and ClientIP/Client Location.
Actual Result
Application Insights-generated log is correct, but OpenTelemetry-generated log as incorrect Scheme and ClientIP/Client Location.
Additional Context
This is somewhat related to #1786.
This has also been logged in Microsoft's distro (link), but was deemed to be a defect in the underlying SDK
OpenTelemetry.Instrumentation.AspNetCore
and therefore closed.The text was updated successfully, but these errors were encountered: