1
votes

I am trying to add specific properties to telemetry request for every route. After digging a bit, I've found that I can create my own custom TelemetryInitializer by implementing ITelemetryInitializer. By doing this I've managed to add global properties to the request. However, I still need to add specific properties at the controller level. Do you have any idea how can I achieve this?

I've tried to inject TelemetryClient into the controller, but if I use it the properties are shared between requests.

This is how I've tried to log in the controller:

private TelemetryClient telemetryClient;

public ValueController(TelemetryClient telemetryClient)
{
    this.telemetryClient = telemetryClient;
}

[HttpGet]
public async Task<IActionResult> RouteOne([FromQuery(Name = "param1")]string param1, [FromQuery(Name = "param2")]string param2)
{                     
      telemetryClient.Context.GlobalProperties["param1"] = param1;
      telemetryClient.Context.GlobalProperties["param2"] = param2;
}

[HttpGet]
public async Task<IActionResult> RouteTwo([FromQuery(Name = "param3")]string param3, [FromQuery(Name = "param4")]string param4)
{                     
      telemetryClient.Context.GlobalProperties["param3"] = param3;
      telemetryClient.Context.GlobalProperties["param4"] = param4;
}

And this is the implementation of ITelemetryInitializer:

public class CustomPropertiesTelemetryInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public CustomPropertiesTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        telemetry.Context.GlobalProperties["RequestId"] = httpContextAccessor.HttpContext.GetProperty("requestId");
        telemetry.Context.GlobalProperties["Ip"] = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress.ToString();
        telemetry.Context.GlobalProperties["RoutePath"] = httpContextAccessor.HttpContext?.Request.Path;           
    }
}
3
Can you add the code that you're using at the moment, and give an idea of how you are logging in the controller?Brendan Green
Thank you for help Brendan! I've updated my question with the code that I'm using at the moment.Cristina Dolea

3 Answers

0
votes

If the properties you added are always like "paramxxx", then there is a workaround(but it's really not very elegant).

In the controller constructor, check the GlobalProperties if it contains key like "paramxxx":

public ValueController(TelemetryClient telemetryClient)
{
    this.telemetryClient = telemetryClient;
    var props = this.telemetryClient.Context.GlobalProperties;
            foreach (var p in props)
            {
                if (p.Key.Contains("param"))
                {
                    props.Remove(p.Key);
                }
            }
}
0
votes

The key here is to use the DI framework. You can use it to get request-scoped data or services into your ITelemetryInitializer.

(These examples are based on the standard ASP.Net Dependency Injection framework. This pattern should work with any DI framework, but will need to be adjusted slightly.)

First, create a class to represent your request-scoped telemetry. I've used a simple DTO, but this could also be a service that knows how to fetch/generate the data itself. Register it using AddScoped. "Scoped" means that a new instance will be created for each HTTP request, and then that instance will be re-used within that request.

Because I used a DTO, I didn't bother with an interface--you should use an interface if the class contains any logic you'll want to mock in unit tests.

public class RequestScopedTelemetry 
{
    public string MyCustomProperty { get; set; }
}
services.AddScoped<RequestScopedTelemetry>();

Now, create the ITelemetryInitializer and register it as a singleton. App Insights will discover and use it through the DI framework.

class RequestScopedTelemetryInitializer : ITelemetryInitializer
{
    readonly IHttpContextAccessor httpContextAccessor;

    public RequestScopedTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
        => this.httpContextAccessor = httpContextAccessor;

    public void Initialize(ITelemetry telemetry)
    {
        // Attempt to resolve the request-scoped telemetry from the DI container
        var requestScopedTelemetry = httpContextAccessor
            .HttpContext?
            .RequestServices?
            .GetService<RequestScopedTelemetry>();

        // RequestScopedTelemetry is only available within an active request scope
        // If no telemetry available, just move along...
        if (requestScopedTelemetry == null)
            return;

        // If telemetry was available, add it to the App Insights telemetry collection
        telemetry.Context.GlobalProperties[nameof(RequestScopedTelemetry.MyCustomProperty)]
            = requestScopedTelemetry.MyCustomProperty;
    }
}
services.AddSingleton<ITelemetryInitializer, RequestScopedTelemetryInitializer>();

Finally, in your controller method, set your per-request values. This part isn't necessary if your telemetry class is able to fetch or generate the data itself.

public class ExampleController : ControllerBase
{
    readonly RequestScopedTelemetry telemetry;

    public ValuesController(RequestScopedTelemetry telemetry)
        => this.telemetry = telemetry;

    [HttpGet]
    public ActionResult Get()
    {
        telemetry.MyCustomProperty = "MyCustomValue";

        // Do what you want to

        return Ok();
    }
}
0
votes

In order to add per request data into telemetry, you need to have a way to share data within the request. A reliable way is by using HttpContent.Items property, which is basically a Dictionary.

You can create a service to keep a Dictionary inside HttpContent.Items with all custom data you want in telemetry (key prefix is used to ensure we only read the things we want later in Initializer):

public class LogTelemetryRequest
{
    private const string KEY_PREFIX = "CustomTelemetryData_";
    private readonly IHttpContextAccessor _httpContextAccessor;

    public LogTelemetryRequest(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void AddProperty(string key, string value)
    {
        _httpContextAccessor.HttpContext.Items[KEY_PREFIX + key] = value;
    }
}

Register this as scoped in Startup.cs:

services.AddScoped<LogTelemetryRequest>();

Use it in your controller:

private LogTelemetryRequest logTelemetryRequest;

public ValueController(LogTelemetryRequest logTelemetryRequest)
{
    this.logTelemetryRequest = logTelemetryRequest;
}

[HttpGet]
public async Task<IActionResult> RouteOne([FromQuery(Name = "param1")]string param1, [FromQuery(Name = "param2")]string param2)
{                     
    // telemetryClient.Context.GlobalProperties["param1"] = param1;
    // telemetryClient.Context.GlobalProperties["param2"] = param2;
    logTelemetryRequest.AddProperty("param1", param1);
    logTelemetryRequest.AddProperty("param2", param2);
}

Then read it within initializer:

public class AddCustomTelemetryInitializer : ITelemetryInitializer
{
    private const string KEY_PREFIX = "CustomTelemetryData_";
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AddCustomTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        var requestTelemetry = telemetry as RequestTelemetry;
        if (requestTelemetry == null) return;

        foreach (var item in _httpContextAccessor.HttpContext.Items)
        {
            if (item.Key is string key && key.StartsWith(KEY_PREFIX))
                requestTelemetry.Properties.Add(key, item.Value.ToString());
        }
    }
}

Ideally LogTelemetryRequest should be registered using an interface, and the key prefix should be a single shared constant, didn't do for the sake of simplicity.