Skip to content

Commit 30d7db5

Browse files
authored
Merge pull request #190 from getappmap/remote-and-requests_20221026
Remote and requests 20221026
2 parents bb33143 + 521a910 commit 30d7db5

19 files changed

+237
-188
lines changed

appmap/_implementation/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from . import configuration
22
from . import env as appmapenv
3-
from . import event, importer, metadata, recorder
3+
from . import event, importer, metadata, recorder, web_framework
44
from .py_version_check import check_py_version
55

66

@@ -12,6 +12,7 @@ def initialize(**kwargs):
1212
recorder.initialize()
1313
configuration.initialize() # needs to be initialized after recorder
1414
metadata.initialize()
15+
web_framework.initialize()
1516

1617

1718
initialize()

appmap/_implementation/configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def _load_config(self):
218218
Env.current.enabled = should_enable
219219
except ParserError:
220220
pass
221-
logger.info("config: %s", self._config)
221+
logger.debug("config: %s", self._config)
222222
return
223223

224224
if not Env.current.enabled:

appmap/_implementation/django.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
This file contains Django middleware that can be inserted into an app's stack to expose the remote
3+
recording endpoint.
4+
5+
It expects the importer to have verified that Django is available.
6+
"""
7+
8+
from http import HTTPStatus
9+
10+
from django.http import HttpRequest, HttpResponse
11+
12+
from . import remote_recording
13+
14+
15+
class RemoteRecording: # pylint: disable=missing-class-docstring
16+
def __init__(self, get_response):
17+
super().__init__()
18+
self.get_response = get_response
19+
20+
def __call__(self, request: HttpRequest):
21+
if request.path != "/_appmap/record":
22+
return self.get_response(request)
23+
24+
handlers = {
25+
"GET": remote_recording.status,
26+
"POST": remote_recording.start,
27+
"DELETE": remote_recording.stop,
28+
}
29+
30+
def not_allowed():
31+
return "", HTTPStatus.METHOD_NOT_ALLOWED
32+
33+
body, status = handlers.get(request.method, not_allowed)()
34+
return HttpResponse(body, status=status, content_type="application/json")

appmap/_implementation/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def display_params(self):
8080
return self.get("APPMAP_DISPLAY_PARAMS", "true").lower() == "true"
8181

8282
def _configure_logging(self):
83-
log_level = self.get("APPMAP_LOG_LEVEL", "warning").upper()
83+
log_level = self.get("APPMAP_LOG_LEVEL", "info").upper()
8484

8585
log_config = self.get("APPMAP_LOG_CONFIG")
8686
log_stream = self.get("APPMAP_LOG_STREAM", "stderr")

appmap/_implementation/event.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
12
import inspect
23
import logging
34
import threading
@@ -90,7 +91,7 @@ class Event:
9091
__slots__ = ["id", "event", "thread_id"]
9192

9293
def __init__(self, event):
93-
self.id = Recorder.get_current().next_event_id()
94+
self.id = Recorder.next_event_id()
9495
self.event = event
9596
self.thread_id = _EventIds.get_thread_id()
9697

appmap/_implementation/flask.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,30 @@
1-
""" remote_recording is a Flask app that can be mounted to expose the remote-recording endpoint. """
2-
import json
1+
"""
2+
This file contains a Flask app that is mounted on /_appmap to expose the remote-recording endpoint
3+
in a user's app.
34
4-
from flask import Flask
5+
It should only be imported if other code has already verified that Flask is available.
6+
"""
57

6-
from . import generation
7-
from .recorder import Recorder
8-
from .web_framework import AppmapMiddleware
8+
from flask import Flask, Response
99

10-
remote_recording = Flask(__name__)
10+
from . import remote_recording
1111

12+
app = Flask(__name__)
1213

13-
@remote_recording.route("/record", methods=["GET"])
14-
def status():
15-
if not AppmapMiddleware.should_record():
16-
return "Appmap is disabled.", 404
1714

18-
return {"enabled": Recorder.get_current().get_enabled()}
15+
@app.route("/record", methods=["GET"])
16+
def status():
17+
body, rrstatus = remote_recording.status()
18+
return Response(body, status=rrstatus, mimetype="application/json")
1919

2020

21-
@remote_recording.route("/record", methods=["POST"])
21+
@app.route("/record", methods=["POST"])
2222
def start():
23-
r = Recorder.get_current()
24-
if r.get_enabled():
25-
return "Recording is already in progress", 409
26-
27-
r.start_recording()
28-
return "", 200
23+
body, rrstatus = remote_recording.start()
24+
return Response(body, status=rrstatus, mimetype="application/json")
2925

3026

31-
@remote_recording.route("/record", methods=["DELETE"])
27+
@app.route("/record", methods=["DELETE"])
3228
def stop():
33-
r = Recorder.get_current()
34-
if not r.get_enabled():
35-
return "No recording is in progress", 404
36-
37-
r.stop_recording()
38-
39-
return json.loads(generation.dump(r))
29+
body, rrstatus = remote_recording.stop()
30+
return Response(body, status=rrstatus, mimetype="application/json")

appmap/_implementation/importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def instrument_functions(filterable):
187187
if not cls.filter_chain.filter(filterable):
188188
return
189189

190-
logger.info(" looking for members of %s", filterable.obj)
190+
logger.debug(" looking for members of %s", filterable.obj)
191191
functions = get_members(filterable.obj)
192192
logger.debug(" functions %s", functions)
193193

appmap/_implementation/instrument.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def call_instrumented(f, instance, args, kwargs):
109109

110110
def instrument(filterable):
111111
"""return an instrumented function"""
112-
logger.info("hooking %s", filterable.fqname)
112+
logger.debug("hooking %s", filterable.fqname)
113113
fn = filterable.obj
114114

115115
make_call_event = event.CallEvent.make(fn, filterable.fntype)

appmap/_implementation/recorder.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
logger = logging.getLogger(__name__)
99

10+
# pylint: disable=global-statement
1011
_default_recorder = None
1112

1213

@@ -22,10 +23,14 @@ class Recorder(ABC):
2223
def events(self):
2324
return self._events
2425

25-
@abstractmethod
26-
def next_event_id(self):
27-
self._next_event_id += 1
28-
return self._next_event_id
26+
_next_event_id = 0
27+
_next_event_id_lock = threading.Lock()
28+
29+
@classmethod
30+
def next_event_id(cls):
31+
with cls._next_event_id_lock:
32+
cls._next_event_id += 1
33+
return cls._next_event_id
2934

3035
# It might be nice to put @property on the getters here. The python maintainers have gone back
3136
# and forth on whether you should be able to combine @classmethod and @property. In 3.11,
@@ -44,24 +49,40 @@ def set_current(cls, r):
4449
"""
4550
Set the recorder for the current thread.
4651
"""
47-
appmap_tls()[cls._RECORDER_KEY] = r
52+
tls = appmap_tls()
53+
if r:
54+
tls[cls._RECORDER_KEY] = r
55+
else:
56+
del tls[cls._RECORDER_KEY]
57+
4858
return r
4959

60+
@classmethod
61+
def get_global(cls):
62+
_, shared = cls._get_current()
63+
return shared
64+
65+
@classmethod
66+
def new_global(cls):
67+
global _default_recorder
68+
_default_recorder = SharedRecorder()
69+
return _default_recorder
70+
5071
@classmethod
5172
def get_enabled(cls):
52-
return cls.get_current()._enabled
73+
return cls.get_current()._enabled # pylint: disable=protected-access
5374

5475
@classmethod
5576
def set_enabled(cls, e):
56-
cls.get_current()._enabled = e
77+
cls.get_current()._enabled = e # pylint: disable=protected-access
5778

5879
@classmethod
5980
def start_recording(cls):
60-
cls.get_current()._start_recording()
81+
cls.get_current()._start_recording() # pylint: disable=protected-access
6182

6283
@classmethod
6384
def stop_recording(cls):
64-
return cls.get_current()._stop_recording()
85+
return cls.get_current()._stop_recording() # pylint: disable=protected-access
6586

6687
@classmethod
6788
def add_event(cls, event):
@@ -70,28 +91,25 @@ def add_event(cls, event):
7091
one).
7192
"""
7293
perthread, shared = cls._get_current()
73-
shared._add_event(event)
94+
shared._add_event(event) # pylint: disable=protected-access
7495
if perthread:
75-
perthread._add_event(event)
96+
perthread._add_event(event) # pylint: disable=protected-access
7697

7798
_RECORDER_KEY = "appmap_recorder"
7899

79100
@classmethod
80101
def _get_current(cls):
81-
global _default_recorder
82102
perthread = appmap_tls().get(cls._RECORDER_KEY, None)
83103

84104
return [perthread, _default_recorder]
85105

86106
def clear(self):
87107
self._events = []
88-
self._next_event_id = 0
89108

90109
def __init__(self, enabled=False):
91110
self._events = []
92111
self._enabled = enabled
93112
self.start_tb = None
94-
self._next_event_id = 0
95113

96114
@abstractmethod
97115
def _start_recording(self):
@@ -122,8 +140,7 @@ def _initialize():
122140
threads initializing the default recorder. If you find yourself wanting to do that, you
123141
should probably be using per-thread recording.
124142
"""
125-
global _default_recorder
126-
_default_recorder = SharedRecorder()
143+
Recorder.new_global()
127144

128145

129146
class ThreadRecorder(Recorder):
@@ -135,9 +152,8 @@ class ThreadRecorder(Recorder):
135152
def events(self):
136153
return super().events
137154

138-
def next_event_id(self):
139-
return super().next_event_id()
140-
155+
# They're not useless, because they're abtract with a default implementation
156+
# pragma pylint: disable=useless-super-delegation
141157
def _start_recording(self):
142158
super()._start_recording()
143159

@@ -147,6 +163,8 @@ def _stop_recording(self):
147163
def _add_event(self, event):
148164
super()._add_event(event)
149165

166+
# pragma pylint: enable=useless-super-delegation
167+
150168

151169
class SharedRecorder(Recorder):
152170
"""
@@ -155,6 +173,14 @@ class SharedRecorder(Recorder):
155173

156174
_lock = threading.Lock()
157175

176+
def __init__(self):
177+
super().__init__()
178+
Recorder._next_event_id = 0
179+
180+
def clear(self):
181+
super().clear()
182+
Recorder._next_event_id = 0
183+
158184
@property
159185
def events(self):
160186
with self._lock:
@@ -168,14 +194,10 @@ def _stop_recording(self):
168194
with self._lock:
169195
return super()._stop_recording()
170196

171-
def next_event_id(self):
172-
with self._lock:
173-
return super().next_event_id()
174-
175197
def _add_event(self, event):
176198
with self._lock:
177199
super()._add_event(event)
178200

179201

180202
def initialize():
181-
Recorder._initialize()
203+
Recorder._initialize() # pylint: disable=protected-access
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
""" remote_recording is a Flask app that can be mounted to expose the remote-recording endpoint. """
2+
import json
3+
from threading import Lock
4+
5+
from . import generation
6+
from .detect_enabled import DetectEnabled
7+
from .recorder import Recorder
8+
9+
# pylint: disable=global-statement
10+
_enabled_lock = Lock()
11+
_enabled = False
12+
13+
14+
def status():
15+
if not DetectEnabled.should_enable("remote"):
16+
return "Appmap is disabled.", 404
17+
18+
with _enabled_lock:
19+
return json.dumps({"enabled": _enabled}), 200
20+
21+
22+
def start():
23+
global _enabled
24+
with _enabled_lock:
25+
if _enabled:
26+
return "Recording is already in progress", 409
27+
28+
Recorder.new_global().start_recording()
29+
_enabled = True
30+
return "", 200
31+
32+
33+
def stop():
34+
global _enabled
35+
with _enabled_lock:
36+
if not _enabled:
37+
return "No recording is in progress", 404
38+
r = Recorder.get_global()
39+
r.stop_recording()
40+
_enabled = False
41+
return generation.dump(r), 200
42+
43+
44+
def initialize():
45+
global _enabled
46+
_enabled = False

0 commit comments

Comments
 (0)