diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-04 15:17:04 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-04 15:24:19 +0000 |
| commit | fd0c87c787d0626b3546fa571541c9c809711821 (patch) | |
| tree | 934f20d973127f380b807d2bd44b25c197cf349c /src/http | |
| parent | 762cd8e815e797f173f541795de774fbbf978fc3 (diff) | |
add prometheus metrics
Diffstat (limited to 'src/http')
| -rw-r--r-- | src/http/mod.rs | 84 | ||||
| -rw-r--r-- | src/http/nip11.rs | 6 |
2 files changed, 85 insertions, 5 deletions
diff --git a/src/http/mod.rs b/src/http/mod.rs index 8b1f687..f584e03 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs | |||
| @@ -7,6 +7,7 @@ pub mod nip11; | |||
| 7 | use std::future::Future; | 7 | use std::future::Future; |
| 8 | use std::net::SocketAddr; | 8 | use std::net::SocketAddr; |
| 9 | use std::pin::Pin; | 9 | use std::pin::Pin; |
| 10 | use std::sync::Arc; | ||
| 10 | 11 | ||
| 11 | use base64::Engine; | 12 | use base64::Engine; |
| 12 | use http_body_util::{BodyExt, Full}; | 13 | use http_body_util::{BodyExt, Full}; |
| @@ -24,6 +25,7 @@ use tokio::net::TcpListener; | |||
| 24 | 25 | ||
| 25 | use crate::config::Config; | 26 | use crate::config::Config; |
| 26 | use crate::git; | 27 | use crate::git; |
| 28 | use crate::metrics::Metrics; | ||
| 27 | use crate::nostr::builder::SharedDatabase; | 29 | use crate::nostr::builder::SharedDatabase; |
| 28 | 30 | ||
| 29 | /// CORS headers required by GRASP-01 specification (lines 40-47) | 31 | /// CORS headers required by GRASP-01 specification (lines 40-47) |
| @@ -90,6 +92,8 @@ struct HttpService { | |||
| 90 | remote: SocketAddr, | 92 | remote: SocketAddr, |
| 91 | /// Database reference for direct queries (e.g., push authorization) | 93 | /// Database reference for direct queries (e.g., push authorization) |
| 92 | database: SharedDatabase, | 94 | database: SharedDatabase, |
| 95 | /// Optional metrics for Prometheus endpoint | ||
| 96 | metrics: Option<Arc<Metrics>>, | ||
| 93 | } | 97 | } |
| 94 | 98 | ||
| 95 | impl HttpService { | 99 | impl HttpService { |
| @@ -98,12 +102,14 @@ impl HttpService { | |||
| 98 | config: Config, | 102 | config: Config, |
| 99 | remote: SocketAddr, | 103 | remote: SocketAddr, |
| 100 | database: SharedDatabase, | 104 | database: SharedDatabase, |
| 105 | metrics: Option<Arc<Metrics>>, | ||
| 101 | ) -> Self { | 106 | ) -> Self { |
| 102 | Self { | 107 | Self { |
| 103 | relay, | 108 | relay, |
| 104 | config, | 109 | config, |
| 105 | remote, | 110 | remote, |
| 106 | database, | 111 | database, |
| 112 | metrics, | ||
| 107 | } | 113 | } |
| 108 | } | 114 | } |
| 109 | } | 115 | } |
| @@ -150,6 +156,7 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 150 | ); | 156 | ); |
| 151 | 157 | ||
| 152 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); | 158 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); |
| 159 | let metrics_clone = self.metrics.clone(); | ||
| 153 | 160 | ||
| 154 | return Box::pin(async move { | 161 | return Box::pin(async move { |
| 155 | // Collect request body once before the match statement | 162 | // Collect request body once before the match statement |
| @@ -170,14 +177,31 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 170 | .and_then(git::protocol::GitService::from_query_param); | 177 | .and_then(git::protocol::GitService::from_query_param); |
| 171 | 178 | ||
| 172 | match service { | 179 | match service { |
| 173 | Some(svc) => git::handlers::handle_info_refs(repo_path, svc).await, | 180 | Some(svc) => { |
| 181 | let result = git::handlers::handle_info_refs(repo_path, svc).await; | ||
| 182 | // Track operation | ||
| 183 | if let Some(ref m) = metrics_clone { | ||
| 184 | let status = if result.is_ok() { "success" } else { "error" }; | ||
| 185 | let operation = match svc { | ||
| 186 | git::protocol::GitService::UploadPack => "fetch", | ||
| 187 | git::protocol::GitService::ReceivePack => "push", | ||
| 188 | }; | ||
| 189 | m.record_git_operation(operation, status); | ||
| 190 | } | ||
| 191 | result | ||
| 192 | } | ||
| 174 | None => Err(git::handlers::GitError::RepositoryNotFound), | 193 | None => Err(git::handlers::GitError::RepositoryNotFound), |
| 175 | } | 194 | } |
| 176 | } | 195 | } |
| 177 | 196 | ||
| 178 | // POST /git-upload-pack (clone/fetch) | 197 | // POST /git-upload-pack (clone/fetch) |
| 179 | (m, "git-upload-pack") if m == Method::POST => { | 198 | (m, "git-upload-pack") if m == Method::POST => { |
| 180 | git::handlers::handle_upload_pack(repo_path, body_bytes).await | 199 | let result = git::handlers::handle_upload_pack(repo_path, body_bytes).await; |
| 200 | if let Some(ref m) = metrics_clone { | ||
| 201 | let status = if result.is_ok() { "success" } else { "error" }; | ||
| 202 | m.record_git_operation("clone", status); | ||
| 203 | } | ||
| 204 | result | ||
| 181 | } | 205 | } |
| 182 | 206 | ||
| 183 | // POST /git-receive-pack (push) - with GRASP authorization via database | 207 | // POST /git-receive-pack (push) - with GRASP authorization via database |
| @@ -187,6 +211,10 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 187 | Ok(pk) => pk.to_hex(), | 211 | Ok(pk) => pk.to_hex(), |
| 188 | Err(e) => { | 212 | Err(e) => { |
| 189 | tracing::warn!("Invalid npub in URL {}: {}", npub, e); | 213 | tracing::warn!("Invalid npub in URL {}: {}", npub, e); |
| 214 | // Track failed push due to invalid npub | ||
| 215 | if let Some(ref m) = metrics_clone { | ||
| 216 | m.record_git_operation("push", "error"); | ||
| 217 | } | ||
| 190 | return Ok(add_cors_headers(Response::builder()) | 218 | return Ok(add_cors_headers(Response::builder()) |
| 191 | .status(hyper::StatusCode::BAD_REQUEST) | 219 | .status(hyper::StatusCode::BAD_REQUEST) |
| 192 | .body(Full::new(Bytes::from(format!("Invalid npub: {}", e)))) | 220 | .body(Full::new(Bytes::from(format!("Invalid npub: {}", e)))) |
| @@ -194,14 +222,21 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 194 | } | 222 | } |
| 195 | }; | 223 | }; |
| 196 | 224 | ||
| 197 | git::handlers::handle_receive_pack( | 225 | let result = git::handlers::handle_receive_pack( |
| 198 | repo_path, | 226 | repo_path, |
| 199 | body_bytes.clone(), | 227 | body_bytes.clone(), |
| 200 | Some(database.clone()), | 228 | Some(database.clone()), |
| 201 | &identifier, | 229 | &identifier, |
| 202 | &owner_pubkey_hex, | 230 | &owner_pubkey_hex, |
| 203 | ) | 231 | ) |
| 204 | .await | 232 | .await; |
| 233 | |||
| 234 | if let Some(ref m) = metrics_clone { | ||
| 235 | let status = if result.is_ok() { "success" } else { "error" }; | ||
| 236 | m.record_git_operation("push", status); | ||
| 237 | } | ||
| 238 | |||
| 239 | result | ||
| 205 | } | 240 | } |
| 206 | 241 | ||
| 207 | _ => Err(git::handlers::GitError::RepositoryNotFound), | 242 | _ => Err(git::handlers::GitError::RepositoryNotFound), |
| @@ -333,17 +368,27 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 333 | 368 | ||
| 334 | let addr = self.remote; | 369 | let addr = self.remote; |
| 335 | let relay = self.relay.clone(); | 370 | let relay = self.relay.clone(); |
| 371 | let metrics_clone = self.metrics.clone(); | ||
| 336 | 372 | ||
| 337 | tokio::spawn(async move { | 373 | tokio::spawn(async move { |
| 338 | match hyper::upgrade::on(req).await { | 374 | match hyper::upgrade::on(req).await { |
| 339 | Ok(upgraded) => { | 375 | Ok(upgraded) => { |
| 340 | tracing::info!("WebSocket connection established from {}", addr); | 376 | tracing::info!("WebSocket connection established from {}", addr); |
| 377 | // Track connection | ||
| 378 | if let Some(ref m) = metrics_clone { | ||
| 379 | m.connection_tracker().on_connect(addr.ip()); | ||
| 380 | m.record_websocket_connection(); | ||
| 381 | } | ||
| 341 | if let Err(e) = | 382 | if let Err(e) = |
| 342 | relay.take_connection(TokioIo::new(upgraded), addr).await | 383 | relay.take_connection(TokioIo::new(upgraded), addr).await |
| 343 | { | 384 | { |
| 344 | tracing::error!("Relay error for {}: {}", addr, e); | 385 | tracing::error!("Relay error for {}: {}", addr, e); |
| 345 | } | 386 | } |
| 346 | tracing::info!("WebSocket connection closed for {}", addr); | 387 | tracing::info!("WebSocket connection closed for {}", addr); |
| 388 | // Untrack connection | ||
| 389 | if let Some(ref m) = metrics_clone { | ||
| 390 | m.connection_tracker().on_disconnect(addr.ip()); | ||
| 391 | } | ||
| 347 | } | 392 | } |
| 348 | Err(e) => tracing::error!("Upgrade error: {}", e), | 393 | Err(e) => tracing::error!("Upgrade error: {}", e), |
| 349 | } | 394 | } |
| @@ -361,6 +406,33 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 361 | } | 406 | } |
| 362 | } | 407 | } |
| 363 | 408 | ||
| 409 | // Serve Prometheus metrics if enabled | ||
| 410 | if path == "/metrics" { | ||
| 411 | if let Some(ref metrics) = self.metrics { | ||
| 412 | let metrics = metrics.clone(); | ||
| 413 | return Box::pin(async move { | ||
| 414 | let output = metrics.render(); | ||
| 415 | Ok( | ||
| 416 | add_cors_headers(Response::builder().header("server", "ngit-grasp")) | ||
| 417 | .status(200) | ||
| 418 | .header("content-type", "text/plain; version=0.0.4; charset=utf-8") | ||
| 419 | .body(Full::new(Bytes::from(output))) | ||
| 420 | .unwrap(), | ||
| 421 | ) | ||
| 422 | }); | ||
| 423 | } else { | ||
| 424 | // Metrics disabled | ||
| 425 | return Box::pin(async move { | ||
| 426 | Ok( | ||
| 427 | add_cors_headers(Response::builder().header("server", "ngit-grasp")) | ||
| 428 | .status(404) | ||
| 429 | .body(Full::new(Bytes::from("Metrics disabled"))) | ||
| 430 | .unwrap(), | ||
| 431 | ) | ||
| 432 | }); | ||
| 433 | } | ||
| 434 | } | ||
| 435 | |||
| 364 | // Serve static icon at /icon.png | 436 | // Serve static icon at /icon.png |
| 365 | if path == "/icon.png" { | 437 | if path == "/icon.png" { |
| 366 | return Box::pin(async move { | 438 | return Box::pin(async move { |
| @@ -419,10 +491,12 @@ fn derive_accept_key(request_key: &[u8]) -> String { | |||
| 419 | /// * `config` - Server configuration | 491 | /// * `config` - Server configuration |
| 420 | /// * `relay` - The LocalRelay for WebSocket connections | 492 | /// * `relay` - The LocalRelay for WebSocket connections |
| 421 | /// * `database` - The database for direct queries (e.g., push authorization) | 493 | /// * `database` - The database for direct queries (e.g., push authorization) |
| 494 | /// * `metrics` - Optional metrics for Prometheus endpoint | ||
| 422 | pub async fn run_server( | 495 | pub async fn run_server( |
| 423 | config: Config, | 496 | config: Config, |
| 424 | relay: LocalRelay, | 497 | relay: LocalRelay, |
| 425 | database: SharedDatabase, | 498 | database: SharedDatabase, |
| 499 | metrics: Option<Arc<Metrics>>, | ||
| 426 | ) -> anyhow::Result<()> { | 500 | ) -> anyhow::Result<()> { |
| 427 | let bind_addr: SocketAddr = config.bind_address.parse()?; | 501 | let bind_addr: SocketAddr = config.bind_address.parse()?; |
| 428 | 502 | ||
| @@ -435,7 +509,7 @@ pub async fn run_server( | |||
| 435 | loop { | 509 | loop { |
| 436 | let (socket, addr) = listener.accept().await?; | 510 | let (socket, addr) = listener.accept().await?; |
| 437 | let io = TokioIo::new(socket); | 511 | let io = TokioIo::new(socket); |
| 438 | let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone()); | 512 | let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone(), metrics.clone()); |
| 439 | 513 | ||
| 440 | tokio::spawn(async move { | 514 | tokio::spawn(async move { |
| 441 | if let Err(e) = http1::Builder::new() | 515 | if let Err(e) = http1::Builder::new() |
diff --git a/src/http/nip11.rs b/src/http/nip11.rs index 1ed80de..1723601 100644 --- a/src/http/nip11.rs +++ b/src/http/nip11.rs | |||
| @@ -102,6 +102,9 @@ mod tests { | |||
| 102 | relay_data_path: "./data/relay".to_string(), | 102 | relay_data_path: "./data/relay".to_string(), |
| 103 | bind_address: "127.0.0.1:8080".to_string(), | 103 | bind_address: "127.0.0.1:8080".to_string(), |
| 104 | database_backend: crate::config::DatabaseBackend::Memory, | 104 | database_backend: crate::config::DatabaseBackend::Memory, |
| 105 | metrics_enabled: true, | ||
| 106 | metrics_connection_per_ip_abuse_threshold: 10, | ||
| 107 | metrics_top_n_repos: 10, | ||
| 105 | }; | 108 | }; |
| 106 | 109 | ||
| 107 | let doc = RelayInformationDocument::from_config(&config); | 110 | let doc = RelayInformationDocument::from_config(&config); |
| @@ -133,6 +136,9 @@ mod tests { | |||
| 133 | relay_data_path: "./data/relay".to_string(), | 136 | relay_data_path: "./data/relay".to_string(), |
| 134 | bind_address: "127.0.0.1:8080".to_string(), | 137 | bind_address: "127.0.0.1:8080".to_string(), |
| 135 | database_backend: crate::config::DatabaseBackend::Memory, | 138 | database_backend: crate::config::DatabaseBackend::Memory, |
| 139 | metrics_enabled: true, | ||
| 140 | metrics_connection_per_ip_abuse_threshold: 10, | ||
| 141 | metrics_top_n_repos: 10, | ||
| 136 | }; | 142 | }; |
| 137 | 143 | ||
| 138 | let doc = RelayInformationDocument::from_config(&config); | 144 | let doc = RelayInformationDocument::from_config(&config); |