Skip to content

Commit babfedd

Browse files
authored
Merge pull request #32 from datalust/dev
2.1.0 Release
2 parents 254795d + ecbcbb7 commit babfedd

21 files changed

+663
-589
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Periodically GET an HTTP resource and write response metrics to Seq. These can t
66

77
### Getting started
88

9-
1. The app requires Seq 5.1 or newer
9+
1. The app requires Seq 2020.4 or newer
1010
2. Navigate to _Settings_ > _Apps_ and select _Install from NuGet_
1111
3. Install the app with package id _Seq.Input.HealthCheck_
1212
4. Back on the _Apps_ screen, choose _Add Instance_
@@ -16,4 +16,3 @@ Periodically GET an HTTP resource and write response metrics to Seq. These can t
1616
- if the URL is an HTTPS URL, the Seq server must trust the SSL certificate used by the server
1717
- the response will be fully downloaded on every check, so ideally the resource won't be more than a few kB
1818
7. Enter a probing interval in seconds; each event is stored internally in Seq, so be aware that shorter intervals will consume more space
19-

appveyor.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ artifacts:
99
deploy:
1010
- provider: NuGet
1111
api_key:
12-
secure: bkES4Ho0Cs/3Ws1PP7fYfSZop6K8VfqAAEFUR25eQRCJXVDPa+atx3alsaMhzVdo
12+
secure: 8a7m6Xbn2SER7teqWvpYqff2MMeeFKVgU1UertfVosvlz3vhmrLZKOB9mtEmcnIL
1313
skip_symbols: true
1414
on:
1515
branch: /^(main|dev)$/
@@ -19,4 +19,4 @@ deploy:
1919
artifact: /Seq.Input.HealthCheck.*\.nupkg/
2020
tag: v$(appveyor_build_version)
2121
on:
22-
branch: master
22+
branch: main

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

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,59 +16,58 @@
1616
using System.IO;
1717
using Newtonsoft.Json;
1818
using Newtonsoft.Json.Linq;
19-
using Serilog.Expressions;
19+
using Seq.Syntax.Expressions;
2020
using Serilog.Formatting.Compact.Reader;
2121
using Serilog.Formatting.Json;
2222

23-
namespace Seq.Input.HealthCheck.Data
23+
namespace Seq.Input.HealthCheck.Data;
24+
25+
public class JsonDataExtractor
2426
{
25-
public class JsonDataExtractor
27+
static readonly JsonValueFormatter ValueFormatter = new("$type");
28+
static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings
2629
{
27-
static readonly JsonValueFormatter ValueFormatter = new("$type");
28-
static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings
29-
{
30-
DateParseHandling = DateParseHandling.None
31-
});
30+
DateParseHandling = DateParseHandling.None
31+
});
3232

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

35-
public JsonDataExtractor(string expression)
35+
public JsonDataExtractor(string expression)
36+
{
37+
if (expression == "@Properties")
38+
{
39+
_extract = v => v ?? JValue.CreateNull();
40+
}
41+
else
3642
{
37-
if (expression == "@Properties")
38-
{
39-
_extract = v => v ?? JValue.CreateNull();
40-
}
41-
else
42-
{
43-
var expr = SerilogExpression.Compile(expression, nameResolver: new SeqSyntaxNameResolver());
44-
_extract = v => {
45-
if (v is not JObject obj)
46-
throw new ArgumentException("Data value extraction requires a JSON object response.");
43+
var expr = SerilogExpression.Compile(expression, nameResolver: new SeqSyntaxNameResolver());
44+
_extract = v => {
45+
if (v is not JObject obj)
46+
throw new ArgumentException("Data value extraction requires a JSON object response.");
4747

48-
if (!obj.ContainsKey("@t"))
49-
obj["@t"] = DateTime.UtcNow.ToString("o");
48+
if (!obj.ContainsKey("@t"))
49+
obj["@t"] = DateTime.UtcNow.ToString("o");
5050

51-
var le = LogEventReader.ReadFromJObject(obj);
51+
var le = LogEventReader.ReadFromJObject(obj);
5252

53-
var value = expr(le);
53+
var value = expr(le);
5454

55-
// `null` here means "undefined", but for most purposes this substitution is convenient.
56-
if (value == null)
57-
return JValue.CreateNull();
55+
// `null` here means "undefined", but for most purposes this substitution is convenient.
56+
if (value == null)
57+
return JValue.CreateNull();
5858

59-
var sw = new StringWriter();
60-
ValueFormatter.Format(value, sw);
61-
return Serializer.Deserialize<JToken>(
62-
new JsonTextReader(new StringReader(sw.ToString())))!;
63-
};
64-
}
59+
var sw = new StringWriter();
60+
ValueFormatter.Format(value, sw);
61+
return Serializer.Deserialize<JToken>(
62+
new JsonTextReader(new StringReader(sw.ToString())))!;
63+
};
6564
}
65+
}
6666

67-
public JToken ExtractData(TextReader json)
68-
{
69-
if (json == null) throw new ArgumentNullException(nameof(json));
70-
var document = Serializer.Deserialize<JToken>(new JsonTextReader(json));
71-
return _extract(document);
72-
}
67+
public JToken ExtractData(TextReader json)
68+
{
69+
if (json == null) throw new ArgumentNullException(nameof(json));
70+
var document = Serializer.Deserialize<JToken>(new JsonTextReader(json));
71+
return _extract(document);
7372
}
74-
}
73+
}

src/Seq.Input.HealthCheck/Data/SeqSyntaxNameResolver.cs

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,28 @@
22
using System.Diagnostics.CodeAnalysis;
33
using System.Reflection;
44
using Serilog.Events;
5-
using Serilog.Expressions;
5+
using Seq.Syntax.Expressions;
66

7-
namespace Seq.Input.HealthCheck.Data
7+
namespace Seq.Input.HealthCheck.Data;
8+
9+
public class SeqSyntaxNameResolver: NameResolver
810
{
9-
public class SeqSyntaxNameResolver: NameResolver
11+
// ReSharper disable once UnusedMember.Global
12+
// ReSharper disable once ReturnTypeCanBeNotNullable
13+
public static LogEventPropertyValue? Has(LogEventPropertyValue? value)
1014
{
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-
}
15+
return new ScalarValue(value != null);
16+
}
1717

18-
public override bool TryResolveFunctionName(string name, [NotNullWhen(true)] out MethodInfo? implementation)
18+
public override bool TryResolveFunctionName(string name, [NotNullWhen(true)] out MethodInfo? implementation)
19+
{
20+
if ("Has".Equals(name, StringComparison.OrdinalIgnoreCase))
1921
{
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;
22+
implementation = GetType().GetMethod("Has", BindingFlags.Static | BindingFlags.Public)!;
23+
return true;
2824
}
2925

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-
}
26+
implementation = null;
27+
return false;
4528
}
4629
}

src/Seq.Input.HealthCheck/HealthCheckInput.cs

Lines changed: 90 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -19,97 +19,105 @@
1919
using Seq.Apps;
2020
using Seq.Input.HealthCheck.Data;
2121
using Seq.Input.HealthCheck.Util;
22+
// ReSharper disable UnusedType.Global
2223
// ReSharper disable MemberCanBePrivate.Global
2324
// ReSharper disable UnusedAutoPropertyAccessor.Global
2425
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
2526

26-
namespace Seq.Input.HealthCheck
27+
namespace Seq.Input.HealthCheck;
28+
29+
[SeqApp("Health Check Input",
30+
Description = "Periodically GET an HTTP resource and publish response metrics to Seq.")]
31+
public class HealthCheckInput : SeqApp, IPublishJson, IDisposable
2732
{
28-
[SeqApp("Health Check Input",
29-
Description = "Periodically GET an HTTP resource and publish response metrics to Seq.")]
30-
public class HealthCheckInput : SeqApp, IPublishJson, IDisposable
33+
readonly List<HealthCheckTask> _healthCheckTasks = [];
34+
HttpClient? _httpClient;
35+
36+
[SeqAppSetting(
37+
DisplayName = "Target URLs",
38+
HelpText = "The HTTP or HTTPS URL that the health check will periodically GET. Multiple URLs " +
39+
"can be checked; enter one per line.",
40+
InputType = SettingInputType.LongText)]
41+
public string TargetUrl { get; set; } = null!;
42+
43+
[SeqAppSetting(
44+
DisplayName = "Follow Redirects",
45+
IsOptional = true,
46+
HelpText = "If selected, the HTTP request will follow redirects. By default, health checks that trigger redirects will fail.",
47+
InputType = SettingInputType.Checkbox)]
48+
public bool FollowRedirects { get; set; } = false;
49+
50+
[SeqAppSetting(InputType = SettingInputType.Password, IsOptional = true, DisplayName = "Authentication Header",
51+
HelpText = "An optional `Name: Value` header, stored as sensitive data, for authentication purposes.")]
52+
public string? AuthenticationHeader { get; set; }
53+
54+
[SeqAppSetting(InputType = SettingInputType.LongText, IsOptional = true, DisplayName = "Other Headers",
55+
HelpText = "Additional headers to send with the request, one per line in `Name: Value` format.")]
56+
public string? OtherHeaders { get; set; }
57+
58+
[SeqAppSetting(
59+
DisplayName = "Bypass HTTP caching",
60+
IsOptional = true,
61+
HelpText = "If selected, the unique probe id will be appended to the target URL query string as " +
62+
"`" + HttpHealthCheck.ProbeIdParameterName + "`, in order to disable any " +
63+
"intermediary HTTP caching. The `Cache-Control: no-store` header will also be sent.")]
64+
public bool BypassHttpCaching { get; set; }
65+
66+
[SeqAppSetting(
67+
DisplayName = "Interval (seconds)",
68+
IsOptional = true,
69+
HelpText = "The time between checks; the default is 60.")]
70+
public int IntervalSeconds { get; set; } = 60;
71+
72+
[SeqAppSetting(
73+
DisplayName = "Data extraction expression",
74+
IsOptional = true,
75+
HelpText = "A Seq query language expression used to extract information from JSON responses. " +
76+
"The expression will be evaluated against the response to produce a `Data` property" +
77+
" on the resulting event. Use the special value `@Properties` to capture the whole " +
78+
"response. The response must be UTF-8 `application/json` for this to be applied.")]
79+
public string? DataExtractionExpression { get; set; }
80+
81+
public void Start(TextWriter inputWriter)
3182
{
32-
readonly List<HealthCheckTask> _healthCheckTasks = new List<HealthCheckTask>();
33-
HttpClient? _httpClient;
34-
35-
[SeqAppSetting(
36-
DisplayName = "Target URLs",
37-
HelpText = "The HTTP or HTTPS URL that the health check will periodically GET. Multiple URLs " +
38-
"can be checked; enter one per line.",
39-
InputType = SettingInputType.LongText)]
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; }
57-
58-
[SeqAppSetting(
59-
DisplayName = "Interval (seconds)",
60-
IsOptional = true,
61-
HelpText = "The time between checks; the default is 60.")]
62-
public int IntervalSeconds { get; set; } = 60;
63-
64-
[SeqAppSetting(
65-
DisplayName = "Data extraction expression",
66-
IsOptional = true,
67-
HelpText = "A Seq query language expression used to extract information from JSON responses. " +
68-
"The expression will be evaluated against the response to produce a `Data` property" +
69-
" on the resulting event. Use the special value `@Properties` to capture the whole " +
70-
"response. The response must be UTF-8 `application/json` for this to be applied.")]
71-
public string? DataExtractionExpression { get; set; }
72-
73-
public void Start(TextWriter inputWriter)
74-
{
75-
_httpClient = HttpHealthCheckClient.Create();
76-
var reporter = new HealthCheckReporter(inputWriter);
77-
78-
JsonDataExtractor? extractor = null;
79-
if (!string.IsNullOrWhiteSpace(DataExtractionExpression))
80-
extractor = new JsonDataExtractor(DataExtractionExpression);
81-
82-
var targetUrls = TargetUrl.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
83-
foreach (var targetUrl in targetUrls)
84-
{
85-
var healthCheck = new HttpHealthCheck(
86-
_httpClient,
87-
App.Title,
88-
targetUrl,
89-
HeaderSettingFormat.FromSettings(AuthenticationHeader, OtherHeaders),
90-
extractor,
91-
BypassHttpCaching);
92-
93-
_healthCheckTasks.Add(new HealthCheckTask(
94-
healthCheck,
95-
TimeSpan.FromSeconds(IntervalSeconds),
96-
reporter,
97-
Log));
98-
}
99-
}
83+
_httpClient = HttpHealthCheckClient.Create();
84+
var reporter = new HealthCheckReporter(inputWriter);
10085

101-
public void Stop()
102-
{
103-
foreach (var task in _healthCheckTasks)
104-
task.Stop();
105-
}
86+
JsonDataExtractor? extractor = null;
87+
if (!string.IsNullOrWhiteSpace(DataExtractionExpression))
88+
extractor = new JsonDataExtractor(DataExtractionExpression);
10689

107-
public void Dispose()
90+
var targetUrls = TargetUrl.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
91+
foreach (var targetUrl in targetUrls)
10892
{
109-
foreach (var task in _healthCheckTasks)
110-
task.Dispose();
93+
var healthCheck = new HttpHealthCheck(
94+
_httpClient,
95+
App.Title,
96+
targetUrl,
97+
HeaderSettingFormat.FromSettings(AuthenticationHeader, OtherHeaders),
98+
extractor,
99+
BypassHttpCaching,
100+
FollowRedirects);
111101

112-
_httpClient?.Dispose();
102+
_healthCheckTasks.Add(new HealthCheckTask(
103+
healthCheck,
104+
TimeSpan.FromSeconds(IntervalSeconds),
105+
reporter,
106+
Log));
113107
}
114108
}
115-
}
109+
110+
public void Stop()
111+
{
112+
foreach (var task in _healthCheckTasks)
113+
task.Stop();
114+
}
115+
116+
public void Dispose()
117+
{
118+
foreach (var task in _healthCheckTasks)
119+
task.Dispose();
120+
121+
_httpClient?.Dispose();
122+
}
123+
}

0 commit comments

Comments
 (0)