diff --git a/src/Roastery/Data/Database.cs b/src/Roastery/Data/Database.cs
index 9234f1ce..251b67f1 100644
--- a/src/Roastery/Data/Database.cs
+++ b/src/Roastery/Data/Database.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
diff --git a/src/Roastery/Util/Distribution.cs b/src/Roastery/Util/Distribution.cs
index b20ffb49..5264cd14 100644
--- a/src/Roastery/Util/Distribution.cs
+++ b/src/Roastery/Util/Distribution.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
-using System.Threading;
namespace Roastery.Util;
diff --git a/src/Roastery/Web/RequestLoggingMiddleware.cs b/src/Roastery/Web/RequestLoggingMiddleware.cs
index c75a5002..92d50bf3 100644
--- a/src/Roastery/Web/RequestLoggingMiddleware.cs
+++ b/src/Roastery/Web/RequestLoggingMiddleware.cs
@@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
-using System.Diagnostics.Metrics;
using System.Net;
using System.Threading.Tasks;
using Roastery.Metrics;
diff --git a/src/SeqCli/Api/EventEntityJson.cs b/src/SeqCli/Api/EventEntityJson.cs
new file mode 100644
index 00000000..308c0735
--- /dev/null
+++ b/src/SeqCli/Api/EventEntityJson.cs
@@ -0,0 +1,104 @@
+// Copyright © Datalust Pty Ltd
+//
+// 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.Globalization;
+using System.Text;
+using System.Text.Json.Nodes;
+using Seq.Api.Model.Events;
+using Seq.Api.Model.Shared;
+using SeqCli.Data;
+using SeqCli.Output;
+
+namespace SeqCli.Api;
+
+///
+/// Converts event entities into compact JSON format for further processing. This class is only necessary because
+/// Seq.Api doesn't yet provide a simple compact-JSON based result format for searches. Once we've filled
+/// that gap, this class, and can be removed.
+///
+static class EventEntityJson
+{
+ public static JsonObject ToEventJson(EventEntity evt)
+ {
+ var eventJson = new JsonObject
+ {
+ // Earlier versions relied on Serilog output formatting to show timestamps in local time; we'll need
+ // to consider adding some compensating mechanism to `Seq.Syntax`.
+ ["@t"] = DateTimeOffset.ParseExact(evt.Timestamp, "o", CultureInfo.InvariantCulture)
+ .ToLocalTime().ToString("o", CultureInfo.InvariantCulture)
+ };
+
+ if (evt.MessageTemplateTokens != null)
+ eventJson["@mt"] = ToMessageTemplateText(evt.MessageTemplateTokens);
+
+ if (!string.IsNullOrWhiteSpace(evt.Level) && evt.Level != "Information")
+ eventJson["@l"] = evt.Level;
+
+ if (!string.IsNullOrWhiteSpace(evt.Exception))
+ eventJson["@x"] = evt.Exception;
+
+ if (!string.IsNullOrWhiteSpace(evt.TraceId))
+ eventJson["@tr"] = evt.TraceId;
+
+ if (!string.IsNullOrWhiteSpace(evt.SpanId))
+ eventJson["@sp"] = evt.SpanId;
+
+ if (!string.IsNullOrWhiteSpace(evt.ParentId))
+ eventJson["@ps"] = evt.ParentId;
+
+ if (!string.IsNullOrWhiteSpace(evt.Start))
+ eventJson["@st"] = evt.Start;
+
+ if (!string.IsNullOrWhiteSpace(evt.SpanKind))
+ eventJson["@sk"] = evt.SpanKind;
+
+ if (evt.Resource?.Count > 0)
+ eventJson["@ra"] = ToPropertiesObject(evt.Resource);
+
+ if (evt.Scope?.Count > 0)
+ eventJson["@sa"] = ToPropertiesObject(evt.Scope);
+
+ if (evt.Properties != null)
+ {
+ foreach (var property in evt.Properties)
+ eventJson[EventJsonFormat.EscapeUserPropertyName(property.Name)] = ToSystemTextJson.FromApiValue(property.Value);
+ }
+
+ return eventJson;
+ }
+
+ static string ToMessageTemplateText(List tokens)
+ {
+ var text = new StringBuilder();
+ foreach (var token in tokens)
+ {
+ if (token.Text != null)
+ text.Append(token.Text.Replace("{", "{{").Replace("}", "}}"));
+ else
+ text.Append(token.RawText ?? $"{{{token.PropertyName}}}");
+ }
+
+ return text.ToString();
+ }
+
+ static JsonObject ToPropertiesObject(List properties)
+ {
+ var result = new JsonObject();
+ foreach (var property in properties)
+ result[property.Name] = ToSystemTextJson.FromApiValue(property.Value);
+ return result;
+ }
+}
diff --git a/src/SeqCli/Api/LevelMapping.cs b/src/SeqCli/Api/LevelMapping.cs
new file mode 100644
index 00000000..bc987977
--- /dev/null
+++ b/src/SeqCli/Api/LevelMapping.cs
@@ -0,0 +1,99 @@
+// 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.Collections.Generic;
+using Seq.Api.Model.LogEvents;
+
+namespace SeqCli.Api;
+
+public static class LevelMapping
+{
+ static readonly Dictionary LevelsByName =
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["t"] = "Trace",
+ ["tr"] = "Trace",
+ ["trc"] = "Trace",
+ ["trce"] = "Trace",
+ ["trace"] = "Trace",
+ ["v"] = "Verbose",
+ ["ver"] = "Verbose",
+ ["vrb"] = "Verbose",
+ ["verb"] = "Verbose",
+ ["verbose"] = "Verbose",
+ ["d"] = "Debug",
+ ["de"] = "Debug",
+ ["dbg"] = "Debug",
+ ["deb"] = "Debug",
+ ["dbug"] = "Debug",
+ ["debu"] = "Debug",
+ ["debug"] = "Debug",
+ ["i"] = "Information",
+ ["in"] = "Information",
+ ["inf"] = "Information",
+ ["info"] = "Information",
+ ["information"] = "Information",
+ ["notice"] = "Notice",
+ ["w"] = "Warning",
+ ["wa"] = "Warning",
+ ["war"] = "Warning",
+ ["wrn"] = "Warning",
+ ["warn"] = "Warning",
+ ["warning"] = "Warning",
+ ["e"] = "Error",
+ ["er"] = "Error",
+ ["err"] = "Error",
+ ["erro"] = "Error",
+ ["eror"] = "Error",
+ ["error"] = "Error",
+ ["f"] = "Fatal",
+ ["fa"] = "Fatal",
+ ["ftl"] = "Fatal",
+ ["fat"] = "Fatal",
+ ["fatl"] = "Fatal",
+ ["fatal"] = "Fatal",
+ ["c"] = "Critical",
+ ["cr"] = "Critical",
+ ["crt"] = "Critical",
+ ["cri"] = "Critical",
+ ["crit"] = "Critical",
+ ["critical"] = "Critical",
+ ["emerg"] = "Emergency",
+ ["alert"] = "Alert",
+ ["panic"] = "Panic"
+ };
+
+ // Intended only for use by ingest extraction patterns.
+ public static string ToFullLevelName(string level)
+ {
+ return LevelsByName.TryGetValue(level, out var m) ? m : level;
+ }
+
+ public static LogEventLevel ToSeqApiLogEventLevel(string level)
+ {
+ if (string.IsNullOrEmpty(level))
+ return LogEventLevel.Information;
+
+ return ToFullLevelName(level) switch
+ {
+ "Trace" or "Verbose" => LogEventLevel.Verbose,
+ "Debug" => LogEventLevel.Debug,
+ "Warning" => LogEventLevel.Warning,
+ "Error" => LogEventLevel.Error,
+ "Fatal" or "Critical" or "Emergency" or "Alert" or "Panic" => LogEventLevel.Fatal,
+ _ => LogEventLevel.Information
+ };
+ }
+}
diff --git a/src/SeqCli/Api/ToSystemTextJson.cs b/src/SeqCli/Api/ToSystemTextJson.cs
new file mode 100644
index 00000000..92428c06
--- /dev/null
+++ b/src/SeqCli/Api/ToSystemTextJson.cs
@@ -0,0 +1,45 @@
+// Copyright © Datalust Pty Ltd
+//
+// 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.Text.Json.Nodes;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using SeqCli.Data;
+
+namespace SeqCli.Api;
+
+static class ToSystemTextJson
+{
+ ///
+ /// Convert a value deserialized by the Seq API client into its `System.Text.Json` equivalent.
+ ///
+ public static JsonNode? FromApiValue(object? value)
+ {
+ return value switch
+ {
+ null => null,
+ JToken token => FromNewtonsoft(token),
+ _ => EventJsonFormat.CreateScalar(value)
+ };
+ }
+
+ /// Conversion helper for values retrieved through the Seq API client.
+ public static JsonNode? FromNewtonsoft(JToken token)
+ {
+ if (token is JValue { Value: null })
+ return null;
+
+ return JsonNode.Parse(token.ToString(Formatting.None));
+ }
+}
diff --git a/src/SeqCli/Apps/AppLoader.cs b/src/SeqCli/Apps/AppLoader.cs
index c0a03ff5..e46cdfed 100644
--- a/src/SeqCli/Apps/AppLoader.cs
+++ b/src/SeqCli/Apps/AppLoader.cs
@@ -34,7 +34,8 @@ class AppLoader : IDisposable
[
typeof(SeqApp).Assembly,
typeof(Log).Assembly,
- typeof(SerilogExpression).Assembly
+ // Seq.Syntax uses version-specific assembly names to improve our chances of successful loading.
+ typeof(SeqExpression).Assembly
];
public AppLoader(string packageBinaryPath)
diff --git a/src/SeqCli/Apps/Hosting/AppContainer.cs b/src/SeqCli/Apps/Hosting/AppContainer.cs
index 8a58c2bd..6949921d 100644
--- a/src/SeqCli/Apps/Hosting/AppContainer.cs
+++ b/src/SeqCli/Apps/Hosting/AppContainer.cs
@@ -21,7 +21,7 @@
using Newtonsoft.Json.Linq;
using Seq.Apps;
using Seq.Apps.LogEvents;
-using SeqCli.Mapping;
+using SeqCli.Api;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
@@ -109,11 +109,11 @@ async Task SendTypedEventAsync(string clef)
{
if (_seqApp is ISubscribeTo led)
{
- led.On(EventFormat.FromRaw(eventId, eventType, serilogEvent));
+ led.On(EventFormat.FromSerilogLogEvent(eventId, eventType, serilogEvent));
}
else if (_seqApp is ISubscribeToAsync leda)
{
- await leda.OnAsync(EventFormat.FromRaw(eventId, eventType, serilogEvent));
+ await leda.OnAsync(EventFormat.FromSerilogLogEvent(eventId, eventType, serilogEvent));
}
else if (_seqApp is ISubscribeTo sled)
{
@@ -143,7 +143,8 @@ LogEvent ReadSerilogEvent(string clef, out string eventId, out uint eventType)
if (jobject.TryGetValue("@l", out var levelToken))
{
jobject.Remove("@l");
- jobject.Add("@l", new JValue(LevelMapping.ToSerilogLevel(levelToken.Value()!).ToString()));
+ // The Seq.Api `LogEventLevel` enum intentionally matches the Serilog one.
+ jobject.Add("@l", new JValue(LevelMapping.ToSeqApiLogEventLevel(levelToken.Value()!).ToString()));
}
SanitizeTraceIdentifiers(jobject);
diff --git a/src/SeqCli/Apps/Hosting/EventFormat.cs b/src/SeqCli/Apps/Hosting/EventFormat.cs
index 1ff23253..67a37b4d 100644
--- a/src/SeqCli/Apps/Hosting/EventFormat.cs
+++ b/src/SeqCli/Apps/Hosting/EventFormat.cs
@@ -24,7 +24,7 @@ namespace SeqCli.Apps.Hosting;
static class EventFormat
{
- public static Event FromRaw(string eventId, uint eventType, LogEvent raw)
+ public static Event FromSerilogLogEvent(string eventId, uint eventType, LogEvent raw)
{
var properties = new Dictionary();
foreach (var prop in raw.Properties)
diff --git a/src/SeqCli/Cli/Commands/Alert/CreateCommand.cs b/src/SeqCli/Cli/Commands/Alert/CreateCommand.cs
index 49ceba1d..d8a2182d 100644
--- a/src/SeqCli/Cli/Commands/Alert/CreateCommand.cs
+++ b/src/SeqCli/Cli/Commands/Alert/CreateCommand.cs
@@ -17,12 +17,10 @@
using System.Linq;
using System.Threading.Tasks;
using Seq.Api.Model.Alerting;
-using Seq.Api.Model.LogEvents;
using Seq.Api.Model.Shared;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
-using SeqCli.Mapping;
using SeqCli.Signals;
using SeqCli.Syntax;
using SeqCli.Util;
@@ -178,7 +176,7 @@ protected override async Task Run()
alert.Having = _having;
if (_notificationLevel != null)
- alert.NotificationLevel = Enum.Parse(LevelMapping.ToFullLevelName(_notificationLevel));
+ alert.NotificationLevel = LevelMapping.ToSeqApiLogEventLevel(_notificationLevel);
if (_suppressionTime != null)
alert.SuppressionTime = DurationMoniker.ToTimeSpan(_suppressionTime);
diff --git a/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs
index fe514434..25375b1e 100644
--- a/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs
+++ b/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs
@@ -16,13 +16,11 @@
using System.Linq;
using System.Threading.Tasks;
using Seq.Api;
-using Seq.Api.Model.LogEvents;
using Seq.Api.Model.Security;
using Seq.Api.Model.Shared;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
-using SeqCli.Mapping;
using SeqCli.Util;
using Serilog;
@@ -125,7 +123,7 @@ protected override async Task Run()
if (_level != null)
{
- apiKey.InputSettings.MinimumLevel = Enum.Parse(LevelMapping.ToFullLevelName(_level));
+ apiKey.InputSettings.MinimumLevel = LevelMapping.ToSeqApiLogEventLevel(_level);
}
apiKey.AssignedPermissions.Clear();
diff --git a/src/SeqCli/Cli/Commands/IngestCommand.cs b/src/SeqCli/Cli/Commands/IngestCommand.cs
index b965ddb3..e0ad35a6 100644
--- a/src/SeqCli/Cli/Commands/IngestCommand.cs
+++ b/src/SeqCli/Cli/Commands/IngestCommand.cs
@@ -14,18 +14,17 @@
using System;
using System.Collections.Generic;
+using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
+using SeqCli.Data;
using SeqCli.Ingestion;
-using SeqCli.Mapping;
using SeqCli.PlainText;
using SeqCli.Syntax;
using Serilog;
-using Serilog.Core;
-using Serilog.Events;
namespace SeqCli.Cli.Commands;
@@ -84,19 +83,19 @@ protected override async Task Run()
{
try
{
- var enrichers = new List();
-
+ var enrichers = new List();
+
if (_level != null)
- enrichers.Add(new ScalarPropertyEnricher(LevelMapping.SurrogateLevelProperty, _level));
-
+ enrichers.Add(new LevelEnricher(_level));
+
foreach (var (name, value) in _properties.FlatProperties)
enrichers.Add(new ScalarPropertyEnricher(name, value));
- Func? filter = null;
+ Func? filter = null;
if (_filter != null)
{
var eval = SeqSyntax.CompileExpression(_filter);
- filter = evt => Seq.Syntax.Expressions.ExpressionResult.IsTrue(eval(evt));
+ filter = evt => eval(evt).IsTrue();
}
var config = RuntimeConfigurationLoader.Load(_storagePath);
@@ -112,9 +111,9 @@ protected override async Task Run()
{
using (input)
{
- ILogEventReader reader = _json
- ? new JsonLogEventReader(input)
- : new PlainTextLogEventReader(input, _pattern);
+ IEventReader reader = _json
+ ? new JsonEventReader(input)
+ : new PlainTextEventReader(input, _pattern);
reader = new EnrichingReader(reader, enrichers);
diff --git a/src/SeqCli/Cli/Commands/PrintCommand.cs b/src/SeqCli/Cli/Commands/PrintCommand.cs
index 2740297f..0bc67f59 100644
--- a/src/SeqCli/Cli/Commands/PrintCommand.cs
+++ b/src/SeqCli/Cli/Commands/PrintCommand.cs
@@ -14,16 +14,16 @@
using System;
using System.IO;
+using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Threading.Tasks;
-using Newtonsoft.Json;
-using Seq.Syntax.Expressions;
using SeqCli.Cli.Features;
using SeqCli.Config;
using SeqCli.Ingestion;
using SeqCli.Output;
+using SeqCli.Syntax;
using SeqCli.Util;
using Serilog;
-using Serilog.Events;
namespace SeqCli.Cli.Commands;
@@ -61,16 +61,16 @@ protected override async Task Run()
{
var config = RuntimeConfigurationLoader.Load(_storage);
- Func? filter = null;
+ Func? filter = null;
if (_filter != null)
{
- if (!SerilogExpression.TryCompile(_filter, out var compiled, out var error))
+ if (!SeqSyntax.TryCompileExpression(_filter, out var compiled, out var error))
{
Log.Error("The specified filter could not be compiled: {Error}", error);
return 1;
}
- filter = evt => ExpressionResult.IsTrue(compiled(evt));
+ filter = evt => compiled(evt).IsTrue();
}
var template = _template == null ? null : PrintTemplate.InterpretEscapeChars(_template);
@@ -80,7 +80,7 @@ protected override async Task Run()
{
using (input)
{
- var reader = new JsonLogEventReader(input);
+ var reader = new JsonEventReader(input);
var isAtEnd = false;
do
@@ -90,12 +90,12 @@ protected override async Task Run()
var result = await reader.TryReadAsync();
isAtEnd = result.IsAtEnd;
- if (result.LogEvent != null && (filter == null || filter(result.LogEvent)))
- output.WriteLogEvent(result.LogEvent);
+ if (result.Document != null && (filter == null || filter(result.Document)))
+ output.WriteEvent(result.Document);
}
catch (Exception ex)
{
- if (ex is not JsonReaderException && ex is not InvalidDataException ||
+ if (ex is not JsonException && ex is not InvalidDataException ||
_invalidDataHandlingFeature.InvalidDataHandling != InvalidDataHandling.Ignore)
throw;
}
diff --git a/src/SeqCli/Cli/Commands/TailCommand.cs b/src/SeqCli/Cli/Commands/TailCommand.cs
index 9d4d4957..291433ba 100644
--- a/src/SeqCli/Cli/Commands/TailCommand.cs
+++ b/src/SeqCli/Cli/Commands/TailCommand.cs
@@ -13,6 +13,8 @@
// limitations under the License.
using System;
+using System.IO;
+using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using SeqCli.Api;
@@ -63,13 +65,15 @@ protected override async Task Run()
try
{
- await foreach (var evt in connection.Events.StreamAsync(
+ await foreach (var evt in connection.Events.StreamDocumentsAsync(
filter: strict,
signal: _signal.Signal,
render: true,
+ clef: true,
cancellationToken: cancel.Token))
{
- output.WriteEventEntity(evt);
+ var eventJson = JsonNode.Parse(evt)?.AsObject() ?? throw new InvalidDataException("Non-JSON document received.");
+ output.WriteEvent(eventJson);
}
}
catch (OperationCanceledException)
diff --git a/src/SeqCli/Cli/Commands/TraceCommand.cs b/src/SeqCli/Cli/Commands/TraceCommand.cs
index 6e255069..67541f3b 100644
--- a/src/SeqCli/Cli/Commands/TraceCommand.cs
+++ b/src/SeqCli/Cli/Commands/TraceCommand.cs
@@ -151,8 +151,8 @@ protected override async Task Run()
}
else
{
- foreach (var logEvent in TraceFormatter.ToLogEvents(subtreeRoot != null ? [subtreeRoot] : roots))
- output.WriteLogEvent(logEvent);
+ foreach (var eventJson in TraceFormatter.ToEventJson(subtreeRoot != null ? [subtreeRoot] : roots))
+ output.WriteEvent(eventJson);
}
return 0;
diff --git a/src/SeqCli/Csv/CsvWriter.cs b/src/SeqCli/Csv/CsvWriter.cs
index 75f6553a..f87ae2a3 100644
--- a/src/SeqCli/Csv/CsvWriter.cs
+++ b/src/SeqCli/Csv/CsvWriter.cs
@@ -1,22 +1,34 @@
using System;
-using System.Collections.Generic;
using System.IO;
using Seq.Api.Model.Data;
+using Seq.Syntax.Templates.Themes;
using SeqCli.Mcp.Data;
-using SeqCli.Output;
-using Serilog.Templates.Themes;
namespace SeqCli.Csv;
static class CsvWriter
{
+ // Delimited output is written directly rather than rendered through a template, so styled
+ // runs are opened and closed here.
+ static void SetStyle(TextWriter output, TemplateTheme? theme, TemplateThemeStyle style)
+ {
+ if (theme?.Open(style) is { } open)
+ output.Write(open);
+ }
+
+ static void ResetStyle(TextWriter output, TemplateTheme? theme, TemplateThemeStyle style)
+ {
+ if (theme?.Close(style) is { } close)
+ output.Write(close);
+ }
+
public static void WriteQueryResult(QueryResultPart result, Func