Skip to content

Commit 57410c1

Browse files
committed
feat: Add seqcli events delete command
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: - datalust/seq-tickets#2529 Signed-off-by: Mateusz Łoskot <mateusz@loskot.net>
1 parent b795597 commit 57410c1

File tree

6 files changed

+309
-0
lines changed

6 files changed

+309
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2026 Datalust Pty Ltd and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Threading.Tasks;
17+
using SeqCli.Api;
18+
using SeqCli.Cli.Features;
19+
using SeqCli.Config;
20+
using Serilog;
21+
// ReSharper disable UnusedType.Global
22+
23+
namespace SeqCli.Cli.Commands;
24+
25+
[Command("events", "delete", "Delete log events that match a given date range or filter",
26+
Example = "seqcli events delete --start \"2026-01-01T00:00:00Z\" --end \"2026-01-31T23:59:59Z\"")]
27+
class DeleteCommand : Command
28+
{
29+
readonly ConnectionFeature _connection;
30+
readonly DateRangeFeature _range;
31+
readonly SignalExpressionFeature _signal;
32+
readonly StoragePathFeature _storagePath;
33+
string? _filter;
34+
35+
public DeleteCommand()
36+
{
37+
Options.Add(
38+
"f=|filter=",
39+
"A filter to apply to deletion, for example `Host = 'xmpweb-01.example.com'`",
40+
v => _filter = v);
41+
42+
_range = Enable<DateRangeFeature>();
43+
_storagePath = Enable<StoragePathFeature>();
44+
_signal = Enable<SignalExpressionFeature>();
45+
46+
_connection = Enable<ConnectionFeature>();
47+
}
48+
49+
protected override async Task<int> Run()
50+
{
51+
try
52+
{
53+
var config = RuntimeConfigurationLoader.Load(_storagePath);
54+
var connection = SeqConnectionFactory.Connect(_connection, config);
55+
56+
string? filter = null;
57+
if (!string.IsNullOrWhiteSpace(_filter))
58+
filter = (await connection.Expressions.ToStrictAsync(_filter)).StrictExpression;
59+
60+
await connection.Events.DeleteAsync(
61+
null,
62+
_signal.Signal,
63+
filter,
64+
_range.Start,
65+
_range.End,
66+
null);
67+
68+
Log.Information("Deleted matching events");
69+
70+
return 0;
71+
}
72+
catch (Exception ex)
73+
{
74+
Log.Error(ex, "Could not delete matching events: {ErrorMessage}", ex.Message);
75+
return 1;
76+
}
77+
}
78+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Seq.Api;
5+
using SeqCli.EndToEnd.Support;
6+
using Serilog;
7+
using Xunit;
8+
9+
namespace SeqCli.EndToEnd.Delete;
10+
11+
public class EventsDeleteTestCase : ICliTestCase
12+
{
13+
public async Task ExecuteAsync(
14+
SeqConnection connection,
15+
ILogger logger,
16+
CliCommandRunner runner)
17+
{
18+
19+
var inputFile = Path.Combine("Data", "events.clef");
20+
Assert.True(File.Exists(inputFile));
21+
22+
var exit = runner.Exec("ingest", $"-i {inputFile}");
23+
Assert.Equal(0, exit);
24+
25+
var eventsBefore = await connection.Events.ListAsync();
26+
Assert.Equal(15, eventsBefore.Count);
27+
28+
exit = runner.Exec("events delete");
29+
Assert.Equal(0, exit);
30+
31+
var eventsAfter = await connection.Events.ListAsync();
32+
Assert.Empty(eventsAfter);
33+
}
34+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Seq.Api;
5+
using SeqCli.EndToEnd.Support;
6+
using Serilog;
7+
using Xunit;
8+
9+
namespace SeqCli.EndToEnd.Delete;
10+
11+
public class EventsDeleteWithDateRangeAllTestCase : ICliTestCase
12+
{
13+
readonly TestDataFolder _testDataFolder;
14+
15+
public EventsDeleteWithDateRangeAllTestCase(TestDataFolder testDataFolder)
16+
{
17+
_testDataFolder = testDataFolder;
18+
}
19+
20+
public async Task ExecuteAsync(
21+
SeqConnection connection,
22+
ILogger logger,
23+
CliCommandRunner runner)
24+
{
25+
var inputFile = _testDataFolder.ItemPath("delete-date-range-all.clef");
26+
27+
var isoNow = DateTime.UtcNow.ToString("o");
28+
await File.WriteAllTextAsync(inputFile,
29+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine +
30+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}" + Environment.NewLine +
31+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}");
32+
var exit = runner.Exec("ingest", $"-i \"{inputFile}\"");
33+
Assert.Equal(0, exit);
34+
35+
var eventsBefore = await connection.Events.ListAsync();
36+
Assert.Equal(3, eventsBefore.Count);
37+
38+
var isoFrom = DateTime.UtcNow.AddDays(-1).ToString("o");
39+
var isoTo = DateTime.UtcNow.AddDays(1).ToString("o");
40+
exit = runner.Exec("events delete", $"--start={isoFrom} --end={isoTo}");
41+
Assert.Equal(0, exit);
42+
43+
var eventsAfter = await connection.Events.ListAsync();
44+
Assert.Empty(eventsAfter);
45+
}
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Seq.Api;
5+
using SeqCli.EndToEnd.Support;
6+
using Serilog;
7+
using Xunit;
8+
9+
namespace SeqCli.EndToEnd.Delete;
10+
11+
public class EventsDeleteWithDateRangeNoneTestCase : ICliTestCase
12+
{
13+
readonly TestDataFolder _testDataFolder;
14+
15+
public EventsDeleteWithDateRangeNoneTestCase(TestDataFolder testDataFolder)
16+
{
17+
_testDataFolder = testDataFolder;
18+
}
19+
20+
public async Task ExecuteAsync(
21+
SeqConnection connection,
22+
ILogger logger,
23+
CliCommandRunner runner)
24+
{
25+
var inputFile = _testDataFolder.ItemPath("delete-date-range-none.clef");
26+
27+
var isoNow = DateTime.UtcNow.ToString("o");
28+
await File.WriteAllTextAsync(inputFile,
29+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine +
30+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}" + Environment.NewLine +
31+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}");
32+
var exit = runner.Exec("ingest", $"-i \"{inputFile}\"");
33+
Assert.Equal(0, exit);
34+
35+
var eventsBefore = await connection.Events.ListAsync();
36+
Assert.Equal(3, eventsBefore.Count);
37+
38+
var isoFrom = DateTime.UtcNow.AddDays(-10).ToString("o");
39+
var isoTo = DateTime.UtcNow.AddDays(-5).ToString("o");
40+
exit = runner.Exec("events delete", $"--start={isoFrom} --end={isoTo}");
41+
Assert.Equal(0, exit);
42+
43+
var eventsAfter = await connection.Events.ListAsync();
44+
Assert.Equal(3, eventsAfter.Count);
45+
}
46+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Seq.Api;
5+
using SeqCli.EndToEnd.Support;
6+
using Serilog;
7+
using Xunit;
8+
9+
namespace SeqCli.EndToEnd.Delete;
10+
11+
public class EventsDeleteWithDateRangeSomeTestCase : ICliTestCase
12+
{
13+
readonly TestDataFolder _testDataFolder;
14+
15+
public EventsDeleteWithDateRangeSomeTestCase(TestDataFolder testDataFolder)
16+
{
17+
_testDataFolder = testDataFolder;
18+
}
19+
20+
public async Task ExecuteAsync(
21+
SeqConnection connection,
22+
ILogger logger,
23+
CliCommandRunner runner)
24+
{
25+
var inputFile = _testDataFolder.ItemPath("delete-date-range-none.clef");
26+
27+
var isoNow = DateTime.UtcNow.ToString("o");
28+
await File.WriteAllTextAsync(inputFile,
29+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine +
30+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}" + Environment.NewLine +
31+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}");
32+
var exit = runner.Exec("ingest", $"-i \"{inputFile}\"");
33+
Assert.Equal(0, exit);
34+
35+
isoNow = DateTime.UtcNow.ToString("o");
36+
await File.WriteAllTextAsync(inputFile,
37+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":4}}" + Environment.NewLine +
38+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":5}}" + Environment.NewLine +
39+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":6}}");
40+
exit = runner.Exec("ingest", $"-i \"{inputFile}\"");
41+
Assert.Equal(0, exit);
42+
43+
var eventsBefore = await connection.Events.ListAsync();
44+
Assert.Equal(6, eventsBefore.Count);
45+
46+
var isoTo = DateTime.UtcNow.AddDays(1).ToString("o");
47+
exit = runner.Exec("events delete", $"--start={isoNow} --end={isoTo}");
48+
Assert.Equal(0, exit);
49+
50+
var eventsAfter = await connection.Events.ListAsync();
51+
Assert.Equal(3, eventsAfter.Count);
52+
}
53+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Seq.Api;
5+
using SeqCli.EndToEnd.Support;
6+
using Serilog;
7+
using Xunit;
8+
9+
namespace SeqCli.EndToEnd.Delete;
10+
11+
public class EventsDeleteWithFilterTestCase : ICliTestCase
12+
{
13+
readonly TestDataFolder _testDataFolder;
14+
15+
public EventsDeleteWithFilterTestCase(TestDataFolder testDataFolder)
16+
{
17+
_testDataFolder = testDataFolder;
18+
}
19+
20+
public async Task ExecuteAsync(
21+
SeqConnection connection,
22+
ILogger logger,
23+
CliCommandRunner runner)
24+
{
25+
var inputFile = _testDataFolder.ItemPath("delete-with-filter.clef");
26+
27+
var isoNow = DateTime.UtcNow.ToString("o");
28+
await File.WriteAllTextAsync(inputFile,
29+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":1}}" + Environment.NewLine +
30+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":2}}");
31+
var hostOne = "xmpweb-01.example.com";
32+
var exit = runner.Exec("ingest", $"-i \"{inputFile}\" -p \"host={hostOne}\"");
33+
Assert.Equal(0, exit);
34+
35+
isoNow = DateTime.UtcNow.ToString("o");
36+
await File.WriteAllTextAsync(inputFile,
37+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":3}}" + Environment.NewLine +
38+
$"{{\"@t\":\"{isoNow}\",\"@mt\":\"Event {{N}}\",\"N\":4}}");
39+
var hostTwo = "xmpweb-02.example.com";
40+
exit = runner.Exec("ingest", $"-i \"{inputFile}\" -p \"host={hostTwo}\"");
41+
Assert.Equal(0, exit);
42+
43+
var eventsBefore = await connection.Events.ListAsync();
44+
Assert.Equal(4, eventsBefore.Count);
45+
46+
exit = runner.Exec("events delete", $"--filter=\"host='{hostTwo}'\"");
47+
Assert.Equal(0, exit);
48+
49+
var eventsAfter = await connection.Events.ListAsync();
50+
Assert.Equal(2, eventsAfter.Count);
51+
}
52+
}

0 commit comments

Comments
 (0)