diff --git a/src/Aspire.Cli/Commands/ExportCommand.cs b/src/Aspire.Cli/Commands/ExportCommand.cs index 44dc69d4c22..1ac612654a3 100644 --- a/src/Aspire.Cli/Commands/ExportCommand.cs +++ b/src/Aspire.Cli/Commands/ExportCommand.cs @@ -259,7 +259,7 @@ private static async Task AddStructuredLogsAsync( IReadOnlyList 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(); @@ -290,7 +290,7 @@ private static async Task AddTracesAsync( IReadOnlyList 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(); diff --git a/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs b/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs index b327cd2476a..a6e41c0b12c 100644 --- a/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs +++ b/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs @@ -26,6 +26,12 @@ internal static class TelemetryCommandHelpers /// internal const string ApiKeyHeaderName = "X-API-Key"; + /// + /// Limit passed to dashboard telemetry APIs. All data is fetched in one API call + /// so there shouldn't be a limit on data returned. + /// + internal const int MaxTelemetryLimit = int.MaxValue; + #region Shared Command Options /// diff --git a/src/Aspire.Cli/Commands/TelemetryLogsCommand.cs b/src/Aspire.Cli/Commands/TelemetryLogsCommand.cs index d07af446a8e..70d768ae7da 100644 --- a/src/Aspire.Cli/Commands/TelemetryLogsCommand.cs +++ b/src/Aspire.Cli/Commands/TelemetryLogsCommand.cs @@ -129,22 +129,10 @@ private async Task 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 { diff --git a/src/Aspire.Cli/Commands/TelemetrySpansCommand.cs b/src/Aspire.Cli/Commands/TelemetrySpansCommand.cs index 7c06a63ede3..84ff7bdb122 100644 --- a/src/Aspire.Cli/Commands/TelemetrySpansCommand.cs +++ b/src/Aspire.Cli/Commands/TelemetrySpansCommand.cs @@ -125,25 +125,10 @@ private async Task 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); diff --git a/src/Aspire.Cli/Commands/TelemetryTracesCommand.cs b/src/Aspire.Cli/Commands/TelemetryTracesCommand.cs index b97018fd7e5..3159c54be22 100644 --- a/src/Aspire.Cli/Commands/TelemetryTracesCommand.cs +++ b/src/Aspire.Cli/Commands/TelemetryTracesCommand.cs @@ -188,18 +188,7 @@ private async Task 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); diff --git a/src/Aspire.Cli/Mcp/Tools/ListStructuredLogsTool.cs b/src/Aspire.Cli/Mcp/Tools/ListStructuredLogsTool.cs index 4df3c286b4d..fb6bb671980 100644 --- a/src/Aspire.Cli/Mcp/Tools/ListStructuredLogsTool.cs +++ b/src/Aspire.Cli/Mcp/Tools/ListStructuredLogsTool.cs @@ -70,7 +70,8 @@ public override async ValueTask 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); diff --git a/src/Aspire.Cli/Mcp/Tools/ListTraceStructuredLogsTool.cs b/src/Aspire.Cli/Mcp/Tools/ListTraceStructuredLogsTool.cs index 87320996e7f..f7543ec2adb 100644 --- a/src/Aspire.Cli/Mcp/Tools/ListTraceStructuredLogsTool.cs +++ b/src/Aspire.Cli/Mcp/Tools/ListTraceStructuredLogsTool.cs @@ -70,7 +70,8 @@ public override async ValueTask 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); diff --git a/src/Aspire.Cli/Mcp/Tools/ListTracesTool.cs b/src/Aspire.Cli/Mcp/Tools/ListTracesTool.cs index 187b8479a7f..bdb96f204bf 100644 --- a/src/Aspire.Cli/Mcp/Tools/ListTracesTool.cs +++ b/src/Aspire.Cli/Mcp/Tools/ListTracesTool.cs @@ -70,7 +70,8 @@ public override async ValueTask 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); diff --git a/src/Aspire.Dashboard/Api/TelemetryApiService.cs b/src/Aspire.Dashboard/Api/TelemetryApiService.cs index 80617479ca7..76df9a7317a 100644 --- a/src/Aspire.Dashboard/Api/TelemetryApiService.cs +++ b/src/Aspire.Dashboard/Api/TelemetryApiService.cs @@ -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(); @@ -50,7 +49,7 @@ internal sealed class TelemetryApiService( { ResourceKey = resourceKey, StartIndex = 0, - Count = MaxQueryCount, + Count = int.MaxValue, Filters = [], FilterText = string.Empty }); @@ -121,7 +120,7 @@ internal sealed class TelemetryApiService( { ResourceKey = resourceKey, StartIndex = 0, - Count = MaxQueryCount, + Count = int.MaxValue, Filters = [], FilterText = string.Empty }); @@ -237,7 +236,7 @@ internal sealed class TelemetryApiService( { ResourceKey = resourceKey, StartIndex = 0, - Count = MaxQueryCount, + Count = int.MaxValue, Filters = filters }); allLogs.AddRange(result.Items); diff --git a/src/Shared/DashboardUrls.cs b/src/Shared/DashboardUrls.cs index 597f9f25b3b..0437cfcdcc6 100644 --- a/src/Shared/DashboardUrls.cs +++ b/src/Shared/DashboardUrls.cs @@ -193,42 +193,92 @@ public static string CombineUrl(string baseUrl, string path) private const string TelemetryApiBasePath = "api/telemetry"; /// - /// Builds the URL for the telemetry logs API with resource filtering. + /// Builds the URL for the telemetry logs API. /// /// The dashboard base URL. /// Optional list of resource names to filter by. - /// Additional query parameters. + /// Optional trace ID to filter logs by. + /// Optional minimum severity level filter. + /// Optional maximum number of results to return. + /// Optional flag to enable streaming mode. /// The full API URL. - public static string TelemetryLogsApiUrl(string baseUrl, List? resources = null, params (string key, string? value)[] additionalParams) + public static string TelemetryLogsApiUrl(string baseUrl, List? 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); } /// - /// Builds the URL for the telemetry spans API with resource filtering. + /// Builds the URL for the telemetry spans API. /// /// The dashboard base URL. /// Optional list of resource names to filter by. - /// Additional query parameters. + /// Optional trace ID to filter spans by. + /// Optional filter for error status. + /// Optional maximum number of results to return. + /// Optional flag to enable streaming mode. /// The full API URL. - public static string TelemetrySpansApiUrl(string baseUrl, List? resources = null, params (string key, string? value)[] additionalParams) + public static string TelemetrySpansApiUrl(string baseUrl, List? 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); } /// - /// Builds the URL for the telemetry traces API with resource filtering. + /// Builds the URL for the telemetry traces API. /// /// The dashboard base URL. /// Optional list of resource names to filter by. - /// Additional query parameters. + /// Optional filter for error status. + /// Optional maximum number of results to return. /// The full API URL. - public static string TelemetryTracesApiUrl(string baseUrl, List? resources = null, params (string key, string? value)[] additionalParams) + public static string TelemetryTracesApiUrl(string baseUrl, List? 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); } /// @@ -255,33 +305,18 @@ public static string TelemetryResourcesApiUrl(string baseUrl) } /// - /// Builds a query string with multiple resource parameters and optional additional parameters. + /// Appends multiple resource query parameters to a URL. /// - internal static string BuildResourceQueryString( - List? resources, - params (string key, string? value)[] additionalParams) + private static string AddResourceParams(string url, List? resources) { - var parts = new List(); - - // 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 diff --git a/tests/Aspire.Cli.Tests/Commands/TelemetryCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/TelemetryCommandTests.cs index 205619fc810..5411b9ded3b 100644 --- a/tests/Aspire.Cli.Tests/Commands/TelemetryCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/TelemetryCommandTests.cs @@ -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] diff --git a/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs b/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs index 15d6995796c..adf88d43af7 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs @@ -238,7 +238,7 @@ public async Task GetSpans_WithQueryParameters_Returns200() using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}"); // Act - test query parameters without resource filter (no resources exist in test) - var response = await httpClient.GetAsync("/api/telemetry/spans?hasError=true&limit=50").DefaultTimeout(); + var response = await httpClient.GetAsync($"/api/telemetry/spans?hasError=true&limit={int.MaxValue}").DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -278,7 +278,7 @@ public async Task GetLogs_WithQueryParameters_Returns200() using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}"); // Act - test query parameters without resource filter (no resources exist in test) - var response = await httpClient.GetAsync("/api/telemetry/logs?severity=Error&limit=50").DefaultTimeout(); + var response = await httpClient.GetAsync($"/api/telemetry/logs?severity=Error&limit={int.MaxValue}").DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/tests/Aspire.Dashboard.Tests/TelemetryApiServiceTests.cs b/tests/Aspire.Dashboard.Tests/TelemetryApiServiceTests.cs index 8bd11576896..4e3138be183 100644 --- a/tests/Aspire.Dashboard.Tests/TelemetryApiServiceTests.cs +++ b/tests/Aspire.Dashboard.Tests/TelemetryApiServiceTests.cs @@ -453,6 +453,164 @@ public void GetTrace_VariousTraceIds_ReturnsExpectedResult(string lookupId, bool } } + [Fact] + public void GetSpans_WithLimit_ReturnsMostRecentSpans() + { + var repository = CreateRepository(); + + repository.AddTraces(new AddContext(), new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: "service1", instanceId: "inst1"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = + { + CreateSpan(traceId: "trace1", spanId: "old-span", startTime: s_testTime, endTime: s_testTime.AddMinutes(1)), + CreateSpan(traceId: "trace2", spanId: "mid-span", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3)), + CreateSpan(traceId: "trace3", spanId: "new-span", startTime: s_testTime.AddMinutes(4), endTime: s_testTime.AddMinutes(5)) + } + } + } + } + }); + + var service = CreateService(repository); + + var result = service.GetSpans(resourceNames: null, traceId: null, hasError: null, limit: 2); + + Assert.NotNull(result); + Assert.Equal(3, result.TotalCount); + Assert.Equal(2, result.ReturnedCount); + + var json = System.Text.Json.JsonSerializer.Serialize(result.Data); + Assert.DoesNotContain("old-span", json); + Assert.Contains("mid-span", json); + Assert.Contains("new-span", json); + } + + [Fact] + public void GetTraces_WithLimit_ReturnsMostRecentTraces() + { + var repository = CreateRepository(); + + for (var i = 1; i <= 3; i++) + { + repository.AddTraces(new AddContext(), new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: "service1", instanceId: "inst1"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = + { + CreateSpan(traceId: $"trace{i}", spanId: $"span{i}", startTime: s_testTime.AddMinutes(i * 10), endTime: s_testTime.AddMinutes(i * 10 + 1)) + } + } + } + } + }); + } + + var service = CreateService(repository); + + var result = service.GetTraces(resourceNames: null, hasError: null, limit: 2); + + Assert.NotNull(result); + Assert.Equal(3, result.TotalCount); + Assert.Equal(2, result.ReturnedCount); + + var json = System.Text.Json.JsonSerializer.Serialize(result.Data); + Assert.DoesNotContain("span1", json); + Assert.Contains("span2", json); + Assert.Contains("span3", json); + } + + [Fact] + public void GetLogs_WithLimit_ReturnsMostRecentLogs() + { + var repository = CreateRepository(); + + repository.AddLogs(new AddContext(), new RepeatedField + { + new ResourceLogs + { + Resource = CreateResource(name: "service1", instanceId: "inst1"), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope("TestLogger"), + LogRecords = + { + CreateLogRecord(time: s_testTime, message: "old-log", severity: SeverityNumber.Info), + CreateLogRecord(time: s_testTime.AddMinutes(1), message: "mid-log", severity: SeverityNumber.Info), + CreateLogRecord(time: s_testTime.AddMinutes(2), message: "new-log", severity: SeverityNumber.Info) + } + } + } + } + }); + + var service = CreateService(repository); + + var result = service.GetLogs(resourceNames: null, traceId: null, severity: null, limit: 2); + + Assert.NotNull(result); + Assert.Equal(3, result.TotalCount); + Assert.Equal(2, result.ReturnedCount); + + var json = System.Text.Json.JsonSerializer.Serialize(result.Data); + Assert.DoesNotContain("old-log", json); + Assert.Contains("mid-log", json); + Assert.Contains("new-log", json); + } + + [Fact] + public void GetLogs_LargeLimit_ReturnsAllLogs() + { + const int totalLogs = 20_000; + var repository = CreateRepository(maxLogCount: totalLogs); + + var logRecords = new RepeatedField(); + for (var i = 0; i < totalLogs; i++) + { + logRecords.Add(CreateLogRecord(time: s_testTime.AddMilliseconds(i), message: $"log{i}", severity: SeverityNumber.Info)); + } + + repository.AddLogs(new AddContext(), new RepeatedField + { + new ResourceLogs + { + Resource = CreateResource(name: "service1", instanceId: "inst1"), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope("TestLogger"), + LogRecords = { logRecords } + } + } + } + }); + + var service = CreateService(repository); + + var result = service.GetLogs(resourceNames: null, traceId: null, severity: null, limit: 100_000); + + Assert.NotNull(result); + Assert.Equal(totalLogs, result.TotalCount); + Assert.Equal(totalLogs, result.ReturnedCount); + } + /// /// Creates a TelemetryApiService instance for testing with optional custom dependencies. /// diff --git a/tests/Shared/Telemetry/TelemetryTestHelpers.cs b/tests/Shared/Telemetry/TelemetryTestHelpers.cs index fd62bee78a7..16dcad85bd6 100644 --- a/tests/Shared/Telemetry/TelemetryTestHelpers.cs +++ b/tests/Shared/Telemetry/TelemetryTestHelpers.cs @@ -237,6 +237,7 @@ public static TelemetryRepository CreateRepository( int? maxAttributeLength = null, int? maxSpanEventCount = null, int? maxTraceCount = null, + int? maxLogCount = null, TimeSpan? subscriptionMinExecuteInterval = null, ILoggerFactory? loggerFactory = null, PauseManager? pauseManager = null, @@ -263,6 +264,10 @@ public static TelemetryRepository CreateRepository( { options.MaxTraceCount = maxTraceCount.Value; } + if (maxLogCount != null) + { + options.MaxLogCount = maxLogCount.Value; + } var repository = new TelemetryRepository( loggerFactory ?? NullLoggerFactory.Instance,