Skip to main content

ic_analytics_sdk/
lib.rs

1//! Rust SDK for sending metrics from any Internet Computer canister to an
2//! IC Metrics analytics canister.
3//!
4//! # Installation
5//!
6//! ```toml
7//! [dependencies]
8//! ic-analytics-sdk = "0.2.3"
9//! ```
10//!
11//! # Quick start
12//!
13//! ## 1. Initialise the client
14//!
15//! [`AnalyticsClient`] holds the principal of your analytics canister. Create
16//! it once and store it in a `thread_local!`.
17//!
18//! ```no_run
19//! use ic_analytics_sdk::AnalyticsClient;
20//! use candid::Principal;
21//!
22//! thread_local! {
23//!     static ANALYTICS: AnalyticsClient = AnalyticsClient::new(
24//!         Principal::from_text("YOUR-ANALYTICS-CANISTER-ID").unwrap()
25//!     );
26//! }
27//! ```
28//!
29//! Find your analytics canister ID on the IC Metrics dashboard after creating
30//! an analytics canister for your Dapp.
31//!
32//! ## 2. Record a metric
33//!
34//! All recording is **fire-and-forget** — [`AnalyticsClient::record_metric`]
35//! enqueues a one-way inter-canister call that does not block your canister's
36//! execution.
37//!
38//! ```no_run
39//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
40//! # use candid::Principal;
41//! # thread_local! { static ANALYTICS: AnalyticsClient = AnalyticsClient::new(Principal::anonymous()); }
42//!
43//!
44//! ANALYTICS.with(|a| {
45//!     a.record_metric(Metric {
46//!         key:   "user_signups".to_string(),
47//!         name:  "User Sign-ups".to_string(),
48//!         value: MetricValue::Counter(1),
49//!     })
50//!     .expect("record_metric failed");
51//! });
52//! ```
53//!
54//! # Metric types
55//!
56//! | Variant | Payload | Behaviour |
57//! |---------|---------|-----------|
58//! | [`MetricValue::Counter`] | Delta (positive or negative) | Accumulated running total |
59//! | [`MetricValue::Gauge`] | Absolute value | Overwritten on each call |
60//! | [`MetricValue::Histogram`] | (x, y) data point | Appended with each call |
61//! | [`MetricValue::TimeSeries`] | Measured value | Histogram with IC timestamp (ms) as x, set automatically |
62//! | [`MetricValue::Log`] | Text entry | Appended with canister timestamp |
63//!
64//! The `key` field is the stable identifier for a metric. The `name` field is
65//! the human-readable label shown in the dashboard. The key is fixed on first
66//! write — changing the metric type for an existing key will trap.
67//!
68//! # Examples
69//!
70//! ## Counter
71//!
72//! Increment a running total. Pass a negative delta to decrement.
73//!
74//! ```no_run
75//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
76//! # use candid::Principal;
77//! # let a = AnalyticsClient::new(Principal::anonymous());
78//! a.record_metric(Metric {
79//!     key:   "transfers_total".to_string(),
80//!     name:  "Total Transfers".to_string(),
81//!     value: MetricValue::Counter(1),
82//! })?;
83//! # Ok::<(), String>(())
84//! ```
85//!
86//! ## Gauge
87//!
88//! Store the latest absolute value. Useful for memory usage, queue depth, etc.
89//!
90//! ```no_run
91//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
92//! # use candid::Principal;
93//! # let a = AnalyticsClient::new(Principal::anonymous());
94//! # let heap_mb = 0.0_f64;
95//! a.record_metric(Metric {
96//!     key:   "heap_usage_mb".to_string(),
97//!     name:  "Heap Usage (MB)".to_string(),
98//!     value: MetricValue::Gauge(heap_mb),
99//! })?;
100//! # Ok::<(), String>(())
101//! ```
102//!
103//! ## TimeSeries
104//!
105//! Append a data point timestamped automatically with the current IC time
106//! (milliseconds). Use this when x should always be the current time.
107//!
108//! ```no_run
109//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
110//! # use candid::Principal;
111//! # let a = AnalyticsClient::new(Principal::anonymous());
112//! a.record_metric(Metric {
113//!     key:   "response_latency_ms".to_string(),
114//!     name:  "Response Latency (ms)".to_string(),
115//!     value: MetricValue::TimeSeries(42.5),
116//! })?;
117//! # Ok::<(), String>(())
118//! ```
119//!
120//! ## Histogram
121//!
122//! Append an (x, y) data point with explicit values. Useful when x is
123//! something other than the current time (e.g. request size vs latency).
124//!
125//! ```no_run
126//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
127//! # use candid::Principal;
128//! # let a = AnalyticsClient::new(Principal::anonymous());
129//! # let request_bytes = 0_u64;
130//! # let latency_ms = 0.0_f64;
131//! a.record_metric(Metric {
132//!     key:   "size_vs_latency".to_string(),
133//!     name:  "Size vs Latency".to_string(),
134//!     value: MetricValue::Histogram { x: request_bytes as f64, y: latency_ms },
135//! })?;
136//! # Ok::<(), String>(())
137//! ```
138//!
139//! ## Log
140//!
141//! Append a timestamped text entry. Useful for events, errors, or audit trails.
142//!
143//! ```no_run
144//! # use ic_analytics_sdk::{AnalyticsClient, Metric, MetricValue};
145//! # use candid::Principal;
146//! # let a = AnalyticsClient::new(Principal::anonymous());
147//! a.record_metric(Metric {
148//!     key:   "canister_events".to_string(),
149//!     name:  "Canister Events".to_string(),
150//!     value: MetricValue::Log("[INFO] Upgrade completed to v2.1.0".to_string()),
151//! })?;
152//! # Ok::<(), String>(())
153//! ```
154
155use candid::{CandidType, Principal};
156use serde::{Deserialize, Serialize};
157
158/// The value carried by an incoming metric record.
159/// Determines how the metric is stored and aggregated.
160#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
161pub enum MetricValue {
162    /// Delta to apply: positive increments, negative decrements.
163    Counter(i64),
164    /// Absolute value to set.
165    Gauge(f64),
166    /// An (x, y) data point. Use [`MetricValue::TimeSeries`] instead when
167    /// x should be the current IC timestamp.
168    Histogram { x: f64, y: f64 },
169    /// A text entry; the canister appends it with a timestamp.
170    Log(String),
171    /// Convenience wrapper: recorded as a [`MetricValue::Histogram`] where `x`
172    /// is the current IC timestamp in milliseconds. Pass only the measured value.
173    TimeSeries(f64),
174}
175
176/// An incoming metric record.
177#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
178pub struct Metric {
179    /// Stable identifier used to look up this metric. Fixed on first write.
180    pub key: String,
181    /// Human-readable label shown in the dashboard.
182    pub name: String,
183    /// The value and its storage semantics.
184    pub value: MetricValue,
185}
186
187/// The aggregated metric as stored in the canister, keyed by `key`.
188#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
189pub enum StoredMetric {
190    Counter { name: String, value: i64 },
191    Gauge { name: String, value: f64 },
192    Histogram { name: String, total: u64 },
193    Log { name: String, total: u64 },
194}
195
196/// A page of log entries returned by `get_log_page`.
197#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
198pub struct LogPage {
199    pub name: String,
200    pub entries: Vec<(u64, String)>,
201    pub total: u64,
202    pub offset: u64,
203}
204
205/// A page of histogram data points returned by `get_histogram_page`.
206#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
207pub struct HistogramPage {
208    pub name: String,
209    pub points: Vec<(f64, f64)>,
210    pub total: u64,
211}
212
213/// The result produced by running a transformer script.
214#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
215pub enum TransformerOutput {
216    /// A single computed number (e.g. integral, average, max).
217    Scalar(f64),
218    /// A derived time series of (x, y) pairs (e.g. derivative, smoothed data).
219    Series(Vec<(f64, f64)>),
220    /// A textual result (e.g. statistical summary).
221    Text(String),
222}
223
224/// Metadata returned when listing or fetching a transformer definition.
225#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
226pub struct TransformerInfo {
227    pub name: String,
228    pub key: String,
229    pub script: String,
230    pub created_at: u64,
231}
232
233/// Client for recording metrics to an IC Metrics analytics canister.
234///
235/// Create once per canister and store in a `thread_local!`. All calls are
236/// fire-and-forget — they enqueue a one-way inter-canister call and return
237/// immediately without blocking execution.
238///
239/// # Example
240///
241/// ```no_run
242/// use ic_analytics_sdk::AnalyticsClient;
243/// use candid::Principal;
244///
245/// thread_local! {
246///     static ANALYTICS: AnalyticsClient = AnalyticsClient::new(
247///         Principal::from_text("YOUR-ANALYTICS-CANISTER-ID").unwrap()
248///     );
249/// }
250/// ```
251#[derive(Clone, Debug)]
252pub struct AnalyticsClient {
253    pub backend_canister_id: Principal,
254}
255
256impl AnalyticsClient {
257    /// Creates a new client pointing at the given analytics canister.
258    pub fn new(backend_canister_id: Principal) -> Self {
259        Self {
260            backend_canister_id,
261        }
262    }
263
264    /// Enqueues a one-way call to record a metric. Non-blocking.
265    ///
266    /// [`MetricValue::TimeSeries`] is automatically converted to
267    /// `Histogram { x: now_ms, y }` using the current IC time before the
268    /// call is sent.
269    pub fn record_metric(&self, metric: Metric) -> Result<(), String> {
270        let metric = match metric.value {
271            MetricValue::TimeSeries(y) => Metric {
272                value: MetricValue::Histogram {
273                    x: (ic_cdk::api::time() / 1_000_000) as f64,
274                    y,
275                },
276                ..metric
277            },
278            _ => metric,
279        };
280        ic_cdk::call::Call::unbounded_wait(self.backend_canister_id, "record_metric")
281            .with_args(&(metric,))
282            .oneway()
283            .map_err(|e| e.to_string())?;
284        Ok(())
285    }
286}