FailoverApp: implemented under maintanence feature to indicate if an address is taken down for maintenance. Code refactoring done.

This commit is contained in:
Shreyas Zare
2021-09-01 18:00:09 +05:30
parent 9953db90c0
commit 8d85c9bbae
10 changed files with 333 additions and 202 deletions

View File

@@ -78,11 +78,17 @@ namespace Failover
if (address.AddressFamily == AddressFamily.InterNetwork) if (address.AddressFamily == AddressFamily.InterNetwork)
{ {
HealthCheckStatus status = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true); HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true);
if (status is null) switch (response.Status)
{
case HealthStatus.Unknown:
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecord(address))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecord(address)));
else if (status.IsHealthy) break;
case HealthStatus.Healthy:
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecord(address))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecord(address)));
break;
}
} }
} }
break; break;
@@ -94,11 +100,17 @@ namespace Failover
if (address.AddressFamily == AddressFamily.InterNetworkV6) if (address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
HealthCheckStatus status = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true); HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true);
if (status is null) switch (response.Status)
{
case HealthStatus.Unknown:
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecord(address))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecord(address)));
else if (status.IsHealthy) break;
case HealthStatus.Healthy:
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecord(address))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecord(address)));
break;
}
} }
} }
break; break;
@@ -113,16 +125,12 @@ namespace Failover
foreach (dynamic jsonAddress in jsonAddresses) foreach (dynamic jsonAddress in jsonAddresses)
{ {
IPAddress address = IPAddress.Parse(jsonAddress.Value); IPAddress address = IPAddress.Parse(jsonAddress.Value);
HealthCheckStatus status = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false); HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false);
string text = "app=failover; addressType=" + type.ToString() + "; address=" + address.ToString() + "; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri); string text = "app=failover; addressType=" + type.ToString() + "; address=" + address.ToString() + "; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";";
if (status is null) if (response.Status == HealthStatus.Failed)
text += "; healthStatus=Unknown;"; text += " failureReason=" + response.FailureReason + ";";
else if (status.IsHealthy)
text += "; healthStatus=Healthy;";
else
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
} }

View File

@@ -57,16 +57,16 @@ namespace Failover
private IReadOnlyList<DnsResourceRecord> GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck, Uri healthCheckUrl) private IReadOnlyList<DnsResourceRecord> GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck, Uri healthCheckUrl)
{ {
HealthCheckStatus status = _healthService.QueryStatus(domain, question.Type, healthCheck, healthCheckUrl, true); HealthCheckResponse response = _healthService.QueryStatus(domain, question.Type, healthCheck, healthCheckUrl, true);
if (status is null) switch (response.Status)
{ {
case HealthStatus.Unknown:
if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecord(domain)) }; //use ANAME return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecord(domain)) }; //use ANAME
else else
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 30, new DnsCNAMERecord(domain)) }; return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 30, new DnsCNAMERecord(domain)) };
}
else if (status.IsHealthy) case HealthStatus.Healthy:
{
if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecord(domain)) }; //use ANAME return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecord(domain)) }; //use ANAME
else else
@@ -79,31 +79,23 @@ namespace Failover
private void GetStatusAnswers(string domain, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers) private void GetStatusAnswers(string domain, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List<DnsResourceRecord> answers)
{ {
{ {
HealthCheckStatus status = _healthService.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, healthCheckUrl, false); HealthCheckResponse response = _healthService.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, healthCheckUrl, false);
string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri); string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";";
if (status is null) if (response.Status == HealthStatus.Failed)
text += "; healthStatus=Unknown;"; text += " failureReason=" + response.FailureReason + ";";
else if (status.IsHealthy)
text += "; healthStatus=Healthy;";
else
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
} }
{ {
HealthCheckStatus status = _healthService.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, healthCheckUrl, false); HealthCheckResponse response = _healthService.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, healthCheckUrl, false);
string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri); string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";";
if (status is null) if (response.Status == HealthStatus.Failed)
text += "; healthStatus=Unknown;"; text += " failureReason=" + response.FailureReason + ";";
else if (status.IsHealthy)
text += "; healthStatus=Healthy;";
else
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text))); answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
} }

View File

@@ -188,7 +188,7 @@ namespace Failover
_smtpClient.Proxy = _service.DnsServer.Proxy; _smtpClient.Proxy = _service.DnsServer.Proxy;
} }
public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckStatus healthCheckStatus) public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse)
{ {
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0)) if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask; return Task.CompletedTask;
@@ -200,32 +200,40 @@ namespace Failover
foreach (MailAddress alertTo in _alertTo) foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo); message.To.Add(alertTo);
if (healthCheckStatus.IsHealthy) message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper();
switch (healthCheckResponse.Status)
{ {
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Healthy"; case HealthStatus.Failed:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address was healthy.
Alert time: " + healthCheckStatus.DateTime.ToString("R") + @"
Regards,
DNS Failover App
";
}
else
{
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Failed";
message.Body = @"Hi, message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address failed to respond. The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address failed to respond.
The failure reason is: " + healthCheckStatus.FailureReason + @" Address: " + address.ToString() + @"
Alert time: " + healthCheckStatus.DateTime.ToString("R") + @" Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Failure Reason: " + healthCheckResponse.FailureReason + @"
Regards, Regards,
DNS Failover App DNS Failover App
"; ";
break;
default:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address status was " + healthCheckResponse.Status.ToString().ToUpper() + @".
Address: " + address.ToString() + @"
Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Regards,
DNS Failover App
";
break;
} }
return SendMailAsync(message); return SendMailAsync(message);
@@ -243,13 +251,16 @@ DNS Failover App
foreach (MailAddress alertTo in _alertTo) foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo); message.To.Add(alertTo);
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Error"; message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is ERROR";
message.Body = @"Hi, message.Body = @"Hi,
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"]. The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"].
The error description is: " + ex.ToString() + @" Address: " + address.ToString() + @"
Alert time: " + DateTime.UtcNow.ToString("R") + @" Health Check: " + healthCheck + @"
Status: ERROR
Alert Time: " + DateTime.UtcNow.ToString("R") + @"
Failure Reason: " + ex.ToString() + @"
Regards, Regards,
DNS Failover App DNS Failover App
@@ -258,7 +269,7 @@ DNS Failover App
return SendMailAsync(message); return SendMailAsync(message);
} }
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckStatus healthCheckStatus) public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse)
{ {
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0)) if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask; return Task.CompletedTask;
@@ -270,34 +281,42 @@ DNS Failover App
foreach (MailAddress alertTo in _alertTo) foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo); message.To.Add(alertTo);
if (healthCheckStatus.IsHealthy) message.Subject = "[Alert] Domain [" + domain + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper();
switch(healthCheckResponse.Status)
{ {
message.Subject = "[Alert] Domain [" + domain + "] Status Is Healthy"; case HealthStatus.Failed:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name was healthy.
DNS record type: " + type.ToString() + @"
Alert time: " + healthCheckStatus.DateTime.ToString("R") + @"
Regards,
DNS Failover App
";
}
else
{
message.Subject = "[Alert] Domain [" + domain + "] Status Is Failed";
message.Body = @"Hi, message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name failed to respond. The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name failed to respond.
The failure reason is: " + healthCheckStatus.FailureReason + @" Domain: " + domain + @"
DNS record type: " + type.ToString() + @" Record Type: " + type.ToString() + @"
Alert time: " + healthCheckStatus.DateTime.ToString("R") + @" Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Failure Reason: " + healthCheckResponse.FailureReason + @"
Regards, Regards,
DNS Failover App DNS Failover App
"; ";
break;
default:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name status was " + healthCheckResponse.Status.ToString().ToUpper() + @".
Domain: " + domain + @"
Record Type: " + type.ToString() + @"
Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Regards,
DNS Failover App
";
break;
} }
return SendMailAsync(message); return SendMailAsync(message);
@@ -315,14 +334,17 @@ DNS Failover App
foreach (MailAddress alertTo in _alertTo) foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo); message.To.Add(alertTo);
message.Subject = "[Alert] Domain [" + domain + "] Status Is Error"; message.Subject = "[Alert] Domain [" + domain + "] Status Is ERROR";
message.Body = @"Hi, message.Body = @"Hi,
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"]. The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"].
The error description is: " + ex.ToString() + @" Domain: " + domain + @"
DNS record type: " + type.ToString() + @" Record Type: " + type.ToString() + @"
Alert time: " + DateTime.UtcNow.ToString("R") + @" Health Check: " + healthCheck + @"
Status: ERROR
Alert Time: " + DateTime.UtcNow.ToString("R") + @"
Failure Reason: " + ex.ToString() + @"
Regards, Regards,
DNS Failover App DNS Failover App

View File

@@ -4,7 +4,7 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Version>1.4</Version> <Version>1.6</Version>
<Company>Technitium</Company> <Company>Technitium</Company>
<Product>Technitium DNS Server</Product> <Product>Technitium DNS Server</Product>
<Authors>Shreyas Zare</Authors> <Authors>Shreyas Zare</Authors>

View File

@@ -25,6 +25,7 @@ using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using TechnitiumLibrary.IO; using TechnitiumLibrary.IO;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Proxy; using TechnitiumLibrary.Net.Proxy;
@@ -258,7 +259,7 @@ namespace Failover
ConditionalHttpReload(); ConditionalHttpReload();
} }
public async Task<HealthCheckStatus> IsHealthyAsync(string domain, DnsResourceRecordType type, Uri healthCheckUrl) public async Task<HealthCheckResponse> IsHealthyAsync(string domain, DnsResourceRecordType type, Uri healthCheckUrl)
{ {
switch (type) switch (type)
{ {
@@ -266,57 +267,68 @@ namespace Failover
{ {
DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN)); DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
if ((response is null) || (response.Answer.Count == 0)) if ((response is null) || (response.Answer.Count == 0))
return new HealthCheckStatus(false, "Failed to resolve address.", null); return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response); IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response);
if (addresses.Count > 0) if (addresses.Count > 0)
{ {
HealthCheckStatus lastStatus = null; HealthCheckResponse lastResponse = null;
foreach (IPAddress address in addresses) foreach (IPAddress address in addresses)
{ {
lastStatus = await IsHealthyAsync(address, healthCheckUrl); lastResponse = await IsHealthyAsync(address, healthCheckUrl);
if (lastStatus.IsHealthy) if (lastResponse.Status == HealthStatus.Healthy)
return lastStatus; return lastResponse;
} }
return lastStatus; return lastResponse;
} }
return new HealthCheckStatus(false, "Failed to resolve address.", null); return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
} }
case DnsResourceRecordType.AAAA: case DnsResourceRecordType.AAAA:
{ {
DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN)); DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
if ((response is null) || (response.Answer.Count == 0)) if ((response is null) || (response.Answer.Count == 0))
return new HealthCheckStatus(false, "Failed to resolve address.", null); return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response); IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response);
if (addresses.Count > 0) if (addresses.Count > 0)
{ {
HealthCheckStatus lastStatus = null; HealthCheckResponse lastResponse = null;
foreach (IPAddress address in addresses) foreach (IPAddress address in addresses)
{ {
lastStatus = await IsHealthyAsync(address, healthCheckUrl); lastResponse = await IsHealthyAsync(address, healthCheckUrl);
if (lastStatus.IsHealthy) if (lastResponse.Status == HealthStatus.Healthy)
return lastStatus; return lastResponse;
} }
return lastStatus; return lastResponse;
} }
return new HealthCheckStatus(false, "Failed to resolve address.", null); return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address.");
} }
default: default:
return new HealthCheckStatus(false, "Not supported.", null); return new HealthCheckResponse(HealthStatus.Failed, "Not supported.");
} }
} }
public async Task<HealthCheckStatus> IsHealthyAsync(IPAddress address, Uri healthCheckUrl) public async Task<HealthCheckResponse> IsHealthyAsync(IPAddress address, Uri healthCheckUrl)
{ {
foreach (KeyValuePair<NetworkAddress, bool> network in _service.UnderMaintenance)
{
if (network.Key.Contains(address))
{
if (network.Value)
return new HealthCheckResponse(HealthStatus.Maintenance);
break;
}
}
switch (_type) switch (_type)
{ {
case HealthCheckType.Ping: case HealthCheckType.Ping:
@@ -332,13 +344,13 @@ namespace Failover
{ {
PingReply reply = await ping.SendPingAsync(address, _timeout); PingReply reply = await ping.SendPingAsync(address, _timeout);
if (reply.Status == IPStatus.Success) if (reply.Status == IPStatus.Success)
return new HealthCheckStatus(true, null, null); return new HealthCheckResponse(HealthStatus.Healthy);
lastReason = reply.Status.ToString(); lastReason = reply.Status.ToString();
} }
while (++retry < _retries); while (++retry < _retries);
return new HealthCheckStatus(false, lastReason, null); return new HealthCheckResponse(HealthStatus.Failed, lastReason);
} }
} }
@@ -368,7 +380,7 @@ namespace Failover
} }
} }
return new HealthCheckStatus(true, null, null); return new HealthCheckResponse(HealthStatus.Healthy);
} }
catch (TimeoutException ex) catch (TimeoutException ex)
{ {
@@ -387,7 +399,7 @@ namespace Failover
} }
while (++retry < _retries); while (++retry < _retries);
return new HealthCheckStatus(false, lastReason, lastException); return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException);
} }
case HealthCheckType.Http: case HealthCheckType.Http:
@@ -410,7 +422,7 @@ namespace Failover
url = _url; url = _url;
if (url is null) if (url is null)
return new HealthCheckStatus(false, "Missing health check URL in APP record as well as in app config.", null); return new HealthCheckResponse(HealthStatus.Failed, "Missing health check URL in APP record as well as in app config.");
if (_type == HealthCheckType.Http) if (_type == HealthCheckType.Http)
{ {
@@ -434,9 +446,9 @@ namespace Failover
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest); HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest);
if (httpResponse.IsSuccessStatusCode) if (httpResponse.IsSuccessStatusCode)
return new HealthCheckStatus(true, null, null); return new HealthCheckResponse(HealthStatus.Healthy);
return new HealthCheckStatus(false, "Received HTTP status code: " + (int)httpResponse.StatusCode + " " + httpResponse.StatusCode.ToString() + "; URL: " + url.AbsoluteUri, null); return new HealthCheckResponse(HealthStatus.Failed, "Received HTTP status code: " + (int)httpResponse.StatusCode + " " + httpResponse.StatusCode.ToString() + "; URL: " + url.AbsoluteUri);
} }
catch (TaskCanceledException ex) catch (TaskCanceledException ex)
{ {
@@ -455,7 +467,7 @@ namespace Failover
} }
while (++retry < _retries); while (++retry < _retries);
return new HealthCheckStatus(false, lastReason, lastException); return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException);
} }
default: default:

View File

@@ -21,12 +21,20 @@ using System;
namespace Failover namespace Failover
{ {
class HealthCheckStatus enum HealthStatus
{
Unknown = 0,
Failed = 1,
Healthy = 2,
Maintenance = 3
}
class HealthCheckResponse
{ {
#region variables #region variables
public readonly DateTime DateTime = DateTime.UtcNow; public readonly DateTime DateTime = DateTime.UtcNow;
public readonly bool IsHealthy; public readonly HealthStatus Status;
public readonly string FailureReason; public readonly string FailureReason;
public readonly Exception Exception; public readonly Exception Exception;
@@ -34,9 +42,9 @@ namespace Failover
#region constructor #region constructor
public HealthCheckStatus(bool isHealthy, string failureReason, Exception exception) public HealthCheckResponse(HealthStatus status, string failureReason = null, Exception exception = null)
{ {
IsHealthy = isHealthy; Status = status;
FailureReason = failureReason; FailureReason = failureReason;
Exception = exception; Exception = exception;
} }

View File

@@ -38,10 +38,10 @@ namespace Failover
readonly Timer _healthCheckTimer; readonly Timer _healthCheckTimer;
const int HEALTH_CHECK_TIMER_INITIAL_INTERVAL = 1000; const int HEALTH_CHECK_TIMER_INITIAL_INTERVAL = 1000;
HealthCheckStatus _healthCheckStatus; HealthCheckResponse _lastHealthCheckResponse;
const int MONITOR_EXPIRY = 1 * 60 * 60 * 1000; //1 hour const int MONITOR_EXPIRY = 1 * 60 * 60 * 1000; //1 hour
DateTime _lastStatusCheckedOn; DateTime _lastHealthStatusCheckedOn;
#endregion #endregion
@@ -59,52 +59,77 @@ namespace Failover
{ {
if (_healthCheck is null) if (_healthCheck is null)
{ {
_healthCheckStatus = null; _lastHealthCheckResponse = null;
} }
else else
{ {
HealthCheckStatus healthCheckStatus = await _healthCheck.IsHealthyAsync(_address, healthCheckUrl); HealthCheckResponse healthCheckResponse = await _healthCheck.IsHealthyAsync(_address, healthCheckUrl);
bool sendAlert = false; bool statusChanged = false;
bool maintenance = false;
if (_healthCheckStatus is null) if (_lastHealthCheckResponse is null)
{ {
if (!healthCheckStatus.IsHealthy) switch (healthCheckResponse.Status)
sendAlert = true; {
case HealthStatus.Failed:
statusChanged = true;
break;
case HealthStatus.Maintenance:
statusChanged = true;
maintenance = true;
break;
}
} }
else else
{ {
if (_healthCheckStatus.IsHealthy != healthCheckStatus.IsHealthy) if (_lastHealthCheckResponse.Status != healthCheckResponse.Status)
sendAlert = true; {
statusChanged = true;
if ((_lastHealthCheckResponse.Status == HealthStatus.Maintenance) || (healthCheckResponse.Status == HealthStatus.Maintenance))
maintenance = true;
}
} }
if (sendAlert) if (statusChanged)
{ {
if (healthCheckStatus.IsHealthy) switch (healthCheckResponse.Status)
_dnsServer.WriteLog("ALERT! Address [" + _address.ToString() + "] status is HEALTHY based on '" + _healthCheck.Name + "' health check."); {
else case HealthStatus.Failed:
_dnsServer.WriteLog("ALERT! Address [" + _address.ToString() + "] status is FAILED based on '" + _healthCheck.Name + "' health check. The failure reason is: " + healthCheckStatus.FailureReason); _dnsServer.WriteLog("ALERT! Address [" + _address.ToString() + "] status is FAILED based on '" + _healthCheck.Name + "' health check. The failure reason is: " + healthCheckResponse.FailureReason);
break;
if (healthCheckStatus.Exception is not null) default:
_dnsServer.WriteLog(healthCheckStatus.Exception); _dnsServer.WriteLog("ALERT! Address [" + _address.ToString() + "] status is " + healthCheckResponse.Status.ToString().ToUpper() + " based on '" + _healthCheck.Name + "' health check.");
break;
}
if (healthCheckResponse.Exception is not null)
_dnsServer.WriteLog(healthCheckResponse.Exception);
if (!maintenance)
{
//avoid sending email alerts when switching from or to maintenance
EmailAlert emailAlert = _healthCheck.EmailAlert; EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null) if (emailAlert is not null)
_ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, healthCheckStatus); _ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, healthCheckResponse);
}
WebHook webHook = _healthCheck.WebHook; WebHook webHook = _healthCheck.WebHook;
if (webHook is not null) if (webHook is not null)
_ = webHook.CallAsync(_address, _healthCheck.Name, healthCheckStatus); _ = webHook.CallAsync(_address, _healthCheck.Name, healthCheckResponse);
} }
_healthCheckStatus = healthCheckStatus; _lastHealthCheckResponse = healthCheckResponse;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_dnsServer.WriteLog(ex); _dnsServer.WriteLog(ex);
if (_healthCheckStatus is null) if (_lastHealthCheckResponse is null)
{ {
EmailAlert emailAlert = _healthCheck.EmailAlert; EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null) if (emailAlert is not null)
@@ -114,11 +139,11 @@ namespace Failover
if (webHook is not null) if (webHook is not null)
_ = webHook.CallAsync(_address, _healthCheck.Name, ex); _ = webHook.CallAsync(_address, _healthCheck.Name, ex);
_healthCheckStatus = new HealthCheckStatus(false, ex.ToString(), ex); _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Failed, ex.ToString(), ex);
} }
else else
{ {
_healthCheckStatus = null; _lastHealthCheckResponse = null;
} }
} }
finally finally
@@ -144,52 +169,77 @@ namespace Failover
{ {
if (_healthCheck is null) if (_healthCheck is null)
{ {
_healthCheckStatus = null; _lastHealthCheckResponse = null;
} }
else else
{ {
HealthCheckStatus healthCheckStatus = await _healthCheck.IsHealthyAsync(_domain, _type, healthCheckUrl); HealthCheckResponse healthCheckResponse = await _healthCheck.IsHealthyAsync(_domain, _type, healthCheckUrl);
bool sendAlert = false; bool statusChanged = false;
bool maintenance = false;
if (_healthCheckStatus is null) if (_lastHealthCheckResponse is null)
{ {
if (!healthCheckStatus.IsHealthy) switch (healthCheckResponse.Status)
sendAlert = true; {
case HealthStatus.Failed:
statusChanged = true;
break;
case HealthStatus.Maintenance:
statusChanged = true;
maintenance = true;
break;
}
} }
else else
{ {
if (_healthCheckStatus.IsHealthy != healthCheckStatus.IsHealthy) if (_lastHealthCheckResponse.Status != healthCheckResponse.Status)
sendAlert = true; {
statusChanged = true;
if ((_lastHealthCheckResponse.Status == HealthStatus.Maintenance) || (healthCheckResponse.Status == HealthStatus.Maintenance))
maintenance = true;
}
} }
if (sendAlert) if (statusChanged)
{ {
if (healthCheckStatus.IsHealthy) switch (healthCheckResponse.Status)
_dnsServer.WriteLog("ALERT! Domain [" + _domain + "] type [" + _type.ToString() + "] status is HEALTHY based on '" + _healthCheck.Name + "' health check."); {
else case HealthStatus.Failed:
_dnsServer.WriteLog("ALERT! Domain [" + _domain + "] type [" + _type.ToString() + "] status is FAILED based on '" + _healthCheck.Name + "' health check. The failure reason is: " + healthCheckStatus.FailureReason); _dnsServer.WriteLog("ALERT! Domain [" + _domain + "] type [" + _type.ToString() + "] status is FAILED based on '" + _healthCheck.Name + "' health check. The failure reason is: " + healthCheckResponse.FailureReason);
break;
if (healthCheckStatus.Exception is not null) default:
_dnsServer.WriteLog(healthCheckStatus.Exception); _dnsServer.WriteLog("ALERT! Domain [" + _domain + "] type [" + _type.ToString() + "] status is " + healthCheckResponse.Status.ToString().ToUpper() + " based on '" + _healthCheck.Name + "' health check.");
break;
}
if (healthCheckResponse.Exception is not null)
_dnsServer.WriteLog(healthCheckResponse.Exception);
if (!maintenance)
{
//avoid sending email alerts when switching from or to maintenance
EmailAlert emailAlert = _healthCheck.EmailAlert; EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null) if (emailAlert is not null)
_ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, healthCheckStatus); _ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, healthCheckResponse);
}
WebHook webHook = _healthCheck.WebHook; WebHook webHook = _healthCheck.WebHook;
if (webHook is not null) if (webHook is not null)
_ = webHook.CallAsync(_domain, _type, _healthCheck.Name, healthCheckStatus); _ = webHook.CallAsync(_domain, _type, _healthCheck.Name, healthCheckResponse);
} }
_healthCheckStatus = healthCheckStatus; _lastHealthCheckResponse = healthCheckResponse;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_dnsServer.WriteLog(ex); _dnsServer.WriteLog(ex);
if (_healthCheckStatus is null) if (_lastHealthCheckResponse is null)
{ {
EmailAlert emailAlert = _healthCheck.EmailAlert; EmailAlert emailAlert = _healthCheck.EmailAlert;
if (emailAlert is not null) if (emailAlert is not null)
@@ -199,11 +249,11 @@ namespace Failover
if (webHook is not null) if (webHook is not null)
_ = webHook.CallAsync(_domain, _type, _healthCheck.Name, ex); _ = webHook.CallAsync(_domain, _type, _healthCheck.Name, ex);
_healthCheckStatus = new HealthCheckStatus(false, ex.ToString(), ex); _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Failed, ex.ToString(), ex);
} }
else else
{ {
_healthCheckStatus = null; _lastHealthCheckResponse = null;
} }
} }
finally finally
@@ -248,19 +298,19 @@ namespace Failover
public bool IsExpired() public bool IsExpired()
{ {
return DateTime.UtcNow > _lastStatusCheckedOn.AddMilliseconds(MONITOR_EXPIRY); return DateTime.UtcNow > _lastHealthStatusCheckedOn.AddMilliseconds(MONITOR_EXPIRY);
} }
#endregion #endregion
#region properties #region properties
public HealthCheckStatus HealthCheckStatus public HealthCheckResponse LastHealthCheckResponse
{ {
get get
{ {
_lastStatusCheckedOn = DateTime.UtcNow; _lastHealthStatusCheckedOn = DateTime.UtcNow;
return _healthCheckStatus; return _lastHealthCheckResponse;
} }
} }

View File

@@ -23,6 +23,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns;
namespace Failover namespace Failover
@@ -38,6 +39,7 @@ namespace Failover
readonly ConcurrentDictionary<string, HealthCheck> _healthChecks = new ConcurrentDictionary<string, HealthCheck>(1, 5); readonly ConcurrentDictionary<string, HealthCheck> _healthChecks = new ConcurrentDictionary<string, HealthCheck>(1, 5);
readonly ConcurrentDictionary<string, EmailAlert> _emailAlerts = new ConcurrentDictionary<string, EmailAlert>(1, 2); readonly ConcurrentDictionary<string, EmailAlert> _emailAlerts = new ConcurrentDictionary<string, EmailAlert>(1, 2);
readonly ConcurrentDictionary<string, WebHook> _webHooks = new ConcurrentDictionary<string, WebHook>(1, 2); readonly ConcurrentDictionary<string, WebHook> _webHooks = new ConcurrentDictionary<string, WebHook>(1, 2);
readonly ConcurrentDictionary<NetworkAddress, bool> _underMaintenance = new ConcurrentDictionary<NetworkAddress, bool>();
readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>(); readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>();
@@ -347,14 +349,28 @@ namespace Failover
} }
} }
} }
//under maintenance networks
_underMaintenance.Clear();
if (jsonConfig.underMaintenance is not null)
{
foreach (dynamic jsonNetwork in jsonConfig.underMaintenance)
{
string network = jsonNetwork.network.Value;
bool enable = jsonNetwork.enable.Value;
_underMaintenance.TryAdd(NetworkAddress.Parse(network), enable);
}
}
} }
public HealthCheckStatus QueryStatus(IPAddress address, string healthCheck, Uri healthCheckUrl, bool tryAdd) public HealthCheckResponse QueryStatus(IPAddress address, string healthCheck, Uri healthCheckUrl, bool tryAdd)
{ {
string healthMonitorKey = GetHealthMonitorKey(address, healthCheck, healthCheckUrl); string healthMonitorKey = GetHealthMonitorKey(address, healthCheck, healthCheckUrl);
if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor)) if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor))
return monitor.HealthCheckStatus; return monitor.LastHealthCheckResponse;
if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck)) if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
{ {
@@ -366,20 +382,22 @@ namespace Failover
monitor.Dispose(); //failed to add first monitor.Dispose(); //failed to add first
} }
return null; return new HealthCheckResponse(HealthStatus.Unknown);
}
else
{
return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck);
}
} }
return new HealthCheckStatus(false, "No such health check: " + healthCheck, null); public HealthCheckResponse QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl, bool tryAdd)
}
public HealthCheckStatus QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl, bool tryAdd)
{ {
domain = domain.ToLower(); domain = domain.ToLower();
string healthMonitorKey = GetHealthMonitorKey(domain, type, healthCheck, healthCheckUrl); string healthMonitorKey = GetHealthMonitorKey(domain, type, healthCheck, healthCheckUrl);
if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor)) if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor))
return monitor.HealthCheckStatus; return monitor.LastHealthCheckResponse;
if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck)) if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
{ {
@@ -391,10 +409,12 @@ namespace Failover
monitor.Dispose(); //failed to add first monitor.Dispose(); //failed to add first
} }
return null; return new HealthCheckResponse(HealthStatus.Unknown);
}
else
{
return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck);
} }
return new HealthCheckStatus(false, "No such health check: " + healthCheck, null);
} }
#endregion #endregion
@@ -410,6 +430,9 @@ namespace Failover
public IReadOnlyDictionary<string, WebHook> WebHooks public IReadOnlyDictionary<string, WebHook> WebHooks
{ get { return _webHooks; } } { get { return _webHooks; } }
public IReadOnlyDictionary<NetworkAddress, bool> UnderMaintenance
{ get { return _underMaintenance; } }
public IDnsServer DnsServer public IDnsServer DnsServer
{ get { return _dnsServer; } } { get { return _dnsServer; } }

View File

@@ -201,7 +201,7 @@ namespace Failover
ConditionalHttpReload(); ConditionalHttpReload();
} }
public Task CallAsync(IPAddress address, string healthCheck, HealthCheckStatus healthCheckStatus) public Task CallAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse)
{ {
if (!_enabled) if (!_enabled)
return Task.CompletedTask; return Task.CompletedTask;
@@ -219,14 +219,17 @@ namespace Failover
jsonWriter.WritePropertyName("healthCheck"); jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck); jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy"); jsonWriter.WritePropertyName("status");
jsonWriter.WriteValue(healthCheckStatus.IsHealthy); jsonWriter.WriteValue(healthCheckResponse.Status.ToString());
if (healthCheckResponse.Status == HealthStatus.Failed)
{
jsonWriter.WritePropertyName("failureReason"); jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(healthCheckStatus.FailureReason); jsonWriter.WriteValue(healthCheckResponse.FailureReason);
}
jsonWriter.WritePropertyName("dateTime"); jsonWriter.WritePropertyName("dateTime");
jsonWriter.WriteValue(healthCheckStatus.DateTime); jsonWriter.WriteValue(healthCheckResponse.DateTime);
jsonWriter.WriteEndObject(); jsonWriter.WriteEndObject();
jsonWriter.Flush(); jsonWriter.Flush();
@@ -257,8 +260,8 @@ namespace Failover
jsonWriter.WritePropertyName("healthCheck"); jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck); jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy"); jsonWriter.WritePropertyName("status");
jsonWriter.WriteValue(false); jsonWriter.WriteValue("Error");
jsonWriter.WritePropertyName("failureReason"); jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(ex.ToString()); jsonWriter.WriteValue(ex.ToString());
@@ -277,7 +280,7 @@ namespace Failover
return CallAsync(content); return CallAsync(content);
} }
public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckStatus healthCheckStatus) public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse)
{ {
if (!_enabled) if (!_enabled)
return Task.CompletedTask; return Task.CompletedTask;
@@ -298,14 +301,17 @@ namespace Failover
jsonWriter.WritePropertyName("healthCheck"); jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck); jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy"); jsonWriter.WritePropertyName("status");
jsonWriter.WriteValue(healthCheckStatus.IsHealthy); jsonWriter.WriteValue(healthCheckResponse.Status.ToString());
if (healthCheckResponse.Status == HealthStatus.Failed)
{
jsonWriter.WritePropertyName("failureReason"); jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(healthCheckStatus.FailureReason); jsonWriter.WriteValue(healthCheckResponse.FailureReason);
}
jsonWriter.WritePropertyName("dateTime"); jsonWriter.WritePropertyName("dateTime");
jsonWriter.WriteValue(healthCheckStatus.DateTime); jsonWriter.WriteValue(healthCheckResponse.DateTime);
jsonWriter.WriteEndObject(); jsonWriter.WriteEndObject();
jsonWriter.Flush(); jsonWriter.Flush();
@@ -339,8 +345,8 @@ namespace Failover
jsonWriter.WritePropertyName("healthCheck"); jsonWriter.WritePropertyName("healthCheck");
jsonWriter.WriteValue(healthCheck); jsonWriter.WriteValue(healthCheck);
jsonWriter.WritePropertyName("isHealthy"); jsonWriter.WritePropertyName("status");
jsonWriter.WriteValue(false); jsonWriter.WriteValue("Error");
jsonWriter.WritePropertyName("failureReason"); jsonWriter.WritePropertyName("failureReason");
jsonWriter.WriteValue(ex.ToString()); jsonWriter.WriteValue(ex.ToString());

View File

@@ -80,5 +80,15 @@
"https://webhooks.example.com/default" "https://webhooks.example.com/default"
] ]
} }
],
"underMaintenance": [
{
"network": "192.168.10.2/32",
"enable": false
},
{
"network": "10.1.1.0/24",
"enable": false
}
] ]
} }