mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
added SLA telemetry
This commit is contained in:
@@ -1 +1 @@
|
||||
951
|
||||
952
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using AutopilotMonitor.Functions.Helpers;
|
||||
using AutopilotMonitor.Functions.Services;
|
||||
using Microsoft.ApplicationInsights;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -15,13 +16,16 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
{
|
||||
private readonly ILogger<SlaMetricsFunction> _logger;
|
||||
private readonly SlaMetricsService _slaMetricsService;
|
||||
private readonly TelemetryClient _telemetryClient;
|
||||
|
||||
public SlaMetricsFunction(
|
||||
ILogger<SlaMetricsFunction> logger,
|
||||
SlaMetricsService slaMetricsService)
|
||||
SlaMetricsService slaMetricsService,
|
||||
TelemetryClient telemetryClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_slaMetricsService = slaMetricsService;
|
||||
_telemetryClient = telemetryClient;
|
||||
}
|
||||
|
||||
[Function("GetSlaMetrics")]
|
||||
@@ -31,21 +35,34 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
{
|
||||
_logger.LogInformation("SLA metrics requested");
|
||||
|
||||
string tenantId = TenantHelper.GetTenantId(req);
|
||||
int months = 3;
|
||||
bool fresh = false;
|
||||
|
||||
try
|
||||
{
|
||||
string tenantId = TenantHelper.GetTenantId(req);
|
||||
|
||||
// Parse query parameters
|
||||
var qs = System.Web.HttpUtility.ParseQueryString(req.Url.Query ?? "");
|
||||
|
||||
int months = 3;
|
||||
if (int.TryParse(qs.Get("months"), out var parsedMonths))
|
||||
months = parsedMonths;
|
||||
|
||||
bool fresh = qs.Get("fresh") == "1";
|
||||
fresh = qs.Get("fresh") == "1";
|
||||
|
||||
var metrics = await _slaMetricsService.ComputeSlaMetricsAsync(tenantId, months, fresh);
|
||||
|
||||
_telemetryClient.TrackEvent("SlaMetricsRequested", new Dictionary<string, string>
|
||||
{
|
||||
["TenantId"] = tenantId,
|
||||
["Months"] = months.ToString(),
|
||||
["Fresh"] = fresh.ToString(),
|
||||
["FromCache"] = metrics.FromCache.ToString(),
|
||||
["ComputeDurationMs"] = metrics.ComputeDurationMs.ToString(),
|
||||
["SuccessRate"] = metrics.CurrentMonth.SuccessRate.ToString("F4"),
|
||||
["P95DurationMinutes"] = metrics.CurrentMonth.P95DurationMinutes.ToString("F2"),
|
||||
["ViolatorCount"] = metrics.Violators.Count.ToString(),
|
||||
["TotalCompleted"] = metrics.CurrentMonth.TotalCompleted.ToString()
|
||||
});
|
||||
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(metrics);
|
||||
return response;
|
||||
@@ -54,6 +71,15 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
{
|
||||
_logger.LogError(ex, "Error computing SLA metrics");
|
||||
|
||||
_telemetryClient.TrackEvent("SlaMetricsFailed", new Dictionary<string, string>
|
||||
{
|
||||
["TenantId"] = tenantId,
|
||||
["Months"] = months.ToString(),
|
||||
["Fresh"] = fresh.ToString(),
|
||||
["Error"] = ex.GetType().Name,
|
||||
["Message"] = ex.Message
|
||||
});
|
||||
|
||||
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useTenantConfig } from "../../TenantConfigContext";
|
||||
import SaveResetBar from "../../components/SaveResetBar";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
|
||||
export function SectionSlaTargets() {
|
||||
const {
|
||||
@@ -95,7 +96,10 @@ export function SectionSlaTargets() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={slaNotifyOnSuccessRateBreach}
|
||||
onChange={(e) => setSlaNotifyOnSuccessRateBreach(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
trackEvent('sla_channel_toggled', { channel: 'success_rate_breach', enabled: e.target.checked });
|
||||
setSlaNotifyOnSuccessRateBreach(e.target.checked);
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
@@ -128,7 +132,10 @@ export function SectionSlaTargets() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={slaNotifyOnDurationBreach}
|
||||
onChange={(e) => setSlaNotifyOnDurationBreach(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
trackEvent('sla_channel_toggled', { channel: 'duration_breach', enabled: e.target.checked });
|
||||
setSlaNotifyOnDurationBreach(e.target.checked);
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
/>
|
||||
<div>
|
||||
@@ -144,7 +151,10 @@ export function SectionSlaTargets() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={slaNotifyOnAppInstallBreach}
|
||||
onChange={(e) => setSlaNotifyOnAppInstallBreach(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
trackEvent('sla_channel_toggled', { channel: 'app_install_breach', enabled: e.target.checked });
|
||||
setSlaNotifyOnAppInstallBreach(e.target.checked);
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
/>
|
||||
<div>
|
||||
@@ -160,7 +170,10 @@ export function SectionSlaTargets() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={slaNotifyOnConsecutiveFailures}
|
||||
onChange={(e) => setSlaNotifyOnConsecutiveFailures(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
trackEvent('sla_channel_toggled', { channel: 'consecutive_failures', enabled: e.target.checked });
|
||||
setSlaNotifyOnConsecutiveFailures(e.target.checked);
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
@@ -189,8 +202,18 @@ export function SectionSlaTargets() {
|
||||
</div>
|
||||
|
||||
<SaveResetBar
|
||||
onSave={handleSaveSlaTargets}
|
||||
onReset={handleResetSlaTargets}
|
||||
onSave={() => {
|
||||
trackEvent('sla_targets_saved', {
|
||||
hasSuccessRateTarget: slaTargetSuccessRate != null,
|
||||
hasDurationTarget: slaTargetMaxDurationMinutes != null,
|
||||
hasAppInstallTarget: slaTargetAppInstallSuccessRate != null,
|
||||
});
|
||||
return handleSaveSlaTargets();
|
||||
}}
|
||||
onReset={() => {
|
||||
trackEvent('sla_targets_reset');
|
||||
return handleResetSlaTargets();
|
||||
}}
|
||||
saving={savingSection === "slaTargets"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"
|
||||
import { SlaGauge } from "@/components/charts/SlaGauge";
|
||||
import AppLineChart from "@/components/charts/AppLineChart";
|
||||
import { chartColors } from "@/components/charts/chartTheme";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
import Link from "next/link";
|
||||
|
||||
interface SlaSnapshot {
|
||||
@@ -125,6 +126,10 @@ export default function SlaPage() {
|
||||
console.error('Error loading SLA metrics:', err);
|
||||
addNotification('error', 'Error', 'Failed to load SLA metrics', 'sla-fetch-error');
|
||||
}
|
||||
trackEvent('sla_load_failed', {
|
||||
months,
|
||||
error: err instanceof Error ? err.message : 'unknown',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -133,6 +138,11 @@ export default function SlaPage() {
|
||||
|
||||
useEffect(() => { fetchMetrics(); }, [fetchMetrics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId) trackEvent('sla_page_viewed', { months });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tenantId]);
|
||||
|
||||
const hasTargets = metrics &&
|
||||
(metrics.targetSuccessRate != null || metrics.targetMaxDurationMinutes != null ||
|
||||
metrics.targetAppInstallSuccessRate != null);
|
||||
@@ -174,7 +184,11 @@ export default function SlaPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={months}
|
||||
onChange={(e) => setMonths(Number(e.target.value))}
|
||||
onChange={(e) => {
|
||||
const next = Number(e.target.value);
|
||||
trackEvent('sla_months_changed', { months: next });
|
||||
setMonths(next);
|
||||
}}
|
||||
className="text-sm border border-gray-300 dark:border-gray-600 rounded-md px-2 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={1}>Last month</option>
|
||||
@@ -182,7 +196,10 @@ export default function SlaPage() {
|
||||
<option value={6}>Last 6 months</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => fetchMetrics(true)}
|
||||
onClick={() => {
|
||||
trackEvent('sla_refresh_clicked', { months });
|
||||
fetchMetrics(true);
|
||||
}}
|
||||
disabled={refreshing}
|
||||
className="px-4 py-2 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2"
|
||||
>
|
||||
@@ -353,7 +370,15 @@ export default function SlaPage() {
|
||||
{metrics.violators.map((v) => (
|
||||
<tr key={v.sessionId} className="hover:bg-gray-50 dark:hover:bg-gray-750">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link href={`/session/${v.tenantId}/${v.sessionId}`} className="text-indigo-600 dark:text-indigo-400 hover:underline text-sm">
|
||||
<Link
|
||||
href={`/session/${v.tenantId}/${v.sessionId}`}
|
||||
onClick={() => trackEvent('sla_violator_opened', {
|
||||
sessionId: v.sessionId,
|
||||
tenantId: v.tenantId,
|
||||
violationType: v.violationType,
|
||||
})}
|
||||
className="text-indigo-600 dark:text-indigo-400 hover:underline text-sm"
|
||||
>
|
||||
{v.deviceName || "Unknown"}
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user