Skip to content

Commit 20cc455

Browse files
authored
Merge pull request #460 from nblumhardt-ro/cli-skill-updates
Update skill, fix some CLI bugs and paper cuts
2 parents ce69818 + 757a2e6 commit 20cc455

22 files changed

Lines changed: 831 additions & 450 deletions

src/SeqCli/Cli/Command.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ protected virtual async Task<int> Run(string[] unrecognized)
8282
{
8383
if (unrecognized.Any())
8484
{
85-
ShowUsageErrors(new [] { "Unrecognized options: " + string.Join(", ", unrecognized) });
85+
ShowUsageErrors(["Unrecognized options: " + string.Join(", ", unrecognized)]);
8686
return 1;
8787
}
8888

src/SeqCli/Cli/CommandLineHost.cs

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,28 @@
1919
using System.Runtime.InteropServices;
2020
using System.Threading.Tasks;
2121
using Autofac.Features.Metadata;
22+
using SeqCli.Cli.Commands;
2223
using Serilog.Core;
2324
using Serilog.Events;
2425

2526
namespace SeqCli.Cli;
2627

27-
class CommandLineHost
28+
class CommandLineHost(IEnumerable<Meta<Lazy<Command>, CommandMetadata>> availableCommands)
2829
{
29-
readonly List<Meta<Lazy<Command>, CommandMetadata>> _availableCommands;
30-
31-
public CommandLineHost(IEnumerable<Meta<Lazy<Command>, CommandMetadata>> availableCommands)
32-
{
33-
_availableCommands = availableCommands.ToList();
34-
}
30+
readonly List<Meta<Lazy<Command>, CommandMetadata>> _availableCommands = availableCommands.ToList();
3531

3632
public async Task<int> Run(string[] args, LoggingLevelSwitch levelSwitch)
3733
{
3834
var ea = Assembly.GetEntryAssembly();
3935
var name = ea!.GetName().Name;
4036

41-
if (args.Length > 0)
37+
if (CommandAliases.RewriteArgs(
38+
ref args,
39+
out var commandName,
40+
out var subCommandName,
41+
out var featureVisibility,
42+
out var verbose))
4243
{
43-
const string prereleaseArg = "--pre", verboseArg = "--verbose";
44-
45-
var commandName = args[0].ToLowerInvariant();
46-
var subCommandName = args.Length > 1 && !args[1].Contains('-') ? args[1].ToLowerInvariant() : null;
47-
48-
var hiddenLegacyCommand = false;
49-
if (subCommandName == null && commandName == "config")
50-
{
51-
hiddenLegacyCommand = true;
52-
subCommandName = "legacy";
53-
}
54-
55-
var featureVisibility = FeatureVisibility.Visible | FeatureVisibility.Hidden;
56-
if (args.Any(a => a.Trim() is prereleaseArg))
57-
featureVisibility |= FeatureVisibility.Preview;
58-
5944
var currentPlatform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
6045
? SupportedPlatforms.Windows
6146
: SupportedPlatforms.Posix;
@@ -67,23 +52,16 @@ public async Task<int> Run(string[] args, LoggingLevelSwitch levelSwitch)
6752

6853
if (cmd != null)
6954
{
70-
var amountToSkip = cmd.Metadata.SubCommand == null || hiddenLegacyCommand ? 1 : 2;
71-
var commandSpecificArgs = args.Skip(amountToSkip).Where(arg => cmd.Metadata.Name == "help" || arg is not prereleaseArg).ToArray();
72-
73-
var verbose = commandSpecificArgs.Any(arg => arg == verboseArg);
7455
if (verbose)
75-
{
7656
levelSwitch.MinimumLevel = LogEventLevel.Information;
77-
commandSpecificArgs = commandSpecificArgs.Where(arg => arg != verboseArg).ToArray();
78-
}
7957

8058
var impl = cmd.Value.Value;
81-
return await impl.Invoke(commandSpecificArgs);
59+
return await impl.Invoke(args);
8260
}
8361
}
84-
62+
8563
Console.WriteLine($"Usage: {name} <command> [<args>]");
8664
Console.WriteLine($"Type `{name} help` for available commands");
8765
return 1;
8866
}
89-
}
67+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using SeqCli.Util;
2222
using Seq.Api.Model.Cluster;
2323
using SeqCli.Api;
24+
using SeqCli.Output;
2425
using Serilog;
2526

2627
namespace SeqCli.Cli.Commands.Cluster;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright © Datalust 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.Diagnostics.CodeAnalysis;
17+
using System.Linq;
18+
19+
namespace SeqCli.Cli.Commands;
20+
21+
static class CommandAliases
22+
{
23+
public static bool RewriteArgs(
24+
ref string[] args,
25+
[NotNullWhen(true)] out string? commandName,
26+
out string? subCommandName,
27+
out FeatureVisibility featureVisibility,
28+
out bool verbose)
29+
{
30+
if (args.Length == 0)
31+
{
32+
commandName = null;
33+
subCommandName = null;
34+
featureVisibility = FeatureVisibility.None;
35+
verbose = false;
36+
return false;
37+
}
38+
39+
featureVisibility = FeatureVisibility.Visible | FeatureVisibility.Hidden;
40+
if (args.Any(arg => IsFlag(arg, "pre")))
41+
{
42+
featureVisibility |= FeatureVisibility.Preview;
43+
args = args.Where(arg => !IsFlag(arg, "pre")).ToArray();
44+
}
45+
46+
verbose = args.Any(arg => IsFlag(arg, "verbose"));
47+
if (verbose)
48+
args = args.Where(arg => !IsFlag(arg, "verbose")).ToArray();
49+
50+
commandName = args[0].ToLowerInvariant();
51+
args = args.Skip(1).ToArray();
52+
53+
if (commandName == "--version")
54+
{
55+
commandName = "version";
56+
}
57+
else if (commandName == "--help")
58+
{
59+
commandName = "help";
60+
}
61+
62+
subCommandName = commandName != "help" && args.Length != 0 && !args[0].StartsWith('-') ? args[0].ToLowerInvariant() : null;
63+
if (subCommandName != null)
64+
{
65+
args = args.Skip(1).ToArray();
66+
}
67+
68+
if (Array.FindIndex(args, arg => IsFlag(arg, "help")) is var index and not -1)
69+
{
70+
args = args.Where((_, i) => i != index).ToArray();
71+
if (subCommandName != null)
72+
{
73+
args = [subCommandName, ..args];
74+
subCommandName = null;
75+
}
76+
args = [commandName, ..args];
77+
commandName = "help";
78+
}
79+
80+
if (subCommandName == null && commandName == "config")
81+
{
82+
subCommandName = "legacy";
83+
}
84+
85+
return true;
86+
}
87+
88+
static bool IsFlag(string flag, string flagName)
89+
{
90+
return flag.EndsWith(flagName, StringComparison.OrdinalIgnoreCase) &&
91+
flag[0] == '-' &&
92+
(flag.Length == flagName.Length + 1 ||
93+
flag.Length == flagName.Length + 2 && flag[1] == '-');
94+
}
95+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using SeqCli.Api;
2323
using SeqCli.Cli.Features;
2424
using SeqCli.Config;
25+
using SeqCli.Output;
2526
using Serilog;
2627

2728
namespace SeqCli.Cli.Commands.Node;

src/SeqCli/Cli/Commands/QueryCommand.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
using System;
1616
using System.Threading.Tasks;
17-
using Newtonsoft.Json;
17+
using Seq.Api.Client;
1818
using SeqCli.Api;
1919
using SeqCli.Cli.Features;
2020
using SeqCli.Config;
@@ -43,7 +43,7 @@ public QueryCommand()
4343
_range = Enable<DateRangeFeature>();
4444
_signal = Enable<SignalExpressionFeature>();
4545
_timeout = Enable<TimeoutFeature>();
46-
_output = Enable<OutputFormatFeature>();
46+
_output = Enable(new OutputFormatFeature(supportNative: true));
4747
_storagePath = Enable<StoragePathFeature>();
4848
Options.Add("trace", "Enable detailed (server-side) query tracing", _ => _trace = true);
4949
_connection = Enable<ConnectionFeature>();
@@ -63,19 +63,18 @@ protected override async Task<int> Run()
6363
var timeout = _timeout.ApplyTimeout(connection.Client.HttpClient);
6464

6565
var output = _output.GetOutputFormat(config);
66-
if (output.Json)
66+
if (output.Text)
6767
{
68-
var result = await connection.Data.QueryAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
69-
70-
// Some friendlier JSON output is definitely possible here
71-
Console.WriteLine(JsonConvert.SerializeObject(result));
68+
// We can fold this into the `WriteQueryResult` case once that path supports themes.
69+
var result = await connection.Data.QueryCsvAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
70+
output.WriteCsv(result);
7271
}
7372
else
7473
{
75-
var result = await connection.Data.QueryCsvAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
76-
output.WriteCsv(result);
74+
var result = await connection.Data.QueryAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
75+
output.WriteQueryResult(result);
7776
}
78-
77+
7978
return 0;
8079
}
8180
}

src/SeqCli/Cli/Commands/SearchCommand.cs

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,12 @@
1414

1515
using System;
1616
using System.Globalization;
17-
using System.Linq;
1817
using System.Threading.Tasks;
19-
using Newtonsoft.Json.Linq;
20-
using Seq.Api.Model.Events;
2118
using SeqCli.Api;
2219
using SeqCli.Cli.Features;
2320
using SeqCli.Config;
24-
using SeqCli.Mapping;
25-
using SeqCli.Util;
2621
using Serilog;
27-
using Serilog.Events;
28-
using Serilog.Parsing;
22+
2923
// ReSharper disable UnusedType.Global
3024

3125
namespace SeqCli.Cli.Commands;
@@ -56,7 +50,7 @@ public SearchCommand()
5650
v => _count = int.Parse(v, CultureInfo.InvariantCulture));
5751

5852
_range = Enable<DateRangeFeature>();
59-
_output = Enable<OutputFormatFeature>();
53+
_output = Enable(new OutputFormatFeature(supportNative: true));
6054
_storagePath = Enable<StoragePathFeature>();
6155
_signal = Enable<SignalExpressionFeature>();
6256

@@ -77,7 +71,7 @@ protected override async Task<int> Run()
7771
try
7872
{
7973
var config = RuntimeConfigurationLoader.Load(_storagePath);
80-
await using var output = _output.GetOutputFormat(config).CreateOutputLogger();
74+
var output = _output.GetOutputFormat(config);
8175
var connection = SeqConnectionFactory.Connect(_connection, config);
8276
connection.Client.HttpClient.Timeout = TimeSpan.FromMilliseconds(_httpClientTimeout);
8377

@@ -95,9 +89,10 @@ protected override async Task<int> Run()
9589
_count,
9690
fromDateUtc: _range.Start,
9791
toDateUtc: _range.End,
98-
trace: _trace))
92+
trace: _trace,
93+
render: output.RequiresRender))
9994
{
100-
output.Write(ToSerilogEvent(evt));
95+
output.WriteEventEntity(evt);
10196
}
10297

10398
return 0;
@@ -114,9 +109,10 @@ protected override async Task<int> Run()
114109
_count,
115110
fromDateUtc: _range.Start,
116111
toDateUtc: _range.End,
117-
trace: _trace))
112+
trace: _trace,
113+
render: output.RequiresRender))
118114
{
119-
output.Write(ToSerilogEvent(evt));
115+
output.WriteEventEntity(evt);
120116
}
121117

122118
return 0;
@@ -127,50 +123,4 @@ protected override async Task<int> Run()
127123
return 1;
128124
}
129125
}
130-
131-
internal static LogEvent ToSerilogEvent(EventEntity evt)
132-
{
133-
return new LogEvent(
134-
DateTimeOffset.ParseExact(evt.Timestamp, "o", CultureInfo.InvariantCulture).ToLocalTime(),
135-
LevelMapping.ToSerilogLevel(evt.Level),
136-
string.IsNullOrWhiteSpace(evt.Exception) ? null : new TextException(evt.Exception),
137-
new MessageTemplate(evt.MessageTemplateTokens.Select(ToMessageTemplateToken)),
138-
evt.Properties
139-
.Select(p => CreateProperty(p.Name, p.Value))
140-
);
141-
}
142-
143-
static MessageTemplateToken ToMessageTemplateToken(MessageTemplateTokenPart token)
144-
{
145-
// Not ideal, we lose renderings, alignment etc. here.
146-
147-
if (token.Text != null)
148-
return new TextToken(token.Text);
149-
return new PropertyToken(token.PropertyName, token.RawText ?? $"{{{token.PropertyName}}}");
150-
}
151-
152-
static LogEventProperty CreateProperty(string name, object value)
153-
{
154-
return LogEventPropertyFactory.SafeCreate(name, CreatePropertyValue(value));
155-
}
156-
157-
static LogEventPropertyValue CreatePropertyValue(object value)
158-
{
159-
switch (value)
160-
{
161-
case JObject jo:
162-
jo.TryGetValue("$typeTag", out var tt);
163-
return new StructureValue(
164-
jo.Properties()
165-
.Where(kvp => kvp.Name != "$typeTag")
166-
.Select(kvp => CreateProperty(kvp.Name, kvp.Value)),
167-
(tt as JValue)?.Value as string);
168-
169-
case JArray ja:
170-
return new SequenceValue(ja.Select(CreatePropertyValue));
171-
172-
default:
173-
return new ScalarValue(value);
174-
}
175-
}
176126
}

0 commit comments

Comments
 (0)