mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 21:19:38 +00:00
chore(backend): remove dead /raw/events/search endpoint + orphaned repo methods
After the MCP search tools moved to the paginated /api/raw/events fan-out (prev commit), /api/raw/events/search had zero callers anywhere in the repo (no MCP tool, no web). Remove the now-dead surface end to end: - Delete SearchEventsFunction (raw/events/search + global/raw/events/search). - Drop both routes from EndpointAccessPolicyCatalog and the no-store cache middleware allowlist (+ its test InlineData). EndpointPolicyCatalog completeness test stays green (route and entry removed together). - Remove the now-orphaned repo method SearchEventsByTypesAsync (its only caller was the deleted endpoint) from ISessionRepository + TableSessionRepository delegation + TableStorageService impl. - Remove the non-paginated SearchSessionsByEventAsync: its only runtime caller was SearchEventsByTypesAsync, and it is strictly redundant with the resumable SearchSessionsByEventPageAsync (used by /api/raw/events + search/sessions-by-event). Reword the surviving paged method's xmldoc/comment that referenced it. No capability lost: multi-type cross-session event search still runs via the MCP fan-out over the paginated single-type endpoint; single-type primitives remain exposed via query_raw_events + search_sessions_by_event. Full solution builds clean (0 errors); catalog-completeness + cache-middleware tests 77/77. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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")]
|
||||
|
||||
-10
@@ -153,16 +153,6 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
int pageSize, string? continuation)
|
||||
=> _storage.SearchSessionsByEventPageAsync(tenantId, eventType, source, severity, phase, pageSize, continuation);
|
||||
|
||||
public Task<List<SessionSummary>> SearchSessionsByEventAsync(
|
||||
string? tenantId, string eventType, string? source, string? severity,
|
||||
string? phase, int limit = 50)
|
||||
=> _storage.SearchSessionsByEventAsync(tenantId, eventType, source, severity, phase, limit);
|
||||
|
||||
public Task<List<EnrollmentEvent>> SearchEventsByTypesAsync(
|
||||
string? tenantId, IEnumerable<string> eventTypes, string? source, string? severity,
|
||||
int sessionLimit = 10, int eventLimit = 50)
|
||||
=> _storage.SearchEventsByTypesAsync(tenantId, eventTypes, source, severity, sessionLimit, eventLimit);
|
||||
|
||||
public Task<List<SessionSummary>> SearchSessionsByCveAsync(
|
||||
string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50)
|
||||
=> _storage.SearchSessionsByCveAsync(tenantId, cveId, minCvssScore, overallRisk, limit);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class SearchEventsFunction
|
||||
{
|
||||
private readonly ILogger<SearchEventsFunction> _logger;
|
||||
private readonly ISessionRepository _sessionRepo;
|
||||
|
||||
public SearchEventsFunction(ILogger<SearchEventsFunction> logger, ISessionRepository sessionRepo)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionRepo = sessionRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /api/raw/events/search — Tenant-scoped index-based event search
|
||||
/// </summary>
|
||||
[Function("SearchEvents")]
|
||||
public async Task<HttpResponseData> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /api/global/raw/events/search — Cross-tenant index-based event search (GlobalAdminOnly)
|
||||
/// </summary>
|
||||
[Function("SearchEventsGlobal")]
|
||||
public async Task<HttpResponseData> 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<HttpResponseData> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -886,72 +886,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public async Task<List<SessionSummary>> 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<AutopilotMonitor.Shared.Models.EventSeverity>(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<string>();
|
||||
await foreach (var entity in tableClient.QueryAsync<TableEntity>(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<SessionSummary>();
|
||||
|
||||
var sessions = await BatchGetSessionsAsync(tenantId, sessionIds);
|
||||
return sessions.Take(limit).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paged variant of <see cref="SearchSessionsByEventAsync"/> — drives
|
||||
/// Paged EventTypeIndex walk that drives
|
||||
/// <c>QueryRawEventsFunction</c>'s cross-session walk. Returns up to
|
||||
/// <paramref name="pageSize"/> distinct sessions plus the underlying
|
||||
/// Azure-Tables continuation; caller follows the wire <c>nextLink</c>
|
||||
@@ -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<AutopilotMonitor.Shared.Models.EventSeverity>(severity, ignoreCase: true, out var parsedSeverity))
|
||||
@@ -1029,77 +964,6 @@ namespace AutopilotMonitor.Functions.Services
|
||||
return new RawPage<SessionSummary>(sessions, nextRawToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public async Task<List<EnrollmentEvent>> SearchEventsByTypesAsync(
|
||||
string? tenantId, IEnumerable<string> 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<EnrollmentEvent>();
|
||||
|
||||
// Step 1: Query EventTypeIndex for each event type to find candidate sessions
|
||||
var candidateSessionIds = new Dictionary<string, string>(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<EnrollmentEvent>();
|
||||
|
||||
// Step 2: For each candidate session, fetch only matching event types via filtered query
|
||||
var allEvents = new List<EnrollmentEvent>();
|
||||
var sem = new SemaphoreSlim(10, 10);
|
||||
|
||||
var tasks = candidateSessionIds.Take(sessionLimit).Select(async kvp =>
|
||||
{
|
||||
await sem.WaitAsync();
|
||||
try
|
||||
{
|
||||
var sessionEvents = new List<EnrollmentEvent>();
|
||||
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<EnrollmentEvent>();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches sessions affected by a specific CVE using the CveIndex.
|
||||
/// </summary>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -152,12 +152,8 @@ namespace AutopilotMonitor.Shared.DataAccess
|
||||
/// </summary>
|
||||
Task<RawPage<SessionSummary>> SearchSessionsPageAsync(
|
||||
string? tenantId, SessionSearchFilter filter, int pageSize, string? continuation);
|
||||
Task<List<SessionSummary>> SearchSessionsByEventAsync(
|
||||
string? tenantId, string eventType, string? source, string? severity,
|
||||
string? phase, int limit = 50);
|
||||
|
||||
/// <summary>
|
||||
/// Paged variant of <see cref="SearchSessionsByEventAsync"/>. Walks the
|
||||
/// Paged walk of the
|
||||
/// <c>EventTypeIndex</c> table page-by-page so callers can drain large
|
||||
/// result sets via <c>nextLink</c>. Replaces the legacy hard-coded
|
||||
/// <c>limit:20</c> session-index lookup that silently dropped recall on
|
||||
@@ -166,9 +162,6 @@ namespace AutopilotMonitor.Shared.DataAccess
|
||||
Task<RawPage<SessionSummary>> SearchSessionsByEventPageAsync(
|
||||
string? tenantId, string eventType, string? source, string? severity, string? phase,
|
||||
int pageSize, string? continuation);
|
||||
Task<List<EnrollmentEvent>> SearchEventsByTypesAsync(
|
||||
string? tenantId, IEnumerable<string> eventTypes, string? source, string? severity,
|
||||
int sessionLimit = 10, int eventLimit = 50);
|
||||
Task<List<SessionSummary>> SearchSessionsByCveAsync(
|
||||
string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user