Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public Task OnViewRelationshipAsync(ResourceDetailRelationshipViewModel relation
public void UpdateTelemetryProperties()
{
TelemetryContext.UpdateTelemetryProperties([
new ComponentTelemetryProperty(TelemetryPropertyKeys.ResourceType, new AspireTelemetryProperty(Resource.ResourceType))
new ComponentTelemetryProperty(TelemetryPropertyKeys.ResourceType, new AspireTelemetryProperty(TelemetryPropertyValues.GetResourceTypeTelemetryValue(Resource.ResourceType)))
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public bool UpdateTelemetryProperties(ReadOnlySpan<ComponentTelemetryProperty> m

foreach (var (name, value) in modifiedProperties)
{
if (value.Value is string s && string.IsNullOrEmpty(s))
{
continue;
}

if (!Properties.TryGetValue(name, out var existingValue) || !existingValue.Value.Equals(value.Value))
{
Properties[name] = value;
Expand Down
5 changes: 1 addition & 4 deletions src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,8 @@ public void Dispose()
public void UpdateTelemetryProperties()
{
TelemetryContext.UpdateTelemetryProperties([
new ComponentTelemetryProperty(TelemetryPropertyKeys.ApplicationInstanceId, new AspireTelemetryProperty(PageViewModel.SelectedApplication.Id?.InstanceId ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsApplicationIsReplica, new AspireTelemetryProperty(PageViewModel.SelectedApplication.Id?.ReplicaSetName is not null)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsInstrumentsCount, new AspireTelemetryProperty(PageViewModel.Instruments?.Count ?? -1)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsSelectedMeter, new AspireTelemetryProperty(PageViewModel.SelectedMeter?.MeterName ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsSelectedInstrument, new AspireTelemetryProperty(PageViewModel.SelectedInstrument?.Name ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsInstrumentsCount, new AspireTelemetryProperty(PageViewModel.Instruments?.Count ?? -1, AspireTelemetryPropertyType.Metric)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsSelectedDuration, new AspireTelemetryProperty(PageViewModel.SelectedDuration.Id.ToString(), AspireTelemetryPropertyType.UserSetting)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsSelectedView, new AspireTelemetryProperty(PageViewModel.SelectedViewKind?.ToString() ?? string.Empty, AspireTelemetryPropertyType.UserSetting))
]);
Expand Down
8 changes: 2 additions & 6 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -833,14 +833,10 @@ public void UpdateTelemetryProperties()
{
var properties = new List<ComponentTelemetryProperty>
{
new(TelemetryPropertyKeys.ResourceView, new AspireTelemetryProperty(PageViewModel.SelectedViewKind.ToString(), AspireTelemetryPropertyType.UserSetting))
new(TelemetryPropertyKeys.ResourceView, new AspireTelemetryProperty(PageViewModel.SelectedViewKind.ToString(), AspireTelemetryPropertyType.UserSetting)),
new(TelemetryPropertyKeys.ResourceTypes, new AspireTelemetryProperty(_resourceByName.Values.Select(r => TelemetryPropertyValues.GetResourceTypeTelemetryValue(r.ResourceType)).OrderBy(t => t).ToList()))
Copy link
Member

Choose a reason for hiding this comment

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

We're no longer passing a count of the resource types and just having a list of strings. Why this change? I'm asking because I have telemetry that passed in multiple values with counts like before this change.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem with the prior approach is that each of the keys count as a different telemetry property and each will need to be classified. That's a manual process and will need to be done for every resource type.

Copy link
Member

@JamesNK JamesNK May 5, 2025

Choose a reason for hiding this comment

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

Should there be a distinct here? The current output will contain duplicates, e.g. 50 containers means container is repeated 50 times. That doesn't seem desirable.

Or is that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's intentional so that we can count the number of occurrences of different types.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's intentional so that we can count the number of occurrences of different types.

};

foreach (var resourceTypeGroup in _resourceByName.Values.GroupBy(r => r.ResourceType))
{
properties.Add(new ComponentTelemetryProperty($"{TelemetryPropertyKeys.ResourceType}.{resourceTypeGroup.Key}", new AspireTelemetryProperty(resourceTypeGroup.Count(), AspireTelemetryPropertyType.Metric)));
}

TelemetryContext.UpdateTelemetryProperties(properties.ToArray());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -495,11 +495,8 @@ public class StructuredLogsPageState
public void UpdateTelemetryProperties()
{
TelemetryContext.UpdateTelemetryProperties([
new ComponentTelemetryProperty(TelemetryPropertyKeys.StructuredLogsSelectedApplication, new AspireTelemetryProperty(PageViewModel.SelectedApplication.Id?.ToString() ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.StructuredLogsSelectedLogLevel, new AspireTelemetryProperty(PageViewModel.SelectedLogLevel.Id?.ToString() ?? string.Empty, AspireTelemetryPropertyType.UserSetting)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.StructuredLogsFilterCount, new AspireTelemetryProperty(ViewModel.Filters.Count.ToString(CultureInfo.InvariantCulture), AspireTelemetryPropertyType.Metric)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.StructuredLogsTraceId, new AspireTelemetryProperty(TraceId ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.StructuredLogsSpanId, new AspireTelemetryProperty(SpanId ?? string.Empty))
new ComponentTelemetryProperty(TelemetryPropertyKeys.StructuredLogsFilterCount, new AspireTelemetryProperty(ViewModel.Filters.Count.ToString(CultureInfo.InvariantCulture), AspireTelemetryPropertyType.Metric))
Copy link
Member

Choose a reason for hiding this comment

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

I noticed you're doing a ToString here but not for TelemetryPropertyKeys.MetricsInstrumentsCount. Why the difference?

]);
}
}
10 changes: 0 additions & 10 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
Expand Down Expand Up @@ -177,8 +176,6 @@ protected override async Task OnParametersSetAsync()
// Navigate to remove ?spanId=xxx in the URL.
NavigationManager.NavigateTo(DashboardUrls.TraceDetailUrl(TraceId), new NavigationOptions { ReplaceHistoryEntry = true });
}

UpdateTelemetryProperties();
}

private void UpdateDetailViewData()
Expand Down Expand Up @@ -337,11 +334,4 @@ public void Dispose()

// IComponentWithTelemetry impl
public ComponentTelemetryContext TelemetryContext { get; } = new(DashboardUrls.TracesBasePath);

public void UpdateTelemetryProperties()
{
TelemetryContext.UpdateTelemetryProperties([
new ComponentTelemetryProperty(TelemetryPropertyKeys.TraceDetailTraceId, new AspireTelemetryProperty(TraceId)),
]);
}
}
9 changes: 0 additions & 9 deletions src/Aspire.Dashboard/Components/Pages/Traces.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -182,7 +181,6 @@ protected override async Task OnParametersSetAsync()

TracesViewModel.ApplicationKey = PageViewModel.SelectedApplication.Id?.GetApplicationKey();
UpdateSubscription();
UpdateTelemetryProperties();
}

private void UpdateApplications()
Expand Down Expand Up @@ -385,11 +383,4 @@ public class TracesPageState

// IComponentWithTelemetry impl
public ComponentTelemetryContext TelemetryContext { get; } = new(DashboardUrls.TracesBasePath);

public void UpdateTelemetryProperties()
{
TelemetryContext.UpdateTelemetryProperties([
new ComponentTelemetryProperty(TelemetryPropertyKeys.ApplicationInstanceId, new AspireTelemetryProperty(PageViewModel.SelectedApplication.Id?.InstanceId ?? string.Empty)),
]);
}
}
15 changes: 11 additions & 4 deletions src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,27 @@ public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel comm
var startEvent = telemetryService.StartOperation(TelemetryEventKeys.ExecuteCommand,
new Dictionary<string, AspireTelemetryProperty>
{
{ TelemetryPropertyKeys.ResourceType, new AspireTelemetryProperty(resource.ResourceType) },
{ TelemetryPropertyKeys.CommandName, new AspireTelemetryProperty(command.Name) },
{ TelemetryPropertyKeys.ResourceType, new AspireTelemetryProperty(TelemetryPropertyValues.GetResourceTypeTelemetryValue(resource.ResourceType)) },
{ TelemetryPropertyKeys.CommandName, new AspireTelemetryProperty(TelemetryPropertyValues.GetCommandNameTelemetryValue(command.Name)) },
});

var operationId = startEvent.Properties.FirstOrDefault();

try
{
await ExecuteAsyncCore(resource, command, getResourceName).ConfigureAwait(false);
telemetryService.EndOperation(operationId, TelemetryResult.Success);

if (operationId is not null)
{
telemetryService.EndOperation(operationId, TelemetryResult.Success);
}
}
catch (Exception ex)
{
telemetryService.EndUserTask(operationId, TelemetryResult.Failure, ex.Message);
if (operationId is not null)
{
telemetryService.EndUserTask(operationId, TelemetryResult.Failure, ex.Message);
}
}
finally
{
Expand Down
8 changes: 4 additions & 4 deletions src/Aspire.Dashboard/Telemetry/DashboardTelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ public OperationContext StartOperation(string eventName, Dictionary<string, Aspi
/// <summary>
/// Ends a long-running operation. This will post the end event and calculate the duration.
/// </summary>
public void EndOperation(OperationContextProperty? operationId, TelemetryResult result, string? errorMessage = null)
public void EndOperation(OperationContextProperty operationId, TelemetryResult result, string? errorMessage = null)
{
if (SkipQueuingRequests() || operationId is null)
if (SkipQueuingRequests())
{
return;
}
Expand Down Expand Up @@ -159,9 +159,9 @@ public OperationContext StartUserTask(string eventName, Dictionary<string, Aspir
/// <summary>
/// Ends a long-running user task. This will post the end event and calculate the duration.
/// </summary>
public void EndUserTask(OperationContextProperty? operationId, TelemetryResult result, string? errorMessage = null)
public void EndUserTask(OperationContextProperty operationId, TelemetryResult result, string? errorMessage = null)
{
if (SkipQueuingRequests() || operationId is null)
if (SkipQueuingRequests())
{
return;
}
Expand Down
11 changes: 1 addition & 10 deletions src/Aspire.Dashboard/Telemetry/TelemetryPropertyKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@ public static class TelemetryPropertyKeys
public const string DashboardComponentId = AspireDashboardPropertyPrefix + "ComponentId";
public const string UserAgent = AspireDashboardPropertyPrefix + "UserAgent";

public const string ApplicationInstanceId = AspireDashboardPropertyPrefix + "Metrics.ApplicationInstanceId";

// ConsoleLogs properties
public const string ConsoleLogsShowTimestamp = AspireDashboardPropertyPrefix + "ConsoleLogs.ShowTimestamp";
public const string ConsoleLogsApplicationName = AspireDashboardPropertyPrefix + "ConsoleLogs.ApplicationName";

// Metrics properties
public const string MetricsApplicationIsReplica = AspireDashboardPropertyPrefix + "Metrics.ApplicationIsReplica";
public const string MetricsInstrumentsCount = AspireDashboardPropertyPrefix + "Metrics.InstrumentsCount";
public const string MetricsSelectedMeter = AspireDashboardPropertyPrefix + "Metrics.SelectedMeter";
public const string MetricsSelectedInstrument = AspireDashboardPropertyPrefix + "Metrics.SelectedInstrument";
public const string MetricsSelectedDuration = AspireDashboardPropertyPrefix + "Metrics.SelectedDuration";
public const string MetricsSelectedView = AspireDashboardPropertyPrefix + "Metrics.SelectedView";

Expand All @@ -35,21 +31,16 @@ public static class TelemetryPropertyKeys
public const string ExceptionStackTrace = AspireDashboardPropertyPrefix + "Exception.StackTrace";

// Resources properties
public const string ResourceTypes = AspireDashboardPropertyPrefix + "Resource.Types";
public const string ResourceType = AspireDashboardPropertyPrefix + "Resource.Type";
public const string ResourceView = AspireDashboardPropertyPrefix + "Resource.View";

// Error properties
public const string ErrorRequestId = AspireDashboardPropertyPrefix + "RequestId";

// Trace detail properties
public const string TraceDetailTraceId = AspireDashboardPropertyPrefix + "TraceDetail.TraceId";

// Structured logs properties
public const string StructuredLogsSelectedApplication = AspireDashboardPropertyPrefix + "StructuredLogs.SelectedApplication";
public const string StructuredLogsSelectedLogLevel = AspireDashboardPropertyPrefix + "StructuredLogs.SelectedLogLevel";
public const string StructuredLogsFilterCount = AspireDashboardPropertyPrefix + "StructuredLogs.FilterCount";
public const string StructuredLogsTraceId = AspireDashboardPropertyPrefix + "StructuredLogs.TraceId";
public const string StructuredLogsSpanId = AspireDashboardPropertyPrefix + "StructuredLogs.SpanId";

// Command properties
public const string CommandName = AspireDashboardPropertyPrefix + "Command.Name";
Expand Down
27 changes: 27 additions & 0 deletions src/Aspire.Dashboard/Telemetry/TelemetryPropertyValues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
using Aspire.Hosting.Dashboard;

namespace Aspire.Dashboard.Telemetry;

public static class TelemetryPropertyValues
{
private const string CustomResourceCommand = "custom-command";
private const string CustomResourceType = "custom-resource-type";

public static string GetCommandNameTelemetryValue(string commandName)
{
return KnownResourceCommands.IsKnownCommand(commandName)
? commandName
: CustomResourceCommand;
}

public static string GetResourceTypeTelemetryValue(string resourceType)
{
return KnownResourceTypes.IsKnownResourceType(resourceType)
? resourceType
: CustomResourceType;
}
}
5 changes: 5 additions & 0 deletions src/Shared/Model/KnownResourceCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ internal static class KnownResourceCommands
public const string StartCommand = "resource-start";
public const string StopCommand = "resource-stop";
public const string RestartCommand = "resource-restart";

public static bool IsKnownCommand(string command)
{
return command is StartCommand or StopCommand or RestartCommand;
}
}
10 changes: 10 additions & 0 deletions src/Shared/Model/KnownResourceTypes.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;

namespace Aspire.Dashboard.Model;

internal static class KnownResourceTypes
{
public const string Executable = "Executable";
public const string Project = "Project";
public const string Container = "Container";

// This field needs to be updated when new resource types are added.
private static readonly ImmutableArray<string> s_builtInResources = ["Resource", "AzureAppConfigurationResource", "AzureContainerAppEnvironmentResource", "AzureApplicationInsightsResource", "AzureOpenAIDeploymentResource", "AzureOpenAIResource", "AzureCosmosDBContainerResource", "AzureCosmosDBDatabaseResource", "AzureCosmosDBEmulatorResource", "AzureCosmosDBResource", "AzureEventHubConsumerGroupResource", "AzureEventHubResource", "AzureEventHubsEmulatorResource", "AzureEventHubsResource", "AzureFunctionsProjectResource", "AzureKeyVaultResource", "AzureLogAnalyticsWorkspaceResource", "AzurePostgresFlexibleServerDatabaseResource", "AzurePostgresFlexibleServerResource", "AzurePostgresResource", "AzureRedisCacheResource", "AzureRedisResource", "AzureSearchResource", "AzureServiceBusEmulatorResource", "AzureServiceBusQueueResource", "AzureServiceBusResource", "AzureServiceBusSubscriptionResource", "AzureServiceBusTopicResource", "AzureSignalREmulatorResource", "AzureSignalRResource", "AzureSqlDatabaseResource", "AzureSqlServerResource", "AzureBlobStorageResource", "AzureQueueStorageResource", "AzureStorageEmulatorResource", "AzureStorageResource", "AzureTableStorageResource", "AzureWebPubSubHubResource", "AzureWebPubSubResource", "AppIdentityResource", "AzureBicepResource", "AzureProvisioningResource", "DockerComposeEnvironmentResource", "DockerComposeServiceResource", "ElasticsearchResource", "GarnetResource", "KafkaServerResource", "KafkaUIContainerResource", "KeycloakResource", "KubernetesEnvironmentResource", "KubernetesResource", "AttuResource", "MilvusDatabaseResource", "MilvusServerResource", "MongoDBDatabaseResource", "MongoDBServerResource", "MongoExpressContainerResource", "MySqlDatabaseResource", "MySqlServerResource", "PhpMyAdminContainerResource", "NatsServerResource", "NodeAppResource", "OracleDatabaseResource", "OracleDatabaseServerResource", "PgAdminContainerResource", "PgWebContainerResource", "PostgresDatabaseResource", "PostgresServerResource", "PythonAppResource", "PythonProjectResource", "QdrantServerResource", "RabbitMQServerResource", "RedisCommanderResource", "RedisInsightResource", "RedisResource", "SeqResource", "SqlServerDatabaseResource", "SqlServerServerResource", "ValkeyResource", "ContainerResource", "ExecutableResource", "ParameterResource", "ProjectResource", "ConnectionStringParameterResource", "ConnectionStringResource", "ExecutableContainerResource", "ProjectContainerResource"];
Copy link
Member

Choose a reason for hiding this comment

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

This list doesn't seem to have executable, project or container. Do they have resource in the end when tested? e.g. ExecutableResource. Can you double check and maybe add a test. Add a comment if they're not needed explaining why.

Copy link
Member Author

Choose a reason for hiding this comment

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

The list had ExecutableResource, ProjectResource, and ContainerResource. These seem to be the only special cases where the resource type differs from the class name. I've updated them


public static bool IsKnownResourceType(string resourceType)
{
return s_builtInResources.Contains(resourceType);
}
}