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
6 changes: 2 additions & 4 deletions src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Components.Layout;
using Aspire.Dashboard.Model;
Expand Down Expand Up @@ -320,11 +321,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?.Name ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsSelectedInstrument, new AspireTelemetryProperty(PageViewModel.SelectedInstrument?.Name ?? string.Empty)),
new ComponentTelemetryProperty(TelemetryPropertyKeys.MetricsInstrumentsCount, new AspireTelemetryProperty((PageViewModel.Instruments?.Count ?? -1).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.

So string is prefered? I want to know because I have telemetry that has a numeric value

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 @@ -860,14 +860,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
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Limits are per-resource. For example, a `MaxLogCount` value of 10,000 configures

## Data collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkId=521839. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

### Opting out of data collection

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;
}
}
8 changes: 8 additions & 0 deletions src/Shared/Model/KnownResourceTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,12 @@ 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 HashSet<string> s_builtInResources = ["AppIdentityResource", "AttuResource", "AzureAppConfigurationResource", "AzureApplicationInsightsResource", "AzureBicepResource", "AzureBlobStorageResource", "AzureContainerAppEnvironmentResource", "AzureCosmosDBContainerResource", "AzureCosmosDBDatabaseResource", "AzureCosmosDBEmulatorResource", "AzureCosmosDBResource", "AzureEventHubConsumerGroupResource", "AzureEventHubResource", "AzureEventHubsEmulatorResource", "AzureEventHubsResource", "AzureFunctionsProjectResource", "AzureKeyVaultResource", "AzureLogAnalyticsWorkspaceResource", "AzureOpenAIDeploymentResource", "AzureOpenAIResource", "AzurePostgresFlexibleServerDatabaseResource", "AzurePostgresFlexibleServerResource", "AzurePostgresResource", "AzureProvisioningResource", "AzureQueueStorageResource", "AzureRedisCacheResource", "AzureRedisResource", "AzureSearchResource", "AzureServiceBusEmulatorResource", "AzureServiceBusQueueResource", "AzureServiceBusResource", "AzureServiceBusSubscriptionResource", "AzureServiceBusTopicResource", "AzureSignalREmulatorResource", "AzureSignalRResource", "AzureSqlDatabaseResource", "AzureSqlServerResource", "AzureStorageEmulatorResource", "AzureStorageResource", "AzureTableStorageResource", "AzureWebPubSubHubResource", "AzureWebPubSubResource", "ConnectionStringParameterResource", "ConnectionStringResource", Container, "DockerComposeEnvironmentResource", "DockerComposeServiceResource", "ElasticsearchResource", Executable, "ExecutableContainerResource", "GarnetResource", "KafkaServerResource", "KafkaUIContainerResource", "KeycloakResource", "KubernetesEnvironmentResource", "KubernetesResource", "MilvusDatabaseResource", "MilvusServerResource", "MongoDBDatabaseResource", "MongoDBServerResource", "MongoExpressContainerResource", "MySqlDatabaseResource", "MySqlServerResource", "NatsServerResource", "NodeAppResource", "OracleDatabaseResource", "OracleDatabaseServerResource", "ParameterResource", "PgAdminContainerResource", "PgWebContainerResource", "PhpMyAdminContainerResource", "PostgresDatabaseResource", "PostgresServerResource", Project, "ProjectContainerResource", "PythonAppResource", "PythonProjectResource", "QdrantServerResource", "RabbitMQServerResource", "RedisCommanderResource", "RedisInsightResource", "RedisResource", "Resource", "SeqResource", "SqlServerDatabaseResource", "SqlServerServerResource", "ValkeyResource"];
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

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

This seems wrong. We shouldn't be treating Azure resources any different than AWS resources. Similarly, CommunityToolkit resources shouldn't act differently than resources we define in our repo.

Is there a better/different way to accomplish what we are trying to do here?

Copy link
Member Author

Choose a reason for hiding this comment

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

We have to maintain an allow list of resources

Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is going to be an acceptable solution. This list is going to get out of date, and already has. Maybe a better way is to add an annotation on our Resources, AllowTelemetryOptInAnnotation or similar.

Copy link
Member

Choose a reason for hiding this comment

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

Why is this hard coded in the source codes

Copy link
Member

Choose a reason for hiding this comment

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

Another option would be to say if the assembly that the Type came from has our public key, it is a "known resource".


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