Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/SeqCli/Cli/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected virtual async Task<int> Run(string[] unrecognized)
{
if (unrecognized.Any())
{
ShowUsageErrors(new [] { "Unrecognized options: " + string.Join(", ", unrecognized) });
ShowUsageErrors(["Unrecognized options: " + string.Join(", ", unrecognized)]);
return 1;
}

Expand Down
46 changes: 12 additions & 34 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,28 @@
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Autofac.Features.Metadata;
using SeqCli.Cli.Commands;
using Serilog.Core;
using Serilog.Events;

namespace SeqCli.Cli;

class CommandLineHost
class CommandLineHost(IEnumerable<Meta<Lazy<Command>, CommandMetadata>> availableCommands)
{
readonly List<Meta<Lazy<Command>, CommandMetadata>> _availableCommands;

public CommandLineHost(IEnumerable<Meta<Lazy<Command>, CommandMetadata>> availableCommands)
{
_availableCommands = availableCommands.ToList();
}
readonly List<Meta<Lazy<Command>, CommandMetadata>> _availableCommands = availableCommands.ToList();

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

if (args.Length > 0)
if (CommandAliases.RewriteArgs(
ref args,
out var commandName,
out var subCommandName,
out var featureVisibility,
out var verbose))
{
const string prereleaseArg = "--pre", verboseArg = "--verbose";

var commandName = args[0].ToLowerInvariant();
var subCommandName = args.Length > 1 && !args[1].Contains('-') ? args[1].ToLowerInvariant() : null;

var hiddenLegacyCommand = false;
if (subCommandName == null && commandName == "config")
{
hiddenLegacyCommand = true;
subCommandName = "legacy";
}

var featureVisibility = FeatureVisibility.Visible | FeatureVisibility.Hidden;
if (args.Any(a => a.Trim() is prereleaseArg))
featureVisibility |= FeatureVisibility.Preview;

var currentPlatform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? SupportedPlatforms.Windows
: SupportedPlatforms.Posix;
Expand All @@ -67,23 +52,16 @@ public async Task<int> Run(string[] args, LoggingLevelSwitch levelSwitch)

if (cmd != null)
{
var amountToSkip = cmd.Metadata.SubCommand == null || hiddenLegacyCommand ? 1 : 2;
var commandSpecificArgs = args.Skip(amountToSkip).Where(arg => cmd.Metadata.Name == "help" || arg is not prereleaseArg).ToArray();

var verbose = commandSpecificArgs.Any(arg => arg == verboseArg);
if (verbose)
{
levelSwitch.MinimumLevel = LogEventLevel.Information;
commandSpecificArgs = commandSpecificArgs.Where(arg => arg != verboseArg).ToArray();
}

var impl = cmd.Value.Value;
return await impl.Invoke(commandSpecificArgs);
return await impl.Invoke(args);
}
}

Console.WriteLine($"Usage: {name} <command> [<args>]");
Console.WriteLine($"Type `{name} help` for available commands");
return 1;
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/Cli/Commands/Cluster/HealthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using SeqCli.Util;
using Seq.Api.Model.Cluster;
using SeqCli.Api;
using SeqCli.Output;
using Serilog;

namespace SeqCli.Cli.Commands.Cluster;
Expand Down
95 changes: 95 additions & 0 deletions src/SeqCli/Cli/Commands/CommandAliases.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright © Datalust and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace SeqCli.Cli.Commands;

static class CommandAliases
{
public static bool RewriteArgs(
ref string[] args,
[NotNullWhen(true)] out string? commandName,
out string? subCommandName,
out FeatureVisibility featureVisibility,
out bool verbose)
{
if (args.Length == 0)
{
commandName = null;
subCommandName = null;
featureVisibility = FeatureVisibility.None;
verbose = false;
return false;
}

featureVisibility = FeatureVisibility.Visible | FeatureVisibility.Hidden;
if (args.Any(arg => IsFlag(arg, "pre")))
{
featureVisibility |= FeatureVisibility.Preview;
args = args.Where(arg => !IsFlag(arg, "pre")).ToArray();
}

verbose = args.Any(arg => IsFlag(arg, "verbose"));
if (verbose)
args = args.Where(arg => !IsFlag(arg, "verbose")).ToArray();

commandName = args[0].ToLowerInvariant();
args = args.Skip(1).ToArray();

if (commandName == "--version")
{
commandName = "version";
}
else if (commandName == "--help")
{
commandName = "help";
}

subCommandName = commandName != "help" && args.Length != 0 && !args[0].StartsWith('-') ? args[0].ToLowerInvariant() : null;
if (subCommandName != null)
{
args = args.Skip(1).ToArray();
}

if (Array.FindIndex(args, arg => IsFlag(arg, "help")) is var index and not -1)
{
args = args.Where((_, i) => i != index).ToArray();
if (subCommandName != null)
{
args = [subCommandName, ..args];
subCommandName = null;
}
args = [commandName, ..args];
commandName = "help";
}

if (subCommandName == null && commandName == "config")
{
subCommandName = "legacy";
}

return true;
}

static bool IsFlag(string flag, string flagName)
{
return flag.EndsWith(flagName, StringComparison.OrdinalIgnoreCase) &&
flag[0] == '-' &&
(flag.Length == flagName.Length + 1 ||
flag.Length == flagName.Length + 2 && flag[1] == '-');
}
}
1 change: 1 addition & 0 deletions src/SeqCli/Cli/Commands/Node/HealthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
using SeqCli.Output;
using Serilog;

namespace SeqCli.Cli.Commands.Node;
Expand Down
19 changes: 9 additions & 10 deletions src/SeqCli/Cli/Commands/QueryCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Seq.Api.Client;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
Expand Down Expand Up @@ -43,7 +43,7 @@ public QueryCommand()
_range = Enable<DateRangeFeature>();
_signal = Enable<SignalExpressionFeature>();
_timeout = Enable<TimeoutFeature>();
_output = Enable<OutputFormatFeature>();
_output = Enable(new OutputFormatFeature(supportNative: true));
_storagePath = Enable<StoragePathFeature>();
Options.Add("trace", "Enable detailed (server-side) query tracing", _ => _trace = true);
_connection = Enable<ConnectionFeature>();
Expand All @@ -63,19 +63,18 @@ protected override async Task<int> Run()
var timeout = _timeout.ApplyTimeout(connection.Client.HttpClient);

var output = _output.GetOutputFormat(config);
if (output.Json)
if (output.Text)
{
var result = await connection.Data.QueryAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);

// Some friendlier JSON output is definitely possible here
Console.WriteLine(JsonConvert.SerializeObject(result));
// We can fold this into the `WriteQueryResult` case once that path supports themes.
var result = await connection.Data.QueryCsvAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
output.WriteCsv(result);
}
else
{
var result = await connection.Data.QueryCsvAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
output.WriteCsv(result);
var result = await connection.Data.QueryAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace);
output.WriteQueryResult(result);
}

return 0;
}
}
68 changes: 9 additions & 59 deletions src/SeqCli/Cli/Commands/SearchCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@

using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Seq.Api.Model.Events;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
using SeqCli.Mapping;
using SeqCli.Util;
using Serilog;
using Serilog.Events;
using Serilog.Parsing;

// ReSharper disable UnusedType.Global

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

_range = Enable<DateRangeFeature>();
_output = Enable<OutputFormatFeature>();
_output = Enable(new OutputFormatFeature(supportNative: true));
_storagePath = Enable<StoragePathFeature>();
_signal = Enable<SignalExpressionFeature>();

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

Expand All @@ -95,9 +89,10 @@ protected override async Task<int> Run()
_count,
fromDateUtc: _range.Start,
toDateUtc: _range.End,
trace: _trace))
trace: _trace,
render: output.RequiresRender))
{
output.Write(ToSerilogEvent(evt));
output.WriteEventEntity(evt);
}

return 0;
Expand All @@ -114,9 +109,10 @@ protected override async Task<int> Run()
_count,
fromDateUtc: _range.Start,
toDateUtc: _range.End,
trace: _trace))
trace: _trace,
render: output.RequiresRender))
{
output.Write(ToSerilogEvent(evt));
output.WriteEventEntity(evt);
}

return 0;
Expand All @@ -127,50 +123,4 @@ protected override async Task<int> Run()
return 1;
}
}

internal static LogEvent ToSerilogEvent(EventEntity evt)
{
return new LogEvent(
DateTimeOffset.ParseExact(evt.Timestamp, "o", CultureInfo.InvariantCulture).ToLocalTime(),
LevelMapping.ToSerilogLevel(evt.Level),
string.IsNullOrWhiteSpace(evt.Exception) ? null : new TextException(evt.Exception),
new MessageTemplate(evt.MessageTemplateTokens.Select(ToMessageTemplateToken)),
evt.Properties
.Select(p => CreateProperty(p.Name, p.Value))
);
}

static MessageTemplateToken ToMessageTemplateToken(MessageTemplateTokenPart token)
{
// Not ideal, we lose renderings, alignment etc. here.

if (token.Text != null)
return new TextToken(token.Text);
return new PropertyToken(token.PropertyName, token.RawText ?? $"{{{token.PropertyName}}}");
}

static LogEventProperty CreateProperty(string name, object value)
{
return LogEventPropertyFactory.SafeCreate(name, CreatePropertyValue(value));
}

static LogEventPropertyValue CreatePropertyValue(object value)
{
switch (value)
{
case JObject jo:
jo.TryGetValue("$typeTag", out var tt);
return new StructureValue(
jo.Properties()
.Where(kvp => kvp.Name != "$typeTag")
.Select(kvp => CreateProperty(kvp.Name, kvp.Value)),
(tt as JValue)?.Value as string);

case JArray ja:
return new SequenceValue(ja.Select(CreatePropertyValue));

default:
return new ScalarValue(value);
}
}
}
Loading
Loading