Skip to content

Commit d185edc

Browse files
kparajuaxsaucedo
authored andcommitted
Add /health/ping and /health/status endpoint to Python REST Microservices (#1026)
* Add /health/ping and /health/status to Python REST microservice * Fix indent bug * Add tests * Black formatting * Add documentation * Add health_status_raw to Low level methods * Add documentation about how to replace the liveness and readiness probes * Add a line about reliability and container stress
1 parent 1d6c461 commit d185edc

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed

doc/source/python/python_component.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,81 @@ class ModelWithMetrics(object):
144144
return {"system":"production"}
145145
```
146146

147+
## REST Health Endpoint
148+
If you wish to add a REST health point, you can implement the `health_status` method with signature as shown below:
149+
```python
150+
def health_status(self) -> Union[np.ndarray, List, str, bytes]:
151+
```
152+
153+
You can use this to verify that your service can respond to HTTP calls after you have built your docker image and also
154+
as kubernetes liveness and readiness probes to verify that your model is healthy.
155+
156+
A simple example is shown below:
157+
158+
```python
159+
class ModelWithHealthEndpoint(object):
160+
def predict(self, X, features_names):
161+
return X
147162

163+
def health_status(self):
164+
response = self.predict([1, 2], ["f1", "f2"])
165+
assert len(response) == 2, "health check returning bad predictions" # or some other simple validation
166+
return response
167+
```
148168

169+
When you use `seldon-core-microservice` to start the HTTP server, you can verify that the model is up and running by
170+
checking the `/health/status` endpoint:
171+
```
172+
$ curl localhost:5000/health/status
173+
{"data":{"names":[],"tensor":{"shape":[2],"values":[1,2]}},"meta":{}}
174+
```
175+
176+
Additionally, you can also use the `/health/ping` endpoint if you want a lightweight call that just checks that
177+
the HTTP server is up:
178+
179+
```0
180+
$ curl localhost:5000/health/ping
181+
pong%
182+
```
183+
184+
You can also override the default liveness and readiness probes and use HTTP health endpoints by adding them in your
185+
`SeldonDeployment` YAML. You can modify the parameters for the probes to suit your reliability needs without putting
186+
too much stress on the container. Read more about these probes in the
187+
[kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).
188+
An example is shown below:
189+
190+
```yaml
191+
apiVersion: machinelearning.seldon.io/v1alpha2
192+
kind: SeldonDeployment
193+
spec:
194+
name: my-app
195+
predictors:
196+
- componentSpecs:
197+
- spec:
198+
containers:
199+
- image: my-app-image:version
200+
name: classifier
201+
livenessProbe:
202+
failureThreshold: 3
203+
initialDelaySeconds: 60
204+
periodSeconds: 5
205+
successThreshold: 1
206+
httpGet:
207+
path: /health/status
208+
port: http
209+
scheme: HTTP
210+
timeoutSeconds: 1
211+
readinessProbe:
212+
failureThreshold: 3
213+
initialDelaySeconds: 20
214+
periodSeconds: 5
215+
successThreshold: 1
216+
httpGet:
217+
path: /health/status
218+
port: http
219+
scheme: HTTP
220+
timeoutSeconds: 1
221+
```
149222
150223
## Low level Methods
151224
If you want more control you can provide a low-level methods that will provide as input the raw proto buffer payloads. The signatures for these are shown below for release `sedon_core>=0.2.6.1`:
@@ -162,6 +235,8 @@ If you want more control you can provide a low-level methods that will provide a
162235
def route_raw(self, msg: prediction_pb2.SeldonMessage) -> prediction_pb2.SeldonMessage:
163236
164237
def aggregate_raw(self, msgs: prediction_pb2.SeldonMessageList) -> prediction_pb2.SeldonMessage:
238+
239+
def health_status_raw(self) -> prediction_pb2.SeldonMessage:
165240
```
166241

167242
## User Defined Exceptions

python/seldon_core/seldon_methods.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
client_transform_output,
1515
client_transform_input,
1616
client_send_feedback,
17+
client_health_status,
1718
SeldonNotImplementedError,
1819
)
1920
from seldon_core.flask_utils import SeldonMicroserviceException
@@ -342,3 +343,26 @@ def aggregate(
342343
return construct_response_json(
343344
user_model, False, request["seldonMessages"][0], client_response
344345
)
346+
347+
348+
def health_status(user_model: Any) -> Union[prediction_pb2.SeldonMessage, List, Dict]:
349+
"""
350+
Call the user model to check the health of the model
351+
352+
Parameters
353+
----------
354+
user_model
355+
User defined class instance
356+
Returns
357+
-------
358+
Health check output
359+
"""
360+
361+
if hasattr(user_model, "health_status_raw"):
362+
try:
363+
return user_model.health_status_raw()
364+
except SeldonNotImplementedError:
365+
pass
366+
367+
client_response = client_health_status(user_model)
368+
return construct_response_json(user_model, False, {}, client_response)

python/seldon_core/user_model.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ def aggregate_raw(
103103
) -> prediction_pb2.SeldonMessage:
104104
raise SeldonNotImplementedError("aggregate_raw is not implemented")
105105

106+
def health_status(self) -> Union[np.ndarray, List, str, bytes]:
107+
raise SeldonNotImplementedError("health is not implemented")
108+
109+
def health_status_raw(self) -> prediction_pb2.SeldonMessage:
110+
raise SeldonNotImplementedError("health_raw is not implemented")
111+
106112

107113
def client_custom_tags(user_model: SeldonComponent) -> Dict:
108114
"""
@@ -417,3 +423,23 @@ def client_aggregate(
417423
return user_model.aggregate(features_list, feature_names_list)
418424
else:
419425
raise SeldonNotImplementedError("Aggregate not defined")
426+
427+
428+
def client_health_status(
429+
user_model: SeldonComponent,
430+
) -> Union[np.ndarray, List, str, bytes]:
431+
"""
432+
Perform a health check
433+
434+
Parameters
435+
----------
436+
user_model
437+
A Seldon user model
438+
Returns
439+
-------
440+
Health check results
441+
"""
442+
if hasattr(user_model, "health_status"):
443+
return user_model.health_status()
444+
else:
445+
raise SeldonNotImplementedError("health_status not defined")

python/seldon_core/wrapper.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ def Aggregate():
9797
logger.debug("REST Response: %s", response)
9898
return jsonify(response)
9999

100+
@app.route("/health/ping", methods=["GET"])
101+
def HealthPing():
102+
"""
103+
Lightweight endpoint to check the liveness of the REST endpoint
104+
"""
105+
return "pong"
106+
107+
@app.route("/health/status", methods=["GET"])
108+
def HealthStatus():
109+
logger.debug("REST Health Status Request")
110+
response = seldon_core.seldon_methods.health_status(user_model)
111+
logger.debug("REST Health Status Response: %s", response)
112+
return jsonify(response)
113+
100114
return app
101115

102116

python/tests/test_model_microservice.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from tensorflow.core.framework.tensor_pb2 import TensorProto
1919
import tensorflow as tf
2020

21+
HEALTH_PING_URL = "/health/ping"
22+
HEALTH_STATUS_URL = "/health/status"
2123

2224
"""
2325
Checksum of bytes. Used to check data integrity of binData passed in multipart/form-data request
@@ -38,6 +40,8 @@ def rs232_checksum(the_bytes):
3840

3941

4042
class UserObject(SeldonComponent):
43+
HEALTH_STATUS_REPONSE = [0.123]
44+
4145
def __init__(self, metrics_ok=True, ret_nparray=False, ret_meta=False):
4246
self.metrics_ok = metrics_ok
4347
self.ret_nparray = ret_nparray
@@ -77,8 +81,13 @@ def metrics(self):
7781
else:
7882
return [{"type": "BAD", "key": "mycounter", "value": 1}]
7983

84+
def health_status(self):
85+
return self.predict(self.HEALTH_STATUS_REPONSE, ["some_float"])
86+
8087

8188
class UserObjectLowLevel(SeldonComponent):
89+
HEALTH_STATUS_RAW_RESPONSE = [123.456, 7.89]
90+
8291
def __init__(self, metrics_ok=True, ret_nparray=False):
8392
self.metrics_ok = metrics_ok
8493
self.ret_nparray = ret_nparray
@@ -101,6 +110,9 @@ def send_feedback_rest(self, request):
101110
def send_feedback_grpc(self, request):
102111
print("Feedback called")
103112

113+
def health_status_raw(self):
114+
return {"data": {"ndarray": self.HEALTH_STATUS_RAW_RESPONSE}}
115+
104116

105117
class UserObjectLowLevelWithStatusInResponse(SeldonComponent):
106118
def __init__(self, metrics_ok=True, ret_nparray=False):
@@ -539,6 +551,36 @@ def test_model_seldon_json_ok():
539551
assert rv.status_code == 200
540552

541553

554+
def test_model_health_ping():
555+
user_object = UserObject()
556+
app = get_rest_microservice(user_object)
557+
client = app.test_client()
558+
rv = client.get(HEALTH_PING_URL)
559+
assert rv.status_code == 200
560+
assert rv.data == b"pong"
561+
562+
563+
def test_model_health_status():
564+
user_object = UserObject()
565+
app = get_rest_microservice(user_object)
566+
client = app.test_client()
567+
rv = client.get(HEALTH_STATUS_URL)
568+
assert rv.status_code == 200
569+
j = json.loads(rv.data)
570+
print(j)
571+
assert j["data"]["tensor"]["values"] == UserObject.HEALTH_STATUS_REPONSE
572+
573+
574+
def test_model_health_status_raw():
575+
user_object = UserObjectLowLevel()
576+
app = get_rest_microservice(user_object)
577+
client = app.test_client()
578+
rv = client.get(HEALTH_STATUS_URL)
579+
assert rv.status_code == 200
580+
j = json.loads(rv.data)
581+
assert j["data"]["ndarray"] == UserObjectLowLevel.HEALTH_STATUS_RAW_RESPONSE
582+
583+
542584
def test_proto_ok():
543585
user_object = UserObject()
544586
app = SeldonModelGRPC(user_object)

0 commit comments

Comments
 (0)