From f5eeb6333a7509adee5fc91cf37ebe26409fb34f Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 3 Jun 2026 13:37:27 +1000 Subject: [PATCH 1/4] Fix error reporting in `seqcli query` --- src/SeqCli/Apps/Hosting/AppContainer.cs | 1 - src/SeqCli/Apps/Hosting/AppHost.cs | 1 - src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs | 1 - .../Cli/Commands/ApiKey/RemoveCommand.cs | 1 - src/SeqCli/Cli/Commands/App/DefineCommand.cs | 1 - src/SeqCli/Cli/Commands/App/InstallCommand.cs | 2 - src/SeqCli/Cli/Commands/App/ListCommand.cs | 3 +- .../Cli/Commands/App/UninstallCommand.cs | 1 - src/SeqCli/Cli/Commands/App/UpdateCommand.cs | 1 - .../Cli/Commands/AppInstance/CreateCommand.cs | 3 +- .../Cli/Commands/AppInstance/ListCommand.cs | 3 +- .../Cli/Commands/AppInstance/RemoveCommand.cs | 3 +- src/SeqCli/Cli/Commands/Bench/BenchCommand.cs | 2 - src/SeqCli/Cli/Commands/Config/SetCommand.cs | 3 - .../Cli/Commands/Dashboard/ListCommand.cs | 1 - .../Cli/Commands/Dashboard/RemoveCommand.cs | 1 - .../Cli/Commands/Dashboard/RenderCommand.cs | 19 +-- .../Commands/ExpressionIndex/CreateCommand.cs | 4 - .../Commands/ExpressionIndex/ListCommand.cs | 1 - src/SeqCli/Cli/Commands/Feed/ListCommand.cs | 1 - src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs | 1 - src/SeqCli/Cli/Commands/Index/ListCommand.cs | 4 - .../Cli/Commands/Index/SuppressCommand.cs | 2 - .../Cli/Commands/License/ShowCommand.cs | 9 +- src/SeqCli/Cli/Commands/Node/ListCommand.cs | 1 - src/SeqCli/Cli/Commands/QueryCommand.cs | 21 +-- .../Commands/RetentionPolicy/ListCommand.cs | 1 - .../Commands/RetentionPolicy/RemoveCommand.cs | 1 - .../Cli/Commands/Sample/SetupCommand.cs | 1 - .../Cli/Commands/Settings/ClearCommand.cs | 1 - src/SeqCli/Cli/Commands/Signal/ListCommand.cs | 1 - .../Cli/Commands/Signal/RemoveCommand.cs | 1 - .../Cli/Commands/Template/ExportCommand.cs | 3 +- src/SeqCli/Cli/Commands/User/ListCommand.cs | 1 - src/SeqCli/Cli/Commands/User/RemoveCommand.cs | 1 - src/SeqCli/Cli/Commands/View/CreateCommand.cs | 1 - src/SeqCli/Cli/Commands/View/ListCommand.cs | 1 - src/SeqCli/Cli/Commands/View/RemoveCommand.cs | 1 - .../Cli/Commands/Workspace/CreateCommand.cs | 3 +- .../Cli/Commands/Workspace/ListCommand.cs | 3 +- .../Cli/Commands/Workspace/RemoveCommand.cs | 3 +- src/SeqCli/Cli/Features/PropertiesFeature.cs | 1 - src/SeqCli/Cli/Features/SettingNameFeature.cs | 1 - src/SeqCli/Csv/CsvToken.cs | 14 -- src/SeqCli/Csv/CsvTokenizer.cs | 142 ----------------- src/SeqCli/Csv/CsvWriter.cs | 144 ++++++++++++------ .../SeqCliForwarderWindowsService.cs | 1 - src/SeqCli/Mcp/Data/QueryResultHelper.cs | 24 ++- src/SeqCli/Output/NativeFormatter.cs | 5 + src/SeqCli/Output/OutputFormat.cs | 18 +-- .../PlainText/Extraction/PatternElement.cs | 1 - src/SeqCli/Program.cs | 1 - src/SeqCli/Templates/Export/EntityName.cs | 7 + .../Templates/Export/TemplateSetExporter.cs | 12 +- .../Workspace/WorkspaceBasicsTestCase.cs | 4 +- test/SeqCli.Tests/Csv/CsvTokenizerTests.cs | 53 ------- 56 files changed, 159 insertions(+), 383 deletions(-) delete mode 100644 src/SeqCli/Csv/CsvToken.cs delete mode 100644 src/SeqCli/Csv/CsvTokenizer.cs delete mode 100644 test/SeqCli.Tests/Csv/CsvTokenizerTests.cs diff --git a/src/SeqCli/Apps/Hosting/AppContainer.cs b/src/SeqCli/Apps/Hosting/AppContainer.cs index 2f52c589..8a58c2bd 100644 --- a/src/SeqCli/Apps/Hosting/AppContainer.cs +++ b/src/SeqCli/Apps/Hosting/AppContainer.cs @@ -15,7 +15,6 @@ using System; using System.Globalization; using System.IO; -using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; diff --git a/src/SeqCli/Apps/Hosting/AppHost.cs b/src/SeqCli/Apps/Hosting/AppHost.cs index 3f48e118..aaea1ac0 100644 --- a/src/SeqCli/Apps/Hosting/AppHost.cs +++ b/src/SeqCli/Apps/Hosting/AppHost.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; -using System.Net; using System.Threading.Tasks; using Seq.Apps; using Serilog; diff --git a/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs index 4cd44f5c..63dffd5e 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs index ac5900b8..23254ca9 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/App/DefineCommand.cs b/src/SeqCli/Cli/Commands/App/DefineCommand.cs index 40e2a54f..3844e351 100644 --- a/src/SeqCli/Cli/Commands/App/DefineCommand.cs +++ b/src/SeqCli/Cli/Commands/App/DefineCommand.cs @@ -16,7 +16,6 @@ using System.Threading.Tasks; using SeqCli.Apps; using SeqCli.Apps.Definitions; -using SeqCli.Cli.Features; using SeqCli.Util; namespace SeqCli.Cli.Commands.App; diff --git a/src/SeqCli/Cli/Commands/App/InstallCommand.cs b/src/SeqCli/Cli/Commands/App/InstallCommand.cs index b365a8d6..4c8ec3b0 100644 --- a/src/SeqCli/Cli/Commands/App/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/App/InstallCommand.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using SeqCli.Api; diff --git a/src/SeqCli/Cli/Commands/App/ListCommand.cs b/src/SeqCli/Cli/Commands/App/ListCommand.cs index 1add7dc5..7acd0f1a 100644 --- a/src/SeqCli/Cli/Commands/App/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/App/ListCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/App/UninstallCommand.cs b/src/SeqCli/Cli/Commands/App/UninstallCommand.cs index 16e74b3f..2dbbc516 100644 --- a/src/SeqCli/Cli/Commands/App/UninstallCommand.cs +++ b/src/SeqCli/Cli/Commands/App/UninstallCommand.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Threading.Tasks; using SeqCli.Api; diff --git a/src/SeqCli/Cli/Commands/App/UpdateCommand.cs b/src/SeqCli/Cli/Commands/App/UpdateCommand.cs index 7399f2f7..cccc8cac 100644 --- a/src/SeqCli/Cli/Commands/App/UpdateCommand.cs +++ b/src/SeqCli/Cli/Commands/App/UpdateCommand.cs @@ -13,7 +13,6 @@ // limitations under the License. using System; -using System.Globalization; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/AppInstance/CreateCommand.cs b/src/SeqCli/Cli/Commands/AppInstance/CreateCommand.cs index 4f9a024d..81756af3 100644 --- a/src/SeqCli/Cli/Commands/AppInstance/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/AppInstance/CreateCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Seq.Api.Model.AppInstances; diff --git a/src/SeqCli/Cli/Commands/AppInstance/ListCommand.cs b/src/SeqCli/Cli/Commands/AppInstance/ListCommand.cs index f882b119..10f9609c 100644 --- a/src/SeqCli/Cli/Commands/AppInstance/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/AppInstance/ListCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs b/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs index d72ae404..0f65c03c 100644 --- a/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/Bench/BenchCommand.cs b/src/SeqCli/Cli/Commands/Bench/BenchCommand.cs index c38e8b6c..416ebeb4 100644 --- a/src/SeqCli/Cli/Commands/Bench/BenchCommand.cs +++ b/src/SeqCli/Cli/Commands/Bench/BenchCommand.cs @@ -13,14 +13,12 @@ // limitations under the License. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Seq.Api; -using Seq.Api.Model.Data; using Seq.Api.Model.Signals; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/Config/SetCommand.cs b/src/SeqCli/Cli/Commands/Config/SetCommand.cs index da055ff5..10bfe686 100644 --- a/src/SeqCli/Cli/Commands/Config/SetCommand.cs +++ b/src/SeqCli/Cli/Commands/Config/SetCommand.cs @@ -12,12 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; using System.Threading.Tasks; using SeqCli.Cli.Features; using SeqCli.Config; -using Serilog; // ReSharper disable once UnusedType.Global diff --git a/src/SeqCli/Cli/Commands/Dashboard/ListCommand.cs b/src/SeqCli/Cli/Commands/Dashboard/ListCommand.cs index 7d9cc7a2..3f2987d1 100644 --- a/src/SeqCli/Cli/Commands/Dashboard/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Dashboard/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs index 28c0845c..52566c9e 100644 --- a/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Dashboard/RenderCommand.cs b/src/SeqCli/Cli/Commands/Dashboard/RenderCommand.cs index 80ed95e9..fb3cac9a 100644 --- a/src/SeqCli/Cli/Commands/Dashboard/RenderCommand.cs +++ b/src/SeqCli/Cli/Commands/Dashboard/RenderCommand.cs @@ -15,7 +15,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json; using Seq.Api.Model.Dashboarding; using Seq.Api.Model.Signals; using SeqCli.Api; @@ -158,20 +157,10 @@ protected override async Task Run() var timeout = _timeout.ApplyTimeout(connection.Client.HttpClient); var output = _output.GetOutputFormat(config); - if (output.Json) - { - var result = await connection.Data.QueryAsync(q, signal: signal, timeout: timeout); - - // Some friendlier JSON output is definitely possible here - Console.WriteLine(JsonConvert.SerializeObject(result)); - } - else - { - var result = await connection.Data.QueryCsvAsync(q, signal: signal, timeout: timeout); - output.WriteCsv(result); - } - - return 0; + var result = await connection.Data.TryQueryAsync(q, signal: signal, timeout: timeout); + output.WriteQueryResult(result); + + return string.IsNullOrWhiteSpace(result.Error) ? 0 : 1; } static string BuildSqlQuery(ChartQueryPart query, DateTime rangeStart, DateTime rangeEnd, TimeSpan? timeGrouping) diff --git a/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs b/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs index cd454a42..3f31d9c1 100644 --- a/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/ExpressionIndex/CreateCommand.cs @@ -12,14 +12,10 @@ // 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.Api; using SeqCli.Cli.Features; using SeqCli.Config; -using SeqCli.Signals; -using SeqCli.Syntax; using SeqCli.Util; using Serilog; diff --git a/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs b/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs index 133c26bc..cf54e03c 100644 --- a/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/ExpressionIndex/ListCommand.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/Feed/ListCommand.cs b/src/SeqCli/Cli/Commands/Feed/ListCommand.cs index f203018f..b10f245d 100644 --- a/src/SeqCli/Cli/Commands/Feed/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Feed/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs index 898b55f0..99a788ed 100644 --- a/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Index/ListCommand.cs b/src/SeqCli/Cli/Commands/Index/ListCommand.cs index 9452f36d..8b1b62a6 100644 --- a/src/SeqCli/Cli/Commands/Index/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Index/ListCommand.cs @@ -12,11 +12,7 @@ // 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.Api; using SeqCli.Cli.Features; using SeqCli.Config; diff --git a/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs b/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs index da742ed1..0b970622 100644 --- a/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs +++ b/src/SeqCli/Cli/Commands/Index/SuppressCommand.cs @@ -12,9 +12,7 @@ // 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.Api; using SeqCli.Cli.Features; using SeqCli.Config; diff --git a/src/SeqCli/Cli/Commands/License/ShowCommand.cs b/src/SeqCli/Cli/Commands/License/ShowCommand.cs index 5f15a451..e3d4e8e3 100644 --- a/src/SeqCli/Cli/Commands/License/ShowCommand.cs +++ b/src/SeqCli/Cli/Commands/License/ShowCommand.cs @@ -1,14 +1,7 @@ -using System; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using Seq.Api.Model; -using Seq.Api.Model.License; +using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; using SeqCli.Config; -using SeqCli.Util; -using Serilog; // ReSharper disable once UnusedType.Global diff --git a/src/SeqCli/Cli/Commands/Node/ListCommand.cs b/src/SeqCli/Cli/Commands/Node/ListCommand.cs index a6f42b3e..16a2cfb2 100644 --- a/src/SeqCli/Cli/Commands/Node/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Node/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/QueryCommand.cs b/src/SeqCli/Cli/Commands/QueryCommand.cs index 768d6293..6dd7db67 100644 --- a/src/SeqCli/Cli/Commands/QueryCommand.cs +++ b/src/SeqCli/Cli/Commands/QueryCommand.cs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Threading.Tasks; -using Seq.Api.Client; using SeqCli.Api; using SeqCli.Cli.Features; using SeqCli.Config; @@ -24,7 +22,7 @@ namespace SeqCli.Cli.Commands; -[Command("query", "Execute an SQL query and receive results in CSV format", +[Command("query", "Execute a query and receive results in CSV, JSON, or native format", Example = "seqcli query -q \"select count(*) from stream group by @Level\" --start=\"2018-02-28T13:00Z\"")] class QueryCommand : Command { @@ -39,7 +37,7 @@ class QueryCommand : Command public QueryCommand() { - Options.Add("q=|query=", "The query to execute", v => _query = v); + Options.Add("q=|query=", "The Seq query to execute", v => _query = v); _range = Enable(); _signal = Enable(); _timeout = Enable(); @@ -63,18 +61,9 @@ protected override async Task Run() var timeout = _timeout.ApplyTimeout(connection.Client.HttpClient); var output = _output.GetOutputFormat(config); - if (output.Text) - { - // We can fold this into the `WriteQueryResult` case once that path supports themes. - var result = await connection.Data.QueryCsvAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace); - output.WriteCsv(result); - } - else - { - var result = await connection.Data.QueryAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace); - output.WriteQueryResult(result); - } + var result = await connection.Data.TryQueryAsync(_query, _range.Start, _range.End, _signal.Signal, timeout: timeout, trace: _trace); + output.WriteQueryResult(result); - return 0; + return string.IsNullOrWhiteSpace(result.Error) ? 0 : 1; } } \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs b/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs index a8d410a7..32bb0414 100644 --- a/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs @@ -12,7 +12,6 @@ // 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; diff --git a/src/SeqCli/Cli/Commands/RetentionPolicy/RemoveCommand.cs b/src/SeqCli/Cli/Commands/RetentionPolicy/RemoveCommand.cs index a6e6acf4..1e7e3fb3 100644 --- a/src/SeqCli/Cli/Commands/RetentionPolicy/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/RetentionPolicy/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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; diff --git a/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs b/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs index 187ca169..caceca7c 100644 --- a/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs +++ b/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using SeqCli.Cli.Features; using SeqCli.Templates.Ast; diff --git a/src/SeqCli/Cli/Commands/Settings/ClearCommand.cs b/src/SeqCli/Cli/Commands/Settings/ClearCommand.cs index 760d9d2f..192667bc 100644 --- a/src/SeqCli/Cli/Commands/Settings/ClearCommand.cs +++ b/src/SeqCli/Cli/Commands/Settings/ClearCommand.cs @@ -12,7 +12,6 @@ // 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; diff --git a/src/SeqCli/Cli/Commands/Signal/ListCommand.cs b/src/SeqCli/Cli/Commands/Signal/ListCommand.cs index 883d1448..46f67513 100644 --- a/src/SeqCli/Cli/Commands/Signal/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Signal/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs index 06f65a60..89189b33 100644 --- a/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Template/ExportCommand.cs b/src/SeqCli/Cli/Commands/Template/ExportCommand.cs index 624db07b..5a77714a 100644 --- a/src/SeqCli/Cli/Commands/Template/ExportCommand.cs +++ b/src/SeqCli/Cli/Commands/Template/ExportCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using SeqCli.Api; diff --git a/src/SeqCli/Cli/Commands/User/ListCommand.cs b/src/SeqCli/Cli/Commands/User/ListCommand.cs index 62ae3979..6bb8f885 100644 --- a/src/SeqCli/Cli/Commands/User/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/User/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/User/RemoveCommand.cs b/src/SeqCli/Cli/Commands/User/RemoveCommand.cs index c855c2ad..dcfe60dd 100644 --- a/src/SeqCli/Cli/Commands/User/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/User/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/View/CreateCommand.cs b/src/SeqCli/Cli/Commands/View/CreateCommand.cs index dfecae50..cad65ef4 100644 --- a/src/SeqCli/Cli/Commands/View/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/View/CreateCommand.cs @@ -17,7 +17,6 @@ using System.Linq; using System.Threading.Tasks; using Seq.Api.Model.Shared; -using Seq.Api.Model.Signals; using SeqCli.Api; using SeqCli.Cli.Features; using SeqCli.Config; diff --git a/src/SeqCli/Cli/Commands/View/ListCommand.cs b/src/SeqCli/Cli/Commands/View/ListCommand.cs index 5acc0f0a..23446e52 100644 --- a/src/SeqCli/Cli/Commands/View/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/View/ListCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/View/RemoveCommand.cs b/src/SeqCli/Cli/Commands/View/RemoveCommand.cs index b5569e11..7f50b62b 100644 --- a/src/SeqCli/Cli/Commands/View/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/View/RemoveCommand.cs @@ -12,7 +12,6 @@ // 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.Api; diff --git a/src/SeqCli/Cli/Commands/Workspace/CreateCommand.cs b/src/SeqCli/Cli/Commands/Workspace/CreateCommand.cs index 1554708e..60c67d1d 100644 --- a/src/SeqCli/Cli/Commands/Workspace/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/Workspace/CreateCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using SeqCli.Api; diff --git a/src/SeqCli/Cli/Commands/Workspace/ListCommand.cs b/src/SeqCli/Cli/Commands/Workspace/ListCommand.cs index 8bf57afa..3acc85ad 100644 --- a/src/SeqCli/Cli/Commands/Workspace/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Workspace/ListCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs index c5efb71f..de1a26da 100644 --- a/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using SeqCli.Api; using SeqCli.Cli.Features; diff --git a/src/SeqCli/Cli/Features/PropertiesFeature.cs b/src/SeqCli/Cli/Features/PropertiesFeature.cs index fd0dde48..d55630ed 100644 --- a/src/SeqCli/Cli/Features/PropertiesFeature.cs +++ b/src/SeqCli/Cli/Features/PropertiesFeature.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Collections; using System.Collections.Generic; using SeqCli.Ingestion; diff --git a/src/SeqCli/Cli/Features/SettingNameFeature.cs b/src/SeqCli/Cli/Features/SettingNameFeature.cs index 025ac25a..b9c70dcf 100644 --- a/src/SeqCli/Cli/Features/SettingNameFeature.cs +++ b/src/SeqCli/Cli/Features/SettingNameFeature.cs @@ -16,7 +16,6 @@ using System.Collections.Generic; using Seq.Api.Model.Settings; using SeqCli.Util; -using Serilog; namespace SeqCli.Cli.Features; diff --git a/src/SeqCli/Csv/CsvToken.cs b/src/SeqCli/Csv/CsvToken.cs deleted file mode 100644 index eb095c4d..00000000 --- a/src/SeqCli/Csv/CsvToken.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SeqCli.Csv; - -enum CsvToken -{ - None, - Newline, - DoubleQuote, - Comma, - Number, - Boolean, - Null, - Text, - EscapedDoubleQuote -} \ No newline at end of file diff --git a/src/SeqCli/Csv/CsvTokenizer.cs b/src/SeqCli/Csv/CsvTokenizer.cs deleted file mode 100644 index 2f3529a4..00000000 --- a/src/SeqCli/Csv/CsvTokenizer.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using Superpower; -using Superpower.Model; -using Superpower.Parsers; - -namespace SeqCli.Csv; - -class CsvTokenizer : Tokenizer -{ - static readonly TextParser Content = Span.WithoutAny(ch => ch == '"'); - - protected override IEnumerable> Tokenize(TextSpan span) - { - var next = SkipInsignificant(span); - if (!next.HasValue) - yield break; - - do - { - // Here we're always either at the beginning of a line, or behind a comma. - if (next.Value == '"') - { - yield return Result.Value(CsvToken.DoubleQuote, next.Location, next.Remainder); - next = next.Remainder.ConsumeChar(); - if (!next.HasValue) yield break; - - var text = Content(next.Location); - while (text.HasValue || !text.Remainder.IsAtEnd) - { - if (text.HasValue) - { - if (TryMatchSpecialContent(text.Value, out var specialTokenType) && - !IsEscapedDoubleQuote(text.Remainder)) - yield return Result.Value(specialTokenType, text.Location, text.Remainder); - else - yield return Result.Value(CsvToken.Text, text.Location, text.Remainder); - } - - next = text.Remainder.ConsumeChar(); - if (!next.HasValue) yield break; - - if (next.Value != '"') - { - yield return Result.Empty(next.Location, ["double-quote"]); - yield break; - } - - var lookahead = next.Remainder.ConsumeChar(); - if (lookahead.HasValue && lookahead.Value == '"') - { - yield return Result.Value(CsvToken.EscapedDoubleQuote, next.Location, lookahead.Remainder); - next = lookahead.Remainder.ConsumeChar(); - if (!next.HasValue) yield break; - } - else - { - yield return Result.Value(CsvToken.DoubleQuote, next.Location, next.Remainder); - next = next.Remainder.ConsumeChar(); - if (!next.HasValue) yield break; - break; // Done with the content - } - - text = Content(next.Location); - } - - next = SkipInsignificant(next.Location); - if (next.Value == ',') - { - yield return Result.Value(CsvToken.Comma, next.Location, next.Remainder); - next = next.Remainder.ConsumeChar(); - if (!next.HasValue) yield break; - } - else if (next.Value == '\n') - { - yield return Result.Value(CsvToken.Newline, next.Location, next.Remainder); - next = next.Remainder.ConsumeChar(); - if (!next.HasValue) yield break; - } - else - { - yield return Result.Empty(next.Location, ["comma", "newline"]); - yield break; - } - } - else - { - yield return Result.Empty(next.Location, ["double-quote"]); - yield break; - } - - next = SkipInsignificant(next.Location); - } while (next.HasValue); - } - - static bool IsEscapedDoubleQuote(TextSpan span) - { - return span.Length >= 2 && - span.Source![span.Position.Absolute] == '"' && - span.Source[span.Position.Absolute + 1] == '"'; - } - - static bool TryMatchSpecialContent(TextSpan text, out CsvToken specialTokenType) - { - // Planning a switch from "True" to "true" for CSV Booleans - if (text.EqualsValueIgnoreCase("true") || - text.EqualsValueIgnoreCase("false")) - { - specialTokenType = CsvToken.Boolean; - return true; - } - - if (text.EqualsValue("null")) - { - specialTokenType = CsvToken.Null; - return true; - } - - // Just a quick temp job here until Superpower `Numerics` gets `Decimal` and `HexNumber`, plus - // an `IsMatch(TextSpan)` on `TextParser`. - var s = text.ToStringValue(); - if (text.Length > 0 - && text.Length < 16 - && (decimal.TryParse(text.ToStringValue(), out _) || - s.StartsWith("0x") && ulong.TryParse(s.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))) - { - specialTokenType = CsvToken.Number; - return true; - } - - specialTokenType = CsvToken.None; - return false; - } - - static Result SkipInsignificant(TextSpan span) - { - var result = span.ConsumeChar(); - while (result.HasValue && result.Value != '\n' && char.IsWhiteSpace(result.Value)) - result = result.Remainder.ConsumeChar(); - return result; - } -} \ No newline at end of file diff --git a/src/SeqCli/Csv/CsvWriter.cs b/src/SeqCli/Csv/CsvWriter.cs index 232f3d6b..049fd877 100644 --- a/src/SeqCli/Csv/CsvWriter.cs +++ b/src/SeqCli/Csv/CsvWriter.cs @@ -1,63 +1,111 @@ using System; -using System.Collections.Generic; +using System.Globalization; using System.IO; +using Seq.Api.Model.Data; +using SeqCli.Mcp.Data; using Serilog.Sinks.SystemConsole.Themes; -using Superpower.Model; namespace SeqCli.Csv; static class CsvWriter { - public static void WriteCsv(IEnumerable> csv, ConsoleTheme theme, TextWriter output, bool hasHeaderRow) + public static void WriteQueryResult(QueryResultPart result, ConsoleTheme theme, TextWriter output) { - var isHeaderRow = hasHeaderRow; - - foreach (var token in csv) + if (!string.IsNullOrWhiteSpace(result.Error)) + { + theme.Set(output, ConsoleThemeStyle.Text); + QueryResultHelper.WriteErrorResult(output, result); + theme.Reset(output); + } + + var first = true; + QueryResultHelper.Flatten(result, row => { - switch (token.Kind) + if (first) { - case CsvToken.Newline: - output.WriteLine(); - isHeaderRow = false; - break; - case CsvToken.Comma: - theme.Set(output, ConsoleThemeStyle.TertiaryText); - output.Write(','); - theme.Reset(output); - break; - case CsvToken.DoubleQuote: - theme.Set(output, ConsoleThemeStyle.TertiaryText); - output.Write('"'); - theme.Reset(output); - break; - case CsvToken.Boolean: - theme.Set(output, ConsoleThemeStyle.Boolean); - output.Write(token.ToStringValue()); - theme.Reset(output); - break; - case CsvToken.Null: - theme.Set(output, ConsoleThemeStyle.Null); - output.Write(token.ToStringValue()); - theme.Reset(output); - break; - case CsvToken.Number: - theme.Set(output, ConsoleThemeStyle.Number); - output.Write(token.ToStringValue()); - theme.Reset(output); - break; - case CsvToken.EscapedDoubleQuote: - theme.Set(output, ConsoleThemeStyle.Scalar); - output.Write(token.ToStringValue()); - theme.Reset(output); - break; - case CsvToken.Text: - theme.Set(output, isHeaderRow ? ConsoleThemeStyle.Name : ConsoleThemeStyle.Text); - output.Write(token.ToStringValue()); - theme.Reset(output); - break; - default: - throw new ArgumentException($"Unrecognized token `{token}`."); + first = false; + var firstCol = true; + foreach (var heading in row) + { + if (firstCol) + { + firstCol = false; + } + else + { + theme.Set(output, ConsoleThemeStyle.TertiaryText); + output.Write(", "); + theme.Reset(output); + } + + WriteCell(output, theme, heading, isHeadingRow: true); + } } + else + { + var firstCol = true; + foreach (var value in row) + { + if (firstCol) + { + firstCol = false; + } + else + { + theme.Set(output, ConsoleThemeStyle.TertiaryText); + output.Write(", "); + theme.Reset(output); + } + + WriteCell(output, theme, value); + } + } + output.WriteLine(); + }); + } + + static void WriteCell(TextWriter output, ConsoleTheme theme, object? value, bool isHeadingRow = false) + { + theme.Set(output, ConsoleThemeStyle.TertiaryText); + output.Write('"'); + theme.Reset(output); + + var valueAsString = value switch + { + null => "null", + true => "true", + false => "false", + decimal + or double or float or Half + or byte or ushort or uint or ulong or UInt128 or + sbyte or short or int or long or Int128 => ((IFormattable)value).ToString(null, CultureInfo.InvariantCulture), + DateTime dt => dt.ToString("o"), + DateTimeOffset dto => dto.ToString("o"), + _ => value.ToString() ?? "" + }; + + var dataStyle = isHeadingRow ? ConsoleThemeStyle.Name : ConsoleThemeStyle.Text; + var doubleQuote = valueAsString.IndexOf('"'); + while (doubleQuote != -1) + { + theme.Set(output, dataStyle); + output.Write(valueAsString[..doubleQuote]); + theme.Reset(output); + + theme.Set(output, ConsoleThemeStyle.Scalar); + output.Write("\"\""); + theme.Reset(output); + + valueAsString = valueAsString[(doubleQuote + 1)..]; + doubleQuote = valueAsString.IndexOf('"'); } + + theme.Set(output, dataStyle); + output.Write(valueAsString); + theme.Reset(output); + + theme.Set(output, ConsoleThemeStyle.TertiaryText); + output.Write('"'); + theme.Reset(output); } } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/ServiceProcess/SeqCliForwarderWindowsService.cs b/src/SeqCli/Forwarder/ServiceProcess/SeqCliForwarderWindowsService.cs index b07a25ac..21b21f6b 100644 --- a/src/SeqCli/Forwarder/ServiceProcess/SeqCliForwarderWindowsService.cs +++ b/src/SeqCli/Forwarder/ServiceProcess/SeqCliForwarderWindowsService.cs @@ -13,7 +13,6 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using System.Net; using System.ServiceProcess; using SeqCli.Forwarder.Web.Host; diff --git a/src/SeqCli/Mcp/Data/QueryResultHelper.cs b/src/SeqCli/Mcp/Data/QueryResultHelper.cs index 613d1aa0..7d86837b 100644 --- a/src/SeqCli/Mcp/Data/QueryResultHelper.cs +++ b/src/SeqCli/Mcp/Data/QueryResultHelper.cs @@ -14,12 +14,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Seq.Api.Model.Data; namespace SeqCli.Mcp.Data; -public static class QueryResultHelper +static class QueryResultHelper { /// /// Convert into a flat table. Seq reduces browser-side processing and optimizes @@ -102,4 +103,25 @@ static IEnumerable MergeColumns(string[] columns, TimeseriesPart? firstS yield return columns[i]; } } + + public static void WriteErrorResult(TextWriter output, QueryResultPart result) + { + output.WriteLine(result.Error); + output.WriteLine(); + + foreach (var reason in result.Reasons) + { + output.WriteLine($" - {reason}"); + } + + if (result.Reasons.Length > 0) + output.WriteLine(); + + if (!string.IsNullOrWhiteSpace(result.Suggestion)) + { + output.WriteLine("Perhaps you meant:"); + output.WriteLine(); + output.WriteLine(result.Suggestion); + } + } } \ No newline at end of file diff --git a/src/SeqCli/Output/NativeFormatter.cs b/src/SeqCli/Output/NativeFormatter.cs index c906ce4d..782a2433 100644 --- a/src/SeqCli/Output/NativeFormatter.cs +++ b/src/SeqCli/Output/NativeFormatter.cs @@ -235,6 +235,11 @@ static void WriteObject(TextWriter output, bool topLevel, params IEnumerable<(st public static void WriteQueryResult(TextWriter output, QueryResultPart result) { + if (!string.IsNullOrWhiteSpace(result.Error)) + { + QueryResultHelper.WriteErrorResult(output, result); + } + var first = true; QueryResultHelper.Flatten(result, row => { diff --git a/src/SeqCli/Output/OutputFormat.cs b/src/SeqCli/Output/OutputFormat.cs index 9a922b85..0e7c47d7 100644 --- a/src/SeqCli/Output/OutputFormat.cs +++ b/src/SeqCli/Output/OutputFormat.cs @@ -102,19 +102,6 @@ Logger CreateOutputLogger() return outputConfiguration.CreateLogger(); } - public void WriteCsv(string csv) - { - if (_noColor ) - { - Console.Write(csv); - } - else - { - var tokens = new CsvTokenizer().Tokenize(csv); - CsvWriter.WriteCsv(tokens, Theme, Console.Out, true); - } - } - public void WriteEntity(Entity entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); @@ -210,8 +197,7 @@ public void WriteQueryResult(QueryResultPart result) { if (Json) { - // Some friendlier JSON output is definitely possible here - Console.WriteLine(JsonConvert.SerializeObject(result)); + WriteObject(result); } else if (Native) { @@ -219,7 +205,7 @@ public void WriteQueryResult(QueryResultPart result) } else { - throw new InvalidOperationException("Plain text formatting not supported for query results."); + CsvWriter.WriteQueryResult(result, Theme, Console.Out); } } diff --git a/src/SeqCli/PlainText/Extraction/PatternElement.cs b/src/SeqCli/PlainText/Extraction/PatternElement.cs index 0b3c5bac..90703f43 100644 --- a/src/SeqCli/PlainText/Extraction/PatternElement.cs +++ b/src/SeqCli/PlainText/Extraction/PatternElement.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Superpower; using Superpower.Model; diff --git a/src/SeqCli/Program.cs b/src/SeqCli/Program.cs index f58599da..5363b98f 100644 --- a/src/SeqCli/Program.cs +++ b/src/SeqCli/Program.cs @@ -17,7 +17,6 @@ using System.Threading.Tasks; using Autofac; using SeqCli.Cli; -using SeqCli.Cli.Features; using SeqCli.Util; using Serilog; using Serilog.Core; diff --git a/src/SeqCli/Templates/Export/EntityName.cs b/src/SeqCli/Templates/Export/EntityName.cs index c4bd5c40..6cf2ab56 100644 --- a/src/SeqCli/Templates/Export/EntityName.cs +++ b/src/SeqCli/Templates/Export/EntityName.cs @@ -1,5 +1,6 @@ using System; using Seq.Api.Model; +using Seq.Api.Model.Queries; namespace SeqCli.Templates.Export; @@ -9,12 +10,18 @@ public static string FromEntityType(Type entityType) { if (!typeof(Entity).IsAssignableFrom(entityType)) throw new ArgumentException("Type is not an entity type."); + + if (typeof(QueryEntity) == entityType) + return "sqlquery"; return entityType.Name.ToLowerInvariant().Replace("entity", ""); } public static string ToResourceGroup(string resource) { + if (resource == "query") + return "sqlqueries"; + if (resource.EndsWith('y')) { return resource.TrimEnd('y') + "ies"; diff --git a/src/SeqCli/Templates/Export/TemplateSetExporter.cs b/src/SeqCli/Templates/Export/TemplateSetExporter.cs index 57d8d059..05c98d68 100644 --- a/src/SeqCli/Templates/Export/TemplateSetExporter.cs +++ b/src/SeqCli/Templates/Export/TemplateSetExporter.cs @@ -11,7 +11,7 @@ using Seq.Api.Model.Indexing; using Seq.Api.Model.Retention; using Seq.Api.Model.Signals; -using Seq.Api.Model.SqlQueries; +using Seq.Api.Model.Queries; using Seq.Api.Model.Workspaces; using Serilog; @@ -35,7 +35,7 @@ public async Task ExportTemplateSet() var templateValueMap = new TemplateValueMap(); templateValueMap.MapNonNullAsArg(nameof(DashboardEntity.OwnerId), "ownerId"); templateValueMap.MapNonNullAsArg(nameof(SignalEntity.OwnerId), "ownerId"); - templateValueMap.MapNonNullAsArg(nameof(SqlQueryEntity.OwnerId), "ownerId"); + templateValueMap.MapNonNullAsArg(nameof(QueryEntity.OwnerId), "ownerId"); templateValueMap.MapNonNullAsArg(nameof(WorkspaceEntity.OwnerId), "ownerId"); templateValueMap.MapNonNullAsArg(nameof(NotificationChannelPart.NotificationAppInstanceId), "notificationAppInstanceId"); templateValueMap.MapAsReference(nameof(SignalExpressionPart.SignalId)); @@ -50,9 +50,9 @@ await ExportTemplates( signal => signal.Title, templateValueMap); - await ExportTemplates( - id => _connection.SqlQueries.FindAsync(id), - () => _connection.SqlQueries.ListAsync(shared: true), + await ExportTemplates( + id => _connection.Queries.FindAsync(id), + () => _connection.Queries.ListAsync(shared: true), query => query.Title, templateValueMap); @@ -97,7 +97,7 @@ async Task ExportTemplates( where TEntity : Entity { List entities; - if (!_include.Any()) + if (_include.Count == 0) { entities = await listEntities(); } diff --git a/test/SeqCli.EndToEnd/Workspace/WorkspaceBasicsTestCase.cs b/test/SeqCli.EndToEnd/Workspace/WorkspaceBasicsTestCase.cs index 114466cc..18b7d6e7 100644 --- a/test/SeqCli.EndToEnd/Workspace/WorkspaceBasicsTestCase.cs +++ b/test/SeqCli.EndToEnd/Workspace/WorkspaceBasicsTestCase.cs @@ -21,14 +21,14 @@ public async Task ExecuteAsync( exit = runner.Exec("workspace list", "-t Example"); Assert.Equal(0, exit); - var output = runner.LastRunProcess.Output; + var output = runner.LastRunProcess!.Output; Assert.Equal("", output.Trim()); var items = ""; var dashboard = (await connection.Dashboards.ListAsync(shared: true)).First(); items += $" --content={dashboard.Id}"; - var query = (await connection.SqlQueries.ListAsync(shared: true)).First(); + var query = (await connection.Queries.ListAsync(shared: true)).First(); items += $" --content={query.Id}"; foreach (var signal in (await connection.Signals.ListAsync(shared: true)).Take(2)) diff --git a/test/SeqCli.Tests/Csv/CsvTokenizerTests.cs b/test/SeqCli.Tests/Csv/CsvTokenizerTests.cs deleted file mode 100644 index 1ff31b38..00000000 --- a/test/SeqCli.Tests/Csv/CsvTokenizerTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using SeqCli.Csv; -using Superpower.Model; -using Xunit; - -namespace SeqCli.Tests.Csv; - -public class CsvTokenizerTests -{ - [Fact] - public void AnEmptyStringYieldsNoTokens() - { - Assert.Empty(Tokenize("")); - } - - [Fact] - public void TokenizesATextCell() - { - var tokens = Tokenize("\"abc\""); - Assert.Equal(3, tokens.Count()); - Assert.Equal(tokens.Select(t => t.Kind), new[]{CsvToken.DoubleQuote, CsvToken.Text, CsvToken.DoubleQuote}); - } - - [Fact] - public void TokenizesARow() - { - var tokens = Tokenize("\"abc\",\"def\"\r\n"); - Assert.Equal(8, tokens.Count()); - } - - [Theory] - [InlineData(CsvToken.Text, "abc")] - [InlineData(CsvToken.Text, "1a")] - [InlineData(CsvToken.Text, "1\"\"")] - [InlineData(CsvToken.Number, "1")] - [InlineData(CsvToken.Number, "-123.45")] - [InlineData(CsvToken.Number, "0xa123")] - [InlineData(CsvToken.Boolean, "true")] - [InlineData(CsvToken.Boolean, "false")] - [InlineData(CsvToken.Null, "null")] - public void DetectsSpecialTokenTypes(object o, string cell) - { - var tokenKind = (CsvToken) o; - var content = Tokenize($"\"{cell}\"").ElementAt(1); - Assert.Equal(tokenKind, content.Kind); - } - - static TokenList Tokenize(string csv) - { - return new CsvTokenizer().Tokenize(csv); - } -} \ No newline at end of file From f827f1fc99f795afef48e12752e52e60621f494c Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 3 Jun 2026 13:41:57 +1000 Subject: [PATCH 2/4] Get rid of spaces trailing commas --- src/SeqCli/Csv/CsvWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SeqCli/Csv/CsvWriter.cs b/src/SeqCli/Csv/CsvWriter.cs index 049fd877..1cf74241 100644 --- a/src/SeqCli/Csv/CsvWriter.cs +++ b/src/SeqCli/Csv/CsvWriter.cs @@ -34,7 +34,7 @@ public static void WriteQueryResult(QueryResultPart result, ConsoleTheme theme, else { theme.Set(output, ConsoleThemeStyle.TertiaryText); - output.Write(", "); + output.Write(','); theme.Reset(output); } @@ -53,7 +53,7 @@ public static void WriteQueryResult(QueryResultPart result, ConsoleTheme theme, else { theme.Set(output, ConsoleThemeStyle.TertiaryText); - output.Write(", "); + output.Write(','); theme.Reset(output); } From 9a535a0bc3d5326dde5a9f45036e5f4f93b17c0a Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 3 Jun 2026 14:29:05 +1000 Subject: [PATCH 3/4] Remove the data resource group helper, make sure views are exported by the `template export` command --- .../Mcp/Data/DataResourceGroupHelper.cs | 56 ------------------- .../Tools/Search/SearchAndQueryToolType.cs | 36 +++--------- .../Templates/Export/TemplateSetExporter.cs | 7 +++ .../Templates/Import/TemplateSetImporter.cs | 2 +- 4 files changed, 16 insertions(+), 85 deletions(-) delete mode 100644 src/SeqCli/Mcp/Data/DataResourceGroupHelper.cs diff --git a/src/SeqCli/Mcp/Data/DataResourceGroupHelper.cs b/src/SeqCli/Mcp/Data/DataResourceGroupHelper.cs deleted file mode 100644 index d6d0559e..00000000 --- a/src/SeqCli/Mcp/Data/DataResourceGroupHelper.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright © Datalust 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.Globalization; -using System.IO; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Seq.Api; -using Seq.Api.Model.Data; - -namespace SeqCli.Mcp.Data; - -public static class DataResourceGroupHelper -{ - static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings - { - DateParseHandling = DateParseHandling.None, - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal, - }); - - public static async Task QueryPreserveErrorResponsesAsync(SeqConnection connection, string? signal, string query, CancellationToken cancellationToken = default) - { - // Unfortunately, the `Data.QueryAsync()` API throws when the server 400s, making this case tricky. Suggests - // we should make some API client improvements... - var queryUri = "api/data?q=" + Uri.EscapeDataString(query); - if (signal != null) - queryUri += "&signal=" + Uri.EscapeDataString(signal); - - var request = new HttpRequestMessage - { - RequestUri = new Uri(queryUri, UriKind.Relative), - Method = HttpMethod.Post, Content = new StringContent("{}", new UTF8Encoding(false), "application/json") - }; - - var response = await connection.Client.HttpClient.SendAsync(request, cancellationToken); - - return Serializer.Deserialize( - new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken))))!; - } -} \ No newline at end of file diff --git a/src/SeqCli/Mcp/Tools/Search/SearchAndQueryToolType.cs b/src/SeqCli/Mcp/Tools/Search/SearchAndQueryToolType.cs index fb51e7e6..c8f19517 100644 --- a/src/SeqCli/Mcp/Tools/Search/SearchAndQueryToolType.cs +++ b/src/SeqCli/Mcp/Tools/Search/SearchAndQueryToolType.cs @@ -30,7 +30,6 @@ using Seq.Api.Model.Signals; using Seq.Syntax.Templates; using SeqCli.Mapping; -using SeqCli.Mcp.Data; using SeqCli.Output; using SeqCli.Signals; using Serilog; @@ -278,10 +277,14 @@ public async Task QueryAsync( "To avoid consuming excessive resources, add a time bound such as `where @Timestamp >= now() - 1d`.", isError: true); } + SignalExpressionPart? parsedSignalExpression = null; + if (!string.IsNullOrWhiteSpace(signal)) + parsedSignalExpression = SignalExpressionParser.ParseExpression(signal); + QueryResultPart result; try { - result = await DataResourceGroupHelper.QueryPreserveErrorResponsesAsync(connection, signal, query, cancellationToken); + result = await connection.Data.TryQueryAsync(query, signal: parsedSignalExpression, cancellationToken: cancellationToken); } catch (Exception ex) { @@ -291,35 +294,12 @@ public async Task QueryAsync( } var error = ex.GetBaseException() is SeqApiException ? ex.GetBaseException().Message : ex.ToString(); - return SimpleTextResult($"The search failed. {error}", isError: true); - } - - if (result.Error != null) - { - return new CallToolResult - { - IsError = true, - Content = - [ - new TextContentBlock - { - Text = $"The query could not be executed. {result.Error}" - }, - new TextContentBlock - { - Text = string.Join(" ", result.Reasons) - }, - new TextContentBlock - { - Text = result.Suggestion != null ? $"Did you mean: {result.Suggestion}?" : "" - } - ] - }; + return SimpleTextResult($"The query failed. {error}", isError: true); } - + var output = new StringWriter(); NativeFormatter.WriteQueryResult(output, result); - return SimpleTextResult(output.ToString()); + return SimpleTextResult(output.ToString(), isError: !string.IsNullOrWhiteSpace(result.Error)); } [McpServerTool(Name = "seq_new_session", ReadOnly = true, Title = "Begin a new Search/Query Session")] diff --git a/src/SeqCli/Templates/Export/TemplateSetExporter.cs b/src/SeqCli/Templates/Export/TemplateSetExporter.cs index 05c98d68..772ca43f 100644 --- a/src/SeqCli/Templates/Export/TemplateSetExporter.cs +++ b/src/SeqCli/Templates/Export/TemplateSetExporter.cs @@ -9,6 +9,7 @@ using Seq.Api.Model.Alerting; using Seq.Api.Model.Dashboarding; using Seq.Api.Model.Indexing; +using Seq.Api.Model.Metrics; using Seq.Api.Model.Retention; using Seq.Api.Model.Signals; using Seq.Api.Model.Queries; @@ -87,6 +88,12 @@ await ExportTemplates( ? expressionIndex.Expression : expressionIndex.Id.Replace("expressionindex-", ""), templateValueMap); + + await ExportTemplates( + id => _connection.Views.FindAsync(id), + () => _connection.Views.ListAsync(shared: true), + view => view.Title, + templateValueMap); } async Task ExportTemplates( diff --git a/src/SeqCli/Templates/Import/TemplateSetImporter.cs b/src/SeqCli/Templates/Import/TemplateSetImporter.cs index 9f14ec5c..85e94263 100644 --- a/src/SeqCli/Templates/Import/TemplateSetImporter.cs +++ b/src/SeqCli/Templates/Import/TemplateSetImporter.cs @@ -40,7 +40,7 @@ static class TemplateSetImporter { var ordering = new List {"users", "signals", "apps", "appinstances", "dashboards", "sqlqueries", "workspaces", "retentionpolicies", - "alerts", "expressionindexes"}; + "alerts", "expressionindexes", "views"}; var sorted = templates.OrderBy(t => ordering.IndexOf(t.ResourceGroup)); From 6cef52e65948902f27e2d5298d4639a5e373a34f Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 3 Jun 2026 14:32:49 +1000 Subject: [PATCH 4/4] Refactor CSV writing --- src/SeqCli/Csv/CsvWriter.cs | 54 +++++++++++-------------------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/src/SeqCli/Csv/CsvWriter.cs b/src/SeqCli/Csv/CsvWriter.cs index 1cf74241..e984ea05 100644 --- a/src/SeqCli/Csv/CsvWriter.cs +++ b/src/SeqCli/Csv/CsvWriter.cs @@ -21,51 +21,29 @@ public static void WriteQueryResult(QueryResultPart result, ConsoleTheme theme, var first = true; QueryResultHelper.Flatten(result, row => { - if (first) + var firstCol = true; + foreach (var value in row) { - first = false; - var firstCol = true; - foreach (var heading in row) - { - if (firstCol) - { - firstCol = false; - } - else - { - theme.Set(output, ConsoleThemeStyle.TertiaryText); - output.Write(','); - theme.Reset(output); - } - - WriteCell(output, theme, heading, isHeadingRow: true); - } - } - else - { - var firstCol = true; - foreach (var value in row) - { - if (firstCol) - { - firstCol = false; - } - else - { - theme.Set(output, ConsoleThemeStyle.TertiaryText); - output.Write(','); - theme.Reset(output); - } - - WriteCell(output, theme, value); - } + WriteCell(output, theme, value, ref firstCol, isHeadingRow: first); } + first = false; output.WriteLine(); }); } - static void WriteCell(TextWriter output, ConsoleTheme theme, object? value, bool isHeadingRow = false) + static void WriteCell(TextWriter output, ConsoleTheme theme, object? value, ref bool firstCol, bool isHeadingRow = false) { + if (firstCol) + { + firstCol = false; + } + else + { + theme.Set(output, ConsoleThemeStyle.TertiaryText); + output.Write(','); + theme.Reset(output); + } + theme.Set(output, ConsoleThemeStyle.TertiaryText); output.Write('"'); theme.Reset(output);