Skip to content
Merged
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
39 changes: 0 additions & 39 deletions src/dstack/_internal/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
from dstack._internal.server.services.jobs.server_connection import job_server_connections_pool
from dstack._internal.server.services.locking import advisory_lock_ctx
from dstack._internal.server.services.projects import get_or_create_default_project
from dstack._internal.server.services.prometheus.client_metrics import http_metrics
from dstack._internal.server.services.proxy.deps import ServerProxyDependencyInjector
from dstack._internal.server.services.proxy.routers import service_proxy
from dstack._internal.server.services.runner.pool import instance_connection_pool
Expand Down Expand Up @@ -299,8 +298,6 @@ async def log_request(request: Request, call_next):
start_time = time.time()
response: Response = await call_next(request)
process_time = time.time() - start_time
# log process_time to be used in the log_http_metrics middleware
request.state.process_time = process_time
logger.debug(
"Processed request %s %s in %s. Status: %s",
request.method,
Expand All @@ -327,42 +324,6 @@ async def profile_request(request: Request, call_next):
else:
return await call_next(request)

# this middleware must be defined after the log_request middleware
@app.middleware("http")
async def log_http_metrics(request: Request, call_next):
def _extract_project_name(request: Request):
project_name = None
prefix = "/api/project/"
if request.url.path.startswith(prefix):
rest = request.url.path[len(prefix) :]
project_name = rest.split("/", 1)[0] if rest else None

return project_name

def _extract_endpoint_label(request: Request, response: Response) -> str:
route = request.scope.get("route")
route_path = getattr(route, "path", None)
if route_path:
return route_path
if not request.url.path.startswith("/api/"):
return "__non_api__"
if response.status_code == status.HTTP_404_NOT_FOUND:
return "__not_found__"
return "__unmatched__"

project_name = _extract_project_name(request)
response: Response = await call_next(request)
endpoint_label = _extract_endpoint_label(request, response)

http_metrics.log_request(
method=request.method,
endpoint=endpoint_label,
http_status=response.status_code,
project_name=project_name,
duration_seconds=request.state.process_time,
)
return response

@app.get("/healthcheck")
async def healthcheck():
return CustomJSONResponse(content={"status": "running"})
Expand Down
43 changes: 0 additions & 43 deletions src/dstack/_internal/server/services/prometheus/client_metrics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Optional

from prometheus_client import Counter, Histogram


Expand Down Expand Up @@ -55,44 +53,3 @@ def increment_pending_runs(self, project_name: str, run_type: str):


run_metrics = RunMetrics()


class HTTPMetrics:
"""Wrapper class for server HTTP Prometheus metrics.

Deprecated in favor of the OpenTelemetry HTTP metrics
(`DSTACK_OTEL_METRICS_ENABLED`), which are correct in multi-replica
deployments. Kept for backward compatibility.
"""

def __init__(self):
self._requests_total = Counter(
"dstack_server_requests_total",
"Total number of HTTP requests",
labelnames=["method", "endpoint", "http_status", "project_name"],
)
self._request_duration = Histogram(
"dstack_server_request_duration_seconds",
"HTTP request duration in seconds",
labelnames=["method", "endpoint", "http_status", "project_name"],
)

def log_request(
self,
method: str,
endpoint: str,
http_status: int,
project_name: Optional[str],
duration_seconds: float,
):
labels = {
"method": method,
"endpoint": endpoint,
"http_status": http_status,
"project_name": project_name,
}
self._request_duration.labels(**labels).observe(duration_seconds)
self._requests_total.labels(**labels).inc()


http_metrics = HTTPMetrics()
38 changes: 5 additions & 33 deletions src/tests/_internal/server/routers/test_prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
get_run_spec,
)

BASE_HTTP_METRICS = b"""
BASE_CLIENT_METRICS = b"""
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 13159.0
Expand All @@ -55,34 +55,6 @@
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="12",patchlevel="2",version="3.12.2"} 1.0
# HELP dstack_server_requests_total Total number of HTTP requests
# TYPE dstack_server_requests_total counter
dstack_server_requests_total{endpoint="/metrics",http_status="200",method="GET",project_name="None"} 1.0
# HELP dstack_server_requests_created Total number of HTTP requests
# TYPE dstack_server_requests_created gauge
dstack_server_requests_created{endpoint="/metrics",http_status="200",method="GET",project_name="None"} 1.67262864e+09
# HELP dstack_server_request_duration_seconds HTTP request duration in seconds
# TYPE dstack_server_request_duration_seconds histogram
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.005",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.01",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.025",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.05",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.075",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.1",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.25",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.5",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="0.75",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="1.0",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="2.5",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="5.0",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="7.5",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="10.0",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_bucket{endpoint="/metrics",http_status="200",le="+Inf",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_count{endpoint="/metrics",http_status="200",method="GET",project_name="None"} 1.0
dstack_server_request_duration_seconds_sum{endpoint="/metrics",http_status="200",method="GET",project_name="None"} 0.0
# HELP dstack_server_request_duration_seconds_created HTTP request duration in seconds
# TYPE dstack_server_request_duration_seconds_created gauge
dstack_server_request_duration_seconds_created{endpoint="/metrics",http_status="200",method="GET",project_name="None"} 1.67262864e+09
"""


Expand All @@ -100,7 +72,7 @@ def enable_metrics(monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
@pytest.mark.usefixtures("image_config_mock", "test_db", "enable_metrics")
class TestGetPrometheusMetrics:
@patch("prometheus_client.generate_latest", lambda: BASE_HTTP_METRICS)
@patch("prometheus_client.generate_latest", lambda: BASE_CLIENT_METRICS)
async def test_returns_metrics(self, session: AsyncSession, client: AsyncClient):
user = await create_user(session=session, name="test-user", global_role=GlobalRole.USER)
offer = get_instance_offer_with_availability(
Expand Down Expand Up @@ -348,15 +320,15 @@ async def test_returns_metrics(self, session: AsyncSession, client: AsyncClient)
FIELD_2{{gpu="1",dstack_project_name="project-1",dstack_user_name="test-user",dstack_run_name="run-1",dstack_run_id="{job_1_1.run_id}",dstack_job_name="run-1-0-0",dstack_job_id="{job_1_1.id}",dstack_job_num="0",dstack_replica_num="0",dstack_run_type="dev-environment",dstack_backend="aws",dstack_gpu="V4"}} 987169.0 1395066363010
""")
+ "\n"
+ BASE_HTTP_METRICS.decode().strip()
+ BASE_CLIENT_METRICS.decode().strip()
)
assert response.text.strip() == expected

@patch("prometheus_client.generate_latest", lambda: BASE_HTTP_METRICS)
@patch("prometheus_client.generate_latest", lambda: BASE_CLIENT_METRICS)
async def test_returns_empty_response_if_no_runs(self, client: AsyncClient):
response = await client.get("/metrics")
assert response.status_code == 200
assert response.text.strip() == BASE_HTTP_METRICS.decode().strip()
assert response.text.strip() == BASE_CLIENT_METRICS.decode().strip()

async def test_returns_404_if_not_enabled(
self, monkeypatch: pytest.MonkeyPatch, client: AsyncClient
Expand Down
Loading