mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-26 05:29:43 +00:00
211 lines
10 KiB
C#
211 lines
10 KiB
C#
using System.Net;
|
|
using AutopilotMonitor.Functions.Helpers;
|
|
using AutopilotMonitor.Functions.Services;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Microsoft.Azure.Functions.Worker;
|
|
using Microsoft.Azure.Functions.Worker.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace AutopilotMonitor.Functions.Functions.Metrics
|
|
{
|
|
public class GetGeographicMetricsFunction
|
|
{
|
|
private readonly ILogger<GetGeographicMetricsFunction> _logger;
|
|
private readonly TableStorageService _storageService;
|
|
private readonly TenantConfigurationService _configService;
|
|
|
|
public GetGeographicMetricsFunction(ILogger<GetGeographicMetricsFunction> logger, TableStorageService storageService, TenantConfigurationService configService)
|
|
{
|
|
_logger = logger;
|
|
_storageService = storageService;
|
|
_configService = configService;
|
|
}
|
|
|
|
[Function("GetGeographicMetrics")]
|
|
public async Task<HttpResponseData> Run(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metrics/geographic")] HttpRequestData req)
|
|
{
|
|
try
|
|
{
|
|
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
_logger.LogInformation("Fetching geographic metrics for tenant {TenantId}", tenantId);
|
|
|
|
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
|
|
var daysParam = query["days"];
|
|
int days = 30;
|
|
if (!string.IsNullOrEmpty(daysParam) && int.TryParse(daysParam, out var parsedDays) && parsedDays > 0)
|
|
days = parsedDays;
|
|
|
|
var groupBy = query["groupBy"] ?? "city";
|
|
|
|
// Check if geo-location is enabled for this tenant
|
|
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
|
|
var geoEnabled = tenantConfig?.EnableGeoLocation ?? true;
|
|
|
|
var cutoff = DateTime.UtcNow.AddDays(-days);
|
|
var sessions = await _storageService.GetSessionsAsync(tenantId, maxResults: 10000, since: cutoff);
|
|
var allSummaries = await _storageService.GetAppInstallSummariesByTenantAsync(tenantId);
|
|
var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList();
|
|
|
|
var result = ComputeGeographicMetrics(sessions, summaries, groupBy);
|
|
result.GeoLocationEnabled = geoEnabled;
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
|
await response.WriteAsJsonAsync(result);
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error fetching geographic metrics");
|
|
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
|
await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error" });
|
|
return errorResponse;
|
|
}
|
|
}
|
|
|
|
internal static GeographicMetricsResponse ComputeGeographicMetrics(
|
|
List<SessionSummary> sessions, List<AppInstallSummary> appSummaries, string groupBy)
|
|
{
|
|
// Filter to sessions with geo data
|
|
var geoSessions = sessions.Where(s => !string.IsNullOrEmpty(s.GeoCountry)).ToList();
|
|
|
|
// Build app install lookup by session
|
|
var appsBySession = appSummaries
|
|
.GroupBy(a => a.SessionId)
|
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Group sessions by location key
|
|
var groups = geoSessions.GroupBy(s => GetLocationKey(s, groupBy)).ToList();
|
|
|
|
var locations = new List<LocationMetrics>();
|
|
|
|
foreach (var group in groups)
|
|
{
|
|
var first = group.First();
|
|
var succeeded = group.Where(s => s.Status == SessionStatus.Succeeded).ToList();
|
|
var failed = group.Where(s => s.Status == SessionStatus.Failed).ToList();
|
|
|
|
// Duration stats from succeeded sessions with valid duration
|
|
var durations = succeeded
|
|
.Where(s => s.DurationSeconds.HasValue && s.DurationSeconds.Value > 0)
|
|
.Select(s => s.DurationSeconds!.Value / 60.0)
|
|
.OrderBy(d => d)
|
|
.ToList();
|
|
|
|
var avgDuration = durations.Count > 0 ? durations.Average() : 0;
|
|
var medianDuration = durations.Count > 0 ? durations[durations.Count / 2] : 0;
|
|
var p95Duration = durations.Count > 0 ? durations[(int)(durations.Count * 0.95)] : 0;
|
|
|
|
// App count per session
|
|
var sessionAppCounts = group.Select(s =>
|
|
appsBySession.ContainsKey(s.SessionId) ? appsBySession[s.SessionId].Count : 0
|
|
).Where(c => c > 0).ToList();
|
|
var avgAppCount = sessionAppCounts.Count > 0 ? sessionAppCounts.Average() : 0;
|
|
var minutesPerApp = avgAppCount > 0 ? avgDuration / avgAppCount : 0;
|
|
|
|
// Network throughput from app install summaries
|
|
var locationApps = group
|
|
.Where(s => appsBySession.ContainsKey(s.SessionId))
|
|
.SelectMany(s => appsBySession[s.SessionId])
|
|
.Where(a => a.DownloadDurationSeconds > 0 && a.DownloadBytes > 0)
|
|
.ToList();
|
|
var totalBytes = locationApps.Sum(a => a.DownloadBytes);
|
|
var totalDownloadSecs = locationApps.Sum(a => (double)a.DownloadDurationSeconds);
|
|
var avgThroughput = totalDownloadSecs > 0 ? totalBytes / totalDownloadSecs : 0;
|
|
|
|
locations.Add(new LocationMetrics
|
|
{
|
|
LocationKey = group.Key,
|
|
Country = first.GeoCountry,
|
|
Region = first.GeoRegion,
|
|
City = first.GeoCity,
|
|
Loc = first.GeoLoc,
|
|
SessionCount = group.Count(),
|
|
Succeeded = succeeded.Count,
|
|
Failed = failed.Count,
|
|
SuccessRate = group.Count() > 0 ? Math.Round((double)succeeded.Count / group.Count() * 100, 1) : 0,
|
|
AvgDurationMinutes = Math.Round(avgDuration, 1),
|
|
MedianDurationMinutes = Math.Round(medianDuration, 1),
|
|
P95DurationMinutes = Math.Round(p95Duration, 1),
|
|
AvgAppCount = Math.Round(avgAppCount, 1),
|
|
MinutesPerApp = Math.Round(minutesPerApp, 2),
|
|
AvgThroughputBytesPerSec = Math.Round(avgThroughput, 0),
|
|
TotalDownloadBytes = totalBytes
|
|
});
|
|
}
|
|
|
|
// Compute global averages
|
|
var allDurations = locations.Where(l => l.SessionCount >= 3 && l.AvgDurationMinutes > 0)
|
|
.Select(l => l.AvgDurationMinutes).ToList();
|
|
var globalAvgDuration = allDurations.Count > 0 ? allDurations.Average() : 0;
|
|
var globalMedianDuration = allDurations.Count > 0 ? allDurations.OrderBy(d => d).ToList()[allDurations.Count / 2] : 0;
|
|
var globalStdDev = allDurations.Count > 1
|
|
? Math.Sqrt(allDurations.Sum(d => Math.Pow(d - globalAvgDuration, 2)) / (allDurations.Count - 1))
|
|
: 0;
|
|
|
|
var allMinutesPerApp = locations.Where(l => l.MinutesPerApp > 0).Select(l => l.MinutesPerApp).ToList();
|
|
var globalAvgMinutesPerApp = allMinutesPerApp.Count > 0 ? allMinutesPerApp.Average() : 0;
|
|
var globalMedianMinutesPerApp = allMinutesPerApp.Count > 0
|
|
? allMinutesPerApp.OrderBy(m => m).ToList()[allMinutesPerApp.Count / 2] : 0;
|
|
|
|
var allThroughputs = locations.Where(l => l.AvgThroughputBytesPerSec > 0)
|
|
.Select(l => l.AvgThroughputBytesPerSec).ToList();
|
|
var globalAvgThroughput = allThroughputs.Count > 0 ? allThroughputs.Average() : 0;
|
|
|
|
// Compute per-location comparisons and outlier detection
|
|
foreach (var loc in locations)
|
|
{
|
|
// Duration vs global
|
|
loc.DurationVsGlobalPct = globalAvgDuration > 0
|
|
? Math.Round((loc.AvgDurationMinutes - globalAvgDuration) / globalAvgDuration * 100, 1) : 0;
|
|
|
|
// Throughput vs global
|
|
loc.ThroughputVsGlobalPct = globalAvgThroughput > 0
|
|
? Math.Round((loc.AvgThroughputBytesPerSec - globalAvgThroughput) / globalAvgThroughput * 100, 1) : 0;
|
|
|
|
// AppLoadScore: normalize minutesPerApp to global median = 100
|
|
loc.AppLoadScore = globalMedianMinutesPerApp > 0
|
|
? Math.Round(loc.MinutesPerApp / globalMedianMinutesPerApp * 100, 0) : 0;
|
|
|
|
// Outlier detection (minimum 3 sessions)
|
|
if (loc.SessionCount >= 3 && globalStdDev > 0 && loc.AvgDurationMinutes > 0)
|
|
{
|
|
var zScore = (loc.AvgDurationMinutes - globalAvgDuration) / globalStdDev;
|
|
loc.IsOutlier = Math.Abs(zScore) > 2;
|
|
loc.OutlierDirection = zScore > 2 ? "slow" : zScore < -2 ? "fast" : null;
|
|
}
|
|
}
|
|
|
|
return new GeographicMetricsResponse
|
|
{
|
|
Success = true,
|
|
Locations = locations.OrderByDescending(l => l.SessionCount).ToList(),
|
|
GlobalAverages = new GlobalAverages
|
|
{
|
|
AvgDurationMinutes = Math.Round(globalAvgDuration, 1),
|
|
MedianDurationMinutes = Math.Round(globalMedianDuration, 1),
|
|
AvgMinutesPerApp = Math.Round(globalAvgMinutesPerApp, 2),
|
|
AvgThroughputBytesPerSec = Math.Round(globalAvgThroughput, 0),
|
|
StdDevDurationMinutes = Math.Round(globalStdDev, 1)
|
|
},
|
|
ComputedAt = DateTime.UtcNow,
|
|
TotalSessions = sessions.Count,
|
|
LocationsWithData = geoSessions.Count
|
|
};
|
|
}
|
|
|
|
private static string GetLocationKey(SessionSummary session, string groupBy)
|
|
{
|
|
return groupBy.ToLower() switch
|
|
{
|
|
"country" => session.GeoCountry,
|
|
"region" => $"{session.GeoRegion}, {session.GeoCountry}",
|
|
_ => !string.IsNullOrEmpty(session.GeoCity)
|
|
? $"{session.GeoCity}, {session.GeoRegion}, {session.GeoCountry}"
|
|
: $"{session.GeoRegion}, {session.GeoCountry}"
|
|
};
|
|
}
|
|
}
|
|
}
|