diff --git a/global.json b/global.json index 1ee82c37..1658e451 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.203" + "version": "8.0.204" } } diff --git a/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs b/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs new file mode 100644 index 00000000..5fc39086 --- /dev/null +++ b/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs @@ -0,0 +1,70 @@ +// Copyright © Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading.Tasks; +using Seq.Api.Model.Signals; +using SeqCli.Cli.Features; +using SeqCli.Config; +using SeqCli.Connection; +using SeqCli.Signals; +using SeqCli.Syntax; +using SeqCli.Util; +using Serilog; + +namespace SeqCli.Cli.Commands.ExpressionIndex; + +[Command("expressionindex", "create", "Create an expression index", + Example = "seqcli expressionindex create --expression \"ServerName\"")] +class CreateCommand : Command +{ + readonly SeqConnectionFactory _connectionFactory; + + readonly ConnectionFeature _connection; + readonly OutputFormatFeature _output; + + string? _expression; + + public CreateCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + Options.Add( + "e=|expression=", + "The expression to index", + v => _expression = ArgumentString.Normalize(v)); + + _connection = Enable(); + _output = Enable(new OutputFormatFeature(config.Output)); + } + + protected override async Task Run() + { + var connection = _connectionFactory.Connect(_connection); + + if (string.IsNullOrEmpty(_expression)) + { + Log.Error("An `expression` must be specified"); + return 1; + } + + var index = await connection.ExpressionIndexes.TemplateAsync(); + index.Expression = _expression; + index = await connection.ExpressionIndexes.AddAsync(index); + + _output.WriteEntity(index); + + return 0; + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs b/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs new file mode 100644 index 00000000..6836d3f1 --- /dev/null +++ b/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using SeqCli.Cli.Features; +using SeqCli.Config; +using SeqCli.Connection; + +namespace SeqCli.Cli.Commands.ExpressionIndex; + +[Command("expressionindex", "list", "List expression indexes", Example="seqcli expressionindex list")] +class ListCommand : Command +{ + readonly SeqConnectionFactory _connectionFactory; + + readonly ConnectionFeature _connection; + readonly OutputFormatFeature _output; + string? _id; + + public ListCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + Options.Add( + "i=|id=", + "The id of a single expression index to list", + id => _id = id); + + _output = Enable(new OutputFormatFeature(config.Output)); + _connection = Enable(); + } + + protected override async Task Run() + { + var connection = _connectionFactory.Connect(_connection); + var list = _id is not null + ? [await connection.ExpressionIndexes.FindAsync(_id)] + : await connection.ExpressionIndexes.ListAsync(); + _output.ListEntities(list); + return 0; + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/ExpressionIndex/RemoveCommand.cs b/src/SeqCli/Cli/Commands/ExpressionIndex/RemoveCommand.cs new file mode 100644 index 00000000..c7ebee37 --- /dev/null +++ b/src/SeqCli/Cli/Commands/ExpressionIndex/RemoveCommand.cs @@ -0,0 +1,59 @@ +// Copyright 2018 Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using SeqCli.Cli.Features; +using SeqCli.Connection; +using Serilog; + +namespace SeqCli.Cli.Commands.ExpressionIndex; + +[Command("expressionindex", "remove", "Remove an expression index from the server", + Example = "seqcli expressionindex -i expressionindex-2529")] +class RemoveCommand : Command +{ + readonly SeqConnectionFactory _connectionFactory; + + readonly ConnectionFeature _connection; + string? _id; + + public RemoveCommand(SeqConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + Options.Add( + "i=|id=", + "The id of an expression index to remove", + id => _id = id); + + _connection = Enable(); + } + + protected override async Task Run() + { + if (_id == null) + { + Log.Error("An `id` must be specified"); + return 1; + } + + var connection = _connectionFactory.Connect(_connection); + var toRemove = await connection.ExpressionIndexes.FindAsync(_id); + await connection.ExpressionIndexes.RemoveAsync(toRemove); + + return 0; + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Index/ListCommand.cs b/src/SeqCli/Cli/Commands/Index/ListCommand.cs new file mode 100644 index 00000000..958451b5 --- /dev/null +++ b/src/SeqCli/Cli/Commands/Index/ListCommand.cs @@ -0,0 +1,61 @@ +// Copyright 2018 Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Seq.Api.Model.Indexes; +using SeqCli.Cli.Features; +using SeqCli.Config; +using SeqCli.Connection; + +namespace SeqCli.Cli.Commands.Index; + +[Command("index", "list", "List indexes", Example="seqcli index list")] +class ListCommand : Command +{ + readonly SeqConnectionFactory _connectionFactory; + + readonly ConnectionFeature _connection; + readonly OutputFormatFeature _output; + string? _id; + + public ListCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + Options.Add( + "i=|id=", + "The id of a single index to list", + id => _id = id); + + _output = Enable(new OutputFormatFeature(config.Output)); + _connection = Enable(); + } + + protected override async Task Run() + { + var connection = _connectionFactory.Connect(_connection); + + var list = _id is not null + ? [await connection.Indexes.FindAsync(_id)] + : await connection.Indexes.ListAsync(); + + _output.ListEntities(list); + + return 0; + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs b/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs new file mode 100644 index 00000000..5aa495bd --- /dev/null +++ b/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs @@ -0,0 +1,59 @@ +// Copyright 2018 Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading.Tasks; +using Seq.Api.Model.Indexes; +using SeqCli.Cli.Features; +using SeqCli.Config; +using SeqCli.Connection; +using Serilog; + +namespace SeqCli.Cli.Commands.Index; + +[Command("index", "suppress", "Suppress an index", Example="seqcli index suppress -i index-2191448f1d9b4f22bd32c6edef752748")] +class SuppressCommand : Command +{ + readonly SeqConnectionFactory _connectionFactory; + readonly ConnectionFeature _connection; + string? _id; + + public SuppressCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + Options.Add( + "i=|id=", + "The id of an index to suppress", + id => _id = id); + + _connection = Enable(); + } + + protected override async Task Run() + { + if (_id == null) + { + Log.Error("An `id` must be specified"); + return 1; + } + + var connection = _connectionFactory.Connect(_connection); + var toSuppress = await connection.Indexes.FindAsync(_id); + await connection.Indexes.SuppressAsync(toSuppress); + + return 0; + } +} \ No newline at end of file diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index 69b925f7..983bf911 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -28,6 +28,7 @@ + @@ -38,7 +39,6 @@ - diff --git a/src/SeqCli/Templates/Export/EntityName.cs b/src/SeqCli/Templates/Export/EntityName.cs index 34512914..c4bd5c40 100644 --- a/src/SeqCli/Templates/Export/EntityName.cs +++ b/src/SeqCli/Templates/Export/EntityName.cs @@ -15,9 +15,16 @@ public static string FromEntityType(Type entityType) public static string ToResourceGroup(string resource) { - if (!resource.EndsWith("y")) - return resource + "s"; + if (resource.EndsWith('y')) + { + return resource.TrimEnd('y') + "ies"; + } - return resource.TrimEnd('y') + "ies"; + if (resource.EndsWith('x')) + { + return resource + "es"; + } + + return resource + "s"; } } \ No newline at end of file diff --git a/src/SeqCli/Templates/Export/TemplateSetExporter.cs b/src/SeqCli/Templates/Export/TemplateSetExporter.cs index 051a3cda..57d8d059 100644 --- a/src/SeqCli/Templates/Export/TemplateSetExporter.cs +++ b/src/SeqCli/Templates/Export/TemplateSetExporter.cs @@ -8,6 +8,7 @@ using Seq.Api.Model; using Seq.Api.Model.Alerting; using Seq.Api.Model.Dashboarding; +using Seq.Api.Model.Indexing; using Seq.Api.Model.Retention; using Seq.Api.Model.Signals; using Seq.Api.Model.SqlQueries; @@ -78,8 +79,16 @@ await ExportTemplates( () => _connection.RetentionPolicies.ListAsync(), retentionPolicy => retentionPolicy.Id.Replace("retentionpolicy-", ""), templateValueMap); - } + await ExportTemplates( + id => _connection.ExpressionIndexes.FindAsync(id), + () => _connection.ExpressionIndexes.ListAsync(), + expressionIndex => expressionIndex.Expression.All(char.IsLetterOrDigit) + ? expressionIndex.Expression + : expressionIndex.Id.Replace("expressionindex-", ""), + templateValueMap); + } + async Task ExportTemplates( Func> findEntity, Func>> listEntities, diff --git a/src/SeqCli/Templates/Import/TemplateSetImporter.cs b/src/SeqCli/Templates/Import/TemplateSetImporter.cs index 8ac069c8..dd04ce6d 100644 --- a/src/SeqCli/Templates/Import/TemplateSetImporter.cs +++ b/src/SeqCli/Templates/Import/TemplateSetImporter.cs @@ -39,7 +39,7 @@ static class TemplateSetImporter bool merge) { var ordering = new[] {"users", "signals", "apps", "appinstances", - "dashboards", "sqlqueries", "workspaces", "retentionpolicies", "alerts"}.ToList(); + "dashboards", "sqlqueries", "workspaces", "retentionpolicies", "alerts", "expressionindexes"}.ToList(); var sorted = templates.OrderBy(t => ordering.IndexOf(t.ResourceGroup)); diff --git a/test/SeqCli.EndToEnd/Indexes/ExpressionIndexBasicsTestCase.cs b/test/SeqCli.EndToEnd/Indexes/ExpressionIndexBasicsTestCase.cs new file mode 100644 index 00000000..4e7497d4 --- /dev/null +++ b/test/SeqCli.EndToEnd/Indexes/ExpressionIndexBasicsTestCase.cs @@ -0,0 +1,36 @@ +using System.Linq; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Indexes; + +[CliTestCase(MinimumApiVersion = "2024.3.0")] +public class ExpressionIndexBasicsTestCase: ICliTestCase +{ + public async Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner) + { + const string expr = "@Resource.service.name"; + var exit = runner.Exec("expressionindex create", $"-e {expr}"); + Assert.Equal(0, exit); + + var entity = (await connection.ExpressionIndexes.ListAsync()).Single(e => e.Expression == expr); + Assert.Equal(expr, entity.Expression); + + exit = runner.Exec("expressionindex list"); + Assert.Equal(0, exit); + + Assert.Contains(expr, runner.LastRunProcess!.Output); + Assert.Contains(entity.Id, runner.LastRunProcess.Output); + + exit = runner.Exec("expressionindex remove", $"-i {entity.Id}"); + Assert.Equal(0, exit); + + exit = runner.Exec("expressionindex list"); + Assert.Equal(0, exit); + + Assert.DoesNotContain(entity.Id, runner.LastRunProcess.Output); + } +} \ No newline at end of file diff --git a/test/SeqCli.EndToEnd/Indexes/IndexesTestCase.cs b/test/SeqCli.EndToEnd/Indexes/IndexesTestCase.cs new file mode 100644 index 00000000..84b82f03 --- /dev/null +++ b/test/SeqCli.EndToEnd/Indexes/IndexesTestCase.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Indexes; + +[CliTestCase(MinimumApiVersion = "2024.3.0")] +public class IndexesTestCase: ICliTestCase +{ + public async Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner) + { + const string expr = "Magic123"; + var exit = runner.Exec("expressionindex create", $"-e {expr}"); + Assert.Equal(0, exit); + + var expressionIndex = (await connection.ExpressionIndexes.ListAsync()).Single(e => e.Expression == expr); + var signal = (await connection.Signals.ListAsync()).First(s => !s.IsIndexSuppressed); + var indexForSignal = (await connection.Indexes.ListAsync()).First(i => i.IndexedEntityId == signal.Id); + + exit = runner.Exec("index list"); + Assert.Equal(0, exit); + Assert.Contains(expressionIndex.Id, runner.LastRunProcess!.Output); + Assert.Contains(signal.Id, runner.LastRunProcess!.Output); + + exit = runner.Exec($"index suppress -i {indexForSignal.Id}"); + Assert.Equal(0, exit); + + signal = await connection.Signals.FindAsync(signal.Id); + Assert.True(signal.IsIndexSuppressed); + } +} diff --git a/test/SeqCli.EndToEnd/Templates/TemplateExportImportTestCase.cs b/test/SeqCli.EndToEnd/Templates/TemplateExportImportTestCase.cs index f97b7fdb..1286d37c 100644 --- a/test/SeqCli.EndToEnd/Templates/TemplateExportImportTestCase.cs +++ b/test/SeqCli.EndToEnd/Templates/TemplateExportImportTestCase.cs @@ -8,7 +8,7 @@ namespace SeqCli.EndToEnd.Templates; -[CliTestCase(MinimumApiVersion = "2021.3.6336")] +[CliTestCase(MinimumApiVersion = "2024.3.0")] public class TemplateExportImportTestCase : ICliTestCase { readonly TestDataFolder _testDataFolder;