使用.NET Core建立服務監視器應用程式
目錄
介紹
本文介紹如何建立服務監視器應用程式,但它是什麼?簡單來說:它是一個允許監視網路中的服務並儲存監視結果到資料庫的應用程式,本例中為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請求單個使用者只能對目標資料庫執行開啟連線,以防資料庫的安全性成為您的任務,建立特定的使用者來執行僅開啟與資料庫的連線並防止洩露敏感資訊。
.NET核心解決方案
現在我們需要為此解決方案定義專案,以獲得有關專案範圍的清晰概念:
專案名 |
型別 |
描述 |
ServiceMonitor.Core |
類庫 |
包含與資料庫儲存相關的所有定義 |
ServiceMonitor.Common |
類庫 |
包含ServiceMonitor專案的常見定義,例如觀察者,序列化器和客戶端(REST) |
ServiceMonitor.WebApi |
Web API |
包含Web API控制器,用於讀取和寫入有關監視的資訊 |
ServiceMonitor |
控制檯應用 |
包含監控所有服務的過程 |
ServiceMonitor.Core
該專案包含實體和資料庫訪問的所有定義,因此我們需要為專案新增以下包:
名稱 |
版 |
描述 |
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(ILogger<DashboardService> logger, ServiceMonitorDbContext dbContext)
: base(logger, dbContext)
{
}
public async Task<IListResponse<ServiceWatcherItemDto>> GetActiveServiceWatcherItemsAsync()
{
Logger?.LogDebug("'{0}' has been invoked", nameof(GetActiveServiceWatcherItemsAsync));
var response = new ListResponse<ServiceWatcherItemDto>();
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<IListResponse<ServiceStatusDetailDto>> GetServiceStatusesAsync(string userName)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusesAsync));
var response = new ListResponse<ServiceStatusDetailDto>();
try
{
var user = await DbContext.GetUserAsync(userName);
if (user == null)
{
Logger?.LogInformation("There isn't data for user '{0}'", userName);
return new ListResponse<ServiceStatusDetailDto>();
}
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<ISingleResponse<ServiceEnvironmentStatus>> GetServiceStatusAsync(ServiceEnvironmentStatus entity)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusAsync));
var response = new SingleResponse<ServiceEnvironmentStatus>();
try
{
response.Model = await DbContext.GetServiceEnvironmentStatusAsync(entity);
}
catch (Exception ex)
{
response.SetError(Logger, nameof(GetServiceStatusAsync), ex);
}
return response;
}
}
}
ServiceMonitor.Common
約定
- IWatcher
- IWatchResponse
- ISerializer
IWatcher 介面程式碼:
using System.Threading.Tasks;
namespace ServiceMonitor.Common.Contracts
{
public interface IWatcher
{
string ActionName { get; }
Task<WatchResponse> WatchAsync(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>(T obj);
T Deserialze<T>(string source);
}
}
觀察者
這些是實現:
- DatabaseWatcher
- HttpRequestWatcher
- PingWatcher
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 Task<WatchResponse> WatchAsync(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 Task<WatchResponse> WatchAsync(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 Task<WatchResponse> WatchAsync(WatcherParameter parameter)
{
var ping = new Ping();
var reply = await ping.SendPingAsync(parameter.Values["Address"]);
return new WatchResponse
{
Success = reply.Status == IPStatus.Success ? true : false
};
}
}
}
ServiceMonitor.WebApi
這個專案代表服務監視器的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(ILogger<DashboardController> logger, IDashboardService service)
{
Logger = logger;
Service = service;
}
#pragma warning restore CS1591
/// <summary>
/// Gets service watcher items (registered services to watch with service monitor)
/// </summary>
/// <returns>A sequence of services to watch</returns>
[HttpGet("ServiceWatcherItem")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetServiceWatcherItemsAsync()
{
Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceWatcherItemsAsync));
var response = await Service.GetActiveServiceWatcherItemsAsync();
return response.ToHttpResponse();
}
/// <summary>
/// Gets the details for service watch
/// </summary>
/// <param name="id">Service ID</param>
/// <returns></returns>
[HttpGet("ServiceStatusDetail/{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(500)]
public async Task<IActionResult> 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(ILogger<AdministrationController> logger, IAdministrationService service)
{
Logger = logger;
Service = service;
}
#pragma warning restore CS1591
/// <summary>
/// Saves a result from service watch action
/// </summary>
/// <param name="request">Service status result</param>
/// <returns>Ok if save it was successfully, Not found if service not exists else server internal error</returns>
[HttpPost("ServiceEnvironmentStatusLog")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> PostServiceStatusLogAsync([FromBody]ServiceEnvironmentStatusLogRequest request)
{
Logger?.LogDebug("'{0}' has been invoked", nameof(PostServiceStatusLogAsync));
var response = await Service
.CreateServiceEnvironmentStatusLogAsync(request.ToEntity(), request.ServiceEnvironmentID);
return response.ToHttpResponse();
}
}
}
ServiceMonitor
這個專案包含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>(T obj)
=> JsonConvert.SerializeObject(obj);
public T Deserialze<T>(string source)
=> JsonConvert.DeserializeObject<T>(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);
}
}
}
}
在執行控制檯應用程式之前,請確保以下方面:
- ServiceMonitor 資料庫可用
- ServiceMonitor 資料庫具有服務類別,服務,服務觀察者和使用者的資訊
- ServiceMonitor API可用
我們可以檢查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<Program>();
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 application
sr trce: ServiceMonitor.Program[0]
06/20/2017 23:09:30 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:30 - Watching 'Northwind Database' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:30 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:35 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:37 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:39 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:42 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:43 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:45 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:47 - Watching 'Northwind Database' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:48 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:48 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:51 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:53 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
06/20/2017 23:09:54 - Watching 'DNS' for 'Development' environment
trce: 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.113
2 1 1 78 2018-11-22 23:11:33.370
3 3 1 366 2018-11-22 23:11:34.620
(3 row(s) affected)
它是如何一起工作的?控制檯應用程式從API中監視所有服務,然後在MonitorController中以無限迴圈的方式為每個監視項啟動一個任務,每個任務都有一個延遲時間,該間隔在服務定義中設定,但是如果沒有為間隔定義值,間隔取自AppSettings; 因此,在執行Watch操作之後,結果將通過API儲存在資料庫中,並且該過程會自行重複。如果要watch對其他型別執行操作,可以建立自己的Watcher類。
原文地址:https://www.codeproject.com/Articles/1165961/Creating-Service-Monitor-Application-with-NET-Core