Skip to content

Commit

Permalink
Merge pull request #23 from nblumhardt/headers-lang-level
Browse files Browse the repository at this point in the history
Target net5.0+, C#9/nullable, update dependencies
  • Loading branch information
nblumhardt authored Apr 26, 2022
2 parents 36a5ef5 + 704a638 commit 2f599dc
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 95 deletions.
2 changes: 1 addition & 1 deletion Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if($LASTEXITCODE -ne 0) { exit 1 }

$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL];
$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]
$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"]

echo "build: Version suffix is $suffix"

Expand Down
6 changes: 2 additions & 4 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
version: '{build}'
skip_tags: true
image: Visual Studio 2017
configuration: Release
install:
image: Visual Studio 2022
build_script:
- ps: ./Build.ps1
test: off
Expand All @@ -14,7 +12,7 @@ deploy:
secure: bkES4Ho0Cs/3Ws1PP7fYfSZop6K8VfqAAEFUR25eQRCJXVDPa+atx3alsaMhzVdo
skip_symbols: true
on:
branch: /^(master|dev)$/
branch: /^(main|dev)$/
- provider: GitHub
auth_token:
secure: hX+cZmW+9BCXy7vyH8myWsYdtQHyzzil9K5yvjJv7dK9XmyrGYYDj/DPzMqsXSjo
Expand Down
Binary file removed asset/seq-input-healthcheck-2.png
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,33 @@
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog.Events;
using Serilog.Filters.Expressions;
using Serilog.Expressions;
using Serilog.Formatting.Compact.Reader;
using Serilog.Formatting.Json;

namespace Seq.Input.HealthCheck
namespace Seq.Input.HealthCheck.Data
{
public class JsonDataExtractor
{
static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter("$type");
static readonly JsonValueFormatter ValueFormatter = new("$type");
static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None
});

readonly Func<JToken, JToken> _extract;
readonly Func<JToken?, JToken> _extract;

public JsonDataExtractor(string expression)
{
if (expression == "@Properties")
{
_extract = v => v;
_extract = v => v ?? JValue.CreateNull();
}
else
{
var expr = FilterLanguage.CreateFilter(expression);
var expr = SerilogExpression.Compile(expression, nameResolver: new SeqSyntaxNameResolver());
_extract = v => {
if (!(v is JObject obj))
if (v is not JObject obj)
throw new ArgumentException("Data value extraction requires a JSON object response.");
if (!obj.ContainsKey("@t"))
Expand All @@ -52,16 +51,15 @@ public JsonDataExtractor(string expression)
var le = LogEventReader.ReadFromJObject(obj);
var value = expr(le);
// `null` here means "undefined", but for most purposes this substitution is convenient.
if (value == null)
return JValue.CreateNull();
if (!(value is LogEventPropertyValue lepv))
return JToken.FromObject(value);
var sw = new StringWriter();
ValueFormatter.Format(lepv, sw);
ValueFormatter.Format(value, sw);
return Serializer.Deserialize<JToken>(
new JsonTextReader(new StringReader(sw.ToString())));
new JsonTextReader(new StringReader(sw.ToString())))!;
};
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/Seq.Input.HealthCheck/Data/SeqSyntaxNameResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Serilog.Events;
using Serilog.Expressions;

namespace Seq.Input.HealthCheck.Data
{
public class SeqSyntaxNameResolver: NameResolver
{
// ReSharper disable once UnusedMember.Global
// ReSharper disable once ReturnTypeCanBeNotNullable
public static LogEventPropertyValue? Has(LogEventPropertyValue? value)
{
return new ScalarValue(value != null);
}

public override bool TryResolveFunctionName(string name, [NotNullWhen(true)] out MethodInfo? implementation)
{
if ("Has".Equals(name, StringComparison.OrdinalIgnoreCase))
{
implementation = GetType().GetMethod("Has", BindingFlags.Static | BindingFlags.Public)!;
return true;
}

implementation = null;
return false;
}

public override bool TryResolveBuiltInPropertyName(string alias, [NotNullWhen(true)] out string? target)
{
target = alias switch
{
"Exception" => "x",
"Level" => "l",
"Message" => "m",
"MessageTemplate" => "mt",
"Properties" => "p",
"Timestamp" => "t",
_ => null
};

return target != null;
}
}
}
20 changes: 11 additions & 9 deletions src/Seq.Input.HealthCheck/HealthCheckInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
using System.IO;
using System.Net.Http;
using Seq.Apps;
using Seq.Input.HealthCheck.Data;
using Seq.Input.HealthCheck.Util;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global

namespace Seq.Input.HealthCheck
{
Expand All @@ -26,22 +30,22 @@ namespace Seq.Input.HealthCheck
public class HealthCheckInput : SeqApp, IPublishJson, IDisposable
{
readonly List<HealthCheckTask> _healthCheckTasks = new List<HealthCheckTask>();
HttpClient _httpClient;
HttpClient? _httpClient;

[SeqAppSetting(
DisplayName = "Target URLs",
HelpText = "The HTTP or HTTPS URL that the health check will periodically GET. Multiple URLs " +
"can be checked; enter one per line.",
InputType = SettingInputType.LongText)]
public string TargetUrl { get; set; }
public string TargetUrl { get; set; } = null!;

[SeqAppSetting(InputType = SettingInputType.Password, IsOptional = true, DisplayName = "Authentication Header",
HelpText = "An optional `Name: Value` header, stored as sensitive data, for authentication purposes.")]
public string AuthenticationHeader { get; set; }
public string? AuthenticationHeader { get; set; }

[SeqAppSetting(InputType = SettingInputType.LongText, IsOptional = true, DisplayName = "Other Headers",
HelpText = "Additional headers to send with the request, one per line in `Name: Value` format.")]
public string OtherHeaders { get; set; }
public string? OtherHeaders { get; set; }

[SeqAppSetting(
DisplayName = "Bypass HTTP caching",
Expand All @@ -64,16 +68,14 @@ public class HealthCheckInput : SeqApp, IPublishJson, IDisposable
"The expression will be evaluated against the response to produce a `Data` property" +
" on the resulting event. Use the special value `@Properties` to capture the whole " +
"response. The response must be UTF-8 `application/json` for this to be applied.")]
public string DataExtractionExpression { get; set; }



public string? DataExtractionExpression { get; set; }

public void Start(TextWriter inputWriter)
{
_httpClient = HttpHealthCheckClient.Create();
var reporter = new HealthCheckReporter(inputWriter);

JsonDataExtractor extractor = null;
JsonDataExtractor? extractor = null;
if (!string.IsNullOrWhiteSpace(DataExtractionExpression))
extractor = new JsonDataExtractor(DataExtractionExpression);

Expand Down
26 changes: 14 additions & 12 deletions src/Seq.Input.HealthCheck/HealthCheckResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable MemberCanBePrivate.Global

namespace Seq.Input.HealthCheck
{
Expand All @@ -25,14 +27,14 @@ class HealthCheckResult
public DateTime UtcTimestamp { get; }

[JsonProperty("@x", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Exception { get; }
public string? Exception { get; }

[JsonProperty("@mt")]
public string MessageTemplate { get; } =
"Health check {Method} {TargetUrl} {Outcome} with status code {StatusCode} in {Elapsed:0.000} ms";

[JsonProperty("@l", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Level { get; }
public string? Level { get; }

[JsonProperty("@r")]
public string[] Renderings => new[] {Elapsed.ToString("0.000", CultureInfo.InvariantCulture)};
Expand All @@ -43,18 +45,18 @@ class HealthCheckResult
public string Outcome { get; }
public double Elapsed { get; }
public int? StatusCode { get; }
public string ContentType { get; }
public string? ContentType { get; }
public long? ContentLength { get; }
public string ProbeId { get; }

[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string InitialContent { get; }
public string? InitialContent { get; }

[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public JToken Data { get; }
public JToken? Data { get; }

[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ProbedUrl { get; }
public string? ProbedUrl { get; }

public HealthCheckResult(
DateTime utcTimestamp,
Expand All @@ -63,15 +65,15 @@ public HealthCheckResult(
string targetUrl,
string outcome,
string probeId,
string level,
string? level,
double elapsed,
int? statusCode,
string contentType,
string? contentType,
long? contentLength,
string initialContent,
Exception exception,
JToken data,
string probedUrl)
string? initialContent,
Exception? exception,
JToken? data,
string? probedUrl)
{
if (utcTimestamp.Kind != DateTimeKind.Utc)
throw new ArgumentException("The timestamp must be UTC.", nameof(utcTimestamp));
Expand Down
37 changes: 16 additions & 21 deletions src/Seq.Input.HealthCheck/HttpHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Seq.Input.HealthCheck.Data;
using Seq.Input.HealthCheck.Util;

namespace Seq.Input.HealthCheck
Expand All @@ -31,18 +32,18 @@ class HttpHealthCheck
readonly string _title;
readonly string _targetUrl;
readonly List<(string, string)> _headers;
readonly JsonDataExtractor _extractor;
readonly JsonDataExtractor? _extractor;
readonly bool _bypassHttpCaching;
readonly HttpClient _httpClient;
readonly byte[] _buffer = new byte[2048];

public const string ProbeIdParameterName = "__probe";

static readonly UTF8Encoding ForgivingEncoding = new UTF8Encoding(false, false);
static readonly UTF8Encoding ForgivingEncoding = new(false, false);
const int InitialContentChars = 16;
const string OutcomeSucceeded = "succeeded", OutcomeFailed = "failed";

public HttpHealthCheck(HttpClient httpClient, string title, string targetUrl, List<(string, string)> headers, JsonDataExtractor extractor, bool bypassHttpCaching)
public HttpHealthCheck(HttpClient httpClient, string title, string targetUrl, List<(string, string)> headers, JsonDataExtractor? extractor, bool bypassHttpCaching)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_title = title ?? throw new ArgumentNullException(nameof(title));
Expand All @@ -56,12 +57,12 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
{
string outcome;

Exception exception = null;
Exception? exception = null;
int? statusCode = null;
string contentType = null;
string? contentType = null;
long? contentLength = null;
string initialContent = null;
JToken data = null;
string? initialContent = null;
JToken? data = null;

var probeId = Nonce.Generate(12);
var probedUrl = _bypassHttpCaching ?
Expand All @@ -75,17 +76,11 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
{
var request = new HttpRequestMessage(HttpMethod.Get, probedUrl);
request.Headers.Add("X-Correlation-ID", probeId);
if (_headers != null)

foreach (var (name, value) in _headers)
{
foreach (var (name, value) in _headers)
{
// the api does not allow overwriting, therefore removal has to happen first.
if (request.Headers.Contains(name))
{
request.Headers.Remove(name);
}
request.Headers.Add(name, value);
}
// This will throw if a header is duplicated (better for the user to detect this configuration problem).
request.Headers.Add(name, value);
}

if (_bypassHttpCaching)
Expand All @@ -97,7 +92,7 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
contentType = response.Content.Headers.ContentType?.ToString();
contentLength = response.Content.Headers.ContentLength;

var content = await response.Content.ReadAsStreamAsync();
var content = await response.Content.ReadAsStreamAsync(cancel);
(initialContent, data) = await DownloadContent(content, contentType, contentLength);

outcome = response.IsSuccessStatusCode ? OutcomeSucceeded : OutcomeFailed;
Expand Down Expand Up @@ -133,18 +128,18 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
}

// Either initial content, or extracted data
async Task<(string initialContent, JToken data)> DownloadContent(Stream body, string contentType, long? contentLength)
async Task<(string? initialContent, JToken? data)> DownloadContent(Stream body, string? contentType, long? contentLength)
{
if (_extractor == null ||
contentLength == 0 ||
contentType != "application/json; charset=utf-8" && contentType != "application/json")
{
var read = await body.ReadAsync(_buffer, 0, _buffer.Length);
var read = await body.ReadAsync(_buffer);
var initial = ForgivingEncoding.GetString(_buffer, 0, Math.Min(read, InitialContentChars));

// Drain the response to avoid dropped connection errors on the server.
while (read > 0)
read = await body.ReadAsync(_buffer, 0, _buffer.Length);
read = await body.ReadAsync(_buffer);

return (initial, null);
}
Expand Down
3 changes: 0 additions & 3 deletions src/Seq.Input.HealthCheck/Properties/AssemblyInfo.cs

This file was deleted.

Loading

0 comments on commit 2f599dc

Please sign in to comment.