added SLA telemetry

This commit is contained in:
Oliver Kieselbach
2026-04-14 10:15:01 +02:00
parent eb0df61b47
commit cfc412d5ff
4 changed files with 90 additions and 16 deletions
@@ -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>
+28 -3
View File
@@ -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>