upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/purgatory/mod.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-23 11:41:10 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-27 20:37:58 +0000
commit73a366cbd7be4edf9c74194cd0891c80a15236a5 (patch)
treec4a631a28496d48ad2bce6c2dfd4eff7ae0e4866 /src/purgatory/mod.rs
parent28cc7820953efeafb2bc4d41ebcf3d682da86711 (diff)
Add structured logging for migration analysis
- Add [PARSE_FAIL] logging when event parsing fails - Add [PURGATORY_EXPIRED] logging when repos expire from purgatory - Logs include: kind, event_id, repo, npub, reason - Supports Phase 4 migration scripts (30-extract-*.sh) - All 382 tests pass
Diffstat (limited to 'src/purgatory/mod.rs')
-rw-r--r--src/purgatory/mod.rs103
1 files changed, 86 insertions, 17 deletions
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index 47798a6..8b75351 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -21,6 +21,7 @@ pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry};
21 21
22use dashmap::DashMap; 22use dashmap::DashMap;
23use nostr_sdk::prelude::*; 23use nostr_sdk::prelude::*;
24use nostr_sdk::ToBech32;
24use serde::{Deserialize, Serialize}; 25use serde::{Deserialize, Serialize};
25use std::collections::HashMap; 26use std::collections::HashMap;
26use std::collections::HashSet; 27use std::collections::HashSet;
@@ -608,6 +609,9 @@ impl Purgatory {
608 /// prevent infinite re-sync loops. Events that expire without finding git data 609 /// prevent infinite re-sync loops. Events that expire without finding git data
609 /// will be filtered out during future negentropy/REQ sync operations. 610 /// will be filtered out during future negentropy/REQ sync operations.
610 /// 611 ///
612 /// Emits structured `[PURGATORY_EXPIRED]` log entries for each expired event
613 /// to support migration scripts and operational monitoring.
614 ///
611 /// # Returns 615 /// # Returns
612 /// Tuple of (num_state_removed, num_pr_removed) 616 /// Tuple of (num_state_removed, num_pr_removed)
613 pub fn cleanup(&self) -> (usize, usize) { 617 pub fn cleanup(&self) -> (usize, usize) {
@@ -615,18 +619,24 @@ impl Purgatory {
615 let mut state_removed = 0; 619 let mut state_removed = 0;
616 620
617 // Remove expired state events and mark them as expired 621 // Remove expired state events and mark them as expired
618 self.state_events.retain(|_, entries| { 622 self.state_events.retain(|identifier, entries| {
619 let original_len = entries.len(); 623 let original_len = entries.len();
620 // Collect event IDs before removing
621 let expired_ids: Vec<EventId> = entries
622 .iter()
623 .filter(|entry| entry.expires_at <= now)
624 .map(|entry| entry.event.id)
625 .collect();
626 624
627 // Mark as expired to prevent re-sync 625 // Log and collect expired entries before removing
628 for event_id in expired_ids { 626 for entry in entries.iter().filter(|e| e.expires_at <= now) {
629 self.mark_expired(event_id); 627 let npub = entry.author.to_bech32().unwrap_or_else(|_| entry.author.to_hex());
628 let event_id_short = &entry.event.id.to_hex()[..12];
629
630 // Structured log for migration scripts
631 tracing::warn!(
632 "[PURGATORY_EXPIRED] repo={} npub={} event_id={}... kind={} reason=\"git data not received within 30 minutes\"",
633 identifier,
634 npub,
635 event_id_short,
636 entry.event.kind.as_u16()
637 );
638
639 self.mark_expired(entry.event.id);
630 } 640 }
631 641
632 // Remove expired entries 642 // Remove expired entries
@@ -636,21 +646,80 @@ impl Purgatory {
636 }); 646 });
637 647
638 // Remove expired PR events and mark them as expired 648 // Remove expired PR events and mark them as expired
639 let expired_prs: Vec<(String, Option<EventId>)> = self 649 let expired_prs: Vec<_> = self
640 .pr_events 650 .pr_events
641 .iter() 651 .iter()
642 .filter(|entry| entry.value().expires_at <= now) 652 .filter(|entry| entry.value().expires_at <= now)
643 .map(|entry| { 653 .map(|entry| {
644 let event_id = entry.value().event.as_ref().map(|e| e.id); 654 let pr_entry = entry.value();
645 (entry.key().clone(), event_id) 655 let event_id_str = entry.key().clone();
656 let event_opt = pr_entry.event.clone();
657 let commit = pr_entry.commit.clone();
658 (event_id_str, event_opt, commit)
646 }) 659 })
647 .collect(); 660 .collect();
648 661
649 let pr_removed = expired_prs.len(); 662 let pr_removed = expired_prs.len();
650 for (event_id_str, event_id_opt) in expired_prs { 663 for (event_id_str, event_opt, commit) in expired_prs {
651 // Mark actual PR events as expired (not placeholders) 664 // Log structured entry for PR events (not placeholders)
652 if let Some(event_id) = event_id_opt { 665 if let Some(ref event) = event_opt {
653 self.mark_expired(event_id); 666 let npub = event.pubkey.to_bech32().unwrap_or_else(|_| event.pubkey.to_hex());
667 let event_id_short = &event.id.to_hex()[..12];
668
669 // Extract ALL repo identifiers from 'a' tags
670 // (PR events can reference multiple repos when there are multiple maintainers)
671 let repos: Vec<String> = event
672 .tags
673 .iter()
674 .filter_map(|tag| {
675 let tag_vec = tag.clone().to_vec();
676 if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") {
677 // Format: 30617:<owner_pubkey>:<identifier>
678 let parts: Vec<&str> = tag_vec[1].split(':').collect();
679 if parts.len() >= 3 {
680 Some(parts[2].to_string())
681 } else {
682 None
683 }
684 } else {
685 None
686 }
687 })
688 .collect();
689
690 // Deduplicate while preserving order
691 let mut seen = std::collections::HashSet::new();
692 let unique_repos: Vec<String> = repos
693 .into_iter()
694 .filter(|r| seen.insert(r.clone()))
695 .collect();
696
697 let repos_to_log = if unique_repos.is_empty() {
698 vec!["unknown".to_string()]
699 } else {
700 unique_repos
701 };
702
703 // Structured log for migration scripts - log once per repo
704 for repo in &repos_to_log {
705 tracing::warn!(
706 "[PURGATORY_EXPIRED] repo={} npub={} event_id={}... kind={} commit={} reason=\"git data not received within 30 minutes\"",
707 repo,
708 npub,
709 event_id_short,
710 event.kind.as_u16(),
711 &commit[..commit.len().min(12)]
712 );
713 }
714
715 self.mark_expired(event.id);
716 } else {
717 // Placeholder (git data arrived first, but PR event never came)
718 tracing::debug!(
719 "[PURGATORY_EXPIRED] placeholder event_id={} commit={} reason=\"PR event not received within 30 minutes\"",
720 &event_id_str[..event_id_str.len().min(12)],
721 &commit[..commit.len().min(12)]
722 );
654 } 723 }
655 self.pr_events.remove(&event_id_str); 724 self.pr_events.remove(&event_id_str);
656 } 725 }