Skip to content
Snippets Groups Projects
Commit 2269aefc authored by Dmytro Bogatov's avatar Dmytro Bogatov :two_hearts:
Browse files

Merge branch '25-report-uptime-for-ping-metrics' into 'master'

Resolve "Report uptime for ping metrics"

Closes #25

See merge request !15
parents b9584e0f ed1b3ad5
Branches
No related tags found
No related merge requests found
Pipeline #
......@@ -22,18 +22,32 @@ Here is how to put **individual metric health** badge in markdown
Where *type* is a metric type (eq. `cpuload`) and *source* is a metric source.
Here is how to put **service uptime** badge in markdown
[![service uptime](https://status.dbogatov.org/health/uptime/url)](https://status.dbogatov.org/home/metric/uptime/url)
Where *url* is a ping server URL (see [configuration](configuration/)) (eq. `google.com`).
### HTML
Here is how to put **system health** badge in markdown
Here is how to put **system health** badge in HTML
<a href="https://status.dbogatov.org/">
<img alt="system health" src="https://status.dbogatov.org/health" />
</a>
Here is how to put **individual metric health** badge in markdown
Here is how to put **individual metric health** badge in HTML
<a href="https://status.dbogatov.org/home/metric/type/source">
<img alt="metric health" src="https://status.dbogatov.org/health/type/source" />
</a>
Where *type* is a metric type (eq. `cpuload`) and *source* is a metric source.
Here is how to put **service uptime** badge in HTML
<a href="https://status.dbogatov.org/home/metric/uptime/url">
<img alt="service uptime" src="https://status.dbogatov.org/health/uptime/url" />
</a>
Where *url* is a ping server URL (see [configuration](configuration/)) (eq. `google.com`).
......@@ -62,6 +62,7 @@ namespace StatusMonitor.Shared.Extensions
services.AddTransient<IMetricService, MetricService>();
services.AddTransient<ILoggingService, LoggingService>();
services.AddTransient<ICleanService, CleanService>();
services.AddTransient<IUptimeReportService, UptimeReportService>();
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<ISlackService, SlackService>();
services.AddTransient<INotificationService, NotificationService>();
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using StatusMonitor.Shared.Extensions;
using StatusMonitor.Shared.Models;
using StatusMonitor.Shared.Models.Entities;
namespace StatusMonitor.Shared.Services
{
public interface IUptimeReportService
{
/// <summary>
/// Computes uptime as a percentage of "OK" ping dta points
/// </summary>
/// <param name="source">Source fo the Ping metric</param>
/// <returns>Uptime percentage</returns>
Task<int> ComputeUptimeAsync(string source);
}
public class UptimeReportService : IUptimeReportService
{
private readonly IDataContext _context;
public UptimeReportService(IDataContext context)
{
_context = context;
}
public async Task<int> ComputeUptimeAsync(string source)
{
var metric = _context.Metrics.Single(mt => mt.Type == Metrics.Ping.AsInt() && mt.Source == source);
return
await _context
.PingDataPoints
.AnyAsync(dp => dp.Metric == metric)
?
(int)Math.Round(100*
(
(double)await _context
.PingDataPoints
.Where(dp => dp.Metric == metric && dp.Success)
.CountAsync()
/
await _context
.PingDataPoints
.Where(dp => dp.Metric == metric)
.CountAsync()
))
:
0
;
}
}
}
......@@ -21,18 +21,21 @@ namespace StatusMonitor.Web.Controllers.View
private readonly IDataContext _context;
private readonly IAuthService _auth;
private readonly IBadgeService _badge;
private readonly IUptimeReportService _uptime;
public HealthController(
IMetricService metricService,
IDataContext context,
IAuthService auth,
IBadgeService badge
IBadgeService badge,
IUptimeReportService uptime
)
{
_metricService = metricService;
_context = context;
_auth = auth;
_badge = badge;
_uptime = uptime;
}
public async Task<IActionResult> Index()
......@@ -81,5 +84,31 @@ namespace StatusMonitor.Web.Controllers.View
)
);
}
[Route("health/uptime/{source}")]
[ResponseCache(Duration = 30)]
public async Task<IActionResult> Uptime(string source)
{
var metrics = await _metricService.GetMetricsAsync(Metrics.Ping, source);
if (metrics.Count() == 0)
{
return NotFound();
}
var metric = metrics.First();
if (!_auth.IsAuthenticated() && !metric.Public)
{
return Unauthorized();
}
return new BadgeResult(
_badge.GetUptimeBadge(
metric.Source,
await _uptime.ComputeUptimeAsync(metric.Source)
)
);
}
}
}
......@@ -29,18 +29,21 @@ namespace StatusMonitor.Web.Controllers.View
private readonly IDataContext _context;
private readonly IAuthService _auth;
private readonly IBadgeService _badge;
private readonly IUptimeReportService _uptime;
public HomeController(
IMetricService metricService,
IDataContext context,
IAuthService auth,
IBadgeService badge
IBadgeService badge,
IUptimeReportService uptime
)
{
_metricService = metricService;
_context = context;
_auth = auth;
_badge = badge;
_uptime = uptime;
}
......@@ -100,6 +103,8 @@ namespace StatusMonitor.Web.Controllers.View
.FirstOrDefaultAsync(setting => new Uri(setting.ServerUrl).Host == source);
ViewBag.Max = pingSetting.MaxResponseTime.TotalMilliseconds;
ViewBag.Uptime = await _uptime.ComputeUptimeAsync(source);
}
return View(model.First());
......
......@@ -25,7 +25,22 @@ namespace StatusMonitor.Web.Services
/// <returns>Badge indicating overall health of the system</returns>
Badge GetSystemHealthBadge(HealthReport report);
/// <summary>
/// Generates individual metric's health badge
/// </summary>
/// <param name="source">Metric source</param>
/// <param name="type">Metric type</param>
/// <param name="label">Metric label</param>
/// <returns>Badge indicating individual health of the metric</returns>
Badge GetMetricHealthBadge(string source, Metrics type, AutoLabels label);
/// <summary>
/// Generates server's uptime
/// </summary>
/// <param name="url">URL of the servers whose uptime is presented</param>
/// <param name="uptime">Percentage of time when service is online</param>
/// <returns>Badge indicating server's uptime</returns>
Badge GetUptimeBadge(string url, int uptime);
}
public class BadgeService : IBadgeService
......@@ -61,6 +76,22 @@ namespace StatusMonitor.Web.Services
)
};
}
public Badge GetUptimeBadge(string url, int uptime)
{
return new Badge
{
Title = $"{url} uptime",
Message = $"{uptime}%",
Status =
uptime >= 95 ?
BadgeStatus.Success :
(uptime >= 85 ?
BadgeStatus.Neutural :
BadgeStatus.Failure
)
};
}
}
/// <summary>
......
......@@ -9,6 +9,10 @@
<h2>
@Model.Title from @Model.Source |
Report generated on <utc-time time="@Model.LastUpdated" /> |
@if (@ViewData["Uptime"] != null)
{
@: Uptime @ViewData["Uptime"]% |
}
<a href="javascript:window.location.href=window.location.href">Reload</a>
</h2>
</div>
......
......@@ -25,6 +25,7 @@ namespace StatusMonitor.Tests.ControllerTests
private readonly Mock<IMetricService> _mockMetricService = new Mock<IMetricService>();
private readonly Mock<IAuthService> _mockAuth = new Mock<IAuthService>();
private readonly Mock<IBadgeService> _mockBadge = new Mock<IBadgeService>();
private readonly Mock<IUptimeReportService> _mockUptime = new Mock<IUptimeReportService>();
private readonly IDataContext _context;
......@@ -57,7 +58,8 @@ namespace StatusMonitor.Tests.ControllerTests
_mockMetricService.Object,
_context,
_mockAuth.Object,
_mockBadge.Object
_mockBadge.Object,
_mockUptime.Object
);
_controller.ControllerContext.HttpContext = new DefaultHttpContext();
......
......@@ -25,6 +25,7 @@ namespace StatusMonitor.Tests.ControllerTests
private readonly Mock<IMetricService> _mockMetricService = new Mock<IMetricService>();
private readonly Mock<IAuthService> _mockAuth = new Mock<IAuthService>();
private readonly Mock<IBadgeService> _mockBadge = new Mock<IBadgeService>();
private readonly Mock<IUptimeReportService> _mockUptime = new Mock<IUptimeReportService>();
private readonly IDataContext _context;
......@@ -57,7 +58,8 @@ namespace StatusMonitor.Tests.ControllerTests
_mockMetricService.Object,
_context,
_mockAuth.Object,
_mockBadge.Object
_mockBadge.Object,
_mockUptime.Object
);
_controller.ControllerContext.HttpContext = new DefaultHttpContext();
_controller.TempData = new Mock<ITempDataDictionary>().Object;
......
......@@ -95,5 +95,70 @@ namespace StatusMonitor.Tests.UnitTests.Services
}
Assert.Equal("System health".ToLower(), badge.Title.ToLower());
}
[Theory]
[InlineData(BadgeStatus.Success)]
[InlineData(BadgeStatus.Neutural)]
[InlineData(BadgeStatus.Failure)]
public void ProducesIndividualHealthBadge(BadgeStatus status)
{
// Act
var badge = new BadgeService().GetMetricHealthBadge(
"the-source",
Metrics.CpuLoad,
status == BadgeStatus.Success ? AutoLabels.Normal : (status == BadgeStatus.Neutural ? AutoLabels.Warning : AutoLabels.Critical)
);
// Assert
Assert.Equal(status, badge.Status);
Assert.NotEqual(0, badge.TitleWidth);
Assert.NotEqual(0, badge.MessageWidth);
switch (status)
{
case BadgeStatus.Success:
Assert.Contains(AutoLabels.Normal.ToString().ToLower(), badge.Message.ToLower());
break;
case BadgeStatus.Neutural:
Assert.Contains(AutoLabels.Warning.ToString().ToLower(), badge.Message.ToLower());
break;
case BadgeStatus.Failure:
Assert.Contains(AutoLabels.Critical.ToString().ToLower(), badge.Message.ToLower());
break;
}
Assert.Contains(Metrics.CpuLoad.ToString().ToLower(), badge.Title.ToLower());
Assert.Contains("the-source", badge.Title.ToLower());
}
[Theory]
[InlineData(BadgeStatus.Success)]
[InlineData(BadgeStatus.Neutural)]
[InlineData(BadgeStatus.Failure)]
public void ProducesUptimeBadge(BadgeStatus status)
{
// Act
var badge = new BadgeService().GetUptimeBadge(
"the-url.com",
status == BadgeStatus.Success ? 98 : (status == BadgeStatus.Neutural ? 90 : 80)
);
// Assert
Assert.Equal(status, badge.Status);
Assert.NotEqual(0, badge.TitleWidth);
Assert.NotEqual(0, badge.MessageWidth);
switch (status)
{
case BadgeStatus.Success:
Assert.Contains(98.ToString(), badge.Message);
break;
case BadgeStatus.Neutural:
Assert.Contains(90.ToString(), badge.Message);
break;
case BadgeStatus.Failure:
Assert.Contains(80.ToString(), badge.Message);
break;
}
Assert.Contains("uptime", badge.Title.ToLower());
Assert.Contains("the-url.com", badge.Title.ToLower());
}
}
}
using System;
using Xunit;
using StatusMonitor.Shared.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using StatusMonitor.Daemons.Services;
using Moq;
using Microsoft.AspNetCore.Hosting;
using StatusMonitor.Shared.Extensions;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using StatusMonitor.Shared.Models.Entities;
using Microsoft.Extensions.Logging;
using StatusMonitor.Shared.Services;
namespace StatusMonitor.Tests.UnitTests.Services
{
public class UptimeReportServiceTest
{
private readonly IServiceProvider _serviceProvider;
public UptimeReportServiceTest()
{
var services = new ServiceCollection();
var mockEnv = new Mock<IHostingEnvironment>();
mockEnv
.SetupGet(environment => environment.EnvironmentName)
.Returns("Testing");
services.RegisterSharedServices(mockEnv.Object, new Mock<IConfiguration>().Object);
_serviceProvider = services.BuildServiceProvider();
}
[Fact]
public async Task ComputesUptime()
{
// Arrange
var context = _serviceProvider.GetRequiredService<IDataContext>();
var metric = await context.Metrics.AddAsync(new Metric {
Type = Metrics.Ping.AsInt(),
Source = "url.com"
});
await context.PingDataPoints.AddRangeAsync(
new PingDataPoint { Metric = metric.Entity, Success = true },
new PingDataPoint { Metric = metric.Entity, Success = false },
new PingDataPoint { Metric = metric.Entity, Success = true },
new PingDataPoint { Metric = metric.Entity, Success = false },
new PingDataPoint { Metric = metric.Entity, Success = true },
new PingDataPoint { Metric = metric.Entity, Success = true },
new PingDataPoint { Metric = metric.Entity, Success = false },
new PingDataPoint { Metric = metric.Entity, Success = true },
new PingDataPoint { Metric = metric.Entity, Success = false },
new PingDataPoint { Metric = metric.Entity, Success = true }
);
await context.SaveChangesAsync();
// Act
var actual = await new UptimeReportService(context)
.ComputeUptimeAsync("url.com");
// Assert
Assert.Equal(60, actual);
}
[Fact]
public async Task ComputesUptimeNoData()
{
// Arrange
var context = _serviceProvider.GetRequiredService<IDataContext>();
await context.Metrics.AddAsync(new Metric {
Type = Metrics.Ping.AsInt(),
Source = "url.com"
});
await context.SaveChangesAsync();
// Act
var actual = await new UptimeReportService(context)
.ComputeUptimeAsync("url.com");
// Assert
Assert.Equal(0, actual);
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment