Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions crates/walgit-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;

use axum::Router;
use axum::body::{Body, to_bytes};
use axum::extract::ConnectInfo;
use axum::http::{Method, Request};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::{Extension, Router};
use bytes::Bytes;
use metrics_exporter_prometheus::PrometheusHandle;
use walgit_store::DynStore;
Expand All @@ -89,7 +89,8 @@ pub struct AppState {
/// HTTP requests in flight (counted until the response body is done); on the watchdog line.
pub inflight: Arc<middleware::Inflight>,
pub caches: cache::ServerCaches,
pub metrics_handle: Arc<PrometheusHandle>,
/// Prometheus recorder handle. Absent when `telemetry.metrics` is disabled.
pub metrics_handle: Option<Arc<PrometheusHandle>>,
/// Read-through LFS upstream client (`upstream.lfs`).
pub lfs_upstream: lfs_upstream::Upstream,
/// Startup prewarm state (gates /readyz when configured).
Expand All @@ -112,7 +113,11 @@ impl AppState {
) -> anyhow::Result<Arc<Self>> {
let registry = walgit_wal::Registry::new(store.clone(), cfg.clone());
let bridge = bridge::Bridge::new(&cfg, registry.clone());
let metrics_handle = metrics::install()?;
let metrics_handle = if cfg.telemetry.metrics {
Some(metrics::install()?)
} else {
None
};
let tls = tls::load(&cfg)?;
if let Some(t) = &tls {
tracing::info!(fingerprint = %t.fingerprint, mode = ?cfg.server.tls.mode, "TLS terminated in-process");
Expand Down Expand Up @@ -148,7 +153,7 @@ pub fn router(state: Arc<AppState>) -> Router {
.gzip(true)
.quality(tower_http::CompressionLevel::Fastest);
// Nothing with content is public: the SPA shell/assets, installer, credential
// helper and metrics all sit behind `web::require_auth` (identity from a
// helper and enabled metrics all sit behind `web::require_auth` (identity from a
// bearer token or a session cookie). The JSON API and git
// endpoints do their own `require_read`/`require_write`; `/_auth/*` is the
// login flow itself; `/services/public/*` is the installer a not-yet-signed-in
Expand All @@ -162,11 +167,26 @@ pub fn router(state: Arc<AppState>) -> Router {
.with_state(())
.layer(web_compression.clone()),
)
.route("/metrics", get(metrics::metrics_route))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
web::require_auth,
));
let metrics = if let Some(metrics_handle) = &state.metrics_handle {
Router::new()
.route("/metrics", get(metrics::metrics_route))
.layer(Extension(metrics_handle.clone()))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
web::require_auth,
))
} else {
// Reserve the top-level name so the SPA's `/{owner}` route cannot turn a
// disabled scrape endpoint into a successful HTML response.
Router::new().route(
"/metrics",
get(|| async { axum::http::StatusCode::NOT_FOUND }),
)
};

Router::new()
.merge(
Expand All @@ -180,6 +200,7 @@ pub fn router(state: Arc<AppState>) -> Router {
.layer(web_compression),
)
.merge(gated)
.merge(metrics)
.route("/healthz", get(health::healthz))
.route("/readyz", get(health::readyz))
// The SDK is a static artefact with no data in it; it must load from a
Expand Down
16 changes: 8 additions & 8 deletions crates/walgit-server/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
//! Prometheus metrics exporter. Installs a recorder up front (once per process)
//! and exposes the rendered scrape via `/metrics`.
//! Prometheus metrics exporter. When enabled, installs a recorder once per
//! process and exposes the rendered scrape via `/metrics`.

use std::sync::{Arc, OnceLock};

use axum::extract::State;
use axum::Extension;
use axum::response::IntoResponse;
use metrics_exporter_prometheus::PrometheusHandle;

use crate::AppState;

static HANDLE: OnceLock<Arc<PrometheusHandle>> = OnceLock::new();

/// Install the Prometheus recorder once per process and return a shared handle.
Expand All @@ -32,13 +30,15 @@ pub fn install() -> anyhow::Result<Arc<PrometheusHandle>> {
}

/// `GET /metrics`
pub async fn metrics_route(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let body = st.metrics_handle.render();
pub async fn metrics_route(
Extension(handle): Extension<Arc<PrometheusHandle>>,
) -> impl IntoResponse {
(
[(
axum::http::header::CONTENT_TYPE,
"text/plain; version=0.0.4",
)],
body,
handle.render(),
)
.into_response()
}
22 changes: 22 additions & 0 deletions crates/walgit-server/tests/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
mod harness;

use axum::http::StatusCode;

use harness::Server;

type TestResult = anyhow::Result<()>;

#[tokio::test]
async fn metrics_config_controls_scrape_endpoint() -> TestResult {
let disabled = Server::start_with_tweak(|cfg| cfg.telemetry.metrics = false).await?;
assert!(disabled.state.metrics_handle.is_none());
let response = reqwest::get(format!("{}/metrics", disabled.base_url)).await?;
assert_eq!(response.status(), StatusCode::NOT_FOUND);

let enabled = Server::start_with_tweak(|cfg| cfg.telemetry.metrics = true).await?;
assert!(enabled.state.metrics_handle.is_some());
let response = reqwest::get(format!("{}/metrics", enabled.base_url)).await?;
assert_eq!(response.status(), StatusCode::OK);

Ok(())
}
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ dev-store-stop:
test:
{{t5}} cargo test --workspace --lib --bins
{{t10}} cargo test -p walgit-store -p walgit-git -p walgit-wal --tests
{{t10}} cargo test -p walgit-server --test web_api --test web_ui --test api_v1 --test static_http --test packfile_uri --test forward --test maintain --test routing_prefix --test lfs_upstream --test drain --test events --test follow --test policy
{{t10}} cargo test -p walgit-server --test web_api --test web_ui --test api_v1 --test static_http --test packfile_uri --test forward --test maintain --test routing_prefix --test metrics --test lfs_upstream --test drain --test events --test follow --test policy

# Smart-HTTP end-to-end against real git (≈ 20 s) — run when touching smart.rs/receive/upload-pack/wal.
e2e *ARGS:
Expand Down