Skip to content

Commit 3f733cc

Browse files
authored
Merge pull request #383 from nblumhardt/health-command-wait
Update the `node health` command to be roughly comparable with `cluster health`
2 parents 380bdfb + 874fbfe commit 3f733cc

File tree

4 files changed

+112
-29
lines changed

4 files changed

+112
-29
lines changed

src/SeqCli/Cli/Commands/Cluster/HealthCommand.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
// limitations under the License.
1414

1515
using System;
16-
using System.Globalization;
17-
using System.Linq;
1816
using System.Threading;
1917
using System.Threading.Tasks;
2018
using Seq.Api;
@@ -38,18 +36,13 @@ class HealthCommand : Command
3836
readonly ConnectionFeature _connection;
3937
readonly OutputFormatFeature _output;
4038
readonly TimeoutFeature _timeout;
41-
42-
bool _waitUntilHealthy;
39+
readonly WaitUntilHealthyFeature _waitUntilHealthy;
4340

4441
public HealthCommand(SeqConnectionFactory connectionFactory, SeqCliOutputConfig outputConfig)
4542
{
4643
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
4744

48-
Options.Add("wait-until-healthy", "Wait until the cluster returns a status of healthy", _ =>
49-
{
50-
_waitUntilHealthy = true;
51-
});
52-
45+
_waitUntilHealthy = Enable(new WaitUntilHealthyFeature("cluster"));
5346
_timeout = Enable(new TimeoutFeature());
5447
_output = Enable(new OutputFormatFeature(outputConfig));
5548
_connection = Enable<ConnectionFeature>();
@@ -61,7 +54,7 @@ protected override async Task<int> Run()
6154

6255
var timeout = _timeout.ApplyTimeout(connection.Client.HttpClient);
6356

64-
if (_waitUntilHealthy)
57+
if (_waitUntilHealthy.ShouldWait)
6558
{
6659
return await RunUntilHealthy(connection, timeout ?? TimeSpan.FromSeconds(30));
6760
}

src/SeqCli/Cli/Commands/Node/HealthCommand.cs

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,86 @@
1313
// limitations under the License.
1414

1515
using System;
16-
using System.Globalization;
16+
using System.IO;
1717
using System.Linq;
18+
using System.Threading;
1819
using System.Threading.Tasks;
20+
using Newtonsoft.Json;
21+
using Seq.Api;
1922
using SeqCli.Cli.Features;
23+
using SeqCli.Config;
2024
using SeqCli.Connection;
21-
using SeqCli.Util;
2225
using Serilog;
2326

2427
namespace SeqCli.Cli.Commands.Node;
2528

2629
[Command("node", "health",
27-
"Probe a Seq node's `/health` endpoint, and print the returned HTTP status code, or 'Unreachable' if the endpoint could not be queried",
30+
"Probe a Seq node's `/health` endpoint, and print the returned HTTP status code, or 'Unreachable' if the endpoint could not be queried; note that no API key is required",
2831
Example = "seqcli node health -s https://seq-2.example.com")]
2932
class HealthCommand : Command
3033
{
3134
readonly SeqConnectionFactory _connectionFactory;
3235

33-
string? _profileName, _serverUrl;
36+
readonly ConnectionFeature _connection;
37+
readonly WaitUntilHealthyFeature _waitUntilHealthy;
38+
readonly TimeoutFeature _timeout;
39+
readonly OutputFormatFeature _output;
3440

35-
public HealthCommand(SeqConnectionFactory connectionFactory)
41+
public HealthCommand(SeqConnectionFactory connectionFactory, SeqCliOutputConfig outputConfig)
3642
{
3743
_connectionFactory = connectionFactory;
38-
39-
Options.Add("s=|server=",
40-
"The URL of the Seq server; by default the `connection.serverUrl` config value will be used",
41-
v => _serverUrl = ArgumentString.Normalize(v));
4244

43-
Options.Add("profile=",
44-
"A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used",
45-
v => _profileName = ArgumentString.Normalize(v));
45+
_waitUntilHealthy = Enable(new WaitUntilHealthyFeature("node"));
46+
_timeout = Enable(new TimeoutFeature());
47+
_connection = Enable<ConnectionFeature>();
48+
_output = Enable(new OutputFormatFeature(outputConfig));
4649
}
4750

4851
protected override async Task<int> Run()
4952
{
50-
// An API key is not accepted; we don't want to imply that /health requires authentication.
51-
var surrogateConnectionFeature = new ConnectionFeature { ProfileName = _profileName, Url = _serverUrl };
52-
var connection = _connectionFactory.Connect(surrogateConnectionFeature);
53+
var connection = _connectionFactory.Connect(_connection);
54+
55+
var timeout = _timeout.ApplyTimeout(connection.Client.HttpClient);
56+
57+
if (_waitUntilHealthy.ShouldWait)
58+
{
59+
return await RunUntilHealthy(connection, timeout ?? TimeSpan.FromSeconds(30));
60+
}
61+
62+
return await RunOnce(connection);
63+
}
64+
65+
async Task<int> RunUntilHealthy(SeqConnection connection, TimeSpan timeout)
66+
{
67+
using var ct = new CancellationTokenSource(timeout);
68+
69+
var tick = TimeSpan.FromSeconds(1);
70+
71+
connection.Client.HttpClient.Timeout = tick;
72+
73+
try
74+
{
75+
return await Task.Run(async () =>
76+
{
77+
while (true)
78+
{
79+
if (await RunOnce(connection) == 0)
80+
{
81+
return 0;
82+
}
5383

84+
await Task.Delay(tick, ct.Token);
85+
}
86+
}, ct.Token);
87+
}
88+
catch (TaskCanceledException)
89+
{
90+
return 1;
91+
}
92+
}
93+
94+
async Task<int> RunOnce(SeqConnection connection)
95+
{
5496
try
5597
{
5698
var response = await connection.Client.HttpClient.GetAsync("health");
@@ -61,17 +103,32 @@ protected override async Task<int> Run()
61103
{
62104
Log.Information("{HeaderName}: {HeaderValue}", key, value);
63105
}
64-
65-
Console.WriteLine(await response.Content.ReadAsStringAsync());
106+
107+
if (_output.Json)
108+
{
109+
var shouldBeJson = await response.Content.ReadAsStringAsync();
110+
try
111+
{
112+
var obj = JsonConvert.DeserializeObject(shouldBeJson) ?? throw new InvalidDataException();
113+
_output.WriteObject(obj);
114+
}
115+
catch
116+
{
117+
_output.WriteObject(new { Response = shouldBeJson });
118+
}
119+
}
120+
else
121+
{
122+
Console.WriteLine((int)response.StatusCode);
123+
}
66124

67125
return response.IsSuccessStatusCode ? 0 : 1;
68126
}
69127
catch (Exception ex)
70128
{
71129
Log.Information(ex, "Exception thrown when calling health endpoint");
72-
130+
73131
Console.WriteLine("Unreachable");
74-
Console.WriteLine(Presentation.FormattedMessage(ex));
75132
return 1;
76133
}
77134
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright © Datalust Pty Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace SeqCli.Cli.Features;
16+
17+
class WaitUntilHealthyFeature(string targetName): CommandFeature
18+
{
19+
public bool ShouldWait { get; private set; }
20+
21+
public override void Enable(OptionSet options)
22+
{
23+
options.Add("wait-until-healthy", $"Wait until the {targetName} returns a status of healthy", _ =>
24+
{
25+
ShouldWait = true;
26+
});
27+
}
28+
}

test/SeqCli.EndToEnd/Node/NodeHealthTestCase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ public Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRun
1313
{
1414
var exit = runner.Exec("node health");
1515
Assert.Equal(0, exit);
16+
Assert.Equal("200", runner.LastRunProcess!.Output.Trim());
17+
18+
exit = runner.Exec("node health --no-color --json");
19+
Assert.Equal(0, exit);
1620
Assert.StartsWith("{\"status\":", runner.LastRunProcess!.Output);
21+
1722
return Task.CompletedTask;
1823
}
1924
}

0 commit comments

Comments
 (0)