agent resilliency

This commit is contained in:
Oliver Kieselbach
2026-03-02 13:50:46 +01:00
parent 5d058bf84c
commit 2de8022c7b
6 changed files with 34 additions and 5 deletions
@@ -19,6 +19,7 @@ namespace AutopilotMonitor.Agent.Core.Configuration
private readonly string _tenantId;
private readonly AgentLogger _logger;
private readonly string _cacheFilePath;
private readonly EmergencyReporter _emergencyReporter;
private AgentConfigResponse _currentConfig;
private readonly object _configLock = new object();
@@ -37,11 +38,12 @@ namespace AutopilotMonitor.Agent.Core.Configuration
}
}
public RemoteConfigService(BackendApiClient apiClient, string tenantId, AgentLogger logger)
public RemoteConfigService(BackendApiClient apiClient, string tenantId, AgentLogger logger, EmergencyReporter emergencyReporter = null)
{
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
_tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_emergencyReporter = emergencyReporter;
var cacheDir = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Config");
_cacheFilePath = Path.Combine(cacheDir, "remote-config.json");
@@ -74,6 +76,14 @@ namespace AutopilotMonitor.Agent.Core.Configuration
catch (Exception ex)
{
_logger.Warning($"Failed to fetch remote config: {ex.Message}");
// Report to emergency channel if available (not available in --run-gather-rules mode)
if (_emergencyReporter != null)
{
_ = _emergencyReporter.TrySendAsync(
AgentErrorType.ConfigFetchFailed,
ex.Message);
}
}
// Fall back to cached config
@@ -304,8 +304,17 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
return (true, null);
}
lastErrorCode = $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}";
var statusCode = (int)response.StatusCode;
lastErrorCode = $"HTTP {statusCode} {response.ReasonPhrase}";
var responseBody = await response.Content.ReadAsStringAsync();
// Auth errors (401/403) are permanent — SAS token invalid or expired, retrying won't help
if (statusCode == 401 || statusCode == 403)
{
_logger.Warning($"Blob upload auth error (not retryable): {lastErrorCode} - {responseBody}");
return (false, lastErrorCode);
}
_logger.Warning($"Blob upload attempt {attempt} failed: {lastErrorCode} - {responseBody}");
}
catch (Exception ex)
@@ -252,7 +252,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
{
try
{
_remoteConfigService = new RemoteConfigService(_apiClient, _configuration.TenantId, _logger);
_remoteConfigService = new RemoteConfigService(_apiClient, _configuration.TenantId, _logger, _emergencyReporter);
_remoteConfigService.FetchConfigAsync().Wait(TimeSpan.FromSeconds(15));
ApplyRuntimeSettingsFromRemoteConfig();
}
@@ -546,6 +546,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
catch (Exception ex)
{
_logger.Error("Failed to register session", ex);
// Report to emergency channel — one-shot, no threshold needed.
// EmergencyReporter dedup ensures RegisterSessionFailed:null sent only once per session.
_ = _emergencyReporter.TrySendAsync(
AgentErrorType.RegisterSessionFailed,
ex.Message);
}
}
@@ -115,6 +115,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
var ndjson = CreateNdjson(request);
var compressedContent = CompressWithGzip(ndjson);
_logger?.Debug($"IngestEventsAsync: POST {url} — {request.Events.Count} events, {compressedContent.Length} bytes compressed");
var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new ByteArrayContent(compressedContent)
@@ -165,6 +167,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
AddSecurityHeaders(httpRequest);
_logger?.Debug($"GetAgentConfigAsync: GET {url}");
var sw = Stopwatch.StartNew();
var failed = false;
long bytesDown = 0;
@@ -17,7 +17,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
///
/// A failure in this reporter is silently swallowed — it must never cascade into the main loop.
/// </summary>
internal class EmergencyReporter
public class EmergencyReporter
{
/// <summary>
/// Number of consecutive upload failures required before the first emergency report is sent.
@@ -1 +1 @@
159
166