Skip to content

Commit

Permalink
Clean up and reorganize some of the fields sent in the alert. When tr…
Browse files Browse the repository at this point in the history
…iggering an alert, use the same dedupkey that from other checks with the same PagerDuty integration key, even if it's a different check. Added automated tests to verify JSON parsing of poorly-documented webhook payload from Freshping
  • Loading branch information
Aldaviva committed Jun 30, 2024
1 parent 8a9bc3c commit 38c6d52
Show file tree
Hide file tree
Showing 15 changed files with 502 additions and 127 deletions.
8 changes: 7 additions & 1 deletion FreshPager.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.35013.160
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FreshPager", "FreshPager\FreshPager.csproj", "{FFDBD983-BBD5-43D3-9B47-31EBF50C2D4F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreshPager", "FreshPager\FreshPager.csproj", "{FFDBD983-BBD5-43D3-9B47-31EBF50C2D4F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53127C8F-7BED-44A4-8A6C-73327AAE4DB6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -15,6 +17,10 @@ Global
{FFDBD983-BBD5-43D3-9B47-31EBF50C2D4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFDBD983-BBD5-43D3-9B47-31EBF50C2D4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFDBD983-BBD5-43D3-9B47-31EBF50C2D4F}.Release|Any CPU.Build.0 = Release|Any CPU
{53127C8F-7BED-44A4-8A6C-73327AAE4DB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53127C8F-7BED-44A4-8A6C-73327AAE4DB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53127C8F-7BED-44A4-8A6C-73327AAE4DB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53127C8F-7BED-44A4-8A6C-73327AAE4DB6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 2 additions & 0 deletions FreshPager.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Freshping/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace FreshPager;
namespace FreshPager.Data;

public class Configuration {

public IDictionary<string, string> pagerDutyIntegrationKeysByService { get; } = new Dictionary<string, string>();
public ushort httpServerPort { get; set; } = 37374;

}
14 changes: 14 additions & 0 deletions FreshPager/Data/Marshal/StringToOptionalIntConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace FreshPager.Data.Marshal;

public class StringToOptionalIntConverter: JsonConverter<int?> {

public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
return int.TryParse(reader.GetString(), out int number) ? number : null;
}

public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) => throw new NotImplementedException();

}
27 changes: 27 additions & 0 deletions FreshPager/Data/Marshal/StringToTimespanConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace FreshPager.Data.Marshal;

public abstract class StringToTimespanConverter: JsonConverter<TimeSpan> {

public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.GetString() is { } rawString ? intToTimeSpan(double.Parse(rawString)) : default;

protected abstract TimeSpan intToTimeSpan(double number);

public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) => throw new NotImplementedException();

public class FromSeconds: StringToTimespanConverter {

protected override TimeSpan intToTimeSpan(double number) => TimeSpan.FromSeconds(number);

}

public class FromMilliseconds: StringToTimespanConverter {

protected override TimeSpan intToTimeSpan(double number) => TimeSpan.FromMilliseconds(number);

}

}
205 changes: 205 additions & 0 deletions FreshPager/Data/WebhookPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using FreshPager.Data.Marshal;
using System.Text.Json.Serialization;

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

namespace FreshPager.Data;

public class WebhookPayload {

/// <summary>
/// <para>The title/subject/summary of the event</para>
/// <para>Examples:</para>
/// <para>Aldaviva HTTP (https://aldaviva.com) is DOWN.</para>
/// <para>Aldaviva SMTP (tcp://aldaviva.com:25) is UP.</para>
/// </summary>
[JsonPropertyName("text")]
public string eventTitle { get; set; }

/// <summary>
/// <para>Numeric ID of the check</para>
/// <para>Examples:</para>
/// <para>36897</para>
/// <para>829684</para>
/// </summary>
[JsonPropertyName("check_id")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public int checkId { get; set; }

/// <summary>
/// <para>The friendly check name</para>
/// <para>Examples:</para>
/// <para>Aldaviva HTTP</para>
/// <para>Aldaviva SMTP</para>
/// </summary>
[JsonPropertyName("check_name")]
public string checkName { get; set; }

/// <summary>
/// <para>The URL that was hit to do the health check</para>
/// <para>Examples:</para>
/// <para>https://aldaviva.com</para>
/// <para>tcp://aldaviva.com:25</para>
/// </summary>
[JsonPropertyName("check_url")]
public Uri checkedUrl { get; set; }

/// <summary>
/// <para>How long the health check had been willing to wait for a response (not how long it actually waited)</para>
/// <para>Example:</para>
/// <para>0:30:00</para>
/// </summary>
[JsonPropertyName("request_timeout")]
[JsonConverter(typeof(StringToTimespanConverter.FromSeconds))]
public TimeSpan requestTimeout { get; set; }

/// <summary>
/// <para>Examples:</para>
/// <para>US East (N. Virginia)</para>
/// <para>Asia Pacific (Tokyo)</para>
/// <para>EU (Ireland)</para>
/// <para>Asia Pacific (Singapore)</para>
/// <para>Canada (Central)</para>
/// <para>Asia Pacific (Sydney)</para>
/// <para>US West (Oregon)</para>
/// <para>Asia Pacific (Mumbai)</para>
/// <para>South America (Sao Paulo)</para>
/// <para>EU (London)</para>
/// </summary>
[JsonPropertyName("request_location")]
public string requestLocation { get; set; }

/// <summary>
/// <para>Example:</para>
/// <para>2024-06-28T18:07:46.709971+00:00</para>
/// </summary>
[JsonPropertyName("request_datetime")]
public DateTimeOffset requestDateTime { get; set; }

/// <summary>
/// <para>Examples:</para>
/// <para><c>None</c> (check is down due to a socket/processing exception like a timeout)</para>
/// <para><c>200</c> (HTTP check is up)</para>
/// <para><c>1</c> (TCP check is up)</para>
/// </summary>
[JsonPropertyName("response_status_code")]
[JsonConverter(typeof(StringToOptionalIntConverter))]
public int? responseStatusCode { get; set; }

/// <summary>
/// <para>Examples:</para>
/// <para>Connection Timeout</para>
/// <para>Not Responding</para>
/// <para>Available</para>
/// </summary>
[JsonPropertyName("response_summary")]
public string responseSummary { get; set; }

public bool isServiceUp => responseSummary == "Available";

/// <summary>
/// <para>Examples:</para>
/// <para>Not Responding</para>
/// <para>Available</para>
/// </summary>
[JsonPropertyName("response_state")]
public string responseState { get; set; }

/// <summary>
/// <para>How long it actually took for the health check to get a response.</para>
/// <para>For the maximum time the check was willing to wait, see <see cref="requestTimeout"/>.</para>
/// <para>Examples:</para>
/// <para>30003</para>
/// <para>17</para>
/// </summary>
[JsonPropertyName("response_time")]
[JsonConverter(typeof(StringToTimespanConverter.FromMilliseconds))]
public TimeSpan responseTime { get; set; }

[JsonPropertyName("event_data")]
[JsonInclude]
private EventData eventData { get; set; }

public string organizationSubdomain => eventData.organizationSubdomain;
public DateTimeOffset eventCreationDateTime => eventData.eventCreationDateTime;
public int eventId => eventData.eventId;
public int organizationId => eventData.organizationId;
public int webhookId => eventData.webhookId;
public EventFilter eventFilter => eventData.eventFilter;

public override string ToString() =>
$"{nameof(eventTitle)}: {eventTitle}, {nameof(checkId)}: {checkId}, {nameof(checkName)}: {checkName}, {nameof(checkedUrl)}: {checkedUrl}, {nameof(requestTimeout)}: {requestTimeout}, {nameof(requestLocation)}: {requestLocation}, {nameof(requestDateTime)}: {requestDateTime}, {nameof(responseStatusCode)}: {responseStatusCode}, {nameof(responseSummary)}: {responseSummary}, {nameof(isServiceUp)}: {isServiceUp}, {nameof(responseState)}: {responseState}, {nameof(responseTime)}: {responseTime}, {nameof(organizationSubdomain)}: {organizationSubdomain}, {nameof(eventCreationDateTime)}: {eventCreationDateTime}, {nameof(eventId)}: {eventId}, {nameof(organizationId)}: {organizationId}, {nameof(webhookId)}: {webhookId}, {nameof(eventFilter)}: {eventFilter}";

public class EventData {

/// <summary>
/// <para>The subdomain of the Freshping organization, also known as Freshping URL (not the Account Name) </para>
/// <para>Examples:</para>
/// <para>aldaviva</para>
/// </summary>
[JsonPropertyName("org_name")]
public string organizationSubdomain { get; set; }

/// <summary>
/// <para>Example:</para>
/// <para>2024-06-27T23:49:30.033405+00:00</para>
/// </summary>
[JsonPropertyName("event_created_on")]
public DateTimeOffset eventCreationDateTime { get; set; }

/// <summary>
/// <para>Unique ID for each webhook message sent</para>
/// <para>Examples:</para>
/// <para>17960894</para>
/// <para>17960760</para>
/// </summary>
[JsonPropertyName("event_id")]
public int eventId { get; set; }

/// <summary>
/// <para>Example:</para>
/// <para>10593</para>
/// </summary>
[JsonPropertyName("org_id")]
public int organizationId { get; set; }

/// <summary>
/// <para>Examples:</para>
/// <para><c>AL</c> (all events)</para>
/// <para><c>AT</c> (up and down events)</para>
/// <para><c>PE</c> (performance degraded events)</para>
/// <para><c>PS</c> (paused and restarted events)</para>
/// </summary>
[JsonPropertyName("webhook_type")]
[JsonInclude]
private string eventFilterRaw { get; set; }

/// <exception cref="ArgumentOutOfRangeException" accessor="get"></exception>
public EventFilter eventFilter => eventFilterRaw switch {
"AL" => EventFilter.ALL,
"AT" => EventFilter.UP_DOWN,
"PE" => EventFilter.DEGRADED_PERFORMANCE,
"PS" => EventFilter.PAUSED_UNPAUSED,
_ => throw new ArgumentOutOfRangeException(nameof(eventFilterRaw), eventFilterRaw, $"Unrecognized webhook_type value '{eventFilterRaw}'")
};

/// <summary>
/// <para>The unique ID of the webhook integration</para>
/// <para>Example:</para>
/// <para>35191</para>
/// </summary>
[JsonPropertyName("webhook_id")]
public int webhookId { get; set; }

}

public enum EventFilter {

ALL,
UP_DOWN,
DEGRADED_PERFORMANCE,
PAUSED_UNPAUSED

}

}
12 changes: 10 additions & 2 deletions FreshPager/FreshPager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
<RuntimeIdentifiers>win-x64;linux-arm;linux-arm64;linux-x64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<RollForward>latestMajor</RollForward>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<ServerGarbageCollection>true</ServerGarbageCollection>

<Authors>Ben Hutchison</Authors>
<Copyright>© 2024 $(Authors)</Copyright>
<Company>$(Authors)</Company>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<ApplicationIcon>freshping.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
Expand All @@ -28,10 +29,17 @@

<ItemGroup>
<PackageReference Include="Bom.Squad" Version="0.3.0" />
<PackageReference Include="jaytwo.FluentUri" Version="0.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.0" />
<PackageReference Include="PagerDuty" Version="1.1.0" />
<!-- <PackageReference Include="PagerDuty" Version="1.1.0" /> -->
<PackageReference Include="ThrottleDebounce" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<Reference Include="PagerDuty">
<HintPath>..\..\PagerDuty\PagerDuty\bin\Release\netstandard2.0\PagerDuty.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
Loading

0 comments on commit 38c6d52

Please sign in to comment.