Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/SeqCli/Cli/Commands/IngestCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ class IngestCommand : Command
readonly FileInputFeature _fileInputFeature;
readonly PropertiesFeature _properties;
readonly ConnectionFeature _connection;
string _filter;
string _filter, _pattern;
bool _json;

public IngestCommand(SeqConnectionFactory connectionFactory)
Expand All@@ -47,14 +47,18 @@ public IngestCommand(SeqConnectionFactory connectionFactory)
_invalidDataHandlingFeature = Enable<InvalidDataHandlingFeature>();
_properties = Enable<PropertiesFeature>();

Options.Add("x=|extract=",
"An extraction pattern to apply to plain-text logs (ignored when `--json` is specified)",
v => _pattern = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

Options.Add("json",
"Read the events as JSON (the default assumes plain text)",
v => _json = true);

Options.Add("f=|filter=",
"Filter expression to select a subset of events",
v => _filter = string.IsNullOrWhiteSpace(v) ? null : v.Trim());

_connection = Enable<ConnectionFeature>();
}

Expand DownExpand Up@@ -82,7 +86,7 @@ protected override async Task<int> Run()

var reader = _json ?
(ILogEventReader)new ClefLogEventReader(input) :
new PlainTextLogEventReader(input);
new PlainTextLogEventReader(input, _pattern);

using (reader as IDisposable)
{
Expand Down
60 changes: 0 additions & 60 deletions src/SeqCli/PlainText/BuiltInPatterns.cs

This file was deleted.

60 changes: 60 additions & 0 deletions src/SeqCli/PlainText/Extraction/ExtractionPatternInterpreter.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SeqCli.PlainText.Patterns;

namespace SeqCli.PlainText.Extraction
{
static class ExtractionPatternInterpreter
{
public static NameValueExtractor MultilineMessageExtractor { get; } = new NameValueExtractor(new[]
{
new PatternElement(Matchers.MultiLineMessage, ReifiedProperties.Message)
});

public static NameValueExtractor CreateNameValueExtractor(ExtractionPattern pattern)
{
if (pattern == null) throw new ArgumentNullException(nameof(pattern));

var patternElements = new PatternElement[pattern.Elements.Count];
for (var i = pattern.Elements.Count - 1; i >= 0; --i)
{
var element = pattern.Elements[i];
switch (element)
{
case LiteralTextPatternExpression text:
patternElements[i] = new PatternElement(Matchers.LiteralText(text.Text));
break;
case CapturePatternExpression capture
when capture.Content is NonGreedyContentExpression ngc:
patternElements[i] = new PatternElement(
Matchers.NonGreedyContent(patternElements.Skip(i + 1).Take(ngc.Lookahead).ToArray()),
capture.Name);
break;
case CapturePatternExpression capture
when capture.Content is MatchTypeContentExpression mtc:
patternElements[i] = new PatternElement(
mtc.Type == null ? Matchers.Token : Matchers.GetByType(mtc.Type),
capture.Name);
break;
default:
throw new InvalidOperationException($"Element `{element}` not recognized.");
}
}

return new NameValueExtractor(patternElements);
}

// What we need to do here is:
// - for each parsed token
// - if it's literal text, map it an anonymous PatternElement with
// BuiltInPatterns.LiteralText()
// - otherwise, if it specifies no format, it's a named element with
// the BuiltInPatterns.Token parser
// - if it does specify a format, look up the parser based on the name, except
// - if the format is `$` it is BuiltInPatterns.SingleLineContent
// - if the format is `$$` it is BuiltInPatterns.MultiLineContent
// - if it's `*`, it's BuiltInPatterns.NonGreedyContent() passing the
// parser that follows it
}
}
15 changes: 15 additions & 0 deletions src/SeqCli/PlainText/Extraction/MatcherAttribute.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
using System;

namespace SeqCli.PlainText.Extraction
{
[AttributeUsage(AttributeTargets.Property)]
class MatcherAttribute : Attribute
{
public string Name { get; }

public MatcherAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
136 changes: 136 additions & 0 deletions src/SeqCli/PlainText/Extraction/Matchers.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SeqCli.PlainText.Parsers;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText.Extraction
{
// ReSharper disable UnusedMember.Global
static class Matchers
{
[Matcher("ident")]
public static TextParser<object> Identifier { get; } =
IdentifierEx.CStyle
.Select(span => (object) span);

[Matcher("nat")]
public static TextParser<object> Natural { get; } =
Numerics.NaturalUInt64
.Select(span => (object) span);

[Matcher("int")]
public static TextParser<object> Integer { get; } =
Numerics.IntegerInt64
.Select(span => (object) span);

[Matcher("dec")]
public static TextParser<object> Decimal { get; } =
NumericsEx.Decimal
.Select(span => (object) span);

[Matcher("alpha")]
public static TextParser<object> Alphabetical { get; } =
Span.WithAll(char.IsLetter)
.Select(span => (object) span);

[Matcher("alphanum")]
public static TextParser<object> Alphanumeric { get; } =
Span.WithAll(char.IsLetterOrDigit)
.Select(span => (object) span);

[Matcher("token")]
public static TextParser<object> Token { get; } =
SpanEx.NonWhiteSpace.Select(span => (object)span);

[Matcher("iso8601dt")]
// A date and time are required by this pattern, though not necessarily by the spec.
public static TextParser<object> Iso8601DateTime { get; } =
DateTimesEx.Iso8601DateTime
.Select(span => (object) span);

public static TextParser<object> SerilogFileTimestamp { get; } =
Span.Regex("\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d(\\.\\d+)? ([+-]\\d\\d:\\d\\d)?")
.Select(span => (object) DateTimeOffset.ParseExact(span.ToStringValue(), "yyyy-MM-dd HH:mm:ss.fff zzz", CultureInfo.InvariantCulture));

[Matcher("timestamp")]
public static TextParser<object> Timestamp { get; } =
Iso8601DateTime.Try().Or(SerilogFileTimestamp);

// Unclear whether we need to name this
public static TextParser<object> MultiLineMessage { get; } =
SpanEx.MatchedBy(
Character.Matching(ch => !char.IsWhiteSpace(ch), "non whitespace character")
.IgnoreThen(Character.AnyChar.Many()))
.Select(span => (object)span);

// Equivalent to :* at end-of-pattern
public static TextParser<object> MultiLineContent { get; } =
Span.WithAll(ch => true)
.Select(span => (object)span);

[Matcher("line")]
public static TextParser<object> SingleLineContent { get; } =
from content in Span.WithoutAny(ch => ch == '\r' || ch == '\n')
from _ in NewLine.OptionalOrDefault()
select (object) content;

[Matcher("n")]
public static TextParser<object> NewLine { get; } =
Span.EqualTo("\r\n").Or(Span.EqualTo("\n"))
.Select(span => (object)span);

[Matcher("t")]
public static TextParser<object> Tab { get; } =
Span.EqualTo("\t")
.Select(span => (object)span);

static readonly Dictionary<string, TextParser<object>> ByType = new Dictionary<string, TextParser<object>>(
from pi in typeof(Matchers).GetTypeInfo().DeclaredProperties
let attr = pi.GetCustomAttribute<MatcherAttribute>()
where attr != null
select KeyValuePair.Create(attr.Name, (TextParser<object>) pi.GetValue(null)));

public static TextParser<object> GetByType(string type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ByType[type];
}

public static TextParser<object> LiteralText(string literalText)
{
return Span.EqualTo(literalText).Select(span => (object) span);
}

public static TextParser<object> NonGreedyContent(params PatternElement[] following)
{
if (following.Length == 0)
return SpanEx.MatchedBy(Character.AnyChar.Many())
.Select(span => span.Length > 0 ? (object) span : null);

var rest = following[0].Parser;
for (var i = 1; i < following.Length; ++i)
{
rest = rest.IgnoreThen(following[i].Parser);
}

return i =>
{
var remainder = i;
var attempt = rest(remainder);
while (!attempt.HasValue || attempt.Remainder == remainder) // A zero-length match doesn't tell us anything
{
remainder = remainder.ConsumeChar().Remainder;
attempt = rest(remainder);
}

var span = i.Until(remainder);
return Result.Value(span.Length > 0 ? (object) span : null, i, remainder);
};
}
}
}
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class Pattern
class NameValueExtractor
{
readonly PatternElement[] _elements;

public Pattern(IEnumerable<PatternElement> elements)
public NameValueExtractor(IEnumerable<PatternElement> elements)
{
_elements = elements?.ToArray() ?? throw new ArgumentNullException(nameof(elements));
if (_elements.Length == 0)
throw new ArgumentException("A match pattern must contain at least one element.");
throw new ArgumentException("An extraction pattern must contain at least one element.");
}

public TextParser<object> FrameStart => _elements[0].Parser;
public TextParser<object> StartMarker => _elements[0].Parser;

public (IDictionary<string, object>, string) Match(string frame)
public (IDictionary<string, object>, string) ExtractValues(string plainText)
{
var input = new TextSpan(frame);
var input = new TextSpan(plainText);
var result = new Dictionary<string, object>();

var remainder = input;
Expand All@@ -42,8 +41,7 @@ public Pattern(IEnumerable<PatternElement> elements)

if (!element.IsIgnored)
{
if (match.Value != null || !element.IsOptional)
result.Add(element.Name, match.Value);
result.Add(element.Name, match.Value);
}
}

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
using System;
using Superpower;

namespace SeqCli.PlainText
namespace SeqCli.PlainText.Extraction
{
class PatternElement
{
public PatternElement(TextParser<object> parser, string name = null, bool isOptional = false)
public PatternElement(TextParser<object> parser, string name = null)
{
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
Name = name;
IsOptional = isOptional;
}

public TextParser<object> Parser { get; }
public string Name { get; }
public bool IsOptional { get; }
public bool IsIgnored => Name == null;
}
}
File renamed without changes.
Loading