ic-analytics-sdk 0.2.3

IC Analytics SDK
Documentation
//! Rust SDK for sending metrics from any Internet Computer canister to an
//! IC Metrics analytics canister.
//!
//! # Installation
//!
//! ```toml
//! [dependencies]
//! ic-analytics-sdk = "0.2.3"
//! ```
//!
//! # Quick start
//!
//! ## 1. Initialise the client
//!
//! [`AnalyticsClient`] holds the principal of your analytics canister. Create
//! it once and store it in a `thread_local!`.
//!
//! ```no_run
//! use ic_analytics_sdk::AnalyticsClient;
//! use candid::Principal;
//!
//! thread_local! {
//!     static ANALYTICS: AnalyticsClient = AnalyticsClient::new(
//!         Principal::from_text("YOUR-ANALYTICS-CANISTER-ID").unwrap()
//!     );
//! }
//! ```
//!
//! Find your analytics canister ID on the IC Metrics dashboard after creating
//! an analytics canister for your Dapp.
//!
//! ## 2. Record a metric
//!
//! All recording is **fire-and-forget** — [`AnalyticsClient::record_metric`]
//! enqueues a one-way inter-canister call that does not block your canister's
//! execution.
//!
//! ```no_run
//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
//! # use candid::Principal;
//! # thread_local! { static ANALYTICS: AnalyticsClient = AnalyticsClient::new(Principal::anonymous()); }
//!
//!
//! ANALYTICS.with(|a| {
//!     a.record_metric(Metric {
//!         key:   "user_signups".to_string(),
//!         name:  "User Sign-ups".to_string(),
//!         value: MetricValue::Counter(1),
//!     })
//!     .expect("record_metric failed");
//! });
//! ```
//!
//! # Metric types
//!
//! | Variant | Payload | Behaviour |
//! |---------|---------|-----------|
//! | [`MetricValue::Counter`] | Delta (positive or negative) | Accumulated running total |
//! | [`MetricValue::Gauge`] | Absolute value | Overwritten on each call |
//! | [`MetricValue::Histogram`] | (x, y) data point | Appended with each call |
//! | [`MetricValue::TimeSeries`] | Measured value | Histogram with IC timestamp (ms) as x, set automatically |
//! | [`MetricValue::Log`] | Text entry | Appended with canister timestamp |
//!
//! The `key` field is the stable identifier for a metric. The `name` field is
//! the human-readable label shown in the dashboard. The key is fixed on first
//! write — changing the metric type for an existing key will trap.
//!
//! # Examples
//!
//! ## Counter
//!
//! Increment a running total. Pass a negative delta to decrement.
//!
//! ```no_run
//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
//! # use candid::Principal;
//! # let a = AnalyticsClient::new(Principal::anonymous());
//! a.record_metric(Metric {
//!     key:   "transfers_total".to_string(),
//!     name:  "Total Transfers".to_string(),
//!     value: MetricValue::Counter(1),
//! })?;
//! # Ok::<(), String>(())
//! ```
//!
//! ## Gauge
//!
//! Store the latest absolute value. Useful for memory usage, queue depth, etc.
//!
//! ```no_run
//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
//! # use candid::Principal;
//! # let a = AnalyticsClient::new(Principal::anonymous());
//! # let heap_mb = 0.0_f64;
//! a.record_metric(Metric {
//!     key:   "heap_usage_mb".to_string(),
//!     name:  "Heap Usage (MB)".to_string(),
//!     value: MetricValue::Gauge(heap_mb),
//! })?;
//! # Ok::<(), String>(())
//! ```
//!
//! ## TimeSeries
//!
//! Append a data point timestamped automatically with the current IC time
//! (milliseconds). Use this when x should always be the current time.
//!
//! ```no_run
//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
//! # use candid::Principal;
//! # let a = AnalyticsClient::new(Principal::anonymous());
//! a.record_metric(Metric {
//!     key:   "response_latency_ms".to_string(),
//!     name:  "Response Latency (ms)".to_string(),
//!     value: MetricValue::TimeSeries(42.5),
//! })?;
//! # Ok::<(), String>(())
//! ```
//!
//! ## Histogram
//!
//! Append an (x, y) data point with explicit values. Useful when x is
//! something other than the current time (e.g. request size vs latency).
//!
//! ```no_run
//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
//! # use candid::Principal;
//! # let a = AnalyticsClient::new(Principal::anonymous());
//! # let request_bytes = 0_u64;
//! # let latency_ms = 0.0_f64;
//! a.record_metric(Metric {
//!     key:   "size_vs_latency".to_string(),
//!     name:  "Size vs Latency".to_string(),
//!     value: MetricValue::Histogram { x: request_bytes as f64, y: latency_ms },
//! })?;
//! # Ok::<(), String>(())
//! ```
//!
//! ## Log
//!
//! Append a timestamped text entry. Useful for events, errors, or audit trails.
//!
//! ```no_run
//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
//! # use candid::Principal;
//! # let a = AnalyticsClient::new(Principal::anonymous());
//! a.record_metric(Metric {
//!     key:   "canister_events".to_string(),
//!     name:  "Canister Events".to_string(),
//!     value: MetricValue::Log("[INFO] Upgrade completed to v2.1.0".to_string()),
//! })?;
//! # Ok::<(), String>(())
//! ```

use candid::{CandidType, Principal};
use serde::{Deserialize, Serialize};

/// The value carried by an incoming metric record.
/// Determines how the metric is stored and aggregated.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub enum MetricValue {
    /// Delta to apply: positive increments, negative decrements.
    Counter(i64),
    /// Absolute value to set.
    Gauge(f64),
    /// An (x, y) data point. Use [`MetricValue::TimeSeries`] instead when
    /// x should be the current IC timestamp.
    Histogram { x: f64, y: f64 },
    /// A text entry; the canister appends it with a timestamp.
    Log(String),
    /// Convenience wrapper: recorded as a [`MetricValue::Histogram`] where `x`
    /// is the current IC timestamp in milliseconds. Pass only the measured value.
    TimeSeries(f64),
}

/// An incoming metric record.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct Metric {
    /// Stable identifier used to look up this metric. Fixed on first write.
    pub key: String,
    /// Human-readable label shown in the dashboard.
    pub name: String,
    /// The value and its storage semantics.
    pub value: MetricValue,
}

/// The aggregated metric as stored in the canister, keyed by `key`.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub enum StoredMetric {
    Counter { name: String, value: i64 },
    Gauge { name: String, value: f64 },
    Histogram { name: String, total: u64 },
    Log { name: String, total: u64 },
}

/// A page of log entries returned by `get_log_page`.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct LogPage {
    pub name: String,
    pub entries: Vec<(u64, String)>,
    pub total: u64,
    pub offset: u64,
}

/// A page of histogram data points returned by `get_histogram_page`.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct HistogramPage {
    pub name: String,
    pub points: Vec<(f64, f64)>,
    pub total: u64,
}

/// The result produced by running a transformer script.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub enum TransformerOutput {
    /// A single computed number (e.g. integral, average, max).
    Scalar(f64),
    /// A derived time series of (x, y) pairs (e.g. derivative, smoothed data).
    Series(Vec<(f64, f64)>),
    /// A textual result (e.g. statistical summary).
    Text(String),
}

/// Metadata returned when listing or fetching a transformer definition.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct TransformerInfo {
    pub name: String,
    pub key: String,
    pub script: String,
    pub created_at: u64,
}

/// Client for recording metrics to an IC Metrics analytics canister.
///
/// Create once per canister and store in a `thread_local!`. All calls are
/// fire-and-forget — they enqueue a one-way inter-canister call and return
/// immediately without blocking execution.
///
/// # Example
///
/// ```no_run
/// use ic_analytics_sdk::AnalyticsClient;
/// use candid::Principal;
///
/// thread_local! {
///     static ANALYTICS: AnalyticsClient = AnalyticsClient::new(
///         Principal::from_text("YOUR-ANALYTICS-CANISTER-ID").unwrap()
///     );
/// }
/// ```
#[derive(Clone, Debug)]
pub struct AnalyticsClient {
    pub backend_canister_id: Principal,
}

impl AnalyticsClient {
    /// Creates a new client pointing at the given analytics canister.
    pub fn new(backend_canister_id: Principal) -> Self {
        Self {
            backend_canister_id,
        }
    }

    /// Enqueues a one-way call to record a metric. Non-blocking.
    ///
    /// [`MetricValue::TimeSeries`] is automatically converted to
    /// `Histogram { x: now_ms, y }` using the current IC time before the
    /// call is sent.
    pub fn record_metric(&self, metric: Metric) -> Result<(), String> {
        let metric = match metric.value {
            MetricValue::TimeSeries(y) => Metric {
                value: MetricValue::Histogram {
                    x: (ic_cdk::api::time() / 1_000_000) as f64,
                    y,
                },
                ..metric
            },
            _ => metric,
        };
        ic_cdk::call::Call::unbounded_wait(self.backend_canister_id, "record_metric")
            .with_args(&(metric,))
            .oneway()
            .map_err(|e| e.to_string())?;
        Ok(())
    }
}