Skip to content
Closed
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
128 changes: 128 additions & 0 deletions Cronitor.Tests/AssertionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using Cronitor.Constants;
using Cronitor.Serialization;
using System.Collections.Generic;
using Xunit;

namespace Cronitor.Tests
{
public class AssertionTests
{
[Fact]
public void MetricDurationLessThan()
{
var result = Assertion.Metric.Duration.LessThan("30s");

Assert.Equal("metric.duration < 30s", result.Value);
}

[Fact]
public void MetricErrorCountLessThan()
{
var result = Assertion.Metric.ErrorCount.LessThan(5);

Assert.Equal("metric.error_count < 5", result.Value);
}

[Fact]
public void MetricCountGreaterThan()
{
var result = Assertion.Metric.Count.GreaterThan(0);

Assert.Equal("metric.count > 0", result.Value);
}

[Fact]
public void ResponseCodeEquals()
{
var result = Assertion.Response.Code.Equals(200);

Assert.Equal("response.code = 200", result.Value);
}

[Fact]
public void ResponseTimeLessThan()
{
var result = Assertion.Response.Time.LessThan("2s");

Assert.Equal("response.time < 2s", result.Value);
}

[Fact]
public void ResponseBodyContains()
{
var result = Assertion.Response.Body.Contains("healthy");

Assert.Equal("response.body contains healthy", result.Value);
}

[Fact]
public void ResponseJsonGreaterThan()
{
var result = Assertion.Response.Json("user.count").GreaterThan(10);

Assert.Equal("response.json user.count > 10", result.Value);
}

[Fact]
public void ResponseHeaderEquals()
{
var result = Assertion.Response.Header("X-Version").Equals("1.2.3");

Assert.Equal("response.header X-Version = 1.2.3", result.Value);
}

[Fact]
public void ToStringReturnsValue()
{
var rule = Assertion.Metric.Duration.LessThan("30s");

Assert.Equal("metric.duration < 30s", rule.ToString());
}

[Fact]
public void ImplicitStringConversion()
{
string result = Assertion.Response.Code.Equals(200);

Assert.Equal("response.code = 200", result);
}

[Fact]
public void ImplicitAssertionRuleFromString()
{
AssertionRule rule = "metric.duration < 30s";

Assert.Equal("metric.duration < 30s", rule.Value);
}

[Fact]
public void SerializesAsJsonString()
{
var rules = new List<AssertionRule>
{
Assertion.Metric.Duration.LessThan("15min")
};

var json = Serializer.Serialize(new { assertions = rules });

Assert.Equal("{\"assertions\":[\"metric.duration < 15min\"]}", json);
}

[Fact]
public void DeserializesFromJsonString()
{
var json = "{\"assertions\":[\"metric.duration < 15min\"]}";

var result = Serializer.Deserialize<AssertionContainer>(json);

Assert.Single(result.Assertions);
Assert.Equal("metric.duration < 15min", result.Assertions[0].Value);
}

private class AssertionContainer
{
[System.Text.Json.Serialization.JsonPropertyName("assertions")]
public List<AssertionRule> Assertions { get; set; }
}
}
}
5 changes: 3 additions & 2 deletions Cronitor.Tests/Builders/JobBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Cronitor.Constants;
using Cronitor.Models.Monitors;

namespace Cronitor.Tests.Builders
Expand All @@ -16,7 +17,7 @@ public class JobBuilder
private readonly int? _scheduleTolerance = 1;
private readonly string _timeZone = "Europe/Stockholm";

private List<string> _assertions = new List<string> { "metric.duration < 30s", "metric.error_count < 5" };
private List<AssertionRule> _assertions = new List<AssertionRule> { Assertion.Metric.Duration.LessThan("30s"), Assertion.Metric.ErrorCount.LessThan(5) };
private List<string> _notify = new List<string> { "developers" };
private readonly List<string> _tags = new List<string> { "tag", "attribute" };

Expand Down Expand Up @@ -45,7 +46,7 @@ public JobBuilder Key(string key)
return this;
}

public JobBuilder Assertions(List<string> assertions)
public JobBuilder Assertions(List<AssertionRule> assertions)
{
_assertions = assertions;
return this;
Expand Down
7 changes: 3 additions & 4 deletions Cronitor.Tests/MonitorTypeTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Cronitor.Constants;
using Cronitor.Models;
using Cronitor.Models.Monitors;
using Cronitor.Tests.Helpers;
using System.Collections.Generic;
Expand Down Expand Up @@ -31,7 +30,7 @@
const string timezone = "Europe/Stockholm";
const string url = "https://www.google.se";

var monitor = new Check(MonitorKey, new Request(url, regions))

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 33 in Cronitor.Tests/MonitorTypeTests.cs

View workflow job for this annotation

GitHub Actions / tests

The type or namespace name 'Request' could not be found (are you missing a using directive or an assembly reference?)
{
Schedule = schedule,
Timezone = timezone
Expand Down Expand Up @@ -73,10 +72,10 @@
[Fact]
public void ShouldCreateJobMonitor()
{
var assertions = new List<string>
var assertions = new List<AssertionRule>
{
"metric.duration < 30s",
"metric.error_count < 5"
Assertion.Metric.Duration.LessThan("30s"),
Assertion.Metric.ErrorCount.LessThan(5)
};
var notify = new List<string>
{
Expand Down
2 changes: 1 addition & 1 deletion Cronitor.Tests/Requests/CreateMonitorRequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public async Task ShouldCreateMonitorRequestAsync(string expected)
.With(x => x.Schedule, "0 0 * * *")
.With(x => x.Notify, new List<string> { "default" })
.With(x => x.Tags, new List<string> { "nightly" })
.With(x => x.Assertions, new List<string> { "metric.duration < 15min" })
.With(x => x.Assertions, new List<AssertionRule> { Assertion.Metric.Duration.LessThan("15min") })
.With(x => x.Timezone, "Europe/Stockholm")
.With(x => x.Note, "note")
.With(x => x.Platform, "linux")
Expand Down
2 changes: 1 addition & 1 deletion Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async Task ShouldCreateUpdateMonitorRequestAsync(string expected)
.With(x => x.Schedule, "0 0 * * *")
.With(x => x.Notify, new List<string> { "default" })
.With(x => x.Tags, new List<string> { "nightly" })
.With(x => x.Assertions, new List<string> { "metric.duration < 15min" })
.With(x => x.Assertions, new List<AssertionRule> { Assertion.Metric.Duration.LessThan("15min") })
.With(x => x.Timezone, "Europe/Stockholm")
.With(x => x.Note, "note")
.With(x => x.Platform, "linux");
Expand Down
84 changes: 84 additions & 0 deletions Cronitor/Constants/Assertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Cronitor.Constants
{
public static class Assertion
{
public static MetricAssertion Metric => new MetricAssertion();
public static ResponseAssertion Response => new ResponseAssertion();
}

public class MetricAssertion
{
public AssertionBuilder Duration => new AssertionBuilder("metric.duration");
public AssertionBuilder Count => new AssertionBuilder("metric.count");
public AssertionBuilder ErrorCount => new AssertionBuilder("metric.error_count");
}

public class ResponseAssertion
{
public AssertionBuilder Code => new AssertionBuilder("response.code");
public AssertionBuilder Time => new AssertionBuilder("response.time");
public AssertionBuilder Body => new AssertionBuilder("response.body");

public AssertionBuilder Json(string key) => new AssertionBuilder("response.json", key);
public AssertionBuilder Header(string key) => new AssertionBuilder("response.header", key);
}

public class AssertionBuilder
{
private readonly string _assertion;
private readonly string _key;

public AssertionBuilder(string assertion, string key = null)
{
_assertion = assertion;
_key = key;
}

public new AssertionRule Equals(object value) => Build("=", value);
public AssertionRule LessThan(object value) => Build("<", value);
public AssertionRule GreaterThan(object value) => Build(">", value);
public AssertionRule Contains(object value) => Build("contains", value);

private AssertionRule Build(string op, object value)
{
var assertion = _key != null
? $"{_assertion} {_key} {op} {value}"
: $"{_assertion} {op} {value}";

return new AssertionRule(assertion);
}
}

[JsonConverter(typeof(AssertionRuleConverter))]
public class AssertionRule
{
public string Value { get; }

public AssertionRule(string value)
{
Value = value;
}

public override string ToString() => Value;

public static implicit operator string(AssertionRule rule) => rule?.Value;
public static implicit operator AssertionRule(string value) => new AssertionRule(value);
}

public class AssertionRuleConverter : JsonConverter<AssertionRule>
{
public override AssertionRule Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return new AssertionRule(reader.GetString());
}

public override void Write(Utf8JsonWriter writer, AssertionRule value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Value);
}
}
}
3 changes: 2 additions & 1 deletion Cronitor/Models/Monitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json.Serialization;
using Cronitor.Constants;

namespace Cronitor.Models
{
Expand Down Expand Up @@ -35,7 +36,7 @@ public class Monitor
/// "response.header X-App-Version = 1.2.3"
/// </summary>
[JsonPropertyName("assertions")]
public IEnumerable<string> Assertions { get; set; }
public IEnumerable<AssertionRule> Assertions { get; set; }
/// <summary>
/// job and event: number of telemetry events with state='fail' to allow before sending an alert.
/// check: number of consecutive failed requests allow before sending an alert.
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,29 @@ public class SomeClass
* Add support for Quartz.NET Jobs
* Implement Timezone constant (if not too big of a hassle to maintain)
* Implement cron expression-language (if found as needed?)
* Implement Cronitor `assertions`-language (if found as needed?)
* ~~Implement Cronitor `assertions`-language (if found as needed?)~~

### Assertions Language
A fluent builder for Cronitor assertions is available via the `Assertion` class in `Cronitor.Constants`. Instead of writing raw assertion strings, you can use the builder for type safety and discoverability:

```c#
using Cronitor.Constants;

var monitor = new Job("my-job")
{
Assertions = new[]
{
Assertion.Metric.Duration.LessThan("30s"),
Assertion.Metric.ErrorCount.LessThan(5),
Assertion.Metric.Count.GreaterThan(0),
Assertion.Response.Code.Equals(200),
Assertion.Response.Time.LessThan("2s"),
Assertion.Response.Body.Contains("healthy"),
Assertion.Response.Json("user.count").GreaterThan(10),
Assertion.Response.Header("X-Version").Equals("1.2.3"),
}
};
```

## Contributing
Pull requests and features are happily considered! By participating in this project you agree to abide by the [Code of Conduct](http://contributor-covenant.org/version/2/0).
Expand Down
Loading