Skip to content

Commit 9039a55

Browse files
karolz-msJamesNK
andauthored
Use uniform convention for naming and annotating DCP objects with regards to OTEL (dotnet#4661)
Co-authored-by: James Newton-King <[email protected]>
1 parent 0c72241 commit 9039a55

31 files changed

+234
-149
lines changed

src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ private void UpdateResourcesList()
191191
}
192192
}
193193

194-
_resources = builder.ToImmutable();
194+
builder.Sort((r1, r2) => StringComparers.ResourceName.Compare(r1.Name, r2.Name));
195+
196+
_resources = builder.ToImmutableList();
195197

196198
SelectViewModel<ResourceTypeDetails> ToOption(ResourceViewModel resource, bool isReplica, string applicationName)
197199
{

src/Aspire.Dashboard/Model/Otlp/ApplicationsSelectHelpers.cs

+10-5
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ public static SelectViewModel<ResourceTypeDetails> GetApplication(this List<Sele
2727
{
2828
// There are multiple matches. Log as much information as possible about applications.
2929
logger.LogWarning(
30-
$"Multiple matches found when getting application '{name}'. " +
31-
$"Available applications: {string.Join(Environment.NewLine, applications)} " +
32-
$"Matched applications: {string.Join(Environment.NewLine, matches)}");
30+
"""
31+
Multiple matches found when getting application '{Name}'.
32+
Available applications:
33+
{AvailableApplications}
34+
Matched applications:
35+
{MatchedApplications}
36+
""", name, string.Join(Environment.NewLine, applications), string.Join(Environment.NewLine, matches));
3337

3438
// Return first match to not break app. Make the UI resilient to unexpectedly bad data.
3539
return matches[0];
@@ -69,10 +73,11 @@ public static List<SelectViewModel<ResourceTypeDetails>> CreateApplications(List
6973
new SelectViewModel<ResourceTypeDetails>
7074
{
7175
Id = ResourceTypeDetails.CreateReplicaInstance(replica.InstanceId, applicationName),
72-
Name = replica.InstanceId
76+
Name = OtlpApplication.GetResourceName(replica, applications)
7377
}));
7478
}
7579

76-
return selectViewModels;
80+
var sortedVMs = selectViewModels.OrderBy(vm => vm.Name, StringComparers.ResourceName).ToList();
81+
return sortedVMs;
7782
}
7883
}

src/Aspire.Dashboard/Model/ResourceViewModel.cs

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public static string GetResourceName(ResourceViewModel resource, ConcurrentDicti
4747
count++;
4848
if (count >= 2)
4949
{
50+
// There are multiple resources with the same display name so they're part of a replica set.
51+
// Need to use the name which has a unique ID to tell them apart.
5052
return resource.Name;
5153
}
5254
}

src/Aspire.Dashboard/Otlp/Model/OtlpApplication.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,12 @@ public static string GetResourceName(OtlpApplication app, List<OtlpApplication>
193193
var count = 0;
194194
foreach (var item in allApplications)
195195
{
196-
if (item.ApplicationName == app.ApplicationName)
196+
if (string.Equals(item.ApplicationName, app.ApplicationName, StringComparisons.ResourceName))
197197
{
198198
count++;
199199
if (count >= 2)
200200
{
201-
return app.InstanceId;
201+
return $"{item.ApplicationName}-{app.InstanceId}";
202202
}
203203
}
204204
}

src/Aspire.Dashboard/ResourceService/DashboardClient.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,17 @@ private void EnsureInitialized()
229229

230230
async Task ConnectAndWatchResourcesAsync(CancellationToken cancellationToken)
231231
{
232-
await ConnectAsync().ConfigureAwait(false);
232+
try
233+
{
234+
await ConnectAsync().ConfigureAwait(false);
233235

234-
await WatchResourcesWithRecoveryAsync().ConfigureAwait(false);
236+
await WatchResourcesWithRecoveryAsync().ConfigureAwait(false);
237+
}
238+
catch (Exception ex)
239+
{
240+
_logger.LogError(ex, "Error loading data from the resource service.");
241+
throw;
242+
}
235243

236244
async Task ConnectAsync()
237245
{

src/Aspire.Hosting/Dcp/ApplicationExecutor.cs

+32-31
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ internal sealed class ApplicationExecutor(ILogger<ApplicationExecutor> logger,
7373
{
7474
private const string DebugSessionPortVar = "DEBUG_SESSION_PORT";
7575

76+
// A random suffix added to every DCP object name ensures that those names (and derived object names, for example container names)
77+
// are unique machine-wide with a high level of probability.
78+
// The length of 8 achieves that while keeping the names relatively short and readable.
79+
// The second purpose of the suffix is to play a role of a unique OpenTelemetry service instance ID.
80+
private const int RandomNameSuffixLength = 8;
81+
7682
private readonly ILogger<ApplicationExecutor> _logger = logger;
7783
private readonly DistributedApplicationModel _model = model;
7884
private readonly Dictionary<string, IResource> _applicationModel = model.Resources.ToDictionary(r => r.Name);
@@ -448,8 +454,8 @@ private void StartLogStream<T>(T resource) where T : CustomResource
448454
{
449455
IAsyncEnumerable<IReadOnlyList<(string, bool)>>? enumerable = resource switch
450456
{
451-
Container c when c.LogsAvailable => new ResourceLogSource<T>(_logger, kubernetesService, resource),
452-
Executable e when e.LogsAvailable => new ResourceLogSource<T>(_logger, kubernetesService, resource),
457+
Container c when c.LogsAvailable => new ResourceLogSource<T>(_logger, kubernetesService, _dcpInfo?.Version, resource),
458+
Executable e when e.LogsAvailable => new ResourceLogSource<T>(_logger, kubernetesService, _dcpInfo?.Version, resource),
453459
_ => null
454460
};
455461

@@ -971,14 +977,16 @@ private void PreparePlainExecutables()
971977

972978
foreach (var executable in modelExecutableResources)
973979
{
974-
var exeName = GetObjectNameForResource(executable);
980+
var nameSuffix = GetRandomNameSuffix();
981+
var exeName = GetObjectNameForResource(executable, nameSuffix);
975982
var exePath = executable.Command;
976983
var exe = Executable.Create(exeName, exePath);
977984

978985
// The working directory is always relative to the app host project directory (if it exists).
979986
exe.Spec.WorkingDirectory = executable.WorkingDirectory;
980987
exe.Spec.ExecutionType = ExecutionType.Process;
981-
exe.Annotate(CustomResource.OtelServiceNameAnnotation, exe.Metadata.Name);
988+
exe.Annotate(CustomResource.OtelServiceNameAnnotation, executable.Name);
989+
exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, nameSuffix);
982990
exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name);
983991
SetInitialResourceState(executable, exe);
984992

@@ -1007,6 +1015,7 @@ private void PrepareProjectExecutables()
10071015

10081016
IAnnotationHolder annotationHolder = ers.Spec.Template;
10091017
annotationHolder.Annotate(CustomResource.OtelServiceNameAnnotation, ers.Metadata.Name);
1018+
// The OTEL service instance ID annotation will be generated and applied automatically by DCP.
10101019
annotationHolder.Annotate(CustomResource.ResourceNameAnnotation, project.Name);
10111020

10121021
SetInitialResourceState(project, annotationHolder);
@@ -1018,29 +1027,10 @@ private void PrepareProjectExecutables()
10181027
{
10191028
exeSpec.ExecutionType = ExecutionType.IDE;
10201029

1021-
if (_dcpInfo?.Version?.CompareTo(DcpVersion.MinimumVersionIdeProtocolV1) >= 0)
1030+
projectLaunchConfiguration.DisableLaunchProfile = project.TryGetLastAnnotation<ExcludeLaunchProfileAnnotation>(out _);
1031+
if (!projectLaunchConfiguration.DisableLaunchProfile && project.TryGetLastAnnotation<LaunchProfileAnnotation>(out var lpa))
10221032
{
1023-
projectLaunchConfiguration.DisableLaunchProfile = project.TryGetLastAnnotation<ExcludeLaunchProfileAnnotation>(out _);
1024-
if (!projectLaunchConfiguration.DisableLaunchProfile && project.TryGetLastAnnotation<LaunchProfileAnnotation>(out var lpa))
1025-
{
1026-
projectLaunchConfiguration.LaunchProfile = lpa.LaunchProfileName;
1027-
}
1028-
}
1029-
else
1030-
{
1031-
#pragma warning disable CS0612 // These annotations are obsolete; remove after Aspire GA
1032-
annotationHolder.Annotate(Executable.CSharpProjectPathAnnotation, projectMetadata.ProjectPath);
1033-
1034-
// ExcludeLaunchProfileAnnotation takes precedence over LaunchProfileAnnotation.
1035-
if (project.TryGetLastAnnotation<ExcludeLaunchProfileAnnotation>(out _))
1036-
{
1037-
annotationHolder.Annotate(Executable.CSharpDisableLaunchProfileAnnotation, "true");
1038-
}
1039-
else if (project.TryGetLastAnnotation<LaunchProfileAnnotation>(out var lpa))
1040-
{
1041-
annotationHolder.Annotate(Executable.CSharpLaunchProfileAnnotation, lpa.LaunchProfileName);
1042-
}
1043-
#pragma warning restore CS0612
1033+
projectLaunchConfiguration.LaunchProfile = lpa.LaunchProfileName;
10441034
}
10451035
}
10461036
else
@@ -1327,10 +1317,14 @@ private void PrepareContainers()
13271317
throw new InvalidOperationException();
13281318
}
13291319

1330-
var ctr = Container.Create(GetObjectNameForResource(container), containerImageName);
1320+
var nameSuffix = GetRandomNameSuffix();
1321+
var containerObjectName = GetObjectNameForResource(container, nameSuffix);
1322+
var ctr = Container.Create(containerObjectName, containerImageName);
13311323

1324+
ctr.Spec.ContainerName = containerObjectName; // Use the same name for container orchestrator (Docker, Podman) resource and DCP object name.
13321325
ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name);
13331326
ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name);
1327+
ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, nameSuffix);
13341328
SetInitialResourceState(container, ctr);
13351329

13361330
if (container.TryGetContainerMounts(out var containerMounts))
@@ -1739,9 +1733,9 @@ static string maybeWithSuffix(string s, string localSuffix, string? globalSuffix
17391733
=> (string.IsNullOrWhiteSpace(localSuffix), string.IsNullOrWhiteSpace(globalSuffix)) switch
17401734
{
17411735
(true, true) => s,
1742-
(false, true) => $"{s}_{localSuffix}",
1743-
(true, false) => $"{s}_{globalSuffix}",
1744-
(false, false) => $"{s}_{localSuffix}_{globalSuffix}"
1736+
(false, true) => $"{s}-{localSuffix}",
1737+
(true, false) => $"{s}-{globalSuffix}",
1738+
(false, false) => $"{s}-{localSuffix}-{globalSuffix}"
17451739
};
17461740
return maybeWithSuffix(resource.Name, suffix, _options.Value.ResourceNameSuffix);
17471741
}
@@ -1753,7 +1747,7 @@ private static string GenerateUniqueServiceName(HashSet<string> serviceNames, st
17531747

17541748
while (!serviceNames.Add(uniqueName))
17551749
{
1756-
uniqueName = $"{candidateName}_{suffix}";
1750+
uniqueName = $"{candidateName}-{suffix}";
17571751
suffix++;
17581752
if (suffix == 100)
17591753
{
@@ -1765,6 +1759,13 @@ private static string GenerateUniqueServiceName(HashSet<string> serviceNames, st
17651759
return uniqueName;
17661760
}
17671761

1762+
private static string GetRandomNameSuffix()
1763+
{
1764+
// RandomNameSuffixLength of lowercase characters
1765+
var suffix = PasswordGenerator.Generate(RandomNameSuffixLength, true, false, false, false, RandomNameSuffixLength, 0, 0, 0);
1766+
return suffix;
1767+
}
1768+
17681769
public async Task DeleteResourcesAsync(CancellationToken cancellationToken = default)
17691770
{
17701771
try

src/Aspire.Hosting/Dcp/DcpVersion.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ namespace Aspire.Hosting.Dcp;
55

66
internal static class DcpVersion
77
{
8-
public static Version MinimumVersionInclusive = new Version(0, 1, 55);
9-
public static Version MinimumVersionIdeProtocolV1 = new Version(0, 1, 61);
8+
public static Version MinimumVersionInclusive = new Version(0, 2, 3); // Aspire GA (8.0) release
9+
public static Version MinimumVersionAspire_8_1 = new Version(0, 5, 6);
1010

1111
/// <summary>
1212
/// Development build version proxy, considered always "current" and supporting latest features.

src/Aspire.Hosting/Dcp/Model/ModelCommon.cs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal abstract class CustomResource : KubernetesObject, IMetadata<V1ObjectMet
2323
public const string EndpointNameAnnotation = "endpoint-name";
2424
public const string ResourceNameAnnotation = "resource-name";
2525
public const string OtelServiceNameAnnotation = "otel-service-name";
26+
public const string OtelServiceInstanceIdAnnotation = "otel-service-instance-id";
2627
public const string ResourceStateAnnotation = "resource-state";
2728

2829
public string? AppModelResourceName => Metadata.Annotations?.TryGetValue(ResourceNameAnnotation, out var value) is true ? value : null;

src/Aspire.Hosting/Dcp/ResourceLogSource.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace Aspire.Hosting.Dcp;
1313
internal sealed class ResourceLogSource<TResource>(
1414
ILogger logger,
1515
IKubernetesService kubernetesService,
16+
Version? dcpVersion,
1617
TResource resource) :
1718
IAsyncEnumerable<LogEntryList>
1819
where TResource : CustomResource
@@ -35,7 +36,7 @@ public async IAsyncEnumerator<LogEntryList> GetAsyncEnumerator(CancellationToken
3536

3637
var timestamps = resource is Container; // Timestamps are available only for Containers as of Aspire P5.
3738

38-
if (resource is Container)
39+
if (resource is Container && dcpVersion?.CompareTo(DcpVersion.MinimumVersionAspire_8_1) >= 0)
3940
{
4041
var startupStderrStream = await kubernetesService.GetLogStreamAsync(resource, Logs.StreamTypeStartupStdErr, follow: true, timestamps: timestamps, cancellationToken).ConfigureAwait(false);
4142
var startupStdoutStream = await kubernetesService.GetLogStreamAsync(resource, Logs.StreamTypeStartupStdOut, follow: true, timestamps: timestamps, cancellationToken).ConfigureAwait(false);

src/Aspire.Hosting/OtlpConfigurationExtensions.cs

+16-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Dcp;
6+
using Aspire.Hosting.Dcp.Model;
57
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.DependencyInjection;
69
using Microsoft.Extensions.Hosting;
710

811
namespace Aspire.Hosting;
@@ -27,7 +30,7 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
2730
// Configure OpenTelemetry in projects using environment variables.
2831
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md
2932

30-
resource.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
33+
resource.Annotations.Add(new EnvironmentCallbackAnnotation(async context =>
3134
{
3235
if (context.ExecutionContext.IsPublishMode)
3336
{
@@ -56,8 +59,18 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
5659
}
5760

5861
// Set the service name and instance id to the resource name and UID. Values are injected by DCP.
59-
context.EnvironmentVariables["OTEL_RESOURCE_ATTRIBUTES"] = "service.instance.id={{- .Name -}}";
60-
context.EnvironmentVariables["OTEL_SERVICE_NAME"] = "{{- index .Annotations \"otel-service-name\" -}}";
62+
var dcpDependencyCheckService = context.ExecutionContext.ServiceProvider.GetRequiredService<IDcpDependencyCheckService>();
63+
var dcpInfo = await dcpDependencyCheckService.GetDcpInfoAsync(context.CancellationToken).ConfigureAwait(false);
64+
if (dcpInfo?.Version?.CompareTo(DcpVersion.MinimumVersionAspire_8_1) >= 0)
65+
{
66+
context.EnvironmentVariables["OTEL_RESOURCE_ATTRIBUTES"] = "service.instance.id={{- index .Annotations \"" + CustomResource.OtelServiceInstanceIdAnnotation + "\" -}}";
67+
}
68+
else
69+
{
70+
// Versions prior to Aspire 8.1 do not OTEL service instance ID annotation for replicated Executables.
71+
context.EnvironmentVariables["OTEL_RESOURCE_ATTRIBUTES"] = "service.instance.id={{- .Name -}}";
72+
}
73+
context.EnvironmentVariables["OTEL_SERVICE_NAME"] = "{{- index .Annotations \"" + CustomResource.OtelServiceNameAnnotation + "\" -}}";
6174

6275
if (configuration["AppHost:OtlpApiKey"] is { } otlpApiKey)
6376
{

0 commit comments

Comments
 (0)