Skip to content

Commit 717d39a

Browse files
authored
Merge pull request #176 from getappmap/sw/feat/record_by_default
feat: Record by default
2 parents 6583cd8 + fb73d80 commit 717d39a

14 files changed

+603
-168
lines changed

appmap/_implementation/__init__.py

Lines changed: 2 additions & 0 deletions
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, importer, metadata, recorder
4+
from .detect_enabled import DetectEnabled
45
from .py_version_check import check_py_version
56

67

@@ -12,6 +13,7 @@ def initialize(**kwargs):
1213
recorder.initialize()
1314
configuration.initialize() # needs to be initialized after recorder
1415
metadata.initialize()
16+
DetectEnabled.initialize()
1517

1618

1719
initialize()
Lines changed: 183 additions & 0 deletions
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

Lines changed: 7 additions & 4 deletions
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

@@ -99,10 +100,6 @@ def __init__(self, name, recorder_type, version=None):
99100

100101
@contextmanager
101102
def record(self, klass, method, **kwds):
102-
if not env.Env.current.enabled:
103-
yield
104-
return
105-
106103
Metadata.add_framework(self.name, self.version)
107104

108105
item = FuncItem(klass, method, **kwds)
@@ -142,3 +139,9 @@ def collect_result_metadata(metadata):
142139
metadata["test_status"] = "failed"
143140
metadata["exception"] = {"class": exn.__class__.__name__, "message": str(exn)}
144141
raise
142+
143+
def file_delete(filename):
144+
try:
145+
os.remove(filename)
146+
except FileNotFoundError:
147+
pass

appmap/_implementation/web_framework.py

Lines changed: 56 additions & 57 deletions
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.recorder import Recorder, ThreadRecorder
@@ -130,32 +131,36 @@ def create_appmap_file(
130131

131132

132133
class AppmapMiddleware(ABC):
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.get_current()
151-
else:
152-
# b) or c)
153-
rec = ThreadRecorder()
154-
Recorder.set_current(rec)
155-
rec.start_recording()
156-
157-
if rec.get_enabled():
158-
start, call_event_id = self.before_request_main(rec, request)
147+
if DetectEnabled.should_enable("requests"):
148+
# a) requests
149+
rec = ThreadRecorder()
150+
Recorder.set_current(rec)
151+
rec.start_recording()
152+
# Each time an event is added for a thread_id it's also
153+
# added to the global Recorder(). So don't add the event
154+
# to the global Recorder() explicitly because that would
155+
# add the event in it twice.
156+
elif DetectEnabled.should_enable("remote") or recording_is_running:
157+
# b) APPMAP=true, or
158+
# c) remote, enabled by POST to /_appmap/record, which set
159+
# recording_is_running
160+
rec = Recorder.get_current()
161+
162+
if rec and rec.get_enabled():
163+
start, call_event_id = self.before_request_main(rec, request)
159164

160165
return rec, start, call_event_id
161166

@@ -167,7 +172,6 @@ def after_request_hook(
167172
self,
168173
request,
169174
request_path,
170-
record_url,
171175
recording_is_running,
172176
request_method,
173177
request_base_url,
@@ -176,44 +180,39 @@ def after_request_hook(
176180
start,
177181
call_event_id,
178182
):
179-
if request_path == record_url:
183+
if request_path == self.record_url:
180184
return response
181185

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

218217
return response
219218

0 commit comments

Comments
 (0)