diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 079a1ab..1cec04f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -35,6 +35,13 @@ jobs: - name: Build & Publish WSM.Client run: dotnet publish "./WSM.Client/WSM.Client.csproj" -o ./build/wsm.client/ -c Release + + - name: Build & Copy Health Checks + run: | + dotnet build "./WSM.HealthChecks/WSM.HealthChecks.csproj" -c Release + mkdir -p ./build/wsm.client/healthchecks + cp ./WSM.HealthChecks/bin/Release/net6.0/WSM.HealthChecks.dll ./build/wsm.client/healthchecks/WSM.HealthChecks.dll + - name: Build WSM.Server Docker uses: docker/build-push-action@v4 @@ -50,8 +57,7 @@ jobs: cp ./WSM.Client/install-service.ps1 ./build/wsm.client/install-service.ps1 cp ./WSM.Client/uninstall-service.ps1 ./build/wsm.client/uninstall-service.ps1 - - name: Create wsm.client.zip - if: github.ref == 'refs/heads/master' + - name: Create wsm.client.zip run: zip -r ./build/wsm.client.zip ./build/wsm.client - name: Create release diff --git a/README.md b/README.md index b60f968..109c0c0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ WSM is a service for monitoring different aspects of a Windows server and alerting when certain conditions are met. ### What can WSM monitor? -See [Health Check Types](#health-check-types) for more detail but in a nutshell processes, ports, docker containers and disk space & http request for now. +See [Health Check Types](#health-check-types) for more detail but in a nutshell processes, ports, docker containers and disk space, free memory & http request for now. ### Why? I had a server which ran lots of different services, Plex, Game services, VPN, DNS and a bunch of docker containers and inevitably something would eventually fail, I wouldn't usually find this out until someone using one of the services let me know. I wanted a tool that was free and easy to set up but couldn't find one that did everything I wanted, also I like coding so I figured it was a good candidate for a project, 3 days later WSM was born. @@ -272,6 +272,18 @@ Sends a HTTP request and check for the correct status code, it can also optional - `RequestBody` (Optional) - The payload that can be sent, make sure you change the `Method` to the appropriate value - `ExpectedResponseBody` (Optional) - An expected response body to validate upon request completion +### FreeMemory +Checks the system for free memory +```json +{ + "Name": "Free Memory", + "Type": "Free Memory", + "Interval": "00:01:00", + "MinimumFreeMemory": 100000 +} +``` +- `MinimumFreeMemory` (Required) - The minimum number of free bytes you expect the server to have + ### Installing the Windows service Run `install-service.ps1` inside `C:\wsm.client\`, this will install and start the service @@ -286,4 +298,4 @@ Put nginx in front of it and proxy to 443->80 on the docker image 2. ??? ### What if a health check type isn't supported? -Create your own or raise an issue on github. Take a look at WSM.PluginExample to see how you write your own health check. Once you've compiled it, drop it into the clients install direction\HealthChecks alongside `WSM.HealthChecks.dll` +Create your own or raise an issue on github. Take a look at WSM.PluginExample to see how you write your own health check. Once you've compiled it, drop it into the clients install directory\HealthChecks alongside `WSM.HealthChecks.dll` diff --git a/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckConfiguration.cs b/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckConfiguration.cs new file mode 100644 index 0000000..5ad742b --- /dev/null +++ b/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckConfiguration.cs @@ -0,0 +1,9 @@ +using WSM.Client.Models; + +namespace WSM.PluginExample +{ + public class FreeMemoryHealthCheckConfiguration: HealthCheckConfigurationBase + { + public long MinimumFreeMemory { get; set; } + } +} diff --git a/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckDefinition.cs b/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckDefinition.cs new file mode 100644 index 0000000..3e44a12 --- /dev/null +++ b/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckDefinition.cs @@ -0,0 +1,10 @@ +using WSM.Client.Models; +using WSM.HealthChecks.FreeMemoryHealthCheck; + +namespace WSM.PluginExample +{ + public class FreeMemoryHealthCheckDefinition : HealthCheckDefinitionBase + { + public override string Type => "FreeMemory"; + } +} diff --git a/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckJob.cs b/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckJob.cs new file mode 100644 index 0000000..c9fcebd --- /dev/null +++ b/WSM.HealthChecks/FreeMemoryHealthCheck/FreeMemoryHealthCheckJob.cs @@ -0,0 +1,129 @@ +using Microsoft.Extensions.Logging; +using Quartz; +using WSM.Client.Jobs; +using WSM.Shared; +using WSM.PluginExample; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace WSM.HealthChecks.FreeMemoryHealthCheck +{ + [DisallowConcurrentExecution] + public class FreeMemoryHealthCheckJob : HealthCheckJobBase + { + private readonly ILogger _logger; + private const string MemoryLowStatus = "Free memory low"; + public FreeMemoryHealthCheckJob(ILogger logger, WSMApiClient apiClient) : base(apiClient) + { + _logger = logger; + } + public override async Task Execute(IJobExecutionContext context) + { + var healthCheckConfiguration = GetConfiguration(context); + try + { + var freeSystemMemory = GetFreeSystemMemory(); + + if (freeSystemMemory < healthCheckConfiguration.MinimumFreeMemory) + { + string mbAvailable = FormatUtils.SizeSuffix(freeSystemMemory); + await CheckIn(healthCheckConfiguration, $"{MemoryLowStatus}, {mbAvailable} available"); + return; + } + + await CheckIn(healthCheckConfiguration, Constants.AvailableStatus); + } + catch (Exception ex) + { + _logger.LogError(ex, ""); + } + } + + private long GetFreeSystemMemory() + { + + MemoryMetricsClient client = new MemoryMetricsClient(); + return client.GetMetrics().Free; + } + + public class MemoryMetrics + { + public long Total; + public long Used; + public long Free; + } + + public class MemoryMetricsClient + { + public MemoryMetrics GetMetrics() + { + if (IsUnix()) + { + return GetUnixMetrics(); + } + + return GetWindowsMetrics(); + } + + private bool IsUnix() + { + var isUnix = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + return isUnix; + } + + private MemoryMetrics GetWindowsMetrics() + { + var output = ""; + + var info = new ProcessStartInfo(); + info.FileName = "wmic"; + info.Arguments = "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value"; + info.RedirectStandardOutput = true; + + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + } + + var lines = output.Trim().Split("\n"); + var freeMemoryParts = lines[0].Split("=", StringSplitOptions.RemoveEmptyEntries); + var totalMemoryParts = lines[1].Split("=", StringSplitOptions.RemoveEmptyEntries); + + var metrics = new MemoryMetrics(); + metrics.Total = long.Parse(totalMemoryParts[1]) * 1000; + metrics.Free =long.Parse(freeMemoryParts[1]) * 1000; + metrics.Used = metrics.Total - metrics.Free; + + return metrics; + } + + private MemoryMetrics GetUnixMetrics() + { + var output = ""; + + var info = new ProcessStartInfo("free -m"); + info.FileName = "/bin/bash"; + info.Arguments = "-c \"free -m\""; + info.RedirectStandardOutput = true; + + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + Console.WriteLine(output); + } + + var lines = output.Split("\n"); + var memory = lines[1].Split(" ", StringSplitOptions.RemoveEmptyEntries); + + var metrics = new MemoryMetrics(); + metrics.Total = long.Parse(memory[1]); + metrics.Used = long.Parse(memory[2]); + metrics.Free = long.Parse(memory[3]); + + return metrics; + } + } + } +}