Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Commands/ExportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ private static async Task AddStructuredLogsAsync(
IReadOnlyList<IOtlpResource> allOtlpResources,
CancellationToken cancellationToken)
{
var url = DashboardUrls.TelemetryLogsApiUrl(baseUrl, resolvedResources);
var url = DashboardUrls.TelemetryLogsApiUrl(baseUrl, resolvedResources, limit: TelemetryCommandHelpers.MaxTelemetryLimit);
var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();

Expand Down Expand Up @@ -290,7 +290,7 @@ private static async Task AddTracesAsync(
IReadOnlyList<IOtlpResource> allOtlpResources,
CancellationToken cancellationToken)
{
var url = DashboardUrls.TelemetryTracesApiUrl(baseUrl, resolvedResources);
var url = DashboardUrls.TelemetryTracesApiUrl(baseUrl, resolvedResources, limit: TelemetryCommandHelpers.MaxTelemetryLimit);
var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();

Expand Down
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ internal static class TelemetryCommandHelpers
/// </summary>
internal const string ApiKeyHeaderName = "X-API-Key";

/// <summary>
/// Limit passed to dashboard telemetry APIs. All data is fetched in one API call
/// so there shouldn't be a limit on data returned.
/// </summary>
internal const int MaxTelemetryLimit = int.MaxValue;

#region Shared Command Options

/// <summary>
Expand Down
18 changes: 3 additions & 15 deletions src/Aspire.Cli/Commands/TelemetryLogsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,10 @@ private async Task<int> FetchLogsAsync(
return ExitCodeConstants.InvalidCommand;
}

// Build query string with multiple resource parameters
var additionalParams = new List<(string key, string? value)>
{
("traceId", traceId),
("severity", severity)
};
if (limit.HasValue && !follow)
{
additionalParams.Add(("limit", limit.Value.ToString(CultureInfo.InvariantCulture)));
}
if (follow)
{
additionalParams.Add(("follow", "true"));
}
// Build URL with query parameters
int? effectiveLimit = (limit.HasValue && !follow) ? limit.Value : null;

var url = DashboardUrls.TelemetryLogsApiUrl(baseUrl, resolvedResources, [.. additionalParams]);
var url = DashboardUrls.TelemetryLogsApiUrl(baseUrl, resolvedResources, traceId: traceId, severity: severity, limit: effectiveLimit, follow: follow ? true : null);

try
{
Expand Down
21 changes: 3 additions & 18 deletions src/Aspire.Cli/Commands/TelemetrySpansCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,25 +125,10 @@ private async Task<int> FetchSpansAsync(
return ExitCodeConstants.InvalidCommand;
}

// Build query string with multiple resource parameters
var additionalParams = new List<(string key, string? value)>
{
("traceId", traceId)
};
if (hasError.HasValue)
{
additionalParams.Add(("hasError", hasError.Value.ToString().ToLowerInvariant()));
}
if (limit.HasValue && !follow)
{
additionalParams.Add(("limit", limit.Value.ToString(CultureInfo.InvariantCulture)));
}
if (follow)
{
additionalParams.Add(("follow", "true"));
}
// Build URL with query parameters
int? effectiveLimit = (limit.HasValue && !follow) ? limit.Value : null;

var url = DashboardUrls.TelemetrySpansApiUrl(baseUrl, resolvedResources, [.. additionalParams]);
var url = DashboardUrls.TelemetrySpansApiUrl(baseUrl, resolvedResources, traceId: traceId, hasError: hasError, limit: effectiveLimit, follow: follow ? true : null);

_logger.LogDebug("Fetching spans from {Url}", url);

Expand Down
13 changes: 1 addition & 12 deletions src/Aspire.Cli/Commands/TelemetryTracesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,7 @@ private async Task<int> FetchTracesAsync(
// Pre-resolve colors so assignment is deterministic regardless of data order
TelemetryCommandHelpers.ResolveResourceColors(_resourceColorMap, allOtlpResources);

// Build query string with multiple resource parameters
var additionalParams = new List<(string key, string? value)>();
if (hasError.HasValue)
{
additionalParams.Add(("hasError", hasError.Value.ToString().ToLowerInvariant()));
}
if (limit.HasValue)
{
additionalParams.Add(("limit", limit.Value.ToString(CultureInfo.InvariantCulture)));
}

var url = DashboardUrls.TelemetryTracesApiUrl(baseUrl, resolvedResources, [.. additionalParams]);
var url = DashboardUrls.TelemetryTracesApiUrl(baseUrl, resolvedResources, hasError: hasError, limit: limit);

_logger.LogDebug("Fetching traces from {Url}", url);

Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Mcp/Tools/ListStructuredLogsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public override async ValueTask<CallToolResult> CallToolAsync(CallToolContext co
};
}

var url = DashboardUrls.TelemetryLogsApiUrl(apiBaseUrl, resolvedResources);
// Fetch all logs from the API. Limiting of returned telemetry to the MCP caller happens later.
var url = DashboardUrls.TelemetryLogsApiUrl(apiBaseUrl, resolvedResources, limit: TelemetryCommandHelpers.MaxTelemetryLimit);

logger.LogDebug("Fetching structured logs from {Url}", url);

Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Mcp/Tools/ListTraceStructuredLogsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public override async ValueTask<CallToolResult> CallToolAsync(CallToolContext co
var resources = await TelemetryCommandHelpers.GetAllResourcesAsync(client, apiBaseUrl, cancellationToken).ConfigureAwait(false);

// Build the logs API URL with traceId filter
var url = DashboardUrls.TelemetryLogsApiUrl(apiBaseUrl, resources: null, ("traceId", traceId));
// Fetch all logs for the trace from the API. Limiting of returned telemetry to the MCP caller happens later.
var url = DashboardUrls.TelemetryLogsApiUrl(apiBaseUrl, traceId: traceId, limit: TelemetryCommandHelpers.MaxTelemetryLimit);

logger.LogDebug("Fetching structured logs from {Url}", url);

Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Mcp/Tools/ListTracesTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public override async ValueTask<CallToolResult> CallToolAsync(CallToolContext co
};
}

var url = DashboardUrls.TelemetryTracesApiUrl(apiBaseUrl, resolvedResources);
// Fetch all traces from the API. Limiting of returned telemetry to the MCP caller happens later.
var url = DashboardUrls.TelemetryTracesApiUrl(apiBaseUrl, resolvedResources, limit: TelemetryCommandHelpers.MaxTelemetryLimit);

logger.LogDebug("Fetching traces from {Url}", url);

Expand Down
7 changes: 3 additions & 4 deletions src/Aspire.Dashboard/Api/TelemetryApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ internal sealed class TelemetryApiService(
{
private const int DefaultLimit = 200;
private const int DefaultTraceLimit = 100;
private const int MaxQueryCount = 10000;

private readonly IOutgoingPeerResolver[] _outgoingPeerResolvers = outgoingPeerResolvers.ToArray();

Expand Down Expand Up @@ -50,7 +49,7 @@ internal sealed class TelemetryApiService(
{
ResourceKey = resourceKey,
StartIndex = 0,
Count = MaxQueryCount,
Count = int.MaxValue,
Filters = [],
FilterText = string.Empty
});
Expand Down Expand Up @@ -121,7 +120,7 @@ internal sealed class TelemetryApiService(
{
ResourceKey = resourceKey,
StartIndex = 0,
Count = MaxQueryCount,
Count = int.MaxValue,
Filters = [],
FilterText = string.Empty
});
Expand Down Expand Up @@ -237,7 +236,7 @@ internal sealed class TelemetryApiService(
{
ResourceKey = resourceKey,
StartIndex = 0,
Count = MaxQueryCount,
Count = int.MaxValue,
Filters = filters
});
allLogs.AddRange(result.Items);
Expand Down
103 changes: 69 additions & 34 deletions src/Shared/DashboardUrls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,42 +193,92 @@ public static string CombineUrl(string baseUrl, string path)
private const string TelemetryApiBasePath = "api/telemetry";

/// <summary>
/// Builds the URL for the telemetry logs API with resource filtering.
/// Builds the URL for the telemetry logs API.
/// </summary>
/// <param name="baseUrl">The dashboard base URL.</param>
/// <param name="resources">Optional list of resource names to filter by.</param>
/// <param name="additionalParams">Additional query parameters.</param>
/// <param name="traceId">Optional trace ID to filter logs by.</param>
/// <param name="severity">Optional minimum severity level filter.</param>
/// <param name="limit">Optional maximum number of results to return.</param>
/// <param name="follow">Optional flag to enable streaming mode.</param>
/// <returns>The full API URL.</returns>
public static string TelemetryLogsApiUrl(string baseUrl, List<string>? resources = null, params (string key, string? value)[] additionalParams)
public static string TelemetryLogsApiUrl(string baseUrl, List<string>? resources = null, string? traceId = null, string? severity = null, int? limit = null, bool? follow = null)
{
var queryString = BuildResourceQueryString(resources, additionalParams);
return CombineUrl(baseUrl, $"{TelemetryApiBasePath}/logs{queryString}");
var url = $"/{TelemetryApiBasePath}/logs";
url = AddResourceParams(url, resources);
if (traceId is not null)
{
url = AddQueryString(url, "traceId", traceId);
}
if (severity is not null)
{
url = AddQueryString(url, "severity", severity);
}
if (limit is not null)
{
url = AddQueryString(url, "limit", limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (follow == true)
{
url = AddQueryString(url, "follow", "true");
}
return CombineUrl(baseUrl, url);
}

/// <summary>
/// Builds the URL for the telemetry spans API with resource filtering.
/// Builds the URL for the telemetry spans API.
/// </summary>
/// <param name="baseUrl">The dashboard base URL.</param>
/// <param name="resources">Optional list of resource names to filter by.</param>
/// <param name="additionalParams">Additional query parameters.</param>
/// <param name="traceId">Optional trace ID to filter spans by.</param>
/// <param name="hasError">Optional filter for error status.</param>
/// <param name="limit">Optional maximum number of results to return.</param>
/// <param name="follow">Optional flag to enable streaming mode.</param>
/// <returns>The full API URL.</returns>
public static string TelemetrySpansApiUrl(string baseUrl, List<string>? resources = null, params (string key, string? value)[] additionalParams)
public static string TelemetrySpansApiUrl(string baseUrl, List<string>? resources = null, string? traceId = null, bool? hasError = null, int? limit = null, bool? follow = null)
{
var queryString = BuildResourceQueryString(resources, additionalParams);
return CombineUrl(baseUrl, $"{TelemetryApiBasePath}/spans{queryString}");
var url = $"/{TelemetryApiBasePath}/spans";
url = AddResourceParams(url, resources);
if (traceId is not null)
{
url = AddQueryString(url, "traceId", traceId);
}
if (hasError is not null)
{
url = AddQueryString(url, "hasError", hasError.Value.ToString().ToLowerInvariant());
}
if (limit is not null)
{
url = AddQueryString(url, "limit", limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (follow == true)
{
url = AddQueryString(url, "follow", "true");
}
return CombineUrl(baseUrl, url);
}

/// <summary>
/// Builds the URL for the telemetry traces API with resource filtering.
/// Builds the URL for the telemetry traces API.
/// </summary>
/// <param name="baseUrl">The dashboard base URL.</param>
/// <param name="resources">Optional list of resource names to filter by.</param>
/// <param name="additionalParams">Additional query parameters.</param>
/// <param name="hasError">Optional filter for error status.</param>
/// <param name="limit">Optional maximum number of results to return.</param>
/// <returns>The full API URL.</returns>
public static string TelemetryTracesApiUrl(string baseUrl, List<string>? resources = null, params (string key, string? value)[] additionalParams)
public static string TelemetryTracesApiUrl(string baseUrl, List<string>? resources = null, bool? hasError = null, int? limit = null)
{
var queryString = BuildResourceQueryString(resources, additionalParams);
return CombineUrl(baseUrl, $"{TelemetryApiBasePath}/traces{queryString}");
var url = $"/{TelemetryApiBasePath}/traces";
url = AddResourceParams(url, resources);
if (hasError is not null)
{
url = AddQueryString(url, "hasError", hasError.Value.ToString().ToLowerInvariant());
}
if (limit is not null)
{
url = AddQueryString(url, "limit", limit.Value.ToString(CultureInfo.InvariantCulture));
}
return CombineUrl(baseUrl, url);
}

/// <summary>
Expand All @@ -255,33 +305,18 @@ public static string TelemetryResourcesApiUrl(string baseUrl)
}

/// <summary>
/// Builds a query string with multiple resource parameters and optional additional parameters.
/// Appends multiple resource query parameters to a URL.
/// </summary>
internal static string BuildResourceQueryString(
List<string>? resources,
params (string key, string? value)[] additionalParams)
private static string AddResourceParams(string url, List<string>? resources)
{
var parts = new List<string>();

// Add each resource as a separate query parameter
if (resources is not null)
{
foreach (var resource in resources)
{
parts.Add($"resource={Uri.EscapeDataString(resource)}");
url = AddQueryString(url, "resource", resource);
}
}

// Add additional parameters
foreach (var (key, value) in additionalParams)
{
if (!string.IsNullOrEmpty(value))
{
parts.Add($"{key}={Uri.EscapeDataString(value)}");
}
}

return parts.Count > 0 ? "?" + string.Join("&", parts) : "";
return url;
}

#endregion
Expand Down
42 changes: 18 additions & 24 deletions tests/Aspire.Cli.Tests/Commands/TelemetryCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,51 +29,45 @@ public async Task TelemetryCommand_WithoutSubcommand_ReturnsInvalidCommand()
}

[Fact]
public void BuildResourceQueryString_WithNoResources_ReturnsEmptyString()
public void TelemetryLogsApiUrl_WithNoParams_ReturnsBaseUrl()
{
var result = DashboardUrls.BuildResourceQueryString(null);
Assert.Equal("", result);
var result = DashboardUrls.TelemetryLogsApiUrl("https://localhost:5000");
Assert.Equal("https://localhost:5000/api/telemetry/logs", result);
}

[Fact]
public void BuildResourceQueryString_WithSingleResource_ReturnsCorrectQueryString()
public void TelemetryLogsApiUrl_WithSingleResource_ReturnsCorrectUrl()
{
var result = DashboardUrls.BuildResourceQueryString(["frontend"]);
Assert.Equal("?resource=frontend", result);
var result = DashboardUrls.TelemetryLogsApiUrl("https://localhost:5000", ["frontend"]);
Assert.Equal("https://localhost:5000/api/telemetry/logs?resource=frontend", result);
}

[Fact]
public void BuildResourceQueryString_WithMultipleResources_ReturnsAllResourceParams()
public void TelemetryLogsApiUrl_WithMultipleResources_ReturnsAllResourceParams()
{
var result = DashboardUrls.BuildResourceQueryString(["frontend-abc123", "frontend-xyz789"]);
Assert.Equal("?resource=frontend-abc123&resource=frontend-xyz789", result);
var result = DashboardUrls.TelemetryLogsApiUrl("https://localhost:5000", ["frontend-abc123", "frontend-xyz789"]);
Assert.Equal("https://localhost:5000/api/telemetry/logs?resource=frontend-abc123&resource=frontend-xyz789", result);
}

[Fact]
public void BuildResourceQueryString_WithResourcesAndAdditionalParams_CombinesCorrectly()
public void TelemetryLogsApiUrl_WithAllParams_CombinesCorrectly()
{
var result = DashboardUrls.BuildResourceQueryString(
["frontend"],
("traceId", "abc123"),
("limit", "10"));
Assert.Equal("?resource=frontend&traceId=abc123&limit=10", result);
var result = DashboardUrls.TelemetryLogsApiUrl("https://localhost:5000", ["frontend"], traceId: "abc123", severity: "Error", limit: 10, follow: true);
Assert.Equal("https://localhost:5000/api/telemetry/logs?resource=frontend&traceId=abc123&severity=Error&limit=10&follow=true", result);
}

[Fact]
public void BuildResourceQueryString_WithNullAdditionalParams_SkipsNullValues()
public void TelemetryLogsApiUrl_WithNullParams_SkipsNullValues()
{
var result = DashboardUrls.BuildResourceQueryString(
["frontend"],
("traceId", null),
("limit", "10"));
Assert.Equal("?resource=frontend&limit=10", result);
var result = DashboardUrls.TelemetryLogsApiUrl("https://localhost:5000", ["frontend"], traceId: null, limit: 10);
Assert.Equal("https://localhost:5000/api/telemetry/logs?resource=frontend&limit=10", result);
}

[Fact]
public void BuildResourceQueryString_WithSpecialCharacters_EncodesCorrectly()
public void TelemetryLogsApiUrl_WithSpecialCharacters_EncodesCorrectly()
{
var result = DashboardUrls.BuildResourceQueryString(["service with spaces"]);
Assert.Equal("?resource=service%20with%20spaces", result);
var result = DashboardUrls.TelemetryLogsApiUrl("https://localhost:5000", ["service with spaces"]);
Assert.Equal("https://localhost:5000/api/telemetry/logs?resource=service%20with%20spaces", result);
}

[Fact]
Expand Down
Loading
Loading