服务仪表板






4.75/5 (8投票s)
一个轻量级的桌面应用程序,帮助您了解网络中的服务器和应用程序的可用性。

引言
您的应用程序服务器是否正在运行?您最好去抓住它!这个轻量级的桌面应用程序将帮助您了解网络中的服务器可用性。利用 .NET 的网络和管理功能,例如 System.Management 中的 WMI 类和 System.Net.WebRequest,可以轻松地与网络上的计算机和服务进行交互,以确保它们正常运行。它易于扩展,可以定义新的服务类型。使用依赖注入库(Castle Windsor)也可以快速完成配置。
背景
市面上有许多现有的应用程序可用性监控解决方案,有些是开源的,有些是商业的,但它们似乎都有一个共同的障碍——安装和维护需要大量的时间和精力。这对于大型或任务关键型操作来说是完全可以接受的,但如果目标较小——例如,只是想知道开发服务当前是否正在运行——那么这些大型应用程序就有点大材小用。对于 Service DashBored,我的目标是创建一个“复制安装”应用程序,以便在我的开发环境中的某些组件出现问题时,能够快速通知我。
Using the Code
第一步是定义一个接口来表示一个服务或应用程序。在概念上,它们将被称为“端点”,每个端点都必须定义几个属性:
名称
——以便我们知道如何称呼它。状态
——其当前状态——这是一个标准枚举,包含:运行中
——端点可用且运行正常。无法访问
——无法与端点建立连接。错误
——端点报告错误状态。超时
——连接请求在给定的时间内从未完成。未知
——在第一次状态更新之前,实例化时的默认状态。
状态描述
——关于端点的文本描述,包括任何特定的错误代码。
public interface IEndpoint
{
string Name { get; }
Status GetStatus();
string StatusDescription { get; }
}
public enum Status
{
Unknown,
Up,
Timeout,
Unreachable,
Error
}
状态
定义为一个Get
方法——许多实现者会做很多工作,所以最好采用这种方式。
因此,这是查询应用程序所需信息的正确类型,但缺少了重要的东西——它怎么知道服务或应用程序住在哪里?每个实现者都需要为自己定义这些属性。Service DashBored 中包含几种现有的端点类型,可以立即使用(只需稍加配置)。
HTTP 端点
如果我的 Web 服务器宕机了,我希望立即知道。此端点将向给定的 URI 发送一个请求,并简单地检查它是否正在响应。
public class HttpEndpoint : IEndpoint
{
public Uri Uri { get; set; }
public virtual Status GetStatus()
{
try
{
var request = WebRequest.Create(Uri);
using (var response = (HttpWebResponse) request.GetResponse())
{
StatusDescription = response.StatusDescription;
return response.StatusCode == HttpStatusCode.OK
? Status.Up
: Status.Unreachable;
}
}
catch (WebException ex)
{
StatusDescription = ex.Message;
return ex.Status == WebExceptionStatus.Timeout
? Status.Timeout
: Status.Unreachable;
}
catch (Exception ex)
{
StatusDescription = ex.Message;
return Status.Error;
}
}
}
以下是如何使用 Castle Project 的 Windsor Container 为 Service DashBored(在app.config 中)配置此端点的示例。
<component id="http.google.endpoint" service="Endpoint.IEndpoint,
Endpoint" type="Endpoint.HttpEndpoint, Endpoint">
<parameters>
<Name>Http - Google</Name>
<UriString>http://www.google.com</UriString>
</parameters>
</component>
就是这样!所有端点的配置方式都相同——只需在<parameters>
块中添加相应的元素即可。
HTML 端点
此类扩展了 HTTP 端点,使其能够扫描和验证 URI 返回的内容——假定内容是 HTML 格式的。
public class HtmlEndpoint : HttpEndpoint
{
private readonly XpathEndpointUtility _xpathEndpointUtility =
new XpathEndpointUtility();
public string XpathQuery
{
get { return _xpathEndpointUtility.XpathQuery; }
set { _xpathEndpointUtility.XpathQuery = value; }
}
public string XpathNamespaces
{
get { return _xpathEndpointUtility.XpathNamespaces; }
set { _xpathEndpointUtility.XpathNamespaces = value; }
}
public string ExpectedXpathResult
{
get { return _xpathEndpointUtility.ExpectedXpathResult; }
set { _xpathEndpointUtility.ExpectedXpathResult = value; }
}
public override Status GetStatus()
{
var baseStatus = base.GetStatus();
if (baseStatus != Status.Up)
return baseStatus;
XmlDocument xml;
try
{
xml = getHtmlXml(Uri, RequestContent);
}
catch (Exception ex)
{
StatusDescription = ex.Message;
return Status.Unreachable;
}
var status = _xpathEndpointUtility.GetStatus(xml);
StatusDescription = _xpathEndpointUtility.StatusDescription;
return status;
}
}
getHtmlXml(Uri, RequestContent)
中的 HTML 到 XML 转换使用捆绑的 HTML Agility Pack 完成。您可以向其提供格式不正确的 HTML,它将返回一个符合标准的 XML 文档,可供 XPath 查询。
至于运行 XPath 查询,此功能已移至一个名为XpathEndpointUtility
的实用程序类。它只是在响应的XmlDocument
上执行SelectNodes(XpathQuery)
,然后验证结果值是否等于ExpectedXpathResult
。
internal class XpathEndpointUtility
{
public Status GetStatus(XmlDocument xml)
{
XmlNodeList nodes;
if (_namespaceToUri != null)
{
var nsMgr = new XmlNamespaceManager(xml.NameTable);
foreach (var namespaceToUri in _namespaceToUri)
{
nsMgr.AddNamespace(namespaceToUri.Key, namespaceToUri.Value);
}
nodes = xml.SelectNodes(XpathQuery, nsMgr);
}
else
nodes = xml.SelectNodes(XpathQuery);
//xml.Save(@"c:\serviceDashBoredResponse.xml");
// verify response has expected value
if (nodes == null || nodes.Count == 0)
{
StatusDescription = "Couldn't find expected value in response";
return Status.Error;
}
var value = nodes[0].Value.Trim();
if (value != ExpectedXpathResult)
{
StatusDescription = String.Format("Result was: '{0}', was expecting '{1}'"
, value
, ExpectedXpathResult);
return Status.Error;
}
return Status.Up;
}
}
对于 Web 文档,让 XPath 查询正常工作可能会很棘手,特别是当文档很大时。Microsoft 发布了一个不错的工具可以提供帮助——XML Notepad 2007。通过导航到要验证的特定元素,然后使用“查找
”功能,将自动生成 XPath 查询以及任何命名空间映射。

SOAP 端点
与 HTML 端点一样,此类扩展了 HTTP 端点。区别在于能够发送 SOAP 请求并在标头数据中嵌入 SOAPAction。然后通过 XPath 验证结果。
public class SoapEndpoint : HttpEndpoint
{
private readonly XpathEndpointUtility _xpathEndpointUtility =
new XpathEndpointUtility();
public string XpathQuery
{
get { return _xpathEndpointUtility.XpathQuery; }
set { _xpathEndpointUtility.XpathQuery = value; }
}
public string XpathNamespaces
{
get { return _xpathEndpointUtility.XpathNamespaces; }
set { _xpathEndpointUtility.XpathNamespaces = value; }
}
public string ExpectedXpathResult
{
get { return _xpathEndpointUtility.ExpectedXpathResult; }
set { _xpathEndpointUtility.ExpectedXpathResult = value; }
}
public string SoapAction { get; set; }
public string SoapRequest { get; set; }
public override Status GetStatus()
{
var baseStatus = base.GetStatus();
if (baseStatus != Status.Up) // if we can't even get to the webserver,
// no need to try and call a WS.
return baseStatus;
var xml = new XmlDocument();
try
{
var headers = new Dictionary<string, string>
{{"SOAPAction", SoapAction}};
// we can assume that the response is well formatted XML
xml.LoadXml(GetUrlContent(Uri, "text/xml; charset=utf-8",
SoapRequest, headers));
}
catch (Exception ex)
{
StatusDescription = ex.Message;
return Status.Unreachable;
}
Status status = _xpathEndpointUtility.GetStatus(xml);
StatusDescription = _xpathEndpointUtility.StatusDescription;
return status;
}
}
配置 SOAP 端点需要几个数据项——即 SOAPAction 和格式为 SOAP 信封的请求。我发现这些可以通过使用 Web 服务测试工具 soapUI 轻松获得。通过使用服务 WSDL 创建一个新项目,soapUI 将自动为每个已定义的会话模拟 SOAP 请求。只需将这些值复制到配置中即可。
Windows 服务端点
此端点将验证配置的服务端点是否处于“运行”状态。只需提供服务名称和计算机名称即可。
public class WindowsServiceEndpoint : IEndpoint
{
public string ServiceName { get; set; }
public string MachineName { get; set; }
public Status GetStatus()
{
ServiceController myservice;
try
{
myservice = !string.IsNullOrEmpty(MachineName)
? new ServiceController(ServiceName, MachineName)
: new ServiceController(ServiceName);
}
catch (Exception ex)
{
StatusDescription = ex.Message;
return Status.Unreachable;
}
try
{
switch (myservice.Status)
{
case ServiceControllerStatus.Running:
StatusDescription = "Running";
return Status.Up;
default:
StatusDescription = myservice.Status.ToString();
return Status.Error;
}
}
catch (Exception ex)
{
StatusDescription = ex.Message;
return Status.Error;
}
}
}
WMI 端点
Windows Management Instrumentation (WMI) 是一项技术,允许管理软件监视(甚至控制)网络资源。此端点可以配置为向网络上的计算机发出 WQL 查询并验证结果。验证比其他任何端点都更复杂——除了能够发出信号表示特定值出现错误外,它还可以对数值阈值做出反应。例如,当“可用磁盘空间”查询结果值低于 1GB 时,状态可以设置为“错误”。
public class WmiEndpoint : IEndpoint
{
public string MachineName { get; set; }
public string ObjectQueryString { get; set; }
public string ConnectionUsername { get; set; }
public string ConnectionPassword { get; set; }
public string ResultPropertyName { get; set; }
public string ErrorResult { get; set; }
public string UpResult { get; set; }
public double MinimumThreshold { get; set; }
public double MaximumThreshold { get; set; }
public WmiEndpoint()
{
MinimumThreshold = double.MinValue;
MaximumThreshold = double.MaxValue;
}
public Status GetStatus()
{
var connectionOptions = new ConnectionOptions();
if (!string.IsNullOrEmpty(ConnectionUsername) &&
!string.IsNullOrEmpty(ConnectionPassword))
{
connectionOptions.Username = ConnectionUsername;
connectionOptions.Password = ConnectionPassword;
}
ManagementScope managementScope;
try
{
managementScope = new ManagementScope(MachineName, connectionOptions);
}
catch (Exception e)
{
StatusDescription
= string.Format("Management Scope for MachineName \"{1}\" Failed : \"{0}\""
, e.Message
, MachineName);
return Status.Error;
}
ObjectQuery objectQuery;
try
{
objectQuery = new ObjectQuery(ObjectQueryString);
}
catch (Exception e)
{
StatusDescription
= string.Format("ObjectQuery initialization \"{1}\" Failed : \"{0}\""
, e.Message
, ObjectQueryString);
return Status.Error;
}
ManagementObjectSearcher results;
try
{
//Execute the query
results = new ManagementObjectSearcher(managementScope, objectQuery);
}
catch (Exception e)
{
StatusDescription = string.Format("Building Query failed : \"{0}\"", e.Message);
return Status.Error;
}
ManagementObjectCollection managementObjectCollection;
try
{
//Get the results
managementObjectCollection = results.Get();
if (managementObjectCollection.Count == 0)
{
StatusDescription = string.Format("Query returned 0 results :
\"{0}\"", ObjectQueryString);
return Status.Error;
}
}
catch (Exception ex)
{
StatusDescription = "Error retrieving results. Exception: " + ex.Message;
return Status.Unreachable;
}
ManagementObject firstResult = null;
//take only the first result if there are more than one
foreach (ManagementObject result in managementObjectCollection)
{
firstResult = result;
break;
}
if (firstResult == null)
{
StatusDescription = "Problem accessing first result object";
return Status.Error;
}
object resultValue;
try
{
resultValue = firstResult[ResultPropertyName];
}
catch (Exception ex)
{
StatusDescription
= string.Format("Error retrieving result property \"{0}\". Exception: {1}"
,ResultPropertyName, ex.Message);
return Status.Error;
}
if (resultValue == null)
{
StatusDescription = string.Format("Result value was null for property \"{0}\".",
ResultPropertyName);
return Status.Error;
}
return GetStatus(resultValue.ToString());
}
protected virtual Status GetStatus(string resultValueString)
{
// Up Value
if (!string.IsNullOrEmpty(UpResult) && UpResult != resultValueString)
{
StatusDescription =
string.Format("Result for property \"{0}\"
was not expected value. Expected \"{1}\" but was \"{2}\"",
ResultPropertyName, UpResult ?? "(NULL)", resultValueString);
return Status.Error;
}
// Error Value
if (!string.IsNullOrEmpty(ErrorResult) && ErrorResult == resultValueString)
{
StatusDescription =
string.Format("Result for property \"{0}\" was error value - \"{1}\"",
ResultPropertyName, resultValueString);
return Status.Error;
}
if (MinimumThreshold != double.MinValue || MaximumThreshold != double.MaxValue)
{
double resultValueDouble;
if (Double.TryParse(resultValueString, out resultValueDouble))
{
if (resultValueDouble < MinimumThreshold)
{
StatusDescription =
string.Format("Result for property \"{0}\"
was less then threshold - {1} < {2}",
ResultPropertyName, resultValueDouble, MinimumThreshold);
return Status.Error;
}
if (resultValueDouble > MaximumThreshold)
{
StatusDescription =
string.Format("Result for property \"{0}\"
was more then threshold - {1} > {2}",
ResultPropertyName, resultValueDouble, MaximumThreshold);
return Status.Error;
}
}
else
{
StatusDescription =
string.Format("Result for property \"{0}\" was not a number - \"{1}\"",
ResultPropertyName, resultValueString);
return Status.Error;
}
}
StatusDescription = resultValueString;
return Status.Up;
}
}
有许多工具可用于浏览和查询 WMI 数据——我一直在使用 这个基于 PowerShell 的实现。
关注点
Service DashBored 使用轮询技术来刷新端点的状态。每个端点都封装在一个EndpointStatus
对象中,该对象包含有关端点的一些元数据——最后更新时间、当前状态图标等。这些EndpointStatus
的列表已绑定到应用程序的DataGridView
。EndpointStatus
实现了INotifyPropertyChanged
,以便表单元素可以直接响应数据更改。例如,当端点的状态从运行中
变为错误
时,将发出PropertyChanged
事件,然后触发DataGridView
以在视觉上更新该数据绑定信息。以下是一些示例代码:
public class EndpointStatus : INotifyPropertyChanged, IDisposable
{
private void updateStatus()
{
//...
var newStatus = _endpoint.GetStatus();
Status = newStatus;
SignalPropertyChanged("Status");
//...
}
private readonly IEndpoint _endpoint;
private readonly Logger _logger;
private readonly AsyncOperation _asyncOperation;
public Status Status { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public EndpointStatus(IEndpoint endpoint, Logger logger)
{
_endpoint = endpoint;
_logger = logger;
Status = Status.Unknown;
_asyncOperation = AsyncOperationManager.CreateOperation(null);
}
protected void SignalPropertyChanged(string propertyName)
{
try
{
// marshall changes back to the UI
_asyncOperation.Post(
delegate
{ PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); }
, null);
}
catch (Exception ex)
{
_logger.LogWarning("SignalPropertyChanged: " + ex);
}
}
}
请注意调用PropertyChanged
事件所需的工作。AsyncOperation.Post()
会将调用放置在适当的 UI 线程上。如果不这样做,将会导致神秘的“BindingSource
不能是它自己的数据源”异常。在我开发过程中,这偶尔发生,非常烦人!以下是一个 论坛帖子,有助于解决此问题。
为了使应用程序易于配置,我选择使用 Windsor Container 依赖注入框架。在早期版本中,我曾使用System.Configuration
中内置的 .NET 配置,但在定义自定义配置类和集合时,代码量爆炸式增长,之后就放弃了。一些用于配置的代码文件比实际代码大好几倍!以下是 Service Dashbored 使用 Windsor 组件配置的示例app.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler,
Castle.Windsor" />
</configSections>
<castle>
<components>
<component id="form.component" type="ServiceDashBored.Main, ServiceDashBored">
<parameters>
<endpoints>
<array>
<item>${http.goodUri.endpoint}</item>
<item>${wmi.freespace.minimum.endpoint}</item>
</array>
</endpoints>
</parameters>
</component>
<component id="logger.component"
service="BitFactory.Logging.Logger, BitFactory.Logging"
type="BitFactory.Logging.FileLogger, BitFactory.Logging">
<parameters>
<aFileName>ServiceDashBored.log</aFileName>
</parameters>
</component>
<component id="http.goodUri.endpoint"
service="Endpoint.IEndpoint, Endpoint" type="Endpoint.HttpEndpoint, Endpoint">
<parameters>
<Name>Http Google</Name>
<UriString>http://www.google.com</UriString>
</parameters>
</component>
<component id="wmi.freespace.minimum.endpoint"
service="Endpoint.IEndpoint, Endpoint" type="Endpoint.WmiEndpoint, Endpoint">
<parameters>
<Name>WMI - Freespace on C: drive at least 10 MB</Name>
<MachineName>\\localhost</MachineName>
<ObjectQueryString>select FreeSpace from Win32_LogicalDisk where
DeviceID='C:'</ObjectQueryString>
<ResultPropertyName>FreeSpace</ResultPropertyName>
<MinimumThreshold>10e6</MinimumThreshold><!-- 10 MB -->
</parameters>
</component>
</components>
</castle>
</configuration>
这很简单——只需定义对象及其依赖项以及任何实例化参数。实例化顶级窗体对象的代码也非常简单:
[STAThread]
static void Main()
{
IWindsorContainer container =
new WindsorContainer(new XmlInterpreter(new ConfigResource("castle")));
// Request the component to use it
var form = (Main)container[typeof(Main)];
Application.Run(form);
}
每个端点都有自己的线程,但为了节省资源,它们都共享一个线程池。这在应用程序启动时通过调用ThreadPool.RegisterWaitForSingleObject
为每个端点完成。
public partial class Main : Form
{
private const int ENDPOINT_POLL_TIME_INTERVAL = 30;
private readonly BindingList<endpointstatus> _endpoints;
private readonly Logger _logger;
public Main(IEnumerable<iendpoint> endpoints, Logger logger)
{
InitializeComponent();
_logger = logger;
_endpoints = new BindingList<endpointstatus>();
endpointBindingSource.DataSource = _endpoints;
foreach (var serviceEndpoint in endpoints)
{
registerServiceEndpoint(serviceEndpoint);
}
}
private void registerServiceEndpoint(IEndpoint endpoint)
{
var endpointStatus = new EndpointStatus(endpoint, _logger);
// Time Interval for each endpoint update
var timeOut = TimeSpan.FromSeconds(ENDPOINT_POLL_TIME_INTERVAL);
// Register a timed callback method in the threadpool.
var waitObject = new AutoResetEvent(false);
ThreadPool.RegisterWaitForSingleObject(
waitObject,
endpointStatus.ThreadPoolCallback,
null,
timeOut,
false
);
_endpoints.Add(endpointStatus);
endpointStatus.PropertyChanged += endpoint_PropertyChanged;
waitObject.Set(); // signal an initial callback when done registering
}
}
未来改进
- 管理钩子。除了能够查询端点的状态外,控制它们也可能很有用。例如,能够直接从 Service DashBored 界面重新启动 Windows 服务。
- 更多端点选项。SNMP 可能对于设备监控来说会很有用。
- WCF 端点定义。让端点实现能够使用 WCF 进行配置。
- 不同的警报选项。除了记录服务可用性和弹出气球图标外,还可以添加其他警报,例如电子邮件、短信和警报器。
- 运行时配置。在应用程序内提供端点配置将提高可用性。我不确定使用当前的依赖注入框架需要多少工作。
如果对后续开发有足够兴趣,我将为该项目设置一个公共版本控制存储库。
引用的项目
- Castle Windsor - 依赖注入
- BitFactory Logging - 日志记录
- HTML Agility Pack - 从 HTML 生成格式良好的 XML
- DryIcons - 精美图标
历史
- 版本 1.0。初始发布