Skip to main content

Shining a light on .NET versions across our organisation with OpenTelemetry – The Azure Monitor edition

In a previous post I showed how to add the .NET runtime version as an OpenTelemetry resource attribute:

ResourceBuilder.CreateEmpty()
    .AddService($"{_sofaSettings.ApplicationName}-{_sofaSettings.EnvironmentName}")
    .AddAttributes(new Dictionary<string, object>
    {
        ["deployment.environment"] = _sofaSettings.EnvironmentName,
        ["service.name"] = _sofaSettings.ApplicationName,
        ["runtime.dotnet.version"] = Environment.Version.ToString()
    })
    .Build();

The idea was clean: attach facts about what is running directly to the resource, and let OpenTelemetry carry them along with every trace, metric, and log automatically.

Unfortunately, there is a catch.

The problem

Azure Monitor's OpenTelemetry exporter only maps a fixed set of well-known resource attributes onto Application Insights fields. service.name and service.namespace become Cloud Role Name, service.instance.id becomes Cloud Role Instance — and that's about it. Anything else you attach to the resource, including custom attributes like runtime.dotnet.version, is silently dropped on the way in.

This turns out to be a known limitation, tracked across multiple GitHub issues for the .NET, Python, and OTel Collector contrib exporters. There is no configuration flag to change this behaviour. It's just not supported (yet).

The workaround

The fix is to stop relying on the resource for this kind of metadata and instead push the attributes onto the telemetry itself using a custom BaseProcessor<Activity>.

Span attributes — called activity tags in .NET — do get exported to Application Insights, where they show up as customDimensions on your requests, dependencies, traces, and exceptions.

First, create the processor:

public class RuntimeVersionEnrichingProcessor : BaseProcessor<Activity>
{
    private readonly string _runtimeVersion;

    public RuntimeVersionEnrichingProcessor()
    {
        _runtimeVersion = Environment.Version.ToString();
    }

    public override void OnEnd(Activity activity)
    {
        activity.SetTag("runtime.dotnet.version", _runtimeVersion);
    }
}

Then register it before UseAzureMonitor() — order matters here, the processor needs to be in the pipeline before the exporter picks up the activity:

builder.Services.ConfigureOpenTelemetryTracerProvider((sp, b) =>
    b.AddProcessor(new RuntimeVersionEnrichingProcessor()));

builder.Services.AddOpenTelemetry().UseAzureMonitor();

That's it. Every span your service emits will now carry runtime.dotnet.version as a tag, and it will land in customDimensions in Application Insights.

 

Querying it

The KQL query from the original post still works, just pointing at the right place:

AppRequests
| extend runtimeVersion = tostring(customDimensions["runtime.dotnet.version"])
| summarize count() by runtimeVersion, AppRoleName
| order by AppRoleName asc

You get the same live view of your runtime landscape — which services are on .NET 10, which are lagging behind — without any spreadsheets or manual tracking.

Remark:The processor approach only covers traces (requests, dependencies, custom spans). If you also want the version on logs, attach it there too using the message template:

logger.LogInformation("Application started on {RuntimeVersion}", Environment.Version);

Logs from ILogger go through a separate pipeline; the activity processor doesn't touch them.

The broader picture

The resource approach was conceptually the right one — that's exactly what resource attributes are designed for. The limitation is on the Azure Monitor exporter side, not in OpenTelemetry itself. The processor workaround gets you the same outcome with a bit more plumbing, and it's what Microsoft's own documentation recommends in the meantime.

More information

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

VS Code Planning mode

After the introduction of Plan mode in Visual Studio , it now also found its way into VS Code. Planning mode, or as I like to call it 'Hannibal mode', extends GitHub Copilot's Agent Mode capabilities to handle larger, multi-step coding tasks with a structured approach. Instead of jumping straight into code generation, Planning mode creates a detailed execution plan. If you want more details, have a look at my previous post . Putting plan mode into action VS Code takes a different approach compared to Visual Studio when using plan mode. Instead of a configuration setting that you can activate but have limited control over, planning is available as a separate chat mode/agent: I like this approach better than how Visual Studio does it as you have explicit control when plan mode is activated. Instead of immediately diving into execution, the plan agent creates a plan and asks some follow up questions: You can further edit the plan by clicking on ‘Open in Editor’: ...