|
| 1 | +// Copyright (c) 2025 Geoffrey Huntley <ghuntley@ghuntley.com>. All rights reserved. |
| 2 | +// SPDX-License-Identifier: Proprietary |
| 3 | + |
| 4 | +//! Self-monitoring for loom-cli. |
| 5 | +//! |
| 6 | +//! Automatically initializes crash monitoring for the CLI by fetching |
| 7 | +//! configuration from the server. This enables Loom to monitor itself |
| 8 | +//! without any user configuration. |
| 9 | +
|
| 10 | +use std::sync::Arc; |
| 11 | + |
| 12 | +use anyhow::Result; |
| 13 | +use loom_crash::{CrashClient, CrashClientBuilder}; |
| 14 | +use serde::Deserialize; |
| 15 | +use tokio::sync::OnceCell; |
| 16 | +use tracing::{debug, warn}; |
| 17 | + |
| 18 | +/// Well-known project ID for loom-cli (from loom-server self_monitoring.rs). |
| 19 | +const LOOM_CLI_PROJECT_ID: &str = "00000000-0000-0000-0000-000000000004"; |
| 20 | + |
| 21 | +/// Global crash client for loom-cli. |
| 22 | +static CLI_CRASH_CLIENT: OnceCell<Arc<CrashClient>> = OnceCell::const_new(); |
| 23 | + |
| 24 | +/// Configuration returned by the self-monitoring API. |
| 25 | +#[derive(Debug, Deserialize)] |
| 26 | +struct CliCrashConfig { |
| 27 | + project_id: String, |
| 28 | + api_key: String, |
| 29 | + release: String, |
| 30 | + environment: String, |
| 31 | +} |
| 32 | + |
| 33 | +/// Initialize self-monitoring for loom-cli. |
| 34 | +/// |
| 35 | +/// This fetches the CLI API key from the server's self-monitoring endpoint |
| 36 | +/// and initializes the crash client with a panic hook. |
| 37 | +/// |
| 38 | +/// Call this once during CLI startup. Failures are logged but do not |
| 39 | +/// prevent the CLI from running. |
| 40 | +pub async fn initialize_self_monitoring(server_url: &str) -> Option<Arc<CrashClient>> { |
| 41 | + // Don't initialize twice |
| 42 | + if let Some(client) = CLI_CRASH_CLIENT.get() { |
| 43 | + return Some(client.clone()); |
| 44 | + } |
| 45 | + |
| 46 | + match fetch_and_initialize(server_url).await { |
| 47 | + Ok(client) => { |
| 48 | + let client = Arc::new(client); |
| 49 | + let _ = CLI_CRASH_CLIENT.set(client.clone()); |
| 50 | + debug!("Self-monitoring initialized for loom-cli"); |
| 51 | + Some(client) |
| 52 | + } |
| 53 | + Err(e) => { |
| 54 | + warn!("Failed to initialize self-monitoring: {}", e); |
| 55 | + None |
| 56 | + } |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +async fn fetch_and_initialize(server_url: &str) -> Result<CrashClient> { |
| 61 | + // Fetch CLI config from server |
| 62 | + let http = loom_common_http::new_client(); |
| 63 | + let url = format!( |
| 64 | + "{}/api/self-monitoring/cli-config", |
| 65 | + server_url.trim_end_matches('/') |
| 66 | + ); |
| 67 | + |
| 68 | + let response = http.get(&url).send().await?; |
| 69 | + |
| 70 | + if !response.status().is_success() { |
| 71 | + anyhow::bail!( |
| 72 | + "Self-monitoring not available: {} {}", |
| 73 | + response.status(), |
| 74 | + response.status().canonical_reason().unwrap_or("") |
| 75 | + ); |
| 76 | + } |
| 77 | + |
| 78 | + let config: CliCrashConfig = response.json().await?; |
| 79 | + |
| 80 | + let client = CrashClientBuilder::new() |
| 81 | + .base_url(server_url) |
| 82 | + .project_id(&config.project_id) |
| 83 | + .auth_token(&config.api_key) |
| 84 | + .release(&config.release) |
| 85 | + .environment(&config.environment) |
| 86 | + .build()?; |
| 87 | + |
| 88 | + // Install panic hook for automatic crash reporting |
| 89 | + client.install_panic_hook(); |
| 90 | + |
| 91 | + Ok(client) |
| 92 | +} |
| 93 | + |
| 94 | +/// Get the CLI crash client for manual error capture. |
| 95 | +pub fn get_crash_client() -> Option<Arc<CrashClient>> { |
| 96 | + CLI_CRASH_CLIENT.get().cloned() |
| 97 | +} |
| 98 | + |
| 99 | +/// Capture an exception using the self-monitoring crash client. |
| 100 | +/// |
| 101 | +/// This is a convenience function that handles the case where |
| 102 | +/// self-monitoring is not initialized. |
| 103 | +#[allow(dead_code)] |
| 104 | +pub fn capture_exception(error: &dyn std::error::Error) -> Option<String> { |
| 105 | + if let Some(client) = get_crash_client() { |
| 106 | + // Use blocking capture in sync context |
| 107 | + // For now, just log since async capture would require runtime |
| 108 | + warn!("CLI crash captured (async send not implemented): {}", error); |
| 109 | + return None; |
| 110 | + } |
| 111 | + None |
| 112 | +} |
| 113 | + |
| 114 | +/// Shutdown the CLI crash client. |
| 115 | +/// |
| 116 | +/// Call this during CLI shutdown to flush pending events. |
| 117 | +pub async fn shutdown_self_monitoring() { |
| 118 | + if let Some(client) = CLI_CRASH_CLIENT.get() { |
| 119 | + if let Err(e) = client.shutdown().await { |
| 120 | + warn!("Failed to shutdown crash client: {}", e); |
| 121 | + } |
| 122 | + } |
| 123 | +} |
0 commit comments