Add metadata to traces in Azure functions

2k Views Asked by At

I have an Azure function(.NET core 2.0) that runs on each PR in an ADO repo. I would like to add the PR-ID as metadata to each trace logged by the Azure function. (I am viewing the logs using the traces table in Azure application insights instance)

The Azure function logs traces via:

public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, ILogger log, ExecutionContext context)
{
  logger.LogInformation("Trace Message");
}

How can I add additional metadata to each trace?

2

There are 2 best solutions below

6
George Chen On

This could be implemented with ITelemetry Initializer, you could add custom dimension to a specified telemetry like only for request.

1.Install the following nuget packages:

Microsoft.ApplicationInsights, version 2.11.0

Microsoft.NET.Sdk.Functions, version 1.0.29

2.The below is my test code.

[assembly: WebJobsStartup(typeof(FunctionApp54.MyStartup))]
namespace FunctionApp54
{

    internal class MyTelemetryInitializer : ITelemetryInitializer
    {
        public void Initialize(ITelemetry telemetry)
        {

            //use telemetry is RequestTelemetry to make sure only add to request
            if (telemetry != null && telemetry is RequestTelemetry && !telemetry.Context.GlobalProperties.ContainsKey("testpro"))
            {
                telemetry.Context.GlobalProperties.Add("testpro", "testvalue");
            }
        }
    }

    public class MyStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            builder.Services.AddSingleton<ITelemetryInitializer, MyTelemetryInitializer>();

        }
    }

    public static class Function2
    {


        [FunctionName("Function2")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            return name != null
                ? (ActionResult)new OkObjectResult($"Hello, {name}")
                : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
        }
    }
}

enter image description here

3
Peter Bons On

Yes this is possible.

Option 1: Using a Telemetry Initializer with the help of AsyncLocal:

    public class CustomTelemetryInitializer : ITelemetryInitializer
    {
        public static AsyncLocal<string> MyValue = new AsyncLocal<string>();

        public void Initialize(ITelemetry telemetry)
        {
            if (telemetry is ISupportProperties propertyItem)
            {
                propertyItem.Properties["myProp"] = MyValue.Value;
            }
        }
    }

You can set the value of the AsyncLocal in the function like this:

        [FunctionName("Function")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
            HttpRequest req,
            ILogger log)
        {
            var name = req.Query["name"];

            CustomTelemetryInitializer.MyValue.Value = name;

            log.LogInformation("C# HTTP trigger function processed a request.");

            return new OkObjectResult($"Hello, {name}");
        }

When you run the function you can see the information in the portal:

enter image description here

You can find the whole code at this repo

Now, addressing your comments. Yes you need something extra. The ITelemetryInitializer instance needs to be registered using dependency injection. That is done in the Startup class as outlined in the documentation:

    using Microsoft.ApplicationInsights.Extensibility;
    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    using Microsoft.Extensions.DependencyInjection;

    [assembly: FunctionsStartup(typeof(FunctionApp.Startup))]

    namespace FunctionApp
    {
        public class Startup : FunctionsStartup
        {
            public override void Configure(IFunctionsHostBuilder builder)
            {
                builder.Services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();
                builder.Services.AddLogging();
            }
        }
    }

Once registered the Application Insights SDK will use the CustomTelemetryInitializer.

Option 2 The other option does not involve any TelemetryInitializer, but you can only add properties to the generated RequestTelemetry that is added by the Azure Function App Insights integration. This is done by making using of the fact that the current TelemetryRequest is stored in the HttpContext:

       [FunctionName("Function")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
            HttpRequest req,
            ILogger log)
        {
            var name = req.Query["name"];

            CustomTelemetryInitializer.MyValue.Value = name;

            log.LogInformation("C# HTTP trigger function processed a request.");

            var telemetryItem = req.HttpContext.Features.Get<RequestTelemetry>();
            telemetryItem.Properties["SetInFunc"] = name;

            return new OkObjectResult($"Hello, {name}");
        }

This will show up in the portal as well:

enter image description here

Is it a problem that the context is added only to the Request? It might, but be aware you can query all related telemetry and know what context is involved, for example:

union (traces), (requests), (dependencies), (customEvents), (exceptions)
| extend itemType = iif(itemType == 'availabilityResult',itemType,iif(itemType == 'customEvent',itemType,iif(itemType == 'dependency',itemType,iif(itemType == 'pageView',itemType,iif(itemType == 'request',itemType,iif(itemType == 'trace',itemType,iif(itemType == 'exception',itemType,"")))))))
| extend prop = customDimensions.SetInFunc
| where ((itemType == 'trace' or (itemType == 'request' or (itemType == 'pageView' or (itemType == 'customEvent' or (itemType == 'exception' or (itemType == 'dependency' or itemType == 'availabilityResult')))))))
| top 101 by timestamp desc

will show:

enter image description here

All telemetry that comes from the same invocation will have the same operation_Id.