From 15bacf0eee8aa01935079ac5220b524408385414 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Jul 2025 16:10:56 +0800 Subject: [PATCH 1/3] Improve span details percentage calculation --- .../Model/Otlp/SpanWaterfallViewModel.cs | 15 +++++++-- .../Model/SpanWaterfallViewModelTests.cs | 32 +++++++++++++++++++ .../Shared/Telemetry/TelemetryTestHelpers.cs | 23 +++++++++---- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/Aspire.Dashboard/Model/Otlp/SpanWaterfallViewModel.cs b/src/Aspire.Dashboard/Model/Otlp/SpanWaterfallViewModel.cs index 10fabcdb77a..2fbc725c7c7 100644 --- a/src/Aspire.Dashboard/Model/Otlp/SpanWaterfallViewModel.cs +++ b/src/Aspire.Dashboard/Model/Otlp/SpanWaterfallViewModel.cs @@ -140,8 +140,8 @@ static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, bool hid var relativeStart = span.StartTime - traceStart; var rootDuration = span.Trace.Duration.TotalMilliseconds; - var leftOffset = relativeStart.TotalMilliseconds / rootDuration * 100; - var width = span.Duration.TotalMilliseconds / rootDuration * 100; + var leftOffset = CalculatePercent(relativeStart.TotalMilliseconds, rootDuration); + var width = CalculatePercent(span.Duration.TotalMilliseconds, rootDuration); // Figure out if the label is displayed to the left or right of the span. // If the label position is based on whether more than half of the span is on the left or right side of the trace. @@ -163,7 +163,7 @@ static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, bool hid { Index = currentSpanLogIndex++, LogEntry = log, - LeftOffset = logRelativeStart.TotalMilliseconds / rootDuration * 100 + LeftOffset = CalculatePercent(logRelativeStart.TotalMilliseconds, rootDuration) }); } } @@ -192,6 +192,15 @@ static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, bool hid return viewModel; } + + static double CalculatePercent(double value, double total) + { + if (total == 0) + { + return 0; + } + return value / total * 100; + } } private static string? ResolveUninstrumentedPeerName(OtlpSpan span, IOutgoingPeerResolver[] outgoingPeerResolvers) diff --git a/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs b/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs index 34ede459094..e9ebd3a4bf6 100644 --- a/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs @@ -5,7 +5,9 @@ using Aspire.Dashboard.Model.Otlp; using Aspire.Dashboard.Otlp.Model; using Aspire.Tests.Shared.Telemetry; +using Google.Protobuf.Collections; using Microsoft.Extensions.Logging.Abstractions; +using OpenTelemetry.Proto.Common.V1; using Xunit; namespace Aspire.Dashboard.Tests.Model; @@ -42,6 +44,36 @@ public void Create_HasChildren_ChildrenPopulated() }); } + [Fact] + public void Create_RootSpanZeroDuration_ZeroPercentage() + { + // Arrange + var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() }; + var app1 = new OtlpApplication("app1", "instance", uninstrumentedPeer: false, context); + var app1View = new OtlpApplicationView(app1, new RepeatedField()); + + var date = new DateTime(2001, 1, 1, 1, 1, 2, DateTimeKind.Utc); + var trace = new OtlpTrace(new byte[] { 1, 2, 3 }); + var scope = TelemetryTestHelpers.CreateOtlpScope(context); + trace.AddSpan(TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: TelemetryTestHelpers.GetHexId("1"), parentSpanId: null, startDate: date, endDate: date)); + var log = TelemetryTestHelpers.CreateOtlpLogEntry(TelemetryTestHelpers.CreateLogRecord(traceId: trace.TraceId, spanId: TelemetryTestHelpers.GetHexId("1")), app1View, scope, context); + + // Act + var vm = SpanWaterfallViewModel.Create(trace, [log], new SpanWaterfallViewModel.TraceDetailState([], [])); + + // Assert + Assert.Collection(vm, + e => + { + Assert.Equal(TelemetryTestHelpers.GetHexId("1"), e.Span.SpanId); + Assert.Equal(0, e.LeftOffset); + Assert.Equal(0, e.Width); + + var spanLog = Assert.Single(e.SpanLogs); + Assert.Equal(0, spanLog.LeftOffset); + }); + } + [Fact] public void Create_OutgoingPeers_BrowserLink() { diff --git a/tests/Shared/Telemetry/TelemetryTestHelpers.cs b/tests/Shared/Telemetry/TelemetryTestHelpers.cs index 04846a40bea..be254a3fbae 100644 --- a/tests/Shared/Telemetry/TelemetryTestHelpers.cs +++ b/tests/Shared/Telemetry/TelemetryTestHelpers.cs @@ -50,6 +50,11 @@ public static OtlpScope CreateOtlpScope(OtlpContext context, string? name = null return new OtlpScope(scope.Name, scope.Version, scope.Attributes.ToKeyValuePairs(context)); } + public static OtlpLogEntry CreateOtlpLogEntry(LogRecord record, OtlpApplicationView app, OtlpScope scope, OtlpContext context) + { + return new OtlpLogEntry(record, app, scope, context); + } + public static InstrumentationScope CreateScope(string? name = null, IEnumerable>? attributes = null) { var scope = new InstrumentationScope() { Name = name ?? "TestScope" }; @@ -157,7 +162,7 @@ public static Span CreateSpan(string traceId, string spanId, DateTime startTime, { var span = new Span { - TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes(traceId)), + TraceId = ConvertHexToId(traceId), SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes(spanId)), ParentSpanId = parentSpanId is null ? ByteString.Empty : ByteString.CopyFrom(Encoding.UTF8.GetBytes(parentSpanId)), StartTimeUnixNano = DateTimeToUnixNanoseconds(startTime), @@ -184,15 +189,16 @@ public static Span CreateSpan(string traceId, string spanId, DateTime startTime, return span; } - public static LogRecord CreateLogRecord(DateTime? time = null, DateTime? observedTime = null, string? message = null, SeverityNumber? severity = null, IEnumerable>? attributes = null, bool? skipBody = null) + public static LogRecord CreateLogRecord(DateTime? time = null, DateTime? observedTime = null, string? message = null, SeverityNumber? severity = null, IEnumerable>? attributes = null, + bool? skipBody = null, string? traceId = null, string? spanId = null) { attributes ??= [new KeyValuePair("{OriginalFormat}", "Test {Log}"), new KeyValuePair("Log", "Value!")]; var logRecord = new LogRecord { Body = (skipBody ?? false) ? null : new AnyValue { StringValue = message ?? "Test Value!" }, - TraceId = ByteString.CopyFrom(Convert.FromHexString("5465737454726163654964")), - SpanId = ByteString.CopyFrom(Convert.FromHexString("546573745370616e4964")), + TraceId = (traceId != null) ? ConvertHexToId(traceId) : ByteString.CopyFrom(Convert.FromHexString("5465737454726163654964")), + SpanId = (spanId != null) ? ConvertHexToId(spanId) : ByteString.CopyFrom(Convert.FromHexString("546573745370616e4964")), TimeUnixNano = time != null ? DateTimeToUnixNanoseconds(time.Value) : 1000, ObservedTimeUnixNano = observedTime != null ? DateTimeToUnixNanoseconds(observedTime.Value) : 1000, SeverityNumber = severity ?? SeverityNumber.Info @@ -303,13 +309,13 @@ public static OtlpContext CreateContext(TelemetryLimitOptions? options = null, I public static OtlpSpan CreateOtlpSpan(OtlpApplication app, OtlpTrace trace, OtlpScope scope, string spanId, string? parentSpanId, DateTime startDate, KeyValuePair[]? attributes = null, OtlpSpanStatusCode? statusCode = null, string? statusMessage = null, OtlpSpanKind kind = OtlpSpanKind.Unspecified, - OtlpApplication? uninstrumentedPeer = null) + OtlpApplication? uninstrumentedPeer = null, DateTime? endDate = null) { return new OtlpSpan(app.GetView([]), trace, scope) { Attributes = attributes ?? [], BackLinks = [], - EndTime = DateTime.MaxValue, + EndTime = endDate ?? DateTime.MaxValue, Events = [], Kind = kind, Links = [], @@ -339,4 +345,9 @@ public static X509Certificate2 GenerateDummyCertificate() return new X509Certificate2(certificate.Export(X509ContentType.Pfx)); } + + private static ByteString ConvertHexToId(string hexString) + { + return ByteString.CopyFrom(Convert.FromHexString(hexString)); + } } From ea5f15cdb71b3d1a4d3eda131f6f463733583f53 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Jul 2025 16:15:54 +0800 Subject: [PATCH 2/3] Update --- .../Model/SpanWaterfallViewModelTests.cs | 2 +- tests/Shared/Telemetry/TelemetryTestHelpers.cs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs b/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs index e9ebd3a4bf6..43529ee8b70 100644 --- a/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs @@ -56,7 +56,7 @@ public void Create_RootSpanZeroDuration_ZeroPercentage() var trace = new OtlpTrace(new byte[] { 1, 2, 3 }); var scope = TelemetryTestHelpers.CreateOtlpScope(context); trace.AddSpan(TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: TelemetryTestHelpers.GetHexId("1"), parentSpanId: null, startDate: date, endDate: date)); - var log = TelemetryTestHelpers.CreateOtlpLogEntry(TelemetryTestHelpers.CreateLogRecord(traceId: trace.TraceId, spanId: TelemetryTestHelpers.GetHexId("1")), app1View, scope, context); + var log = new OtlpLogEntry(TelemetryTestHelpers.CreateLogRecord(traceId: trace.TraceId, spanId: TelemetryTestHelpers.GetHexId("1")), app1View, scope, context); // Act var vm = SpanWaterfallViewModel.Create(trace, [log], new SpanWaterfallViewModel.TraceDetailState([], [])); diff --git a/tests/Shared/Telemetry/TelemetryTestHelpers.cs b/tests/Shared/Telemetry/TelemetryTestHelpers.cs index be254a3fbae..2a877796bf8 100644 --- a/tests/Shared/Telemetry/TelemetryTestHelpers.cs +++ b/tests/Shared/Telemetry/TelemetryTestHelpers.cs @@ -50,11 +50,6 @@ public static OtlpScope CreateOtlpScope(OtlpContext context, string? name = null return new OtlpScope(scope.Name, scope.Version, scope.Attributes.ToKeyValuePairs(context)); } - public static OtlpLogEntry CreateOtlpLogEntry(LogRecord record, OtlpApplicationView app, OtlpScope scope, OtlpContext context) - { - return new OtlpLogEntry(record, app, scope, context); - } - public static InstrumentationScope CreateScope(string? name = null, IEnumerable>? attributes = null) { var scope = new InstrumentationScope() { Name = name ?? "TestScope" }; From b548cbc6676ef102bc65d86a75bab2b0d373375a Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Jul 2025 16:56:53 +0800 Subject: [PATCH 3/3] Fix tests --- .../Model/SpanWaterfallViewModelTests.cs | 6 +++--- tests/Shared/Telemetry/TelemetryTestHelpers.cs | 11 +++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs b/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs index 43529ee8b70..a31f3928331 100644 --- a/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/SpanWaterfallViewModelTests.cs @@ -55,8 +55,8 @@ public void Create_RootSpanZeroDuration_ZeroPercentage() var date = new DateTime(2001, 1, 1, 1, 1, 2, DateTimeKind.Utc); var trace = new OtlpTrace(new byte[] { 1, 2, 3 }); var scope = TelemetryTestHelpers.CreateOtlpScope(context); - trace.AddSpan(TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: TelemetryTestHelpers.GetHexId("1"), parentSpanId: null, startDate: date, endDate: date)); - var log = new OtlpLogEntry(TelemetryTestHelpers.CreateLogRecord(traceId: trace.TraceId, spanId: TelemetryTestHelpers.GetHexId("1")), app1View, scope, context); + trace.AddSpan(TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: "31", parentSpanId: null, startDate: date, endDate: date)); + var log = new OtlpLogEntry(TelemetryTestHelpers.CreateLogRecord(traceId: trace.TraceId, spanId: "1"), app1View, scope, context); // Act var vm = SpanWaterfallViewModel.Create(trace, [log], new SpanWaterfallViewModel.TraceDetailState([], [])); @@ -65,7 +65,7 @@ public void Create_RootSpanZeroDuration_ZeroPercentage() Assert.Collection(vm, e => { - Assert.Equal(TelemetryTestHelpers.GetHexId("1"), e.Span.SpanId); + Assert.Equal("31", e.Span.SpanId); Assert.Equal(0, e.LeftOffset); Assert.Equal(0, e.Width); diff --git a/tests/Shared/Telemetry/TelemetryTestHelpers.cs b/tests/Shared/Telemetry/TelemetryTestHelpers.cs index 2a877796bf8..fe85b03c702 100644 --- a/tests/Shared/Telemetry/TelemetryTestHelpers.cs +++ b/tests/Shared/Telemetry/TelemetryTestHelpers.cs @@ -157,7 +157,7 @@ public static Span CreateSpan(string traceId, string spanId, DateTime startTime, { var span = new Span { - TraceId = ConvertHexToId(traceId), + TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes(traceId)), SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes(spanId)), ParentSpanId = parentSpanId is null ? ByteString.Empty : ByteString.CopyFrom(Encoding.UTF8.GetBytes(parentSpanId)), StartTimeUnixNano = DateTimeToUnixNanoseconds(startTime), @@ -192,8 +192,8 @@ public static LogRecord CreateLogRecord(DateTime? time = null, DateTime? observe var logRecord = new LogRecord { Body = (skipBody ?? false) ? null : new AnyValue { StringValue = message ?? "Test Value!" }, - TraceId = (traceId != null) ? ConvertHexToId(traceId) : ByteString.CopyFrom(Convert.FromHexString("5465737454726163654964")), - SpanId = (spanId != null) ? ConvertHexToId(spanId) : ByteString.CopyFrom(Convert.FromHexString("546573745370616e4964")), + TraceId = (traceId != null) ? ByteString.CopyFrom(Encoding.UTF8.GetBytes(traceId)) : ByteString.CopyFrom(Convert.FromHexString("5465737454726163654964")), + SpanId = (spanId != null) ? ByteString.CopyFrom(Encoding.UTF8.GetBytes(spanId)) : ByteString.CopyFrom(Convert.FromHexString("546573745370616e4964")), TimeUnixNano = time != null ? DateTimeToUnixNanoseconds(time.Value) : 1000, ObservedTimeUnixNano = observedTime != null ? DateTimeToUnixNanoseconds(observedTime.Value) : 1000, SeverityNumber = severity ?? SeverityNumber.Info @@ -340,9 +340,4 @@ public static X509Certificate2 GenerateDummyCertificate() return new X509Certificate2(certificate.Export(X509ContentType.Pfx)); } - - private static ByteString ConvertHexToId(string hexString) - { - return ByteString.CopyFrom(Convert.FromHexString(hexString)); - } }