Skip to content

Commit 3168889

Browse files
committed
feat: Record by default
1 parent 5858681 commit 3168889

12 files changed

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

appmap/_implementation/testing_framework.py

+14-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,16 @@ def collect_result_metadata(metadata):
147144
metadata["test_status"] = "failed"
148145
metadata["exception"] = {"class": exn.__class__.__name__, "message": str(exn)}
149146
raise
147+
148+
149+
def file_write(filename, contents):
150+
f = open(filename, "w")
151+
f.write(contents)
152+
f.close()
153+
154+
155+
def file_delete(filename):
156+
try:
157+
os.remove(filename)
158+
except FileNotFoundError:
159+
pass

appmap/_implementation/web_framework.py

+53-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,33 @@ 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+
DetectEnabled.initialize()
137+
138+
def before_request_hook(self, request, request_path, recording_is_running):
139+
if request_path == self.record_url:
137140
return None, None, None
138141

142+
rec = None
139143
start = None
140144
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)
145+
if DetectEnabled.should_enable("requests"):
146+
# a) requests
147+
rec = Recorder(_EventIds.get_thread_id())
148+
rec.start_recording()
149+
# Each time an event is added for a thread_id it's also
150+
# added to the global Recorder(). So don't add the event
151+
# to the global Recorder() explicitly because that would
152+
# add the event in it twice.
153+
elif DetectEnabled.should_enable("remote") or recording_is_running:
154+
# b) APPMAP=true, or
155+
# c) remote, enabled by POST to /_appmap/record, which set
156+
# recording_is_running
157+
rec = Recorder()
158+
159+
if rec and rec.enabled:
160+
start, call_event_id = self.before_request_main(rec, request)
162161

163162
return rec, start, call_event_id
164163

@@ -170,7 +169,6 @@ def after_request_hook(
170169
self,
171170
request,
172171
request_path,
173-
record_url,
174172
recording_is_running,
175173
request_method,
176174
request_base_url,
@@ -179,44 +177,39 @@ def after_request_hook(
179177
start,
180178
call_event_id,
181179
):
182-
if request_path == record_url:
180+
if request_path == self.record_url:
183181
return response
184182

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()
183+
if DetectEnabled.should_enable("requests"):
184+
# a) requests
185+
rec = Recorder(_EventIds.get_thread_id())
186+
# Each time an event is added for a thread_id it's also
187+
# added to the global Recorder(). So don't add the event
188+
# to the global Recorder() explicitly because that would
189+
# add the event in it twice.
190+
try:
195191
if rec.enabled:
196192
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()
193+
194+
output_dir = Env.current.output_dir / "requests"
195+
create_appmap_file(
196+
output_dir,
197+
request_method,
198+
request_path,
199+
request_base_url,
200+
response,
201+
response_headers,
202+
rec,
203+
)
204+
finally:
205+
rec.stop_recording()
206+
elif DetectEnabled.should_enable("remote") or recording_is_running:
207+
# b) APPMAP=true, or
208+
# c) remote, enabled by POST to /_appmap/record, which set
209+
# recording_is_running
210+
rec = Recorder()
211+
if rec.enabled:
212+
self.after_request_main(rec, response, start, call_event_id)
220213

221214
return response
222215

0 commit comments

Comments
 (0)