diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-23 11:41:10 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-27 20:37:58 +0000 |
| commit | 73a366cbd7be4edf9c74194cd0891c80a15236a5 (patch) | |
| tree | c4a631a28496d48ad2bce6c2dfd4eff7ae0e4866 /src/purgatory/mod.rs | |
| parent | 28cc7820953efeafb2bc4d41ebcf3d682da86711 (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.rs | 103 |
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 | ||
| 22 | use dashmap::DashMap; | 22 | use dashmap::DashMap; |
| 23 | use nostr_sdk::prelude::*; | 23 | use nostr_sdk::prelude::*; |
| 24 | use nostr_sdk::ToBech32; | ||
| 24 | use serde::{Deserialize, Serialize}; | 25 | use serde::{Deserialize, Serialize}; |
| 25 | use std::collections::HashMap; | 26 | use std::collections::HashMap; |
| 26 | use std::collections::HashSet; | 27 | use 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 | } |