We can face a big problem at Prometheus Counter if we want to add the path for requests. Let imagine the scenario:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("Country/{country}/State/{state}/City/{city}")]
public async Task<IActionResult> Get(string country, string state, string city)
{
// Do stuff
return Ok();
}
}
When making a request seaching for country Brasil, state Goias and City Goiania we´re going to get the context Request Path Value, and will be /WeatherForecast/Country/brasil/State/goias/City/goiania
Searching for other city /WeatherForecast/Country/brasil/State/goias/City/anapolis
And the counter will count
/WeatherForecast/Country/brasil/State/goias/City/goiania = 1
/WeatherForecast/Country/brasil/State/goias/City/anapolis = 1
And what we want is to count /WeatherForecast/Country/{country}/State/{state}/City/{city} = 2
Unless you want to exactly count the values of route, this will not generate metrics data correctly, we want a perspective counter by method and show the route with placeholders.
In order to solve this we can create an extension method for HttpContext that will extract that information replacing route value with route placeholders.
private static string GetRouteEndpoint(this HttpContext context)
{
// Getting a ObjectPool for StringBuilder, this is used because StringBuilder is a large object, and this benefits the application by reusing the object avoiding creating unecessary object.
var stringBuilderPool = context.RequestServices.GetRequiredService<ObjectPool<StringBuilder>>();
var stringBuilder = stringBuilderPool.Get();
// Here we get all the paths and values from request
var allPaths = context.Request.Path.Value?.Split('/');
var routeData = context.GetRouteData();
// this is a control list of the paths added to stringBuilder
var keyAdded = new List<string>();
// This looping is for replace some actual values for some PlaceHolders, this will avoid over counting
foreach (var path in allPaths)
{
if (string.IsNullOrWhiteSpace(path))
continue;
stringBuilder.Append('/');
// searching for the KeyPair for the actual path
var routeDataKeyValue = routeData.Values.FirstOrDefault(x
=> x.Value is not null
&& x.Value.ToString().Equals(path)
&& !keyAdded.Contains(path));
// here we need the value of controller
if (!string.IsNullOrEmpty(routeDataKeyValue.Key) && routeDataKeyValue.Key.Equals("controller", StringComparison.OrdinalIgnoreCase))
{
stringBuilder.Append(routeDataKeyValue.Value);
continue;
}
if (!string.IsNullOrEmpty(routeDataKeyValue.Key))
{
// we need to check if the value is already added, if exists we need to search for the next one
if (keyAdded.Contains(routeDataKeyValue.Key))
{
routeDataKeyValue = routeData.Values.FirstOrDefault(x
=> x.Value is not null
&& x.Value.ToString().Equals(path)
&& !x.Key.Equals(routeDataKeyValue.Key));
}
keyAdded.Add(routeDataKeyValue.Key);
// adding the Key PlaceHolder instead of route value, here were we avoid the over counting
stringBuilder.Append($"{{{routeDataKeyValue.Key}}}");
continue;
}
// append constant path
stringBuilder.Append(path);
}
var endpoint = stringBuilder.ToString();
stringBuilderPool.Return(stringBuilder);
return endpoint;
}
This is the final result.
![](https://andrearima.wordpress.com/wp-content/uploads/2024/06/image-4.png?w=723)
You can check full code at https://github.com/andrearima/prometheus it´s a simple Api adding the ObjectPool of StringBuilder and Prometheus Metrics with Counter
Leave a comment