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

可扩展的监控解决方案

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2012年10月22日

CPOL

4分钟阅读

viewsIcon

12188

Intel App Innovation Contest 的参赛作品

引言

该解决方案包含多个组件。一个 Windows 服务(F#,C#)负责监控。它通过反射加载“监控”功能的库,用于实例化具体的“监控器”实例。监控器可以组合成“监控树”类型,使用布尔组合运算符 - 例如,如果您想监控一个进程是否正在运行,但仅在特定时间段内,您可以组合一个“与”监控树,其中包含一个检查进程是否正在运行的监控器,以及一个检查当前日期时间是否在指定时间段内的监控器。该服务通过 WebSocket 以 JSON 格式将监控数据发布到服务器(Azure 工作角色,F#)。客户端(WPF,C#,F#)进行身份验证,并订阅 WebSocket 服务器,在图表中显示监控数据。客户端还可以配置服务,编辑现有监控器,创建新监控器等。通信通过 Azure 服务进行代理。

背景

我选择这个项目参加了应用创新大赛,主要是因为大部分服务/服务器端代码已经写好了。

代码

服务

监控器被声明为一个抽象类,唯一的抽象成员是“SampleFunc”,它是一个接受 string[] 参数并返回 int 的函数。

[<AbstractClass>]
[<DataContract>]
type Monitor (s:MonitorSettings) =
 
let mutable _val = 0
    let threshExceededEvt = new Event<_>()
    abstract member SampleFunc : string[] -> int
    [<DataMember>]
    member x.Name = s.name
    [<DataMember>]
    member x.Threshold = s.threshold
    [<DataMember>]
    member x.Frequency = s.freq
    [<DataMember>]
    member x.Value with get() = _val
    [<DataMember>]
    member x.IsOutOfThreshold = _val > x.Threshold
    member x.Sample() = async {
                               _val <-x.SampleFunc(s.parms)
                               if x.IsOutOfThreshold then
                               threshExceededEvt.Trigger(x) 
                              }
    member x.ThresholdExceeded = threshExceededEvt.Publish
 

这允许使用不同的 SampleFuncs 来实例化类的实例,这些 SampleFuncs 知道如何监控不同的东西。例如,要监控文件大小,您可以提供一个 SampleFunc,如下所示:

let FileSizeFunc(parms:string[])   = let file = FileInfo(parms.[0])
                                     let size = file.Length / 1024L //in KB
                                     int size

而要监控当前时间是否在两个指定时间之间(在 params string[] 中以字符串形式表示):

let DateTimeFunc(parms:string[])   = let strtDte = match DateTime.TryParse(parms.[0]) with
                                                   |true,start -> start
                                                   |false,_    -> new DateTime()
                                     let endDte  = match DateTime.TryParse(parms.[1]) with
                                                   |true,nd    -> nd
                                                   |false,_    -> new DateTime()
                                     if ((strtDte < DateTime.Now) && (DateTime.Now < endDte)) then 1
                                     else 0

Windows 传感器 API

由于本次比赛旨在突出超极本的特殊功能,我将使用传感器 API 实现一些 SampleFuncs。这需要一点调整,因为我们 F# 的爱好者似乎在 WinRT 的世界里被忽视了,至少目前是这样 - 据我所知,无法从 F# 项目中引用 WinRT 库。因此,项目需要是 C#。但是,这会给我们带来另一个问题,因为 F# 函数与 C# Func 委托不同。F# 使用 FSharpFunc<>,所以我们需要将 C# Func 委托转换为 FSharpFunc 类型。这可以使用 FSharp PowerPack 中的扩展来完成。首先,我创建一个 C# 库项目。然后,我需要引用 WinRT 库,如这里所述:http://www.hanselman.com/blog/HowToCallWinRTAPIsInWindows8FromCDesktopApplicationsWinRTDiagram.aspx[^],以便我可以使用传感器 API。基本原理是,您卸载项目,手动编辑 .csproj 文件,并添加:

  <PropertyGroup>
    <TargetPlatformVersion>8.0</TargetPlatformVersion>
  <PropertyGroup>

然后,对于 Func -> FSharpFunc 的转换,我需要引用 FSharp.Core、FSharp.PowerPack 和 FSharp.PowerPack.Linq dlls,并打开命名空间。

using Microsoft.FSharp;
using Microsoft.FSharp.Control;
using Microsoft.FSharp.Core;
using Windows.Devices.Sensors;

现在,我可以将转换器实现为一个扩展:

    static class FuncToFSFuncConverter
    {
        public static FSharpFunc<A, B> ToFastFunc<A, B>(this Func<A, B> f)
        {
            return FuncConvertExtensions.ToFSharpFunc(f);
        }
    }
...并在我的 C# 代码中使用此扩展,例如:
    static class SensorSampleFuncs
    {
        public static FSharpFunc<string[], int> LightMonitorSampleFunc()
        { 
            LightSensor lightsens = LightSensor.GetDefault();
            var reading = (int)lightsens.GetCurrentReading().IlluminanceInLux;
            return (new Func<string[], int>(p => reading)).ToFastFunc();
        }
    }

...这将为我提供一个 SampleFunc,它返回来自光传感器的当前读数(以勒克斯为单位)。

监控树 DU

MonitorTree 类型被声明为一个递归的区分联合类型:

type MonitorTree =
    |And of String * MonitorTree * MonitorTree 
    |Or  of String * MonitorTree * MonitorTree 
    |Not of String * MonitorTree
    |Tip of Monitor
    member self.isAlarming = match self with
                             |And(_,l,r) -> l.isAlarming && r.isAlarming
                             |Or (_,l,r) -> l.isAlarming || r.isAlarming
                             |Not(_,l)   -> not (l.isAlarming)
                             |Tip(mon)   -> mon.IsOutOfThreshold
    member self.Name = match self with
                             |And(n,_,_)
                             |Or (n,_,_)
                             |Not(n,_)   -> n
                             |Tip(mon)   -> mon.Name

因此,MonitorTree 可以是“Tip”(叶子),即单个 Monitor,也可以是一个由字符串(用于名称)和两个 MonitorTree 分支组成的元组,用于 And/Or 类型,或者是一个字符串 * MonitorTree 的元组,用于 Not 类型。其他两个成员使用模式匹配来分解类型,并根据此返回相应的值 - 例如,如果 MonitorTree 是 And 类型,那么 isAlarming 成员当且仅当左右两个树的 isAlarming 成员都为“true”时才为“true”。

服务从 XML 文件读取配置,并以异步任务并行启动监控器列表。

当监控器中的 SampleFunc 函数返回 int 结果时,“根”MonitorTree 父项被序列化为 JSON,并发送到 Azure 服务。

    let serialiser = new JsonSerializer()
    do serialiser.Converters.Add(new UnionTypeConverter())
    do serialiser.Formatting <- Formatting.Indented
    let toJSON obj = let wrtr = new StringWriter()
                     serialiser.Serialize(wrtr,obj)
                     wrtr.ToString()

JSON.Net 序列化器通过自定义的 Converter 进行扩展,该 Converter 实现了区分联合类型的序列化 - 这是从 Robert Pickering 借用的。

https://github.com/robertpi/FsRavenDbTools/blob/master/src/FsRavenDbTools/Converters.fs

WebSocket 客户端使用 Basic Auth 与服务进行身份验证,并且来自标头中的用户名用于在服务器上标记数据,以便可以将正确的数据发布给正确的客户端,并且来自客户端的配置数据可以发送给正确的服务。

let sckt = new ClientWebSocket()
    let authheader = Convert.ToBase64String(Encoding.UTF8.GetBytes(un + ":" + pw))
    do sckt.Options.SetRequestHeader("Authorization","Basic " + authheader)
    let openSckt() =
        do sckt.ConnectAsync(new Uri("wss://server:1234/update"), CancellationToken.None) |> ignore
    do openSckt()
    let resp (json:string) =
        let buf = Encoding.UTF8.GetBytes(json)
        printfn "%s" (sckt.State.ToString())
        match sckt.State with
        |WebSocketState.Open -> do sckt.SendAsync(ArraySegment<byte>(buf),WebSocketMessageType.Text,false,CancellationToken.None) |> ignore
        |WebSocketState.Aborted
        |WebSocketState.CloseReceived
        |WebSocketState.Closed -> try 
                                    do sckt.ConnectAsync(new Uri("wss://server:1234/update"), CancellationToken.None) |> ignore
                                  with
                                  |_ -> printfn "Server sckt closed"
        |_ -> ()
    

服务器

服务器侦听传入的 HTTP 请求,使用 Authorization 标头进行身份验证,如果请求是 WebSocket 请求,则根据 URL(/update 或 /subscr)将用户名和 WebSocket 传递给 MailBoxProcessor 中实现的两个函数之一。

let server = async { 
    use listener = new HttpListener()
    listener.Prefixes.Add(url)
    listener.AuthenticationSchemes <- AuthenticationSchemes.Basic
    listener.Start()
    while true do 
      let! context = listener.AsyncGetContext()
      let isWS = context.Request.IsWebSocketRequest
      let path   = context.Request.Url.LocalPath
      match isWS with
      |false -> {serve static files etc.} ..
      |true -> let! wsctxt = context.AsyncAcceptWebSocket()
               let ws = wsctxt.WebSocket
               let auth = context.Request.Headers.["Authorization"].Split([|' '|]).[1]
               let usrpwd =  Encoding.UTF8.GetString(Convert.FromBase64String(auth)).Split([|':'|])
               let usr,pwd = usrpwd.[0],usrpwd.[1]
               match authenticate(usr,pwd) with
               |true ->match path with
                        |"/update" -> agent.Post(Publish(usr,ws))
                        |"/subscr" -> agent.Post(Subscribe(usr,ws))
                        |_ -> failwith "unknow URL"
               |false->failwith "Did not authenticate!"
      }

 MailBoxProcessor 代理内部维护 2 * Dictionary<string,websocket>(一个用于服务,一个用于客户端),以便它可以将来自服务的传入更新匹配给已连接的“客户端”,并将数据发布给正确的已连接客户端。反之亦然,将来自客户端的传入配置数据匹配给正确的已连接服务。

客户端

客户端实现为一个 WPF 应用程序,结合了 F# 和 C#。与 WebSocket 服务器通信并反序列化数据的代码是用 F# 实现的。当消息到达并被反序列化时,会引发一个事件,C# WPF 应用程序会订阅该事件,以便 UI 可以使用新数据进行更新。 

客户端的界面将实现一些触摸交互,使其在 Windows-8 上更具吸引力……

© . All rights reserved.