Skip to content

Commit

Permalink
Fixed period query parameter being ignored. Changed default precision…
Browse files Browse the repository at this point in the history
… from 7 to 4, because Freshping granularity only needs that many digits after the decimal point for any possible difference. Clip report duration to valid range [1 min, 90 days]. Added bad Italian localization. Updated dependencies. Documented badge color behavior.
  • Loading branch information
Aldaviva committed Jan 31, 2025
1 parent 733bc9c commit 68f3e2d
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 39 deletions.
7 changes: 4 additions & 3 deletions FreshBadge/FreshBadge.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<Nullable>enable</Nullable>
<RollForward>latestMajor</RollForward>
<LangVersion>latest</LangVersion>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<Authors>Ben Hutchison</Authors>
<Copyright>© 2025 $(Authors)</Copyright>
<Company>$(Authors)</Company>
Expand All @@ -22,9 +22,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="NodaTime" Version="3.2.1" />
<PackageReference Include="Tavis.UriTemplates" Version="2.0.0" />
<PackageReference Include="Unfucked" Version="0.0.0-beta3" />
<PackageReference Include="UnionTypes" Version="1.0.1" />
<PackageReference Include="Unfucked" Version="0.0.0-beta4" />
<PackageReference Include="UnionTypes" Version="1.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
19 changes: 10 additions & 9 deletions FreshBadge/FreshpingClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FreshBadge.Data;
using NodaTime;
using System.Net;
using System.Text.Json;
using Tavis.UriTemplates;
Expand All @@ -8,7 +9,7 @@ namespace FreshBadge;
public interface FreshpingClient {

/// <exception cref="FreshBadgeException">the request to Freshping failed</exception>
public Task<CheckStatus> fetchCheckStatus(long checkId, TimeSpan period);
public Task<CheckStatus> fetchCheckStatus(long checkId, Duration duration);

}

Expand All @@ -21,12 +22,12 @@ public class FreshpingClientImpl(HttpClient http): FreshpingClient {
private static UriTemplate checkUptimeUrl => new("https://api.freshping.io/v1/public-check-stats-reports/{check_id}/{?start_time,end_time}");

/// <inheritdoc />
public async Task<CheckStatus> fetchCheckStatus(long checkId, TimeSpan period) {
public async Task<CheckStatus> fetchCheckStatus(long checkId, Duration duration) {
try {
DateTime now = DateTime.UtcNow;
Instant now = SystemClock.Instance.GetCurrentInstant();

Task<PublicCheckStatusReport> stateTask = fetchCheckStatusReport(checkId, TimeSpan.FromSeconds(1), now);
Task<PublicCheckStatusReport> uptimeTask = fetchCheckStatusReport(checkId, period, now);
Task<PublicCheckStatusReport> stateTask = fetchCheckStatusReport(checkId, Duration.FromSeconds(1), now);
Task<PublicCheckStatusReport> uptimeTask = fetchCheckStatusReport(checkId, duration, now);

PublicCheckStatusReport state = await stateTask;
PublicCheckStatusReport uptime = await uptimeTask;
Expand All @@ -43,14 +44,14 @@ public async Task<CheckStatus> fetchCheckStatus(long checkId, TimeSpan period) {

/// <exception cref="FreshBadgeException"></exception>
/// <exception cref="HttpRequestException"></exception>
private async Task<PublicCheckStatusReport> fetchCheckStatusReport(long checkId, TimeSpan period, DateTime utcNow = default) {
private async Task<PublicCheckStatusReport> fetchCheckStatusReport(long checkId, Duration duration, Instant now = default) {
try {
utcNow = utcNow == default ? DateTime.UtcNow : utcNow;
now = now == default ? SystemClock.Instance.GetCurrentInstant() : now;

return (await http.GetFromJsonAsync<PublicCheckStatusReport>(checkUptimeUrl.AddParameters(new {
check_id = checkId,
start_time = utcNow.Subtract(period).ToString("O"),
end_time = utcNow.ToString("O")
start_time = (now - duration).ToString(),
end_time = now.ToString()
}).Resolve(), JSON_OPTIONS))!;
} catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound) {
throw new FreshBadgeException("Check must be added to a Freshping Status Page");
Expand Down
123 changes: 123 additions & 0 deletions FreshBadge/Internationalization/Resources.it.resx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="uptime" xml:space="preserve">
<value>tempo di attività</value>
</data>
</root>
34 changes: 26 additions & 8 deletions FreshBadge/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
using FreshBadge.Data.Shields;
using FreshBadge.Internationalization;
using Microsoft.AspNetCore.Mvc;
using NodaTime;
using NodaTime.Text;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

ShieldLogo freshpingLogo = new(Resources.freshpingLogo);
CultureInfo defaultCulture = CultureInfo.CurrentCulture;
ShieldLogo freshpingLogo = new(Resources.freshpingLogo);
CultureInfo defaultCulture = CultureInfo.CurrentCulture;
Duration minimumDuration = Duration.FromMinutes(1);
Duration maximumDuration = Duration.FromDays(90);
Duration defaultDuration = maximumDuration;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

Expand All @@ -21,20 +26,33 @@

await using WebApplication webApp = builder.Build();

webApp.MapGet("/{checkId:long}", async ([FromRoute] long checkId, [FromQuery] string? period, [FromQuery] byte? precision, [FromQuery] string? locale, [FromServices] FreshpingClient client) => {
webApp.MapGet("/{checkId:long}", async ([FromRoute] long checkId,
[FromQuery] string? period,
[FromQuery] byte? precision,
[FromQuery] string? locale,
[FromServices] FreshpingClient client) => {
try {
TimeSpan reportPeriod = TimeSpan.TryParse(period, out TimeSpan p) ? p : TimeSpan.FromDays(90);
CheckStatus status = await client.fetchCheckStatus(checkId, reportPeriod);
precision ??= 7;
Duration reportDuration = period is not null && PeriodPattern.NormalizingIso.Parse(period) is { Success: true, Value: var p } && p.ToDuration() is var d ?
d < minimumDuration ? minimumDuration :
d > maximumDuration ? maximumDuration :
d :
defaultDuration;
precision ??= 4; // 60/7776000*100 = 0.0007716 (all increments affect 4 digits after the decimal point)

CheckStatus status = await client.fetchCheckStatus(checkId, reportDuration);

try {
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = locale != null ? CultureInfo.GetCultureInfo(locale) : defaultCulture;
} catch (CultureNotFoundException) {
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = defaultCulture;
}

string message = Math.Round(status.uptime, (int) precision + 2, MidpointRounding.ToNegativeInfinity).ToString("P" + precision);
return new ShieldsBadgeResponse(Resources.uptime, message, messageColor: status.isUp ? ShieldColor.SUCCESS : ShieldColor.CRITICAL, isError: !status.isUp, logo: freshpingLogo);
return new ShieldsBadgeResponse(
label: Resources.uptime,
message: Math.Round(status.uptime, (int) precision + 2, MidpointRounding.ToNegativeInfinity).ToString("P" + precision),
messageColor: status.isUp ? ShieldColor.SUCCESS : ShieldColor.CRITICAL,
isError: !status.isUp,
logo: freshpingLogo);
} catch (FreshBadgeException e) {
return new ShieldsBadgeResponse(Resources.error, e.Message, messageColor: ShieldColor.CRITICAL, isError: true, logo: freshpingLogo);
}
Expand Down
18 changes: 12 additions & 6 deletions FreshBadge/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
"version": 1,
"dependencies": {
"net8.0": {
"NodaTime": {
"type": "Direct",
"requested": "[3.2.1, )",
"resolved": "3.2.1",
"contentHash": "D1aHhUfPQUxU2nfDCVuSLahpp0xCYZTmj/KNH3mSK/tStJYcx9HO9aJ0qbOP3hzjGPV/DXOqY2AHe27Nt4xs4g=="
},
"Tavis.UriTemplates": {
"type": "Direct",
"requested": "[2.0.0, )",
Expand All @@ -10,15 +16,15 @@
},
"Unfucked": {
"type": "Direct",
"requested": "[0.0.0-beta3, )",
"resolved": "0.0.0-beta3",
"contentHash": "yV5nP4hBAz0HpVcvVqx+kbuUDu/lMYkPbNo4Ym9SLff8Xb0mgs3oAgGMAbJ3EVbj+TAid7o/GYgzt5/yfuNG6A=="
"requested": "[0.0.0-beta4, )",
"resolved": "0.0.0-beta4",
"contentHash": "pWwHZP9Ok/LAZqFSrZ9CNddcWhx92wFxDh8oUgSrsNZjv/lFbM3HPSdcmLeqj4hhv1eir5a/DcQk+5XPSHzyvQ=="
},
"UnionTypes": {
"type": "Direct",
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "Nf7oNa8/mFwOiJYKmWcI7hcNwbFiXpSCZ2/DsMVWQk/aucpJtTKNvt4EYuTeOZi4IAseaPw04uf4rQEG5B6+tA=="
"requested": "[1.1.0, )",
"resolved": "1.1.0",
"contentHash": "9OEQYxKvKfGAKKRZ6BtuOPE5cD/QKHXmmUY+jyk/i3mUbGpLOW6HBkKLa1tS+Ut3hc3QYzchAlSNWVmGYni+wg=="
}
},
"net8.0/linux-arm": {},
Expand Down
27 changes: 14 additions & 13 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ for example
https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333
```

- The Badge background color is green when the check is up and red when the check is down.
- You can customize the Badge appearance using the [Endpoint Badge query parameters](https://shields.io/badges/endpoint-badge#:~:text=the%20query%20string.-,Query%20Parameters,-url%20string%20%E2%80%94).
```url
https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333&label=uptime+(90+days)
Expand All @@ -92,46 +93,46 @@ https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge

## API

- URL template: `https://west.aldaviva.com/freshbadge/{checkId}?period={period}`
- URL template: `https://west.aldaviva.com/freshbadge/{checkId}{?period,precision,locale}`
- Verb: `GET`
- Parameters
- `checkId`
- **importance:** required
- **location:** path parameter
- **location:** path
- **type:** number (64-bit signed integer)
- **meaning:** numeric ID of the Freshping Check to show uptime for, as seen in the `check_id` query parameter of the Freshping report page for this Check
- **meaning:** numeric ID of the Freshping Check to show uptime for, as seen in the `check_id` query parameter of the Freshping report page for this Check (`/reports?check_id={checkId}`)
- `period`
- **importance:** optional
- **location:** query parameter
- **location:** query
- **type:** string
- **format:** [ISO 8601 time period](https://en.wikipedia.org/wiki/ISO_8601#Durations)
- **meaning:** period over which to calculate the Check's uptime percentage, ending at the current time
- **validity:** in the range (0, 90 days]
- **range:** [1 minute, 90 days]
- **default:** 90 days
- **example:** `?period=P30D` (30 days) ![30 days](https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333%3Fperiod%3DP30D)
- `precision`
- **importance:** optional
- **location:** query parameter
- **location:** query
- **type:** number (8-bit unsigned integer)
- **meaning:** how many digits after the decimal point the uptime percentage should show
- **validity:** in the range [0, 256)
- **default:** 7 (for the fabled "9 nines," because 99.9999999% has 7 nines after the decimal point)
- **example:** `?precision=2` (2 digits after the decimal point: `99.99%`) ![2 digits](https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333%3Fprecision%3D2)
- **range:** [0, 256)
- **default:** 4 (the finest Freshping check granularity is 1 minute, and the longest data retention is 90 days, which means 4 digits after the decimal point will always be precise enough to express even the shortest outage)
- **example:** `?precision=2` (2 digits after the decimal point) ![2 digits](https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333%3Fprecision%3D2)
- `locale`
- **importance:** optional
- **location:** query parameter
- **location:** query
- **type:** string
- **format:** [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag)
- **meaning:** locale to use when rendering the uptime label and percentage
- **validity:** any language tag supported by [Windows](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c) or [ICU](https://icu.unicode.org/) (on Unix); the "uptime" label is currently badly localized in `de`, `en`, `es`, and `fr`.
- **range:** any language tag supported by [Windows](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c) or [ICU](https://icu.unicode.org/) (on Unix); the "uptime" label is currently badly localized in `de`, `es`, `fr`, and `it`.
- **default:** `en-US` (US English), or the server user's locale when self-hosted
- **example:** `?locale=fr` (France format: `99,99 %`) ![2 digits](https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333%3Flocale%3Dfr)
- **example:** `?locale=fr` (France format) ![French](https://img.shields.io/endpoint?url=https%3A%2F%2Fwest.aldaviva.com%2Ffreshbadge%2F304333%3Flocale%3Dfr)
- Response body: JSON object that conforms to the [Shields.io JSON Endpoint schema](https://shields.io/badges/endpoint-badge#:~:text=Example%20Shields%20Response-,Schema,-Property)
```json
{
"schemaVersion": 1,
"label": "uptime",
"message": "99.9132844%",
"message": "99.9133%",
"color": "success",
"isError": false,
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><path fill=\"#fff\" d=\"M28 0H16C7.2 0 0 7.2 0 16s7.2 16 16 16 16-7.2 16-16V4c0-2.2-1.8-4-4-4zM16 7.7c4.4 0 8 3.5 8.3 7.8h-4l-2.4-3.1c-.2-.3-.6-.4-1-.4-.4.1-.7.3-.8.7l-1.8 5.1-1.3-1.9c-.2-.3-.5-.4-.8-.4H7.7c.2-4.4 3.9-7.8 8.3-7.8zm0 16.6c-4.1 0-7.5-2.9-8.2-6.8h3.9l2.3 3c.2.3.5.4.8.4h.2c.4-.1.7-.3.8-.7l1.8-5.1 1.5 2c.2.2.5.4.8.4h4.4c-.8 3.9-4.2 6.8-8.3 6.8z\"/></svg>"
Expand Down

0 comments on commit 68f3e2d

Please sign in to comment.