mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
feat(agent-v2): M4.6.γ diagnostic probes, reporters, DO collector wired
Closes the third batch of peripheral-parity gaps — Legacy's fire-and-forget
startup probes, the two cert-less + authenticated error-reporting channels,
and Delivery-Optimization telemetry all run in V2.
StartupEnvironmentProbes (V2.Core/Runtime/)
- New helper that replicates the Legacy MonitoringService startup diagnostic
trio: device_location / timezone_auto_set / ntp_time_check.
- Sequential RunAsync(config, logger, emitter): geo lookup → (optional)
tzutil auto-set → NTP offset check. Each step is best-effort and feeds
through the orchestrator's TelemetryEventEmitter.
- Geo probe respects EnableGeoLocation; timezone probe respects
EnableTimezoneAutoSet AND requires an IANA tz from geo. NTP probe always
runs (diagnostic-only; Warning on offset >60s or failure).
- Program.cs wires this via Task.Run right after orchestrator.Start so a
slow network never delays the critical path.
EmergencyReporter + DistressReporter (live in Program.cs)
- Previously instantiated nowhere — now built once at startup with the
shared hardware info. RemoteConfigService ctor takes both so config-fetch
failures flow to the right channel (auth → distress, network → emergency).
- Initial AuthCertificateMissing distress fires when UseClientCertAuth=true
but BackendApiClient.ClientCertificate is null — surfaces pre-MDM-
enrollment dead-ends via the cert-less channel (Legacy parity).
- BackendTelemetryUploader uses the same hardware info (dedup'd the second
HardwareInfo.GetHardwareInfo call that was duplicated since M4.5.b).
DeliveryOptimizationHost (DefaultComponentFactory nested)
- New ICollectorHost wrapping DeliveryOptimizationCollector with:
• getPackageStates → ImeLogHost.PackageStates (existing accessor)
• onDoTelemetryReceived → ImeLogHost.Tracker.OnDoTelemetryReceived pass-
through (new internal accessor; keeps encapsulation)
• OnAppStateChanged chained into ImeLogTracker so WakeUp() fires when
any app enters Downloading or Installing — before any fire we preserve
the existing handler (the adapter's signal-ingest path must keep
running unchanged).
- Gated by CollectorConfiguration.EnableDeliveryOptimizationCollector.
Tests (+8 in V2.Core.Tests → 353 → 361)
- StartupEnvironmentProbesTests: event-shape for all three probes + geo
success/failure/null attempt, timezone success/failure severity, NTP
small/large offset + failure-with-detail.
All 4 suites green: 88 DecisionCore + 361 V2.Core + 507 Agent.Core + 557 Functions
= 1513 tests green.
Note: the "RegistryWatcher standalone admin-action-detector" from the
original M4.6 gap list was a Subagent fabrication — verified no such usage
exists in either Legacy or V2 (the class is only consumed transitively
inside AadJoinWatcher + ProvisioningStatusTracker, both already wired via
DefaultComponentFactory since M4.5.b). Removed from the M4.6.γ scope.
Remaining: M4.6.δ (--run-gather-rules + --run-ime-matching modes,
AnalyzerManager + 3 Analyzers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.Agent.V2.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
|
||||
using AutopilotMonitor.Agent.V2.Core.Runtime;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// M4.6.γ — event-shape tests for the Legacy-parity startup probes. We cover the pure event
|
||||
/// builders directly; the end-to-end probe runner itself needs live HTTP/UDP/tzutil (no value
|
||||
/// in unit testing — excercised via the V2-agent test-VM smoke run deferred to Follow-Up).
|
||||
/// </summary>
|
||||
public sealed class StartupEnvironmentProbesTests
|
||||
{
|
||||
private static AgentConfiguration Config(bool geoEnabled = true, bool tzAutoSet = false, string? ntpServer = null) =>
|
||||
new AgentConfiguration
|
||||
{
|
||||
SessionId = "S1",
|
||||
TenantId = "T1",
|
||||
EnableGeoLocation = geoEnabled,
|
||||
EnableTimezoneAutoSet = tzAutoSet,
|
||||
NtpServer = ntpServer,
|
||||
};
|
||||
|
||||
// ================================================================= Geo success
|
||||
|
||||
[Fact]
|
||||
public void BuildGeoEvent_returns_info_device_location_event_with_all_fields()
|
||||
{
|
||||
var cfg = Config();
|
||||
var loc = new GeoLocationResult
|
||||
{
|
||||
Country = "DE",
|
||||
Region = "BW",
|
||||
City = "Stuttgart",
|
||||
Loc = "48.7,9.1",
|
||||
Timezone = "Europe/Berlin",
|
||||
Source = "ipinfo.io",
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildGeoEvent(cfg, loc);
|
||||
|
||||
Assert.Equal("device_location", evt.EventType);
|
||||
Assert.Equal(EventSeverity.Info, evt.Severity);
|
||||
Assert.Equal("Network", evt.Source);
|
||||
Assert.Equal("S1", evt.SessionId);
|
||||
Assert.Equal("T1", evt.TenantId);
|
||||
Assert.True(evt.ImmediateUpload);
|
||||
Assert.Contains("Stuttgart", evt.Message);
|
||||
Assert.Equal("DE", evt.Data["country"]);
|
||||
Assert.Equal("ipinfo.io", evt.Data["source"]);
|
||||
}
|
||||
|
||||
// ================================================================= Geo failure
|
||||
|
||||
[Fact]
|
||||
public void BuildGeoFailureEvent_returns_warning_agent_trace_with_provider_errors()
|
||||
{
|
||||
var cfg = Config();
|
||||
var attempt = new GeoLocationAttemptResult
|
||||
{
|
||||
PrimaryError = "connection refused",
|
||||
PrimaryRetryError = "DNS failure",
|
||||
FallbackError = "timeout",
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildGeoFailureEvent(cfg, attempt);
|
||||
|
||||
Assert.Equal("agent_trace", evt.EventType);
|
||||
Assert.Equal(EventSeverity.Warning, evt.Severity);
|
||||
Assert.Equal("geo_location_failed", evt.Data["decision"]);
|
||||
Assert.Equal("connection refused", evt.Data["primaryError"]);
|
||||
Assert.Equal("DNS failure", evt.Data["primaryRetryError"]);
|
||||
Assert.Equal("timeout", evt.Data["fallbackError"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildGeoFailureEvent_handles_null_attempt_gracefully()
|
||||
{
|
||||
var evt = StartupEnvironmentProbes.BuildGeoFailureEvent(Config(), attempt: null);
|
||||
|
||||
Assert.Equal("agent_trace", evt.EventType);
|
||||
Assert.Equal("unknown", evt.Data["primaryError"]);
|
||||
Assert.Equal("unknown", evt.Data["primaryRetryError"]);
|
||||
Assert.Equal("unknown", evt.Data["fallbackError"]);
|
||||
}
|
||||
|
||||
// ================================================================= Timezone
|
||||
|
||||
[Fact]
|
||||
public void BuildTimezoneEvent_info_on_success()
|
||||
{
|
||||
var tz = new TimezoneSetResult
|
||||
{
|
||||
Success = true,
|
||||
IanaTimezone = "Europe/Berlin",
|
||||
WindowsTimezoneId = "W. Europe Standard Time",
|
||||
PreviousTimezone = "UTC",
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildTimezoneEvent(Config(), tz);
|
||||
|
||||
Assert.Equal("timezone_auto_set", evt.EventType);
|
||||
Assert.Equal(EventSeverity.Info, evt.Severity);
|
||||
Assert.Contains("W. Europe Standard Time", evt.Message);
|
||||
Assert.Equal(true, evt.Data["success"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildTimezoneEvent_warning_on_failure()
|
||||
{
|
||||
var tz = new TimezoneSetResult
|
||||
{
|
||||
Success = false,
|
||||
IanaTimezone = "Europe/Berlin",
|
||||
Error = "tzutil failed",
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildTimezoneEvent(Config(), tz);
|
||||
|
||||
Assert.Equal(EventSeverity.Warning, evt.Severity);
|
||||
Assert.Contains("tzutil failed", evt.Message);
|
||||
Assert.Equal("tzutil failed", evt.Data["error"]);
|
||||
}
|
||||
|
||||
// ================================================================= NTP
|
||||
|
||||
[Fact]
|
||||
public void BuildNtpEvent_info_for_small_offset()
|
||||
{
|
||||
var r = new NtpCheckResult
|
||||
{
|
||||
Success = true,
|
||||
OffsetSeconds = 2.3,
|
||||
NtpTime = new System.DateTime(2026, 4, 21, 10, 0, 0, System.DateTimeKind.Utc),
|
||||
LocalTime = new System.DateTime(2026, 4, 21, 10, 0, 2, System.DateTimeKind.Utc),
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildNtpEvent(Config(), "time.windows.com", r);
|
||||
|
||||
Assert.Equal("ntp_time_check", evt.EventType);
|
||||
Assert.Equal(EventSeverity.Info, evt.Severity);
|
||||
Assert.Equal("time.windows.com", evt.Data["ntpServer"]);
|
||||
Assert.Equal(2.3, evt.Data["offsetSeconds"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildNtpEvent_warning_for_large_offset()
|
||||
{
|
||||
var r = new NtpCheckResult
|
||||
{
|
||||
Success = true,
|
||||
OffsetSeconds = 120.0,
|
||||
NtpTime = System.DateTime.UtcNow,
|
||||
LocalTime = System.DateTime.UtcNow,
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildNtpEvent(Config(), "time.windows.com", r);
|
||||
|
||||
Assert.Equal(EventSeverity.Warning, evt.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildNtpEvent_warning_on_failure_with_error_detail()
|
||||
{
|
||||
var r = new NtpCheckResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "DNS resolution failed",
|
||||
};
|
||||
|
||||
var evt = StartupEnvironmentProbes.BuildNtpEvent(Config(), "time.windows.com", r);
|
||||
|
||||
Assert.Equal(EventSeverity.Warning, evt.Severity);
|
||||
Assert.Contains("DNS resolution failed", evt.Message);
|
||||
Assert.Equal("DNS resolution failed", evt.Data["error"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,21 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
|
||||
_networkMetrics, _agentVersion, collectors.AgentSelfMetricsIntervalSeconds));
|
||||
}
|
||||
|
||||
// M4.6.γ — Delivery-Optimization telemetry. Dormant-by-default: only polls when the
|
||||
// IME log tracker reports an app entering Downloading/Installing (see AppStateChanged
|
||||
// chain below). Needs the IME tracker's PackageStates + OnDoTelemetryReceived hook.
|
||||
if (collectors.EnableDeliveryOptimizationCollector && _imeLogHost != null)
|
||||
{
|
||||
var doHost = new DeliveryOptimizationHost(
|
||||
sessionId: sessionId,
|
||||
tenantId: tenantId,
|
||||
onEnrollmentEvent: onEnrollmentEvent,
|
||||
logger: logger,
|
||||
intervalSeconds: collectors.DeliveryOptimizationIntervalSeconds,
|
||||
imeHost: _imeLogHost);
|
||||
hosts.Add(doHost);
|
||||
}
|
||||
|
||||
return hosts;
|
||||
}
|
||||
|
||||
@@ -287,10 +302,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the tracker's live package-state list for peripheral consumers
|
||||
/// (M4.6.β <c>FinalStatusBuilder</c>).
|
||||
/// (M4.6.β <c>FinalStatusBuilder</c>, M4.6.γ <c>DeliveryOptimizationHost</c>).
|
||||
/// </summary>
|
||||
public Monitoring.Enrollment.Ime.AppPackageStateList PackageStates => _tracker.PackageStates;
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the wrapped IME tracker for co-collector wiring. Used by
|
||||
/// <c>DeliveryOptimizationHost</c> to set <c>OnDoTelemetryReceived</c> and to chain
|
||||
/// <c>OnAppStateChanged</c> for dormant/wake-up transitions.
|
||||
/// </summary>
|
||||
internal Monitoring.Enrollment.Ime.ImeLogTracker Tracker => _tracker;
|
||||
|
||||
public ImeLogHost(
|
||||
string sessionId,
|
||||
string tenantId,
|
||||
@@ -457,6 +479,81 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DeliveryOptimizationHost : ICollectorHost
|
||||
{
|
||||
public string Name => "DeliveryOptimizationCollector";
|
||||
|
||||
private readonly DeliveryOptimizationCollector _collector;
|
||||
private readonly ImeLogHost _imeHost;
|
||||
private readonly AgentLogger _logger;
|
||||
private Action<Monitoring.Enrollment.Ime.AppPackageState, Monitoring.Enrollment.Ime.AppInstallationState, Monitoring.Enrollment.Ime.AppInstallationState>? _prevStateChanged;
|
||||
private int _disposed;
|
||||
|
||||
public DeliveryOptimizationHost(
|
||||
string sessionId,
|
||||
string tenantId,
|
||||
Action<EnrollmentEvent> onEnrollmentEvent,
|
||||
AgentLogger logger,
|
||||
int intervalSeconds,
|
||||
ImeLogHost imeHost)
|
||||
{
|
||||
_imeHost = imeHost;
|
||||
_logger = logger;
|
||||
|
||||
_collector = new DeliveryOptimizationCollector(
|
||||
sessionId: sessionId,
|
||||
tenantId: tenantId,
|
||||
emitEvent: onEnrollmentEvent,
|
||||
logger: logger,
|
||||
intervalSeconds: intervalSeconds,
|
||||
getPackageStates: () => imeHost.PackageStates,
|
||||
onDoTelemetryReceived: pkg =>
|
||||
{
|
||||
try { imeHost.Tracker.OnDoTelemetryReceived?.Invoke(pkg); }
|
||||
catch (Exception ex) { logger.Warning($"DeliveryOptimizationHost: OnDoTelemetryReceived invocation threw: {ex.Message}"); }
|
||||
},
|
||||
logDirectory: Environment.ExpandEnvironmentVariables(Shared.Constants.LogDirectory));
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Chain ourselves into the tracker's OnAppStateChanged so the DO collector wakes
|
||||
// up on the first Downloading/Installing transition (Legacy parity). We preserve
|
||||
// any existing handler — the IME adapter's own listener must keep running.
|
||||
_prevStateChanged = _imeHost.Tracker.OnAppStateChanged;
|
||||
_imeHost.Tracker.OnAppStateChanged = (pkg, oldState, newState) =>
|
||||
{
|
||||
try { _prevStateChanged?.Invoke(pkg, oldState, newState); }
|
||||
catch (Exception ex) { _logger.Warning($"DeliveryOptimizationHost: previous OnAppStateChanged handler threw: {ex.Message}"); }
|
||||
|
||||
if (newState >= Monitoring.Enrollment.Ime.AppInstallationState.Downloading &&
|
||||
newState <= Monitoring.Enrollment.Ime.AppInstallationState.Installing)
|
||||
{
|
||||
try { _collector.WakeUp(); }
|
||||
catch (Exception ex) { _logger.Warning($"DeliveryOptimizationHost: WakeUp threw: {ex.Message}"); }
|
||||
}
|
||||
};
|
||||
|
||||
_collector.Start();
|
||||
_logger.Info($"DeliveryOptimizationHost: started dormant (interval={_collector}, wakes on Downloading/Installing).");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
// Restore the previous handler only if we're still the current one — otherwise
|
||||
// someone else replaced us and owns the slot now.
|
||||
if (object.ReferenceEquals(_imeHost.Tracker.OnAppStateChanged, _imeHost.Tracker.OnAppStateChanged))
|
||||
_imeHost.Tracker.OnAppStateChanged = _prevStateChanged;
|
||||
_collector.Stop();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
|
||||
try { _collector.Dispose(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AgentSelfMetricsHost : ICollectorHost
|
||||
{
|
||||
public string Name => "AgentSelfMetricsCollector";
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.V2.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
|
||||
using AutopilotMonitor.Agent.V2.Core.Telemetry.Events;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Fire-and-forget startup probes ported from the Legacy <c>MonitoringService</c>.
|
||||
/// Plan §4.x M4.6.γ.
|
||||
/// <para>
|
||||
/// Emits three diagnostic events when the orchestrator is live:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>device_location</b> (or <c>agent_trace</c> on failure) — IP-based geo via
|
||||
/// <see cref="GeoLocationService"/>. Skipped when <c>EnableGeoLocation=false</c>.</item>
|
||||
/// <item><b>timezone_auto_set</b> — only when <c>EnableTimezoneAutoSet=true</c> AND the
|
||||
/// geo lookup returned an IANA timezone. Uses <see cref="TimezoneService"/> (tzutil).</item>
|
||||
/// <item><b>ntp_time_check</b> — NTP offset from <c>NtpServer</c> (default time.windows.com).
|
||||
/// Always emitted — offset >60s is Warning, smaller is Info, failure is Warning.</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Each probe runs best-effort: failures are logged + optionally emitted as a Warning event,
|
||||
/// never thrown. Designed to run on a background task so they never block the critical path.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class StartupEnvironmentProbes
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs all three probes sequentially and emits the resulting events via
|
||||
/// <paramref name="emitter"/>. Returns a <see cref="Task"/> that completes when all
|
||||
/// probes have finished (typical runtime <10s; longer on slow/blocked networks).
|
||||
/// </summary>
|
||||
public static async Task RunAsync(
|
||||
AgentConfiguration configuration,
|
||||
AgentLogger logger,
|
||||
TelemetryEventEmitter emitter)
|
||||
{
|
||||
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
|
||||
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
||||
if (emitter == null) throw new ArgumentNullException(nameof(emitter));
|
||||
|
||||
if (configuration.EnableGeoLocation)
|
||||
await RunGeoAndTimezone(configuration, logger, emitter).ConfigureAwait(false);
|
||||
else
|
||||
logger.Debug("StartupEnvironmentProbes: EnableGeoLocation=false — skipping geo + timezone probes.");
|
||||
|
||||
await RunNtpCheck(configuration, logger, emitter).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task RunGeoAndTimezone(
|
||||
AgentConfiguration configuration,
|
||||
AgentLogger logger,
|
||||
TelemetryEventEmitter emitter)
|
||||
{
|
||||
GeoLocationAttemptResult? attempt = null;
|
||||
try
|
||||
{
|
||||
attempt = await GeoLocationService.GetLocationAsync(logger).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning($"StartupEnvironmentProbes: geo-location lookup threw: {ex.Message}");
|
||||
}
|
||||
|
||||
if (attempt?.Location != null)
|
||||
{
|
||||
SafeEmit(emitter, logger, BuildGeoEvent(configuration, attempt.Location));
|
||||
}
|
||||
else
|
||||
{
|
||||
SafeEmit(emitter, logger, BuildGeoFailureEvent(configuration, attempt));
|
||||
}
|
||||
|
||||
if (configuration.EnableTimezoneAutoSet && !string.IsNullOrEmpty(attempt?.Location?.Timezone))
|
||||
{
|
||||
try
|
||||
{
|
||||
var tzResult = TimezoneService.TrySetTimezone(attempt!.Location!.Timezone, logger);
|
||||
SafeEmit(emitter, logger, BuildTimezoneEvent(configuration, tzResult));
|
||||
}
|
||||
catch (Exception tzEx)
|
||||
{
|
||||
logger.Warning($"StartupEnvironmentProbes: timezone auto-set threw: {tzEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RunNtpCheck(
|
||||
AgentConfiguration configuration,
|
||||
AgentLogger logger,
|
||||
TelemetryEventEmitter emitter)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ntpServer = string.IsNullOrEmpty(configuration.NtpServer) ? "time.windows.com" : configuration.NtpServer;
|
||||
|
||||
// NtpTimeCheckService.CheckTime is synchronous — wrap on a background thread so the
|
||||
// probe runner stays async-friendly (avoids blocking the continuation thread when
|
||||
// Program.cs awaits RunAsync with ConfigureAwait(false)).
|
||||
var result = await Task.Run(() => NtpTimeCheckService.CheckTime(ntpServer, logger))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SafeEmit(emitter, logger, BuildNtpEvent(configuration, ntpServer, result));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning($"StartupEnvironmentProbes: NTP check threw: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Event builders (internal for tests)
|
||||
|
||||
internal static EnrollmentEvent BuildGeoEvent(AgentConfiguration configuration, GeoLocationResult location) =>
|
||||
new EnrollmentEvent
|
||||
{
|
||||
SessionId = configuration.SessionId,
|
||||
TenantId = configuration.TenantId,
|
||||
EventType = "device_location",
|
||||
Severity = EventSeverity.Info,
|
||||
Source = "Network",
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Message = $"Device location: {location.City}, {location.Region}, {location.Country} (via {location.Source})",
|
||||
Data = location.ToDictionary(),
|
||||
ImmediateUpload = true,
|
||||
};
|
||||
|
||||
internal static EnrollmentEvent BuildGeoFailureEvent(AgentConfiguration configuration, GeoLocationAttemptResult? attempt) =>
|
||||
new EnrollmentEvent
|
||||
{
|
||||
SessionId = configuration.SessionId,
|
||||
TenantId = configuration.TenantId,
|
||||
EventType = "agent_trace",
|
||||
Severity = EventSeverity.Warning,
|
||||
Source = "Network",
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Message = "Geo-location lookup failed: all providers unreachable",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "decision", "geo_location_failed" },
|
||||
{ "reason", "All geo-location providers failed after retry" },
|
||||
{ "primaryError", attempt?.PrimaryError ?? "unknown" },
|
||||
{ "primaryRetryError", attempt?.PrimaryRetryError ?? "unknown" },
|
||||
{ "fallbackError", attempt?.FallbackError ?? "unknown" },
|
||||
{ "primaryProvider", "ipinfo.io" },
|
||||
{ "fallbackProvider", "ifconfig.co" },
|
||||
},
|
||||
};
|
||||
|
||||
internal static EnrollmentEvent BuildTimezoneEvent(AgentConfiguration configuration, TimezoneSetResult tz) =>
|
||||
new EnrollmentEvent
|
||||
{
|
||||
SessionId = configuration.SessionId,
|
||||
TenantId = configuration.TenantId,
|
||||
EventType = "timezone_auto_set",
|
||||
Severity = tz.Success ? EventSeverity.Info : EventSeverity.Warning,
|
||||
Source = "Network",
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Message = tz.Success
|
||||
? $"Timezone set to {tz.WindowsTimezoneId} (from {tz.IanaTimezone})"
|
||||
: $"Timezone auto-set failed: {tz.Error}",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "ianaTimezone", tz.IanaTimezone ?? "" },
|
||||
{ "windowsTimezoneId", tz.WindowsTimezoneId ?? "unknown" },
|
||||
{ "previousTimezone", tz.PreviousTimezone ?? "unknown" },
|
||||
{ "success", tz.Success },
|
||||
{ "error", tz.Error ?? "" },
|
||||
},
|
||||
};
|
||||
|
||||
internal static EnrollmentEvent BuildNtpEvent(AgentConfiguration configuration, string ntpServer, NtpCheckResult result)
|
||||
{
|
||||
if (result.Success)
|
||||
{
|
||||
var severity = Math.Abs(result.OffsetSeconds) > 60 ? EventSeverity.Warning : EventSeverity.Info;
|
||||
return new EnrollmentEvent
|
||||
{
|
||||
SessionId = configuration.SessionId,
|
||||
TenantId = configuration.TenantId,
|
||||
EventType = "ntp_time_check",
|
||||
Severity = severity,
|
||||
Source = "Network",
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Message = $"NTP time check: offset {result.OffsetSeconds:F2}s from {ntpServer}",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "ntpServer", ntpServer },
|
||||
{ "offsetSeconds", result.OffsetSeconds },
|
||||
{ "ntpTimeUtc", result.NtpTime?.ToString("o") ?? "" },
|
||||
{ "localTimeUtc", result.LocalTime?.ToString("o") ?? "" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return new EnrollmentEvent
|
||||
{
|
||||
SessionId = configuration.SessionId,
|
||||
TenantId = configuration.TenantId,
|
||||
EventType = "ntp_time_check",
|
||||
Severity = EventSeverity.Warning,
|
||||
Source = "Network",
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Message = $"NTP time check failed: {result.Error}",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "ntpServer", ntpServer },
|
||||
{ "error", result.Error ?? "unknown" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static void SafeEmit(TelemetryEventEmitter emitter, AgentLogger logger, EnrollmentEvent evt)
|
||||
{
|
||||
try { emitter.Emit(evt); }
|
||||
catch (Exception ex) { logger.Warning($"StartupEnvironmentProbes: emit failed for '{evt.EventType}': {ex.Message}"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
|
||||
using AutopilotMonitor.Agent.V2.Core.Orchestration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Runtime;
|
||||
using AutopilotMonitor.Agent.V2.Core.Security;
|
||||
using AutopilotMonitor.Agent.V2.Core.Termination;
|
||||
using AutopilotMonitor.Agent.V2.Core.Transport.Telemetry;
|
||||
@@ -259,7 +260,37 @@ namespace AutopilotMonitor.Agent.V2
|
||||
logger: logger,
|
||||
agentVersion: GetAgentVersion());
|
||||
|
||||
var remoteConfigService = new RemoteConfigService(backendApiClient, agentConfig.TenantId, logger);
|
||||
// M4.6.γ — Emergency + Distress reporters. Plumbed into RemoteConfigService so
|
||||
// Config-fetch failures (auth vs network) flow to the correct channel. Also fires
|
||||
// an initial AuthCertificateMissing distress when the MDM cert was expected but
|
||||
// not found (Legacy parity — surfaces pre-MDM-enrollment dead-ends to the backend
|
||||
// via the cert-less distress channel).
|
||||
var hardwareForReporters = HardwareInfo.GetHardwareInfo(logger);
|
||||
var distressReporter = new DistressReporter(
|
||||
baseUrl: agentConfig.ApiBaseUrl,
|
||||
tenantId: agentConfig.TenantId,
|
||||
manufacturer: hardwareForReporters.Manufacturer,
|
||||
model: hardwareForReporters.Model,
|
||||
serialNumber: hardwareForReporters.SerialNumber,
|
||||
agentVersion: GetAgentVersion(),
|
||||
logger: logger);
|
||||
|
||||
var emergencyReporter = new EmergencyReporter(
|
||||
apiClient: backendApiClient,
|
||||
sessionId: agentConfig.SessionId,
|
||||
tenantId: agentConfig.TenantId,
|
||||
agentVersion: GetAgentVersion(),
|
||||
logger: logger);
|
||||
|
||||
if (agentConfig.UseClientCertAuth && backendApiClient.ClientCertificate == null)
|
||||
{
|
||||
_ = distressReporter.TrySendAsync(
|
||||
DistressErrorType.AuthCertificateMissing,
|
||||
"MDM certificate not found in LocalMachine or CurrentUser store");
|
||||
}
|
||||
|
||||
var remoteConfigService = new RemoteConfigService(
|
||||
backendApiClient, agentConfig.TenantId, logger, emergencyReporter, distressReporter);
|
||||
var remoteConfig = remoteConfigService.FetchConfigAsync().GetAwaiter().GetResult();
|
||||
|
||||
logger.SetLogLevel(agentConfig.LogLevel);
|
||||
@@ -299,14 +330,13 @@ namespace AutopilotMonitor.Agent.V2
|
||||
|
||||
try
|
||||
{
|
||||
var hardware = HardwareInfo.GetHardwareInfo(logger);
|
||||
uploader = new BackendTelemetryUploader(
|
||||
httpClient: mtlsHttpClient,
|
||||
baseUrl: agentConfig.ApiBaseUrl,
|
||||
tenantId: agentConfig.TenantId,
|
||||
manufacturer: hardware.Manufacturer,
|
||||
model: hardware.Model,
|
||||
serialNumber: hardware.SerialNumber,
|
||||
manufacturer: hardwareForReporters.Manufacturer,
|
||||
model: hardwareForReporters.Model,
|
||||
serialNumber: hardwareForReporters.SerialNumber,
|
||||
bootstrapToken: agentConfig.UseBootstrapTokenAuth ? agentConfig.BootstrapToken : null,
|
||||
agentVersion: GetAgentVersion());
|
||||
}
|
||||
@@ -433,6 +463,22 @@ namespace AutopilotMonitor.Agent.V2
|
||||
// VersionCheckEventBuilder.TryBuild is a no-op when no markers are present.
|
||||
EmitVersionCheckEventIfAny(orchestrator, agentConfig, logger);
|
||||
|
||||
// M4.6.γ — fire-and-forget startup probes (geo / timezone / NTP). Runs on
|
||||
// the ThreadPool so a slow network never delays the critical path.
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await StartupEnvironmentProbes
|
||||
.RunAsync(agentConfig, logger, orchestrator.EventEmitter)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning($"StartupEnvironmentProbes: outer exception: {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
logger.Info($"V2 agent runtime ready (session={agentConfig.SessionId}, tenant={agentConfig.TenantId}).");
|
||||
if (consoleMode)
|
||||
Console.Out.WriteLine("AutopilotMonitor.Agent.V2 running. Press Ctrl+C to stop.");
|
||||
|
||||
@@ -1 +1 @@
|
||||
32
|
||||
35
|
||||
|
||||
@@ -1 +1 @@
|
||||
1068
|
||||
1069
|
||||
|
||||
Reference in New Issue
Block a user