From 1128e7a0db2ed980d6c05fae579c0dbbdd4b95f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Thu, 19 Feb 2026 09:45:25 +0100 Subject: [PATCH] feat: Add `seqcli events delete` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `events delete` command is modelled after the `seqcli search` and accepts date range as well as filter expressions. The `events delete` command is covered with e2e tests. The idea of adding delete command came up in this thread: - https://github.com/datalust/seq-tickets/discussions/2529 Signed-off-by: Mateusz Łoskot --- .../Cli/Commands/Events/DeleteCommand.cs | 78 +++++++++++++++++++ .../Events/EventsDeleteTestCase.cs | 34 ++++++++ .../EventsDeleteWithDateRangeAllTestCase.cs | 46 +++++++++++ .../EventsDeleteWithDateRangeNoneTestCase.cs | 46 +++++++++++ .../EventsDeleteWithDateRangeSomeTestCase.cs | 53 +++++++++++++ .../Events/EventsDeleteWithFilterTestCase.cs | 52 +++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 src/SeqCli/Cli/Commands/Events/DeleteCommand.cs create mode 100644 test/SeqCli.EndToEnd/Events/EventsDeleteTestCase.cs create mode 100644 test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeAllTestCase.cs create mode 100644 test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeNoneTestCase.cs create mode 100644 test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeSomeTestCase.cs create mode 100644 test/SeqCli.EndToEnd/Events/EventsDeleteWithFilterTestCase.cs diff --git a/src/SeqCli/Cli/Commands/Events/DeleteCommand.cs b/src/SeqCli/Cli/Commands/Events/DeleteCommand.cs new file mode 100644 index 00000000..2e9aa26e --- /dev/null +++ b/src/SeqCli/Cli/Commands/Events/DeleteCommand.cs @@ -0,0 +1,78 @@ +// Copyright 2026 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 SeqCli.Api; +using SeqCli.Cli.Features; +using SeqCli.Config; +using Serilog; +// ReSharper disable UnusedType.Global + +namespace SeqCli.Cli.Commands; + +[Command("events", "delete", "Delete log events that match a given date range or filter", + Example = "seqcli events delete --start \"2026-01-01T00:00:00Z\" --end \"2026-01-31T23:59:59Z\"")] +class DeleteCommand : Command +{ + readonly ConnectionFeature _connection; + readonly DateRangeFeature _range; + readonly SignalExpressionFeature _signal; + readonly StoragePathFeature _storagePath; + string? _filter; + + public DeleteCommand() + { + Options.Add( + "f=|filter=", + "A filter to apply to deletion, for example `Host = 'xmpweb-01.example.com'`", + v => _filter = v); + + _range = Enable(); + _storagePath = Enable(); + _signal = Enable(); + + _connection = Enable(); + } + + protected override async Task Run() + { + try + { + var config = RuntimeConfigurationLoader.Load(_storagePath); + var connection = SeqConnectionFactory.Connect(_connection, config); + + string? filter = null; + if (!string.IsNullOrWhiteSpace(_filter)) + filter = (await connection.Expressions.ToStrictAsync(_filter)).StrictExpression; + + await connection.Events.DeleteAsync( + null, + _signal.Signal, + filter, + _range.Start, + _range.End, + null); + + Log.Information("Deleted matching events"); + + return 0; + } + catch (Exception ex) + { + Log.Error(ex, "Could not delete matching events: {ErrorMessage}", ex.Message); + return 1; + } + } +} \ No newline at end of file diff --git a/test/SeqCli.EndToEnd/Events/EventsDeleteTestCase.cs b/test/SeqCli.EndToEnd/Events/EventsDeleteTestCase.cs new file mode 100644 index 00000000..29b585c9 --- /dev/null +++ b/test/SeqCli.EndToEnd/Events/EventsDeleteTestCase.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Delete; + +public class EventsDeleteTestCase : ICliTestCase +{ + public async Task ExecuteAsync( + SeqConnection connection, + ILogger logger, + CliCommandRunner runner) + { + + var inputFile = Path.Combine("Data", "events.clef"); + Assert.True(File.Exists(inputFile)); + + var exit = runner.Exec("ingest", $"-i {inputFile}"); + Assert.Equal(0, exit); + + var eventsBefore = await connection.Events.ListAsync(); + Assert.Equal(15, eventsBefore.Count); + + exit = runner.Exec("events delete"); + Assert.Equal(0, exit); + + var eventsAfter = await connection.Events.ListAsync(); + Assert.Empty(eventsAfter); + } +} diff --git a/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeAllTestCase.cs b/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeAllTestCase.cs new file mode 100644 index 00000000..ed56ac08 --- /dev/null +++ b/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeAllTestCase.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Delete; + +public class EventsDeleteWithDateRangeAllTestCase : ICliTestCase +{ + readonly TestDataFolder _testDataFolder; + + public EventsDeleteWithDateRangeAllTestCase(TestDataFolder testDataFolder) + { + _testDataFolder = testDataFolder; + } + + public async Task ExecuteAsync( + SeqConnection connection, + ILogger logger, + CliCommandRunner runner) + { + var inputFile = _testDataFolder.ItemPath("delete-date-range-all.clef"); + + var isoNow = DateTime.UtcNow.ToString("o"); + await File.WriteAllTextAsync(inputFile, + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}"); + var exit = runner.Exec("ingest", $"-i \"{inputFile}\""); + Assert.Equal(0, exit); + + var eventsBefore = await connection.Events.ListAsync(); + Assert.Equal(3, eventsBefore.Count); + + var isoFrom = DateTime.UtcNow.AddDays(-1).ToString("o"); + var isoTo = DateTime.UtcNow.AddDays(1).ToString("o"); + exit = runner.Exec("events delete", $"--start={isoFrom} --end={isoTo}"); + Assert.Equal(0, exit); + + var eventsAfter = await connection.Events.ListAsync(); + Assert.Empty(eventsAfter); + } +} diff --git a/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeNoneTestCase.cs b/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeNoneTestCase.cs new file mode 100644 index 00000000..f95a7fb6 --- /dev/null +++ b/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeNoneTestCase.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Delete; + +public class EventsDeleteWithDateRangeNoneTestCase : ICliTestCase +{ + readonly TestDataFolder _testDataFolder; + + public EventsDeleteWithDateRangeNoneTestCase(TestDataFolder testDataFolder) + { + _testDataFolder = testDataFolder; + } + + public async Task ExecuteAsync( + SeqConnection connection, + ILogger logger, + CliCommandRunner runner) + { + var inputFile = _testDataFolder.ItemPath("delete-date-range-none.clef"); + + var isoNow = DateTime.UtcNow.ToString("o"); + await File.WriteAllTextAsync(inputFile, + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}"); + var exit = runner.Exec("ingest", $"-i \"{inputFile}\""); + Assert.Equal(0, exit); + + var eventsBefore = await connection.Events.ListAsync(); + Assert.Equal(3, eventsBefore.Count); + + var isoFrom = DateTime.UtcNow.AddDays(-10).ToString("o"); + var isoTo = DateTime.UtcNow.AddDays(-5).ToString("o"); + exit = runner.Exec("events delete", $"--start={isoFrom} --end={isoTo}"); + Assert.Equal(0, exit); + + var eventsAfter = await connection.Events.ListAsync(); + Assert.Equal(3, eventsAfter.Count); + } +} diff --git a/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeSomeTestCase.cs b/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeSomeTestCase.cs new file mode 100644 index 00000000..a5b0f65a --- /dev/null +++ b/test/SeqCli.EndToEnd/Events/EventsDeleteWithDateRangeSomeTestCase.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Delete; + +public class EventsDeleteWithDateRangeSomeTestCase : ICliTestCase +{ + readonly TestDataFolder _testDataFolder; + + public EventsDeleteWithDateRangeSomeTestCase(TestDataFolder testDataFolder) + { + _testDataFolder = testDataFolder; + } + + public async Task ExecuteAsync( + SeqConnection connection, + ILogger logger, + CliCommandRunner runner) + { + var inputFile = _testDataFolder.ItemPath("delete-date-range-none.clef"); + + var isoNow = DateTime.UtcNow.ToString("o"); + await File.WriteAllTextAsync(inputFile, + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}"); + var exit = runner.Exec("ingest", $"-i \"{inputFile}\""); + Assert.Equal(0, exit); + + isoNow = DateTime.UtcNow.ToString("o"); + await File.WriteAllTextAsync(inputFile, + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":4}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":5}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":6}}"); + exit = runner.Exec("ingest", $"-i \"{inputFile}\""); + Assert.Equal(0, exit); + + var eventsBefore = await connection.Events.ListAsync(); + Assert.Equal(6, eventsBefore.Count); + + var isoTo = DateTime.UtcNow.AddDays(1).ToString("o"); + exit = runner.Exec("events delete", $"--start={isoNow} --end={isoTo}"); + Assert.Equal(0, exit); + + var eventsAfter = await connection.Events.ListAsync(); + Assert.Equal(3, eventsAfter.Count); + } +} diff --git a/test/SeqCli.EndToEnd/Events/EventsDeleteWithFilterTestCase.cs b/test/SeqCli.EndToEnd/Events/EventsDeleteWithFilterTestCase.cs new file mode 100644 index 00000000..b00a8b18 --- /dev/null +++ b/test/SeqCli.EndToEnd/Events/EventsDeleteWithFilterTestCase.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Delete; + +public class EventsDeleteWithFilterTestCase : ICliTestCase +{ + readonly TestDataFolder _testDataFolder; + + public EventsDeleteWithFilterTestCase(TestDataFolder testDataFolder) + { + _testDataFolder = testDataFolder; + } + + public async Task ExecuteAsync( + SeqConnection connection, + ILogger logger, + CliCommandRunner runner) + { + var inputFile = _testDataFolder.ItemPath("delete-with-filter.clef"); + + var isoNow = DateTime.UtcNow.ToString("o"); + await File.WriteAllTextAsync(inputFile, + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}"); + var hostOne = "xmpweb-01.example.com"; + var exit = runner.Exec("ingest", $"-i \"{inputFile}\" -p \"host={hostOne}\""); + Assert.Equal(0, exit); + + isoNow = DateTime.UtcNow.ToString("o"); + await File.WriteAllTextAsync(inputFile, + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}" + Environment.NewLine + + $"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":4}}"); + var hostTwo = "xmpweb-02.example.com"; + exit = runner.Exec("ingest", $"-i \"{inputFile}\" -p \"host={hostTwo}\""); + Assert.Equal(0, exit); + + var eventsBefore = await connection.Events.ListAsync(); + Assert.Equal(4, eventsBefore.Count); + + exit = runner.Exec("events delete", $"--filter=\"host='{hostTwo}'\""); + Assert.Equal(0, exit); + + var eventsAfter = await connection.Events.ListAsync(); + Assert.Equal(2, eventsAfter.Count); + } +}