1use std::net::SocketAddr;
7
8use axum::{Router, http::StatusCode, response::IntoResponse, routing::get};
9use metrics::{counter, gauge, histogram};
10use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
11
12pub fn init_metrics_server(
23 listen: &str,
24 extra_routes: Option<Router>,
25) -> Result<tokio::task::JoinHandle<()>, String> {
26 let addr: SocketAddr = listen
27 .parse()
28 .map_err(|e| format!("invalid metrics listen address: {}", e))?;
29
30 let builder = PrometheusBuilder::new();
32 let handle = builder
33 .install_recorder()
34 .map_err(|e| format!("failed to install prometheus recorder: {}", e))?;
35
36 let mut app = Router::new()
38 .route("/metrics", get(move || metrics_handler(handle.clone())))
39 .route("/health", get(health_handler))
40 .route("/ready", get(ready_handler));
41
42 if let Some(extra) = extra_routes {
43 app = app.merge(extra);
44 }
45
46 let server_handle = tokio::spawn(async move {
48 let listener = match tokio::net::TcpListener::bind(addr).await {
49 Ok(l) => l,
50 Err(e) => {
51 eprintln!("failed to bind metrics server to {}: {}", addr, e);
52 return;
53 }
54 };
55 if let Err(e) = axum::serve(
56 listener,
57 app.into_make_service_with_connect_info::<SocketAddr>(),
58 )
59 .await
60 {
61 eprintln!("metrics server error: {}", e);
62 }
63 });
64
65 Ok(server_handle)
66}
67
68async fn metrics_handler(handle: PrometheusHandle) -> impl IntoResponse {
70 handle.render()
71}
72
73async fn health_handler() -> impl IntoResponse {
75 (StatusCode::OK, "OK")
76}
77
78async fn ready_handler() -> impl IntoResponse {
80 (StatusCode::OK, "READY")
81}
82
83#[deprecated(since = "0.2.0", note = "Use init_metrics_server instead")]
88pub fn init_prometheus(listen: &str) -> Result<(), String> {
89 let addr: SocketAddr = listen
90 .parse()
91 .map_err(|e| format!("invalid metrics listen address: {}", e))?;
92
93 PrometheusBuilder::new()
94 .with_http_listener(addr)
95 .install()
96 .map_err(|e| format!("failed to install prometheus exporter: {}", e))?;
97
98 Ok(())
99}
100
101pub const CONNECTIONS_TOTAL: &str = "trojan_connections_total";
107pub const CONNECTIONS_ACTIVE: &str = "trojan_connections_active";
109pub const AUTH_SUCCESS_TOTAL: &str = "trojan_auth_success_total";
111pub const AUTH_FAILURE_TOTAL: &str = "trojan_auth_failure_total";
113pub const FALLBACK_TOTAL: &str = "trojan_fallback_total";
115pub const BYTES_RECEIVED_TOTAL: &str = "trojan_bytes_received_total";
117pub const BYTES_SENT_TOTAL: &str = "trojan_bytes_sent_total";
119pub const CONNECT_REQUESTS_TOTAL: &str = "trojan_connect_requests_total";
121pub const UDP_ASSOCIATE_REQUESTS_TOTAL: &str = "trojan_udp_associate_requests_total";
123pub const UDP_PACKETS_TOTAL: &str = "trojan_udp_packets_total";
125pub const CONNECTION_DURATION_SECONDS: &str = "trojan_connection_duration_seconds";
127pub const ERRORS_TOTAL: &str = "trojan_errors_total";
129pub const CONNECTIONS_REJECTED_TOTAL: &str = "trojan_connections_rejected_total";
131pub const TLS_HANDSHAKE_DURATION_SECONDS: &str = "trojan_tls_handshake_duration_seconds";
133pub const CONNECTION_QUEUE_DEPTH: &str = "trojan_connection_queue_depth";
135pub const TARGET_CONNECTIONS_TOTAL: &str = "trojan_target_connections_total";
137pub const TARGET_BYTES_TOTAL: &str = "trojan_target_bytes_total";
139pub const FALLBACK_POOL_SIZE: &str = "trojan_fallback_pool_size";
141pub const FALLBACK_POOL_WARM_FAIL_TOTAL: &str = "trojan_fallback_pool_warm_fail_total";
143pub const DNS_RESOLVE_DURATION_SECONDS: &str = "trojan_dns_resolve_duration_seconds";
145pub const TARGET_CONNECT_DURATION_SECONDS: &str = "trojan_target_connect_duration_seconds";
147pub const RULE_UPDATES_TOTAL: &str = "trojan_rule_updates_total";
149pub const RULE_UPDATE_ERRORS_TOTAL: &str = "trojan_rule_update_errors_total";
151pub const CONNECTIONS_BY_COUNTRY: &str = "trojan_connections_by_country_total";
153pub const BYTES_BY_COUNTRY: &str = "trojan_bytes_by_country_total";
155pub const AUTH_FAILURE_BY_COUNTRY: &str = "trojan_auth_failure_by_country_total";
157
158#[inline]
164pub fn record_connection_accepted() {
165 counter!(CONNECTIONS_TOTAL).increment(1);
166 gauge!(CONNECTIONS_ACTIVE).increment(1.0);
167}
168
169#[inline]
171pub fn record_connection_closed(duration_secs: f64) {
172 gauge!(CONNECTIONS_ACTIVE).decrement(1.0);
173 histogram!(CONNECTION_DURATION_SECONDS).record(duration_secs);
174}
175
176#[inline]
178pub fn record_auth_success() {
179 counter!(AUTH_SUCCESS_TOTAL).increment(1);
180}
181
182#[inline]
184pub fn record_auth_failure() {
185 counter!(AUTH_FAILURE_TOTAL).increment(1);
186}
187
188#[inline]
190pub fn record_fallback() {
191 counter!(FALLBACK_TOTAL).increment(1);
192}
193
194#[inline]
196pub fn record_bytes_received(bytes: u64) {
197 counter!(BYTES_RECEIVED_TOTAL).increment(bytes);
198}
199
200#[inline]
202pub fn record_bytes_sent(bytes: u64) {
203 counter!(BYTES_SENT_TOTAL).increment(bytes);
204}
205
206#[inline]
208pub fn record_connect_request() {
209 counter!(CONNECT_REQUESTS_TOTAL).increment(1);
210}
211
212#[inline]
214pub fn record_udp_associate_request() {
215 counter!(UDP_ASSOCIATE_REQUESTS_TOTAL).increment(1);
216}
217
218#[inline]
220pub fn record_udp_packet(direction: &'static str) {
221 counter!(UDP_PACKETS_TOTAL, "direction" => direction).increment(1);
222}
223
224#[inline]
226pub fn record_error(error_type: &'static str) {
227 counter!(ERRORS_TOTAL, "type" => error_type).increment(1);
228}
229
230#[inline]
232pub fn record_connection_rejected(reason: &'static str) {
233 counter!(CONNECTIONS_REJECTED_TOTAL, "reason" => reason).increment(1);
234}
235
236#[inline]
238pub fn record_tls_handshake_duration(duration_secs: f64) {
239 histogram!(TLS_HANDSHAKE_DURATION_SECONDS).record(duration_secs);
240}
241
242#[inline]
244pub fn set_connection_queue_depth(depth: f64) {
245 gauge!(CONNECTION_QUEUE_DEPTH).set(depth);
246}
247
248#[inline]
253pub fn record_target_connection(target: &str) {
254 counter!(TARGET_CONNECTIONS_TOTAL, "target" => target.to_owned()).increment(1);
255}
256
257#[inline]
262pub fn record_target_bytes(target: &str, direction: &'static str, bytes: u64) {
263 counter!(TARGET_BYTES_TOTAL, "target" => target.to_owned(), "direction" => direction)
264 .increment(bytes);
265}
266
267#[inline]
269pub fn set_fallback_pool_size(size: usize) {
270 gauge!(FALLBACK_POOL_SIZE).set(size as f64);
271}
272
273#[inline]
275pub fn record_fallback_pool_warm_fail() {
276 counter!(FALLBACK_POOL_WARM_FAIL_TOTAL).increment(1);
277}
278
279#[inline]
281pub fn record_dns_resolve_duration(duration_secs: f64) {
282 histogram!(DNS_RESOLVE_DURATION_SECONDS).record(duration_secs);
283}
284
285#[inline]
287pub fn record_target_connect_duration(duration_secs: f64) {
288 histogram!(TARGET_CONNECT_DURATION_SECONDS).record(duration_secs);
289}
290
291#[inline]
293pub fn record_rule_update() {
294 counter!(RULE_UPDATES_TOTAL).increment(1);
295}
296
297#[inline]
299pub fn record_rule_update_error() {
300 counter!(RULE_UPDATE_ERRORS_TOTAL).increment(1);
301}
302
303#[inline]
305pub fn record_connection_with_geo(country: &str) {
306 counter!(CONNECTIONS_BY_COUNTRY, "country" => country.to_owned()).increment(1);
307}
308
309#[inline]
312pub fn record_bytes_with_geo(country: &str, direction: &'static str, bytes: u64) {
313 counter!(BYTES_BY_COUNTRY, "country" => country.to_owned(), "direction" => direction)
314 .increment(bytes);
315}
316
317#[inline]
319pub fn record_auth_failure_with_geo(country: &str) {
320 counter!(AUTH_FAILURE_BY_COUNTRY, "country" => country.to_owned()).increment(1);
321}
322
323pub use trojan_core::{
328 ERROR_AUTH, ERROR_CONFIG, ERROR_IO, ERROR_PROTOCOL, ERROR_RESOLVE, ERROR_TIMEOUT,
329 ERROR_TLS_HANDSHAKE,
330};