mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
chore: retarget SignalR quota watcher to Standard S1 limits + CI build hygiene
- SignalR quota watcher/health card: defaults 20/20k (Free tier) -> 1000/1M (Standard S1 x1 unit); settings renamed SignalRFreeTier* -> SignalRConnectionLimit/ SignalRDailyMessageLimit (no Azure overrides were set); all operator-facing texts tier-neutral (connections 429 hard, message overage is billed not throttled) - web: drop obsolete edge runtime from opengraph/twitter/apple-icon image routes (next/og runs on Node since 14; images now statically generated at build) - web: update caniuse-lite browser db (silences 6-months-old Browserslist warning) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ using AutopilotMonitor.Functions.Services;
|
||||
namespace AutopilotMonitor.Functions.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tier-boundary + percent-math tests for the SignalR free-tier quota watcher.
|
||||
/// Tier-boundary + percent-math tests for the SignalR quota watcher.
|
||||
/// Mirrors the cert-expiry tests' approach: pure-function coverage of the
|
||||
/// boundary risks (off-by-one, floor vs ceil, zero-division). Full-service
|
||||
/// smoke tests would require constructing 16 dependencies; the percent +
|
||||
|
||||
@@ -7,10 +7,11 @@ using Microsoft.Extensions.Logging;
|
||||
namespace AutopilotMonitor.Functions.Functions.Admin
|
||||
{
|
||||
/// <summary>
|
||||
/// Timer-triggered watcher for the Azure SignalR Free-tier limits (20
|
||||
/// concurrent connections / 20,000 messages per UTC day). Runs every full
|
||||
/// hour and emits OpsEvents when the configured thresholds are crossed so
|
||||
/// operators get a Telegram ping before clients start getting 429'd.
|
||||
/// Timer-triggered watcher for the Azure SignalR plan limits (Standard S1
|
||||
/// x1 unit: 1,000 concurrent connections / 1M messages per UTC day). Runs
|
||||
/// every full hour and emits OpsEvents when the configured thresholds are
|
||||
/// crossed so operators get a Telegram ping before clients start getting
|
||||
/// 429'd or message overage is billed.
|
||||
///
|
||||
/// Separate from the 2h MaintenanceFunction: the SignalR limits can be
|
||||
/// breached by a brief connection spike, so the cadence is tighter. Cost
|
||||
|
||||
@@ -290,7 +290,7 @@ public class HealthCheckService
|
||||
var check = new HealthCheck
|
||||
{
|
||||
Name = "SignalR Quota",
|
||||
Description = "Free-tier connection + daily message usage"
|
||||
Description = "Connection + daily message usage vs. plan limits"
|
||||
};
|
||||
|
||||
var resourceId = _configuration["SignalRResourceId"];
|
||||
@@ -302,9 +302,9 @@ public class HealthCheckService
|
||||
}
|
||||
|
||||
var connectionLimit = ParsePositiveInt(
|
||||
"SignalRFreeTierConnectionLimit", MaintenanceService.DefaultSignalRConnectionLimit);
|
||||
"SignalRConnectionLimit", MaintenanceService.DefaultSignalRConnectionLimit);
|
||||
var messageLimit = ParsePositiveLong(
|
||||
"SignalRFreeTierDailyMessageLimit", MaintenanceService.DefaultSignalRDailyMessageLimit);
|
||||
"SignalRDailyMessageLimit", MaintenanceService.DefaultSignalRDailyMessageLimit);
|
||||
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var startOfDayUtc = new DateTimeOffset(nowUtc.UtcDateTime.Date, TimeSpan.Zero);
|
||||
@@ -372,9 +372,9 @@ public class HealthCheckService
|
||||
|
||||
check.Message = worstTier switch
|
||||
{
|
||||
MaintenanceService.SignalRQuotaTier.Critical => "Free-tier limit nearly saturated — scale to Standard before clients get 429'd",
|
||||
MaintenanceService.SignalRQuotaTier.Warning => "Approaching free-tier limit — consider scaling to Standard",
|
||||
_ => "Within free-tier limits"
|
||||
MaintenanceService.SignalRQuotaTier.Critical => "Plan limit nearly saturated — add SignalR units before clients get 429'd",
|
||||
MaintenanceService.SignalRQuotaTier.Warning => "Approaching plan limit — consider adding SignalR units",
|
||||
_ => "Within plan limits"
|
||||
};
|
||||
check.Details = details;
|
||||
return check;
|
||||
|
||||
+16
-14
@@ -9,11 +9,13 @@ using Microsoft.Extensions.Logging;
|
||||
namespace AutopilotMonitor.Functions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// SignalR Free-tier quota watcher. Reads ConnectionCount (max over the last
|
||||
/// hour) and MessageCount (total since UTC midnight) from Azure Monitor and
|
||||
/// emits tiered OpsEvents when either approaches the documented Free-tier
|
||||
/// caps - 20 concurrent connections and 20,000 messages per day. The
|
||||
/// operator can then scale to Standard before clients get 429'd.
|
||||
/// SignalR quota watcher. Reads ConnectionCount (max over the last hour)
|
||||
/// and MessageCount (total since UTC midnight) from Azure Monitor and
|
||||
/// emits tiered OpsEvents when either approaches the plan caps - on
|
||||
/// Standard S1 that is 1,000 concurrent connections per unit (hard limit,
|
||||
/// excess connections are rejected) and 1M messages per unit per day
|
||||
/// (included quota; overage is billed, not throttled). The operator can
|
||||
/// then add units before clients get 429'd or overage costs accumulate.
|
||||
///
|
||||
/// Globally scoped (TenantId = null) - SignalR is one shared resource for
|
||||
/// the whole Function App, not a per-tenant concern.
|
||||
@@ -25,12 +27,12 @@ namespace AutopilotMonitor.Functions.Services
|
||||
internal const int SignalRQuotaCriticalPercent = 95;
|
||||
internal const int SignalRQuotaWarningPercent = 80;
|
||||
|
||||
// Default Free-tier caps as documented by Microsoft. Overridable via
|
||||
// app settings so an operator who has scaled to a paid tier can re-target
|
||||
// the watcher without a code change (or disable it by setting the cap
|
||||
// very high).
|
||||
internal const int DefaultSignalRConnectionLimit = 20;
|
||||
internal const long DefaultSignalRDailyMessageLimit = 20_000;
|
||||
// Default caps for Standard S1 with one unit, as documented by Microsoft.
|
||||
// Overridable via app settings so an operator who adds units or changes
|
||||
// tier can re-target the watcher without a code change (or disable it by
|
||||
// setting the cap very high).
|
||||
internal const int DefaultSignalRConnectionLimit = 1_000;
|
||||
internal const long DefaultSignalRDailyMessageLimit = 1_000_000;
|
||||
|
||||
// Dedup prefix - all SignalR EventTypes start with this, so the seen-index
|
||||
// can be built with a single Security-category read.
|
||||
@@ -59,7 +61,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
|
||||
/// <summary>
|
||||
/// Reads SignalR ConnectionCount + MessageCount metrics from Azure Monitor
|
||||
/// and emits Warning/Error OpsEvents when the Free-tier limits are
|
||||
/// and emits Warning/Error OpsEvents when the plan limits are
|
||||
/// approached. Dedup: one event per EventType per UTC day. Fail-soft -
|
||||
/// missing config or Azure Monitor errors yield a single log line, never
|
||||
/// a thrown exception.
|
||||
@@ -75,9 +77,9 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
|
||||
var connectionLimit = ParseIntSetting(
|
||||
"SignalRFreeTierConnectionLimit", DefaultSignalRConnectionLimit);
|
||||
"SignalRConnectionLimit", DefaultSignalRConnectionLimit);
|
||||
var messageLimit = ParseLongSetting(
|
||||
"SignalRFreeTierDailyMessageLimit", DefaultSignalRDailyMessageLimit);
|
||||
"SignalRDailyMessageLimit", DefaultSignalRDailyMessageLimit);
|
||||
|
||||
var seenToday = await BuildSignalRQuotaSeenIndexAsync();
|
||||
|
||||
|
||||
@@ -310,25 +310,25 @@ namespace AutopilotMonitor.Functions.Services
|
||||
|
||||
public Task RecordSignalRConnectionsHighAsync(int observed, int limit, int percent, string resourceId)
|
||||
=> WriteAsync(OpsEventCategory.Security, "SignalRConnectionsHigh", OpsEventSeverity.Warning,
|
||||
$"SignalR concurrent connections at {percent}% of free-tier limit ({observed}/{limit}) - watch for 429s; consider scaling to Standard before saturation",
|
||||
$"SignalR concurrent connections at {percent}% of plan limit ({observed}/{limit}) - watch for 429s; consider adding units before saturation",
|
||||
null, "System.Monitoring",
|
||||
new { metric = "ConnectionCount", aggregation = "Maximum", windowMinutes = 60, observed, limit, percent, resourceId });
|
||||
|
||||
public Task RecordSignalRConnectionsCriticalAsync(int observed, int limit, int percent, string resourceId)
|
||||
=> WriteAsync(OpsEventCategory.Security, "SignalRConnectionsCritical", OpsEventSeverity.Error,
|
||||
$"CRITICAL: SignalR concurrent connections at {percent}% of free-tier limit ({observed}/{limit}) - new client connections will be 429'd at 100%; scale to Standard now",
|
||||
$"CRITICAL: SignalR concurrent connections at {percent}% of plan limit ({observed}/{limit}) - new client connections will be 429'd at 100%; add units now",
|
||||
null, "System.Monitoring",
|
||||
new { metric = "ConnectionCount", aggregation = "Maximum", windowMinutes = 60, observed, limit, percent, resourceId });
|
||||
|
||||
public Task RecordSignalRMessagesHighAsync(long observed, long limit, int percent, string resourceId)
|
||||
=> WriteAsync(OpsEventCategory.Security, "SignalRMessagesHigh", OpsEventSeverity.Warning,
|
||||
$"SignalR daily message count at {percent}% of free-tier limit ({observed}/{limit}) - quota resets at 00:00 UTC; consider scaling if pattern persists",
|
||||
$"SignalR daily message count at {percent}% of included plan quota ({observed}/{limit}) - resets at 00:00 UTC; overage is billed per extra million messages",
|
||||
null, "System.Monitoring",
|
||||
new { metric = "MessageCount", aggregation = "Total", windowDay = "UTC", observed, limit, percent, resourceId });
|
||||
|
||||
public Task RecordSignalRMessagesCriticalAsync(long observed, long limit, int percent, string resourceId)
|
||||
=> WriteAsync(OpsEventCategory.Security, "SignalRMessagesCritical", OpsEventSeverity.Error,
|
||||
$"CRITICAL: SignalR daily message count at {percent}% of free-tier limit ({observed}/{limit}) - hub will throttle at 100% until 00:00 UTC reset; scale to Standard now",
|
||||
$"CRITICAL: SignalR daily message count at {percent}% of included plan quota ({observed}/{limit}) - overage beyond 100% is billed per extra million messages; review traffic or add units",
|
||||
null, "System.Monitoring",
|
||||
new { metric = "MessageCount", aggregation = "Total", windowDay = "UTC", observed, limit, percent, resourceId });
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"AzureTableStorageConnectionString": "UseDevelopmentStorage=true",
|
||||
"AzureBlobStorageConnectionString": "UseDevelopmentStorage=true",
|
||||
|
||||
"// SignalR free-tier quota watcher": "Optional. Set to the full Azure resource id of the SignalR Service to enable hourly OpsEvents when the 20-connections / 20k-messages-per-day free-tier limits are approached. Function App's managed identity needs 'Monitoring Reader' on this resource. Leave unset to skip the watcher.",
|
||||
"// SignalR quota watcher": "Optional. Set to the full Azure resource id of the SignalR Service to enable hourly OpsEvents when the plan limits are approached (defaults: Standard S1 x1 unit = 1000 connections / 1M messages per day; override the limits when adding units or changing tier). Function App's managed identity needs 'Monitoring Reader' on this resource. Leave unset to skip the watcher.",
|
||||
"SignalRResourceId": "",
|
||||
"SignalRFreeTierConnectionLimit": "20",
|
||||
"SignalRFreeTierDailyMessageLimit": "20000"
|
||||
"SignalRConnectionLimit": "1000",
|
||||
"SignalRDailyMessageLimit": "1000000"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const size = { width: 180, height: 180 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const alt = "AutopilotMonitor – Real-Time Windows Enrollment Monitoring";
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export { default, alt, size, contentType } from "./opengraph-image";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
+10
-7
@@ -2851,13 +2851,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
||||
"version": "2.10.43",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
|
||||
"integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
@@ -3002,9 +3005,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001769",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
|
||||
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
|
||||
"version": "1.0.30001806",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
|
||||
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
||||
Reference in New Issue
Block a user