From 6c75d06a3ba6b94ccbf6df3295522abf6a254c46 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Fri, 28 Oct 2016 11:55:04 +0200 Subject: [PATCH] Extracting Nrgs.Hub.Monitoring into Greentube.Monitoring --- .gitignore | 6 + GlobalAssemblyInfo.cs | 10 + Greentube.Monitoring.sln | 71 ++ Greentube.Monitoring.sln.DotSettings | 3 + global.json | 6 + .../Greentube.Monitoring.MongoDB.xproj | 21 + .../MongoDbPingHealthCheckStrategy.cs | 45 ++ .../MongoDbPingMonitor.cs | 35 + .../Properties/AssemblyInfo.cs | 6 + src/Greentube.Monitoring.MongoDB/project.json | 22 + .../Greentube.Monitoring.Redis.xproj | 21 + .../Properties/AssemblyInfo.cs | 6 + .../RedisPingHealthCheckStrategy.cs | 46 ++ .../RedisPingMonitor.cs | 45 ++ src/Greentube.Monitoring.Redis/project.json | 27 + .../Greentube.Monitoring.xproj | 21 + .../HttpResourceMonitor.cs | 39 ++ ...ttpSuccessStatusCodeHealthCheckStrategy.cs | 47 ++ .../IHealthCheckStrategy.cs | 18 + .../IResourceCurrentState.cs | 33 + src/Greentube.Monitoring/IResourceMonitor.cs | 31 + .../IResourceMonitorConfiguration.cs | 47 ++ .../IResourceMonitorEvent.cs | 43 ++ .../IResourceStateCollector.cs | 44 ++ .../Properties/Annotations.cs | 632 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 11 + src/Greentube.Monitoring/ResourceMonitor.cs | 214 ++++++ .../ResourceMonitorConfiguration.cs | 76 +++ .../ResourceMonitorEventArgs.cs | 40 ++ .../ResourceStateCollector.cs | 173 +++++ .../Threading/AbstractStartable.cs | 43 ++ .../Threading/BoundedQueue.cs | 58 ++ .../Threading/IStartable.cs | 24 + src/Greentube.Monitoring/Threading/ITimer.cs | 22 + .../Threading/ITimerFactory.cs | 9 + .../Threading/TimerAdapter.cs | 34 + .../Threading/TimerAdapterFactory.cs | 24 + src/Greentube.Monitoring/project.json | 20 + .../Greentube.Monitoring.MongoDB.Tests.xproj | 22 + .../MongoDbPingHealthCheckStrategyTests.cs | 52 ++ .../MongoDbPingMonitorTests.cs | 81 +++ .../Properties/AssemblyInfo.cs | 18 + .../project.json | 23 + .../Greentube.Monitoring.Redis.Tests.xproj | 22 + .../Properties/AssemblyInfo.cs | 18 + .../RedisPingHealthCheckStrategyTests.cs | 71 ++ .../RedisPingMonitorTests.cs | 76 +++ .../project.json | 23 + .../BoundedQueueTests.cs | 43 ++ .../Greentube.Monitoring.Tests.xproj | 22 + .../Properties/AssemblyInfo.cs | 18 + .../ResourceMonitorTests.cs | 285 ++++++++ .../ResourceStateCollectorTests.cs | 307 +++++++++ test/Greentube.Monitoring.Tests/project.json | 23 + 54 files changed, 3177 insertions(+) create mode 100644 .gitignore create mode 100644 GlobalAssemblyInfo.cs create mode 100644 Greentube.Monitoring.sln create mode 100644 Greentube.Monitoring.sln.DotSettings create mode 100644 global.json create mode 100644 src/Greentube.Monitoring.MongoDB/Greentube.Monitoring.MongoDB.xproj create mode 100644 src/Greentube.Monitoring.MongoDB/MongoDbPingHealthCheckStrategy.cs create mode 100644 src/Greentube.Monitoring.MongoDB/MongoDbPingMonitor.cs create mode 100644 src/Greentube.Monitoring.MongoDB/Properties/AssemblyInfo.cs create mode 100644 src/Greentube.Monitoring.MongoDB/project.json create mode 100644 src/Greentube.Monitoring.Redis/Greentube.Monitoring.Redis.xproj create mode 100644 src/Greentube.Monitoring.Redis/Properties/AssemblyInfo.cs create mode 100644 src/Greentube.Monitoring.Redis/RedisPingHealthCheckStrategy.cs create mode 100644 src/Greentube.Monitoring.Redis/RedisPingMonitor.cs create mode 100644 src/Greentube.Monitoring.Redis/project.json create mode 100644 src/Greentube.Monitoring/Greentube.Monitoring.xproj create mode 100644 src/Greentube.Monitoring/HttpResourceMonitor.cs create mode 100644 src/Greentube.Monitoring/HttpSuccessStatusCodeHealthCheckStrategy.cs create mode 100644 src/Greentube.Monitoring/IHealthCheckStrategy.cs create mode 100644 src/Greentube.Monitoring/IResourceCurrentState.cs create mode 100644 src/Greentube.Monitoring/IResourceMonitor.cs create mode 100644 src/Greentube.Monitoring/IResourceMonitorConfiguration.cs create mode 100644 src/Greentube.Monitoring/IResourceMonitorEvent.cs create mode 100644 src/Greentube.Monitoring/IResourceStateCollector.cs create mode 100644 src/Greentube.Monitoring/Properties/Annotations.cs create mode 100644 src/Greentube.Monitoring/Properties/AssemblyInfo.cs create mode 100644 src/Greentube.Monitoring/ResourceMonitor.cs create mode 100644 src/Greentube.Monitoring/ResourceMonitorConfiguration.cs create mode 100644 src/Greentube.Monitoring/ResourceMonitorEventArgs.cs create mode 100644 src/Greentube.Monitoring/ResourceStateCollector.cs create mode 100644 src/Greentube.Monitoring/Threading/AbstractStartable.cs create mode 100644 src/Greentube.Monitoring/Threading/BoundedQueue.cs create mode 100644 src/Greentube.Monitoring/Threading/IStartable.cs create mode 100644 src/Greentube.Monitoring/Threading/ITimer.cs create mode 100644 src/Greentube.Monitoring/Threading/ITimerFactory.cs create mode 100644 src/Greentube.Monitoring/Threading/TimerAdapter.cs create mode 100644 src/Greentube.Monitoring/Threading/TimerAdapterFactory.cs create mode 100644 src/Greentube.Monitoring/project.json create mode 100644 test/Greentube.Monitoring.MongoDB.Tests/Greentube.Monitoring.MongoDB.Tests.xproj create mode 100644 test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingHealthCheckStrategyTests.cs create mode 100644 test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingMonitorTests.cs create mode 100644 test/Greentube.Monitoring.MongoDB.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Greentube.Monitoring.MongoDB.Tests/project.json create mode 100644 test/Greentube.Monitoring.Redis.Tests/Greentube.Monitoring.Redis.Tests.xproj create mode 100644 test/Greentube.Monitoring.Redis.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Greentube.Monitoring.Redis.Tests/RedisPingHealthCheckStrategyTests.cs create mode 100644 test/Greentube.Monitoring.Redis.Tests/RedisPingMonitorTests.cs create mode 100644 test/Greentube.Monitoring.Redis.Tests/project.json create mode 100644 test/Greentube.Monitoring.Tests/BoundedQueueTests.cs create mode 100644 test/Greentube.Monitoring.Tests/Greentube.Monitoring.Tests.xproj create mode 100644 test/Greentube.Monitoring.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Greentube.Monitoring.Tests/ResourceMonitorTests.cs create mode 100644 test/Greentube.Monitoring.Tests/ResourceStateCollectorTests.cs create mode 100644 test/Greentube.Monitoring.Tests/project.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c782ee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Visual studio +.vs/ +*.user +bin/ +obj/ +project.lock.json \ No newline at end of file diff --git a/GlobalAssemblyInfo.cs b/GlobalAssemblyInfo.cs new file mode 100644 index 0000000..4a7a853 --- /dev/null +++ b/GlobalAssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Reflection; + +[assembly: AssemblyCompany("Greentube Internet Entertainment")] +[assembly: AssemblyCopyright("Copyright © Greentube Internet Entertainment Solutions GmbH 2016")] + +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] + +// Replaced by TeamCity's Build Feature: Assembly Info Patcher +[assembly: AssemblyInformationalVersion("branch - 0000000000000000000000000000000000000000")] diff --git a/Greentube.Monitoring.sln b/Greentube.Monitoring.sln new file mode 100644 index 0000000..09891df --- /dev/null +++ b/Greentube.Monitoring.sln @@ -0,0 +1,71 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Greentube.Monitoring", "src\Greentube.Monitoring\Greentube.Monitoring.xproj", "{2BB29E61-02DD-4745-A3A2-145711D5FFB8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{843C8ED0-4792-40E0-8903-A3E5FF74A0A3}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Greentube.Monitoring.MongoDB", "src\Greentube.Monitoring.MongoDB\Greentube.Monitoring.MongoDB.xproj", "{5D358A61-8266-4739-845D-E6E973B71995}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Greentube.Monitoring.Redis", "src\Greentube.Monitoring.Redis\Greentube.Monitoring.Redis.xproj", "{78A6706F-599A-4CBD-B867-C6FE053788D3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AE749C50-94C1-445C-B13F-E9D19372CBDB}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Greentube.Monitoring.MongoDB.Tests", "test\Greentube.Monitoring.MongoDB.Tests\Greentube.Monitoring.MongoDB.Tests.xproj", "{3AB84CBA-75AF-4027-A398-0AB8CD4C004E}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Greentube.Monitoring.Redis.Tests", "test\Greentube.Monitoring.Redis.Tests\Greentube.Monitoring.Redis.Tests.xproj", "{FF559CD8-5FFE-4460-A6BC-84F8F7A05CB7}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Greentube.Monitoring.Tests", "test\Greentube.Monitoring.Tests\Greentube.Monitoring.Tests.xproj", "{06356707-146F-48F7-9C18-7E5D3D67E765}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{11D063CA-B13E-4EFB-9000-A47BA4B2AEC7}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + global.json = global.json + GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2BB29E61-02DD-4745-A3A2-145711D5FFB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BB29E61-02DD-4745-A3A2-145711D5FFB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BB29E61-02DD-4745-A3A2-145711D5FFB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BB29E61-02DD-4745-A3A2-145711D5FFB8}.Release|Any CPU.Build.0 = Release|Any CPU + {5D358A61-8266-4739-845D-E6E973B71995}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D358A61-8266-4739-845D-E6E973B71995}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D358A61-8266-4739-845D-E6E973B71995}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D358A61-8266-4739-845D-E6E973B71995}.Release|Any CPU.Build.0 = Release|Any CPU + {78A6706F-599A-4CBD-B867-C6FE053788D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78A6706F-599A-4CBD-B867-C6FE053788D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78A6706F-599A-4CBD-B867-C6FE053788D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78A6706F-599A-4CBD-B867-C6FE053788D3}.Release|Any CPU.Build.0 = Release|Any CPU + {3AB84CBA-75AF-4027-A398-0AB8CD4C004E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AB84CBA-75AF-4027-A398-0AB8CD4C004E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AB84CBA-75AF-4027-A398-0AB8CD4C004E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AB84CBA-75AF-4027-A398-0AB8CD4C004E}.Release|Any CPU.Build.0 = Release|Any CPU + {FF559CD8-5FFE-4460-A6BC-84F8F7A05CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF559CD8-5FFE-4460-A6BC-84F8F7A05CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF559CD8-5FFE-4460-A6BC-84F8F7A05CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF559CD8-5FFE-4460-A6BC-84F8F7A05CB7}.Release|Any CPU.Build.0 = Release|Any CPU + {06356707-146F-48F7-9C18-7E5D3D67E765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06356707-146F-48F7-9C18-7E5D3D67E765}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06356707-146F-48F7-9C18-7E5D3D67E765}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06356707-146F-48F7-9C18-7E5D3D67E765}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2BB29E61-02DD-4745-A3A2-145711D5FFB8} = {843C8ED0-4792-40E0-8903-A3E5FF74A0A3} + {5D358A61-8266-4739-845D-E6E973B71995} = {843C8ED0-4792-40E0-8903-A3E5FF74A0A3} + {78A6706F-599A-4CBD-B867-C6FE053788D3} = {843C8ED0-4792-40E0-8903-A3E5FF74A0A3} + {3AB84CBA-75AF-4027-A398-0AB8CD4C004E} = {AE749C50-94C1-445C-B13F-E9D19372CBDB} + {FF559CD8-5FFE-4460-A6BC-84F8F7A05CB7} = {AE749C50-94C1-445C-B13F-E9D19372CBDB} + {06356707-146F-48F7-9C18-7E5D3D67E765} = {AE749C50-94C1-445C-B13F-E9D19372CBDB} + EndGlobalSection +EndGlobal diff --git a/Greentube.Monitoring.sln.DotSettings b/Greentube.Monitoring.sln.DotSettings new file mode 100644 index 0000000..8db7635 --- /dev/null +++ b/Greentube.Monitoring.sln.DotSettings @@ -0,0 +1,3 @@ + + Greentube.Monitoring + True \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..3e3cf4d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-preview2-003131" + } +} diff --git a/src/Greentube.Monitoring.MongoDB/Greentube.Monitoring.MongoDB.xproj b/src/Greentube.Monitoring.MongoDB/Greentube.Monitoring.MongoDB.xproj new file mode 100644 index 0000000..dfc5a0b --- /dev/null +++ b/src/Greentube.Monitoring.MongoDB/Greentube.Monitoring.MongoDB.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 5d358a61-8266-4739-845d-e6e973b71995 + Greentube.Monitoring.MongoDB + .\obj + .\bin\ + v4.6.1 + + + + 2.0 + + + diff --git a/src/Greentube.Monitoring.MongoDB/MongoDbPingHealthCheckStrategy.cs b/src/Greentube.Monitoring.MongoDB/MongoDbPingHealthCheckStrategy.cs new file mode 100644 index 0000000..dd7ea34 --- /dev/null +++ b/src/Greentube.Monitoring.MongoDB/MongoDbPingHealthCheckStrategy.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Greentube.Monitoring.MongoDB +{ + /// + /// Health Check using Ping command + /// + /// + /// Not suitable for Replica Set: A replica set status shall be checked. + /// Ping could be successful in one of the nodes but the replica set is broken + /// + /// + public class MongoDbPingHealthCheckStrategy : IHealthCheckStrategy + { + private readonly IMongoDatabase _database; + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// + public MongoDbPingHealthCheckStrategy(IMongoDatabase database) + { + if (database == null) throw new ArgumentNullException(nameof(database)); + _database = database; + } + + /// + /// Checks connectivity with MongoDB via ping command + /// + /// The token. + /// + public async Task Check(CancellationToken token) + { + await _database.RunCommandAsync((Command)"{ping:1}", cancellationToken: token) + .ConfigureAwait(false); + + return true; + } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring.MongoDB/MongoDbPingMonitor.cs b/src/Greentube.Monitoring.MongoDB/MongoDbPingMonitor.cs new file mode 100644 index 0000000..53a5eaf --- /dev/null +++ b/src/Greentube.Monitoring.MongoDB/MongoDbPingMonitor.cs @@ -0,0 +1,35 @@ +using System.Linq; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Greentube.Monitoring.MongoDB +{ + /// + /// Monitors MongoDB connectivity + /// + /// + public sealed class MongoDbPingMonitor : ResourceMonitor + { + /// + /// Initializes a new instance of the class. + /// + /// The mongo database. + /// The logger. + /// The configuration. + /// Name of the resource (If not provided: will be based on Servers EndPoints). + /// if set to true [is critical]. + public MongoDbPingMonitor( + IMongoDatabase mongoDatabase, + ILogger logger, + ResourceMonitorConfiguration configuration, + string resourceName = null, + bool isCritical = true) + : base(resourceName ?? "MongoDB:" + string.Join(",", mongoDatabase.Client.Settings.Servers.Select(e => e.ToString())), + new MongoDbPingHealthCheckStrategy(mongoDatabase), + configuration, + logger, + isCritical) + { + } + } +} diff --git a/src/Greentube.Monitoring.MongoDB/Properties/AssemblyInfo.cs b/src/Greentube.Monitoring.MongoDB/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6ff2728 --- /dev/null +++ b/src/Greentube.Monitoring.MongoDB/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyProduct("Greentube.Monitoring.MongoDB")] + +[assembly: Guid("5d358a61-8266-4739-845d-e6e973b71995")] diff --git a/src/Greentube.Monitoring.MongoDB/project.json b/src/Greentube.Monitoring.MongoDB/project.json new file mode 100644 index 0000000..7362c55 --- /dev/null +++ b/src/Greentube.Monitoring.MongoDB/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-*", + + "buildOptions": { + "compile": "../../GlobalAssemblyInfo.cs", + "xmlDoc": true + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "mongocsharpdriver": "2.3.0-rc1", + "Greentube.Monitoring": "1.0.0" + }, + + "frameworks": { + "netstandard1.6": { + "imports": "dnxcore50" + }, + "net46": { + } + } +} diff --git a/src/Greentube.Monitoring.Redis/Greentube.Monitoring.Redis.xproj b/src/Greentube.Monitoring.Redis/Greentube.Monitoring.Redis.xproj new file mode 100644 index 0000000..290e190 --- /dev/null +++ b/src/Greentube.Monitoring.Redis/Greentube.Monitoring.Redis.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 78a6706f-599a-4cbd-b867-c6fe053788d3 + Greentube.Monitoring.Redis + .\obj + .\bin\ + v4.6.1 + + + + 2.0 + + + diff --git a/src/Greentube.Monitoring.Redis/Properties/AssemblyInfo.cs b/src/Greentube.Monitoring.Redis/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d9b6a5c --- /dev/null +++ b/src/Greentube.Monitoring.Redis/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyProduct("Greentube.Monitoring.Redis")] + +[assembly: Guid("78a6706f-599a-4cbd-b867-c6fe053788d3")] diff --git a/src/Greentube.Monitoring.Redis/RedisPingHealthCheckStrategy.cs b/src/Greentube.Monitoring.Redis/RedisPingHealthCheckStrategy.cs new file mode 100644 index 0000000..55b280e --- /dev/null +++ b/src/Greentube.Monitoring.Redis/RedisPingHealthCheckStrategy.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace Greentube.Monitoring.Redis +{ + /// + /// HealthCheck based on Ping command + /// + /// + public class RedisPingHealthCheckStrategy : IHealthCheckStrategy + { + private readonly IConnectionMultiplexer _multiplexer; + + /// + /// Initializes a new instance of the class. + /// + /// The multiplexer. + /// + public RedisPingHealthCheckStrategy(IConnectionMultiplexer multiplexer) + { + if (multiplexer == null) throw new ArgumentNullException(nameof(multiplexer)); + _multiplexer = multiplexer; + } + + /// + /// Checks the multiplexer is connected + /// + /// The token. + /// + public async Task Check(CancellationToken token) + { + if (_multiplexer.IsConnected) + return true; + + // If not connected, make a call to let exception bubble + await _multiplexer + .GetDatabase(0) + .PingAsync(CommandFlags.DemandMaster) + .ConfigureAwait(false); + + return true; // if it didn't throw, it's back up + } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring.Redis/RedisPingMonitor.cs b/src/Greentube.Monitoring.Redis/RedisPingMonitor.cs new file mode 100644 index 0000000..daca8e3 --- /dev/null +++ b/src/Greentube.Monitoring.Redis/RedisPingMonitor.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace Greentube.Monitoring.Redis +{ + /// + /// Redis ping monitor + /// + /// + public sealed class RedisPingMonitor : ResourceMonitor + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is critical Resource]. + /// The connection multiplexer. + /// The logger. + /// The configuration. + /// Name of the resource. + public RedisPingMonitor( + bool isCritical, + IConnectionMultiplexer connectionMultiplexer, + ILogger logger, + ResourceMonitorConfiguration configuration, + string resourceName = null) + : base(resourceName ?? GetResourceName(connectionMultiplexer), + new RedisPingHealthCheckStrategy(connectionMultiplexer), + configuration, + logger, + isCritical) + { + } + + private static string GetResourceName(IConnectionMultiplexer connectionMultiplexer) + { + if (connectionMultiplexer == null) throw new ArgumentNullException(nameof(connectionMultiplexer)); + + return "Redis:" + string.Join(",", + ConfigurationOptions.Parse(connectionMultiplexer.Configuration) + .EndPoints.Select(e => e.ToString())); + } + } +} diff --git a/src/Greentube.Monitoring.Redis/project.json b/src/Greentube.Monitoring.Redis/project.json new file mode 100644 index 0000000..a04303d --- /dev/null +++ b/src/Greentube.Monitoring.Redis/project.json @@ -0,0 +1,27 @@ +{ + "version": "1.0.0-*", + + "buildOptions": { + "compile": "../../GlobalAssemblyInfo.cs", + "xmlDoc": true + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "Greentube.Monitoring": "1.0.0" + }, + + "frameworks": { + "netstandard1.6": { + "imports": "dnxcore50", + "dependencies": { + "StackExchange.Redis": "1.1.604-alpha" + } + }, + "net46": { + "dependencies": { + "StackExchange.Redis": "1.0.488" + } + } + } +} diff --git a/src/Greentube.Monitoring/Greentube.Monitoring.xproj b/src/Greentube.Monitoring/Greentube.Monitoring.xproj new file mode 100644 index 0000000..28c3eb6 --- /dev/null +++ b/src/Greentube.Monitoring/Greentube.Monitoring.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 2bb29e61-02dd-4745-a3a2-145711d5ffb8 + Greentube.Monitoring + .\obj + .\bin\ + v4.6.1 + + + + 2.0 + + + diff --git a/src/Greentube.Monitoring/HttpResourceMonitor.cs b/src/Greentube.Monitoring/HttpResourceMonitor.cs new file mode 100644 index 0000000..025d79b --- /dev/null +++ b/src/Greentube.Monitoring/HttpResourceMonitor.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using Microsoft.Extensions.Logging; + +namespace Greentube.Monitoring +{ + /// + /// Health check based on successful status code + /// + /// + [PublicAPI] + public class HttpResourceMonitor : ResourceMonitor + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the resource. + /// The health check endpoint. + /// The HTTP client. + /// The configuration. + /// The logger. + /// if set to true [is critical]. + public HttpResourceMonitor( + string resourceName, + Uri healthCheckEndpoint, + HttpClient httpClient, + ResourceMonitorConfiguration configuration, + ILogger logger, + bool isCritical = false) + : base( + resourceName, + new HttpSuccessStatusCodeHealthCheckStrategy(httpClient, healthCheckEndpoint), + configuration, + logger, + isCritical) + { + } + } +} diff --git a/src/Greentube.Monitoring/HttpSuccessStatusCodeHealthCheckStrategy.cs b/src/Greentube.Monitoring/HttpSuccessStatusCodeHealthCheckStrategy.cs new file mode 100644 index 0000000..5ad9e69 --- /dev/null +++ b/src/Greentube.Monitoring/HttpSuccessStatusCodeHealthCheckStrategy.cs @@ -0,0 +1,47 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Greentube.Monitoring +{ + /// + /// HTTP health check based on successful status code + /// + /// + /// + public class HttpSuccessStatusCodeHealthCheckStrategy : IHealthCheckStrategy + { + private readonly HttpClient _client; + private readonly Uri _endpoint; + + /// + /// Initializes a new instance of the class. + /// + /// The client. + /// The endpoint. + /// + /// + public HttpSuccessStatusCodeHealthCheckStrategy(HttpClient client, Uri endpoint) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); + _client = client; + _endpoint = endpoint; + } + + /// + /// Makes a request and throws in case of error + /// + /// The token. + /// + public async Task Check(CancellationToken token) + { + var check = await _client.GetAsync(_endpoint, token) + .ConfigureAwait(false); + + check.EnsureSuccessStatusCode(); // Basic Health-check should return 200 + return true; + } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/IHealthCheckStrategy.cs b/src/Greentube.Monitoring/IHealthCheckStrategy.cs new file mode 100644 index 0000000..b4f0d93 --- /dev/null +++ b/src/Greentube.Monitoring/IHealthCheckStrategy.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Greentube.Monitoring +{ + /// + /// Health Check Strategy + /// + public interface IHealthCheckStrategy + { + /// + /// Health check. + /// + /// The token. + /// true if resource is up or false (or throws in case there's context) when down + Task Check(CancellationToken token); + } +} diff --git a/src/Greentube.Monitoring/IResourceCurrentState.cs b/src/Greentube.Monitoring/IResourceCurrentState.cs new file mode 100644 index 0000000..4050dab --- /dev/null +++ b/src/Greentube.Monitoring/IResourceCurrentState.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Greentube.Monitoring +{ + /// + /// Represents an External Resources' current state + /// + /// + public interface IResourceCurrentState + { + /// + /// Gets a value indicating whether this resource is up. + /// + /// + /// true if this instance is up; otherwise, false. + /// + bool IsUp { get; } + /// + /// Gets the resource monitor which does the actual check and reports state + /// + /// + /// The resource monitor. + /// + IResourceMonitor ResourceMonitor { get; } + /// + /// Gets the Monitor Events for this resource + /// + /// + /// The monitor events. + /// + IEnumerable MonitorEvents { get; } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/IResourceMonitor.cs b/src/Greentube.Monitoring/IResourceMonitor.cs new file mode 100644 index 0000000..17148cc --- /dev/null +++ b/src/Greentube.Monitoring/IResourceMonitor.cs @@ -0,0 +1,31 @@ +using System; +using Greentube.Monitoring.Threading; + +namespace Greentube.Monitoring +{ + /// + /// Monitors the state of an external resource + /// + public interface IResourceMonitor : IStartable + { + /// + /// Gets the name of the resource. + /// + /// + /// The name of the resource. + /// + string ResourceName { get; } + /// + /// Gets a value indicating whether this resource is critical to the functioning of the system + /// + /// + /// true if this instance is critical; otherwise, false. + /// + [PublicAPI] + bool IsCritical { get; } + /// + /// Occurs when a verification is executed + /// + event EventHandler MonitoringEvent; + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/IResourceMonitorConfiguration.cs b/src/Greentube.Monitoring/IResourceMonitorConfiguration.cs new file mode 100644 index 0000000..d3dc4b4 --- /dev/null +++ b/src/Greentube.Monitoring/IResourceMonitorConfiguration.cs @@ -0,0 +1,47 @@ +using System; + +namespace Greentube.Monitoring +{ + /// + /// Resource Monitor configuration + /// + /// + public interface IResourceMonitorConfiguration + { + /// + /// Gets or sets the interval when Resource is Down + /// + /// + /// The interval when down. + /// + TimeSpan IntervalWhenDown { get; } + + /// + /// Gets or sets the interval when Resource is up. + /// + /// + /// The interval when up. + /// + TimeSpan IntervalWhenUp { get; } + + /// + /// Gets or sets the timeout. + /// + /// + /// The timeout. + /// + TimeSpan Timeout { get; } + + /// + /// Gets or sets a value indicating whether [run first check synchronously]. + /// + /// + /// When running integration tests, waiting for Timers to trigger the Monitor delays the tests + /// This flag instructs the Monitors to run the first check synchronously (Starting the Monitor becomes a blocking call) + /// + /// + /// true if [run first check synchronously]; otherwise, false. + /// + bool RunFirstCheckSynchronously { get; } + } +} diff --git a/src/Greentube.Monitoring/IResourceMonitorEvent.cs b/src/Greentube.Monitoring/IResourceMonitorEvent.cs new file mode 100644 index 0000000..2e7124e --- /dev/null +++ b/src/Greentube.Monitoring/IResourceMonitorEvent.cs @@ -0,0 +1,43 @@ +using System; + +namespace Greentube.Monitoring +{ + /// + /// An event raised by a Resource Monitor + /// + /// + public interface IResourceMonitorEvent + { + /// + /// Gets the verification time UTC. + /// + /// + /// The verification time UTC. + /// + [PublicAPI] + DateTime VerificationTimeUtc { get; } + /// + /// Gets a value indicating whether this resource is up. + /// + /// + /// true if this instance is up; otherwise, false. + /// + bool IsUp { get; } + /// + /// Gets the latency of the health check event + /// + /// + /// The latency. + /// + [PublicAPI] + TimeSpan Latency { get; } + /// + /// Gets the exception, in case one was thrown. + /// + /// + /// The exception. + /// + [PublicAPI] + Exception Exception { get; } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/IResourceStateCollector.cs b/src/Greentube.Monitoring/IResourceStateCollector.cs new file mode 100644 index 0000000..2b2d4bc --- /dev/null +++ b/src/Greentube.Monitoring/IResourceStateCollector.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Greentube.Monitoring.Threading; + +namespace Greentube.Monitoring +{ + /// + /// Resource State Collector + /// + /// + /// Collects from its + /// + /// + /// + /// + public interface IResourceStateCollector : IStartable + { + /// + /// Gets the maximum state per resource. + /// + /// + /// The maximum state per resource. + /// + [PublicAPI] + int MaxStatePerResource { get; } + + /// + /// Gets the Collected Resources states + /// + /// + IEnumerable GetStates(); + + /// + /// Adds a new Monitor to the Collector + /// + /// The monitor. + void AddMonitor(IResourceMonitor resourceMonitor); + + /// + /// Removes a monitor from the Collector + /// + /// The monitor. + void RemoveMonitor(IResourceMonitor resourceMonitor); + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/Properties/Annotations.cs b/src/Greentube.Monitoring/Properties/Annotations.cs new file mode 100644 index 0000000..a38dba3 --- /dev/null +++ b/src/Greentube.Monitoring/Properties/Annotations.cs @@ -0,0 +1,632 @@ +using System; +// ReSharper disable NotNullMemberIsNotInitialized + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedParameter.Local +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +// ReSharper disable once CheckNamespace +namespace Greentube.Monitoring +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so the check for null is necessary before its usage + /// + /// + /// [CanBeNull] public object Test() { return null; } + /// public void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field)] + internal sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element could never be null + /// + /// + /// [NotNull] public object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field)] + internal sealed class NotNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds string by format pattern and (optional) arguments. + /// Parameter, which contains format string, should be given in constructor. The format string + /// should be in -like form + /// + /// + /// [StringFormatMethod("message")] + /// public void ShowError(string message, params object[] args) { /* do something */ } + /// public void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method)] + internal sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as format-string + /// + public StringFormatMethodAttribute(string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + public string FormatParameterName { get; private set; } + } + + /// + /// Indicates that the function argument should be string literal and match one + /// of the parameters of the caller function. For example, ReSharper annotates + /// the parameter of + /// + /// + /// public void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements + /// interface + /// and this method is used to notify that some property value changed + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// internal class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// private string _name; + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] + internal sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; private set; } + } + + /// + /// Describes dependency between method input and output + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If method has single input parameter, it's name could be omitted.
+ /// Using halt (or void/nothing, which is the same) + /// for method output means that the methos doesn't return normally.
+ /// canbenull annotation is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, + /// or use single attribute with rows separated by semicolon.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("halt <= condition: false")] + /// public void Assert(bool condition, string text) // regular assertion method + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("s:null=>false; =>true,result:notnull; =>false, result:null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + internal sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) + { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + public string Contract { get; private set; } + public bool ForceFullStates { get; private set; } + } + + /// + /// Indicates that marked element should be localized or not + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// internal class Foo { + /// private string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] + internal sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; private set; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// class UsesNoEquality { + /// public void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Interface | AttributeTargets.Class | + AttributeTargets.Struct)] + internal sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// internal class ComponentAttribute : Attribute { } + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// internal class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + internal sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] + public Type BaseType { get; private set; } + } + + /// + /// Indicates that the marked symbol is used implicitly + /// (e.g. via reflection, in external library), so this symbol + /// will not be marked as unused (as well as by other usage inspections) + /// + [AttributeUsage(AttributeTargets.All), MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + internal sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) + { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) + { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) + { } + + public UsedImplicitlyAttribute( + ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; private set; } + public ImplicitUseTargetFlags TargetFlags { get; private set; } + } + + /// + /// Should be used on attributes and causes ReSharper + /// to not mark symbols marked with such attributes as unused + /// (as well as by other usage inspections) + /// + [AttributeUsage(AttributeTargets.Class)] + internal sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) + { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) + { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) + { } + + public MeansImplicitUseAttribute( + ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] + public ImplicitUseKindFlags UseKindFlags { get; private set; } + [UsedImplicitly] + public ImplicitUseTargetFlags TargetFlags { get; private set; } + } + + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used + Access = 1, + /// Indicates implicit assignment to a member + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type + InstantiatedNoFixedConstructorSignature = 8 + } + + /// + /// Specify what is considered used implicitly + /// when marked with + /// or + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of entity marked with attribute are considered used + Members = 2, + /// Entity marked with attribute and all its members considered used + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API + /// which should not be removed and so is treated as used + /// + [MeansImplicitUse] + internal sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [NotNull] + public string Comment { get; private set; } + } + + /// + /// Tells code analysis engine if the parameter is completely handled + /// when the invoked method is on stack. If the parameter is a delegate, + /// indicates that delegate is executed while the method is executed. + /// If the parameter is an enumerable, indicates that it is enumerated + /// while the method is executed + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class InstantHandleAttribute : Attribute { } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute + /// + /// + /// [Pure] private int Multiply(int x, int y) { return x * y; } + /// public void Foo() { + /// const int a = 2, b = 2; + /// Multiply(a, b); // Waring: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] + internal sealed class PureAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder + /// within a web project. Path can be relative or absolute, + /// starting from web root (~) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + public PathReferenceAttribute([PathReference] string basePath) + { + BasePath = basePath; + } + + [NotNull] + public string BasePath { get; private set; } + } + + // ASP.NET MVC attributes + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute(string format) { } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute(string format) { } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute(string format) { } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute(string format) { } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute(string format) { } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute(string format) { } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String) + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + internal sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [NotNull] + public string AnonymousProperty { get; private set; } + } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class AspMvcAreaAttribute : PathReferenceAttribute + { + public AspMvcAreaAttribute() { } + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [NotNull] + public string AnonymousProperty { get; private set; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that + /// the parameter is an MVC controller. If applied to a method, + /// the MVC controller name is calculated implicitly from the context. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String) + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + internal sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [NotNull] + public string AnonymousProperty { get; private set; } + } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC Master. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(String, String) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC model type. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(String, Object) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that + /// the parameter is an MVC partial view. If applied to a method, + /// the MVC partial view name is calculated implicitly from the context. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String) + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + internal sealed class AspMvcPartialViewAttribute : PathReferenceAttribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling all inspections + /// for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + internal sealed class AspMvcSupressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String) + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object) + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + internal sealed class AspMvcViewAttribute : PathReferenceAttribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + internal sealed class AspMvcActionSelectorAttribute : Attribute { } + + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Field)] + internal sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] + public string Name { get; private set; } + } + + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | + AttributeTargets.Property)] + internal sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] + public string Name { get; private set; } + } + + // Razor attributes + + /// + /// Razor attribute. Indicates that a parameter or a method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String) + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + internal sealed class RazorSectionAttribute : Attribute { } +} diff --git a/src/Greentube.Monitoring/Properties/AssemblyInfo.cs b/src/Greentube.Monitoring/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a56f22a --- /dev/null +++ b/src/Greentube.Monitoring/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyProduct("Greentube.Monitoring")] + +[assembly: Guid("2bb29e61-02dd-4745-a3a2-145711d5ffb8")] + +[assembly: InternalsVisibleTo("Greentube.Monitoring.Tests")] +[assembly: InternalsVisibleTo("Greentube.Monitoring.Redis.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Greentube.Monitoring/ResourceMonitor.cs b/src/Greentube.Monitoring/ResourceMonitor.cs new file mode 100644 index 0000000..726291c --- /dev/null +++ b/src/Greentube.Monitoring/ResourceMonitor.cs @@ -0,0 +1,214 @@ +using System; +using System.Diagnostics; +using System.Threading; +using Greentube.Monitoring.Threading; +using Microsoft.Extensions.Logging; + +namespace Greentube.Monitoring +{ + /// + /// Resource Monitor: when started, raises events reporting the status of a resource + /// + /// + /// + public class ResourceMonitor : AbstractStartable, IResourceMonitor, IDisposable + { + private readonly object _verificationLock = new object(); + private readonly ITimer _timer; + private readonly ILogger _logger; + private readonly IHealthCheckStrategy _verificationStrategy; + + /// + /// Gets the configuration. + /// + /// + /// The configuration. + /// + [PublicAPI] + public IResourceMonitorConfiguration Configuration { get; } + + /// + /// Gets the name of the resource. + /// + /// + /// The name of the resource. + /// + public string ResourceName { get; } + + /// + /// Gets a value indicating whether this resource is critical to the functioning of the system + /// + /// + /// true if this instance is critical; otherwise, false. + /// + public bool IsCritical { get; } + + /// + /// Occurs when a verification is executed + /// + public event EventHandler MonitoringEvent; + + /// + /// Initializes a new instance of the class. + /// + /// Name of the resource. + /// The verify strategy. + /// The configuration. + /// The logger. + /// if set to true [is critical]. + [PublicAPI] + public ResourceMonitor( + string resourceName, + IHealthCheckStrategy verificationStrategy, + IResourceMonitorConfiguration configuration, + ILogger logger, + bool isCritical = false) + : this(resourceName, + verificationStrategy, + configuration, + logger, + isCritical, + new TimerAdapterFactory()) + { + } + + internal ResourceMonitor( + string resourceName, + IHealthCheckStrategy verificationStrategy, + IResourceMonitorConfiguration configuration, + ILogger logger, + bool isCritical, + ITimerFactory timerFactory) + { + if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); + if (verificationStrategy == null) throw new ArgumentNullException(nameof(verificationStrategy)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (timerFactory == null) throw new ArgumentNullException(nameof(timerFactory)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + _verificationStrategy = verificationStrategy; + _logger = logger; + ResourceName = resourceName; + Configuration = configuration; + IsCritical = isCritical; + + _timer = timerFactory.Create(OnTimer); + if (_timer == null) + throw new InvalidOperationException("Expected TimerFactory to return a Timer."); + } + + /// + /// Starts to Monitor the resource + /// + protected override void DoStart() + { + if (Configuration.RunFirstCheckSynchronously) + { + _logger.LogInformation("Starting ResourceMonitor - Executing the first check for: {ResourceName} synchronously", ResourceName); + Verify(); + } + else + { + var period = Configuration.IntervalWhenUp; + _logger.LogInformation("Starting ResourceMonitor - Scheduling the first check for: {ResourceName} in {Period}", ResourceName, period); + + _timer.Change(period, period); + } + + } + + /// + /// Stops monitoring the resource + /// + protected override void DoStop() + { + _logger.LogInformation("Stopping ResourceMonitor: {ResourceName}", ResourceName); + _timer.Change(Timeout.Infinite, Timeout.Infinite); + } + + private void OnTimer(object _) + { + // If the timer is faster than the verification, skip the check + if (Monitor.TryEnter(_verificationLock)) + { + try + { + Verify(); + } + finally + { + Monitor.Exit(_verificationLock); + } + } + } + + internal void Verify() // Internal for testability + { + var sw = Stopwatch.StartNew(); + + var evt = CreateVerificationEvent(); + + sw.Stop(); + evt.Latency = sw.Elapsed; + + var period = evt.IsUp + ? Configuration.IntervalWhenUp + : Configuration.IntervalWhenDown; + + _timer.Change(period, period); + + OnMonitoringEvent(evt); + } + + private ResourceMonitorEventArgs CreateVerificationEvent() + { + var evt = new ResourceMonitorEventArgs(); + var timedOut = false; + try + { + var source = new CancellationTokenSource(Configuration.Timeout); + + var verificationTask = _verificationStrategy.Check(source.Token); + timedOut = !verificationTask + .Wait(Configuration.Timeout) + || source.Token.IsCancellationRequested; + + // It's up if it didn't timeout and the check was OK. + evt.IsUp = !timedOut && verificationTask.Result; + } + catch (Exception ex) + { + evt.IsUp = false; + evt.Exception = ex; + } + finally + { + if (timedOut) // To avoid throwing from the try block + evt.Exception = new TimeoutException(); + } + + return evt; + } + + private void OnMonitoringEvent(ResourceMonitorEventArgs e) + { + try + { + MonitoringEvent?.Invoke(this, e); + } + catch (Exception ex) + { + _logger.LogCritical(0, ex, "The event handler has thrown an exception. The Exception will be re-thrown and the Monitoring will stop."); + throw; + } + } + + /// + /// Disposes the inner timer, stopping the resource monitoring + /// + public void Dispose() + { + _timer.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/ResourceMonitorConfiguration.cs b/src/Greentube.Monitoring/ResourceMonitorConfiguration.cs new file mode 100644 index 0000000..1b45c25 --- /dev/null +++ b/src/Greentube.Monitoring/ResourceMonitorConfiguration.cs @@ -0,0 +1,76 @@ +using System; + +namespace Greentube.Monitoring +{ + /// + /// Configuration for the Resource Monitor + /// + public class ResourceMonitorConfiguration : IResourceMonitorConfiguration + { + /// + /// Gets or sets the interval when Resource is Down + /// + /// + /// The interval when down. + /// + public TimeSpan IntervalWhenDown { get; } + + /// + /// Gets or sets the interval when Resource is up. + /// + /// + /// The interval when up. + /// + public TimeSpan IntervalWhenUp { get; } + + /// + /// Gets or sets the timeout. + /// + /// + /// The timeout. + /// + public TimeSpan Timeout { get; } + + /// + /// Gets or sets a value indicating whether [run first check synchronously]. + /// + /// + /// When running integration tests, waiting for Timers to trigger the Monitor delays the tests + /// This flag instructs the Monitors to run the first check synchronously (Starting the Monitor becomes a blocking call) + /// + /// + /// true if [run first check synchronously]; otherwise, false. + /// + public bool RunFirstCheckSynchronously { get; } + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [run first check synchronously]. + /// The interval when down. Default: 10 seconds. + /// The interval when up. Default: 10 seconds. + /// The timeout. Default: Same as intervalWhenDown. + public ResourceMonitorConfiguration( + bool runFirstCheckSynchronously, + TimeSpan? intervalWhenDown = null, + TimeSpan? intervalWhenUp = null, + TimeSpan? timeout = null) + { + RunFirstCheckSynchronously = runFirstCheckSynchronously; + IntervalWhenDown = intervalWhenDown ?? TimeSpan.FromSeconds(10); + IntervalWhenUp = intervalWhenUp ?? TimeSpan.FromSeconds(20); + Timeout = timeout ?? IntervalWhenDown; + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + return $"IntervalWhenDown: {IntervalWhenDown}, IntervalWhenUp: {IntervalWhenUp}, Timeout: {Timeout}"; + } + } +} diff --git a/src/Greentube.Monitoring/ResourceMonitorEventArgs.cs b/src/Greentube.Monitoring/ResourceMonitorEventArgs.cs new file mode 100644 index 0000000..33757be --- /dev/null +++ b/src/Greentube.Monitoring/ResourceMonitorEventArgs.cs @@ -0,0 +1,40 @@ +using System; + +namespace Greentube.Monitoring +{ + /// + /// Event args raised by a Resource Monitor + /// + /// + public class ResourceMonitorEventArgs : EventArgs, IResourceMonitorEvent + { + /// + /// Gets the verification time UTC. + /// + /// + /// The verification time UTC. + /// + public DateTime VerificationTimeUtc { get; } = DateTime.UtcNow; + /// + /// Gets or sets a value indicating whether this instance is up. + /// + /// + /// true if this instance is up; otherwise, false. + /// + public bool IsUp { get; set; } + /// + /// Gets or sets the latency. + /// + /// + /// The latency. + /// + public TimeSpan Latency { get; set; } + /// + /// Gets or sets the exception. + /// + /// + /// The exception. + /// + public Exception Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/ResourceStateCollector.cs b/src/Greentube.Monitoring/ResourceStateCollector.cs new file mode 100644 index 0000000..0ec645e --- /dev/null +++ b/src/Greentube.Monitoring/ResourceStateCollector.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Greentube.Monitoring.Threading; +using Microsoft.Extensions.Logging; + +namespace Greentube.Monitoring +{ + /// + /// Keeps the last N events of each + /// + public class ResourceStateCollector : AbstractStartable, IResourceStateCollector + { + private readonly ILogger _logger; + private readonly ConcurrentDictionary _resourceStates; + + public int MaxStatePerResource { get; } + + public ResourceStateCollector( + IEnumerable initialMonitors, + int maxStatePerResource, + ILogger logger) + { + if (initialMonitors == null) throw new ArgumentNullException(nameof(initialMonitors)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger; + MaxStatePerResource = maxStatePerResource; + _resourceStates = new ConcurrentDictionary( + initialMonitors.ToDictionary(r => r, m => new ResourceCurrentState(m, maxStatePerResource))); + } + + /// + /// Adds a new Monitor to the Collector + /// + /// The monitor. + public void AddMonitor(IResourceMonitor resourceMonitor) + { + var resourceState = new ResourceCurrentState(resourceMonitor, MaxStatePerResource); + if (!_resourceStates.TryAdd(resourceMonitor, resourceState)) + { + _logger.LogError("Couldn't add {Monitor} to collector.", resourceMonitor); + return; + } + + if (IsRunning) + resourceState.Start(); + + _logger.LogTrace("Added Resource Monitor {Monitor}. Collector running state: {IsCollectorRunning}, Resource running state: {IsResourceRunning}", + resourceMonitor, IsRunning, resourceMonitor.IsRunning); + } + + /// + /// Removes a monitor from the Collector + /// + /// The monitor. + public void RemoveMonitor(IResourceMonitor resourceMonitor) + { + ResourceCurrentState resourceState; + if (_resourceStates.TryRemove(resourceMonitor, out resourceState)) + { + if (IsRunning) // No need to Stop it if we are not running + resourceState.Stop(); + + _logger.LogTrace("Removed Resource Monitor {Monitor}. Collector running state: {IsCollectorRunning}, Resource running state: {IsResourceRunning}", + resourceMonitor, IsRunning, resourceMonitor.IsRunning); + } + else + { + _logger.LogWarning("Tried to remove a Resource Monitor with no Resource State being collected."); + } + } + + protected override void DoStart() + { + foreach (var resourceState in _resourceStates) + { + resourceState.Value.Start(); + } + } + + protected override void DoStop() + { + foreach (var resourceMonitor in _resourceStates) + { + resourceMonitor.Value.Stop(); + } + } + + /// + /// Gets the latests Resource States available + /// + /// + public IEnumerable GetStates() + { + return _resourceStates.Values; + } + + /// + /// Private class to encapsulate Starting/subscribing and Stopping/unsubscribing + /// + /// + [DebuggerDisplay("Up: {IsUp} - Monitor: {ResourceMonitor.ResourceName}")] + private sealed class ResourceCurrentState : IResourceCurrentState + { + private readonly EventHandler _handler; + private readonly BoundedQueue _boundedQueue; + + /// + /// Uses the latest Event as an indication whether this Resource is Up or not + /// + /// + /// true if this instance is up; otherwise, false. + /// + public bool IsUp + { + get + { + var lastEvent = MonitorEvents.LastOrDefault(); + return lastEvent != null && lastEvent.IsUp; + } + } + + /// + /// Gets the Monitor Events for this resource + /// + /// + /// The monitor events. + /// + /// + public IEnumerable MonitorEvents => _boundedQueue; + /// + /// Gets the resource monitor which does the actual check and reports state + /// + /// + /// The resource monitor. + /// + public IResourceMonitor ResourceMonitor { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The resource monitor. + /// The maximum state per resource. + /// + public ResourceCurrentState(IResourceMonitor resourceMonitor, int maxStatePerResource) + { + var boundedQueue = new BoundedQueue(maxStatePerResource); + if (resourceMonitor == null) throw new ArgumentNullException(nameof(resourceMonitor)); + ResourceMonitor = resourceMonitor; + _boundedQueue = boundedQueue; + + _handler = (sender, @event) => + { + _boundedQueue.Enqueue(@event); + }; + } + + public void Start() + { + ResourceMonitor.MonitoringEvent += _handler; + ResourceMonitor.Start(); + } + + public void Stop() + { + ResourceMonitor.Stop(); + ResourceMonitor.MonitoringEvent -= _handler; + } + } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/Threading/AbstractStartable.cs b/src/Greentube.Monitoring/Threading/AbstractStartable.cs new file mode 100644 index 0000000..3581ef8 --- /dev/null +++ b/src/Greentube.Monitoring/Threading/AbstractStartable.cs @@ -0,0 +1,43 @@ +using System; + +namespace Greentube.Monitoring.Threading +{ + public abstract class AbstractStartable : IStartable + { + private readonly object _lock = new object(); + public bool IsRunning { get; private set; } + + /// + /// Starts this instance. + /// + /// This instance already in running mode. Consider Stopping it first. + public void Start() + { + lock (_lock) + { + if (IsRunning) + throw new InvalidOperationException("This instance already in running mode. Consider Stopping it first."); + DoStart(); + IsRunning = true; + } + } + + /// + /// Stops this instance. + /// + /// This instance is not in running mode. Consider Starting it first. + public void Stop() + { + lock (_lock) + { + if (!IsRunning) + throw new InvalidOperationException("This instance is not in running mode. Consider Starting it first."); + DoStop(); + IsRunning = false; + } + } + + protected abstract void DoStart(); + protected abstract void DoStop(); + } +} diff --git a/src/Greentube.Monitoring/Threading/BoundedQueue.cs b/src/Greentube.Monitoring/Threading/BoundedQueue.cs new file mode 100644 index 0000000..23d60d2 --- /dev/null +++ b/src/Greentube.Monitoring/Threading/BoundedQueue.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Greentube.Monitoring.Threading +{ + /// + /// Queue that drops items automatically when limit is reached + /// + /// + /// Allows you insert items indefinitely without running out of memory + /// + /// + /// + internal sealed class BoundedQueue : IEnumerable + { + private readonly object _dequeueLock = new object(); + private readonly ConcurrentQueue _concurrentQueue = new ConcurrentQueue(); + + private readonly int _maxSize; + + public int Count => _concurrentQueue.Count; + + public BoundedQueue(int maxSize) + { + if (maxSize <= 0) throw new ArgumentOutOfRangeException(nameof(maxSize)); + _maxSize = maxSize; + } + + public void Enqueue(T obj) + { + _concurrentQueue.Enqueue(obj); + + if (_concurrentQueue.Count <= _maxSize) + return; + + lock (_dequeueLock) + { + while (_concurrentQueue.Count > _maxSize) + { + T outObj; + _concurrentQueue.TryDequeue(out outObj); + } + } + } + + public IEnumerator GetEnumerator() + { + return _concurrentQueue.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Greentube.Monitoring/Threading/IStartable.cs b/src/Greentube.Monitoring/Threading/IStartable.cs new file mode 100644 index 0000000..531e00c --- /dev/null +++ b/src/Greentube.Monitoring/Threading/IStartable.cs @@ -0,0 +1,24 @@ +namespace Greentube.Monitoring.Threading +{ + /// + /// Start/Stop + /// + public interface IStartable + { + /// + /// Starts this instance. + /// + void Start(); + /// + /// Stops this instance. + /// + void Stop(); + /// + /// Gets a value indicating whether this instance is running. + /// + /// + /// true if this instance is running; otherwise, false. + /// + bool IsRunning { get; } + } +} diff --git a/src/Greentube.Monitoring/Threading/ITimer.cs b/src/Greentube.Monitoring/Threading/ITimer.cs new file mode 100644 index 0000000..000fe0e --- /dev/null +++ b/src/Greentube.Monitoring/Threading/ITimer.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Greentube.Monitoring.Threading +{ + /// + /// An abstraction for + /// + /// + /// To enable Unit testing on code depending on Timer + /// More members of Timer can be added as needed + /// + /// + /// + internal interface ITimer : IDisposable + { + [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global", Justification = "Part of the Timer signature")] + bool Change(TimeSpan dueTime, TimeSpan period); + [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global", Justification = "Part of the Timer signature")] + bool Change(int dueTime, int period); + } +} diff --git a/src/Greentube.Monitoring/Threading/ITimerFactory.cs b/src/Greentube.Monitoring/Threading/ITimerFactory.cs new file mode 100644 index 0000000..a6a1555 --- /dev/null +++ b/src/Greentube.Monitoring/Threading/ITimerFactory.cs @@ -0,0 +1,9 @@ +using System.Threading; + +namespace Greentube.Monitoring.Threading +{ + internal interface ITimerFactory + { + ITimer Create(TimerCallback callback); + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/Threading/TimerAdapter.cs b/src/Greentube.Monitoring/Threading/TimerAdapter.cs new file mode 100644 index 0000000..43cbb2d --- /dev/null +++ b/src/Greentube.Monitoring/Threading/TimerAdapter.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; + +namespace Greentube.Monitoring.Threading +{ + /// + /// Adapts to + /// + /// + internal sealed class TimerAdapter : ITimer + { + private readonly Timer _timer; + + public TimerAdapter(TimerCallback callback) + { + _timer = new Timer(callback, null, -1, -1); + } + + public bool Change(TimeSpan dueTime, TimeSpan period) + { + return _timer.Change(dueTime, period); + } + + public bool Change(int dueTime, int period) + { + return _timer.Change(dueTime, period); + } + + public void Dispose() + { + _timer.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Greentube.Monitoring/Threading/TimerAdapterFactory.cs b/src/Greentube.Monitoring/Threading/TimerAdapterFactory.cs new file mode 100644 index 0000000..cd1db96 --- /dev/null +++ b/src/Greentube.Monitoring/Threading/TimerAdapterFactory.cs @@ -0,0 +1,24 @@ +using System.Threading; + +namespace Greentube.Monitoring.Threading +{ + /// + /// Creates TimerAdapter + /// + /// + /// This class is thread-safe + /// + /// + internal sealed class TimerAdapterFactory : ITimerFactory + { + /// + /// Creates a Timer with the specified callback. + /// + /// The callback. + /// + public ITimer Create(TimerCallback callback) + { + return new TimerAdapter(callback); + } + } +} diff --git a/src/Greentube.Monitoring/project.json b/src/Greentube.Monitoring/project.json new file mode 100644 index 0000000..4974e60 --- /dev/null +++ b/src/Greentube.Monitoring/project.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0-*", + + "buildOptions": { + "compile": "../../GlobalAssemblyInfo.cs" + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "Microsoft.Extensions.Logging.Abstractions": "1.0.0" + }, + + "frameworks": { + "netstandard1.6": { + "imports": "dnxcore50" + }, + "net46": { + } + } +} diff --git a/test/Greentube.Monitoring.MongoDB.Tests/Greentube.Monitoring.MongoDB.Tests.xproj b/test/Greentube.Monitoring.MongoDB.Tests/Greentube.Monitoring.MongoDB.Tests.xproj new file mode 100644 index 0000000..4937bcc --- /dev/null +++ b/test/Greentube.Monitoring.MongoDB.Tests/Greentube.Monitoring.MongoDB.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 3ab84cba-75af-4027-a398-0ab8cd4c004e + Greentube.Monitoring.MongoDB.Tests + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingHealthCheckStrategyTests.cs b/test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingHealthCheckStrategyTests.cs new file mode 100644 index 0000000..af044a8 --- /dev/null +++ b/test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingHealthCheckStrategyTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using NSubstitute; +using Xunit; +using NSubstitute.ExceptionExtensions; + +namespace Greentube.Monitoring.MongoDB.Tests +{ + public class MongoDbPingHealthCheckStrategyTests + { + private class Fixture + { + public IMongoDatabase MongoDatabase { get; set; } = Substitute.For(); + + public MongoDbPingHealthCheckStrategy GetSut() + { + return new MongoDbPingHealthCheckStrategy(MongoDatabase); + } + } + + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public async Task Check_PingCommandSuccess_ReturnsTrue() + { + var target = _fixture.GetSut(); + var actual = await target.Check(CancellationToken.None); + Assert.True(actual); + } + + [Fact] + public async Task Check_DatabaseThrows_ExceptionBubbles() + { + _fixture.MongoDatabase.RunCommandAsync(Arg.Any>()) + .Throws(); + + var target = _fixture.GetSut(); + + await Assert.ThrowsAsync(() => target.Check(CancellationToken.None)); + } + + [Fact] + public void Constructor_NullMongoDatabase_ThrowsNullArgument() + { + _fixture.MongoDatabase = null; + Assert.Throws(() => _fixture.GetSut()); + } + } +} diff --git a/test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingMonitorTests.cs b/test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingMonitorTests.cs new file mode 100644 index 0000000..a539474 --- /dev/null +++ b/test/Greentube.Monitoring.MongoDB.Tests/MongoDbPingMonitorTests.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; +using NSubstitute; +using Xunit; + +namespace Greentube.Monitoring.MongoDB.Tests +{ + public sealed class MongoDbPingMonitorTests + { + private sealed class Fixture + { + public IMongoDatabase MongoDatabase { get; set; } = Substitute.For(); + public ILogger Logger { private get; set; } = Substitute.For>(); + public ResourceMonitorConfiguration ResourceMonitorConfiguration { private get; set; } = new ResourceMonitorConfiguration( + true, + TimeSpan.FromMilliseconds(0), + TimeSpan.FromMilliseconds(0), + TimeSpan.FromSeconds(2)); + + private bool IsCritical { get; } = true; + public string ResourceName { private get; set; } = "MongoDB 123"; + + public MongoDbPingMonitor GetSut() + { + return new MongoDbPingMonitor( + MongoDatabase, + Logger, + ResourceMonitorConfiguration, + ResourceName, + IsCritical); + } + } + + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public void ResourceName_IncludesEndPoints() + { + var expected = new[] + { + new MongoServerAddress("1.1.1.1", 1), + new MongoServerAddress("2.2.2.2", 2) + }; + + var client = Substitute.For(); + var settings = new MongoClientSettings { Servers = expected }; + client.Settings.Returns(settings); + _fixture.MongoDatabase.Client.Returns(client); + _fixture.ResourceName = null; + + var target = _fixture.GetSut(); + + foreach (var address in expected) + { + Assert.Contains($"{address.Host}:{address.Port}", target.ResourceName); + } + } + + [Fact] + public void Constructor_NullMongoDatabase_ThrowsNullArgument() + { + _fixture.MongoDatabase = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullLogger_ThrowsNullArgument() + { + _fixture.Logger = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullConfiguration_ThrowsNullArgument() + { + _fixture.ResourceMonitorConfiguration = null; + Assert.Throws(() => _fixture.GetSut()); + } + } +} diff --git a/test/Greentube.Monitoring.MongoDB.Tests/Properties/AssemblyInfo.cs b/test/Greentube.Monitoring.MongoDB.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7a3d32f --- /dev/null +++ b/test/Greentube.Monitoring.MongoDB.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Greentube.Monitoring.MongoDB.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3ab84cba-75af-4027-a398-0ab8cd4c004e")] diff --git a/test/Greentube.Monitoring.MongoDB.Tests/project.json b/test/Greentube.Monitoring.MongoDB.Tests/project.json new file mode 100644 index 0000000..a4eccb0 --- /dev/null +++ b/test/Greentube.Monitoring.MongoDB.Tests/project.json @@ -0,0 +1,23 @@ +{ + "buildOptions": { + "nowarn": [ "1591" ] + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.1" + } + } + } + }, + "version": "1.0.0", + "testRunner": "xunit", + "dependencies": { + "NSubstitute": "2.0.0-rc", + "Greentube.Monitoring.MongoDB": "1.0.0", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "xunit": "2.2.0-beta2-build3300" + } +} diff --git a/test/Greentube.Monitoring.Redis.Tests/Greentube.Monitoring.Redis.Tests.xproj b/test/Greentube.Monitoring.Redis.Tests/Greentube.Monitoring.Redis.Tests.xproj new file mode 100644 index 0000000..9760101 --- /dev/null +++ b/test/Greentube.Monitoring.Redis.Tests/Greentube.Monitoring.Redis.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ff559cd8-5ffe-4460-a6bc-84f8f7a05cb7 + Greentube.Monitoring.Redis.Tests + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Greentube.Monitoring.Redis.Tests/Properties/AssemblyInfo.cs b/test/Greentube.Monitoring.Redis.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..183be4c --- /dev/null +++ b/test/Greentube.Monitoring.Redis.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Greentube.Monitoring.Redis.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ff559cd8-5ffe-4460-a6bc-84f8f7a05cb7")] diff --git a/test/Greentube.Monitoring.Redis.Tests/RedisPingHealthCheckStrategyTests.cs b/test/Greentube.Monitoring.Redis.Tests/RedisPingHealthCheckStrategyTests.cs new file mode 100644 index 0000000..5c26c36 --- /dev/null +++ b/test/Greentube.Monitoring.Redis.Tests/RedisPingHealthCheckStrategyTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NSubstitute; +using StackExchange.Redis; +using Xunit; +using NSubstitute.ExceptionExtensions; + +namespace Greentube.Monitoring.Redis.Tests +{ + public class RedisPingHealthCheckStrategyTests + { + private class Fixture + { + public IConnectionMultiplexer Multiplexer { get; set; } = Substitute.For(); + + public RedisPingHealthCheckStrategy GetSut() + { + return new RedisPingHealthCheckStrategy(Multiplexer); + } + } + + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public async Task Check_IsConnectedTrue_ReturnsTrue() + { + _fixture.Multiplexer.IsConnected.Returns(true); + + var target = _fixture.GetSut(); + var actual = await target.Check(CancellationToken.None); + Assert.True(actual); + } + + [Fact] + public async Task Check_IsConnectedFalsePingSuccessful_ReturnsTrue() + { + _fixture.Multiplexer.IsConnected.Returns(false); + + _fixture.Multiplexer.GetDatabase(0) + .PingAsync(Arg.Any()) + .Returns(TimeSpan.FromMilliseconds(1)); + + var target = _fixture.GetSut(); + var actual = await target.Check(CancellationToken.None); + Assert.True(actual); + } + + [Fact] + public async Task Check_IsConnectedFalsePingThrows_ExceptionBubbles() + { + _fixture.Multiplexer.IsConnected.Returns(false); + + _fixture.Multiplexer + .GetDatabase(0) + .PingAsync(Arg.Any()) + .Throws(new ArithmeticException()); + + var target = _fixture.GetSut(); + + await Assert.ThrowsAsync(() => target.Check(CancellationToken.None)); + } + + [Fact] + public void Constructor_NullConnectionMultiplexer_ThrowsNullArgument() + { + _fixture.Multiplexer = null; + Assert.Throws(() => _fixture.GetSut()); + } + } +} diff --git a/test/Greentube.Monitoring.Redis.Tests/RedisPingMonitorTests.cs b/test/Greentube.Monitoring.Redis.Tests/RedisPingMonitorTests.cs new file mode 100644 index 0000000..16d22f7 --- /dev/null +++ b/test/Greentube.Monitoring.Redis.Tests/RedisPingMonitorTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Net; +using Microsoft.Extensions.Logging; +using NSubstitute; +using StackExchange.Redis; +using Xunit; + +namespace Greentube.Monitoring.Redis.Tests +{ + public sealed class RedisPingMonitorTests + { + private sealed class Fixture + { + public ResourceMonitorConfiguration ResourceMonitorConfiguration { private get; set; } = new ResourceMonitorConfiguration(true); + + public ILogger Logger { private get; set; } = Substitute.For>(); + + public IConnectionMultiplexer ConnectionMultiplexer { get; set; } = Substitute.For(); + + private bool IsCritical { get; } = false; + + public Fixture() + { + var options = new ConfigurationOptions { EndPoints = { "1.2.3.4" } }; + ConnectionMultiplexer.Configuration.Returns(options.ToString()); + } + + public RedisPingMonitor GetSut() + { + return new RedisPingMonitor( + IsCritical, + ConnectionMultiplexer, + Logger, + ResourceMonitorConfiguration); + } + } + + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public void ResourceName_IncludesEndPoints() + { + var loopback = IPAddress.Loopback.ToString(); + var broadcast = IPAddress.Broadcast.ToString(); + var options = new ConfigurationOptions { EndPoints = { loopback, broadcast } }; + + _fixture.ConnectionMultiplexer.Configuration.Returns(options.ToString()); + + var target = _fixture.GetSut(); + + Assert.Contains(loopback, target.ResourceName); + Assert.Contains(broadcast, target.ResourceName); + } + + [Fact] + public void Constructor_NullConnectionMultiplexer_ThrowsNullArgument() + { + _fixture.ConnectionMultiplexer = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullLogger_ThrowsNullArgument() + { + _fixture.Logger = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullConfiguration_ThrowsNullArgument() + { + _fixture.ResourceMonitorConfiguration = null; + Assert.Throws(() => _fixture.GetSut()); + } + } +} diff --git a/test/Greentube.Monitoring.Redis.Tests/project.json b/test/Greentube.Monitoring.Redis.Tests/project.json new file mode 100644 index 0000000..919dc7a --- /dev/null +++ b/test/Greentube.Monitoring.Redis.Tests/project.json @@ -0,0 +1,23 @@ +{ + "buildOptions": { + "nowarn": [ "1591" ] + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.1" + } + } + } + }, + "version": "1.0.0", + "testRunner": "xunit", + "dependencies": { + "NSubstitute": "2.0.0-rc", + "Greentube.Monitoring.Redis": "1.0.0", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "xunit": "2.2.0-beta2-build3300" + } +} diff --git a/test/Greentube.Monitoring.Tests/BoundedQueueTests.cs b/test/Greentube.Monitoring.Tests/BoundedQueueTests.cs new file mode 100644 index 0000000..e7f56bf --- /dev/null +++ b/test/Greentube.Monitoring.Tests/BoundedQueueTests.cs @@ -0,0 +1,43 @@ +using System.Linq; +using Greentube.Monitoring.Threading; +using Xunit; + +namespace Greentube.Monitoring.Tests +{ + public sealed class BoundedQueueTests + { + [Fact] + public void Enqueue_FullQueue_RemovesFirstItem() + { + const int maxSize = 1; + var first = new object(); + var second = new object(); + + var target = new BoundedQueue(maxSize); + + target.Enqueue(first); + target.Enqueue(second); + + Assert.Equal(maxSize, target.Count); + Assert.Equal(second, target.Single()); + } + + [Fact] + public void Enqueue_EnumeratesItems() + { + var numbers = Enumerable.Range(1, 10).ToList(); + + var maxSize = numbers.Count / 2; + var target = new BoundedQueue(maxSize); + + for (int i = 0; i < numbers.Count; i++) + { + target.Enqueue(numbers[i]); + + // Exact number of enqueued or maxSize + var expected = i + 1 <= maxSize ? i + 1 : maxSize; + Assert.Equal(expected, target.Count); + } + } + } +} diff --git a/test/Greentube.Monitoring.Tests/Greentube.Monitoring.Tests.xproj b/test/Greentube.Monitoring.Tests/Greentube.Monitoring.Tests.xproj new file mode 100644 index 0000000..8adc078 --- /dev/null +++ b/test/Greentube.Monitoring.Tests/Greentube.Monitoring.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 06356707-146f-48f7-9c18-7e5d3d67e765 + Greentube.Monitoring.Tests + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Greentube.Monitoring.Tests/Properties/AssemblyInfo.cs b/test/Greentube.Monitoring.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..33b4fc5 --- /dev/null +++ b/test/Greentube.Monitoring.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Greentube.Monitoring.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("06356707-146f-48f7-9c18-7e5d3d67e765")] diff --git a/test/Greentube.Monitoring.Tests/ResourceMonitorTests.cs b/test/Greentube.Monitoring.Tests/ResourceMonitorTests.cs new file mode 100644 index 0000000..272b1f2 --- /dev/null +++ b/test/Greentube.Monitoring.Tests/ResourceMonitorTests.cs @@ -0,0 +1,285 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Greentube.Monitoring.Threading; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Greentube.Monitoring.Tests +{ + public sealed class ResourceMonitorTests + { + private sealed class Fixture + { + private string ResourceName { get; } = "Resource Monitor Test"; + public IHealthCheckStrategy HealthCheckStrategy { get; set; } = Substitute.For(); + public ResourceMonitorConfiguration ResourceMonitorConfiguration { private get; set; } = new ResourceMonitorConfiguration( + true, + TimeSpan.FromMilliseconds(0), + TimeSpan.FromMilliseconds(0), + TimeSpan.FromSeconds(2)); + public ILogger Logger { get; set; } = Substitute.For>(); + private bool IsCritical { get; } = true; + public ITimerFactory TimerFactory { get; set; } = Substitute.For(); + public ITimer Timer { get; } = Substitute.For(); + + public Fixture() + { + TimerFactory.Create(Arg.Any()).Returns(Timer); + } + + public ResourceMonitor GetSut() + { + return new ResourceMonitor( + ResourceName, + HealthCheckStrategy, + ResourceMonitorConfiguration, + Logger, + IsCritical, + TimerFactory); + } + } + + private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(2); + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public void Verify_StrategyThrowsException_RaisesEventResourceDown() + { + var expected = new ArithmeticException(); + _fixture.HealthCheckStrategy.Check(Arg.Any()).Throws(expected); + + var target = _fixture.GetSut(); + + var evt = new ManualResetEventSlim(); + target.MonitoringEvent += (sender, args) => + { + Assert.False(args.IsUp); + var actual = Assert.IsType(args.Exception); + Assert.Same(expected, actual); + evt.Set(); + }; + + target.Verify(); + + Assert.True(evt.Wait(TimeSpan.FromSeconds(1))); + } + + [Fact] + public void Verify_BlockingVerification_ThrowsTimeoutException() + { + // Verification will take longer than Timeout + _fixture.ResourceMonitorConfiguration = new ResourceMonitorConfiguration(true, timeout: TimeSpan.Zero); + var nonStartedTask = new Task(() => false); + _fixture.HealthCheckStrategy.Check(Arg.Any()).Returns(nonStartedTask); + + var target = _fixture.GetSut(); + + var evt = new ManualResetEventSlim(); + target.MonitoringEvent += (sender, args) => + { + Assert.False(args.IsUp); // Has Timed out + Assert.IsType(args.Exception); + evt.Set(); + }; + + target.Verify(); + + Assert.True(evt.Wait(TimeSpan.FromSeconds(1))); + } + + [Fact] + public void Verify_SuccessCheck_RaisesEvent() + { + _fixture.HealthCheckStrategy.Check(Arg.Any()).Returns(Task.FromResult(true)); + + var evt = new ManualResetEventSlim(); + + var sut = _fixture.GetSut(); + var sw = Stopwatch.StartNew(); + var testStartTimeUtc = DateTime.UtcNow; + + sut.MonitoringEvent += (s, a) => + { + Assert.True(a.IsUp); + Assert.Null(a.Exception); + + sw.Stop(); + AssertEventTime(a, sw, testStartTimeUtc); + + evt.Set(); + }; + + sut.Verify(); + + _fixture.Logger + .DidNotReceive() + .Log(LogLevel.Critical, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>()); + + Assert.True(evt.Wait(WaitTimeout), "Callback not called or didn't not each event.Set()"); + } + + [Fact] + public void Verify_FailedCheck_RaisesEvent() + { + _fixture.HealthCheckStrategy.Check(Arg.Any()).Throws(); + + var evt = new ManualResetEventSlim(); + + var sut = _fixture.GetSut(); + var sw = Stopwatch.StartNew(); + var testStartTimeUtc = DateTime.UtcNow; + + sut.MonitoringEvent += (s, a) => + { + Assert.False(a.IsUp); + Assert.NotNull(a.Exception); + Assert.IsType(a.Exception); + + sw.Stop(); + AssertEventTime(a, sw, testStartTimeUtc); + + evt.Set(); + }; + + sut.Verify(); + + Assert.True(evt.Wait(WaitTimeout), "Callback not called or didn't not each event.Set()"); + } + + [Fact] + public void Start_TimerIsStarted() + { + var upInterval = TimeSpan.FromDays(10); + var downInterval = TimeSpan.FromDays(20); + + _fixture.ResourceMonitorConfiguration = new ResourceMonitorConfiguration(true, downInterval, upInterval); + _fixture.HealthCheckStrategy.Check(Arg.Any()).Returns(Task.FromResult(true)); + + using (var sut = _fixture.GetSut()) + sut.Start(); + + _fixture.Timer.Received(1).Change(upInterval, upInterval); + } + + [Fact] + public void Stop_TimerIsStopped() + { + using (var sut = _fixture.GetSut()) + { + sut.Start(); + sut.Stop(); + } + + _fixture.Timer.Received(1).Change(Timeout.Infinite, Timeout.Infinite); + } + + [Fact] + public void Verify_EventHandlerThrows_CriticalLogEntry() + { + using (var sut = _fixture.GetSut()) + { + sut.MonitoringEvent += (s, a) => + { + throw new ArithmeticException(); + }; + + Assert.Throws(() => sut.Verify()); + + _fixture.Logger + .Received(1) + .Log(LogLevel.Critical, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>()); + } + } + + [Fact] + public void Verify_FailCheck_IntervalChanges() + { + var upInterval = TimeSpan.FromDays(10); + var downInterval = TimeSpan.FromDays(20); + + _fixture.ResourceMonitorConfiguration = new ResourceMonitorConfiguration(true, downInterval, upInterval); + + _fixture.HealthCheckStrategy + .Check(Arg.Any()) + .Throws(); + + using (var sut = _fixture.GetSut()) + sut.Verify(); + + _fixture.Timer.Received(1).Change(downInterval, downInterval); + } + + [Fact] + public void Verify_TimeoutCheck_ReportsDown() + { + _fixture.ResourceMonitorConfiguration = new ResourceMonitorConfiguration(true, timeout: TimeSpan.FromMilliseconds(2)); + + _fixture.HealthCheckStrategy + .Check(Arg.Any()) + .Returns(new Task(() => true, new CancellationToken(true))); + + var evt = new ManualResetEventSlim(); + using (var sut = _fixture.GetSut()) + { + sut.MonitoringEvent += (s, a) => + { + Assert.False(a.IsUp); + evt.Set(); + }; + sut.Verify(); + } + + Assert.True(evt.Wait(WaitTimeout), "Callback not called or didn't not each event.Set()"); + } + + [Fact] + public void Dispose_TimerIsDisposed() + { + _fixture.GetSut().Dispose(); + _fixture.Timer.Received(1).Dispose(); + } + + [Fact] + public void Constructor_NullLogger_ThrowsNullArgument() + { + _fixture.Logger = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullHeathCheckStrategy_ThrowsNullArgument() + { + _fixture.HealthCheckStrategy = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullTimerFactory_ThrowsNullArgument() + { + _fixture.TimerFactory = null; + Assert.Throws(() => _fixture.GetSut()); + } + + [Fact] + public void Constructor_NullTimerFactoryReturn_ThrowsInvalidOperation() + { + _fixture.TimerFactory.Create(Arg.Any()).ReturnsNull(); + Assert.Throws(() => _fixture.GetSut()); + } + + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + private static void AssertEventTime(ResourceMonitorEventArgs evt, Stopwatch sw, DateTime testStartTimeUtc) + { + Assert.True(evt.Latency < sw.Elapsed); // The check must have taken less then the wrapping code calling it + Assert.True(evt.VerificationTimeUtc >= testStartTimeUtc, + "Verification can't have started before the test that invoked it."); + Assert.True(evt.VerificationTimeUtc <= DateTime.UtcNow, "The Verification can't have started after this assertion."); + } + } +} diff --git a/test/Greentube.Monitoring.Tests/ResourceStateCollectorTests.cs b/test/Greentube.Monitoring.Tests/ResourceStateCollectorTests.cs new file mode 100644 index 0000000..0724367 --- /dev/null +++ b/test/Greentube.Monitoring.Tests/ResourceStateCollectorTests.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Greentube.Monitoring.Tests +{ + public sealed class ResourceStateCollectorTests + { + private sealed class Fixture + { + public IResourceMonitor ResourceMonitorMock1 { get; } = Substitute.For(); + public IResourceMonitor ResourceMonitorMock2 { get; } = Substitute.For(); + public IEnumerable ResourceMonitorsMocks { get; set; } + private ILogger LoggerMock { get; } = Substitute.For>(); + public int MaxStatePerResource { private get; set; } = 100; + + public Fixture() + { + ResourceMonitorsMocks = new[] { ResourceMonitorMock1, ResourceMonitorMock2 }; + } + + public IResourceStateCollector GetSut() + { + return new ResourceStateCollector(ResourceMonitorsMocks, MaxStatePerResource, LoggerMock); + } + } + + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public void AddMonitor_CollectorNotStarted_DoesNotStartNewMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.AddMonitor(monitor); + // Assert + monitor.DidNotReceive().Start(); + } + + [Fact] + public void AddMonitor_CollectorStarted_StartsNewMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.Start(); + sut.AddMonitor(monitor); + // Assert + monitor.Received().Start(); + } + + [Fact] + public void AddMonitor_CollectorNotStarted_GetStatesReturnsNewMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.AddMonitor(monitor); + // Assert + var actual = sut.GetStates().FirstOrDefault(s => s.ResourceMonitor == monitor); + Assert.NotNull(actual); + } + + [Fact] + public void AddMonitor_CollectorStarted_GetStatesReturnsNewMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.Start(); + sut.AddMonitor(monitor); + // Assert + var actual = sut.GetStates().FirstOrDefault(s => s.ResourceMonitor == monitor); + Assert.NotNull(actual); + } + + [Fact] + public void RemoveMonitor_CollectorStarted_StopsResourceMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.Start(); + sut.AddMonitor(monitor); + sut.RemoveMonitor(monitor); + // Assert + monitor.Received().Start(); + monitor.Received().Stop(); + } + + [Fact] + public void RemoveMonitor_CollectorNotStarted_DoesNotStopResourceMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.AddMonitor(monitor); + sut.RemoveMonitor(monitor); + // Assert + monitor.DidNotReceive().Start(); + monitor.DidNotReceive().Stop(); + } + + [Fact] + public void RemoveMonitor_CollectorNotStarted_GetStatesDoesNotReturnMonitor() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.AddMonitor(monitor); + sut.RemoveMonitor(monitor); + // Assert + var actual = sut.GetStates().FirstOrDefault(s => s.ResourceMonitor == monitor); + Assert.Null(actual); + } + + [Fact] + public void RemoveMonitor_CollectorStarted_GetStatesDoesNotReturnMonitor() + { + var sut = _fixture.GetSut(); + sut.Start(); + var monitor = Substitute.For(); + // Act + sut.AddMonitor(monitor); + sut.RemoveMonitor(monitor); + // Assert + var actual = sut.GetStates().FirstOrDefault(s => s.ResourceMonitor == monitor); + Assert.Null(actual); + } + + [Fact] + public void RemoveMonitor_UnknownMonitor_RemoveIsNoOp() + { + var sut = _fixture.GetSut(); + var monitor = Substitute.For(); + // Act + sut.RemoveMonitor(monitor); + // Assert + var actual = sut.GetStates().FirstOrDefault(s => s.ResourceMonitor == monitor); + Assert.Null(actual); + } + + [Fact] + public void Start_StartsAllResourceMonitors() + { + var sut = _fixture.GetSut(); + + sut.Start(); + + Assert.All(_fixture.ResourceMonitorsMocks, monitor => monitor.Received(1).Start()); + } + + [Fact] + public void Start_SubscribesAllResourceMonitors() + { + var sut = _fixture.GetSut(); + sut.Start(); + + // Act + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), new ResourceMonitorEventArgs()); + _fixture.ResourceMonitorMock2.MonitoringEvent += Raise.EventWith(new object(), new ResourceMonitorEventArgs()); + + // Assert + Assert.Equal(2, sut.GetStates().Count()); + } + + [Fact] + public void Start_MarksAsRunning() + { + var sut = _fixture.GetSut(); + sut.Start(); + + Assert.True(sut.IsRunning); + } + + [Fact] + public void Stop_MarksAsNotRunning() + { + var sut = _fixture.GetSut(); + sut.Start(); + sut.Stop(); + + Assert.False(sut.IsRunning); + } + + [Fact] + public void Stop_StopsAllResourceMonitors() + { + var sut = _fixture.GetSut(); + + sut.Start(); + sut.Stop(); + + Assert.All(_fixture.ResourceMonitorsMocks, monitor => monitor.Received(1).Start()); + Assert.All(_fixture.ResourceMonitorsMocks, monitor => monitor.Received(1).Stop()); + } + + [Fact] + public void Stop_UnsubscribesAllResourceMonitors() + { + var sut = _fixture.GetSut(); + sut.Start(); + sut.Stop(); + + // Act + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), new ResourceMonitorEventArgs()); + _fixture.ResourceMonitorMock2.MonitoringEvent += Raise.EventWith(new object(), new ResourceMonitorEventArgs()); + + // Assert + Assert.Equal(0, sut.GetStates().SelectMany(s => s.MonitorEvents).Count()); + } + + [Fact] + public void GetStates_NoValidStates_ReturnsEmptyEnumerable() + { + var sut = _fixture.GetSut(); + + Assert.Empty(sut.GetStates().SelectMany(s => s.MonitorEvents)); + } + + [Fact] + public void GetStates_MoreThanMaxEvents_ReturnsOnlyMaxValue() + { + _fixture.MaxStatePerResource = 1; + var sut = _fixture.GetSut(); + sut.Start(); + + // Act + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), new ResourceMonitorEventArgs()); + _fixture.ResourceMonitorMock2.MonitoringEvent += Raise.EventWith(new object(), new ResourceMonitorEventArgs()); + + // Assert single event per ResourceState + Assert.Equal(sut.GetStates().Count(), sut.GetStates().SelectMany(s => s.MonitorEvents).Count()); + } + + [Fact] + public void GetStates_DownEvent_ReturnsResourceDown() + { + // Use only the first Resource + _fixture.ResourceMonitorsMocks = new[] { _fixture.ResourceMonitorMock1 }; + var sut = _fixture.GetSut(); + sut.Start(); + + var down = new ResourceMonitorEventArgs { IsUp = false }; + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), down); + + Assert.False(sut.GetStates().Single().IsUp); + } + + [Fact] + public void GetStates_UpEvent_ReturnsResourceUp() + { + // Use only the first Resource + _fixture.ResourceMonitorsMocks = new[] { _fixture.ResourceMonitorMock1 }; + var sut = _fixture.GetSut(); + sut.Start(); + + var down = new ResourceMonitorEventArgs { IsUp = true }; + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), down); + + Assert.True(sut.GetStates().Single().IsUp); + } + + [Fact] + public void GetStates_DownAndUpEvent_ReturnsResourceUp() + { + _fixture.ResourceMonitorsMocks = new[] { _fixture.ResourceMonitorMock1 }; + var sut = _fixture.GetSut(); + sut.Start(); + + var down = new ResourceMonitorEventArgs { IsUp = false }; + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), down); + + var up = new ResourceMonitorEventArgs { IsUp = true }; + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), up); + + Assert.True(sut.GetStates().Single().IsUp); + } + + [Fact] + public void GetStates_UpAndDownEvent_ReturnsResourceDown() + { + _fixture.ResourceMonitorsMocks = new[] { _fixture.ResourceMonitorMock1 }; + var sut = _fixture.GetSut(); + sut.Start(); + + var up = new ResourceMonitorEventArgs { IsUp = true }; + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), up); + + var down = new ResourceMonitorEventArgs { IsUp = false }; + _fixture.ResourceMonitorMock1.MonitoringEvent += Raise.EventWith(new object(), down); + + Assert.False(sut.GetStates().Single().IsUp); + } + + [Fact] + public void Constructor_NullResourceMonitor_ThrowsArgumentNull() + { + _fixture.ResourceMonitorsMocks = null; + Assert.Throws(() => _fixture.GetSut()); + } + } +} diff --git a/test/Greentube.Monitoring.Tests/project.json b/test/Greentube.Monitoring.Tests/project.json new file mode 100644 index 0000000..06bc235 --- /dev/null +++ b/test/Greentube.Monitoring.Tests/project.json @@ -0,0 +1,23 @@ +{ + "buildOptions": { + "nowarn": [ "1591" ] + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.1" + } + } + } + }, + "version": "1.0.0", + "testRunner": "xunit", + "dependencies": { + "NSubstitute": "2.0.0-rc", + "Greentube.Monitoring": "1.0.0", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "xunit": "2.2.0-beta2-build3300" + } +}