Skip to content

Commit 89cd04e

Browse files
1vndeliahu
authored andcommitted
Add debug mode for prediction requests (#369)
1 parent 1a7e844 commit 89cd04e

File tree

10 files changed

+150
-191
lines changed

10 files changed

+150
-191
lines changed

cli/cmd/get.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ func describeAPI(name string, resourcesRes *schema.GetResourcesResponse, flagVer
288288
apiEndpoint := urls.Join(resourcesRes.APIsBaseURL, anyAPIStatus.Path)
289289

290290
out := "\n" + console.Bold("url: ") + apiEndpoint + "\n"
291+
out += "\n" + console.Bold("debug url: ") + apiEndpoint + "?debug=true" + "\n\n"
291292
out += fmt.Sprintf("%s curl -X POST -H \"Content-Type: application/json\" %s -d @samples.json\n", console.Bold("curl:"), apiEndpoint)
292293
out += fmt.Sprintf(console.Bold("updated at:")+" %s\n\n", libtime.LocalTimestamp(updatedAt))
293294

cli/cmd/predict.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ import (
3131
"github.com/cortexlabs/cortex/pkg/operator/api/resource"
3232
)
3333

34+
var predictDebug bool
35+
3436
func init() {
3537
addAppNameFlag(predictCmd)
3638
addEnvFlag(predictCmd)
39+
predictCmd.Flags().BoolVar(&predictDebug, "debug", false, "Predict with debug mode")
3740
}
3841

3942
type PredictResponse struct {
@@ -80,6 +83,9 @@ var predictCmd = &cobra.Command{
8083

8184
apiPath := apiGroupStatus.ActiveStatus.Path
8285
apiURL := urls.Join(resourcesRes.APIsBaseURL, apiPath)
86+
if predictDebug {
87+
apiURL += "?debug=true"
88+
}
8389
predictResponse, err := makePredictRequest(apiURL, samplesJSONPath)
8490
if err != nil {
8591
if strings.Contains(err.Error(), "503 Service Temporarily Unavailable") || strings.Contains(err.Error(), "502 Bad Gateway") {

docs/deployments/apis.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,12 @@ See [packaging models](packaging-models.md) for how to export the model.
4343
Request handlers are used to decouple the interface of an API endpoint from its model. A `pre_inference` request handler can be used to modify request payloads before they are sent to the model. A `post_inference` request handler can be used to modify model predictions in the server before they are sent to the client.
4444

4545
See [request handlers](request-handlers.md) for a detailed guide.
46+
47+
## Debugging
48+
49+
You can log more information about each request by adding a `?debug=true` parameter to your requests. This will print:
50+
51+
1. The raw sample
52+
2. The value after running the `pre_inference` function (if applicable)
53+
3. The value after running inference
54+
4. The value after running the `post_inference` function (if applicable)

pkg/workloads/cortex/lib/log.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import logging
16+
from cortex.lib import stringify
1617

1718
logger = logging.getLogger("cortex")
1819
handler = logging.StreamHandler()
@@ -21,5 +22,12 @@
2122
logger.setLevel(logging.DEBUG)
2223

2324

25+
def debug_obj(name, sample, debug):
26+
if not debug:
27+
return
28+
29+
logger.info("{}: {}".format(name, stringify.truncate(sample)))
30+
31+
2432
def get_logger():
2533
return logging.getLogger("cortex")

pkg/workloads/cortex/lib/package.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ def build(args):
132132
ctx.upload_resource_status_start(*python_packages_list)
133133
try:
134134
build_packages(python_packages, ctx.storage)
135-
util.log_job_finished(ctx.workload_id)
135+
timestamp = util.now_timestamp_rfc_3339()
136+
logger.info("workload: {}, completed: {}".format(ctx.workload_id, timestamp))
136137
except CortexException as e:
137138
e.wrap("error")
138139
logger.exception(e)

pkg/workloads/cortex/lib/stringify.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2019 Cortex Labs, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections.abc import Iterable
16+
17+
18+
def truncate(item, max_elements=10, max_str_len=500):
19+
if isinstance(item, str):
20+
s = item
21+
if max_str_len > 3 and len(s) > max_str_len:
22+
s = s[: max_str_len - 3] + "..."
23+
return '"{}"'.format(s)
24+
25+
if isinstance(item, dict):
26+
count = 0
27+
item_strs = []
28+
for key in item:
29+
if max_elements > 0 and count >= max_elements:
30+
item_strs.append("...")
31+
break
32+
33+
key_str = truncate(key, max_elements, max_str_len)
34+
val_str = truncate(item[key], max_elements, max_str_len)
35+
item_strs.append("{}: {}".format(key_str, val_str))
36+
count += 1
37+
38+
return "{" + ", ".join(item_strs) + "}"
39+
40+
if isinstance(item, Iterable) and hasattr(item, "__getitem__"):
41+
item_strs = []
42+
43+
for element in item[:max_elements]:
44+
item_strs.append(truncate(element, max_elements, max_str_len))
45+
46+
if max_elements > 0 and len(item) > max_elements:
47+
item_strs.append("...")
48+
49+
return "[" + ", ".join(item_strs) + "]"
50+
51+
# Fallback
52+
s = str(item)
53+
if max_str_len > 3 and len(s) > max_str_len:
54+
s = s[: max_str_len - 3] + "..."
55+
return s

pkg/workloads/cortex/lib/test/util_test.py

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -64,46 +64,3 @@ def test_is_number_col():
6464
assert util.is_number_col([None, 1, None])
6565
assert util.is_number_col([None, 1.1, None])
6666
assert not util.is_number_col([None, None, None])
67-
68-
69-
def test_print_samples_horiz(caplog):
70-
caplog.set_level(logging.INFO)
71-
72-
samples = [
73-
{
74-
"test1": float(120),
75-
"test2": 0.2,
76-
"testlongstring": 0.20,
77-
"a": 0.23,
78-
"b": 0.233333333333333333,
79-
},
80-
{
81-
"test1": 11,
82-
"test2": 18,
83-
"testreallyreallyreallyreallyreallylongstring": 18,
84-
"a": -12,
85-
"b": 10,
86-
},
87-
{
88-
"test1": 13,
89-
"test2": 13,
90-
"testlongstring": 13,
91-
"testreallyreallyreallyreallyreallylongstring": 13,
92-
"a": 133333,
93-
"b": None,
94-
},
95-
]
96-
util.print_samples_horiz(samples)
97-
98-
records = [r.message for r in caplog.records]
99-
100-
expected = (
101-
"a: 0.23, -12, 133333\n"
102-
"b: 0.23, 10,\n"
103-
"test1: 120.00, 11, 13\n"
104-
"test2: 0.20, 18, 13\n"
105-
"testlongstring: 0.20, , 13\n"
106-
"testreallyreallyr...: , 18, 13\n"
107-
)
108-
109-
assert "\n".join(records) + "\n" == expected

pkg/workloads/cortex/lib/util.py

Lines changed: 1 addition & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import errno
1717
import shutil
1818
import stat
19-
import pprint
2019
import pickle
2120
import json
2221
import collections
@@ -28,7 +27,7 @@
2827
from datetime import datetime
2928

3029
from cortex.lib.log import get_logger
31-
30+
from cortex.lib import stringify
3231
import json_tricks
3332

3433

@@ -39,64 +38,12 @@ def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
3938
return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
4039

4140

42-
def indent_str(text, indent):
43-
if not is_str(text):
44-
text = repr(text)
45-
return indent * " " + text.replace("\n", "\n" + indent * " ")
46-
47-
48-
def json_tricks_dump(obj, **kwargs):
49-
return json_tricks.dumps(obj, primitives=True, **kwargs)
50-
51-
5241
def json_tricks_encoder(*args, **kwargs):
5342
kwargs["primitives"] = True
5443
kwargs["obj_encoders"] = json_tricks.nonp.DEFAULT_ENCODERS
5544
return json_tricks.TricksEncoder(*args, **kwargs)
5645

5746

58-
def pp_str(obj, indent=0):
59-
try:
60-
out = json_tricks_dump(obj, sort_keys=True, indent=2)
61-
except:
62-
out = pprint.pformat(obj, width=120)
63-
return indent_str(out, indent)
64-
65-
66-
def pp(obj, indent=0):
67-
print(pp_str(obj, indent))
68-
69-
70-
def pp_str_flat(obj, indent=0):
71-
try:
72-
out = json_tricks_dump(obj, sort_keys=True)
73-
except:
74-
out = str(obj).replace("\n", "")
75-
return indent_str(out, indent)
76-
77-
78-
def user_obj_str(obj):
79-
return truncate_str(pp_str_flat(obj), 1000)
80-
81-
82-
def log_indent(obj, indent=0, logging_func=logger.info):
83-
if not is_str(obj):
84-
text = repr(obj)
85-
else:
86-
text = obj
87-
logging_func(indent_str(text, indent))
88-
89-
90-
def log_pretty(obj, indent=0, logging_func=logger.info):
91-
formatted_str = pp_str(obj, indent)
92-
for line in formatted_str.split("\n"):
93-
logging_func(line)
94-
95-
96-
def log_pretty_flat(obj, indent=0, logging_func=logger.info):
97-
logging_func(pp_str_flat(obj, indent))
98-
99-
10047
def pluralize(num, singular, plural):
10148
if num == 1:
10249
return str(num) + " " + singular
@@ -545,51 +492,6 @@ def extract_zip(zip_path, dest_dir=None, delete_zip_file=False):
545492
rm_file(zip_path)
546493

547494

548-
def print_samples_horiz(
549-
samples, truncate=20, sep=",", first_sep=":", pad=1, first_pad=None, key_list=None
550-
):
551-
if first_pad is None:
552-
first_pad = pad
553-
554-
if not key_list:
555-
field_names = sorted(list(set(flatten([list(sample.keys()) for sample in samples]))))
556-
else:
557-
field_names = key_list
558-
559-
rows = []
560-
for field_name in field_names:
561-
rows.append([field_name] + [sample.get(field_name, None) for sample in samples])
562-
563-
rows_strs = []
564-
for row in rows:
565-
row_strs = []
566-
for i, item in enumerate(row):
567-
row_strs.append(str_rep(item, truncate))
568-
rows_strs.append(row_strs)
569-
570-
max_lens = []
571-
for i in range(len(rows_strs[0])):
572-
max_lens.append(max_len([row[i] for row in rows_strs]))
573-
574-
types = []
575-
for i in range(len(rows[0])):
576-
types.append(is_number_col([row[i] for row in rows]))
577-
578-
for row_strs in rows_strs:
579-
row_str = ""
580-
for i, item in enumerate(row_strs):
581-
if i == 0:
582-
row_str += pad_smart(item + first_sep, max_lens[i] + len(first_sep), types[i])
583-
row_str += " " * first_pad
584-
elif i == len(row_strs) - 1:
585-
row_str += pad_smart(item, max_lens[i], types[i]).rstrip()
586-
else:
587-
row_str += pad_smart(item + sep, max_lens[i] + len(sep), types[i])
588-
row_str += " " * pad
589-
590-
logger.info(row_str.strip())
591-
592-
593495
def max_len(strings):
594496
return max(len(s) for s in strings)
595497

@@ -609,24 +511,6 @@ def pad_left(string, width):
609511
return string.rjust(width)
610512

611513

612-
def str_rep(item, truncate=20, round=2):
613-
if item is None:
614-
return ""
615-
if is_float(item):
616-
out = "{0:.2f}".format(item)
617-
else:
618-
out = str(item)
619-
620-
return truncate_str(out, truncate)
621-
622-
623-
def truncate_str(item, truncate=20):
624-
if is_str(item) and truncate is not None and truncate > 3 and len(item) > truncate:
625-
trim = truncate - 3
626-
return item[:trim] + "..."
627-
return item
628-
629-
630514
def is_number_col(items):
631515
if all(item is None for item in items):
632516
return False
@@ -638,11 +522,6 @@ def is_number_col(items):
638522
return True
639523

640524

641-
def log_job_finished(workload_id):
642-
timestamp = now_timestamp_rfc_3339()
643-
logger.info("workload: {}, completed: {}".format(workload_id, timestamp))
644-
645-
646525
def has_function(impl, fn_name):
647526
fn = getattr(impl, fn_name, None)
648527
if fn is None:

0 commit comments

Comments
 (0)