Skip to content

Commit f74101c

Browse files
authored
admin: SHOW STATS (#46)
* admin: show stats * warning * tests * lint * type mod
1 parent 8e06824 commit f74101c

File tree

7 files changed

+184
-2
lines changed

7 files changed

+184
-2
lines changed

.circleci/run_tests.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ cd tests/ruby && \
5757
ruby tests.rb && \
5858
cd ../..
5959

60+
# Admin tests
61+
psql -e -h 127.0.0.1 -p 6432 -d pgbouncer -c 'SHOW STATS' > /dev/null
62+
(! psql -e -h 127.0.0.1 -p 6432 -d random_db -c 'SHOW STATS' > /dev/null)
63+
6064
# Start PgCat in debug to demonstrate failover better
6165
start_pgcat "debug"
6266

src/admin.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use bytes::{Buf, BufMut, BytesMut};
2+
use log::trace;
3+
use tokio::net::tcp::OwnedWriteHalf;
4+
5+
use std::collections::HashMap;
6+
7+
use crate::constants::{OID_NUMERIC, OID_TEXT};
8+
use crate::errors::Error;
9+
use crate::messages::write_all_half;
10+
use crate::stats::get_stats;
11+
12+
/// Handle admin client
13+
pub async fn handle_admin(stream: &mut OwnedWriteHalf, mut query: BytesMut) -> Result<(), Error> {
14+
let code = query.get_u8() as char;
15+
16+
if code != 'Q' {
17+
return Err(Error::ProtocolSyncError);
18+
}
19+
20+
let len = query.get_i32() as usize;
21+
let query = String::from_utf8_lossy(&query[..len - 5])
22+
.to_string()
23+
.to_ascii_uppercase();
24+
25+
if query.starts_with("SHOW STATS") {
26+
trace!("SHOW STATS");
27+
show_stats(stream).await
28+
} else {
29+
Err(Error::ProtocolSyncError)
30+
}
31+
}
32+
33+
/// SHOW STATS
34+
pub async fn show_stats(stream: &mut OwnedWriteHalf) -> Result<(), Error> {
35+
let columns = [
36+
"database",
37+
"total_xact_count",
38+
"total_query_count",
39+
"total_received",
40+
"total_sent",
41+
"total_xact_time",
42+
"total_query_time",
43+
"total_wait_time",
44+
"avg_xact_count",
45+
"avg_query_count",
46+
"avg_recv",
47+
"avg_sent",
48+
"avg_xact_time",
49+
"avg_query_time",
50+
"avg_wait_time",
51+
];
52+
53+
let stats = get_stats().unwrap_or(HashMap::new());
54+
let mut res = BytesMut::new();
55+
let mut row_desc = BytesMut::new();
56+
let mut data_row = BytesMut::new();
57+
58+
// Number of columns: 1
59+
row_desc.put_i16(columns.len() as i16);
60+
data_row.put_i16(columns.len() as i16);
61+
62+
for (i, column) in columns.iter().enumerate() {
63+
// RowDescription
64+
65+
// Column name
66+
row_desc.put_slice(&format!("{}\0", column).as_bytes());
67+
68+
// Doesn't belong to any table
69+
row_desc.put_i32(0);
70+
71+
// Doesn't belong to any table
72+
row_desc.put_i16(0);
73+
74+
// Data type
75+
row_desc.put_i32(if i == 0 { OID_TEXT } else { OID_NUMERIC });
76+
77+
// Numeric/text size = variable (-1)
78+
row_desc.put_i16(-1);
79+
80+
// Type modifier: none that I know
81+
row_desc.put_i32(-1);
82+
83+
// Format being used: text (0), binary (1)
84+
row_desc.put_i16(0);
85+
86+
// DataRow
87+
let value = if i == 0 {
88+
String::from("all shards")
89+
} else {
90+
stats.get(&column.to_string()).unwrap_or(&0).to_string()
91+
};
92+
93+
data_row.put_i32(value.len() as i32);
94+
data_row.put_slice(value.as_bytes());
95+
}
96+
97+
let command_complete = BytesMut::from(&"SHOW\0"[..]);
98+
99+
res.put_u8(b'T');
100+
res.put_i32(row_desc.len() as i32 + 4);
101+
res.put(row_desc);
102+
103+
res.put_u8(b'D');
104+
res.put_i32(data_row.len() as i32 + 4);
105+
res.put(data_row);
106+
107+
res.put_u8(b'C');
108+
res.put_i32(command_complete.len() as i32 + 4);
109+
res.put(command_complete);
110+
111+
res.put_u8(b'Z');
112+
res.put_i32(5);
113+
res.put_u8(b'I');
114+
115+
write_all_half(stream, res).await
116+
}

src/client.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tokio::net::{
1111

1212
use std::collections::HashMap;
1313

14+
use crate::admin::handle_admin;
1415
use crate::config::get_config;
1516
use crate::constants::*;
1617
use crate::errors::Error;
@@ -54,6 +55,9 @@ pub struct Client {
5455

5556
// Statistics
5657
stats: Reporter,
58+
59+
// Clients want to talk to admin
60+
admin: bool,
5761
}
5862

5963
impl Client {
@@ -118,6 +122,15 @@ impl Client {
118122
ready_for_query(&mut stream).await?;
119123
trace!("Startup OK");
120124

125+
let database = parameters
126+
.get("database")
127+
.unwrap_or(parameters.get("user").unwrap());
128+
let admin = ["pgcat", "pgbouncer"]
129+
.iter()
130+
.filter(|db| *db == &database)
131+
.count()
132+
== 1;
133+
121134
// Split the read and write streams
122135
// so we can control buffering.
123136
let (read, write) = stream.into_split();
@@ -133,6 +146,7 @@ impl Client {
133146
client_server_map: client_server_map,
134147
parameters: parameters,
135148
stats: stats,
149+
admin: admin,
136150
});
137151
}
138152

@@ -154,6 +168,7 @@ impl Client {
154168
client_server_map: client_server_map,
155169
parameters: HashMap::new(),
156170
stats: stats,
171+
admin: false,
157172
});
158173
}
159174

@@ -220,6 +235,13 @@ impl Client {
220235
return Ok(());
221236
}
222237

238+
// Handle admin database real quick
239+
if self.admin {
240+
trace!("Handling admin command");
241+
handle_admin(&mut self.write, message).await?;
242+
continue;
243+
}
244+
223245
// Handle all custom protocol commands here.
224246
match query_router.try_execute_command(message.clone()) {
225247
// Normal query

src/constants.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ pub const AUTHENTICATION_SUCCESSFUL: i32 = 0;
2020

2121
// ErrorResponse: A code identifying the field type; if zero, this is the message terminator and no string follows.
2222
pub const MESSAGE_TERMINATOR: u8 = 0;
23+
24+
//
25+
// Data types
26+
//
27+
pub const OID_NUMERIC: i32 = 1700;
28+
pub const OID_TEXT: i32 = 25;

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use tokio::{
4747
use std::collections::HashMap;
4848
use std::sync::Arc;
4949

50+
mod admin;
5051
mod client;
5152
mod config;
5253
mod constants;

src/messages.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ pub async fn show_response(
274274
row_desc.put_i16(-1);
275275

276276
// Type modifier: none that I know
277-
row_desc.put_i32(0);
277+
row_desc.put_i32(-1);
278278

279279
// Format being used: text (0), binary (1)
280280
row_desc.put_i16(0);

src/stats.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
use log::info;
1+
use log::{error, info};
2+
use once_cell::sync::OnceCell;
23
use statsd::Client;
34
/// Events collector and publisher.
45
use tokio::sync::mpsc::{Receiver, Sender};
56

67
use std::collections::HashMap;
8+
use std::sync::{Arc, Mutex};
79

810
use crate::config::get_config;
911

12+
static LATEST_STATS: OnceCell<Arc<Mutex<HashMap<String, i64>>>> = OnceCell::new();
13+
1014
#[derive(Debug, Clone, Copy)]
1115
enum EventName {
1216
CheckoutTime,
@@ -212,6 +216,13 @@ impl Collector {
212216
pub async fn collect(&mut self) {
213217
info!("Events reporter started");
214218

219+
match LATEST_STATS.set(Arc::new(Mutex::new(HashMap::new()))) {
220+
Ok(_) => (),
221+
Err(_) => {
222+
error!("Latest stats will not be available");
223+
}
224+
};
225+
215226
let mut stats = HashMap::from([
216227
("total_query_count", 0),
217228
("total_xact_count", 0),
@@ -352,6 +363,17 @@ impl Collector {
352363

353364
info!("{:?}", stats);
354365

366+
match LATEST_STATS.get() {
367+
Some(arc) => {
368+
let mut guard = arc.lock().unwrap();
369+
for (key, value) in &stats {
370+
guard.insert(key.to_string(), value.clone());
371+
}
372+
}
373+
374+
None => (),
375+
};
376+
355377
let mut pipeline = self.client.pipeline();
356378

357379
for (key, value) in stats.iter_mut() {
@@ -365,3 +387,14 @@ impl Collector {
365387
}
366388
}
367389
}
390+
391+
pub fn get_stats() -> Option<HashMap<String, i64>> {
392+
match LATEST_STATS.get() {
393+
Some(arc) => {
394+
let guard = arc.lock().unwrap();
395+
Some(guard.clone())
396+
}
397+
398+
None => None,
399+
}
400+
}

0 commit comments

Comments
 (0)