Skip to content

Commit 8d9993d

Browse files
symwellapotterri
authored andcommitted
feat: record by default
1 parent 5858681 commit 8d9993d

14 files changed

+573
-128
lines changed

appmap/_implementation/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from . import configuration
22
from . import env as appmapenv
33
from . import event, metadata, recording
4+
from .detect_enabled import DetectEnabled
45
from .py_version_check import check_py_version
56

67

@@ -11,6 +12,7 @@ def initialize(**kwargs):
1112
recording.initialize()
1213
configuration.initialize() # needs to be initialized after recording
1314
metadata.initialize()
15+
DetectEnabled.initialize()
1416

1517

1618
initialize()
+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
Detect if AppMap is enabled.
3+
"""
4+
5+
import importlib
6+
import logging
7+
import os
8+
from textwrap import dedent
9+
10+
logger = logging.getLogger(__name__)
11+
12+
RECORDING_METHODS = ["pytest", "unittest", "remote", "requests"]
13+
14+
# Detects whether AppMap recording should be enabled. This test can be
15+
# performed generally, or for a particular recording method. Recording
16+
# can be enabled explicitly, for example via APPMAP=true, or it can be
17+
# enabled implicitly, by running in a dev or test web application
18+
# environment. Recording can also be disabled explicitly, using
19+
# environment variables.
20+
class DetectEnabled:
21+
_instance = None
22+
23+
def __new__(cls):
24+
if cls._instance is None:
25+
logger.debug("Creating the DetectEnabled object")
26+
cls._instance = super(DetectEnabled, cls).__new__(cls)
27+
cls._instance._initialized = False
28+
return cls._instance
29+
30+
def __init__(self):
31+
if self._initialized:
32+
return
33+
34+
self._initialized = True
35+
self._detected_for_method = {}
36+
37+
@classmethod
38+
def initialize(cls):
39+
cls._instance = None
40+
# because apparently __new__ and __init__ don't get called
41+
cls._detected_for_method = {}
42+
43+
@classmethod
44+
def clear_cache(cls):
45+
cls._detected_for_method = {}
46+
47+
@classmethod
48+
def is_appmap_repo(cls):
49+
return os.path.exists("appmap/__init__.py") and os.path.exists(
50+
"appmap/_implementation/__init__.py"
51+
)
52+
53+
@classmethod
54+
def should_enable(cls, recording_method):
55+
"""
56+
Should recording be enabled for the current recording method?
57+
"""
58+
if recording_method in cls._detected_for_method:
59+
return cls._detected_for_method[recording_method]
60+
else:
61+
message, enabled = cls.detect_should_enable(recording_method)
62+
cls._detected_for_method[recording_method] = enabled
63+
if enabled:
64+
logger.warning(dedent(f"AppMap recording is enabled because {message}"))
65+
return enabled
66+
67+
@classmethod
68+
def detect_should_enable(cls, recording_method):
69+
if not recording_method:
70+
return ["no recording method is set", False]
71+
72+
if recording_method not in RECORDING_METHODS:
73+
return ["invalid recording method", False]
74+
75+
# explicitly disabled or enabled
76+
if "APPMAP" in os.environ:
77+
if os.environ["APPMAP"] == "false":
78+
return ["APPMAP=false", False]
79+
elif os.environ["APPMAP"] == "true":
80+
return ["APPMAP=true", True]
81+
82+
# recording method explicitly disabled or enabled
83+
if recording_method:
84+
for one_recording_method in RECORDING_METHODS:
85+
if one_recording_method == recording_method.lower():
86+
env_var = "_".join(["APPMAP", "RECORD", recording_method.upper()])
87+
if env_var in os.environ:
88+
if os.environ[env_var] == "false":
89+
return [f"{env_var}=false", False]
90+
elif os.environ[env_var] == "true":
91+
return [f"{env_var}=true", True]
92+
93+
# it's flask
94+
message, should_enable = cls.is_flask_and_should_enable()
95+
if should_enable == True or should_enable == False:
96+
return [message, should_enable]
97+
98+
# it's django
99+
message, should_enable = cls.is_django_and_should_enable()
100+
if should_enable == True or should_enable == False:
101+
return [message, should_enable]
102+
103+
if recording_method in RECORDING_METHODS:
104+
return ["will record by default", True]
105+
106+
return ["it's not enabled by any configuration or framework", False]
107+
108+
@classmethod
109+
def is_flask_and_should_enable(cls):
110+
if "FLASK_DEBUG" in os.environ:
111+
if os.environ["FLASK_DEBUG"] == "1":
112+
return [f"FLASK_DEBUG={os.environ['FLASK_DEBUG']}", True]
113+
elif os.environ["FLASK_DEBUG"] == "0":
114+
return [f"FLASK_DEBUG={os.environ['FLASK_DEBUG']}", False]
115+
116+
if "FLASK_ENV" in os.environ:
117+
if os.environ["FLASK_ENV"] == "development":
118+
return [f"FLASK_ENV={os.environ['FLASK_ENV']}", True]
119+
elif os.environ["FLASK_ENV"] == "production":
120+
return [f"FLASK_ENV={os.environ['FLASK_ENV']}", False]
121+
122+
return ["it's not Flask", None]
123+
124+
@classmethod
125+
def is_django_and_should_enable(cls):
126+
if (
127+
"DJANGO_SETTINGS_MODULE" in os.environ
128+
and os.environ["DJANGO_SETTINGS_MODULE"] != ""
129+
):
130+
try:
131+
settings = importlib.import_module(os.environ["DJANGO_SETTINGS_MODULE"])
132+
except Exception as exn:
133+
settings = None
134+
return [
135+
"couldn't load DJANGO_SETTINGS_MODULE={os.environ['DJANGO_SETTINGS_MODULE']}",
136+
False,
137+
]
138+
139+
if settings:
140+
try:
141+
# don't crash if the settings file doesn't contain
142+
# a DEBUG variable
143+
if settings.DEBUG == True:
144+
return [
145+
f"{os.environ['DJANGO_SETTINGS_MODULE']}.DEBUG={settings.DEBUG}",
146+
True,
147+
]
148+
elif settings.DEBUG == False:
149+
return [
150+
f"{os.environ['DJANGO_SETTINGS_MODULE']}.DEBUG={settings.DEBUG}",
151+
False,
152+
]
153+
except AttributeError as exn:
154+
# it wasn't set. it's ok. don't crash
155+
# AttributeError: module 'app.settings_appmap_false' has no attribute 'DEBUG'
156+
pass
157+
158+
if settings:
159+
try:
160+
# don't crash if the settings file doesn't contain
161+
# an APPMAP variable
162+
if (
163+
settings.APPMAP == True
164+
or str(settings.APPMAP).upper() == "true".upper()
165+
):
166+
return [
167+
f"{os.environ['DJANGO_SETTINGS_MODULE']}.APPMAP={settings.APPMAP}",
168+
True,
169+
]
170+
elif (
171+
settings.APPMAP == False
172+
or str(settings.APPMAP).upper() == "false".upper()
173+
):
174+
return [
175+
f"{os.environ['DJANGO_SETTINGS_MODULE']}.APPMAP={settings.APPMAP}",
176+
False,
177+
]
178+
except AttributeError as exn:
179+
# it wasn't set. it's ok. don't crash
180+
# AttributeError: module 'app.settings_appmap_false' has no attribute 'APPMAP'
181+
pass
182+
183+
return ["it's not Django", None]

appmap/_implementation/testing_framework.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Shared infrastructure for testing framework integration."""
22

3+
import os
34
import re
45
from contextlib import contextmanager
56

@@ -104,10 +105,6 @@ def __init__(self, name, recorder_type, version=None):
104105

105106
@contextmanager
106107
def record(self, klass, method, **kwds):
107-
if not env.Env.current.enabled:
108-
yield
109-
return
110-
111108
Metadata.add_framework(self.name, self.version)
112109

113110
item = FuncItem(klass, method, **kwds)
@@ -147,3 +144,9 @@ def collect_result_metadata(metadata):
147144
metadata["test_status"] = "failed"
148145
metadata["exception"] = {"class": exn.__class__.__name__, "message": str(exn)}
149146
raise
147+
148+
def file_delete(filename):
149+
try:
150+
os.remove(filename)
151+
except FileNotFoundError:
152+
pass

appmap/_implementation/web_framework.py

+55-60
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from tempfile import NamedTemporaryFile
1111

1212
from appmap._implementation import generation
13+
from appmap._implementation.detect_enabled import DetectEnabled
1314
from appmap._implementation.env import Env
1415
from appmap._implementation.event import Event, ReturnEvent, _EventIds, describe_value
1516
from appmap._implementation.recording import Recorder
@@ -130,35 +131,35 @@ def create_appmap_file(
130131

131132

132133
class AppmapMiddleware:
133-
def before_request_hook(
134-
self, request, request_path, record_url, recording_is_running
135-
):
136-
if request_path == record_url:
134+
def __init__(self):
135+
self.record_url = "/_appmap/record"
136+
137+
def should_record(self):
138+
return DetectEnabled.should_enable("remote") or DetectEnabled.should_enable("requests")
139+
140+
def before_request_hook(self, request, request_path, recording_is_running):
141+
if request_path == self.record_url:
137142
return None, None, None
138143

144+
rec = None
139145
start = None
140146
call_event_id = None
141-
if Env.current.enabled or recording_is_running:
142-
# It should be recording or it's currently recording. The
143-
# recording is either
144-
# a) remote, enabled by POST to /_appmap/record, which set
145-
# recording_is_running, or
146-
# b) requests, set by Env.current.record_all_requests, or
147-
# c) both remote and requests; there are multiple active recorders.
148-
if not Env.current.record_all_requests and recording_is_running:
149-
# a)
150-
rec = Recorder()
151-
else:
152-
# b) or c)
153-
rec = Recorder(_EventIds.get_thread_id())
154-
rec.start_recording()
155-
# Each time an event is added for a thread_id it's also
156-
# added to the global Recorder(). So don't add the event
157-
# to the global Recorder() explicitly because that would
158-
# add the event in it twice.
159-
160-
if rec.enabled:
161-
start, call_event_id = self.before_request_main(rec, request)
147+
if DetectEnabled.should_enable("requests"):
148+
# a) requests
149+
rec = Recorder(_EventIds.get_thread_id())
150+
rec.start_recording()
151+
# Each time an event is added for a thread_id it's also
152+
# added to the global Recorder(). So don't add the event
153+
# to the global Recorder() explicitly because that would
154+
# add the event in it twice.
155+
elif DetectEnabled.should_enable("remote") or recording_is_running:
156+
# b) APPMAP=true, or
157+
# c) remote, enabled by POST to /_appmap/record, which set
158+
# recording_is_running
159+
rec = Recorder()
160+
161+
if rec and rec.enabled:
162+
start, call_event_id = self.before_request_main(rec, request)
162163

163164
return rec, start, call_event_id
164165

@@ -170,7 +171,6 @@ def after_request_hook(
170171
self,
171172
request,
172173
request_path,
173-
record_url,
174174
recording_is_running,
175175
request_method,
176176
request_base_url,
@@ -179,44 +179,39 @@ def after_request_hook(
179179
start,
180180
call_event_id,
181181
):
182-
if request_path == record_url:
182+
if request_path == self.record_url:
183183
return response
184184

185-
if Env.current.enabled or recording_is_running:
186-
# It should be recording or it's currently recording. The
187-
# recording is either
188-
# a) remote, enabled by POST to /_appmap/record, which set
189-
# self.recording.is_running, or
190-
# b) requests, set by Env.current.record_all_requests, or
191-
# c) both remote and requests; there are multiple active recorders.
192-
if not Env.current.record_all_requests and recording_is_running:
193-
# a)
194-
rec = Recorder()
185+
if DetectEnabled.should_enable("requests"):
186+
# a) requests
187+
rec = Recorder(_EventIds.get_thread_id())
188+
# Each time an event is added for a thread_id it's also
189+
# added to the global Recorder(). So don't add the event
190+
# to the global Recorder() explicitly because that would
191+
# add the event in it twice.
192+
try:
195193
if rec.enabled:
196194
self.after_request_main(rec, response, start, call_event_id)
197-
else:
198-
# b) or c)
199-
rec = Recorder(_EventIds.get_thread_id())
200-
# Each time an event is added for a thread_id it's also
201-
# added to the global Recorder(). So don't add the event
202-
# to the global Recorder() explicitly because that would
203-
# add the event in it twice.
204-
try:
205-
if rec.enabled:
206-
self.after_request_main(rec, response, start, call_event_id)
207-
208-
output_dir = Env.current.output_dir / "requests"
209-
create_appmap_file(
210-
output_dir,
211-
request_method,
212-
request_path,
213-
request_base_url,
214-
response,
215-
response_headers,
216-
rec,
217-
)
218-
finally:
219-
rec.stop_recording()
195+
196+
output_dir = Env.current.output_dir / "requests"
197+
create_appmap_file(
198+
output_dir,
199+
request_method,
200+
request_path,
201+
request_base_url,
202+
response,
203+
response_headers,
204+
rec,
205+
)
206+
finally:
207+
rec.stop_recording()
208+
elif DetectEnabled.should_enable("remote") or recording_is_running:
209+
# b) APPMAP=true, or
210+
# c) remote, enabled by POST to /_appmap/record, which set
211+
# recording_is_running
212+
rec = Recorder()
213+
if rec.enabled:
214+
self.after_request_main(rec, response, start, call_event_id)
220215

221216
return response
222217

0 commit comments

Comments
 (0)