292 lines
9.3 KiB
Rust
292 lines
9.3 KiB
Rust
use std::{sync::Arc, time::Instant};
|
|
|
|
use actix_web::{App, HttpResponse, HttpServer, dev::Service, web};
|
|
use cache::AppCache;
|
|
use config::AppConfig;
|
|
use db::database::AppDatabase;
|
|
use sqlx;
|
|
use tracing_actix_web::TracingLogger;
|
|
use track::{CounterVec, HistogramVec};
|
|
|
|
pub mod action;
|
|
pub mod auth;
|
|
pub mod handler;
|
|
pub mod lfs;
|
|
pub mod lfs_routes;
|
|
pub mod rate_limit;
|
|
pub mod routes;
|
|
pub mod utils;
|
|
|
|
const REQUEST_LOG_EXCLUDED_PATHS: &[&str] = &[
|
|
"/health",
|
|
"/live",
|
|
"/ready",
|
|
"/metrics",
|
|
"/favicon.ico",
|
|
"/robots.txt",
|
|
];
|
|
|
|
fn should_log_request(path: &str) -> bool {
|
|
!REQUEST_LOG_EXCLUDED_PATHS.contains(&path)
|
|
}
|
|
|
|
fn record_http_request(
|
|
registry: &track::MetricsRegistry,
|
|
method: &str,
|
|
status: u16,
|
|
elapsed: std::time::Duration,
|
|
) {
|
|
http_requests_total(registry)
|
|
.with_label_values(&[method, &status.to_string()])
|
|
.inc();
|
|
http_request_duration(registry)
|
|
.with_label_values(&[method, &status.to_string()])
|
|
.observe(elapsed.as_secs_f64());
|
|
}
|
|
|
|
fn http_requests_total(registry: &track::MetricsRegistry) -> CounterVec {
|
|
registry
|
|
.register_counter_vec(
|
|
"http_requests_total",
|
|
"Total HTTP requests",
|
|
&["method", "status"],
|
|
)
|
|
.expect("failed to register http_requests_total")
|
|
}
|
|
|
|
fn http_request_duration(registry: &track::MetricsRegistry) -> HistogramVec {
|
|
registry
|
|
.register_histogram_vec(
|
|
"http_request_duration_seconds",
|
|
"HTTP request duration in seconds",
|
|
&["method", "status"],
|
|
vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
|
|
)
|
|
.expect("failed to register http_request_duration_seconds")
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct HttpAppState {
|
|
pub db: AppDatabase,
|
|
pub cache: AppCache,
|
|
pub sync: crate::sync::ReceiveSyncService,
|
|
pub rate_limiter: Arc<rate_limit::RateLimiter>,
|
|
pub config: AppConfig,
|
|
pub git_state: crate::AppGitState,
|
|
pub metrics: Option<track::MetricsRegistry>,
|
|
}
|
|
|
|
async fn robots(state: web::Data<HttpAppState>) -> HttpResponse {
|
|
let sitemap_url = state
|
|
.config
|
|
.git_http_domain()
|
|
.map(|d| format!("{}/sitemap.xml", d.trim_end_matches('/')))
|
|
.unwrap_or_default();
|
|
|
|
let body = if sitemap_url.is_empty() {
|
|
"User-agent: *\nDisallow: /\n".to_string()
|
|
} else {
|
|
format!("User-agent: *\nDisallow: /\n\nSitemap: {sitemap_url}\n")
|
|
};
|
|
|
|
HttpResponse::Ok()
|
|
.content_type("text/plain; charset=utf-8")
|
|
.body(body)
|
|
}
|
|
|
|
async fn health(state: web::Data<HttpAppState>) -> HttpResponse {
|
|
let db_ok = sqlx::query("SELECT 1")
|
|
.execute(state.db.reader())
|
|
.await
|
|
.is_ok();
|
|
let cache_ok = state.cache.ping_cluster().await.is_ok();
|
|
|
|
if db_ok && cache_ok {
|
|
HttpResponse::Ok().json(serde_json::json!({
|
|
"status": "ok",
|
|
"db": "ok",
|
|
"cache": "ok",
|
|
}))
|
|
} else {
|
|
HttpResponse::ServiceUnavailable().json(serde_json::json!({
|
|
"status": "unhealthy",
|
|
"db": if db_ok { "ok" } else { "error" },
|
|
"cache": if cache_ok { "ok" } else { "error" },
|
|
}))
|
|
}
|
|
}
|
|
|
|
async fn metrics(state: web::Data<HttpAppState>) -> HttpResponse {
|
|
let Some(metrics) = &state.metrics else {
|
|
return HttpResponse::NotFound()
|
|
.content_type("text/plain")
|
|
.body("metrics not configured");
|
|
};
|
|
|
|
match metrics.encode() {
|
|
Ok(body) => HttpResponse::Ok()
|
|
.content_type("text/plain; version=0.0.4")
|
|
.body(body),
|
|
Err(e) => HttpResponse::InternalServerError()
|
|
.content_type("text/plain")
|
|
.body(format!("metrics encoding error: {e}")),
|
|
}
|
|
}
|
|
|
|
pub fn git_http_cfg(cfg: &mut web::ServiceConfig) {
|
|
cfg.route("/robots.txt", web::get().to(robots))
|
|
.route("/health", web::get().to(health))
|
|
.route("/metrics", web::get().to(metrics))
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/action",
|
|
web::get().to(action::action_poll),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/refs",
|
|
web::get().to(routes::info_refs),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/git-upload-pack",
|
|
web::post().to(routes::upload_pack),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/git-receive-pack",
|
|
web::post().to(routes::receive_pack),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/objects/batch",
|
|
web::post().to(lfs_routes::lfs_batch),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
|
|
web::put().to(lfs_routes::lfs_upload),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
|
|
web::get().to(lfs_routes::lfs_download),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/locks",
|
|
web::post().to(lfs_routes::lfs_lock_create),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/locks",
|
|
web::get().to(lfs_routes::lfs_lock_list),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
|
|
web::get().to(lfs_routes::lfs_lock_get),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
|
|
web::delete().to(lfs_routes::lfs_lock_delete),
|
|
)
|
|
.route(
|
|
"/{namespace}/{repo_name}.git/graphql",
|
|
web::post().to(crate::graphql::graphql_handle),
|
|
);
|
|
}
|
|
|
|
#[tracing::instrument(skip(config, db, cache, redis_pool, metrics_registry))]
|
|
pub async fn run_http(
|
|
config: AppConfig,
|
|
db: AppDatabase,
|
|
cache: AppCache,
|
|
redis_pool: deadpool_redis::cluster::Pool,
|
|
metrics_registry: Option<track::MetricsRegistry>,
|
|
) -> anyhow::Result<()> {
|
|
let sync = crate::sync::ReceiveSyncService::new(redis_pool);
|
|
|
|
let rate_limiter = Arc::new(rate_limit::RateLimiter::new(
|
|
rate_limit::RateLimitConfig::default(),
|
|
));
|
|
let _cleanup = rate_limiter.clone().start_cleanup();
|
|
|
|
let git_state = crate::AppGitState {
|
|
cache: cache.clone(),
|
|
db: db.clone(),
|
|
};
|
|
|
|
let state = HttpAppState {
|
|
db: db.clone(),
|
|
cache: cache.clone(),
|
|
sync,
|
|
rate_limiter,
|
|
config: config.clone(),
|
|
git_state,
|
|
metrics: metrics_registry,
|
|
};
|
|
|
|
let http_port = config.git_http_port()?;
|
|
tracing::info!("Starting git HTTP server on 0.0.0.0:{}", http_port);
|
|
|
|
let server = HttpServer::new(move || {
|
|
let request_metrics = state.metrics.clone();
|
|
|
|
App::new()
|
|
.app_data(web::Data::new(state.clone()))
|
|
.wrap(TracingLogger::default())
|
|
.wrap_fn(move |req, srv| {
|
|
let metrics = request_metrics.clone();
|
|
let should_log = should_log_request(req.path());
|
|
let method = req.method().clone();
|
|
let path = req.path().to_owned();
|
|
let peer_addr =
|
|
req.connection_info().peer_addr().map(str::to_owned);
|
|
let started_at = Instant::now();
|
|
let fut = srv.call(req);
|
|
|
|
async move {
|
|
match fut.await {
|
|
Ok(res) => {
|
|
let elapsed = started_at.elapsed();
|
|
let status = res.status().as_u16();
|
|
if let Some(metrics) = &metrics {
|
|
record_http_request(metrics, method.as_str(), status, elapsed);
|
|
}
|
|
if should_log {
|
|
tracing::info!(
|
|
method = %method,
|
|
path = %path,
|
|
status = status,
|
|
elapsed_ms = elapsed.as_millis(),
|
|
peer_addr = peer_addr.as_deref().unwrap_or("-"),
|
|
"http request"
|
|
);
|
|
}
|
|
Ok(res)
|
|
}
|
|
Err(err) => {
|
|
let elapsed = started_at.elapsed();
|
|
if let Some(metrics) = &metrics {
|
|
record_http_request(metrics, method.as_str(), 500, elapsed);
|
|
}
|
|
if should_log {
|
|
tracing::warn!(
|
|
method = %method,
|
|
path = %path,
|
|
elapsed_ms = elapsed.as_millis(),
|
|
peer_addr = peer_addr.as_deref().unwrap_or("-"),
|
|
error = %err,
|
|
"http request failed"
|
|
);
|
|
}
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.configure(git_http_cfg)
|
|
})
|
|
.bind(format!("0.0.0.0:{}", http_port))?
|
|
.run();
|
|
|
|
let result = server.await;
|
|
if let Err(e) = result {
|
|
tracing::error!("HTTP server error: {}", e);
|
|
}
|
|
|
|
tracing::info!("Git HTTP server stopped");
|
|
Ok(())
|
|
}
|