Skip to content

Add --properties option to seqcli ingest and seqcli log #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class IngestCommand : Command
readonly SendFailureHandlingFeature _sendFailureHandlingFeature;
readonly ConnectionFeature _connection;
readonly BatchSizeFeature _batchSize;
readonly PropertiesExpressionFeature _propertiesExpression;
string? _filter, _level, _message;
string _pattern = DefaultPattern;
bool _json;
Expand All @@ -50,6 +51,7 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_fileInputFeature = Enable(new FileInputFeature("File(s) to ingest", supportsWildcard: true));
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();
_propertiesExpression = Enable<PropertiesExpressionFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
Expand Down Expand Up @@ -87,6 +89,9 @@ protected override async Task<int> Run()
if (_level != null)
enrichers.Add(new ScalarPropertyEnricher(LevelMapping.SurrogateLevelProperty, _level));

if (_propertiesExpression.GetEnricher() is {} enricher)
enrichers.Add(enricher);

foreach (var (name, value) in _properties.Properties)
enrichers.Add(new ScalarPropertyEnricher(name, value));

Expand All @@ -109,11 +114,11 @@ protected override async Task<int> Run()
? (ILogEventReader) new JsonLogEventReader(input)
: new PlainTextLogEventReader(input, _pattern);

reader = new EnrichingReader(reader, enrichers);

if (_message != null)
reader = new StaticMessageTemplateReader(reader, _message);

reader = new EnrichingReader(reader, enrichers);

var exit = await LogShipper.ShipEvents(
connection,
apiKey,
Expand Down
54 changes: 40 additions & 14 deletions src/SeqCli/Cli/Commands/LogCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Connection;
using SeqCli.Output;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;

// ReSharper disable UseAwaitUsing, MethodHasAsyncOverload
// ReSharper disable UnusedType.Global, UseAwaitUsing, MethodHasAsyncOverload

namespace SeqCli.Cli.Commands;

Expand All @@ -35,6 +38,7 @@ class LogCommand : Command
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string? _message, _level, _timestamp, _exception;
readonly PropertiesExpressionFeature _propertiesExpression;

public LogCommand(SeqConnectionFactory connectionFactory)
{
Expand All @@ -61,6 +65,7 @@ public LogCommand(SeqConnectionFactory connectionFactory)
v => _exception = v);

_properties = Enable<PropertiesFeature>();
_propertiesExpression = Enable<PropertiesExpressionFeature>();
_connection = Enable<ConnectionFeature>();
}

Expand All @@ -80,26 +85,47 @@ protected override async Task<int> Run()
if (!string.IsNullOrWhiteSpace(_exception))
payload["@x"] = _exception;

foreach (var (key, value) in _properties.Properties)
StringContent content;
if (_propertiesExpression.GetEnricher() is { } enricher)
{
if (string.IsNullOrWhiteSpace(key))
continue;
var jo = JObject.FromObject(payload);
var evt = LogEventReader.ReadFromJObject(jo);
// We're breaking the nullability contract of `ILogEventEnricher.Enrich()`, here.
enricher.Enrich(evt, null!);
foreach (var (key, value) in _properties.Properties)
{
if (string.IsNullOrWhiteSpace(key))
continue;

var name = key.Trim();
if (name.StartsWith('@'))
name = $"@{name}";
var name = key.Trim();
evt.AddOrUpdateProperty(new LogEventProperty(name, new ScalarValue(value)));
}

payload[name] = new JValue(value);
var sw = new StringWriter();
OutputFormatter.Json.Format(evt, sw);
content = new StringContent(sw.ToString(), Encoding.UTF8, ApiConstants.ClefMediaType);
}

StringContent content;
using (var builder = new StringWriter())
using (var jsonWriter = new JsonTextWriter(builder))
else
{
// Explicit properties override computed ones.
foreach (var (key, value) in _properties.Properties)
{
if (string.IsNullOrWhiteSpace(key))
continue;

var name = key.Trim();
if (name.StartsWith('@'))
name = $"@{name}";

payload[name] = new JValue(value);
}

using var sw = new StringWriter();
using var jsonWriter = new JsonTextWriter(sw);
payload.WriteTo(jsonWriter);
jsonWriter.Flush();
builder.WriteLine();
content = new StringContent(builder.ToString(), Encoding.UTF8, ApiConstants.ClefMediaType);
sw.WriteLine();
content = new StringContent(sw.ToString(), Encoding.UTF8, ApiConstants.ClefMediaType);
}

var connection = _connectionFactory.Connect(_connection);
Expand Down
69 changes: 69 additions & 0 deletions src/SeqCli/Cli/Features/PropertiesExpressionFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using Seq.Syntax.Expressions;
using SeqCli.Syntax;
using SeqCli.Util;
using Serilog.Core;
using Serilog.Events;

namespace SeqCli.Cli.Features;

class PropertiesExpressionFeature : CommandFeature
{
string? _expression;

public override void Enable(OptionSet options)
{
options.Add(
"properties=",
"A Seq syntax object expression describing additional event properties",
v => _expression = ArgumentString.Normalize(v));
}

public override IEnumerable<string> GetUsageErrors()
{
if (_expression == null) yield break;

if (!SerilogExpression.TryCompile(_expression, null, new SeqCliNameResolver(), out _, out var error))
yield return error;
}

public ILogEventEnricher? GetEnricher()
{
if (_expression == null) return null;

// Spread enables the expression to overwrite existing property values, use them to compute new properties, and
// remove them with `x: undefined()`.
var fullExpr = $"{{..@p, ..{_expression}}}";
var expr = SerilogExpression.Compile(fullExpr, null, new SeqCliNameResolver());
return new PropertiesExpressionEnricher(expr);
}

class PropertiesExpressionEnricher : ILogEventEnricher
{
readonly CompiledExpression _expr;

public PropertiesExpressionEnricher(CompiledExpression expr)
{
_expr = expr;
}

public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var newProps = _expr(logEvent) as StructureValue ??
throw new InvalidOperationException("Properties expression did not evaluate to an object value.");

var missing = new HashSet<string>(logEvent.Properties.Keys);
foreach (var prop in newProps.Properties)
{
missing.Remove(prop.Name);
logEvent.AddOrUpdateProperty(prop);
}

foreach (var propName in missing)
{
logEvent.RemovePropertyIfPresent(propName);
}
}
}
}
1 change: 1 addition & 0 deletions src/SeqCli/Cli/Features/PropertiesFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using System.Collections.Generic;
using System.Linq;

namespace SeqCli.Cli.Features;

Expand Down
2 changes: 1 addition & 1 deletion src/SeqCli/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"profiles": {
"SeqCli": {
"commandName": "Project",
"commandLineArgs": "signal update --json-stdin"
"commandLineArgs": "log -m test -p Name=World"
}
}
}