本文共 20508 字,大约阅读时间需要 68 分钟。
目录
本文介绍如何创建服务监视器应用程序,但它是什么?简单来说:它是一个允许监视网络中的服务并保存监视结果到数据库的应用程序,本例中为SQL Server。
我知道有很多工具可以提供这个功能,还有更好的工具,可以用钱买,但本文的意图是展示如何使用.NET核心能力来构建开发人员可以扩展以满足自定义要求的应用程序。
基本思想是这样的:有一个以无限方式运行的进程来监视主机,数据库和API; 将监控结果保存在SQL Server数据库中,然后我们可以为最终用户构建一个精美的UI并显示每个服务的状态,我们可以有很多目标进行监控,但最好是允许用户订阅特定服务而不是全部; 例如,DBA需要观察数据库服务器而不是API,开发人员需要观察开发数据库和API等。
还要考虑在开发室中安装大型显示器并观察服务状态,并且最好的情况是使用图表。:)
一个特殊功能可能是让一个通知服务在一个或多个服务失败的情况下为所有管理员发送消息,在这种情况下,服务意味着目标,如主机,数据库,API。
在本文中,我们将使用以下服务进行监控:
名称 | 描述 |
主机 | Ping现有主机 |
数据库 | 打开并关闭现有数据库的连接 |
RESTful API | 从现有API中获取一个操作 |
正如我们之前所说,我们将创建一个应用程序来监视现有目标(主机,数据库,API),因此我们需要掌握有关这些概念的基本知识。
主机将使用ping操作进行监控,因此我们将添加与网络相关的包以执行此操作。
数据库将通过开放和关闭连接进行监视,不使用集成安全性,因为您需要使用凭据模拟服务监视器进程,因此在这种情况下,最好让特定用户与数据库连接,并且只有这样才能避免黑客攻击。
RESTful API将使用REST客户端进行监视,以定位返回简单JSON的操作。
在存储库内部,有一个名为\ Resources \ Database的目录,该目录包含相关的数据库文件,请确保按以下顺序运行以下文件:
文件名 | 描述 |
00 - Database.sql | 数据库定义 |
01 - Tables.sql | 表定义 |
02 - Constraints.sql | 约束(主键,外键和唯一性) |
03 - Rows.sql | 初始数据 |
我们可以在找到数据库脚本。
表说明 | |
表 | 描述 |
EnvironmentCategory | 包含环境的所有类别:开发,qa和生产 |
ServiceCategory | 包含服务的所有类别:数据库,rest API,服务器,URL和Web服务 |
Service | 包含所有服务定义 |
ServiceWatcher | 包含C#端的所有组件以执行监视操作 |
ServiceEnvironment | 包含服务和环境的关系,例如我们可以定义一个以不同环境命名的FinanceService服务:开发,qa和生产 |
ServiceEnvironmentStatus | 包含每个环境的每个服务的状态 |
ServiceEnvironmentStatusLog | 包含每个服务环境状态的详细信息 |
Owner | 包含代表所有所有者的应用程序的用户列表 |
ServiceOwner | 包含服务和所有者之间的关系 |
User | 包含观看服务的所有用户 |
ServiceUser | 包含服务和用户之间的关系 |
请不要忘记我们正在使用在本地计算机上运行的解决方案,资源目录中有一个示例API来执行测试,但您需要更改连接字符串并根据您的上下文添加服务。
另外我不建议在ServiceEnvironment表中公开真实的连接字符串,请向您的DBA请求单个用户只能对目标数据库执行打开连接,以防数据库的安全性成为您的任务,创建特定的用户来执行仅打开与数据库的连接并防止泄露敏感信息。
现在我们需要为此解决方案定义项目,以获得有关项目范围的清晰概念:
项目名 | 类型 | 描述 |
ServiceMonitor.Core | 类库 | 包含与数据库存储相关的所有定义 |
ServiceMonitor.Common | 类库 | 包含ServiceMonitor项目的常见定义,例如观察者,序列化器和客户端(REST) |
ServiceMonitor.WebApi | Web API | 包含Web API控制器,用于读取和写入有关监视的信息 |
ServiceMonitor | 控制台应用 | 包含监控所有服务的过程 |
该项目包含实体和数据库访问的所有定义,因此我们需要为项目添加以下包:
名称 | 版 | 描述 |
Microsoft.EntityFrameworkCore.SqlServer | 最新版本 | 通过EF Core提供对SQL Server的访问 |
该项目包含三个层次:业务逻辑,数据库访问和实体。
DashboardService 类代码:
using System;using System.Threading.Tasks;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Logging;using ServiceMonitor.Core.BusinessLayer.Contracts;using ServiceMonitor.Core.BusinessLayer.Responses;using ServiceMonitor.Core.DataLayer;using ServiceMonitor.Core.DataLayer.DataContracts;using ServiceMonitor.Core.EntityLayer;namespace ServiceMonitor.Core.BusinessLayer{ public class DashboardService : Service, IDashboardService { public DashboardService(ILoggerlogger, ServiceMonitorDbContext dbContext) : base(logger, dbContext) { } public async Task > GetActiveServiceWatcherItemsAsync() { Logger?.LogDebug("'{0}' has been invoked", nameof(GetActiveServiceWatcherItemsAsync)); var response = new ListResponse (); try { response.Model = await DbContext.GetActiveServiceWatcherItems().ToListAsync(); Logger?.LogInformation("The service watch items were loaded successfully"); } catch (Exception ex) { response.SetError(Logger, nameof(GetActiveServiceWatcherItemsAsync), ex); } return response; } public async Task > GetServiceStatusesAsync(string userName) { Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusesAsync)); var response = new ListResponse (); try { var user = await DbContext.GetUserAsync(userName); if (user == null) { Logger?.LogInformation("There isn't data for user '{0}'", userName); return new ListResponse (); } else { response.Model = await DbContext.GetServiceStatuses(user).ToListAsync(); Logger?.LogInformation("The service status details for '{0}' user were loaded successfully", userName); } } catch (Exception ex) { response.SetError(Logger, nameof(GetServiceStatusesAsync), ex); } return response; } public async Task > GetServiceStatusAsync(ServiceEnvironmentStatus entity) { Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusAsync)); var response = new SingleResponse (); try { response.Model = await DbContext.GetServiceEnvironmentStatusAsync(entity); } catch (Exception ex) { response.SetError(Logger, nameof(GetServiceStatusAsync), ex); } return response; } }}
IWatcher 接口代码:
using System.Threading.Tasks;namespace ServiceMonitor.Common.Contracts{ public interface IWatcher { string ActionName { get; } TaskWatchAsync(WatcherParameter parameter); }}
IWatchResponse 接口代码:
namespace ServiceMonitor.Common.Contracts{ public interface IWatchResponse { bool Success { get; set; } string Message { get; set; } string StackTrace { get; set; } }}
ISerializer 接口代码:
namespace ServiceMonitor.Common.Contracts{ public interface ISerializer { string Serialize(T obj); T Deserialze (string source); }}
这些是实现:
DatabaseWatcher 类代码:
using System;using System.Data.SqlClient;using System.Threading.Tasks;using ServiceMonitor.Common.Contracts;namespace ServiceMonitor.Common{ public class DatabaseWatcher : IWatcher { public string ActionName => "OpenDatabaseConnection"; public async TaskWatchAsync(WatcherParameter parameter) { var response = new WatchResponse(); using (var connection = new SqlConnection(parameter.Values["ConnectionString"])) { try { await connection.OpenAsync(); response.Success = true; } catch (Exception ex) { response.Success = false; response.Message = ex.Message; response.StackTrace = ex.ToString(); } } return response; } }}
HttpWebRequestWatcher 类代码:
using System;using System.Threading.Tasks;using ServiceMonitor.Common.Contracts;namespace ServiceMonitor.Common{ public class HttpRequestWatcher : IWatcher { public string ActionName => "HttpRequest"; public async TaskWatchAsync(WatcherParameter parameter) { var response = new WatchResponse(); try { var restClient = new RestClient(); await restClient.GetAsync(parameter.Values["Url"]); response.Success = true; } catch (Exception ex) { response.Success = false; response.Message = ex.Message; response.StackTrace = ex.ToString(); } return response; } }}
PingWatcher 类代码:
using System.Net.NetworkInformation;using System.Threading.Tasks;using ServiceMonitor.Common.Contracts;namespace ServiceMonitor.Common{ public class PingWatcher : IWatcher { public string ActionName => "Ping"; public async TaskWatchAsync(WatcherParameter parameter) { var ping = new Ping(); var reply = await ping.SendPingAsync(parameter.Values["Address"]); return new WatchResponse { Success = reply.Status == IPStatus.Success ? true : false }; } }}
这个项目代表服务监视器的RESTful API,所以我们将有两个控制器:DashboardController和AdministrationController。仪表板具有与最终用户结果相关的所有操作,管理包含与保存信息(创建,编辑和删除)相关的所有操作。
DashboardController 类代码:
using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;using ServiceMonitor.Core.BusinessLayer.Contracts;using ServiceMonitor.WebApi.Responses;namespace ServiceMonitor.WebApi.Controllers{#pragma warning disable CS1591 [Route("api/v1/[controller]")] [ApiController] public class DashboardController : ControllerBase { protected ILogger Logger; protected IDashboardService Service; public DashboardController(ILoggerlogger, IDashboardService service) { Logger = logger; Service = service; }#pragma warning restore CS1591 /// /// Gets service watcher items (registered services to watch with service monitor) /// ///A sequence of services to watch [HttpGet("ServiceWatcherItem")] [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] public async TaskGetServiceWatcherItemsAsync() { Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceWatcherItemsAsync)); var response = await Service.GetActiveServiceWatcherItemsAsync(); return response.ToHttpResponse(); } /// /// Gets the details for service watch /// /// Service ID ///[HttpGet("ServiceStatusDetail/{id}")] [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] public async Task GetServiceStatusDetailsAsync(string id) { Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusDetailsAsync)); var response = await Service.GetServiceStatusesAsync(id); return response.ToHttpResponse(); } }}
AdministrationController 类代码:
using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;using ServiceMonitor.Core.BusinessLayer.Contracts;using ServiceMonitor.WebApi.Requests;using ServiceMonitor.WebApi.Responses;namespace ServiceMonitor.WebApi.Controllers{#pragma warning disable CS1591 [Route("api/v1/[controller]")] [ApiController] public class AdministrationController : ControllerBase { protected ILogger Logger; protected IAdministrationService Service; public AdministrationController(ILoggerlogger, IAdministrationService service) { Logger = logger; Service = service; }#pragma warning restore CS1591 /// /// Saves a result from service watch action /// /// Service status result ///Ok if save it was successfully, Not found if service not exists else server internal error [HttpPost("ServiceEnvironmentStatusLog")] [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] public async TaskPostServiceStatusLogAsync([FromBody]ServiceEnvironmentStatusLogRequest request) { Logger?.LogDebug("'{0}' has been invoked", nameof(PostServiceStatusLogAsync)); var response = await Service .CreateServiceEnvironmentStatusLogAsync(request.ToEntity(), request.ServiceEnvironmentID); return response.ToHttpResponse(); } }}
这个项目包含Service Monitor Client的所有对象,在这个项目中,我们添加了Newtonsoft.Json用于JSON序列化的包,在ServiceMonitor.Common中有一个名称为ISerializer的接口,因为我不想强制使用特定的序列化程序,你可以改变它在这个层。:)
ServiceMonitorSerializer 类代码:
using Newtonsoft.Json;using ServiceMonitor.Common.Contracts;namespace ServiceMonitor{ public class ServiceMonitorSerializer : ISerializer { public string Serialize(T obj) => JsonConvert.SerializeObject(obj); public T Deserialze (string source) => JsonConvert.DeserializeObject (source); }}
接下来,我们将开始MonitorController类,在这个类中,我们将执行所有观察操作,并通过Service Monitor API 中的AdministrationController将所有结果保存在数据库中。
MonitorController 类代码:
using System;using System.Threading;using System.Threading.Tasks;using Microsoft.Extensions.Logging;using ServiceMonitor.Common;using ServiceMonitor.Common.Contracts;using ServiceMonitor.Models;namespace ServiceMonitor{ public class MonitorController { public MonitorController(AppSettings appSettings, ILogger logger, IWatcher watcher, RestClient restClient) { AppSettings = appSettings; Logger = logger; Watcher = watcher; RestClient = restClient; } public AppSettings AppSettings { get; } public ILogger Logger { get; } public IWatcher Watcher { get; } public RestClient RestClient { get; } public async Task ProcessAsync(ServiceWatchItem item) { while (true) { try { Logger?.LogTrace("{0} - Watching '{1}' for '{2}' environment", DateTime.Now, item.ServiceName, item.Environment); var watchResponse = await Watcher.WatchAsync(new WatcherParameter(item.ToDictionary())); if (watchResponse.Success) Logger?.LogInformation(" Success watch for '{0}' in '{1}' environment", item.ServiceName, item.Environment); else Logger?.LogError(" Failed watch for '{0}' in '{1}' environment", item.ServiceName, item.Environment); var watchLog = new ServiceStatusLog { ServiceID = item.ServiceID, ServiceEnvironmentID = item.ServiceEnvironmentID, Target = item.ServiceName, ActionName = Watcher.ActionName, Success = watchResponse.Success, Message = watchResponse.Message, StackTrace = watchResponse.StackTrace }; try { await RestClient.PostJsonAsync(AppSettings.ServiceStatusLogUrl, watchLog); } catch (Exception ex) { Logger?.LogError(" Error on saving watch response ({0}): '{1}'", item.ServiceName, ex.Message); } } catch (Exception ex) { Logger?.LogError(" Error watching service: '{0}': '{1}'", item.ServiceName, ex.Message); } Thread.Sleep(item.Interval ?? AppSettings.DelayTime); } } }}
在运行控制台应用程序之前,请确保以下方面:
我们可以检查url api/v1/Dashboard/ServiceWatcherItems的返回值:
{ "message":null, "didError":false, "errorMessage":null, "model":[ { "serviceID":1, "serviceEnvironmentID":1, "environment":"Development", "serviceName":"Northwind Database", "interval":15000, "url":null, "address":null, "connectionString":"server=(local);database=Northwind;user id=johnd;password=SqlServer2017$", "typeName":"ServiceMonitor.Common.DatabaseWatcher, ServiceMonitor.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, { "serviceID":2, "serviceEnvironmentID":3, "environment":"Development", "serviceName":"DNS", "interval":3000, "url":null, "address":"192.168.1.1", "connectionString":null, "typeName":"ServiceMonitor.Common.PingWatcher, ServiceMonitor.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, { "serviceID":3, "serviceEnvironmentID":4, "environment":"Development", "serviceName":"Sample API", "interval":5000, "url":"http://localhost:5612/api/values", "address":null, "connectionString":null, "typeName":"ServiceMonitor.Common.HttpWebRequestWatcher, ServiceMonitor.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" } ]}
正如我们所看到的,API为DefaultUser返回所有服务,请记住关于一个用户可以订阅多个服务的概念,显然在此示例中,我们的默认用户被绑定到所有服务但我们可以在ServiceUser表中更改此链接。
Program 类代码:
using System;using System.Threading.Tasks;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.Logging;using ServiceMonitor.Common;using ServiceMonitor.Common.Contracts;namespace ServiceMonitor{ class Program { private static ILogger logger; private static readonly AppSettings appSettings; static Program() { logger = LoggingHelper.GetLogger(); var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); var configuration = builder.Build(); appSettings = new AppSettings { ServiceWatcherItemsUrl = configuration["serviceWatcherItemUrl"], ServiceStatusLogUrl = configuration["serviceStatusLogUrl"], DelayTime = Convert.ToInt32(configuration["delayTime"]) }; } static void Main(string[] args) { StartAsync(args).GetAwaiter().GetResult(); Console.ReadLine(); } static async Task StartAsync(string[] args) { logger.LogDebug("Starting application..."); var initializer = new ServiceMonitorInitializer(appSettings); try { await initializer.LoadResponseAsync(); } catch (Exception ex) { logger.LogError("Error on retrieve watch items: {0}", ex); return; } try { initializer.DeserializeResponse(); } catch (Exception ex) { logger.LogError("Error on deserializing object: {0}", ex); return; } foreach (var item in initializer.Response.Model) { var watcherType = Type.GetType(item.TypeName, true); var watcherInstance = Activator.CreateInstance(watcherType) as IWatcher; var task = Task.Factory.StartNew(async () => { var controller = new MonitorController(appSettings, logger, watcherInstance, initializer.RestClient); await controller.ProcessAsync(item); }); } } }}
一旦我们检查了之前的方面,现在我们继续转向控制台应用程序,控制台输出是这样的:
dbug: ServiceMonitor.Program[0] Starting applicationsr trce: ServiceMonitor.Program[0] 06/20/2017 23:09:30 - Watching 'Sample API' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:30 - Watching 'Northwind Database' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:30 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:35 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:37 - Watching 'Sample API' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:39 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:42 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:43 - Watching 'Sample API' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:45 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:47 - Watching 'Northwind Database' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:48 - Watching 'Sample API' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:48 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:51 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:53 - Watching 'Sample API' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:54 - Watching 'DNS' for 'Development' environmenttrce: ServiceMonitor.Program[0] 06/20/2017 23:09:57 - Watching 'DNS' for 'Development' environment
现在我们继续检查数据库中保存的数据,请检查ServiceEnvironmentStatus表,你会得到这样的结果:
ServiceEnvironmentStatusID ServiceEnvironmentID Success WatchCount LastWatch-------------------------- -------------------- ------- ----------- -----------------------1 4 1 212 2018-11-22 23:11:34.1132 1 1 78 2018-11-22 23:11:33.3703 3 1 366 2018-11-22 23:11:34.620(3 row(s) affected)
它是如何一起工作的?控制台应用程序从API中监视所有服务,然后在MonitorController中以无限循环的方式为每个监视项启动一个任务,每个任务都有一个延迟时间,该间隔在服务定义中设置,但是如果没有为间隔定义值,间隔取自AppSettings; 因此,在执行Watch操作之后,结果将通过API保存在数据库中,并且该过程会自行重复。如果要watch对其他类型执行操作,可以创建自己的Watcher类。
原文地址:
转载地址:http://luzhj.baihongyu.com/