azure-functions.net-8.0open-telemetry

Issues with OpenTelemetry Integration in .NET 8 Azure Function App


I have developed a .NET 8 Function App using the isolated worker model. Following the guidelines provided in the Microsoft documentation : https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto?tabs=app-insights&pivots=programming-language-csharp, I have integrated OpenTelemetry with Azure Functions.

Below are the details of my implementation:

host.json:

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "maxTelemetryItemsPerSecond": 20,
        "excludedTypes": "Request;Exception"
      },
      "enableLiveMetrics": true
    },
    "logLevel": {
      "default": "Information",
      "Function": "Information",
      "Host.Aggregator": "Trace",
      "Host.Results": "Information"
    }
  },
  "telemetryMode": "openTelemetry",
  "retry": {
    "strategy": "fixedDelay",
    "maxRetryCount": 1,
    "delayInterval": "00:00:04"
  },
  "extensions": {
    "queues": {
      "maxPollingInterval": "00:00:00.200",
      "visibilityTimeout": "00:00:30",
      "batchSize": 16,
      "maxDequeueCount": 200,
      "newBatchThreshold": 8
    }
  }
}

Project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <Content Include="local.settings.json">
      <CopyToOutputDirectory>Never</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </Content>
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
    <PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.OpenTelemetry" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
    <PackageReference Include="Microsoft.Data.SqlClient" />
    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
    <PackageReference Include="OpenTelemetry.Extensions.Hosting" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
  <ItemGroup>
    <Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
  </ItemGroup>
</Project>

private static void ConfigureOpenTelemetry(IServiceCollection services, IConfiguration config, string appName)
{
    // Binds configuration to AppCloudServiceOptions and validates it
    // Returns the validated configuration object
    var appConfig = services.BindValidateReturn<AppCloudServiceOptions>(config);

    // Configures logging services with OpenTelemetry
    services.AddLogging(loggingBuilder =>
    {
        // Removes any existing logging providers
        loggingBuilder.ClearProviders();

        // Adds OpenTelemetry logging provider with specific configuration
        loggingBuilder.AddOpenTelemetry(logging =>
        {
            // Enables including formatted log messages in telemetry
            logging.IncludeFormattedMessage = true;

            // Enables logging scopes to maintain context across log entries
            logging.IncludeScopes = true;

        });
    });

    // Adds OpenTelemetry services and configures Azure Monitor as the telemetry backend
    services.AddOpenTelemetry().UseFunctionsWorkerDefaults().UseAzureMonitor();

    // Configures OpenTelemetry with Application Insights connection string and application name
    // This enables sending telemetry data to Azure Application Insights
    services.AddOpenTelemetry(appConfig.AppInsightsConnString, appName);
}

internal static OpenTelemetryBuilder AddOpenTelemetry(this IServiceCollection services, string appInsightsConnString, string appName) =>
services.AddOpenTelemetry().UseAzureMonitor(options =>
{
    options.ConnectionString = appInsightsConnString;
})
    .ConfigureResource(resource => resource.AddService(appName))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddRedisInstrumentation()
    .AddAzureMonitorTraceExporter(options => options.ConnectionString = appInsightsConnString))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
    .AddAzureMonitorMetricExporter(options => options.ConnectionString = appInsightsConnString));

I deployed the above application in one DEV environment and did some validation. On validation I found that no requests are getting logged for the functionapp case. I only see entries of itemType: trace, dependency, exception. Also all the trace, dependency, exception does not have operation_Name column. On the other hand when I try to compare these with API, I see entries of itemType : request, trace, dependency, exception including the operation_Name column.

Could anyone please help me understand the reasons for the issues I am encountering with this setup?


Solution

  • I am able to send traces to Application insights using below code:

    host.json:

    {
      "version": "2.0",
      "telemetryMode": "openTelemetry",
      "logging": {
        "applicationInsights": {
          "enableLiveMetrics": true,
          "samplingSettings": {
            "isEnabled": true,
            "maxTelemetryItemsPerSecond": 20,
            "excludedTypes": "Request;Exception"
          }
        },
        "logLevel": {
          "default": "Information",
          "Function": "Information",
          "Host.Aggregator": "Trace",
          "Host.Results": "Information"
        }
      }
    }
    
    

    local.settings.json:

    {
      "IsEncrypted": false,
      "Values": {
        "AzureWebJobsStorage": "",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
        "TestRith__testconstring": "InstrumentationKey=rithwik;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=cfrithwik35"
      }
      }
    

    Program.cs:

    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Logging;
    using OpenTelemetry.Resources;
    using OpenTelemetry.Trace;
    using OpenTelemetry.Metrics;
    using Azure.Monitor.OpenTelemetry.Exporter;
    using FunctionApp12;
    
    var ri_hst = new HostBuilder()
        .ConfigureFunctionsWebApplication()
        .ConfigureAppConfiguration(config =>
        {
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                  .AddEnvironmentVariables();
        })
        .ConfigureServices((context, services) =>
        {
            var config = context.Configuration;
            var rithConf = new RithServiceCldOps();
            config.Bind("TestRith", rithConf);
    
            string riappnme = "RithwikFunctionApp";
            services.AddOpenTelemetry()
                .ConfigureResource(resource => resource.AddService(riappnme))
                .WithTracing(b =>
                {
                    b.AddHttpClientInstrumentation()
                     .AddAzureMonitorTraceExporter(o =>
                     {
                         o.ConnectionString = rithConf.testconstring;
                     });
                })
                .WithMetrics(b =>
                {
                    b.AddHttpClientInstrumentation()
                     .AddRuntimeInstrumentation()
                     .AddAzureMonitorMetricExporter(o =>
                     {
                         o.ConnectionString = rithConf.testconstring;
                     });
                });
            services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.ClearProviders();
                loggingBuilder.AddOpenTelemetry(tstlgr =>
                {
                    tstlgr.IncludeFormattedMessage = true;
                    tstlgr.IncludeScopes = true;
                    tstlgr.ParseStateValues = true;
                    tstlgr.AddAzureMonitorLogExporter(o =>
                    {
                        o.ConnectionString = rithConf.testconstring;
                    });
                });
            });
        })
        .Build();
    
    ri_hst.Run();
    
    namespace FunctionApp12
    {
        public class RithServiceCldOps
        {
            public string testconstring { get; set; } = string.Empty;
        }
    }
    
    

    Function.cs:

    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Azure.Functions.Worker;
    using Microsoft.Extensions.Logging;
    
    namespace FunctionApp12
    {
        public class Function1
        {
            private readonly ILogger<Function1> rilg;
            public Function1(ILogger<Function1> lg)
            {
                rilg = lg;
            }
            [Function("Function1")]
            public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
            {
                rilg.LogInformation("Rithwik Test info");
                rilg.LogError("Rithwik Test error");
                rilg.LogCritical("Rithwik Test critical");
                rilg.LogWarning("Rithwik Test warning");
                return new OkObjectResult("Test Rithwik!");
            }
        }
    }
    
    

    Output:

    Application Insights Logs:

    enter image description here

    enter image description here

    Here, I have used Http trigger, you can use your trigger.

    Edit:

    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Logging;
    using OpenTelemetry.Resources;
    using OpenTelemetry.Trace;
    using OpenTelemetry.Metrics;
    using Azure.Monitor.OpenTelemetry.Exporter;
    using FunctionApp12;
    
    var chohost = new HostBuilder()
        .ConfigureFunctionsWebApplication()
        .ConfigureAppConfiguration(config =>
        {
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                  .AddEnvironmentVariables();
        })
        .ConfigureServices((context, services) =>
        {
            var config = context.Configuration;
            var chcnfg = new RithServiceCldOps();
            config.Bind("TestRith", chcnfg);
    
            string riappnme = "TestRIthwik";
    
            services.AddOpenTelemetry()
                .ConfigureResource(resource => resource.AddService(riappnme))
                .WithTracing(b =>
                {
                    b.AddSource("Microsoft.Azure.Functions.Worker") 
                     .AddHttpClientInstrumentation()
                     .AddAzureMonitorTraceExporter(o =>
                     {
                         o.ConnectionString = chcnfg.testconstring;
                     });
                })
                .WithMetrics(b =>
                {
                    b.AddHttpClientInstrumentation()
                     .AddRuntimeInstrumentation()
                     .AddAzureMonitorMetricExporter(o =>
                     {
                         o.ConnectionString = chcnfg.testconstring;
                     });
                });
    
            services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.ClearProviders();
                loggingBuilder.AddOpenTelemetry(rilgr =>
                {
                    rilgr.IncludeFormattedMessage = true;
                    rilgr.IncludeScopes = true;
                    rilgr.ParseStateValues = true;
                    rilgr.AddAzureMonitorLogExporter(o =>
                    {
                        o.ConnectionString = chcnfg.testconstring;
                    });
                });
            });
        })
        .Build();
    
    chohost.Run();
    
    namespace FunctionApp12
    {
        public class RithServiceCldOps
        {
            public string testconstring { get; set; } = string.Empty;
        }
    }
    
    

    Output:

    enter image description here