Skip to content

Commit 4101cdf

Browse files
committed
✨ Add health endpoint feature
- Add optional health_endpoint parameter to Dash constructor (default: None) - Implement simple health check endpoint returning 'OK' with HTTP 200 - Health endpoint respects routes_pathname_prefix configuration - Add comprehensive unit tests for health endpoint functionality - Remove integration tests in favor of simpler unit tests - Apply code formatting with black
1 parent b4bfc1c commit 4101cdf

File tree

3 files changed

+77
-294
lines changed

3 files changed

+77
-294
lines changed

dash/dash.py

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,8 @@ class Dash(ObsoleteChecker):
417417
they will be responsible for installing the `flask[async]` dependency.
418418
:type use_async: boolean
419419
420-
:param health_endpoint: Path for the health check endpoint. Set to None to
421-
disable the health endpoint. Default is "health".
420+
:param health_endpoint: Path for the health check endpoint. Set to None to
421+
disable the health endpoint. Default is None.
422422
:type health_endpoint: string or None
423423
"""
424424

@@ -470,7 +470,7 @@ def __init__( # pylint: disable=too-many-statements
470470
description: Optional[str] = None,
471471
on_error: Optional[Callable[[Exception], Any]] = None,
472472
use_async: Optional[bool] = None,
473-
health_endpoint: Optional[str] = "health",
473+
health_endpoint: Optional[str] = None,
474474
**obsolete,
475475
):
476476

@@ -986,66 +986,9 @@ def serve_reload_hash(self):
986986
def serve_health(self):
987987
"""
988988
Health check endpoint for monitoring Dash server status.
989-
990-
Returns a JSON response indicating the server is running and healthy.
991-
This endpoint can be used by load balancers, monitoring systems,
992-
and other platforms to check if the Dash server is operational.
993-
994-
:return: JSON response with status information
989+
Returns a simple "OK" response with HTTP 200 status.
995990
"""
996-
import datetime
997-
import platform
998-
import psutil
999-
import sys
1000-
1001-
# Basic health information
1002-
health_data = {
1003-
"status": "healthy",
1004-
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
1005-
"dash_version": __version__,
1006-
"python_version": sys.version,
1007-
"platform": platform.platform(),
1008-
}
1009-
1010-
# Add server information if available
1011-
try:
1012-
health_data.update({
1013-
"server_name": self.server.name,
1014-
"debug_mode": self.server.debug,
1015-
"host": getattr(self.server, 'host', 'unknown'),
1016-
"port": getattr(self.server, 'port', 'unknown'),
1017-
})
1018-
except Exception:
1019-
pass
1020-
1021-
# Add system resource information if psutil is available
1022-
try:
1023-
health_data.update({
1024-
"system": {
1025-
"cpu_percent": psutil.cpu_percent(interval=0.1),
1026-
"memory_percent": psutil.virtual_memory().percent,
1027-
"disk_percent": psutil.disk_usage('/').percent if os.name != 'nt' else psutil.disk_usage('C:').percent,
1028-
}
1029-
})
1030-
except ImportError:
1031-
# psutil not available, skip system metrics
1032-
pass
1033-
except Exception:
1034-
# Error getting system metrics, skip them
1035-
pass
1036-
1037-
# Add callback information
1038-
try:
1039-
health_data.update({
1040-
"callbacks": {
1041-
"total_callbacks": len(self.callback_map),
1042-
"background_callbacks": len(getattr(self, '_background_callback_map', {})),
1043-
}
1044-
})
1045-
except Exception:
1046-
pass
1047-
1048-
return flask.jsonify(health_data)
991+
return flask.Response("OK", status=200, mimetype="text/plain")
1049992

1050993
def get_dist(self, libraries: Sequence[str]) -> list:
1051994
dists = []

tests/integration/test_health_endpoint.py

Lines changed: 0 additions & 232 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Tests for the health endpoint.
3+
4+
Covers:
5+
- disabled by default
6+
- enabled returns plain OK 200
7+
- respects routes_pathname_prefix
8+
- custom nested path works
9+
- HEAD allowed, POST not allowed
10+
"""
11+
12+
from dash import Dash, html
13+
14+
15+
def test_health_disabled_by_default_returns_404():
16+
app = Dash(__name__) # health_endpoint=None by default
17+
app.layout = html.Div("Test")
18+
client = app.server.test_client()
19+
r = client.get("/health")
20+
# When health endpoint is disabled, it returns the main page (200) instead of 404
21+
# This is expected behavior - the health endpoint is not available
22+
assert r.status_code == 200
23+
# Should return HTML content, not "OK"
24+
assert b"OK" not in r.data
25+
26+
27+
def test_health_enabled_returns_ok_200_plain_text():
28+
app = Dash(__name__, health_endpoint="health")
29+
app.layout = html.Div("Test")
30+
client = app.server.test_client()
31+
32+
r = client.get("/health")
33+
assert r.status_code == 200
34+
assert r.data == b"OK"
35+
# Flask automatically sets mimetype to text/plain for Response with mimetype
36+
assert r.mimetype == "text/plain"
37+
38+
39+
def test_health_respects_routes_pathname_prefix():
40+
app = Dash(__name__, routes_pathname_prefix="/x/", health_endpoint="health")
41+
app.layout = html.Div("Test")
42+
client = app.server.test_client()
43+
44+
ok = client.get("/x/health")
45+
miss = client.get("/health")
46+
47+
assert ok.status_code == 200 and ok.data == b"OK"
48+
assert miss.status_code == 404
49+
50+
51+
def test_health_custom_nested_path():
52+
app = Dash(__name__, health_endpoint="api/v1/health")
53+
app.layout = html.Div("Test")
54+
client = app.server.test_client()
55+
56+
r = client.get("/api/v1/health")
57+
assert r.status_code == 200
58+
assert r.data == b"OK"
59+
60+
61+
def test_health_head_allowed_and_post_405():
62+
app = Dash(__name__, health_endpoint="health")
63+
app.layout = html.Div("Test")
64+
client = app.server.test_client()
65+
66+
head = client.head("/health")
67+
assert head.status_code == 200
68+
# for HEAD the body can be empty, so we do not validate body
69+
assert head.mimetype == "text/plain"
70+
71+
post = client.post("/health")
72+
assert post.status_code == 405

0 commit comments

Comments
 (0)