diff --git a/Lite/Services/LocalDataService.CollectionHealth.cs b/Lite/Services/LocalDataService.CollectionHealth.cs index 6f4c1902..226a3177 100644 --- a/Lite/Services/LocalDataService.CollectionHealth.cs +++ b/Lite/Services/LocalDataService.CollectionHealth.cs @@ -31,8 +31,9 @@ public async Task> GetCollectionHealthAsync(int serverI AVG(duration_ms) AS avg_duration_ms, MAX(CASE WHEN status = 'SUCCESS' THEN collection_time END) AS last_success_time, MAX(collection_time) AS last_run_time, - MAX(CASE WHEN status = 'ERROR' THEN error_message END) AS last_error, - MAX(CASE WHEN status = 'ERROR' THEN collection_time END) AS last_error_time + MAX(CASE WHEN status IN ('ERROR', 'PERMISSIONS') THEN error_message END) AS last_error, + MAX(CASE WHEN status IN ('ERROR', 'PERMISSIONS') THEN collection_time END) AS last_error_time, + SUM(CASE WHEN status = 'PERMISSIONS' THEN 1 ELSE 0 END) AS permission_denied_count FROM collection_log WHERE server_id = $1 AND collection_time >= $2 @@ -56,7 +57,8 @@ GROUP BY collector_name LastSuccessTime = reader.IsDBNull(5) ? null : reader.GetDateTime(5), LastRunTime = reader.IsDBNull(6) ? null : reader.GetDateTime(6), LastError = reader.IsDBNull(7) ? null : reader.GetString(7), - LastErrorTime = reader.IsDBNull(8) ? null : reader.GetDateTime(8) + LastErrorTime = reader.IsDBNull(8) ? null : reader.GetDateTime(8), + PermissionDeniedCount = reader.IsDBNull(9) ? 0 : ToInt64(reader.GetValue(9)) }); } @@ -147,6 +149,7 @@ public class CollectorHealthRow public DateTime? LastRunTime { get; set; } public string? LastError { get; set; } public DateTime? LastErrorTime { get; set; } + public long PermissionDeniedCount { get; set; } public double FailureRatePercent => TotalRuns > 0 ? (double)ErrorCount / TotalRuns * 100 : 0; public double HoursSinceLastSuccess => LastSuccessTime.HasValue @@ -158,6 +161,7 @@ public string HealthStatus get { if (TotalRuns == 0) return "NEVER_RUN"; + if (PermissionDeniedCount > 0 && ErrorCount == 0 && SuccessCount == 0) return "NO_PERMISSIONS"; if (HoursSinceLastSuccess > 24) return "FAILING"; if (HoursSinceLastSuccess > 4) return "STALE"; if (FailureRatePercent > 20) return "WARNING"; diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index ea1e79a2..8b71b154 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -153,7 +153,7 @@ public CollectorHealthSummary GetHealthSummary(int? serverId = null) /// /// Records a collector execution result for health tracking. /// - private void RecordCollectorResult(int serverId, string collectorName, bool success, string? errorMessage = null) + private void RecordCollectorResult(int serverId, string collectorName, string status, string? errorMessage = null) { lock (_healthLock) { @@ -164,12 +164,20 @@ private void RecordCollectorResult(int serverId, string collectorName, bool succ _collectorHealth[key] = entry; } - if (success) + if (status == "SUCCESS") { entry.LastSuccessTime = DateTime.UtcNow; entry.ConsecutiveErrors = 0; entry.TotalSuccesses++; } + else if (status == "PERMISSIONS") + { + /* Permission errors are not transient — don't count as failures + (which would show FAILING) but don't count as success either. + Record the error message so the user can see what's wrong. */ + entry.LastErrorTime = DateTime.UtcNow; + entry.LastErrorMessage = errorMessage; + } else { entry.LastErrorTime = DateTime.UtcNow; @@ -337,6 +345,7 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam } else if (ex.Number == 229 || ex.Number == 297 || ex.Number == 300) { + status = "PERMISSIONS"; _logger?.LogWarning("Collector '{Collector}' permission denied for server '{Server}': {Message}", collectorName, server.DisplayName, ex.Message); } @@ -369,7 +378,7 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam } // Track collector health - RecordCollectorResult(GetServerId(server), collectorName, status == "SUCCESS", errorMessage); + RecordCollectorResult(GetServerId(server), collectorName, status, errorMessage); // Log the collection attempt await LogCollectionAsync(GetServerId(server), server.DisplayName, collectorName, startTime, status, errorMessage, rowsCollected, _lastSqlMs, _lastDuckDbMs);