Skip to content

Commit 00526d6

Browse files
authored
Add threads view (#5)
Add the `tui`'s Threads view This new view relies on the Logstash's hot-threads API to display the busiest threads and their traces. In addition to that, it also added the `User-Agent` header to HTTP requests, and fixed minor UI issues.
1 parent 66407a3 commit 00526d6

File tree

19 files changed

+807
-40
lines changed

19 files changed

+807
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.4.0
2+
- Introduced a new view `Threads`, which relies on the Logstash's hot-threads API to display the busiest threads and their traces.
3+
- Added the `User-Agent` header to requests so the source can be identified.
4+
- Minor UI fixes.
5+
16
## 0.3.0
27
- Bumped a few dependencies.
38
- Added a command option (`diagnostic-path`) to poll the data from a Logstash diagnostic path.

Cargo.lock

Lines changed: 46 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ documentation = "https://github.com/edmocosta/tuistash"
77
keywords = ["logstash", "tui", "cli", "terminal"]
88
categories = ["command-line-utilities"]
99
authors = ["Edmo Vamerlatti Costa <edinhow@gmail.com>"]
10-
version = "0.3.0"
10+
version = "0.4.0"
1111
edition = "2021"
1212

1313
[dependencies]
@@ -29,7 +29,8 @@ crossterm = { version = "0.27.0", default-features = false, features = ["event-s
2929
num-format = { version = "0.4", default-features = false, features = ["with-num-bigint"] }
3030
human_format = { version = "1.1" }
3131
uuid = { version = "1.4", features = ["v4"] }
32-
time = { version = "0.3", features = ["default", "formatting", "local-offset"] }
32+
time = { version = "0.3", features = ["default", "formatting", "local-offset", "parsing"] }
33+
regex = { version = "1.10.5", features = [] }
3334

3435
[[bin]]
3536
name = "tuistash"

docs/img/demo.gif

815 KB
Loading

src/cli/api/hot_threads.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::collections::HashMap;
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
6+
#[serde(default)]
7+
pub struct NodeHotThreads {
8+
pub hot_threads: HotThreads,
9+
}
10+
11+
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
12+
#[serde(default)]
13+
pub struct HotThreads {
14+
pub time: String,
15+
pub busiest_threads: u64,
16+
#[serde(with = "threads")]
17+
pub threads: HashMap<i64, Thread>,
18+
}
19+
20+
mod threads {
21+
use std::collections::HashMap;
22+
23+
use serde::de::{Deserialize, Deserializer};
24+
use serde::ser::Serializer;
25+
26+
use super::Thread;
27+
28+
pub fn serialize<S>(map: &HashMap<i64, Thread>, serializer: S) -> Result<S::Ok, S::Error>
29+
where
30+
S: Serializer,
31+
{
32+
serializer.collect_seq(map.values())
33+
}
34+
35+
pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<i64, Thread>, D::Error>
36+
where
37+
D: Deserializer<'de>,
38+
{
39+
let vertices = Vec::<Thread>::deserialize(deserializer)?;
40+
let mut map = HashMap::with_capacity(vertices.len());
41+
42+
for item in vertices {
43+
map.insert(item.thread_id, item);
44+
}
45+
46+
Ok(map)
47+
}
48+
}
49+
50+
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
51+
#[serde(default)]
52+
pub struct Thread {
53+
pub name: String,
54+
pub thread_id: i64,
55+
pub percent_of_cpu_time: f64,
56+
pub state: String,
57+
pub traces: Vec<String>,
58+
}

src/cli/api/mod.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ use std::sync::Arc;
33

44
use base64::prelude::BASE64_STANDARD;
55
use base64::write::EncoderWriter;
6-
use ureq::{Agent, Response};
6+
use ureq::{Agent, AgentBuilder, Response};
77

88
use crate::api::tls::SkipServerVerification;
99
use crate::errors::AnyError;
1010

11+
pub mod hot_threads;
1112
pub mod node;
1213
pub mod node_api;
1314
pub mod stats;
@@ -46,21 +47,23 @@ impl<'a> Client<'a> {
4647
password: Option<&'a str>,
4748
skip_tls_verification: bool,
4849
) -> Result<Self, AnyError> {
49-
let agent: Agent = if skip_tls_verification {
50+
let user_agent = "tuistash";
51+
52+
let agent_builder: AgentBuilder = if skip_tls_verification {
5053
let tls_config = rustls::ClientConfig::builder()
5154
.dangerous()
5255
.with_custom_certificate_verifier(SkipServerVerification::new())
5356
.with_no_client_auth();
54-
55-
ureq::AgentBuilder::new()
57+
AgentBuilder::new()
58+
.user_agent(user_agent)
5659
.tls_config(Arc::new(tls_config))
57-
.build()
5860
} else {
59-
ureq::AgentBuilder::new().build()
60-
};
61+
AgentBuilder::new().user_agent(user_agent)
62+
}
63+
.user_agent(format!("tuistash/{}", env!("CARGO_PKG_VERSION")).as_str());
6164

6265
Ok(Self {
63-
client: agent,
66+
client: agent_builder.build(),
6467
config: ClientConfig {
6568
base_url: host.to_string(),
6669
username,

src/cli/api/node_api.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::api::hot_threads::NodeHotThreads;
12
use crate::api::node::{NodeInfo, NodeInfoType};
23
use crate::api::stats::NodeStats;
34
use crate::api::Client;
@@ -44,6 +45,15 @@ impl Client<'_> {
4445
Ok(node_stats)
4546
}
4647

48+
pub fn get_hot_threads(
49+
&self,
50+
query: Option<&[(&str, &str)]>,
51+
) -> Result<NodeHotThreads, AnyError> {
52+
let response = self.request("GET", &self.node_request_path("hot_threads"), query)?;
53+
let hot_threads: NodeHotThreads = response.into_json()?;
54+
Ok(hot_threads)
55+
}
56+
4757
fn node_info_request_path(&self, types: &[NodeInfoType]) -> String {
4858
let filterable_types = types
4959
.iter()

src/cli/commands/tui/app.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::time::Duration;
22

3+
use crate::api::hot_threads::NodeHotThreads;
34
use crossterm::event::{KeyCode, KeyEvent};
45

56
use crate::api::node::NodeInfo;
@@ -11,6 +12,7 @@ use crate::commands::tui::flows::state::FlowsState;
1112
use crate::commands::tui::node::state::NodeState;
1213
use crate::commands::tui::pipelines::state::PipelinesState;
1314
use crate::commands::tui::shared_state::SharedState;
15+
use crate::commands::tui::threads::state::ThreadsState;
1416
use crate::commands::tui::widgets::TabsState;
1517
use crate::errors::AnyError;
1618

@@ -20,6 +22,7 @@ pub(crate) struct AppData<'a> {
2022
data_fetcher: &'a dyn DataFetcher<'a>,
2123
node_info: Option<NodeInfo>,
2224
node_stats: Option<NodeStats>,
25+
hot_threads: Option<NodeHotThreads>,
2326
}
2427

2528
impl<'a> AppData<'a> {
@@ -30,6 +33,7 @@ impl<'a> AppData<'a> {
3033
data_fetcher,
3134
node_stats: None,
3235
node_info: None,
36+
hot_threads: None,
3337
}
3438
}
3539

@@ -65,6 +69,16 @@ impl<'a> AppData<'a> {
6569
}
6670
}
6771

72+
match self.data_fetcher.fetch_hot_threads() {
73+
Ok(hot_threads) => {
74+
self.hot_threads = Some(hot_threads);
75+
}
76+
Err(e) => {
77+
self.handle_error(&e);
78+
return Err(e);
79+
}
80+
}
81+
6882
data_decorator::decorate(
6983
self.node_info.as_mut().unwrap(),
7084
self.node_stats.as_mut().unwrap(),
@@ -84,6 +98,10 @@ impl<'a> AppData<'a> {
8498
return self.node_stats.as_ref();
8599
}
86100

101+
pub(crate) fn hot_threads(&self) -> Option<&NodeHotThreads> {
102+
return self.hot_threads.as_ref();
103+
}
104+
87105
pub(crate) fn errored(&self) -> bool {
88106
self.errored
89107
}
@@ -102,6 +120,7 @@ pub(crate) struct App<'a> {
102120
pub node_state: NodeState,
103121
pub pipelines_state: PipelinesState<'a>,
104122
pub flows_state: FlowsState,
123+
pub threads_state: ThreadsState,
105124
pub data: AppData<'a>,
106125
pub host: &'a str,
107126
pub sampling_interval: Option<Duration>,
@@ -110,7 +129,8 @@ pub(crate) struct App<'a> {
110129
impl<'a> App<'a> {
111130
pub const TAB_PIPELINES: usize = 0;
112131
pub const TAB_FLOWS: usize = 1;
113-
pub const TAB_NODE: usize = 2;
132+
pub const TAB_THREADS: usize = 2;
133+
pub const TAB_NODE: usize = 3;
114134

115135
pub fn new(
116136
title: &'a str,
@@ -130,6 +150,7 @@ impl<'a> App<'a> {
130150
host,
131151
shared_state: SharedState::new(),
132152
flows_state: FlowsState::new(),
153+
threads_state: ThreadsState::new(),
133154
}
134155
}
135156

@@ -200,6 +221,9 @@ impl<'a> App<'a> {
200221
"n" => {
201222
self.select_tab(Self::TAB_NODE);
202223
}
224+
"t" => {
225+
self.select_tab(Self::TAB_THREADS);
226+
}
203227
_ => {}
204228
}
205229
}
@@ -225,6 +249,7 @@ impl<'a> App<'a> {
225249
&mut self.pipelines_state,
226250
&mut self.flows_state,
227251
&mut self.node_state,
252+
&mut self.threads_state,
228253
];
229254

230255
for listener in listeners {
@@ -249,6 +274,7 @@ impl<'a> App<'a> {
249274
Self::TAB_PIPELINES => Some(&mut self.pipelines_state),
250275
Self::TAB_NODE => Some(&mut self.node_state),
251276
Self::TAB_FLOWS => Some(&mut self.flows_state),
277+
Self::TAB_THREADS => Some(&mut self.threads_state),
252278
_ => None,
253279
};
254280

0 commit comments

Comments
 (0)