upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/http
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 15:17:04 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 15:24:19 +0000
commitfd0c87c787d0626b3546fa571541c9c809711821 (patch)
tree934f20d973127f380b807d2bd44b25c197cf349c /src/http
parent762cd8e815e797f173f541795de774fbbf978fc3 (diff)
add prometheus metrics
Diffstat (limited to 'src/http')
-rw-r--r--src/http/mod.rs84
-rw-r--r--src/http/nip11.rs6
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;
7use std::future::Future; 7use std::future::Future;
8use std::net::SocketAddr; 8use std::net::SocketAddr;
9use std::pin::Pin; 9use std::pin::Pin;
10use std::sync::Arc;
10 11
11use base64::Engine; 12use base64::Engine;
12use http_body_util::{BodyExt, Full}; 13use http_body_util::{BodyExt, Full};
@@ -24,6 +25,7 @@ use tokio::net::TcpListener;
24 25
25use crate::config::Config; 26use crate::config::Config;
26use crate::git; 27use crate::git;
28use crate::metrics::Metrics;
27use crate::nostr::builder::SharedDatabase; 29use 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
95impl HttpService { 99impl 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
422pub async fn run_server( 495pub 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);