diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/NoStoreCacheMiddlewareTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/NoStoreCacheMiddlewareTests.cs index d8c060ce..fb7943e4 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/NoStoreCacheMiddlewareTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/NoStoreCacheMiddlewareTests.cs @@ -27,7 +27,6 @@ public class NoStoreCacheMiddlewareTests // GA cross-tenant raw + global PII surfaces [InlineData("/api/raw/sessions")] [InlineData("/api/raw/events")] - [InlineData("/api/raw/events/search")] [InlineData("/api/global/sessions")] [InlineData("/api/global/raw/sessions")] [InlineData("/api/global/raw/events")] diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs index 8c601b8c..77f7f568 100644 --- a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs @@ -153,16 +153,6 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage int pageSize, string? continuation) => _storage.SearchSessionsByEventPageAsync(tenantId, eventType, source, severity, phase, pageSize, continuation); - public Task> SearchSessionsByEventAsync( - string? tenantId, string eventType, string? source, string? severity, - string? phase, int limit = 50) - => _storage.SearchSessionsByEventAsync(tenantId, eventType, source, severity, phase, limit); - - public Task> SearchEventsByTypesAsync( - string? tenantId, IEnumerable eventTypes, string? source, string? severity, - int sessionLimit = 10, int eventLimit = 50) - => _storage.SearchEventsByTypesAsync(tenantId, eventTypes, source, severity, sessionLimit, eventLimit); - public Task> SearchSessionsByCveAsync( string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50) => _storage.SearchSessionsByCveAsync(tenantId, cveId, minCvssScore, overallRisk, limit); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Raw/SearchEventsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Raw/SearchEventsFunction.cs deleted file mode 100644 index ef1447b0..00000000 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Raw/SearchEventsFunction.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Net; -using AutopilotMonitor.Functions.Helpers; -using AutopilotMonitor.Functions.Services; -using AutopilotMonitor.Shared.DataAccess; -using AutopilotMonitor.Shared.Models; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.Extensions.Logging; - -namespace AutopilotMonitor.Functions.Functions.Raw -{ - /// - /// Index-based event search across sessions. - /// Uses EventTypeIndex for pre-filtering (severity, source), then fetches only matching - /// event types per session via server-side OData filter — eliminates the N+1 pattern. - /// Designed for MCP search tools (search_events_semantic, deep_search_events). - /// - public class SearchEventsFunction - { - private readonly ILogger _logger; - private readonly ISessionRepository _sessionRepo; - - public SearchEventsFunction(ILogger logger, ISessionRepository sessionRepo) - { - _logger = logger; - _sessionRepo = sessionRepo; - } - - /// - /// GET /api/raw/events/search — Tenant-scoped index-based event search - /// - [Function("SearchEvents")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "raw/events/search")] HttpRequestData req) - { - try - { - var tenantId = TenantHelper.GetTenantId(req); - return await SearchEvents(req, tenantId); - } - catch (UnauthorizedAccessException) - { - var err = req.CreateResponse(HttpStatusCode.Unauthorized); - await err.WriteAsJsonAsync(new { error = "Unauthorized" }); - return err; - } - catch (Exception ex) - { - return await req.InternalServerErrorAsync(_logger, ex, "Search events"); - } - } - - /// - /// GET /api/global/raw/events/search — Cross-tenant index-based event search (GlobalAdminOnly) - /// - [Function("SearchEventsGlobal")] - public async Task RunGlobal( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/raw/events/search")] HttpRequestData req) - { - try - { - var tenantId = req.Query["tenantId"]; - return await SearchEvents(req, string.IsNullOrEmpty(tenantId) ? null : tenantId); - } - catch (Exception ex) - { - return await req.InternalServerErrorAsync(_logger, ex, "Search events global"); - } - } - - private async Task SearchEvents(HttpRequestData req, string? tenantId) - { - var eventTypesStr = req.Query["eventTypes"]; - var severity = req.Query["severity"]; - var source = req.Query["source"]; - var sessionLimitStr = req.Query["sessionLimit"]; - var limitStr = req.Query["limit"]; - - if (string.IsNullOrEmpty(eventTypesStr)) - { - var bad = req.CreateResponse(HttpStatusCode.BadRequest); - await bad.WriteAsJsonAsync(new { error = "eventTypes parameter is required (comma-separated list)" }); - return bad; - } - - var eventTypes = eventTypesStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var sessionLimit = int.TryParse(sessionLimitStr, out var sl) ? Math.Clamp(sl, 1, 50) : 10; - var limit = int.TryParse(limitStr, out var l) ? Math.Clamp(l, 1, 500) : 50; - - var events = await _sessionRepo.SearchEventsByTypesAsync(tenantId, eventTypes, source, severity, sessionLimit, limit); - ErrorCodeEnricher.EnrichEvents(events); - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(new - { - tenantId, - eventTypes, - count = events.Count, - events - }); - return response; - } - } -} diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/NoStoreCacheMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/NoStoreCacheMiddleware.cs index 87c2e327..100a9590 100644 --- a/src/Backend/AutopilotMonitor.Functions/Middleware/NoStoreCacheMiddleware.cs +++ b/src/Backend/AutopilotMonitor.Functions/Middleware/NoStoreCacheMiddleware.cs @@ -37,7 +37,6 @@ public class NoStoreCacheMiddleware : IFunctionsWorkerMiddleware // GA cross-tenant raw + global PII surfaces "/api/raw/sessions", "/api/raw/events", - "/api/raw/events/search", "/api/global/sessions", "/api/global/distress-reports", "/api/global/session-reports", // covers list; /download-url + /{id}/note via prefix diff --git a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs index 81ab497e..5274db0e 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs @@ -130,7 +130,6 @@ public static class EndpointAccessPolicyCatalog // ── MemberRead (Admin + Operator, later + Viewer) ─────────────── new("GET", "raw/sessions", EndpointPolicy.MemberRead), new("GET", "raw/events", EndpointPolicy.MemberRead), - new("GET", "raw/events/search", EndpointPolicy.MemberRead), new("GET", "search/quick", EndpointPolicy.MemberRead), new("GET", "search/sessions", EndpointPolicy.MemberRead), new("GET", "search/sessions-by-event", EndpointPolicy.MemberRead), @@ -238,7 +237,6 @@ public static class EndpointAccessPolicyCatalog new("GET", "global/session-deletions", EndpointPolicy.GlobalAdminOnly), new("GET", "global/raw/sessions", EndpointPolicy.GlobalAdminOnly), new("GET", "global/raw/events", EndpointPolicy.GlobalAdminOnly), - new("GET", "global/raw/events/search", EndpointPolicy.GlobalAdminOnly), new("GET", "global/raw/tables", EndpointPolicy.GlobalAdminOnly), new("GET", "global/raw/tables/{tableName}", EndpointPolicy.GlobalAdminOnly), new("POST", "global/raw/logs", EndpointPolicy.GlobalAdminOnly), diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.AgentApi.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.AgentApi.cs index 96946e6b..b4558a5b 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.AgentApi.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.AgentApi.cs @@ -886,72 +886,7 @@ namespace AutopilotMonitor.Functions.Services } /// - /// Searches sessions that contain a specific event type using the EventTypeIndex. - /// Supports server-side severity prefilter via MaxSeverity (enriched index rows). - /// Source filtering is applied client-side on the Sources field. - /// - public async Task> SearchSessionsByEventAsync( - string? tenantId, string eventType, string? source, string? severity, string? phase, int limit = 50) - { - var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.EventTypeIndex); - - // Build OData filter: PartitionKey + optional severity prefilter - int? minSeverity = null; - if (!string.IsNullOrEmpty(severity) && - Enum.TryParse(severity, ignoreCase: true, out var parsedSeverity)) - { - minSeverity = (int)parsedSeverity; - } - - // Tenant-scoped: partition-targeted (cheap, exact-match on PK). - // Cross-tenant: server-side filter on the EventType column. The - // PartitionKey shape is `{tenantId}_{eventType}` and tenantIds may - // contain dashes but never underscores, while eventType itself may - // contain underscores (e.g. `app_install_failed`) — so a PK-suffix - // split is brittle. Filtering on the explicit EventType column - // avoids that ambiguity entirely (regression: previously used - // `LastIndexOf('_')` and silently dropped every multi-underscore - // event type). - string? oDataFilter; - if (!string.IsNullOrEmpty(tenantId)) - { - oDataFilter = $"PartitionKey eq '{ODataSanitizer.EscapeValue(tenantId)}_{ODataSanitizer.EscapeValue(eventType)}'"; - if (minSeverity.HasValue) - oDataFilter += $" and MaxSeverity ge {minSeverity.Value}"; - } - else - { - oDataFilter = $"EventType eq '{ODataSanitizer.EscapeValue(eventType)}'"; - if (minSeverity.HasValue) - oDataFilter += $" and MaxSeverity ge {minSeverity.Value}"; - } - - var sessionIds = new List(); - await foreach (var entity in tableClient.QueryAsync(filter: oDataFilter)) - { - - // Client-side source filter on the Sources field - if (!string.IsNullOrEmpty(source)) - { - var sources = entity.GetString("Sources") ?? ""; - if (!sources.Contains(source, StringComparison.OrdinalIgnoreCase)) continue; - } - - var sessionId = entity.GetString("SessionId"); - if (!string.IsNullOrEmpty(sessionId) && !sessionIds.Contains(sessionId)) - sessionIds.Add(sessionId); - - if (sessionIds.Count >= limit * 2) break; - } - - if (sessionIds.Count == 0) return new List(); - - var sessions = await BatchGetSessionsAsync(tenantId, sessionIds); - return sessions.Take(limit).ToList(); - } - - /// - /// Paged variant of — drives + /// Paged EventTypeIndex walk that drives /// QueryRawEventsFunction's cross-session walk. Returns up to /// distinct sessions plus the underlying /// Azure-Tables continuation; caller follows the wire nextLink @@ -965,8 +900,8 @@ namespace AutopilotMonitor.Functions.Services var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.EventTypeIndex); - // Build OData filter — same as legacy SearchSessionsByEventAsync but - // without the limit*2 over-fetch, since pagination handles depth. + // Build OData filter (PartitionKey eq for tenant-scoped, EventType eq + // for cross-tenant) — no limit*2 over-fetch; pagination handles depth. int? minSeverity = null; if (!string.IsNullOrEmpty(severity) && Enum.TryParse(severity, ignoreCase: true, out var parsedSeverity)) @@ -1029,77 +964,6 @@ namespace AutopilotMonitor.Functions.Services return new RawPage(sessions, nextRawToken); } - /// - /// Searches events across sessions for multiple event types using the EventTypeIndex for pre-filtering, - /// then fetches only matching events per session via server-side OData filter on the Events table. - /// Designed for MCP search tools to replace the N+1 fetchSessionEvents pattern. - /// - public async Task> SearchEventsByTypesAsync( - string? tenantId, IEnumerable eventTypes, string? source, string? severity, - int sessionLimit = 10, int eventLimit = 50) - { - var typeList = eventTypes.Where(t => !string.IsNullOrEmpty(t)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); - if (typeList.Count == 0) return new List(); - - // Step 1: Query EventTypeIndex for each event type to find candidate sessions - var candidateSessionIds = new Dictionary(StringComparer.OrdinalIgnoreCase); // sessionId → tenantId - - foreach (var eventType in typeList) - { - var sessions = await SearchSessionsByEventAsync(tenantId, eventType, source, severity, null, limit: sessionLimit); - foreach (var session in sessions) - { - candidateSessionIds.TryAdd(session.SessionId, session.TenantId); - } - - if (candidateSessionIds.Count >= sessionLimit) break; - } - - if (candidateSessionIds.Count == 0) return new List(); - - // Step 2: For each candidate session, fetch only matching event types via filtered query - var allEvents = new List(); - var sem = new SemaphoreSlim(10, 10); - - var tasks = candidateSessionIds.Take(sessionLimit).Select(async kvp => - { - await sem.WaitAsync(); - try - { - var sessionEvents = new List(); - foreach (var eventType in typeList) - { - var events = await GetSessionEventsByTypeAsync(kvp.Value, kvp.Key, eventType, eventLimit); - sessionEvents.AddRange(events); - } - - // Apply remaining client-side filters (severity, source) - return sessionEvents.Where(e => - (string.IsNullOrEmpty(severity) || e.Severity.ToString().Equals(severity, StringComparison.OrdinalIgnoreCase)) && - (string.IsNullOrEmpty(source) || (e.Source ?? "").Contains(source, StringComparison.OrdinalIgnoreCase)) - ).ToList(); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to fetch events for session {SessionId}", kvp.Key); - return new List(); - } - finally - { - sem.Release(); - } - }); - - var results = await Task.WhenAll(tasks); - foreach (var batch in results) - allEvents.AddRange(batch); - - return allEvents - .OrderByDescending(e => e.Timestamp) - .Take(eventLimit) - .ToList(); - } - /// /// Searches sessions affected by a specific CVE using the CveIndex. /// diff --git a/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts b/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts index 64b69a6e..056fe383 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts @@ -43,9 +43,9 @@ function extractEventTypeCandidates(keywords: string[]): string[] { // // The cross-session search walks the already-paginated /api/raw/events // endpoint once per candidate event type, following nextLink to a bounded -// budget and merging client-side. This replaces the legacy capped -// /api/raw/events/search call (fixed sessionLimit:10 + limit:200, no cursor, -// no nextLink) whose result silently truncated without telling the caller. +// budget and merging client-side. This replaces a former capped multi-type +// search call (fixed sessionLimit:10 + limit:200, no cursor, no nextLink) +// whose result silently truncated without telling the caller. // Because each walk filters by a single event type and Azure-Tables // continuation pages never overlap, the same event can never appear in two // walks — only sessions recur across type-walks, which we union into a Set. diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs index fb4096dd..caa3d797 100644 --- a/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs @@ -152,12 +152,8 @@ namespace AutopilotMonitor.Shared.DataAccess /// Task> SearchSessionsPageAsync( string? tenantId, SessionSearchFilter filter, int pageSize, string? continuation); - Task> SearchSessionsByEventAsync( - string? tenantId, string eventType, string? source, string? severity, - string? phase, int limit = 50); - /// - /// Paged variant of . Walks the + /// Paged walk of the /// EventTypeIndex table page-by-page so callers can drain large /// result sets via nextLink. Replaces the legacy hard-coded /// limit:20 session-index lookup that silently dropped recall on @@ -166,9 +162,6 @@ namespace AutopilotMonitor.Shared.DataAccess Task> SearchSessionsByEventPageAsync( string? tenantId, string eventType, string? source, string? severity, string? phase, int pageSize, string? continuation); - Task> SearchEventsByTypesAsync( - string? tenantId, IEnumerable eventTypes, string? source, string? severity, - int sessionLimit = 10, int eventLimit = 50); Task> SearchSessionsByCveAsync( string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50);