///|
/// Health rules applied to protocol transactions.
pub enum MonitorRule {
CompletionRequired
MaximumLatency
MinimumGap
PayloadRequired
AcceptanceRequired
ThroughputMinimum
} derive(Debug, Eq)
///|
/// Severity of a protocol monitor finding.
pub enum MonitorSeverity {
MonitorInfo
MonitorWarning
MonitorError
} derive(Debug, Eq)
///|
/// One transaction monitor finding.
pub struct MonitorFinding {
channel : String
rule : MonitorRule
severity : MonitorSeverity
transaction_index : Int?
time : Int64
observed : String
expected : String
message : String
} derive(Debug)
///|
/// Monitor policy for a protocol channel.
pub struct MonitorOptions {
require_completed : Bool
maximum_latency : Int64?
minimum_gap : Int64?
require_payload : Bool
require_acceptance : Bool
minimum_throughput : Double?
} derive(Debug)
///|
/// Protocol monitor output.
pub struct MonitorReport {
channel : String
transactions : Array[ProtocolTransaction]
findings : Array[MonitorFinding]
passed : Bool
score : Int
} derive(Debug)
///|
/// List monitor rules.
pub fn monitor_rule_catalog() -> Array[MonitorRule] {
[
CompletionRequired,
MaximumLatency,
MinimumGap,
PayloadRequired,
AcceptanceRequired,
ThroughputMinimum,
]
}
///|
/// List monitor severities.
pub fn monitor_severity_catalog() -> Array[MonitorSeverity] {
[MonitorInfo, MonitorWarning, MonitorError]
}
///|
/// Create a permissive monitor policy.
pub fn default_monitor_options() -> MonitorOptions {
{
require_completed: true,
maximum_latency: None,
minimum_gap: None,
require_payload: false,
require_acceptance: false,
minimum_throughput: None,
}
}
///|
/// Require a maximum transaction latency.
pub fn MonitorOptions::with_max_latency(
self : MonitorOptions,
limit : Int64?,
) -> MonitorOptions {
{ ..self, maximum_latency: limit }
}
///|
/// Require non-empty payloads for accepted transfers.
pub fn MonitorOptions::with_payloads(
self : MonitorOptions,
required : Bool,
) -> MonitorOptions {
{ ..self, require_payload: required }
}
///|
/// Require a minimum gap between completed transactions.
pub fn MonitorOptions::with_gap(
self : MonitorOptions,
gap : Int64?,
) -> MonitorOptions {
{ ..self, minimum_gap: gap }
}
///|
/// Check one decoded protocol channel.
pub fn monitor_protocol(
trace : Trace,
channel : ProtocolChannel,
options : MonitorOptions,
) -> MonitorReport {
let transactions = protocol_transactions(trace, channel)
let findings : Array[MonitorFinding] = []
for transaction in transactions {
if options.require_completed && !transaction.completed {
findings.push({
channel: channel.name,
rule: CompletionRequired,
severity: MonitorError,
transaction_index: Some(transaction.index),
time: transaction.start,
observed: "incomplete",
expected: "completed",
message: "transaction has no completion event",
})
}
match options.maximum_latency {
Some(limit) if transaction.completed && transaction.latency > limit =>
findings.push({
channel: channel.name,
rule: MaximumLatency,
severity: MonitorError,
transaction_index: Some(transaction.index),
time: transaction.end,
observed: transaction.latency.to_string(),
expected: limit.to_string(),
message: "transaction latency exceeded limit",
})
_ => ()
}
if options.require_payload && transaction.payload is None {
findings.push({
channel: channel.name,
rule: PayloadRequired,
severity: MonitorWarning,
transaction_index: Some(transaction.index),
time: transaction.start,
observed: "null",
expected: "payload",
message: "transaction did not carry a payload",
})
}
if options.require_acceptance && !transaction.accepted {
findings.push({
channel: channel.name,
rule: AcceptanceRequired,
severity: MonitorError,
transaction_index: Some(transaction.index),
time: transaction.start,
observed: "rejected",
expected: "accepted",
message: "transaction was not accepted",
})
}
}
match options.minimum_gap {
Some(gap) => {
let mut previous : Int64? = None
for transaction in transactions {
match previous {
Some(end) if transaction.start - end < gap =>
findings.push({
channel: channel.name,
rule: MinimumGap,
severity: MonitorWarning,
transaction_index: Some(transaction.index),
time: transaction.start,
observed: (transaction.start - end).to_string(),
expected: gap.to_string(),
message: "transactions are closer than the configured gap",
})
_ => ()
}
previous = Some(transaction.end)
}
}
None => ()
}
match options.minimum_throughput {
Some(minimum) => {
let actual = if trace.end_time > 0L {
transactions.length().to_double() / trace.end_time.to_double()
} else {
0.0
}
if actual < minimum {
findings.push({
channel: channel.name,
rule: ThroughputMinimum,
severity: MonitorError,
transaction_index: None,
time: trace.end_time,
observed: actual.to_string(),
expected: minimum.to_string(),
message: "channel throughput is below the configured minimum",
})
}
}
None => ()
}
let errors = findings
.filter(fn(item) { item.severity is MonitorError })
.length()
{
channel: channel.name,
transactions,
findings,
passed: errors == 0,
score: Int::max(0, 100 - errors * 20),
}
}
///|
/// Return whether the monitor passed without error findings.
pub fn MonitorReport::is_success(self : MonitorReport) -> Bool {
self.passed
}
///|
/// Return error and warning findings in stable order.
pub fn MonitorReport::findings_by_severity(
self : MonitorReport,
severity : MonitorSeverity,
) -> Array[MonitorFinding] {
self.findings.filter(fn(item) { item.severity == severity })
}
///|
/// Render monitor findings as CSV.
pub fn monitor_findings_csv(findings : Array[MonitorFinding]) -> String {
let out = StringBuilder()
out.write_string(
"channel,rule,severity,index,time,observed,expected,message\n",
)
for finding in findings {
out.write_string(
escape_text(finding.channel) +
"," +
monitor_rule_name(finding.rule) +
"," +
monitor_severity_name(finding.severity) +
",",
)
out.write_string(
monitor_optional_index(finding.transaction_index) +
"," +
finding.time.to_string() +
"," +
escape_text(finding.observed) +
"," +
escape_text(finding.expected) +
"," +
escape_text(finding.message) +
"\n",
)
}
out.to_string()
}
///|
/// Render a protocol monitor summary.
pub fn MonitorReport::to_text(self : MonitorReport) -> String {
self.channel +
" passed=" +
self.passed.to_string() +
" score=" +
self.score.to_string() +
" findings=" +
self.findings.length().to_string()
}
///|
/// Render a protocol monitor summary as JSON.
pub fn MonitorReport::to_json(self : MonitorReport) -> String {
let findings = self.findings.map(monitor_finding_json).join(",")
"{\"channel\":\"" +
escape_text(self.channel) +
"\",\"passed\":" +
self.passed.to_string() +
" ,\"score\":" +
self.score.to_string() +
",\"findings\":[" +
findings +
"]}"
}
///|
/// Name a monitor rule.
pub fn monitor_rule_name(rule : MonitorRule) -> String {
match rule {
CompletionRequired => "completion-required"
MaximumLatency => "maximum-latency"
MinimumGap => "minimum-gap"
PayloadRequired => "payload-required"
AcceptanceRequired => "acceptance-required"
ThroughputMinimum => "throughput-minimum"
}
}
///|
/// Name a monitor severity.
pub fn monitor_severity_name(value : MonitorSeverity) -> String {
match value {
MonitorInfo => "info"
MonitorWarning => "warning"
MonitorError => "error"
}
}
///|
/// Encode an optional transaction index.
fn monitor_optional_index(value : Int?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode one finding as JSON.
fn monitor_finding_json(item : MonitorFinding) -> String {
"{\"rule\":\"" +
monitor_rule_name(item.rule) +
"\",\"severity\":\"" +
monitor_severity_name(item.severity) +
"\",\"index\":" +
monitor_optional_index_json(item.transaction_index) +
",\"time\":" +
item.time.to_string() +
",\"observed\":\"" +
escape_text(item.observed) +
"\",\"expected\":\"" +
escape_text(item.expected) +
"\",\"message\":\"" +
escape_text(item.message) +
"\"}"
}
///|
/// Encode an optional index as JSON.
fn monitor_optional_index_json(value : Int?) -> String {
match value {
Some(item) => item.to_string()
None => "null"
}
}