Skip to content

Commit

Permalink
feat: implement local service health check (#240)
Browse files Browse the repository at this point in the history
  • Loading branch information
winstxnhdw authored Oct 23, 2023
1 parent d0d1327 commit f0af036
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 23 deletions.
107 changes: 107 additions & 0 deletions Consul.Test/AgentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,113 @@ public async Task Agent_MonitorJSON()
}
}

[Theory]
[InlineData("passing")]
[InlineData("warning")]
[InlineData("critical")]
public async Task Agent_GetLocalServiceHealth(string statusString)
{
var healthStatus = HealthStatus.Parse(statusString);
var svcName = KVTest.GenerateTestKeyName();
var registration = new AgentServiceRegistration
{
Name = svcName,
Tags = new[] { "bar", "baz" },
Port = 8000,
Checks = new[]
{
new AgentServiceCheck
{
TTL = TimeSpan.FromSeconds(15),
Status = HealthStatus.Passing,
},
new AgentServiceCheck
{
TTL = TimeSpan.FromSeconds(15),
Status = healthStatus,
}
}
};

await _client.Agent.ServiceRegister(registration);

var status = await _client.Agent.GetLocalServiceHealth(svcName);

Assert.Equal(healthStatus, status.Response[0].AggregatedStatus);
}

[Theory]
[InlineData("passing")]
[InlineData("warning")]
[InlineData("critical")]
public async Task Agent_GetLocalServiceHealthByID(string statusString)
{
var healthStatus = HealthStatus.Parse(statusString);
var svcID = KVTest.GenerateTestKeyName();
var registration = new AgentServiceRegistration
{
ID = svcID,
Name = KVTest.GenerateTestKeyName(),
Tags = new[] { "bar", "baz" },
Port = 8000,
Checks = new[]
{
new AgentServiceCheck
{
TTL = TimeSpan.FromSeconds(15),
Status = HealthStatus.Passing,
},
new AgentServiceCheck
{
TTL = TimeSpan.FromSeconds(15),
Status = healthStatus,
}
}
};

await _client.Agent.ServiceRegister(registration);

var status = await _client.Agent.GetLocalServiceHealthByID(svcID);

Assert.Equal(healthStatus, status.Response.AggregatedStatus);
}

[Theory]
[InlineData("passing")]
[InlineData("warning")]
[InlineData("critical")]
public async Task Agent_GetWorstLocalServiceHealth(string statusString)
{
var healthStatus = HealthStatus.Parse(statusString);
var svcName = KVTest.GenerateTestKeyName();

var registration = new AgentServiceRegistration
{
Name = svcName,
Tags = new[] { "bar", "baz" },
Port = 8000,
Checks = new[]
{
new AgentServiceCheck
{
TTL = TimeSpan.FromSeconds(15),
Status = HealthStatus.Passing,
},
new AgentServiceCheck
{
TTL = TimeSpan.FromSeconds(15),
Status = healthStatus,
}
}
};

await _client.Agent.ServiceRegister(registration);

var status = await _client.Agent.GetWorstLocalServiceHealth(svcName);

Assert.Equal(healthStatus.Status, status.Response);
}

[Fact]
public async Task Agent_FilterServices()
{
Expand Down
77 changes: 76 additions & 1 deletion Consul/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,20 @@ public class AgentServiceCheck
public TimeSpan? DeregisterCriticalServiceAfter { get; set; }
}


/// <summary>
/// LocalServiceHealth represents the health of a service and its associated checks as returned by the Agent API
/// </summary>
public class LocalServiceHealth
{
[JsonConverter(typeof(HealthStatusConverter))]
public HealthStatus AggregatedStatus { get; set; }

public AgentService Service { get; set; }

public AgentCheck[] Checks { get; set; }
}

/// <summary>
/// Log Level Enum
/// </summary>
Expand Down Expand Up @@ -668,7 +682,6 @@ public Task<WriteResult> ForceLeave(string node, CancellationToken ct = default)
return _client.PutNothing(string.Format("/v1/agent/force-leave/{0}", node)).Execute(ct);
}


/// <summary>
/// Leave is used to have the agent gracefully leave the cluster and shutdown
/// </summary>
Expand Down Expand Up @@ -764,6 +777,68 @@ public async Task<LogStream> MonitorJSON(LogLevel level = default, CancellationT
return new LogStream(res.Response);
}

/// <summary>
/// GetLocalServiceHealth returns the health info of a service registered on the local agent
/// </summary>
/// <param name="serviceName">Name of service</param>
/// <returns>An array containing the details of each passing, warning, or critical service</returns>
public async Task<QueryResult<LocalServiceHealth[]>> GetLocalServiceHealth(string serviceName, QueryOptions q, CancellationToken ct = default)
{
return await _client.Get<LocalServiceHealth[]>($"v1/agent/health/service/name/{serviceName}", q).Execute(ct).ConfigureAwait(false);
}

/// <summary>
/// GetLocalServiceHealth returns the health info of a service registered on the local agent
/// </summary>
/// <param name="serviceName">Name of service</param>
/// <returns>An array containing the details of each passing, warning, or critical service</returns>
public async Task<QueryResult<LocalServiceHealth[]>> GetLocalServiceHealth(string serviceName, CancellationToken ct = default)
{
return await GetLocalServiceHealth(serviceName, QueryOptions.Default, ct).ConfigureAwait(false);
}

/// <summary>
/// GetWorstLocalServiceHealth returns the worst aggregated status of a service registered on the local agent
/// </summary>
/// <param name="serviceName">Name of service</param>
/// <returns>passing, warning, or critical</returns>
public async Task<QueryResult<string>> GetWorstLocalServiceHealth(string serviceName, QueryOptions q, CancellationToken ct = default)
{
var req = _client.Get($"v1/agent/health/service/name/{serviceName}", q);
req.Params["format"] = "text";
return await req.Execute(ct).ConfigureAwait(false);
}

/// <summary>
/// GetWorstLocalServiceHealth returns the worst aggregated status of a service registered on the local agent
/// </summary>
/// <param name="serviceName">Name of service</param>
/// <returns>passing, warning, or critical</returns>
public async Task<QueryResult<string>> GetWorstLocalServiceHealth(string serviceName, CancellationToken ct = default)
{
return await GetWorstLocalServiceHealth(serviceName, QueryOptions.Default, ct).ConfigureAwait(false);
}

/// <summary>
/// GetLocalServiceHealthByID returns the health info of a service registered on the local agent by ID
/// </summary>
/// <param name="serviceID">ID of the service</param>
/// <returns>An array containing the details of each passing, warning, or critical service</returns>
public async Task<QueryResult<LocalServiceHealth>> GetLocalServiceHealthByID(string serviceID, QueryOptions q, CancellationToken ct = default)
{
return await _client.Get<LocalServiceHealth>($"v1/agent/health/service/id/{serviceID}", q).Execute(ct).ConfigureAwait(false);
}

/// <summary>
/// GetLocalServiceHealthByID returns the health info of a service registered on the local agent by ID
/// </summary>
/// <param name="serviceID">ID of the service</param>
/// <returns>An array containing the details of each passing, warning, or critical service</returns>
public async Task<QueryResult<LocalServiceHealth>> GetLocalServiceHealthByID(string serviceID, CancellationToken ct = default)
{
return await GetLocalServiceHealthByID(serviceID, QueryOptions.Default, ct).ConfigureAwait(false);
}

/// <summary>
/// Log streamer
/// </summary>
Expand Down
31 changes: 27 additions & 4 deletions Consul/Client_GetRequests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,19 @@ public async Task<QueryResult<TOut>> Execute(CancellationToken ct)
result.StatusCode = response.StatusCode;
ResponseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

if (response.StatusCode != HttpStatusCode.NotFound && !response.IsSuccessStatusCode)

Func<bool> isSpecialStatusCode = () =>
{
var isAgentServiceName = Endpoint.StartsWith("v1/agent/health/service/name/", StringComparison.OrdinalIgnoreCase);
var isAgentServiceId = Endpoint.StartsWith("v1/agent/health/service/id/", StringComparison.OrdinalIgnoreCase);
return ((int)response.StatusCode == 503 && isAgentServiceName) ||
((int)response.StatusCode == 429 && isAgentServiceName) ||
((int)response.StatusCode == 503 && isAgentServiceId) ||
((int)response.StatusCode == 429 && isAgentServiceId);
};

if (response.StatusCode != HttpStatusCode.NotFound && !response.IsSuccessStatusCode && !isSpecialStatusCode())
{
if (ResponseStream == null)
{
Expand All @@ -93,7 +105,7 @@ public async Task<QueryResult<TOut>> Execute(CancellationToken ct)
}
}

if (response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode || isSpecialStatusCode())
{
result.Response = Deserialize<TOut>(ResponseStream);
}
Expand Down Expand Up @@ -320,7 +332,18 @@ public async Task<QueryResult<string>> Execute(CancellationToken ct)
result.StatusCode = response.StatusCode;
ResponseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

if (response.StatusCode != HttpStatusCode.NotFound && !response.IsSuccessStatusCode)
Func<bool> isSpecialStatusCode = () =>
{
var isAgentServiceName = Endpoint.StartsWith("v1/agent/health/service/name/", StringComparison.OrdinalIgnoreCase);
var isAgentServiceId = Endpoint.StartsWith("v1/agent/health/service/id/", StringComparison.OrdinalIgnoreCase);
return ((int)response.StatusCode == 503 && isAgentServiceName) ||
((int)response.StatusCode == 429 && isAgentServiceName) ||
((int)response.StatusCode == 503 && isAgentServiceId) ||
((int)response.StatusCode == 429 && isAgentServiceId);
};

if (response.StatusCode != HttpStatusCode.NotFound && !response.IsSuccessStatusCode && !isSpecialStatusCode())
{
if (ResponseStream == null)
{
Expand All @@ -334,7 +357,7 @@ public async Task<QueryResult<string>> Execute(CancellationToken ct)
}
}

if (response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode || isSpecialStatusCode())
{
using (var reader = new StreamReader(ResponseStream))
{
Expand Down
36 changes: 18 additions & 18 deletions Consul/Health.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ public class HealthStatus : IEquatable<HealthStatus>

public static HealthStatus Any { get; } = new HealthStatus() { Status = "any" };

public static HealthStatus Parse(string status)
{
switch (status)
{
case "passing":
return Passing;
case "warning":
return Warning;
case "critical":
return Critical;
default:
throw new ArgumentException("Invalid Check status value during deserialization");
}
}

public bool Equals(HealthStatus other)
{
return other != null && ReferenceEquals(this, other);
Expand All @@ -71,30 +86,15 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
serializer.Serialize(writer, ((HealthStatus)value).Status);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var status = (string)serializer.Deserialize(reader, typeof(string));
switch (status)
{
case "passing":
return HealthStatus.Passing;
case "warning":
return HealthStatus.Warning;
case "critical":
return HealthStatus.Critical;
default:
throw new ArgumentException("Invalid Check status value during deserialization");
}
return HealthStatus.Parse(status);
}

public override bool CanConvert(Type objectType)
{
if (objectType == typeof(HealthStatus))
{
return true;
}
return false;
return objectType == typeof(HealthStatus);
}
}

Expand Down
6 changes: 6 additions & 0 deletions Consul/Interfaces/IAgentEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ public interface IAgentEndpoint
Task WarnTTL(string checkID, string note, CancellationToken ct = default);
Task<Agent.LogStream> Monitor(LogLevel level = default, CancellationToken ct = default);
Task<Agent.LogStream> MonitorJSON(LogLevel level = default, CancellationToken ct = default);
Task<QueryResult<LocalServiceHealth[]>> GetLocalServiceHealth(string serviceName, QueryOptions q, CancellationToken ct = default);
Task<QueryResult<LocalServiceHealth[]>> GetLocalServiceHealth(string serviceName, CancellationToken ct = default);
Task<QueryResult<string>> GetWorstLocalServiceHealth(string serviceName, QueryOptions q, CancellationToken ct = default);
Task<QueryResult<string>> GetWorstLocalServiceHealth(string serviceName, CancellationToken ct = default);
Task<QueryResult<LocalServiceHealth>> GetLocalServiceHealthByID(string serviceID, QueryOptions q, CancellationToken ct = default);
Task<QueryResult<LocalServiceHealth>> GetLocalServiceHealthByID(string serviceID, CancellationToken ct = default);

Task<WriteResult> Leave(string node, CancellationToken ct = default);
Task<WriteResult> Reload(string node, CancellationToken ct = default);
Expand Down

0 comments on commit f0af036

Please sign in to comment.