diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md index 119befa383e6..f64416b25139 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md @@ -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) diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/StandardMetricsExtractionProcessor.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/StandardMetricsExtractionProcessor.cs index 025023d98e88..5793b7d9e0f0 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/StandardMetricsExtractionProcessor.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/StandardMetricsExtractionProcessor.cs @@ -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 { @@ -23,7 +23,7 @@ internal sealed class StandardMetricsExtractionProcessor : BaseProcessor _meterProvider; private readonly Meter? _standardMetricMeter; private readonly Meter? _perfCounterMeter; @@ -75,8 +75,14 @@ internal StandardMetricsExtractionProcessor(AzureMonitorMetricExporter metricExp _enableStandardMetrics = options.EnableStandardMetrics; _enablePerfCounters = options.EnablePerfCounters; - if (_enableStandardMetrics || _enablePerfCounters) + // Initialize Lazy for thread-safe lazy initialization of MeterProvider + _meterProvider = new Lazy(() => { + if (!_enableStandardMetrics && !_enablePerfCounters) + { + return null; + } + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder(); if (_enableStandardMetrics) @@ -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) { @@ -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) @@ -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 handles thread-safety automatically + _ = _meterProvider.Value; + } + private void InitializeCpuBaseline() { if (_process == null) @@ -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(); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/StandardMetricTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/StandardMetricTests.cs index 6510410ef8b9..e38e77d6abe4 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/StandardMetricTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/StandardMetricTests.cs @@ -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; @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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) @@ -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); @@ -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 @@ -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); @@ -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); @@ -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(); + var metricTelemetryItems = new List(); + + var options = new AzureMonitorExporterOptions(); + var standardMetricCustomProcessor = new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(new MockTransmitter(metricTelemetryItems)), options); + + // Configure custom resource attributes + var customRoleName = new KeyValuePair("service.name", "MyCustomService"); + var customRoleInstance = new KeyValuePair("service.instance.id", "MyCustomInstance123"); + var resourceAttributes = new KeyValuePair[] { 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); + } + } } }