Skip to content

[blazor] Diagnostic metrics - OTEL names review #62754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 17, 2025
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
40 changes: 13 additions & 27 deletions src/Components/Components/src/ComponentsMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal sealed class ComponentsMetrics : IDisposable
private readonly Histogram<double> _eventDuration;
private readonly Histogram<double> _parametersDuration;
private readonly Histogram<double> _batchDuration;
private readonly Histogram<int> _batchSize;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing that there would never be more than int.MaxValue items rendered in a batch


public bool IsNavigationEnabled => _navigationCount.Enabled;

Expand All @@ -37,27 +38,33 @@ public ComponentsMetrics(IMeterFactory meterFactory)
_lifeCycleMeter = meterFactory.Create(LifecycleMeterName);

_navigationCount = _meter.CreateCounter<long>(
"aspnetcore.components.navigation",
"aspnetcore.components.navigate",
unit: "{route}",
description: "Total number of route changes.");

_eventDuration = _meter.CreateHistogram(
"aspnetcore.components.event_handler",
"aspnetcore.components.handle_event.duration",
unit: "s",
description: "Duration of processing browser event. It includes business logic of the component but not affected child components.",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });

_parametersDuration = _lifeCycleMeter.CreateHistogram(
"aspnetcore.components.update_parameters",
"aspnetcore.components.update_parameters.duration",
unit: "s",
description: "Duration of processing component parameters. It includes business logic of the component.",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries });

_batchDuration = _lifeCycleMeter.CreateHistogram(
"aspnetcore.components.render_diff",
"aspnetcore.components.render_diff.duration",
unit: "s",
description: "Duration of rendering component tree and producing HTML diff. It includes business logic of the changed components.",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries });

_batchSize = _lifeCycleMeter.CreateHistogram(
"aspnetcore.components.render_diff.size",
unit: "{elements}",
description: "Number of HTML elements modified during a rendering batch.",
advice: new InstrumentAdvice<int> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingDiffLengthBucketBoundaries });
}

public void Navigation(string componentType, string route)
Expand Down Expand Up @@ -137,10 +144,7 @@ public void FailParametersSync(Exception ex, long startTimestamp, string? compon

public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffLength)
{
var tags = new TagList
{
{ "aspnetcore.components.diff.length", BucketDiffLength(diffLength) }
};
var tags = new TagList();

try
{
Expand All @@ -152,37 +156,19 @@ public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffL
}
var duration = Stopwatch.GetElapsedTime(startTimestamp);
_batchDuration.Record(duration.TotalSeconds, tags);
_batchSize.Record(diffLength, tags);
}

public void FailBatchSync(Exception ex, long startTimestamp)
{
var duration = Stopwatch.GetElapsedTime(startTimestamp);
var tags = new TagList
{
{ "aspnetcore.components.diff.length", 0 },
{ "error.type", ex.GetType().FullName ?? "unknown" }
};
_batchDuration.Record(duration.TotalSeconds, tags);
}

private static int BucketDiffLength(int diffLength)
{
return diffLength switch
{
<= 1 => 1,
<= 2 => 2,
<= 5 => 5,
<= 10 => 10,
<= 20 => 20,
<= 50 => 50,
<= 100 => 100,
<= 500 => 500,
<= 1000 => 1000,
<= 10000 => 10000,
_ => 10001,
};
}

public void Dispose()
{
_meter.Dispose();
Expand Down
45 changes: 23 additions & 22 deletions src/Components/Components/test/ComponentsMetricsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void Navigation_RecordsMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var navigationCounter = new MetricCollector<long>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
ComponentsMetrics.MeterName, "aspnetcore.components.navigate");

// Act
componentsMetrics.Navigation("TestComponent", "/test-route");
Expand All @@ -61,7 +61,7 @@ public void IsNavigationEnabled_ReturnsCorrectState()

// Create a collector to ensure the meter is enabled
using var navigationCounter = new MetricCollector<long>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
ComponentsMetrics.MeterName, "aspnetcore.components.navigate");

// Act & Assert
Assert.True(componentsMetrics.IsNavigationEnabled);
Expand All @@ -73,7 +73,7 @@ public async Task CaptureEventDuration_RecordsSuccessMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration");

// Act
var startTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -98,7 +98,7 @@ public async Task CaptureEventDuration_RecordsErrorMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration");

// Act
var startTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -123,7 +123,7 @@ public void FailEventSync_RecordsErrorMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration");
var exception = new InvalidOperationException();

// Act
Expand All @@ -150,7 +150,7 @@ public void IsEventEnabled_ReturnsCorrectState()

// Create a collector to ensure the meter is enabled
using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration");

// Act & Assert
Assert.True(componentsMetrics.IsEventEnabled);
Expand All @@ -162,7 +162,7 @@ public async Task CaptureParametersDuration_RecordsSuccessMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration");

// Act
var startTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -184,7 +184,7 @@ public async Task CaptureParametersDuration_RecordsErrorMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration");

// Act
var startTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -207,7 +207,7 @@ public void FailParametersSync_RecordsErrorMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration");
var exception = new InvalidOperationException();

// Act
Expand All @@ -231,7 +231,7 @@ public void IsParametersEnabled_ReturnsCorrectState()

// Create a collector to ensure the meter is enabled
using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration");

// Act & Assert
Assert.True(componentsMetrics.IsParametersEnabled);
Expand All @@ -243,7 +243,7 @@ public async Task CaptureBatchDuration_RecordsSuccessMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration");

// Act
var startTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -255,7 +255,6 @@ public async Task CaptureBatchDuration_RecordsSuccessMetric()

Assert.Single(measurements);
Assert.True(measurements[0].Value > 0);
Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
Assert.DoesNotContain("error.type", measurements[0].Tags);
}

Expand All @@ -265,7 +264,7 @@ public async Task CaptureBatchDuration_RecordsErrorMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration");

// Act
var startTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -278,7 +277,6 @@ await componentsMetrics.CaptureBatchDuration(Task.FromException(new InvalidOpera

Assert.Single(measurements);
Assert.True(measurements[0].Value > 0);
Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
}

Expand All @@ -288,7 +286,7 @@ public void FailBatchSync_RecordsErrorMetric()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration");
var exception = new InvalidOperationException();

// Act
Expand All @@ -300,7 +298,6 @@ public void FailBatchSync_RecordsErrorMetric()

Assert.Single(measurements);
Assert.True(measurements[0].Value > 0);
Assert.Equal(0, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
}

Expand All @@ -312,7 +309,7 @@ public void IsBatchEnabled_ReturnsCorrectState()

// Create a collector to ensure the meter is enabled
using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration");

// Act & Assert
Assert.True(componentsMetrics.IsBatchEnabled);
Expand All @@ -324,13 +321,15 @@ public async Task ComponentLifecycle_RecordsAllMetricsCorrectly()
// Arrange
var componentsMetrics = new ComponentsMetrics(_meterFactory);
using var navigationCounter = new MetricCollector<long>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
ComponentsMetrics.MeterName, "aspnetcore.components.navigate");
using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
ComponentsMetrics.MeterName, "aspnetcore.components.handle_event.duration");
using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration");
using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.duration");
using var batchSizeHistogram = new MetricCollector<int>(_meterFactory,
ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff.size");

// Act - Simulate a component lifecycle
// 1. Navigation
Expand All @@ -357,11 +356,13 @@ await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2
var eventMeasurements = eventDurationHistogram.GetMeasurementSnapshot();
var parametersMeasurements = parametersDurationHistogram.GetMeasurementSnapshot();
var batchMeasurements = batchDurationHistogram.GetMeasurementSnapshot();
var batchSizeMeasurements = batchSizeHistogram.GetMeasurementSnapshot();

Assert.Single(navigationMeasurements);
Assert.Single(eventMeasurements);
Assert.Single(parametersMeasurements);
Assert.Single(batchMeasurements);
Assert.Single(batchSizeMeasurements);

// Check navigation
Assert.Equal(1, navigationMeasurements[0].Value);
Expand All @@ -380,7 +381,7 @@ await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2

// Check batch duration
Assert.True(batchMeasurements[0].Value > 0);
Assert.Equal(20, Assert.Contains("aspnetcore.components.diff.length", batchMeasurements[0].Tags));
Assert.True(batchSizeMeasurements[0].Value > 0);
}

[Fact]
Expand Down
7 changes: 5 additions & 2 deletions src/Shared/Metrics/MetricsConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ internal static class MetricsConstants
// Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration. See https://github.com/open-telemetry/semantic-conventions/issues/336
public static readonly IReadOnlyList<double> LongSecondsBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300];

// For blazor rendering, which should be very fast.
// For Blazor rendering, which should be very fast.
public static readonly IReadOnlyList<double> BlazorRenderingSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has more buckets than other durations (18 compared to 14). I don't know if that's a problem or not, but it's an inconsistency.


// For blazor circuit sessions, which can last a long time.
// For measuring the length of HTML diff in Blazor rendering.
public static readonly IReadOnlyList<int> BlazorRenderingDiffLengthBucketBoundaries = [0, 1, 2, 4, 6, 8, 10, 20, 30, 40, 50, 100];

// For Blazor circuit sessions, which can last a long time.
public static readonly IReadOnlyList<double> BlazorCircuitSecondsBucketBoundaries = [1, 3, 10, 30, 1 * 60, 3 * 60, 10 * 60, 30 * 60, 1 * 60 * 60, 3 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60];
}
Loading