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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
4 changes: 2 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand DownExpand Up@@ -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.

Expand Down
9 changes: 6 additions & 3 deletions src/SeqCli/Cli/CommandLineHost.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,11 +38,14 @@ public async Task<int> 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());
}
}
Expand Down
38 changes: 14 additions & 24 deletions src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs
Original file line numberDiff line numberDiff line change
@@ -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<ConnectionFeature>();
}

Expand All@@ -27,25 +30,12 @@ protected override async Task<int> 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;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
{
Expand Down
71 changes: 52 additions & 19 deletions src/SeqCli/Cli/Commands/HelpCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,13 +37,29 @@ protected override Task<int> 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 ? " [<args>]" : "";
Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp);
Console.WriteLine();
Expand All@@ -52,19 +68,13 @@ protected override Task<int> 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);
}

Expand DownExpand Up@@ -126,17 +136,40 @@ void PrintHelp(string executableName)
Console.WriteLine();
Console.WriteLine("Available commands are:");

foreach (var availableCommand in _availableCommands)
var printedGroups = new HashSet<string>(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}", "<sub-command>", 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 <command>` for detailed help");
}

void PrintHelp(string executableName, string topLevelCommand)
{
Console.WriteLine($"Usage: {executableName} {topLevelCommand} <sub-command> [<args>]");
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} <sub-command>` for detailed help");
}
}
}
32 changes: 32 additions & 0 deletions src/SeqCli/Cli/Features/OutputFormatFeature.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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<StripStructureTypeEnricher>()
.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}");
}
}
}
}
25 changes: 25 additions & 0 deletions src/SeqCli/Output/StripStructureTypeEnricher.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Serilog.Core;
using Serilog.Data;
using Serilog.Events;

namespace SeqCli.Output
{
public class StripStructureTypeEnricher : LogEventPropertyValueRewriter<object>, 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))));
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/SeqCli.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Destructurama.JsonNet" Version="1.2.0" />
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="Serilog" Version="2.6.0" />
<PackageReference Include="serilog.filters.expressions" Version="1.1.0" />
Expand Down
51 changes: 4 additions & 47 deletions test/SeqCli.Tests/Cli/CommandLineHostTests.cs
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -21,57 +18,17 @@ public async Task CheckCommandLineHostPicksCorrectCommand()
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new CommandMetadata {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test2"))),
new CommandMetadata() {Name = "test2"})
new CommandMetadata {Name = "test2"})
};
var commandLineHost = new CommandLineHost(availableCommands);
await commandLineHost.Run(new []{ "test"});

Assert.Equal(commandsRan.First(), "test");
}

[Fact]
public async Task WhenCommandAndSubcommandAndTheUserRunsWithoutSubcommandEnsurePickedCorrect()
{
var commandsRan = new List<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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<string>();
var availableCommands =
new List<Meta<Lazy<Command>, CommandMetadata>>
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test"))),
new CommandMetadata() {Name = "test"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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()
{
Expand All@@ -81,10 +38,10 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic
{
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))),
new CommandMetadata() {Name = "test", SubCommand = "subcommand1"}),
new CommandMetadata {Name = "test", SubCommand = "subcommand1"}),
new Meta<Lazy<Command>, CommandMetadata>(
new Lazy<Command>(() => 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" });
Expand Down