use crate::errors::ParsingError;
use crate::types::{FromRedisValue, RedisWrite, ToRedisArgs, Value};
use std::collections::HashMap;
pub const HOTKEYS_COUNT_MIN: u64 = 1;
pub const HOTKEYS_COUNT_MAX: u64 = 64;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct HotkeysOptions {
cpu: bool,
net: bool,
count_k: Option<u64>,
duration_secs: Option<u64>,
sample_ratio: Option<u64>,
slots: Option<Vec<u16>>,
}
impl HotkeysOptions {
pub fn new_with_cpu() -> Self {
Self {
cpu: true,
net: false,
count_k: None,
duration_secs: None,
sample_ratio: None,
slots: None,
}
}
pub fn new_with_net() -> Self {
Self {
cpu: false,
net: true,
count_k: None,
duration_secs: None,
sample_ratio: None,
slots: None,
}
}
pub fn and_cpu(mut self) -> Self {
self.cpu = true;
self
}
pub fn and_net(mut self) -> Self {
self.net = true;
self
}
fn metrics_count(&self) -> u64 {
self.cpu as u64 + self.net as u64
}
pub fn with_count(mut self, k: u64) -> Result<Self, String> {
if !(HOTKEYS_COUNT_MIN..=HOTKEYS_COUNT_MAX).contains(&k) {
return Err(format!(
"COUNT must be between {HOTKEYS_COUNT_MIN} and {HOTKEYS_COUNT_MAX}, got: {k}"
));
}
self.count_k = Some(k);
Ok(self)
}
pub fn with_duration_secs(mut self, seconds: u64) -> Self {
self.duration_secs = Some(seconds);
self
}
pub fn with_sample_ratio(mut self, ratio: u64) -> Self {
self.sample_ratio = Some(ratio);
self
}
pub fn with_slots(mut self, slots: Vec<u16>) -> Self {
self.slots = Some(slots);
self
}
}
impl ToRedisArgs for HotkeysOptions {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
out.write_arg(b"METRICS");
out.write_arg_fmt(self.metrics_count());
if self.cpu {
out.write_arg(b"CPU");
}
if self.net {
out.write_arg(b"NET");
}
if let Some(k) = self.count_k {
out.write_arg(b"COUNT");
out.write_arg_fmt(k);
}
if let Some(secs) = self.duration_secs {
out.write_arg(b"DURATION");
out.write_arg_fmt(secs);
}
if let Some(ratio) = self.sample_ratio {
out.write_arg(b"SAMPLE");
out.write_arg_fmt(ratio);
}
if let Some(ref slots) = self.slots {
out.write_arg(b"SLOTS");
out.write_arg_fmt(slots.len());
for slot in slots {
out.write_arg_fmt(slot);
}
}
}
fn num_of_args(&self) -> usize {
let mut n = 2;
n += self.cpu as usize;
n += self.net as usize;
if self.count_k.is_some() {
n += 2;
}
if self.duration_secs.is_some() {
n += 2;
}
if self.sample_ratio.is_some() {
n += 2;
}
if let Some(ref slots) = self.slots {
n += 2 + slots.len();
}
n
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct HotKeyEntry {
pub key: String,
pub value: u64,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct SlotRange {
pub start: u16,
pub end: u16,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct HotkeysResponse {
pub tracking_active: bool,
pub sample_ratio: u64,
pub selected_slots: Vec<SlotRange>,
pub all_commands_all_slots_us: u64,
pub net_bytes_all_commands_all_slots: u64,
pub collection_start_time_unix_ms: u64,
pub collection_duration_ms: u64,
pub total_cpu_time_user_ms: Option<u64>,
pub total_cpu_time_sys_ms: Option<u64>,
pub total_net_bytes: Option<u64>,
pub by_cpu_time_us: Option<Vec<HotKeyEntry>>,
pub by_net_bytes: Option<Vec<HotKeyEntry>>,
pub sampled_commands_selected_slots_us: Option<u64>,
pub all_commands_selected_slots_us: Option<u64>,
pub net_bytes_sampled_commands_selected_slots: Option<u64>,
pub net_bytes_all_commands_selected_slots: Option<u64>,
}
fn strip_quotes(s: String) -> String {
if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
s[1..s.len() - 1].to_string()
} else {
s
}
}
fn parse_hotkey_entries(arr: &[Value]) -> Result<Vec<HotKeyEntry>, ParsingError> {
use crate::types::from_redis_value_ref;
let mut entries = Vec::with_capacity(arr.len() / 2);
let mut iter = arr.iter();
while let Some(key_val) = iter.next() {
let key: String = from_redis_value_ref(key_val)?;
let key = strip_quotes(key);
let value: u64 = iter
.next()
.ok_or_else(|| ParsingError::from("Expected value after key in hotkey entry"))
.and_then(from_redis_value_ref)?;
entries.push(HotKeyEntry { key, value });
}
Ok(entries)
}
fn parse_slot_ranges(arr: &[Value]) -> Result<Vec<SlotRange>, ParsingError> {
use crate::types::from_redis_value_ref;
let mut ranges = Vec::with_capacity(arr.len());
for item in arr {
let Value::Array(range_arr) = item else {
crate::errors::invalid_type_error!("Expected array for slot range", item);
};
match range_arr.len() {
1 => {
let slot: u16 = from_redis_value_ref(&range_arr[0])?;
ranges.push(SlotRange {
start: slot,
end: slot,
});
}
n if n >= 2 => {
let start: u16 = from_redis_value_ref(&range_arr[0])?;
let end: u16 = from_redis_value_ref(&range_arr[1])?;
ranges.push(SlotRange { start, end });
}
_ => crate::errors::invalid_type_error!("Empty slot range entry", range_arr),
}
}
Ok(ranges)
}
impl FromRedisValue for HotkeysResponse {
fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
use crate::types::from_redis_value;
let v = match v {
Value::Array(mut arr) if arr.len() == 1 => arr.remove(0),
other => other,
};
let mut fields: HashMap<String, Value> = match v {
Value::Array(arr) => {
let mut map = HashMap::new();
let mut iter = arr.into_iter();
while let Some(key) = iter.next() {
let key_str: String = from_redis_value(key)?;
let key_str = strip_quotes(key_str);
if let Some(val) = iter.next() {
map.insert(key_str, val);
}
}
map
}
Value::Map(pairs) => {
let mut map = HashMap::new();
for (k, v) in pairs {
let key_str: String = from_redis_value(k)?;
let key_str = strip_quotes(key_str);
map.insert(key_str, v);
}
map
}
_ => {
crate::errors::invalid_type_error!(
"Expected array or map response for HOTKEYS GET",
v
);
}
};
let mut response = HotkeysResponse::default();
if let Some(v) = fields.remove("tracking-active") {
response.tracking_active = from_redis_value::<i64>(v)? != 0;
}
if let Some(v) = fields.remove("sample-ratio") {
response.sample_ratio = from_redis_value(v)?;
}
if let Some(Value::Array(arr)) = fields.remove("selected-slots") {
response.selected_slots = parse_slot_ranges(&arr)?;
}
if let Some(v) = fields.remove("all-commands-all-slots-us") {
response.all_commands_all_slots_us = from_redis_value(v)?;
}
if let Some(v) = fields.remove("net-bytes-all-commands-all-slots") {
response.net_bytes_all_commands_all_slots = from_redis_value(v)?;
}
if let Some(v) = fields.remove("collection-start-time-unix-ms") {
response.collection_start_time_unix_ms = from_redis_value(v)?;
}
if let Some(v) = fields.remove("collection-duration-ms") {
response.collection_duration_ms = from_redis_value(v)?;
}
if let Some(v) = fields.remove("total-cpu-time-user-ms") {
response.total_cpu_time_user_ms = Some(from_redis_value(v)?);
}
if let Some(v) = fields.remove("total-cpu-time-sys-ms") {
response.total_cpu_time_sys_ms = Some(from_redis_value(v)?);
}
if let Some(Value::Array(arr)) = fields.remove("by-cpu-time-us") {
response.by_cpu_time_us = Some(parse_hotkey_entries(&arr)?);
}
if let Some(v) = fields.remove("total-net-bytes") {
response.total_net_bytes = Some(from_redis_value(v)?);
}
if let Some(Value::Array(arr)) = fields.remove("by-net-bytes") {
response.by_net_bytes = Some(parse_hotkey_entries(&arr)?);
}
if let Some(v) = fields.remove("sampled-commands-selected-slots-us") {
response.sampled_commands_selected_slots_us = Some(from_redis_value(v)?);
}
if let Some(v) = fields.remove("all-commands-selected-slots-us") {
response.all_commands_selected_slots_us = Some(from_redis_value(v)?);
}
if let Some(v) = fields.remove("net-bytes-sampled-commands-selected-slots") {
response.net_bytes_sampled_commands_selected_slots = Some(from_redis_value(v)?);
}
if let Some(v) = fields.remove("net-bytes-all-commands-selected-slots") {
response.net_bytes_all_commands_selected_slots = Some(from_redis_value(v)?);
}
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hotkeys_options_cpu_constructor() {
let opts = HotkeysOptions::new_with_cpu();
assert_eq!(opts.num_of_args(), 3); let args = opts.to_redis_args();
assert_eq!(args.len(), 3);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"1");
assert_eq!(args[2], b"CPU");
}
#[test]
fn test_hotkeys_options_net_constructor() {
let opts = HotkeysOptions::new_with_net();
assert_eq!(opts.num_of_args(), 3); let args = opts.to_redis_args();
assert_eq!(args.len(), 3);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"1");
assert_eq!(args[2], b"NET");
}
#[test]
fn test_hotkeys_options_cpu_and_net() {
let opts = HotkeysOptions::new_with_cpu().and_net();
assert_eq!(opts.num_of_args(), 4); let args = opts.to_redis_args();
assert_eq!(args.len(), 4);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"2");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"NET");
}
#[test]
fn test_hotkeys_options_net_and_cpu() {
let opts = HotkeysOptions::new_with_net().and_cpu();
assert_eq!(opts.num_of_args(), 4); let args = opts.to_redis_args();
assert_eq!(args.len(), 4);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"2");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"NET");
}
#[test]
fn test_hotkeys_options_with_duration() {
let opts = HotkeysOptions::new_with_cpu().with_duration_secs(60);
assert_eq!(opts.num_of_args(), 5); let args = opts.to_redis_args();
assert_eq!(args.len(), 5);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"1");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"DURATION");
assert_eq!(args[4], b"60");
}
#[test]
fn test_hotkeys_options_with_count() {
let opts = HotkeysOptions::new_with_cpu().with_count(50).unwrap();
assert_eq!(opts.num_of_args(), 5); let args = opts.to_redis_args();
assert_eq!(args.len(), 5);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"1");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"COUNT");
assert_eq!(args[4], b"50");
}
#[test]
fn test_hotkeys_options_with_count_min_valid() {
let opts = HotkeysOptions::new_with_cpu()
.with_count(HOTKEYS_COUNT_MIN)
.unwrap();
let args = opts.to_redis_args();
assert_eq!(args[3], b"COUNT");
assert_eq!(args[4], HOTKEYS_COUNT_MIN.to_string().as_bytes());
}
#[test]
fn test_hotkeys_options_with_count_max_valid() {
let opts = HotkeysOptions::new_with_cpu()
.with_count(HOTKEYS_COUNT_MAX)
.unwrap();
let args = opts.to_redis_args();
assert_eq!(args[3], b"COUNT");
assert_eq!(args[4], HOTKEYS_COUNT_MAX.to_string().as_bytes());
}
#[test]
fn test_hotkeys_options_with_count_too_low() {
let result = HotkeysOptions::new_with_cpu().with_count(HOTKEYS_COUNT_MIN - 1);
assert!(result.is_err());
assert!(result.unwrap_err().contains(&format!(
"COUNT must be between {HOTKEYS_COUNT_MIN} and {HOTKEYS_COUNT_MAX}"
)));
}
#[test]
fn test_hotkeys_options_with_count_too_high() {
let result = HotkeysOptions::new_with_cpu().with_count(HOTKEYS_COUNT_MAX + 1);
assert!(result.is_err());
assert!(result.unwrap_err().contains(&format!(
"COUNT must be between {HOTKEYS_COUNT_MIN} and {HOTKEYS_COUNT_MAX}"
)));
}
#[test]
fn test_hotkeys_options_with_sample() {
let opts = HotkeysOptions::new_with_cpu().with_sample_ratio(1000);
assert_eq!(opts.num_of_args(), 5); let args = opts.to_redis_args();
assert_eq!(args.len(), 5);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"1");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"SAMPLE");
assert_eq!(args[4], b"1000");
}
#[test]
fn test_hotkeys_options_with_slots() {
let opts = HotkeysOptions::new_with_cpu().with_slots(vec![0, 100, 200]);
assert_eq!(opts.num_of_args(), 8); let args = opts.to_redis_args();
assert_eq!(args.len(), 8);
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"1");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"SLOTS");
assert_eq!(args[4], b"3");
assert_eq!(args[5], b"0");
assert_eq!(args[6], b"100");
assert_eq!(args[7], b"200");
}
#[test]
fn test_hotkeys_options_full() {
let opts = HotkeysOptions::new_with_cpu()
.and_net()
.with_count(50)
.unwrap()
.with_duration_secs(120)
.with_sample_ratio(500);
assert_eq!(opts.num_of_args(), 10);
let args = opts.to_redis_args();
assert_eq!(args[0], b"METRICS");
assert_eq!(args[1], b"2");
assert_eq!(args[2], b"CPU");
assert_eq!(args[3], b"NET");
assert_eq!(args[4], b"COUNT");
assert_eq!(args[5], b"50");
assert_eq!(args[6], b"DURATION");
assert_eq!(args[7], b"120");
assert_eq!(args[8], b"SAMPLE");
assert_eq!(args[9], b"500");
}
#[test]
fn test_hotkeys_response_parsing_resp2() {
use crate::Value;
let response = Value::Array(vec![
Value::BulkString(b"tracking-active".to_vec()),
Value::Int(1),
Value::BulkString(b"sample-ratio".to_vec()),
Value::Int(1),
Value::BulkString(b"selected-slots".to_vec()),
Value::Array(vec![Value::Array(vec![Value::Int(0), Value::Int(16383)])]),
Value::BulkString(b"all-commands-all-slots-us".to_vec()),
Value::Int(5000),
Value::BulkString(b"net-bytes-all-commands-all-slots".to_vec()),
Value::Int(2048),
Value::BulkString(b"collection-start-time-unix-ms".to_vec()),
Value::Int(1700000000000),
Value::BulkString(b"collection-duration-ms".to_vec()),
Value::Int(10000),
Value::BulkString(b"total-cpu-time-user-ms".to_vec()),
Value::Int(100),
Value::BulkString(b"total-cpu-time-sys-ms".to_vec()),
Value::Int(50),
Value::BulkString(b"by-cpu-time-us".to_vec()),
Value::Array(vec![
Value::BulkString(b"key1".to_vec()),
Value::Int(1500),
Value::BulkString(b"key2".to_vec()),
Value::Int(750),
]),
]);
let result = HotkeysResponse::from_redis_value(response).unwrap();
assert!(result.tracking_active);
assert_eq!(result.sample_ratio, 1);
assert_eq!(result.selected_slots.len(), 1);
assert_eq!(result.selected_slots[0].start, 0);
assert_eq!(result.selected_slots[0].end, 16383);
assert_eq!(result.all_commands_all_slots_us, 5000);
assert_eq!(result.net_bytes_all_commands_all_slots, 2048);
assert_eq!(result.collection_start_time_unix_ms, 1700000000000);
assert_eq!(result.collection_duration_ms, 10000);
assert_eq!(result.total_cpu_time_user_ms, Some(100));
assert_eq!(result.total_cpu_time_sys_ms, Some(50));
let cpu_keys = result.by_cpu_time_us.unwrap();
assert_eq!(cpu_keys.len(), 2);
assert_eq!(cpu_keys[0].key, "key1");
assert_eq!(cpu_keys[0].value, 1500);
assert_eq!(cpu_keys[1].key, "key2");
assert_eq!(cpu_keys[1].value, 750);
}
#[test]
fn test_hotkeys_response_parsing_resp3() {
use crate::Value;
let response = Value::Map(vec![
(
Value::BulkString(b"tracking-active".to_vec()),
Value::Int(1),
),
(Value::BulkString(b"sample-ratio".to_vec()), Value::Int(1)),
(
Value::BulkString(b"selected-slots".to_vec()),
Value::Array(vec![Value::Array(vec![Value::Int(0), Value::Int(16383)])]),
),
(
Value::BulkString(b"all-commands-all-slots-us".to_vec()),
Value::Int(5000),
),
(
Value::BulkString(b"all-commands-selected-slots-us".to_vec()),
Value::Int(4000),
),
(
Value::BulkString(b"net-bytes-all-commands-all-slots".to_vec()),
Value::Int(2048),
),
(
Value::BulkString(b"net-bytes-all-commands-selected-slots".to_vec()),
Value::Int(1024),
),
(
Value::BulkString(b"collection-start-time-unix-ms".to_vec()),
Value::Int(1700000000000),
),
(
Value::BulkString(b"collection-duration-ms".to_vec()),
Value::Int(10000),
),
(
Value::BulkString(b"total-cpu-time-user-ms".to_vec()),
Value::Int(100),
),
(
Value::BulkString(b"total-cpu-time-sys-ms".to_vec()),
Value::Int(50),
),
(
Value::BulkString(b"by-cpu-time-us".to_vec()),
Value::Array(vec![
Value::BulkString(b"key1".to_vec()),
Value::Int(1500),
Value::BulkString(b"key2".to_vec()),
Value::Int(750),
]),
),
]);
let result = HotkeysResponse::from_redis_value(response).unwrap();
assert!(result.tracking_active);
assert_eq!(result.sample_ratio, 1);
assert_eq!(result.selected_slots.len(), 1);
assert_eq!(result.selected_slots[0].start, 0);
assert_eq!(result.selected_slots[0].end, 16383);
assert_eq!(result.all_commands_all_slots_us, 5000);
assert_eq!(result.all_commands_selected_slots_us, Some(4000));
assert_eq!(result.net_bytes_all_commands_all_slots, 2048);
assert_eq!(result.net_bytes_all_commands_selected_slots, Some(1024));
assert_eq!(result.collection_start_time_unix_ms, 1700000000000);
assert_eq!(result.collection_duration_ms, 10000);
assert_eq!(result.total_cpu_time_user_ms, Some(100));
assert_eq!(result.total_cpu_time_sys_ms, Some(50));
let cpu_keys = result.by_cpu_time_us.unwrap();
assert_eq!(cpu_keys.len(), 2);
assert_eq!(cpu_keys[0].key, "key1");
assert_eq!(cpu_keys[0].value, 1500);
assert_eq!(cpu_keys[1].key, "key2");
assert_eq!(cpu_keys[1].value, 750);
}
#[test]
fn test_hotkeys_response_parsing_with_net() {
use crate::Value;
let response = Value::Array(vec![
Value::BulkString(b"tracking-active".to_vec()),
Value::Int(0),
Value::BulkString(b"sample-ratio".to_vec()),
Value::Int(1),
Value::BulkString(b"selected-slots".to_vec()),
Value::Array(vec![]),
Value::BulkString(b"all-commands-all-slots-us".to_vec()),
Value::Int(0),
Value::BulkString(b"net-bytes-all-commands-all-slots".to_vec()),
Value::Int(4096),
Value::BulkString(b"collection-start-time-unix-ms".to_vec()),
Value::Int(1700000000000),
Value::BulkString(b"collection-duration-ms".to_vec()),
Value::Int(5000),
Value::BulkString(b"total-net-bytes".to_vec()),
Value::Int(8192),
Value::BulkString(b"by-net-bytes".to_vec()),
Value::Array(vec![
Value::BulkString(b"bigkey".to_vec()),
Value::Int(4096),
Value::BulkString(b"smallkey".to_vec()),
Value::Int(256),
]),
]);
let result = HotkeysResponse::from_redis_value(response).unwrap();
assert!(!result.tracking_active);
assert_eq!(result.total_net_bytes, Some(8192));
let net_keys = result.by_net_bytes.unwrap();
assert_eq!(net_keys.len(), 2);
assert_eq!(net_keys[0].key, "bigkey");
assert_eq!(net_keys[0].value, 4096);
assert_eq!(net_keys[1].key, "smallkey");
assert_eq!(net_keys[1].value, 256);
}
#[test]
fn test_hotkeys_response_nil() {
use crate::Value;
use crate::types::from_redis_value;
let response = Value::Nil;
let result: Option<HotkeysResponse> = from_redis_value(response).unwrap();
assert!(result.is_none());
}
}