///|
pub(all) enum DiagnosticSeverity {
DiagnosticInfo
DiagnosticWarning
DiagnosticError
} derive(Eq, @debug.Debug)
///|
pub(all) struct DiagnosticEntry {
severity : DiagnosticSeverity
code : String
subject : String
message : String
} derive(Eq, @debug.Debug)
///|
pub(all) struct DiagnosticReport {
name : String
checked : Int
infos : Int
warnings : Int
errors : Int
entries : Array[DiagnosticEntry]
} derive(Eq, @debug.Debug)
///|
pub fn diagnostic_report(
name? : StringView = "diagnostics",
) -> DiagnosticReport {
{
name: name.to_owned(),
checked: 0,
infos: 0,
warnings: 0,
errors: 0,
entries: [],
}
}
///|
pub fn DiagnosticReport::add(
self : DiagnosticReport,
severity : DiagnosticSeverity,
code : StringView,
subject : StringView,
message : StringView,
) -> DiagnosticReport {
let entries = self.entries.copy()
entries.push({
severity,
code: code.to_owned(),
subject: subject.to_owned(),
message: message.to_owned(),
})
let (infos, warnings, errors) = match severity {
DiagnosticInfo => (self.infos + 1, self.warnings, self.errors)
DiagnosticWarning => (self.infos, self.warnings + 1, self.errors)
DiagnosticError => (self.infos, self.warnings, self.errors + 1)
}
{ ..self, checked: self.checked + 1, infos, warnings, errors, entries }
}
///|
pub fn DiagnosticReport::info(
self : DiagnosticReport,
code : StringView,
subject : StringView,
message : StringView,
) -> DiagnosticReport {
self.add(DiagnosticInfo, code, subject, message)
}
///|
pub fn DiagnosticReport::warning(
self : DiagnosticReport,
code : StringView,
subject : StringView,
message : StringView,
) -> DiagnosticReport {
self.add(DiagnosticWarning, code, subject, message)
}
///|
pub fn DiagnosticReport::error(
self : DiagnosticReport,
code : StringView,
subject : StringView,
message : StringView,
) -> DiagnosticReport {
self.add(DiagnosticError, code, subject, message)
}
///|
pub fn DiagnosticReport::merge(
self : DiagnosticReport,
other : DiagnosticReport,
) -> DiagnosticReport {
let mut report = self
for entry in other.entries {
report = report.add(
entry.severity,
entry.code,
entry.subject,
entry.message,
)
}
report
}
///|
pub fn DiagnosticReport::ok(self : DiagnosticReport) -> Bool {
self.errors == 0
}
///|
pub fn DiagnosticReport::has_warnings(self : DiagnosticReport) -> Bool {
self.warnings > 0
}
///|
pub fn DiagnosticReport::codes(self : DiagnosticReport) -> Array[String] {
self.entries.map(entry => entry.code)
}
///|
pub fn DiagnosticReport::summary(self : DiagnosticReport) -> String {
"diagnostics=\{escape_wire_text(self.name)} checked=\{self.checked} infos=\{self.infos} warnings=\{self.warnings} errors=\{self.errors}"
}
///|
pub fn DiagnosticReport::lines(self : DiagnosticReport) -> Array[String] {
self.entries.map(entry => entry.to_wire())
}
///|
pub fn DiagnosticReport::manifest_lines(
self : DiagnosticReport,
) -> Array[String] {
let lines : Array[String] = []
lines.push(self.summary())
for entry in self.entries {
lines.push(entry.to_wire())
}
lines
}
///|
pub fn DiagnosticReport::manifest(self : DiagnosticReport) -> String {
self.manifest_lines().join("\n")
}
///|
pub fn DiagnosticEntry::to_wire(self : DiagnosticEntry) -> String {
"severity=\{self.severity.to_wire()};code=\{escape_wire_text(self.code)};subject=\{escape_wire_text(self.subject)};message=\{escape_wire_text(self.message)}"
}
///|
pub fn DiagnosticSeverity::to_wire(self : DiagnosticSeverity) -> String {
match self {
DiagnosticInfo => "info"
DiagnosticWarning => "warning"
DiagnosticError => "error"
}
}
///|
pub fn diagnose_bus(name : StringView, bus : Bus) -> DiagnosticReport {
let audit = bus.audit()
let stats = bus.delivery_stats()
let mut report = diagnostic_report(name~)
if audit.subscriptions == 0 {
report = report.error(
"bus.subscriptions", "bus", "bus has no subscriptions",
)
} else {
report = report.info(
"bus.subscriptions",
"bus",
"subscriptions=\{audit.subscriptions}",
)
}
if audit.enabled == 0 {
report = report.error(
"bus.enabled", "bus", "bus has no enabled subscriptions",
)
} else {
report = report.info("bus.enabled", "bus", "enabled=\{audit.enabled}")
}
if audit.guarded == 0 && audit.subscriptions > 0 {
report = report.warning(
"bus.guards", "bus", "no subscription guards configured",
)
} else {
report = report.info("bus.guards", "bus", "guarded=\{audit.guarded}")
}
if bus.dead_letter_count() > 0 {
report = report.warning(
"bus.dead_letters",
"bus",
"dead_letters=\{bus.dead_letter_count()}",
)
} else {
report = report.info("bus.dead_letters", "bus", "dead_letters=0")
}
if stats.failure_count() > 0 {
report = report.warning("bus.delivery_stats", "bus", stats.to_wire())
} else {
report = report.info("bus.delivery_stats", "bus", stats.to_wire())
}
for issue in audit.issues {
report = report.warning("bus.audit", "bus", issue)
}
report
}
///|
pub fn diagnose_batch(
name : StringView,
batch : EventBatch,
) -> DiagnosticReport {
let mut report = diagnostic_report(name~)
if batch.is_empty() {
report = report.warning("batch.events", batch.name, "batch is empty")
} else {
report = report.info("batch.events", batch.name, "events=\{batch.len()}")
}
let invalid_topics = collect_invalid_event_topics(batch.events)
if invalid_topics.length() == 0 {
report = report.info(
"batch.topics",
batch.name,
"all event topics are valid",
)
} else {
for topic in invalid_topics {
report = report.error("batch.topic.invalid", batch.name, topic)
}
}
let duplicate_count = count_duplicate_fingerprints(batch.events)
if duplicate_count == 0 {
report = report.info(
"batch.fingerprints",
batch.name,
"no duplicate fingerprints",
)
} else {
report = report.warning(
"batch.fingerprints",
batch.name,
"duplicate_fingerprints=\{duplicate_count}",
)
}
let topics = batch.topic_counts()
if topics.length() > 0 {
report = report.info(
"batch.topic_count",
batch.name,
"topics=\{topics.length()}",
)
}
report
}
///|
pub fn diagnose_batch_schema(
name : StringView,
batch : EventBatch,
schema : EventSchema,
) -> DiagnosticReport {
let mut report = diagnose_batch(name, batch)
let validation = batch.validate(schema)
if validation.invalid == 0 {
report = report.info("schema.validation", schema.name, validation.summary())
} else {
report = report.error(
"schema.validation",
schema.name,
validation.summary(),
)
for issue in validation.issues {
report = report.error("schema.issue", issue.path, issue.message)
}
}
report
}
///|
pub fn diagnose_batch_rules(
name : StringView,
batch : EventBatch,
rules : RuleSet,
) -> DiagnosticReport {
let mut report = diagnose_batch(name, batch)
let decisions = batch.evaluate_rules(rules)
if decisions.rejected == 0 {
report = report.info("rules.evaluation", rules.name, decisions.summary())
} else {
report = report.warning("rules.evaluation", rules.name, decisions.summary())
for decision in decisions.decisions {
if decision.decision is Reject(reason) {
report = report.warning("rules.rejected", decision.event_id, reason)
}
}
}
report
}
///|
pub fn diagnose_snapshot(
name : StringView,
snapshot : EventSnapshot,
) -> DiagnosticReport {
let mut report = diagnostic_report(name~)
if snapshot.event_count == 0 {
report = report.warning(
"snapshot.events",
snapshot.name,
"snapshot is empty",
)
} else {
report = report.info(
"snapshot.events",
snapshot.name,
"events=\{snapshot.event_count}",
)
}
if snapshot.topic_counts.length() == 0 {
report = report.warning(
"snapshot.topics",
snapshot.name,
"no topics recorded",
)
} else {
report = report.info(
"snapshot.topics",
snapshot.name,
"topics=\{snapshot.topic_counts.length()}",
)
}
if snapshot.dead_letter_count > 0 {
report = report.warning(
"snapshot.dead_letters",
snapshot.name,
"dead_letters=\{snapshot.dead_letter_count}",
)
} else {
report = report.info(
"snapshot.dead_letters",
snapshot.name,
"dead_letters=0",
)
}
if snapshot.delivery_stats.failure_count() > 0 {
report = report.warning(
"snapshot.delivery_stats",
snapshot.name,
snapshot.delivery_stats.to_wire(),
)
} else {
report = report.info(
"snapshot.delivery_stats",
snapshot.name,
snapshot.delivery_stats.to_wire(),
)
}
report
}
///|
pub fn diagnose_pipeline_run(
name : StringView,
run : PipelineRun,
) -> DiagnosticReport {
let mut report = diagnostic_report(name~)
if run.stages.length() == 0 {
report = report.warning(
"pipeline.stages",
run.name,
"pipeline has no stages",
)
} else {
report = report.info(
"pipeline.stages",
run.name,
"stages=\{run.stages.length()}",
)
}
for stage in run.stages {
match stage.status {
StagePassed =>
report = report.info(
"pipeline.stage",
"stage:\{stage.index}",
stage.summary,
)
StageWarning =>
report = report.warning(
"pipeline.stage",
"stage:\{stage.index}",
stage.summary,
)
StageFailed =>
report = report.error(
"pipeline.stage",
"stage:\{stage.index}",
stage.summary,
)
}
}
report
}
///|
pub fn diagnose_retention(
name : StringView,
retention : RetentionReport,
) -> DiagnosticReport {
let mut report = diagnostic_report(name~)
if retention.scanned == 0 {
report = report.warning(
"retention.scanned",
retention.policy,
"no events scanned",
)
} else {
report = report.info(
"retention.scanned",
retention.policy,
"scanned=\{retention.scanned}",
)
}
if retention.kept == 0 && retention.scanned > 0 {
report = report.warning(
"retention.kept",
retention.policy,
"no events retained",
)
} else {
report = report.info(
"retention.kept",
retention.policy,
"kept=\{retention.kept}",
)
}
if retention.dropped > 0 {
report = report.info(
"retention.dropped",
retention.policy,
"dropped=\{retention.dropped}",
)
} else {
report = report.info("retention.dropped", retention.policy, "dropped=0")
}
for topic in retention.topic_summaries {
if topic.kept == 0 && topic.dropped > 0 {
report = report.warning("retention.topic", topic.topic, topic.to_wire())
} else {
report = report.info("retention.topic", topic.topic, topic.to_wire())
}
}
report
}
///|
fn collect_invalid_event_topics(events : Array[Envelope]) -> Array[String] {
let invalid : Array[String] = []
for event in events {
match topic_segments(event.topic) {
Ok(_) => ()
Err(err) =>
invalid.push(
"event=\{escape_wire_text(event.id)};topic=\{escape_wire_text(event.topic)};message=\{escape_wire_text(err.message())}",
)
}
}
invalid
}
///|
fn count_duplicate_fingerprints(events : Array[Envelope]) -> Int {
let seen : Array[String] = []
let duplicates : Array[String] = []
for event in events {
let fingerprint = event.fingerprint()
if seen.contains(fingerprint) {
if !duplicates.contains(fingerprint) {
duplicates.push(fingerprint)
}
} else {
seen.push(fingerprint)
}
}
duplicates.length()
}