diff --git a/README.md b/README.md index 00b62546..f84ae153 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Stream log events matching a filter. Print the current executable version. -## Extraction Patterns +## Extraction patterns The `seqcli ingest` command can be used for parsing plain text logs into structured log events. @@ -201,7 +201,7 @@ Different matchers are needed so that a piece of text like `200OK` can be separa There are three kinds of matchers: - * Matchers like `alpha` and `nat` are built-in _named_ matchers. These are built-in. + * Matchers like `alpha` and `nat` are built-in _named_ matchers. * The special matchers `*`, `**` and so-on, are _non-greedy content_ matchers; these will match any text up until the next pattern element matches (`*`), the next two elements match, and so-on. We saw this in action with the `{@m:*}{:n}` elements in the example - the message is all of the text up until the next newline. * More complex _compound_ matchers are described using a sub-expression. These are prefixed with an equals sign `=`, like `{Phone:={:nat}-{:nat}-{:nat}}`. This will extract chunks of text like `123-456-7890` into the `Phone` property. diff --git a/src/SeqCli/Cli/CommandLineHost.cs b/src/SeqCli/Cli/CommandLineHost.cs index bc725e3c..bf78fb1d 100644 --- a/src/SeqCli/Cli/CommandLineHost.cs +++ b/src/SeqCli/Cli/CommandLineHost.cs @@ -38,11 +38,14 @@ public async Task Run(string[] args) if (args.Length > 0) { var norm = args[0].ToLowerInvariant(); - var subCommandNorm = args.Length > 1 && !args[1].Contains("-") ? args[1].ToLowerInvariant() : default; - var cmd = _availableCommands.SingleOrDefault(c => c.Metadata.Name == norm && c.Metadata.SubCommand == subCommandNorm); + var subCommandNorm = args.Length > 1 && !args[1].Contains("-") ? args[1].ToLowerInvariant() : null; + + var cmd = _availableCommands.SingleOrDefault(c => + c.Metadata.Name == norm && (c.Metadata.SubCommand == subCommandNorm || c.Metadata.SubCommand == null)); + if (cmd != null) { - var amountToSkip = subCommandNorm == default ? 1 : 2; + var amountToSkip = cmd.Metadata.SubCommand == null ? 1 : 2; return await cmd.Value.Value.Invoke(args.Skip(amountToSkip).ToArray()); } } diff --git a/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs index 393c9bb7..432f75a5 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs @@ -1,23 +1,26 @@ using System; -using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json; using SeqCli.Cli.Features; +using SeqCli.Config; using SeqCli.Connection; using Serilog; namespace SeqCli.Cli.Commands.ApiKey { - [Command("apikey", "list", "List of API Keys", Example = - "seqcli apikey list")] + [Command("apikey", "list", "List API keys on the server", Example="seqcli apikey list")] class ListCommand : Command { - private readonly SeqConnectionFactory _connectionFactory; - private readonly ConnectionFeature _connection; + readonly SeqConnectionFactory _connectionFactory; + + readonly ConnectionFeature _connection; + readonly OutputFormatFeature _output; - public ListCommand(SeqConnectionFactory connectionFactory) + public ListCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config) { - _connectionFactory = connectionFactory; + if (config == null) throw new ArgumentNullException(nameof(config)); + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + _output = Enable(new OutputFormatFeature(config.Output)); _connection = Enable(); } @@ -27,25 +30,12 @@ protected override async Task Run() var apiKeys = await connection.ApiKeys.ListAsync(); Log.Debug("Retrieved ApiKeys {@ApiKeys}", apiKeys); - var data = apiKeys.Select(a => new - { - a.Title, - a.Id, - a.Token, - a.MinimumLevel, - a.AppliedProperties, - a.CanActAsPrincipal, - a.InputFilter, - a.UseServerTimestamps, - a.IsDefault - }); - foreach (var apiKey in data) + foreach (var apiKey in apiKeys) { - var apiKeyString = JsonConvert.SerializeObject(apiKey); - - Console.WriteLine(apiKeyString); + _output.WriteEntity(apiKey); } + return 0; } } diff --git a/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs index 84b43b2f..98fb39e7 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs @@ -9,14 +9,14 @@ namespace SeqCli.Cli.Commands.ApiKey { - [Command("apikey", "remove", "Remove API Key from the server", Example = - "seqcli apikey remove -t TestApiKey")] + [Command("apikey", "remove", "Remove an API key from the server", + Example="seqcli apikey remove -t TestApiKey")] class RemoveCommand : Command { - private readonly SeqConnectionFactory _connectionFactory; - private readonly ConnectionFeature _connection; - private string _title; - private string _id; + readonly SeqConnectionFactory _connectionFactory; + readonly ConnectionFeature _connection; + string _title; + string _id; public RemoveCommand(SeqConnectionFactory connectionFactory) { diff --git a/src/SeqCli/Cli/Commands/HelpCommand.cs b/src/SeqCli/Cli/Commands/HelpCommand.cs index dda56ae8..13161ada 100644 --- a/src/SeqCli/Cli/Commands/HelpCommand.cs +++ b/src/SeqCli/Cli/Commands/HelpCommand.cs @@ -37,13 +37,29 @@ protected override Task Run(string[] unrecognised) { var ea = Assembly.GetEntryAssembly(); var name = ea.GetName().Name; - + + if (_markdown) + { + if (unrecognised.Length != 0) + return base.Run(unrecognised); + + PrintMarkdownHelp(name); + return Task.FromResult(0); + } + + string topLevelCommand = null; if (unrecognised.Length > 0) { - var target = unrecognised[0].ToLowerInvariant(); - var cmd = _availableCommands.SingleOrDefault(c => c.Metadata.Name == target); - if (cmd != null) + topLevelCommand = unrecognised[0].ToLowerInvariant(); + var subCommand = unrecognised.Length > 1 && !unrecognised[1].Contains("-") ? unrecognised[1] : null; + var cmds = _availableCommands.Where(c => c.Metadata.Name == topLevelCommand && + (subCommand == null || subCommand == c.Metadata.SubCommand)).ToArray(); + if (cmds.Length == 0) + return base.Run(unrecognised); + + if (cmds.Length == 1) { + var cmd = cmds.Single(); var argHelp = cmd.Value.Value.HasArgs ? " []" : ""; Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp); Console.WriteLine(); @@ -52,19 +68,13 @@ protected override Task Run(string[] unrecognised) cmd.Value.Value.PrintUsage(); return Task.FromResult(0); } - - return base.Run(unrecognised); } - if (_markdown) - { - PrintMarkdownHelp(name); - } + if (topLevelCommand != null) + PrintHelp(name, topLevelCommand); else - { PrintHelp(name); - } - + return Task.FromResult(0); } @@ -126,17 +136,40 @@ void PrintHelp(string executableName) Console.WriteLine(); Console.WriteLine("Available commands are:"); - foreach (var availableCommand in _availableCommands) + var printedGroups = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var avail in _availableCommands) { - Printing.Define( - " " + availableCommand.Metadata.Name, - availableCommand.Metadata.HelpText, - 13, - Console.Out); + if (avail.Metadata.SubCommand != null) + { + if (!printedGroups.Contains(avail.Metadata.Name)) + { + Printing.Define($" {avail.Metadata.Name}", "", 13, Console.Out); + printedGroups.Add(avail.Metadata.Name); + } + } + else + { + Printing.Define($" {avail.Metadata.Name}", avail.Metadata.HelpText, 13, Console.Out); + } } Console.WriteLine(); Console.WriteLine($"Type `{executableName} help ` for detailed help"); } + + void PrintHelp(string executableName, string topLevelCommand) + { + Console.WriteLine($"Usage: {executableName} {topLevelCommand} []"); + Console.WriteLine(); + Console.WriteLine("Available sub-commands are:"); + + foreach (var avail in _availableCommands.Where(c => c.Metadata.Name == topLevelCommand)) + { + Printing.Define($" {avail.Metadata.SubCommand}", avail.Metadata.HelpText, 13, Console.Out); + } + + Console.WriteLine(); + Console.WriteLine($"Type `{executableName} help {topLevelCommand} ` for detailed help"); + } } } \ No newline at end of file diff --git a/src/SeqCli/Cli/Features/OutputFormatFeature.cs b/src/SeqCli/Cli/Features/OutputFormatFeature.cs index 506b231b..6cfbd0a7 100644 --- a/src/SeqCli/Cli/Features/OutputFormatFeature.cs +++ b/src/SeqCli/Cli/Features/OutputFormatFeature.cs @@ -13,6 +13,10 @@ // limitations under the License. using System; +using Destructurama; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Seq.Api.Model; using SeqCli.Config; using SeqCli.Csv; using SeqCli.Output; @@ -75,5 +79,33 @@ public void WriteCsv(string csv) CsvWriter.WriteCsv(tokens, Theme, Console.Out, true); } } + + public void WriteEntity(Entity entity) + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + + var jo = JObject.FromObject(entity); + + if (_json) + { + jo.Remove("Links"); + // Proof-of-concept; this is a very inefficient + // way to write colorized JSON ;) + + var writer = new LoggerConfiguration() + .Destructure.JsonNetTypes() + .Enrich.With() + .WriteTo.Console( + outputTemplate: "{@Message:j}{NewLine}", + theme: Theme) + .CreateLogger(); + writer.Information("{@Entity}", jo); + } + else + { + var dyn = (dynamic) jo; + Console.WriteLine($"{entity.Id} {dyn.Title ?? dyn.Name}"); + } + } } } diff --git a/src/SeqCli/Output/StripStructureTypeEnricher.cs b/src/SeqCli/Output/StripStructureTypeEnricher.cs new file mode 100644 index 00000000..b50b1831 --- /dev/null +++ b/src/SeqCli/Output/StripStructureTypeEnricher.cs @@ -0,0 +1,25 @@ +using System.Linq; +using Serilog.Core; +using Serilog.Data; +using Serilog.Events; + +namespace SeqCli.Output +{ + public class StripStructureTypeEnricher : LogEventPropertyValueRewriter, ILogEventEnricher + { + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + foreach (var property in logEvent.Properties) + { + var updated = new LogEventProperty(property.Key, Visit(null, property.Value)); + logEvent.AddOrUpdateProperty(updated); + } + } + + protected override LogEventPropertyValue VisitStructureValue(object state, StructureValue structure) + { + return new StructureValue(structure.Properties.Select(p => + new LogEventProperty(p.Name, Visit(null, p.Value)))); + } + } +} \ No newline at end of file diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index 0292c67e..75f4ec66 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -18,6 +18,7 @@ + diff --git a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs index 049c24df..732bcd7a 100644 --- a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs +++ b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs @@ -1,12 +1,9 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Autofac.Features.Metadata; using SeqCli.Cli; -using SeqCli.Cli.Commands; using Xunit; namespace SeqCli.Tests.Cli @@ -21,10 +18,10 @@ public async Task CheckCommandLineHostPicksCorrectCommand() { new Meta, CommandMetadata>( new Lazy(() => new ActionCommand(() => commandsRan.Add("test"))), - new CommandMetadata() {Name = "test"}), + new CommandMetadata {Name = "test"}), new Meta, CommandMetadata>( new Lazy(() => new ActionCommand(() => commandsRan.Add("test2"))), - new CommandMetadata() {Name = "test2"}) + new CommandMetadata {Name = "test2"}) }; var commandLineHost = new CommandLineHost(availableCommands); await commandLineHost.Run(new []{ "test"}); @@ -32,46 +29,6 @@ public async Task CheckCommandLineHostPicksCorrectCommand() Assert.Equal(commandsRan.First(), "test"); } - [Fact] - public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect() - { - var commandsRan = new List(); - var availableCommands = - new List, CommandMetadata>> - { - new Meta, CommandMetadata>( - new Lazy(() => new ActionCommand(() => commandsRan.Add("test"))), - new CommandMetadata() {Name = "test"}), - new Meta, CommandMetadata>( - new Lazy(() => new ActionCommand(() => commandsRan.Add("test-subcommand"))), - new CommandMetadata() {Name = "test", SubCommand = "subcommand"}) - }; - var commandLineHost = new CommandLineHost(availableCommands); - await commandLineHost.Run(new[] { "test" }); - - Assert.Equal(commandsRan.First(), "test"); - } - - [Fact] - public async Task WhenCommandAndSubcommandAndTheUserRunsWithSubcommandEnsurePickedCorrect() - { - var commandsRan = new List(); - var availableCommands = - new List, CommandMetadata>> - { - new Meta, CommandMetadata>( - new Lazy(() => new ActionCommand(() => commandsRan.Add("test"))), - new CommandMetadata() {Name = "test"}), - new Meta, CommandMetadata>( - new Lazy(() => new ActionCommand(() => commandsRan.Add("test-subcommand"))), - new CommandMetadata() {Name = "test", SubCommand = "subcommand"}) - }; - var commandLineHost = new CommandLineHost(availableCommands); - await commandLineHost.Run(new[] { "test", "subcommand" }); - - Assert.Equal(commandsRan.First(), "test-subcommand"); - } - [Fact] public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePickedCorrect() { @@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic { new Meta, CommandMetadata>( new Lazy(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))), - new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}), + new CommandMetadata {Name = "test", SubCommand = "subcommand1"}), new Meta, CommandMetadata>( new Lazy(() => new ActionCommand(() => commandsRan.Add("test-subcommand2"))), - new CommandMetadata() {Name = "test", SubCommand = "subcommand2"}) + new CommandMetadata {Name = "test", SubCommand = "subcommand2"}) }; var commandLineHost = new CommandLineHost(availableCommands); await commandLineHost.Run(new[] { "test", "subcommand2" });