Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement local service health check #240

Merged
merged 23 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eb857a3
feat: implement local service health check
winstxnhdw Sep 4, 2023
05cfb2d
Merge branch 'master' into health
winstxnhdw Sep 4, 2023
fcd0c9a
chore: remove `AggregatedStatus` enum
winstxnhdw Sep 5, 2023
197155e
refactor: use `HealthStatus` instead of `string` for `AggregatedStatus`
winstxnhdw Sep 5, 2023
77f446e
refactor: test every status
winstxnhdw Sep 5, 2023
35461f4
feat: add `Parse` to `HealthStatus`
winstxnhdw Sep 5, 2023
a239167
feat: use `HealthStatus`
winstxnhdw Sep 5, 2023
d7a0880
Merge branch 'master' into health
winstxnhdw Sep 6, 2023
2b9312c
fix: add special case for unsuccessful status code
winstxnhdw Sep 8, 2023
2bbbfe4
fix: handle `TooManyRequests` and `ServiceUnavailable`
winstxnhdw Oct 18, 2023
a3648d5
Merge branch 'master' into health
winstxnhdw Oct 18, 2023
9869308
feat: add support for `format` query parameter
winstxnhdw Oct 18, 2023
22c65eb
Merge branch 'master' into health
marcin-krystianc Oct 19, 2023
4b251ce
refactor: use `svcName` for `ID`
winstxnhdw Oct 19, 2023
ad7e7da
fix: use correct method
winstxnhdw Oct 19, 2023
30e9a91
fix: remove redundant `GetWorstLocalServiceHealthByID`
winstxnhdw Oct 19, 2023
65c8e48
fix/refactor: use a more specific exemption
winstxnhdw Oct 20, 2023
bbcb470
Merge branch 'master' into health
winstxnhdw Oct 20, 2023
1b11340
perf: lazily evaluate special status exemption
winstxnhdw Oct 20, 2023
5425096
chore: rename `svcID` to `svcName`
winstxnhdw Oct 23, 2023
527216a
fix: use `svcName`
winstxnhdw Oct 23, 2023
53d3007
refactor: reuse `HealthStatus.Parse` in `HealthStatusConverter`
winstxnhdw Oct 23, 2023
19a4ffb
refactor: simplify if condition
winstxnhdw Oct 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 svcID = KVTest.GenerateTestKeyName();
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved

var registration = new AgentServiceRegistration
{
Name = svcID,
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(svcID);
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved

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
15 changes: 15 additions & 0 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)
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
{
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 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