Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

### Bugs Fixed

* Fixed performance counter metrics not using configured resource attributes
(cloud_RoleName and cloud_RoleInstance), which previously showed
"unknown_service:appName" instead of the configured values.
([#54944](https://github.com/Azure/azure-sdk-for-net/pull/54944))

### Other Changes

## 1.6.0-beta.1 (2025-12-03)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
using System.Threading;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.Diagnostics;
using Azure.Monitor.OpenTelemetry.Exporter.Models;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
{
Expand All @@ -23,7 +23,7 @@ internal sealed class StandardMetricsExtractionProcessor : BaseProcessor<Activit
private readonly bool _enableStandardMetrics;
private readonly bool _enablePerfCounters;

internal readonly MeterProvider? _meterProvider;
internal readonly Lazy<MeterProvider?> _meterProvider;
private readonly Meter? _standardMetricMeter;
private readonly Meter? _perfCounterMeter;

Expand Down Expand Up @@ -75,8 +75,14 @@ internal StandardMetricsExtractionProcessor(AzureMonitorMetricExporter metricExp
_enableStandardMetrics = options.EnableStandardMetrics;
_enablePerfCounters = options.EnablePerfCounters;

if (_enableStandardMetrics || _enablePerfCounters)
// Initialize Lazy<T> for thread-safe lazy initialization of MeterProvider
_meterProvider = new Lazy<MeterProvider?>(() =>
{
if (!_enableStandardMetrics && !_enablePerfCounters)
{
return null;
}

var meterProviderBuilder = Sdk.CreateMeterProviderBuilder();

if (_enableStandardMetrics)
Expand All @@ -89,15 +95,18 @@ internal StandardMetricsExtractionProcessor(AzureMonitorMetricExporter metricExp
meterProviderBuilder.AddMeter(PerfCounterConstants.PerfCounterMeterName);
}

_meterProvider = meterProviderBuilder
// Configure resource from ParentProvider - works for both DI and manual scenarios
var resource = ParentProvider?.GetResource();
if (resource != null)
{
meterProviderBuilder.ConfigureResource(rb => rb.AddAttributes(resource.Attributes));
}

return meterProviderBuilder
.AddReader(new PeriodicExportingMetricReader(metricExporter)
{ TemporalityPreference = MetricReaderTemporalityPreference.Delta })
.Build();
}
else
{
_meterProvider = null;
}
}, LazyThreadSafetyMode.ExecutionAndPublication);

if (_enableStandardMetrics)
{
Expand Down Expand Up @@ -137,6 +146,8 @@ internal StandardMetricsExtractionProcessor(AzureMonitorMetricExporter metricExp

public override void OnEnd(Activity activity)
{
EnsureMeterProviderInitialized();

if (activity.Kind == ActivityKind.Server || activity.Kind == ActivityKind.Consumer)
{
if (_requestDuration != null && _requestDuration.Enabled)
Expand Down Expand Up @@ -423,6 +434,13 @@ private bool TryCalculateCPUCounter(out double rawValue, out double normalizedVa
return true;
}

private void EnsureMeterProviderInitialized()
{
// Access Value to trigger lazy initialization if not yet done
// Lazy<T> handles thread-safety automatically
_ = _meterProvider.Value;
}

private void InitializeCpuBaseline()
{
if (_process == null)
Expand Down Expand Up @@ -450,7 +468,10 @@ protected override void Dispose(bool disposing)
{
try
{
_meterProvider?.Dispose();
if (_meterProvider.IsValueCreated)
{
_meterProvider.Value?.Dispose();
}
_standardMetricMeter?.Dispose();
_perfCounterMeter?.Dispose();
_process?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
Expand Down Expand Up @@ -48,7 +48,7 @@ public void ValidateRequestDurationMetric()

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

// Find the specific Request Duration metric among possibly many perf counter metrics.
var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.RequestDurationMetricIdValue);
Expand Down Expand Up @@ -97,7 +97,7 @@ public void ValidateRequestDurationMetricNew()

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.RequestDurationMetricIdValue);
Assert.NotNull(metricTelemetry);
Expand Down Expand Up @@ -148,7 +148,7 @@ public void ValidateRequestDurationMetricConsumerKind()

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.RequestDurationMetricIdValue);
Assert.NotNull(metricTelemetry);
Expand Down Expand Up @@ -204,7 +204,7 @@ public void ValidateDependencyDurationMetric(bool isAzureSDK)

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.DependencyDurationMetricIdValue);
Assert.NotNull(metricTelemetry);
Expand Down Expand Up @@ -272,7 +272,7 @@ public void ValidateDependencyDurationMetricForProducerKind(bool isAzureSDKSpan)

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.DependencyDurationMetricIdValue);
Assert.NotNull(metricTelemetry);
Expand Down Expand Up @@ -339,7 +339,7 @@ public void ValidateDependencyDurationMetricNew(bool isAzureSDK)

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.DependencyDurationMetricIdValue);
Assert.NotNull(metricTelemetry);
Expand Down Expand Up @@ -398,7 +398,7 @@ public void ValidateNullStatusCode(ActivityKind kind)

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricIdToFind = kind == ActivityKind.Client ? StandardMetricConstants.DependencyDurationMetricIdValue : StandardMetricConstants.RequestDurationMetricIdValue;
var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, metricIdToFind);
Expand Down Expand Up @@ -446,7 +446,7 @@ public void ValidateNullStatusCodeNew(ActivityKind kind)

WaitForActivityExport(traceTelemetryItems);

standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

var metricIdToFind = kind == ActivityKind.Client ? StandardMetricConstants.DependencyDurationMetricIdValue : StandardMetricConstants.RequestDurationMetricIdValue;
var metricTelemetry = GetMetricTelemetry(metricTelemetryItems, metricIdToFind);
Expand Down Expand Up @@ -512,7 +512,7 @@ public void ValidatePerfCounterMetrics()
var perfCountersCollected = SpinWait.SpinUntil(
condition: () =>
{
standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();
Thread.Sleep(100);
var requestRate = metricTelemetryItems
.Select(ti => (MetricsData)ti.Data.BaseData)
Expand Down Expand Up @@ -615,7 +615,7 @@ public void ValidateStandardMetricsDisabled()

tracerProvider?.ForceFlush();
WaitForActivityExport(traceTelemetryItems);
standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

// Standard metrics should not be present
var requestMetric = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.RequestDurationMetricIdValue);
Expand Down Expand Up @@ -654,7 +654,7 @@ public void ValidatePerfCountersDisabled()

// Wait briefly for any potential perf counter collection
Thread.Sleep(1000);
standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

// Performance counter metrics should not be present
MetricsData? FindMetric(string expectedName) => metricTelemetryItems
Expand Down Expand Up @@ -713,7 +713,7 @@ public void ValidateBothMetricsAndPerfCountersDisabled()
WaitForActivityExport(traceTelemetryItems);

Thread.Sleep(1000);
standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

// No metrics should be present at all
Assert.Empty(metricTelemetryItems);
Expand Down Expand Up @@ -760,7 +760,7 @@ public void ValidateEnablePropertiesConfiguration(bool enableStandardMetrics, bo
WaitForActivityExport(traceTelemetryItems);

Thread.Sleep(1000);
standardMetricCustomProcessor._meterProvider?.ForceFlush();
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();

// Verify standard metrics
var requestMetric = GetMetricTelemetry(metricTelemetryItems, StandardMetricConstants.RequestDurationMetricIdValue);
Expand Down Expand Up @@ -796,5 +796,102 @@ public void ValidateEnablePropertiesConfiguration(bool enableStandardMetrics, bo
Assert.Null(privateBytes);
}
}

[Fact]
public void ValidatePerfCountersUseConfiguredResourceAttributes()
{
// This test verifies the fix for the bug where performance counters were not using
// the configured cloud_RoleName and cloud_RoleInstance from the TracerProvider resource.
var activitySource = new ActivitySource(nameof(StandardMetricTests.ValidatePerfCountersUseConfiguredResourceAttributes));
var traceTelemetryItems = new List<TelemetryItem>();
var metricTelemetryItems = new List<TelemetryItem>();

var options = new AzureMonitorExporterOptions();
var standardMetricCustomProcessor = new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(new MockTransmitter(metricTelemetryItems)), options);

// Configure custom resource attributes
var customRoleName = new KeyValuePair<string, object>("service.name", "MyCustomService");
var customRoleInstance = new KeyValuePair<string, object>("service.instance.id", "MyCustomInstance123");
var resourceAttributes = new KeyValuePair<string, object>[] { customRoleName, customRoleInstance };

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetSampler(new AlwaysOnSampler())
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddAttributes(resourceAttributes))
.AddSource(nameof(StandardMetricTests.ValidatePerfCountersUseConfiguredResourceAttributes))
.AddProcessor(standardMetricCustomProcessor)
.AddProcessor(new BatchActivityExportProcessor(new AzureMonitorTraceExporter(new AzureMonitorExporterOptions(), new MockTransmitter(traceTelemetryItems))))
.Build();

// Generate some activities to trigger perf counter collection
for (int i = 0; i < 5; i++)
{
using var a = activitySource.StartActivity("Req", ActivityKind.Server);
a?.SetTag(SemanticConventions.AttributeHttpStatusCode, 200);
}

tracerProvider?.ForceFlush();
WaitForActivityExport(traceTelemetryItems);

// Wait for performance counter collection
var perfCountersCollected = SpinWait.SpinUntil(
condition: () =>
{
standardMetricCustomProcessor._meterProvider?.Value?.ForceFlush();
Thread.Sleep(100);
return metricTelemetryItems.Any(ti =>
{
var data = (MetricsData)ti.Data.BaseData;
return data.Metrics.Count > 0 &&
(data.Metrics[0].Name == PerfCounterConstants.RequestRateMetricIdValue ||
data.Metrics[0].Name == PerfCounterConstants.ProcessPrivateBytesMetricIdValue);
});
},
timeout: TimeSpan.FromSeconds(5));

Assert.True(perfCountersCollected, "Performance counter metrics were not collected within the timeout period.");

// Verify that perf counter telemetry items have the configured resource attributes in Tags
var perfCounterTelemetryItems = metricTelemetryItems
.Where(ti => ti.Data.BaseType == "MetricData")
.Where(ti =>
{
var data = (MetricsData)ti.Data.BaseData;
return data.Metrics.Count > 0 &&
(data.Metrics[0].Name == PerfCounterConstants.RequestRateMetricIdValue ||
data.Metrics[0].Name == PerfCounterConstants.ProcessPrivateBytesMetricIdValue ||
data.Metrics[0].Name == PerfCounterConstants.ProcessCpuMetricIdValue ||
data.Metrics[0].Name == PerfCounterConstants.ProcessCpuNormalizedMetricIdValue);
})
.ToList();

Assert.NotEmpty(perfCounterTelemetryItems);

// Verify each perf counter telemetry item has the correct cloud role tags
foreach (var telemetryItem in perfCounterTelemetryItems)
{
Assert.True(telemetryItem.Tags.TryGetValue(ContextTagKeys.AiCloudRole.ToString(), out var cloudRole),
"Performance counter should have cloud role tag");
Assert.Equal("MyCustomService", cloudRole);

Assert.True(telemetryItem.Tags.TryGetValue(ContextTagKeys.AiCloudRoleInstance.ToString(), out var cloudRoleInstance),
"Performance counter should have cloud role instance tag");
Assert.Equal("MyCustomInstance123", cloudRoleInstance);
}

// Additionally verify standard metrics also have cloud role attributes as properties
var standardMetrics = metricTelemetryItems
.Where(ti => ti.Data.BaseType == "MetricData")
.Select(ti => (MetricsData)ti.Data.BaseData)
.Where(md => md.Metrics.Count > 0 && md.Metrics[0].Name == StandardMetricConstants.RequestDurationMetricIdValue)
.FirstOrDefault();

if (standardMetrics != null)
{
Assert.True(standardMetrics.Properties.TryGetValue(StandardMetricConstants.CloudRoleNameKey, out var roleName));
Assert.Equal("MyCustomService", roleName);
Assert.True(standardMetrics.Properties.TryGetValue(StandardMetricConstants.CloudRoleInstanceKey, out var roleInstance));
Assert.Equal("MyCustomInstance123", roleInstance);
}
}
}
}
Loading