65.9K
CodeProject 正在变化。 阅读更多。
Home

服务仪表板

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (8投票s)

2009年1月6日

CC (ASA 2.5)

7分钟阅读

viewsIcon

57275

downloadIcon

1159

一个轻量级的桌面应用程序,帮助您了解网络中的服务器和应用程序的可用性。

ServiceDashBored_src

引言

您的应用程序服务器是否正在运行?您最好去抓住它!这个轻量级的桌面应用程序将帮助您了解网络中的服务器可用性。利用 .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 ProjectWindsor 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 查询以及任何命名空间映射。

ServiceDashBored_XMLNotepad.png

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的列表已绑定到应用程序的DataGridViewEndpointStatus实现了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 进行配置。
  • 不同的警报选项。除了记录服务可用性和弹出气球图标外,还可以添加其他警报,例如电子邮件、短信和警报器。
  • 运行时配置。在应用程序内提供端点配置将提高可用性。我不确定使用当前的依赖注入框架需要多少工作。

如果对后续开发有足够兴趣,我将为该项目设置一个公共版本控制存储库。

引用的项目

历史

  • 版本 1.0。初始发布
© . All rights reserved.