Skip to content

Commit caba478

Browse files
authored
Merge pull request #207 from nblumhardt/node-commands
node list, node health, node demote commands; -v universal verbose option
2 parents 4be8e14 + bd50ccb commit caba478

File tree

17 files changed

+420
-83
lines changed

17 files changed

+420
-83
lines changed

src/SeqCli/Cli/Command.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,16 @@ protected T Enable<T>(T t)
5151

5252
public void PrintUsage()
5353
{
54-
if (Options.Any())
54+
var allOptions = new OptionSet();
55+
foreach (var option in Options)
5556
{
56-
Console.Error.WriteLine("Arguments:");
57-
Options.WriteOptionDescriptions(Console.Error);
57+
allOptions.Add(option);
5858
}
59+
60+
allOptions.Add("v|verbose", "Print verbose output to `STDERR`", _ => { });
61+
62+
Console.Error.WriteLine("Arguments:");
63+
allOptions.WriteOptionDescriptions(Console.Error);
5964
}
6065

6166
public async Task<int> Invoke(string[] args)

src/SeqCli/Cli/CommandLineHost.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
using System.Reflection;
1919
using System.Threading.Tasks;
2020
using Autofac.Features.Metadata;
21+
using Serilog.Core;
22+
using Serilog.Events;
23+
24+
#nullable enable
2125

2226
namespace SeqCli.Cli
2327
{
@@ -30,10 +34,10 @@ public CommandLineHost(IEnumerable<Meta<Lazy<Command>, CommandMetadata>> availab
3034
_availableCommands = availableCommands.ToList();
3135
}
3236

33-
public async Task<int> Run(string[] args)
37+
public async Task<int> Run(string[] args, LoggingLevelSwitch levelSwitch)
3438
{
3539
var ea = Assembly.GetEntryAssembly();
36-
var name = ea.GetName().Name;
40+
var name = ea!.GetName().Name;
3741

3842
if (args.Length > 0)
3943
{
@@ -46,7 +50,16 @@ public async Task<int> Run(string[] args)
4650
if (cmd != null)
4751
{
4852
var amountToSkip = cmd.Metadata.SubCommand == null ? 1 : 2;
49-
return await cmd.Value.Value.Invoke(args.Skip(amountToSkip).ToArray());
53+
var commandSpecificArgs = args.Skip(amountToSkip).ToArray();
54+
55+
var verboseArg = commandSpecificArgs.FirstOrDefault(arg => arg is "-v" or "--verbose");
56+
if (verboseArg != null)
57+
{
58+
levelSwitch.MinimumLevel = LogEventLevel.Information;
59+
commandSpecificArgs = commandSpecificArgs.Where(arg => arg != verboseArg).ToArray();
60+
}
61+
62+
return await cmd.Value.Value.Invoke(commandSpecificArgs);
5063
}
5164
}
5265

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using Seq.Api.Model.Cluster;
5+
using SeqCli.Cli.Features;
6+
using SeqCli.Connection;
7+
using Serilog;
8+
9+
#nullable enable
10+
11+
namespace SeqCli.Cli.Commands.Node
12+
{
13+
[Command("node", "demote", "Begin demotion of the current leader node",
14+
Example = "seqcli node demote -v --wait")]
15+
class DemoteCommand : Command
16+
{
17+
readonly SeqConnectionFactory _connectionFactory;
18+
19+
readonly ConnectionFeature _connection;
20+
readonly ConfirmFeature _confirm;
21+
bool _wait;
22+
23+
public DemoteCommand(SeqConnectionFactory connectionFactory)
24+
{
25+
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
26+
27+
Options.Add("wait", "Wait for the leader to be demoted before exiting", _ => _wait = true);
28+
_confirm = Enable<ConfirmFeature>();
29+
30+
_connection = Enable<ConnectionFeature>();
31+
}
32+
33+
protected override async Task<int> Run()
34+
{
35+
var connection = _connectionFactory.Connect(_connection);
36+
37+
if (!_confirm.TryConfirm("This will demote the current cluster leader."))
38+
{
39+
await Console.Error.WriteLineAsync("Canceled by user.");
40+
return 1;
41+
}
42+
43+
var demoting = (await connection.ClusterNodes.ListAsync()).SingleOrDefault(n => n.Role == NodeRole.Leader);
44+
45+
if (demoting == null)
46+
{
47+
Log.Error("No cluster node is in the leader role");
48+
return 1;
49+
}
50+
51+
await connection.ClusterNodes.DemoteAsync(demoting);
52+
53+
if (!_wait)
54+
{
55+
Log.Information("Demotion of node {ClusterNodeId}/{ClusterNodeName} commenced", demoting.Id, demoting.Name);
56+
return 0;
57+
}
58+
59+
var lastStatus = demoting.StateDescription;
60+
Log.Information("Waiting for demotion of node {ClusterNodeId}/{ClusterNodeName} to complete ({LeaderNodeStatus})", demoting.Id, demoting.Name, lastStatus);
61+
62+
while (true)
63+
{
64+
await Task.Delay(100);
65+
66+
var nodes = await connection.ClusterNodes.ListAsync();
67+
demoting = nodes.FirstOrDefault(n => n.Id == demoting.Id);
68+
69+
if (demoting == null)
70+
{
71+
var newLeader = nodes.Single(n => n.Role == NodeRole.Leader);
72+
Log.Information("Demotion completed; old leader is out of service, new leader is {ClusterNodeId}/{ClusterNodeName}", newLeader.Id, newLeader.Name);
73+
return 0;
74+
}
75+
76+
if (demoting.StateDescription != lastStatus)
77+
{
78+
lastStatus = demoting.StateDescription;
79+
Log.Information("Demotion in progress ({LeaderNodeStatus})", lastStatus);
80+
}
81+
}
82+
}
83+
}
84+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using SeqCli.Cli.Features;
6+
using SeqCli.Connection;
7+
using SeqCli.Util;
8+
using Serilog;
9+
10+
#nullable enable
11+
12+
namespace SeqCli.Cli.Commands.Node
13+
{
14+
[Command("node", "health",
15+
"Probe a Seq node's `/health` endpoint, and print the returned HTTP status code, or 'Unreachable' if the endpoint could not be queried",
16+
Example = "seqcli node health -s https://seq-2.example.com")]
17+
class HealthCommand : Command
18+
{
19+
readonly SeqConnectionFactory _connectionFactory;
20+
21+
string? _profileName, _serverUrl;
22+
23+
public HealthCommand(SeqConnectionFactory connectionFactory)
24+
{
25+
_connectionFactory = connectionFactory;
26+
27+
Options.Add("s=|server=",
28+
"The URL of the Seq server; by default the `connection.serverUrl` config value will be used",
29+
v => _serverUrl = ArgumentString.Normalize(v));
30+
31+
Options.Add("profile=",
32+
"A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used",
33+
v => _profileName = ArgumentString.Normalize(v));
34+
}
35+
36+
protected override async Task<int> Run()
37+
{
38+
// An API key is not accepted; we don't want to imply that /health requires authentication.
39+
var surrogateConnectionFeature = new ConnectionFeature { ProfileName = _profileName, Url = _serverUrl };
40+
var connection = _connectionFactory.Connect(surrogateConnectionFeature);
41+
42+
try
43+
{
44+
var response = await connection.Client.HttpClient.GetAsync("health");
45+
Console.WriteLine($"HTTP {response.Version} {((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)} {response.ReasonPhrase}");
46+
foreach (var (key, values) in response.Headers.Concat(response.Content.Headers))
47+
foreach (var value in values)
48+
{
49+
Console.WriteLine($"{key}: {value}");
50+
}
51+
Console.WriteLine(await response.Content.ReadAsStringAsync());
52+
return response.IsSuccessStatusCode ? 0 : 1;
53+
}
54+
catch (Exception ex)
55+
{
56+
Log.Information(ex, "Exception thrown when calling health endpoint");
57+
58+
Console.WriteLine("Unreachable");
59+
Console.WriteLine(Presentation.FormattedMessage(ex));
60+
return 1;
61+
}
62+
}
63+
}
64+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright Datalust Pty Ltd and Contributors
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+
using System;
16+
using System.Linq;
17+
using System.Threading.Tasks;
18+
using SeqCli.Cli.Features;
19+
using SeqCli.Config;
20+
using SeqCli.Connection;
21+
22+
#nullable enable
23+
24+
namespace SeqCli.Cli.Commands.Node
25+
{
26+
[Command("node", "list", "List nodes in the Seq cluster",
27+
Example = "seqcli node list --json")]
28+
class ListCommand : Command
29+
{
30+
readonly SeqConnectionFactory _connectionFactory;
31+
32+
readonly ConnectionFeature _connection;
33+
readonly OutputFormatFeature _output;
34+
35+
string? _name, _id;
36+
37+
public ListCommand(SeqConnectionFactory connectionFactory, SeqCliOutputConfig outputConfig)
38+
{
39+
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
40+
41+
Options.Add(
42+
"n=|name=",
43+
"The name of the cluster node to list",
44+
n => _name = n);
45+
46+
Options.Add(
47+
"i=|id=",
48+
"The id of a single cluster node to list",
49+
id => _id = id);
50+
51+
_output = Enable(new OutputFormatFeature(outputConfig));
52+
_connection = Enable<ConnectionFeature>();
53+
}
54+
55+
protected override async Task<int> Run()
56+
{
57+
var connection = _connectionFactory.Connect(_connection);
58+
59+
var list = _id != null ?
60+
new[] { await connection.ClusterNodes.FindAsync(_id) } :
61+
(await connection.ClusterNodes.ListAsync())
62+
.Where(n => _name == null || _name == n.Name)
63+
.ToArray();
64+
65+
foreach (var clusterNode in list)
66+
{
67+
_output.WriteEntity(clusterNode);
68+
}
69+
70+
return 0;
71+
}
72+
}
73+
}

src/SeqCli/Cli/Commands/SearchCommand.cs

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -65,41 +65,39 @@ protected override async Task<int> Run()
6565
{
6666
try
6767
{
68-
using (var output = _output.CreateOutputLogger())
69-
{
70-
var connection = _connectionFactory.Connect(_connection);
68+
using var output = _output.CreateOutputLogger();
69+
var connection = _connectionFactory.Connect(_connection);
7170

72-
string filter = null;
73-
if (!string.IsNullOrWhiteSpace(_filter))
74-
filter = (await connection.Expressions.ToStrictAsync(_filter)).StrictExpression;
71+
string filter = null;
72+
if (!string.IsNullOrWhiteSpace(_filter))
73+
filter = (await connection.Expressions.ToStrictAsync(_filter)).StrictExpression;
7574

76-
string nextPageAfterId = null;
77-
var remaining = _count;
78-
while (remaining > 0)
75+
string nextPageAfterId = null;
76+
var remaining = _count;
77+
while (remaining > 0)
78+
{
79+
var page = await connection.Events.InSignalAsync(
80+
null,
81+
_signal.Signal,
82+
filter: filter,
83+
count: remaining,
84+
fromDateUtc: _range.Start,
85+
toDateUtc: _range.End,
86+
afterId: nextPageAfterId);
87+
88+
nextPageAfterId = page.Statistics.LastReadEventId;
89+
90+
foreach (var evt in page.Events)
7991
{
80-
var page = await connection.Events.InSignalAsync(
81-
null,
82-
_signal.Signal,
83-
filter: filter,
84-
count: remaining,
85-
fromDateUtc: _range.Start,
86-
toDateUtc: _range.End,
87-
afterId: nextPageAfterId);
88-
89-
nextPageAfterId = page.Statistics.LastReadEventId;
90-
91-
foreach (var evt in page.Events)
92-
{
93-
output.Write(ToSerilogEvent(evt));
94-
remaining--;
95-
96-
if (remaining == 0)
97-
break;
98-
}
99-
100-
if (page.Statistics.Status != ResultSetStatus.Partial)
92+
output.Write(ToSerilogEvent(evt));
93+
remaining--;
94+
95+
if (remaining == 0)
10196
break;
10297
}
98+
99+
if (page.Statistics.Status != ResultSetStatus.Partial)
100+
break;
103101
}
104102

105103
return 0;

src/SeqCli/Cli/Commands/TailCommand.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,18 @@ protected override async Task<int> Run()
6161
strict = converted.StrictExpression;
6262
}
6363

64-
using (var output = _output.CreateOutputLogger())
65-
using (var stream = await connection.Events.StreamAsync<JObject>(
64+
using var output = _output.CreateOutputLogger();
65+
using var stream = await connection.Events.StreamAsync<JObject>(
6666
filter: strict,
6767
signal: _signal.Signal,
68-
cancellationToken: cancel.Token))
69-
{
70-
var subscription = stream
71-
.Select(JsonLogEventReader.ReadFromJObject)
72-
.Subscribe(evt => output.Write(evt), () => cancel.Cancel());
68+
cancellationToken: cancel.Token);
69+
var subscription = stream
70+
.Select(JsonLogEventReader.ReadFromJObject)
71+
// ReSharper disable once AccessToDisposedClosure
72+
.Subscribe(evt => output.Write(evt), () => cancel.Cancel());
7373

74-
cancel.Token.WaitHandle.WaitOne();
75-
subscription.Dispose();
76-
}
74+
cancel.Token.WaitHandle.WaitOne();
75+
subscription.Dispose();
7776

7877
return 0;
7978
}

0 commit comments

Comments
 (0)