mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
fix fleet health
This commit is contained in:
@@ -1 +1 @@
|
||||
825
|
||||
827
|
||||
|
||||
+4
-4
@@ -33,11 +33,11 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
public Task<string?> FindSessionTenantIdAsync(string sessionId)
|
||||
=> _storage.FindSessionTenantIdAsync(sessionId);
|
||||
|
||||
public Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null)
|
||||
=> _storage.GetSessionsAsync(tenantId, maxResults, cursor);
|
||||
public Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null, int? days = null)
|
||||
=> _storage.GetSessionsAsync(tenantId, maxResults, cursor, days);
|
||||
|
||||
public Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null)
|
||||
=> _storage.GetAllSessionsAsync(maxResults, cursor);
|
||||
public Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null, int? days = null)
|
||||
=> _storage.GetAllSessionsAsync(maxResults, cursor, days);
|
||||
|
||||
public Task<bool> DeleteSessionAsync(string tenantId, string sessionId)
|
||||
=> _storage.DeleteSessionAsync(tenantId, sessionId);
|
||||
|
||||
@@ -34,6 +34,10 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
|
||||
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
|
||||
var cursor = query["cursor"];
|
||||
var tenantIdFilter = query["tenantId"];
|
||||
var daysParam = query["days"];
|
||||
int? days = null;
|
||||
if (!string.IsNullOrEmpty(daysParam) && int.TryParse(daysParam, out var parsedDays) && parsedDays > 0)
|
||||
days = parsedDays;
|
||||
|
||||
SessionPage page;
|
||||
|
||||
@@ -46,13 +50,13 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
|
||||
return badRequest;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Fetching sessions for tenant {TenantId} (User: {UserEmail}, cursor: {Cursor})", tenantIdFilter, userEmail, cursor ?? "none");
|
||||
page = await _sessionRepo.GetSessionsAsync(tenantIdFilter, maxResults: 100, cursor: cursor);
|
||||
_logger.LogInformation("Fetching sessions for tenant {TenantId} (User: {UserEmail}, cursor: {Cursor}, days: {Days})", tenantIdFilter, userEmail, cursor ?? "none", days?.ToString() ?? "all");
|
||||
page = await _sessionRepo.GetSessionsAsync(tenantIdFilter, maxResults: 100, cursor: cursor, days: days);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Fetching all sessions across all tenants (User: {UserEmail}, cursor: {Cursor})", userEmail, cursor ?? "none");
|
||||
page = await _sessionRepo.GetAllSessionsAsync(maxResults: 100, cursor: cursor);
|
||||
_logger.LogInformation("Fetching all sessions across all tenants (User: {UserEmail}, cursor: {Cursor}, days: {Days})", userEmail, cursor ?? "none", days?.ToString() ?? "all");
|
||||
page = await _sessionRepo.GetAllSessionsAsync(maxResults: 100, cursor: cursor, days: days);
|
||||
}
|
||||
|
||||
return await req.OkAsync(new
|
||||
|
||||
@@ -30,10 +30,14 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
|
||||
var tenantId = TenantHelper.GetTenantId(req);
|
||||
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
|
||||
var cursor = query["cursor"];
|
||||
var daysParam = query["days"];
|
||||
int? days = null;
|
||||
if (!string.IsNullOrEmpty(daysParam) && int.TryParse(daysParam, out var parsedDays) && parsedDays > 0)
|
||||
days = parsedDays;
|
||||
|
||||
_logger.LogInformation("Fetching sessions for tenant {TenantId} (cursor: {Cursor})", tenantId, cursor ?? "none");
|
||||
_logger.LogInformation("Fetching sessions for tenant {TenantId} (cursor: {Cursor}, days: {Days})", tenantId, cursor ?? "none", days?.ToString() ?? "all");
|
||||
|
||||
var page = await _sessionRepo.GetSessionsAsync(tenantId, maxResults: 100, cursor: cursor);
|
||||
var page = await _sessionRepo.GetSessionsAsync(tenantId, maxResults: 100, cursor: cursor, days: days);
|
||||
|
||||
return await req.OkAsync(new
|
||||
{
|
||||
|
||||
@@ -583,7 +583,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
/// so Azure Table Storage returns results in descending time order natively.
|
||||
/// Falls back to the Sessions table if SessionsIndex is empty (pre-migration).
|
||||
/// </summary>
|
||||
public async Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null)
|
||||
public async Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null, int? days = null)
|
||||
{
|
||||
SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId));
|
||||
|
||||
@@ -598,24 +598,45 @@ namespace AutopilotMonitor.Functions.Services
|
||||
filter += $" and RowKey gt '{cursor}'";
|
||||
}
|
||||
|
||||
// Fetch maxResults + 1 to determine hasMore
|
||||
var fetchCount = maxResults + 1;
|
||||
// When days is specified, add RowKey upper bound to limit scan to date range.
|
||||
// Inverted-tick ordering: newer sessions have smaller RowKey values.
|
||||
// Sessions older than cutoff have RowKey >= cutoffRowKeyPrefix.
|
||||
if (days.HasValue)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-days.Value);
|
||||
var cutoffRowKeyPrefix = $"{(DateTime.MaxValue.Ticks - cutoffDate.Ticks):D19}";
|
||||
filter += $" and RowKey lt '{cutoffRowKeyPrefix}'";
|
||||
}
|
||||
|
||||
// When days is set, return all matching sessions (no pagination); otherwise paginate.
|
||||
var safetyCap = days.HasValue ? 10000 : maxResults + 1;
|
||||
var query = indexTableClient.QueryAsync<TableEntity>(
|
||||
filter: filter,
|
||||
maxPerPage: Math.Min(fetchCount, 1000)
|
||||
maxPerPage: Math.Min(safetyCap, 1000)
|
||||
);
|
||||
|
||||
var sessions = new List<SessionSummary>();
|
||||
await foreach (var entity in query)
|
||||
{
|
||||
sessions.Add(MapIndexEntityToSessionSummary(entity));
|
||||
if (sessions.Count >= fetchCount) break;
|
||||
if (sessions.Count >= safetyCap) break;
|
||||
}
|
||||
|
||||
// If SessionsIndex is empty and no cursor, fall back to Sessions table (pre-migration)
|
||||
if (sessions.Count == 0 && string.IsNullOrEmpty(cursor))
|
||||
{
|
||||
return await GetSessionsFromPrimaryTableAsync(tenantId, maxResults);
|
||||
return await GetSessionsFromPrimaryTableAsync(tenantId, maxResults, days);
|
||||
}
|
||||
|
||||
if (days.HasValue)
|
||||
{
|
||||
// Date-bounded query: all matching sessions returned, no pagination
|
||||
return new SessionPage
|
||||
{
|
||||
Sessions = sessions,
|
||||
HasMore = false,
|
||||
Cursor = null
|
||||
};
|
||||
}
|
||||
|
||||
var hasMore = sessions.Count > maxResults;
|
||||
@@ -648,20 +669,29 @@ namespace AutopilotMonitor.Functions.Services
|
||||
/// <summary>
|
||||
/// Fallback: queries the Sessions table directly (pre-migration, before SessionsIndex is populated).
|
||||
/// </summary>
|
||||
private async Task<SessionPage> GetSessionsFromPrimaryTableAsync(string tenantId, int maxResults)
|
||||
private async Task<SessionPage> GetSessionsFromPrimaryTableAsync(string tenantId, int maxResults, int? days = null)
|
||||
{
|
||||
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions);
|
||||
var filter = $"PartitionKey eq '{tenantId}'";
|
||||
var query = tableClient.QueryAsync<TableEntity>(filter: filter, maxPerPage: Math.Min(maxResults + 1, 1000));
|
||||
var safetyCap = days.HasValue ? 10000 : maxResults + 1;
|
||||
var query = tableClient.QueryAsync<TableEntity>(filter: filter, maxPerPage: Math.Min(safetyCap, 1000));
|
||||
|
||||
var sessions = new List<SessionSummary>();
|
||||
await foreach (var entity in query)
|
||||
{
|
||||
sessions.Add(MapToSessionSummary(entity));
|
||||
if (sessions.Count >= maxResults + 1) break;
|
||||
if (sessions.Count >= safetyCap) break;
|
||||
}
|
||||
|
||||
sessions = sessions.OrderByDescending(s => s.StartedAt).ToList();
|
||||
|
||||
if (days.HasValue)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-days.Value);
|
||||
sessions = sessions.Where(s => s.StartedAt >= cutoffDate).ToList();
|
||||
return new SessionPage { Sessions = sessions, HasMore = false };
|
||||
}
|
||||
|
||||
var hasMore = sessions.Count > maxResults;
|
||||
if (hasMore)
|
||||
sessions.RemoveAt(sessions.Count - 1);
|
||||
@@ -674,7 +704,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
/// with cursor-based pagination. Uses per-tenant fan-out to leverage the
|
||||
/// inverted-tick RowKey ordering within each partition, then merge-sorts.
|
||||
/// </summary>
|
||||
public async Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null)
|
||||
public async Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null, int? days = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -690,7 +720,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
|
||||
if (tenantIds.Count == 0 && string.IsNullOrEmpty(cursor))
|
||||
return await GetAllSessionsFromPrimaryTableAsync(maxResults);
|
||||
return await GetAllSessionsFromPrimaryTableAsync(maxResults, days);
|
||||
|
||||
// Step 2: Per-tenant fan-out using RowKey ordering (inverted ticks → newest first).
|
||||
// Parse cursor into invertedTicks prefix for cross-tenant RowKey filtering.
|
||||
@@ -706,7 +736,16 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
}
|
||||
|
||||
var fetchPerTenant = maxResults + 1;
|
||||
// When days is set, fetch all matching sessions per tenant (with safety cap); otherwise paginate.
|
||||
var fetchPerTenant = days.HasValue ? 5000 : maxResults + 1;
|
||||
|
||||
// Compute RowKey upper bound for date filtering
|
||||
string? cutoffRowKeyPrefix = null;
|
||||
if (days.HasValue)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-days.Value);
|
||||
cutoffRowKeyPrefix = $"{(DateTime.MaxValue.Ticks - cutoffDate.Ticks):D19}";
|
||||
}
|
||||
|
||||
// Parallel fan-out: query all tenants concurrently
|
||||
var tasks = tenantIds.Select(async tenantId =>
|
||||
@@ -714,6 +753,8 @@ namespace AutopilotMonitor.Functions.Services
|
||||
var filter = $"PartitionKey eq '{tenantId}'";
|
||||
if (cursorRowKeyPrefix != null)
|
||||
filter += $" and RowKey ge '{cursorRowKeyPrefix}'";
|
||||
if (cutoffRowKeyPrefix != null)
|
||||
filter += $" and RowKey lt '{cutoffRowKeyPrefix}'";
|
||||
|
||||
var tenantSessions = new List<SessionSummary>();
|
||||
await foreach (var entity in indexTableClient.QueryAsync<TableEntity>(
|
||||
@@ -739,6 +780,17 @@ namespace AutopilotMonitor.Functions.Services
|
||||
allSessions = allSessions.Skip(cursorIdx + 1).ToList();
|
||||
}
|
||||
|
||||
if (days.HasValue)
|
||||
{
|
||||
// Date-bounded query: all matching sessions returned, no pagination
|
||||
return new SessionPage
|
||||
{
|
||||
Sessions = allSessions,
|
||||
HasMore = false,
|
||||
Cursor = null
|
||||
};
|
||||
}
|
||||
|
||||
var hasMore = allSessions.Count > maxResults;
|
||||
allSessions = allSessions.Take(maxResults).ToList();
|
||||
|
||||
@@ -766,19 +818,28 @@ namespace AutopilotMonitor.Functions.Services
|
||||
/// <summary>
|
||||
/// Fallback: queries the Sessions table directly for global admin (pre-migration).
|
||||
/// </summary>
|
||||
private async Task<SessionPage> GetAllSessionsFromPrimaryTableAsync(int maxResults)
|
||||
private async Task<SessionPage> GetAllSessionsFromPrimaryTableAsync(int maxResults, int? days = null)
|
||||
{
|
||||
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions);
|
||||
var query = tableClient.QueryAsync<TableEntity>(maxPerPage: Math.Min(maxResults + 1, 1000));
|
||||
var safetyCap = days.HasValue ? 10000 : maxResults + 1;
|
||||
var query = tableClient.QueryAsync<TableEntity>(maxPerPage: Math.Min(safetyCap, 1000));
|
||||
|
||||
var sessions = new List<SessionSummary>();
|
||||
await foreach (var entity in query)
|
||||
{
|
||||
sessions.Add(MapToSessionSummary(entity));
|
||||
if (sessions.Count >= maxResults + 1) break;
|
||||
if (sessions.Count >= safetyCap) break;
|
||||
}
|
||||
|
||||
sessions = sessions.OrderByDescending(s => s.StartedAt).ToList();
|
||||
|
||||
if (days.HasValue)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-days.Value);
|
||||
sessions = sessions.Where(s => s.StartedAt >= cutoffDate).ToList();
|
||||
return new SessionPage { Sessions = sessions, HasMore = false };
|
||||
}
|
||||
|
||||
var hasMore = sessions.Count > maxResults;
|
||||
if (hasMore)
|
||||
sessions.RemoveAt(sessions.Count - 1);
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace AutopilotMonitor.Shared.DataAccess
|
||||
Task<bool> StoreSessionAsync(SessionRegistration registration);
|
||||
Task<SessionSummary?> GetSessionAsync(string tenantId, string sessionId);
|
||||
Task<string?> FindSessionTenantIdAsync(string sessionId);
|
||||
Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null);
|
||||
Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null);
|
||||
Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null, int? days = null);
|
||||
Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null, int? days = null);
|
||||
Task<bool> DeleteSessionAsync(string tenantId, string sessionId);
|
||||
|
||||
// --- Session Updates ---
|
||||
|
||||
@@ -65,6 +65,7 @@ export default function FleetHealthPage() {
|
||||
isTimeRangeMount.current = false;
|
||||
return;
|
||||
}
|
||||
fetchSessions(timeRange);
|
||||
fetchAppMetrics(timeRange);
|
||||
}, [timeRange]);
|
||||
|
||||
@@ -121,11 +122,12 @@ export default function FleetHealthPage() {
|
||||
};
|
||||
}, [on, off]);
|
||||
|
||||
const fetchSessions = async () => {
|
||||
const fetchSessions = async (range: "7d" | "30d" | "90d" = timeRange) => {
|
||||
try {
|
||||
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
|
||||
const endpoint = globalAdminMode
|
||||
? api.globalSessions.list()
|
||||
: api.sessions.list(tenantId);
|
||||
? api.globalSessions.list(undefined, days)
|
||||
: api.sessions.list(tenantId, days);
|
||||
const response = await authenticatedFetch(endpoint, getAccessToken);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
@@ -21,8 +21,8 @@ export const api = {
|
||||
|
||||
// ── Sessions ──────────────────────────────────────────────────────────────
|
||||
sessions: {
|
||||
list: (tenantId?: string) =>
|
||||
`${API_BASE_URL}/api/sessions${qs({ tenantId })}`,
|
||||
list: (tenantId?: string, days?: number) =>
|
||||
`${API_BASE_URL}/api/sessions${qs({ tenantId, days: days?.toString() })}`,
|
||||
get: (sessionId: string, tenantId?: string) =>
|
||||
`${API_BASE_URL}/api/sessions/${sessionId}${qs({ tenantId })}`,
|
||||
events: (sessionId: string, tenantId?: string) =>
|
||||
@@ -45,8 +45,8 @@ export const api = {
|
||||
|
||||
// ── Global Sessions (global admin) ────────────────────────────────────────
|
||||
globalSessions: {
|
||||
list: (tenantId?: string) =>
|
||||
`${API_BASE_URL}/api/global/sessions${qs({ tenantId })}`,
|
||||
list: (tenantId?: string, days?: number) =>
|
||||
`${API_BASE_URL}/api/global/sessions${qs({ tenantId, days: days?.toString() })}`,
|
||||
},
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user