Skip to content

Commit 254795d

Browse files
authored
Merge pull request #24 from datalust/dev
2.0.0 Release
2 parents 1d55b33 + 2f599dc commit 254795d

16 files changed

+222
-80
lines changed

Build.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ if($LASTEXITCODE -ne 0) { exit 1 }
1414

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

1919
echo "build: Version suffix is $suffix"
2020

appveyor.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
version: '{build}'
22
skip_tags: true
3-
image: Visual Studio 2017
4-
configuration: Release
5-
install:
3+
image: Visual Studio 2022
64
build_script:
75
- ps: ./Build.ps1
86
test: off
@@ -11,10 +9,10 @@ artifacts:
119
deploy:
1210
- provider: NuGet
1311
api_key:
14-
secure: ABsZ0uLbAvoUFXnkUJ/DZwQwGQ1EIKzvSOm3aFSUZ0kY4lTBIQhI38KxrNFahQRB
12+
secure: bkES4Ho0Cs/3Ws1PP7fYfSZop6K8VfqAAEFUR25eQRCJXVDPa+atx3alsaMhzVdo
1513
skip_symbols: true
1614
on:
17-
branch: /^(master|dev)$/
15+
branch: /^(main|dev)$/
1816
- provider: GitHub
1917
auth_token:
2018
secure: hX+cZmW+9BCXy7vyH8myWsYdtQHyzzil9K5yvjJv7dK9XmyrGYYDj/DPzMqsXSjo

asset/seq-input-healthcheck-2.png

-7.69 KB
Binary file not shown.

src/Seq.Input.HealthCheck/JsonDataExtractor.cs renamed to src/Seq.Input.HealthCheck/Data/JsonDataExtractor.cs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,33 @@
1616
using System.IO;
1717
using Newtonsoft.Json;
1818
using Newtonsoft.Json.Linq;
19-
using Serilog.Events;
20-
using Serilog.Filters.Expressions;
19+
using Serilog.Expressions;
2120
using Serilog.Formatting.Compact.Reader;
2221
using Serilog.Formatting.Json;
2322

24-
namespace Seq.Input.HealthCheck
23+
namespace Seq.Input.HealthCheck.Data
2524
{
2625
public class JsonDataExtractor
2726
{
28-
static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter("$type");
27+
static readonly JsonValueFormatter ValueFormatter = new("$type");
2928
static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings
3029
{
3130
DateParseHandling = DateParseHandling.None
3231
});
3332

34-
readonly Func<JToken, JToken> _extract;
33+
readonly Func<JToken?, JToken> _extract;
3534

3635
public JsonDataExtractor(string expression)
3736
{
3837
if (expression == "@Properties")
3938
{
40-
_extract = v => v;
39+
_extract = v => v ?? JValue.CreateNull();
4140
}
4241
else
4342
{
44-
var expr = FilterLanguage.CreateFilter(expression);
43+
var expr = SerilogExpression.Compile(expression, nameResolver: new SeqSyntaxNameResolver());
4544
_extract = v => {
46-
if (!(v is JObject obj))
45+
if (v is not JObject obj)
4746
throw new ArgumentException("Data value extraction requires a JSON object response.");
4847

4948
if (!obj.ContainsKey("@t"))
@@ -52,16 +51,15 @@ public JsonDataExtractor(string expression)
5251
var le = LogEventReader.ReadFromJObject(obj);
5352

5453
var value = expr(le);
54+
55+
// `null` here means "undefined", but for most purposes this substitution is convenient.
5556
if (value == null)
5657
return JValue.CreateNull();
5758

58-
if (!(value is LogEventPropertyValue lepv))
59-
return JToken.FromObject(value);
60-
6159
var sw = new StringWriter();
62-
ValueFormatter.Format(lepv, sw);
60+
ValueFormatter.Format(value, sw);
6361
return Serializer.Deserialize<JToken>(
64-
new JsonTextReader(new StringReader(sw.ToString())));
62+
new JsonTextReader(new StringReader(sw.ToString())))!;
6563
};
6664
}
6765
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Reflection;
4+
using Serilog.Events;
5+
using Serilog.Expressions;
6+
7+
namespace Seq.Input.HealthCheck.Data
8+
{
9+
public class SeqSyntaxNameResolver: NameResolver
10+
{
11+
// ReSharper disable once UnusedMember.Global
12+
// ReSharper disable once ReturnTypeCanBeNotNullable
13+
public static LogEventPropertyValue? Has(LogEventPropertyValue? value)
14+
{
15+
return new ScalarValue(value != null);
16+
}
17+
18+
public override bool TryResolveFunctionName(string name, [NotNullWhen(true)] out MethodInfo? implementation)
19+
{
20+
if ("Has".Equals(name, StringComparison.OrdinalIgnoreCase))
21+
{
22+
implementation = GetType().GetMethod("Has", BindingFlags.Static | BindingFlags.Public)!;
23+
return true;
24+
}
25+
26+
implementation = null;
27+
return false;
28+
}
29+
30+
public override bool TryResolveBuiltInPropertyName(string alias, [NotNullWhen(true)] out string? target)
31+
{
32+
target = alias switch
33+
{
34+
"Exception" => "x",
35+
"Level" => "l",
36+
"Message" => "m",
37+
"MessageTemplate" => "mt",
38+
"Properties" => "p",
39+
"Timestamp" => "t",
40+
_ => null
41+
};
42+
43+
return target != null;
44+
}
45+
}
46+
}

src/Seq.Input.HealthCheck/HealthCheckInput.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
using System.IO;
1818
using System.Net.Http;
1919
using Seq.Apps;
20+
using Seq.Input.HealthCheck.Data;
21+
using Seq.Input.HealthCheck.Util;
22+
// ReSharper disable MemberCanBePrivate.Global
23+
// ReSharper disable UnusedAutoPropertyAccessor.Global
24+
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
2025

2126
namespace Seq.Input.HealthCheck
2227
{
@@ -25,14 +30,30 @@ namespace Seq.Input.HealthCheck
2530
public class HealthCheckInput : SeqApp, IPublishJson, IDisposable
2631
{
2732
readonly List<HealthCheckTask> _healthCheckTasks = new List<HealthCheckTask>();
28-
HttpClient _httpClient;
33+
HttpClient? _httpClient;
2934

3035
[SeqAppSetting(
3136
DisplayName = "Target URLs",
3237
HelpText = "The HTTP or HTTPS URL that the health check will periodically GET. Multiple URLs " +
3338
"can be checked; enter one per line.",
3439
InputType = SettingInputType.LongText)]
35-
public string TargetUrl { get; set; }
40+
public string TargetUrl { get; set; } = null!;
41+
42+
[SeqAppSetting(InputType = SettingInputType.Password, IsOptional = true, DisplayName = "Authentication Header",
43+
HelpText = "An optional `Name: Value` header, stored as sensitive data, for authentication purposes.")]
44+
public string? AuthenticationHeader { get; set; }
45+
46+
[SeqAppSetting(InputType = SettingInputType.LongText, IsOptional = true, DisplayName = "Other Headers",
47+
HelpText = "Additional headers to send with the request, one per line in `Name: Value` format.")]
48+
public string? OtherHeaders { get; set; }
49+
50+
[SeqAppSetting(
51+
DisplayName = "Bypass HTTP caching",
52+
IsOptional = true,
53+
HelpText = "If selected, the unique probe id will be appended to the target URL query string as " +
54+
"`" + HttpHealthCheck.ProbeIdParameterName + "`, in order to disable any " +
55+
"intermediary HTTP caching. The `Cache-Control: no-store` header will also be sent.")]
56+
public bool BypassHttpCaching { get; set; }
3657

3758
[SeqAppSetting(
3859
DisplayName = "Interval (seconds)",
@@ -47,22 +68,14 @@ public class HealthCheckInput : SeqApp, IPublishJson, IDisposable
4768
"The expression will be evaluated against the response to produce a `Data` property" +
4869
" on the resulting event. Use the special value `@Properties` to capture the whole " +
4970
"response. The response must be UTF-8 `application/json` for this to be applied.")]
50-
public string DataExtractionExpression { get; set; }
51-
52-
[SeqAppSetting(
53-
DisplayName = "Bypass HTTP caching",
54-
IsOptional = true,
55-
HelpText = "If selected, the unique probe id will be appended to the target URL query string as " +
56-
"`" + HttpHealthCheck.ProbeIdParameterName + "`, in order to disable any " +
57-
"intermediary HTTP caching. The `Cache-Control: no-store` header will also be sent.")]
58-
public bool BypassHttpCaching { get; set; }
59-
71+
public string? DataExtractionExpression { get; set; }
72+
6073
public void Start(TextWriter inputWriter)
6174
{
6275
_httpClient = HttpHealthCheckClient.Create();
6376
var reporter = new HealthCheckReporter(inputWriter);
6477

65-
JsonDataExtractor extractor = null;
78+
JsonDataExtractor? extractor = null;
6679
if (!string.IsNullOrWhiteSpace(DataExtractionExpression))
6780
extractor = new JsonDataExtractor(DataExtractionExpression);
6881

@@ -73,6 +86,7 @@ public void Start(TextWriter inputWriter)
7386
_httpClient,
7487
App.Title,
7588
targetUrl,
89+
HeaderSettingFormat.FromSettings(AuthenticationHeader, OtherHeaders),
7690
extractor,
7791
BypassHttpCaching);
7892

src/Seq.Input.HealthCheck/HealthCheckResult.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
using System.Globalization;
1717
using Newtonsoft.Json;
1818
using Newtonsoft.Json.Linq;
19+
// ReSharper disable UnusedAutoPropertyAccessor.Global
20+
// ReSharper disable MemberCanBePrivate.Global
1921

2022
namespace Seq.Input.HealthCheck
2123
{
@@ -25,14 +27,14 @@ class HealthCheckResult
2527
public DateTime UtcTimestamp { get; }
2628

2729
[JsonProperty("@x", DefaultValueHandling = DefaultValueHandling.Ignore)]
28-
public string Exception { get; }
30+
public string? Exception { get; }
2931

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

3436
[JsonProperty("@l", DefaultValueHandling = DefaultValueHandling.Ignore)]
35-
public string Level { get; }
37+
public string? Level { get; }
3638

3739
[JsonProperty("@r")]
3840
public string[] Renderings => new[] {Elapsed.ToString("0.000", CultureInfo.InvariantCulture)};
@@ -43,18 +45,18 @@ class HealthCheckResult
4345
public string Outcome { get; }
4446
public double Elapsed { get; }
4547
public int? StatusCode { get; }
46-
public string ContentType { get; }
48+
public string? ContentType { get; }
4749
public long? ContentLength { get; }
4850
public string ProbeId { get; }
4951

5052
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
51-
public string InitialContent { get; }
53+
public string? InitialContent { get; }
5254

5355
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
54-
public JToken Data { get; }
56+
public JToken? Data { get; }
5557

5658
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
57-
public string ProbedUrl { get; }
59+
public string? ProbedUrl { get; }
5860

5961
public HealthCheckResult(
6062
DateTime utcTimestamp,
@@ -63,15 +65,15 @@ public HealthCheckResult(
6365
string targetUrl,
6466
string outcome,
6567
string probeId,
66-
string level,
68+
string? level,
6769
double elapsed,
6870
int? statusCode,
69-
string contentType,
71+
string? contentType,
7072
long? contentLength,
71-
string initialContent,
72-
Exception exception,
73-
JToken data,
74-
string probedUrl)
73+
string? initialContent,
74+
Exception? exception,
75+
JToken? data,
76+
string? probedUrl)
7577
{
7678
if (utcTimestamp.Kind != DateTimeKind.Utc)
7779
throw new ArgumentException("The timestamp must be UTC.", nameof(utcTimestamp));

src/Seq.Input.HealthCheck/HttpHealthCheck.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Collections.Generic;
1617
using System.Diagnostics;
1718
using System.IO;
1819
using System.Net.Http;
@@ -21,6 +22,7 @@
2122
using System.Threading;
2223
using System.Threading.Tasks;
2324
using Newtonsoft.Json.Linq;
25+
using Seq.Input.HealthCheck.Data;
2426
using Seq.Input.HealthCheck.Util;
2527

2628
namespace Seq.Input.HealthCheck
@@ -29,36 +31,38 @@ class HttpHealthCheck
2931
{
3032
readonly string _title;
3133
readonly string _targetUrl;
32-
readonly JsonDataExtractor _extractor;
34+
readonly List<(string, string)> _headers;
35+
readonly JsonDataExtractor? _extractor;
3336
readonly bool _bypassHttpCaching;
3437
readonly HttpClient _httpClient;
3538
readonly byte[] _buffer = new byte[2048];
3639

3740
public const string ProbeIdParameterName = "__probe";
3841

39-
static readonly UTF8Encoding ForgivingEncoding = new UTF8Encoding(false, false);
42+
static readonly UTF8Encoding ForgivingEncoding = new(false, false);
4043
const int InitialContentChars = 16;
4144
const string OutcomeSucceeded = "succeeded", OutcomeFailed = "failed";
4245

43-
public HttpHealthCheck(HttpClient httpClient, string title, string targetUrl, JsonDataExtractor extractor, bool bypassHttpCaching)
46+
public HttpHealthCheck(HttpClient httpClient, string title, string targetUrl, List<(string, string)> headers, JsonDataExtractor? extractor, bool bypassHttpCaching)
4447
{
4548
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
4649
_title = title ?? throw new ArgumentNullException(nameof(title));
4750
_targetUrl = targetUrl ?? throw new ArgumentNullException(nameof(targetUrl));
51+
_headers = headers;
4852
_extractor = extractor;
4953
_bypassHttpCaching = bypassHttpCaching;
5054
}
51-
55+
5256
public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
5357
{
5458
string outcome;
5559

56-
Exception exception = null;
60+
Exception? exception = null;
5761
int? statusCode = null;
58-
string contentType = null;
62+
string? contentType = null;
5963
long? contentLength = null;
60-
string initialContent = null;
61-
JToken data = null;
64+
string? initialContent = null;
65+
JToken? data = null;
6266

6367
var probeId = Nonce.Generate(12);
6468
var probedUrl = _bypassHttpCaching ?
@@ -73,6 +77,12 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
7377
var request = new HttpRequestMessage(HttpMethod.Get, probedUrl);
7478
request.Headers.Add("X-Correlation-ID", probeId);
7579

80+
foreach (var (name, value) in _headers)
81+
{
82+
// This will throw if a header is duplicated (better for the user to detect this configuration problem).
83+
request.Headers.Add(name, value);
84+
}
85+
7686
if (_bypassHttpCaching)
7787
request.Headers.CacheControl = new CacheControlHeaderValue { NoStore = true };
7888

@@ -82,7 +92,7 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
8292
contentType = response.Content.Headers.ContentType?.ToString();
8393
contentLength = response.Content.Headers.ContentLength;
8494

85-
var content = await response.Content.ReadAsStreamAsync();
95+
var content = await response.Content.ReadAsStreamAsync(cancel);
8696
(initialContent, data) = await DownloadContent(content, contentType, contentLength);
8797

8898
outcome = response.IsSuccessStatusCode ? OutcomeSucceeded : OutcomeFailed;
@@ -118,18 +128,18 @@ public async Task<HealthCheckResult> CheckNow(CancellationToken cancel)
118128
}
119129

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

130140
// Drain the response to avoid dropped connection errors on the server.
131141
while (read > 0)
132-
read = await body.ReadAsync(_buffer, 0, _buffer.Length);
142+
read = await body.ReadAsync(_buffer);
133143

134144
return (initial, null);
135145
}

0 commit comments

Comments
 (0)