|
1 | | -import datetime |
2 | | -import json |
3 | | -import os.path |
4 | 1 | import re |
5 | 2 | import time |
6 | | -from functools import wraps |
7 | 3 |
|
8 | 4 | import flask |
9 | 5 | import flask.cli |
10 | 6 | import jinja2 |
11 | 7 | from flask import _app_ctx_stack, request |
| 8 | +from flask.cli import ScriptInfo |
12 | 9 | from werkzeug.exceptions import BadRequest |
| 10 | +from werkzeug.middleware.dispatcher import DispatcherMiddleware |
13 | 11 |
|
14 | | -from appmap._implementation import generation, web_framework |
| 12 | +import appmap.wrapt as wrapt |
15 | 13 | from appmap._implementation.detect_enabled import DetectEnabled |
16 | 14 | from appmap._implementation.env import Env |
17 | 15 | from appmap._implementation.event import HttpServerRequestEvent, HttpServerResponseEvent |
| 16 | +from appmap._implementation.flask import remote_recording |
18 | 17 | from appmap._implementation.recorder import Recorder |
19 | 18 | from appmap._implementation.web_framework import AppmapMiddleware |
20 | 19 | from appmap._implementation.web_framework import TemplateHandler as BaseTemplateHandler |
@@ -50,61 +49,35 @@ def request_params(req): |
50 | 49 |
|
51 | 50 |
|
52 | 51 | class AppmapFlask(AppmapMiddleware): |
53 | | - def __init__(self, app=None): |
| 52 | + """ |
| 53 | + A Flask extension to add remote recording to an application. |
| 54 | + Should be loaded by default, but can also be added manually. |
| 55 | +
|
| 56 | + For example: |
| 57 | +
|
| 58 | + ``` |
| 59 | + from appmap.flask import AppmapFlask |
| 60 | +
|
| 61 | + app = new Flask(__Name__) |
| 62 | + AppmapFlask().init_app(app) |
| 63 | + ``` |
| 64 | + """ |
| 65 | + |
| 66 | + def __init__(self): |
54 | 67 | super().__init__() |
55 | | - self.app = app |
56 | | - if app is not None: |
57 | | - self.init_app(app) |
58 | 68 |
|
59 | 69 | def init_app(self, app): |
60 | 70 | if self.should_record(): |
61 | | - # it may record requests but not remote (APPMAP=false) |
62 | 71 | self.recorder = Recorder.get_current() |
63 | 72 |
|
64 | 73 | if DetectEnabled.should_enable("remote"): |
65 | | - app.add_url_rule( |
66 | | - self.record_url, |
67 | | - "appmap_record_get", |
68 | | - view_func=self.record_get, |
69 | | - methods=["GET"], |
70 | | - ) |
71 | | - app.add_url_rule( |
72 | | - self.record_url, |
73 | | - "appmap_record_post", |
74 | | - view_func=self.record_post, |
75 | | - methods=["POST"], |
76 | | - ) |
77 | | - app.add_url_rule( |
78 | | - self.record_url, |
79 | | - "appmap_record_delete", |
80 | | - view_func=self.record_delete, |
81 | | - methods=["DELETE"], |
| 74 | + app.wsgi_app = DispatcherMiddleware( |
| 75 | + app.wsgi_app, {"/_appmap": remote_recording} |
82 | 76 | ) |
83 | 77 |
|
84 | 78 | app.before_request(self.before_request) |
85 | 79 | app.after_request(self.after_request) |
86 | 80 |
|
87 | | - def record_get(self): |
88 | | - if not self.should_record(): |
89 | | - return "Appmap is disabled.", 404 |
90 | | - |
91 | | - return {"enabled": self.recorder.get_enabled()} |
92 | | - |
93 | | - def record_post(self): |
94 | | - if self.recorder.get_enabled(): |
95 | | - return "Recording is already in progress", 409 |
96 | | - |
97 | | - self.recorder.start_recording() |
98 | | - return "", 200 |
99 | | - |
100 | | - def record_delete(self): |
101 | | - if not self.recorder.get_enabled(): |
102 | | - return "No recording is in progress", 404 |
103 | | - |
104 | | - self.recorder.stop_recording() |
105 | | - |
106 | | - return json.loads(generation.dump(self.recorder)) |
107 | | - |
108 | 81 | def before_request(self): |
109 | 82 | if not self.should_record(): |
110 | 83 | return |
@@ -182,18 +155,18 @@ class TemplateHandler(BaseTemplateHandler): |
182 | 155 | pass |
183 | 156 |
|
184 | 157 |
|
185 | | -def wrap_cli_fn(fn): |
186 | | - @wraps(fn) |
187 | | - def install_middleware(*args, **kwargs): |
188 | | - app = fn(*args, **kwargs) |
189 | | - if app: |
190 | | - appmap_flask = AppmapFlask() |
191 | | - appmap_flask.init_app(app) |
192 | | - return app |
| 158 | +def install_extension(wrapped, _, args, kwargs): |
| 159 | + app = wrapped(*args, **kwargs) |
| 160 | + if app: |
| 161 | + AppmapFlask().init_app(app) |
193 | 162 |
|
194 | | - return install_middleware |
| 163 | + return app |
195 | 164 |
|
196 | 165 |
|
197 | 166 | if Env.current.enabled: |
198 | | - flask.cli.call_factory = wrap_cli_fn(flask.cli.call_factory) |
199 | | - flask.cli.locate_app = wrap_cli_fn(flask.cli.locate_app) |
| 167 | + # ScriptInfo.load_app is the function that's used by the Flask cli to load an app, no matter how |
| 168 | + # the app's module is specified (e.g. with the FLASK_APP env var, the `--app` flag, etc). Hook |
| 169 | + # it so it installs our extension on the app. |
| 170 | + ScriptInfo.load_app = wrapt.wrap_function_wrapper( |
| 171 | + "flask.cli", "ScriptInfo.load_app", install_extension |
| 172 | + ) |
0 commit comments