Skip to content

Commit 0e44e9c

Browse files
authored
Move to HTTP Inspector (#2626)
1 parent bfb54b0 commit 0e44e9c

15 files changed

Lines changed: 585 additions & 337 deletions

File tree

sanic/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
140140
"configure_logging",
141141
"ctx",
142142
"error_handler",
143+
"inspector_class",
143144
"go_fast",
144145
"listeners",
145146
"multiplexer",
@@ -176,6 +177,7 @@ def __init__(
176177
dumps: Optional[Callable[..., AnyStr]] = None,
177178
loads: Optional[Callable[..., Any]] = None,
178179
inspector: bool = False,
180+
inspector_class: Optional[Type[Inspector]] = None,
179181
) -> None:
180182
super().__init__(name=name)
181183
# logging
@@ -211,6 +213,7 @@ def __init__(
211213
self.configure_logging: bool = configure_logging
212214
self.ctx: Any = ctx or SimpleNamespace()
213215
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
216+
self.inspector_class: Type[Inspector] = inspector_class or Inspector
214217
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
215218
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
216219
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}

sanic/cli/app.py

Lines changed: 73 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,21 @@
33
import shutil
44
import sys
55

6-
from argparse import ArgumentParser, RawTextHelpFormatter
6+
from argparse import Namespace
77
from functools import partial
88
from textwrap import indent
9-
from typing import Any, List, Union
9+
from typing import List, Union, cast
1010

1111
from sanic.app import Sanic
1212
from sanic.application.logo import get_logo
1313
from sanic.cli.arguments import Group
14-
from sanic.log import error_logger
15-
from sanic.worker.inspector import inspect
14+
from sanic.cli.base import SanicArgumentParser, SanicHelpFormatter
15+
from sanic.cli.inspector import make_inspector_parser
16+
from sanic.cli.inspector_client import InspectorClient
17+
from sanic.log import Colors, error_logger
1618
from sanic.worker.loader import AppLoader
1719

1820

19-
class SanicArgumentParser(ArgumentParser):
20-
...
21-
22-
2321
class SanicCLI:
2422
DESCRIPTION = indent(
2523
f"""
@@ -46,7 +44,7 @@ def __init__(self) -> None:
4644
self.parser = SanicArgumentParser(
4745
prog="sanic",
4846
description=self.DESCRIPTION,
49-
formatter_class=lambda prog: RawTextHelpFormatter(
47+
formatter_class=lambda prog: SanicHelpFormatter(
5048
prog,
5149
max_help_position=36 if width > 96 else 24,
5250
indent_increment=4,
@@ -58,16 +56,27 @@ def __init__(self) -> None:
5856
self.main_process = (
5957
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
6058
)
61-
self.args: List[Any] = []
59+
self.args: Namespace = Namespace()
6260
self.groups: List[Group] = []
61+
self.inspecting = False
6362

6463
def attach(self):
64+
if sys.argv[1] == "inspect":
65+
self.inspecting = True
66+
self.parser.description = get_logo(True)
67+
make_inspector_parser(self.parser)
68+
return
69+
6570
for group in Group._registry:
6671
instance = group.create(self.parser)
6772
instance.attach()
6873
self.groups.append(instance)
6974

7075
def run(self, parse_args=None):
76+
if self.inspecting:
77+
self._inspector()
78+
return
79+
7180
legacy_version = False
7281
if not parse_args:
7382
# This is to provide backwards compat -v to display version
@@ -86,52 +95,21 @@ def run(self, parse_args=None):
8695
self.args = self.parser.parse_args(args=parse_args)
8796
self._precheck()
8897
app_loader = AppLoader(
89-
self.args.module,
90-
self.args.factory,
91-
self.args.simple,
92-
self.args,
98+
self.args.module, self.args.factory, self.args.simple, self.args
9399
)
94100

101+
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
102+
self._inspector_legacy(app_loader)
103+
return
104+
95105
try:
96106
app = self._get_app(app_loader)
97107
kwargs = self._build_run_kwargs()
98108
except ValueError as e:
99109
error_logger.exception(f"Failed to run app: {e}")
100110
else:
101-
if (
102-
self.args.inspect
103-
or self.args.inspect_raw
104-
or self.args.trigger
105-
or self.args.scale is not None
106-
):
107-
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true"
108-
else:
109-
for http_version in self.args.http:
110-
app.prepare(**kwargs, version=http_version)
111-
112-
if (
113-
self.args.inspect
114-
or self.args.inspect_raw
115-
or self.args.trigger
116-
or self.args.scale is not None
117-
):
118-
if self.args.scale is not None:
119-
if self.args.scale <= 0:
120-
error_logger.error("There must be at least 1 worker")
121-
sys.exit(1)
122-
action = f"scale={self.args.scale}"
123-
else:
124-
action = self.args.trigger or (
125-
"raw" if self.args.inspect_raw else "pretty"
126-
)
127-
inspect(
128-
app.config.INSPECTOR_HOST,
129-
app.config.INSPECTOR_PORT,
130-
action,
131-
)
132-
del os.environ["SANIC_IGNORE_PRODUCTION_WARNING"]
133-
return
134-
111+
for http_version in self.args.http:
112+
app.prepare(**kwargs, version=http_version)
135113
if self.args.single:
136114
serve = Sanic.serve_single
137115
elif self.args.legacy:
@@ -140,6 +118,53 @@ def run(self, parse_args=None):
140118
serve = partial(Sanic.serve, app_loader=app_loader)
141119
serve(app)
142120

121+
def _inspector_legacy(self, app_loader: AppLoader):
122+
host = port = None
123+
module = cast(str, self.args.module)
124+
if ":" in module:
125+
maybe_host, maybe_port = module.rsplit(":", 1)
126+
if maybe_port.isnumeric():
127+
host, port = maybe_host, int(maybe_port)
128+
if not host:
129+
app = self._get_app(app_loader)
130+
host, port = app.config.INSPECTOR_HOST, app.config.INSPECTOR_PORT
131+
132+
action = self.args.trigger or "info"
133+
134+
InspectorClient(
135+
str(host), int(port or 6457), False, self.args.inspect_raw, ""
136+
).do(action)
137+
sys.stdout.write(
138+
f"\n{Colors.BOLD}{Colors.YELLOW}WARNING:{Colors.END} "
139+
"You are using the legacy CLI command that will be removed in "
140+
f"{Colors.RED}v23.3{Colors.END}. See ___ or checkout the new "
141+
"style commands:\n\n\t"
142+
f"{Colors.YELLOW}sanic inspect --help{Colors.END}\n"
143+
)
144+
145+
def _inspector(self):
146+
args = sys.argv[2:]
147+
self.args, unknown = self.parser.parse_known_args(args=args)
148+
if unknown:
149+
for arg in unknown:
150+
if arg.startswith("--"):
151+
key, value = arg.split("=")
152+
setattr(self.args, key.lstrip("-"), value)
153+
154+
kwargs = {**self.args.__dict__}
155+
host = kwargs.pop("host")
156+
port = kwargs.pop("port")
157+
secure = kwargs.pop("secure")
158+
raw = kwargs.pop("raw")
159+
action = kwargs.pop("action") or "info"
160+
api_key = kwargs.pop("api_key")
161+
positional = kwargs.pop("positional", None)
162+
if action == "<custom>" and positional:
163+
action = positional[0]
164+
if len(positional) > 1:
165+
kwargs["args"] = positional[1:]
166+
InspectorClient(host, port, secure, raw, api_key).do(action, **kwargs)
167+
143168
def _precheck(self):
144169
# Custom TLS mismatch handling for better diagnostics
145170
if self.main_process and (

sanic/cli/arguments.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,6 @@ def attach(self):
115115
const="shutdown",
116116
help=("Trigger all processes to shutdown"),
117117
)
118-
group.add_argument(
119-
"--scale",
120-
dest="scale",
121-
type=int,
122-
help=("Scale number of workers"),
123-
)
124118

125119

126120
class HTTPVersionGroup(Group):

sanic/cli/base.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from argparse import (
2+
SUPPRESS,
3+
Action,
4+
ArgumentParser,
5+
RawTextHelpFormatter,
6+
_SubParsersAction,
7+
)
8+
from typing import Any
9+
10+
11+
class SanicArgumentParser(ArgumentParser):
12+
def _check_value(self, action: Action, value: Any) -> None:
13+
if isinstance(action, SanicSubParsersAction):
14+
return
15+
super()._check_value(action, value)
16+
17+
18+
class SanicHelpFormatter(RawTextHelpFormatter):
19+
def add_usage(self, usage, actions, groups, prefix=None):
20+
if not usage:
21+
usage = SUPPRESS
22+
# Add one linebreak, but not two
23+
self.add_text("\x1b[1A'")
24+
super().add_usage(usage, actions, groups, prefix)
25+
26+
27+
class SanicSubParsersAction(_SubParsersAction):
28+
def __call__(self, parser, namespace, values, option_string=None):
29+
self._name_parser_map
30+
parser_name = values[0]
31+
if parser_name not in self._name_parser_map:
32+
self._name_parser_map[parser_name] = parser
33+
values = ["<custom>", *values]
34+
35+
super().__call__(parser, namespace, values, option_string)

sanic/cli/inspector.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from argparse import ArgumentParser
2+
3+
from sanic.application.logo import get_logo
4+
from sanic.cli.base import SanicHelpFormatter, SanicSubParsersAction
5+
6+
7+
def _add_shared(parser: ArgumentParser) -> None:
8+
parser.add_argument("--host", "-H", default="localhost")
9+
parser.add_argument("--port", "-p", default=6457, type=int)
10+
parser.add_argument("--secure", "-s", action="store_true")
11+
parser.add_argument("--api-key", "-k")
12+
parser.add_argument(
13+
"--raw",
14+
action="store_true",
15+
help="Whether to output the raw response information",
16+
)
17+
18+
19+
class InspectorSubParser(ArgumentParser):
20+
def __init__(self, *args, **kwargs):
21+
super().__init__(*args, **kwargs)
22+
_add_shared(self)
23+
if not self.description:
24+
self.description = ""
25+
self.description = get_logo(True) + self.description
26+
27+
28+
def make_inspector_parser(parser: ArgumentParser) -> None:
29+
_add_shared(parser)
30+
subparsers = parser.add_subparsers(
31+
action=SanicSubParsersAction,
32+
dest="action",
33+
description=(
34+
"Run one of the below subcommands. If you have created a custom "
35+
"Inspector instance, then you can run custom commands.\nSee ___ "
36+
"for more details."
37+
),
38+
title="Required\n========\n Subcommands",
39+
parser_class=InspectorSubParser,
40+
)
41+
subparsers.add_parser(
42+
"reload",
43+
help="Trigger a reload of the server workers",
44+
formatter_class=SanicHelpFormatter,
45+
)
46+
subparsers.add_parser(
47+
"shutdown",
48+
help="Shutdown the application and all processes",
49+
formatter_class=SanicHelpFormatter,
50+
)
51+
scale = subparsers.add_parser(
52+
"scale",
53+
help="Scale the number of workers",
54+
formatter_class=SanicHelpFormatter,
55+
)
56+
scale.add_argument("replicas", type=int)
57+
58+
custom = subparsers.add_parser(
59+
"<custom>",
60+
help="Run a custom command",
61+
description=(
62+
"keyword arguments:\n When running a custom command, you can "
63+
"add keyword arguments by appending them to your command\n\n"
64+
"\tsanic inspect foo --one=1 --two=2"
65+
),
66+
formatter_class=SanicHelpFormatter,
67+
)
68+
custom.add_argument(
69+
"positional",
70+
nargs="*",
71+
help="Add one or more non-keyword args to your custom command",
72+
)

0 commit comments

Comments
 (0)