XQuery/正常运行时间监控
您希望监控多个网站或网络服务的服务可用性。您希望使用 XQuery 完成所有这些操作并将结果存储在 XML 文件中。您还希望看到正常运行时间的“仪表板”图形显示。
有几种商业服务(Pingdom,Host-tracker),它们会根据正常运行时间和响应时间监控您的网站的性能。
尽管可靠服务的生产需要一个服务器网络,但基本功能可以使用 XQuery 在几个脚本中执行。
这种方法侧重于网页的正常运行时间和响应时间。核心方法是使用 eXist 作业调度程序定期执行 XQuery 脚本。此脚本对 URI 执行 HTTP GET 并将网站的 statusCode 记录到 XML 数据文件中。
操作的计时是为了从经过的时间(在使用率低的服务器上有效)收集响应时间,并存储测试结果。然后可以从测试结果运行报告,并在观察到网站关闭时发送警报。
即使是一个原型,对细粒度数据的访问已经揭示了大学中某个网站的一些响应时间问题。
此 ER 模型是在 QSEE 中创建的,QSEE 还可以生成 SQL 或 XSD。
在此表示法中,条形表示 Test 是一个弱实体,存在依赖于 Watch。
由于 Test 依赖于 Watch,因此 Watch-Test 关系可以作为组合来实现,多个 Test 元素包含在 Log 元素中,Log 元素本身是 Watch 元素的子元素。测试按时间顺序存储。
两种可能的方法
- 将 Log 作为元素添加到 Watch 的基本数据中
Watch uri name Log Test
- 构造一个 Watch 元素,该元素包含 Watch 基本数据作为 WatchSpec 和 Log
Watch WatchSpec (the Watch entity ) uri name Log
第二种方法保留原始 Watch 实体作为节点,并且也适合使用 XForms,允许将整个 WatchSpec 节点包含在表单中。但是,它引入了一个难以命名的中间元素,并导致如下路径:
$watch/WatchSpec/uri
何时
$watch/uri would be more natural.
这里我们选择第一种方法,理由是,为了更简单地实现特定接口,不希望引入中间元素。
观察实体可以作为文件或集合中的元素来实现。这里我们选择将 Watch 作为文档中 Monitor 容器中的元素来实现。但是,这是一个艰难的决定,XQuery 代码应该尽可能地隐藏此决定。
观察属性映射到元素。测试属性映射到属性。
QSEE 将生成一个XML 模式。在此映射中,所有关系都使用外键实现,使用 key 和 keyref 来描述关系。在这种情况下,需要编辑模式以通过组合来实现 Watch-Test 关系。
此模式是通过 Trang(在 Oxygen 中)从示例文档生成的,示例文档是在系统运行时创建的。
- 紧凑型 Relax NG
element Monitor { element Watch { element uri { xsd:anyURI }, element name { text }, element Log { element Test { attribute at { xsd:dateTime }, attribute responseTime { xsd:integer }, attribute statusCode { xsd:integer } }+ } }+ }
- XML 模式
编辑 QSEE 生成的模式会导致包含对 statusCodes 的限制的模式。
XQuery 脚本将 XML 模式(或其子集)转换为符合该模式的文档的随机实例。
测试按属性 at 升序排列的约束在此模式中没有定义。生成器需要通过有关字符串长度和枚举值、迭代和可选元素的概率分布的附加信息来帮助生成有用的测试数据。
CREATE TABLE Watch(
uri VARCHAR(8) NOT NULL,
name VARCHAR(8) NOT NULL,
CONSTRAINT pk_Watch PRIMARY KEY (uri)
) ;
CREATE TABLE Test(
at TIMESTAMP NOT NULL,
responseTime INTEGER NOT NULL,
statusCode INTEGER NOT NULL,
uri VARCHAR(8) NOT NULL,
CONSTRAINT pk_Test PRIMARY KEY (at,uri)
) ;
ALTER TABLE Test
ADD INDEX (uri),
ADD CONSTRAINT fk1_Test_to_Watch FOREIGN KEY(uri)
REFERENCES Watch(uri)
ON DELETE RESTRICT
ON UPDATE RESTRICT;
在关系型实现中,Watch 的主键 **uri** 是 Test 的外键。将系统生成的 ID 添加到此有意义的 URI 中以代替它将有利于消除冗余并减小外键的大小。但是,需要一种机制来分配唯一的 ID。
实现
[edit | edit source]依赖
[edit | edit source]eXistdb 模块
[edit | edit source]- xmldb 用于数据库更新和登录
- datetime 用于日期格式化
- util - 用于 system-time 函数
- httpclient - 用于 HTTP GET
- scheduler - 用于调度监控任务
- validation - 用于数据库验证
其他
[edit | edit source]- Google 图表
函数
[edit | edit source]单个 XQuery 模块中的函数。
module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor";
数据库访问
[edit | edit source]访问监控数据库,该数据库可能是本地数据库文档或远程文档。
declare function monitor:get-watch-list($base as xs:string) as element(Watch)* {
doc($base)/Monitor/Watch
};
特定的 Watch 实体由其 URI 标识
let $wl:= monitor:get-watch-list("/db/Wiki/Monitor3/monitor.xml")
对 Watch 的进一步引用是通过引用进行的。例如
declare function monitor:get-watch-by-uri($base as xs:string, $uri as xs:string) as element(Watch)* {
monitor:get-watch-list($base)[uri=$uri]
};
执行测试
[edit | edit source]测试对 uri 进行 HTTP GET。GET 由对 util:system-time() 的调用括起来,以计算以毫秒为单位的经过的挂钟时间。测试报告包含 statusCode。
declare function monitor:run-test($watch as element(Watch)) as element(Test) {
let $uri := $watch/uri
let $start := util:system-time()
let $response := httpclient:get(xs:anyURI($uri),false(),())
let $end := util:system-time()
let $runtimems := (($end - $start) div xs:dayTimeDuration('PT1S')) * 1000
let $statusCode := string($response/@statusCode)
return
<Test at="{current-dateTime()}" responseTime="{$runtimems}" statusCode="{$statusCode}"/>
};
生成的测试将追加到日志的末尾
declare function monitor:put-test($watch as element(Watch), $test as element(Test)) {
update insert $test into $watch/Log
};
要执行测试,脚本将登录,遍历 Watch 实体,并为每个实体执行测试并存储结果
import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";
let $login := xmldb:login("/db/","user","password")
let $base := "/db/Wiki/Monitor3/Monitor.xml"
for $watch in monitor:get-watch-list($base)
let $test := monitor:run-test($watch)
let $update :=monitor:put-test($watch,$test)
return $update
作业调度
[edit | edit source]计划每 5 分钟运行此脚本的作业。
let $login := xmldb:login("/db","user","password")
return scheduler:schedule-xquery-cron-job("/db/Wiki/Monitor/runTests.xq" , "0 0/5 * * * ?")
索引页面
[edit | edit source]索引页面基于提供的 Monitor 文档,默认情况下为生产数据库。
import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";
declare option exist:serialize "method=xhtml media-type=text/html";
declare variable $heading := "Monitor Index";
declare variable $base := request:get-parameter("base","/db/Wiki/Monitor3/Monitor.xml");
<html>
<head>
<title>{$heading}</title>
</head>
<body>
<h1>{$heading}</h1>
<ul>
{for $watch in monitor:get-watch-list($base)
return
<li>{string($watch/name)}   
<a href="report.xq?base={encode-for-uri($base)}&uri={encode-for-uri($watch/uri)}">Report</a>
</li>
}
</ul>
</body>
</html>
在此实现中,监控文档的 URI 通过 URI 传递给依赖脚本。另一种方法是通过会话变量传递此数据。
报告
[edit | edit source]报告借鉴了 Watch 的 Tests 日志
declare function monitor:get-tests($watch as element(Watch)) as element(Test)* {
$watch/Log/Test
};
概述报告
[edit | edit source]基本报告显示有关受监控 URI 的摘要数据以及响应时间随时间变化的嵌入图表。正常运行时间是状态代码为 200 的测试与测试总数的比率。
import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";
declare option exist:serialize "method=xhtml media-type=text/html";
let $base := request:get-parameter("base",())
let $uri:= request:get-parameter("uri",())
let $watch :=monitor:get-watch-by-uri($base,$uri)
let $tests := monitor:get-tests($watch)
let $countAll := count($tests)
let $uptests := $tests[@statusCode="200"]
let $last24hrs := $tests[position() >($countAll - 24 * 12)]
let $heading := concat("Performance results for ", string($watch/name))
return
<html>
<head>
<title>{$heading}</title>
</head>
<body>
<h3>
<a href="index.xq">Index</a>
</h3>
<h1>{$heading}</h1>
<h2><a href="{$watch/uri}">{string($watch/uri)}</a></h2>
{if (empty($tests))
then ()
else
<div>
<table border="1">
<tr>
<th>Monitoring started</th>
<td> {datetime:format-dateTime($tests[1]/@at,"EE dd/MM HH:mm")}</td>
</tr>
<tr>
<th>Latest test</th>
<td> {datetime:format-dateTime($tests[last()]/@at,"EE dd/MM HH:mm")}</td>
</tr>
<tr>
<th>Minimum response time </th>
<td> {min($tests/@responseTime)} ms </td>
</tr>
<tr>
<th>Average response time</th>
<td> { round(sum($tests/@responseTime) div count($tests))} ms</td>
</tr>
<tr>
<th>Maximum response time </th>
<td> {max($tests/@responseTime)} ms</td>
</tr>
<tr>
<th>Uptime</th>
<td>{round(count($uptests) div count($tests) * 100) } %</td>
</tr>
<tr>
<th>Raw Data </th>
<td>
<a href="testData.xq?base={encode-for-uri($base)}&uri={encode-for-uri($uri)}">View</a>
</td>
</tr>
<tr>
<th>Response Distribution </th>
<td>
<a href="responseDistribution.xq?base={encode-for-uri($base)}&uri={encode-for-uri($uri)}">View</a>
</td>
</tr>
</table>
<h2>Last 24 hours </h2>
{monitor:responseTime-chart($last24hrs)}
<h2>1 hour averages </h2>
{monitor:responseTime-chart(monitor:average($tests,12))}
</div>
}
</body>
</html>
响应时间图
[edit | edit source]该图表使用 Google 图表 API 生成。从 0 到 100 的默认垂直刻度适合典型的响应时间。在这个简单的例子中,图表是未修饰的或未解释的。
declare function monitor:responseTime-chart($test as element(Test)* ) as element(img) {
let $points :=
string-join($test/@responseTime,",")
let $chartType := "lc"
let $chartSize := "300x200"
let $uri := concat("http://chart.apis.google.com/chart?",
"cht=",$chartType,"&chs=",$chartSize,"&chd=t:",$points)
return
<img src="{$uri}"/>
};
响应时间频率分布
[edit | edit source]响应时间的频率分布总结了响应时间。首先,分布本身被计算为一系列组。间隔计算很粗略,使用 11 个组来适应 Google 图表。
declare function monitor:response-distribution($test as element(Test)* ) as element(Distribution) {
let $times := $test/@responseTime
let $min := min($times)
let $max := max($times)
let $range := $max - $min
let $step := round( $range div 10)
return
<Distribution>
{
for $i in (0 to 10)
let $low := $min + $i * $step
let $high :=$low + $step
return
<Group i="{$i}" mid="{round(($low + $high ) div 2)}" count="{ count($times[. >= $low] [. < $high]) }"/>
}
</Distribution>
};
然后可以将此分组分布绘制为条形图。在这种情况下需要缩放。
declare function monitor:distribution-chart($distribution as element(Distribution)) as element(img) {
let $maxcount := max($distribution/Group/@count)
let $scale :=100 div $maxcount
let $points :=
string-join( $distribution/Group/xs:string($scale * @count),",")
let $chartType := "bvs"
let $chartSize := "300x200"
let $uri := concat("http://chart.apis.google.com/chart?",
"cht=",$chartType,"&chs=",$chartSize,"&chd=t:",$points)
return
<img src="{$uri}"/>
};
最后是用于创建页面的脚本
import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";
declare option exist:serialize "method=xhtml media-type=text/html";
let $base := request:get-parameter("base",())
let $uri:= request:get-parameter("uri",())
let $watch := monitor:get-watch($base,$uri)
let $tests := monitor:get-tests($watch)
let $heading := concat("Distribution for ", string($watch/name))
let $distribution := monitor:response-distribution($tests)
return
<html>
<head>
<title>{$heading}</title>
</head>
<body>
<h1>{$heading}</h1> {monitor:distribution-chart($distribution)} <br/>
<table border="1">
<tr>
<th>I </th>
<th>Mid</th>
<th>Count</th>
</tr> {for $group in $distribution/Group return <tr>
<td>{string($group/@i)}</td>
<td>{string($group/@mid)}</td>
<td>{string($group/@count)}</td>
</tr> } </table>
</body>
</html>
验证
[edit | edit source]eXist 模块提供了用于根据模式验证文档的函数。Monitor 文档链接到模式
let $doc := "/db/Wiki/Monitor3/Monitor.xml"
return
<report>
<document>{$doc}</document>
{validation:validate-report(doc($doc))}
</report>
或者,可以根据任何模式验证文档
let $schema := "http://www.cems.uwe.ac.uk/xmlwiki/Monitor3/trangmonitor.xsd"
let $doc := "/db/Wiki/Monitor3/Monitor.xml"
return
<report>
<document>{$doc}</document>
<schema>{$schema}</schema>
{validation:validate-report(doc($doc),xs:anyURI($schema))}
</report>
这用于检查随机生成的实例是否有效
let $schema := request:get-parameter("schema",())
let $file := doc(concat("http://www.cems.uwe.ac.uk/xmlwiki/XMLSchema/schema2instance.xq?file=",$schema))
return
<result>
<schema>{$schema}</schema>
{validation:validate-report($file,xs:anyURI($schema))}
{$file}
</result>
停机警报
[edit | edit source]监控的目的是向负责网站的人员发出故障警报。此类警报可能是通过短信、电子邮件或其他一些渠道发送的。Watch 实体需要用配置参数进行增强。
检查是否失败
[edit | edit source]首先,有必要计算网站是否已关闭。如果过去 $watch/fail-minutes 中的所有测试都没有返回状态代码 200,则 monitor:failing() 返回 true()。
declare function monitor:failing($watch as element(Watch)) as xs:boolean {
let $now := current-dateTime()
let $lastTestTime := $now - $watch/failMinutes * xs:dayTimeDuration("PT1M")
let $recentTests := $watch/Log/Test[@at > $lastTestTime]
return
every $t in $recentTests satisfies
not($t/statusCode = "200")
};
检查是否已发送警报
[edit | edit source]如果此测试由计划的作业重复执行,则可以在适当的通道上生成警报消息。但是,警报消息将在条件为真的每次都发送。最好减少发送警报的频率。一种方法是将警报元素添加到日志中,与测试交织在一起。这不会影响访问测试的代码,但允许我们在最近发送过警报时抑制警报。如果在过去 $watch/alert-minutes 中发送过警报,则 alert-sent() 将为 true。
declare function monitor:alert-sent($watch as element(Watch) as xs:boolean ) {
let $now := current-dateTime()
let $lastAlertTime := $now - $watch/alertMinutes * xs:dayTimeDuration("PT1M")
let $recentAlerts := $watch/Log/Alert[@at > $lastAlertTime]
return
exists($recentAlerts)
};
更改通知任务
[edit | edit source]用于检查监控日志的任务将遍历 Watches,并为每个 Watch 检查它是否失败,但在此期间没有发送警报。如果是,则将构建一条消息并将警报元素添加到日志中。使用日志来记录警报事件意味着不需要保留其他状态,并且此任务执行的频率与警报周期无关。
import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";
let $login := xmldb:login("/db/","user","password")
let $base := "/db/Wiki/Monitor3/Monitor.xml"
for $watch in monitor:get-watch-list($base)
return
if (monitor:failing($watch) and not(monitor:alert-sent($watch)))
then
let $update := update insert <Alert at="{current-dateTime()}"/> into $watch/Log
let $alert := monitor:send-alert($watch,$message)
return true()
else false()
讨论
[edit | edit source]警报事件可以添加到单独的 AlertLog 中,但添加一类新事件可能比为每个事件创建单独的序列更容易。还可能存在测试和事件之间的顺序关系有用的情况。
[重新设计的架构]
待办事项
[edit | edit source]- 添加创建/编辑 Watch
- 检测丢失的测试
- 通过在分析之前按日期过滤测试来支持对日期范围的分析
- 改进图表的显示效果