Skip to content

Commit dd78143

Browse files
authored
Merge branch 'main' into span_compression
2 parents d9c72f1 + 44eca82 commit dd78143

File tree

9 files changed

+125
-40
lines changed

9 files changed

+125
-40
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ endif::[]
3838
3939
* Add OpenTelemetry API bridge {pull}1411[#1411]
4040
* Change default for `sanitize_field_names` to sanitize `*auth*` instead of `authorization` {pull}1494[#1494]
41+
* Add `span_stack_trace_min_duration` to replace deprecated `span_frames_min_duration` {pull}1498[#1498]
4142
* Enable exact_match span compression by default {pull}1504[#1504]
4243
4344
[float]

docs/configuration.asciidoc

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,33 @@ while any positive integer value will be used as the maximum number of frames to
666666
To disable the limit and always capture all frames, set the value to `-1`.
667667

668668

669+
[float]
670+
[[config-span-stack-trace-min-duration]]
671+
==== `span_stack_trace_min_duration`
672+
673+
<<dynamic-configuration, image:./images/dynamic-config.svg[] >>
674+
675+
[options="header"]
676+
|============
677+
| Environment | Django/Flask | Default
678+
| `ELASTIC_APM_SPAN_STACK_TRACE_MIN_DURATION` | `SPAN_STACK_TRACE_MIN_DURATION` | `"5ms"`
679+
|============
680+
681+
By default, the APM agent collects a stack trace with every recorded span
682+
that has a duration equal to or longer than this configured threshold. While
683+
stack traces are very helpful to find the exact place in your code from which a
684+
span originates, collecting this stack trace does have some overhead. Tune this
685+
threshold to ensure that you only collect stack traces for spans that
686+
could be problematic.
687+
688+
To collect traces for all spans, regardless of their length, set the value to `0`.
689+
690+
To disable stack trace collection for spans completely, set the value to `-1`.
691+
692+
Except for the special values `-1` and `0`,
693+
this setting should be provided in *<<config-format-duration, duration format>>*.
694+
695+
669696
[float]
670697
[[config-span-frames-min-duration]]
671698
==== `span_frames_min_duration`
@@ -678,18 +705,9 @@ To disable the limit and always capture all frames, set the value to `-1`.
678705
| `ELASTIC_APM_SPAN_FRAMES_MIN_DURATION` | `SPAN_FRAMES_MIN_DURATION` | `"5ms"`
679706
|============
680707

681-
In its default settings, the APM agent will collect a stack trace with every recorded span.
682-
While this is very helpful to find the exact place in your code that causes the span,
683-
collecting this stack trace does have some overhead.
684-
685-
To collect traces for all spans, independent of the length, set the value to `-1`.
686-
Setting it to a positive value, e.g. `5ms`, will limit stack trace collection to spans
687-
with durations equal or longer than the given value.
708+
NOTE: This config value is being deprecated. Use
709+
<<config-span-stack-trace-min-duration,`span_stack_trace_min_duration`>> instead.
688710

689-
To disable stack trace collection for spans completely, set the value to `0`.
690-
691-
Except for the special values `-1` and `0`,
692-
this setting has to be provided in *<<config-format-duration, duration format>>*.
693711

694712
[float]
695713
[[config-span-compression-enabled]]

docs/tuning.asciidoc

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
== Performance tuning
33

44
Using an APM solution comes with certain trade-offs, and the Python agent for Elastic APM is no different.
5-
Instrumenting your code, measuring timings, recording context data, etc., all need resources:
5+
Instrumenting your code, measuring timings, recording context data, etc., all need resources:
66

77
* CPU time
88
* memory
@@ -67,24 +67,28 @@ In some cases, however, the number of spans can explode:
6767

6868
* long-running transactions
6969
* unoptimized code, e.g. doing hundreds of SQL queries in a loop
70-
70+
7171
To avoid these edge cases overloading both the agent and the APM Server,
7272
the agent stops recording spans when a specified limit is reached.
7373
You can configure this limit by changing the <<config-transaction-max-spans,`transaction_max_spans`>> setting.
7474

75-
Another option to reduce the overhead of collecting contextual data for spans is to disable collection for very short spans.
76-
While this contextual data (specifically, the stack trace) can be very useful to pinpoint where exactly the span is caused in your code,
77-
it is less interesting for very short spans.
78-
You can define a minimal threshold for span duration in milliseconds,
79-
using the <<config-span-frames-min-duration,`span_frames_min_duration`>> setting.
80-
If a span takes less than this duration, no stack frames will be collected for this span.
81-
Other contextual information, like the SQL query, will still be available.
75+
[float]
76+
[[tuning-span-stack-trace-collection]]
77+
=== Span Stack Trace Collection
78+
79+
Collecting stack traces for spans can be fairly costly from a performance standpoint.
80+
Stack traces are very useful for pinpointing which part of your code is generating a span;
81+
however, these stack traces are less useful for very short spans (as problematic spans tend to be longer).
82+
83+
You can define a minimal threshold for span duration
84+
using the <<config-span-stack-trace-min-duration,`span_stack_trace_min_duration`>> setting.
85+
If a span's duration is less than this config value, no stack frames will be collected for this span.
8286

8387
[float]
8488
[[tuning-frame-context]]
8589
=== Collecting Frame Context
8690

87-
When a stack trace is captured, the agent will also capture several lines of source code around each frame location in the stack trace. This allows the APM app to give greater insight into where exactly the error or span happens.
91+
When a stack trace is captured, the agent will also capture several lines of source code around each frame location in the stack trace. This allows the APM app to give greater insight into where exactly the error or span happens.
8892

8993
There are four settings you can modify to control this behavior:
9094

@@ -93,7 +97,7 @@ There are four settings you can modify to control this behavior:
9397
* <<config-source-lines-span-app-frames,`source_lines_span_app_frames`>>
9498
* <<config-source-lines-span-library-frames,`source_lines_span_library_frames`>>
9599

96-
As you can see, these settings are divided between app frames, which represent your application code, and library frames, which represent the code of your dependencies. Each of these categories are also split into separate error and span settings.
100+
As you can see, these settings are divided between app frames, which represent your application code, and library frames, which represent the code of your dependencies. Each of these categories are also split into separate error and span settings.
97101

98102
Reading source files inside a running application can cause a lot of disk I/O, and sending up source lines for each frame will have a network and storage cost that is quite high. Turning down these limits will help prevent excessive memory usage.
99103

elasticapm/conf/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,9 @@ class Config(_ConfigBase):
608608
span_frames_min_duration = _DurationConfigValue(
609609
"SPAN_FRAMES_MIN_DURATION", default=timedelta(seconds=0.005), unitless_factor=0.001
610610
)
611+
span_stack_trace_min_duration = _DurationConfigValue(
612+
"SPAN_STACK_TRACE_MIN_DURATION", default=timedelta(seconds=0.005), unitless_factor=0.001
613+
)
611614
span_compression_enabled = _BoolConfigValue("SPAN_COMPRESSION_ENABLED", default=True)
612615
span_compression_exact_match_max_duration = _DurationConfigValue(
613616
"SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION",

elasticapm/traces.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import threading
3535
import time
3636
import timeit
37+
import warnings
3738
from collections import defaultdict
3839
from datetime import timedelta
3940
from types import TracebackType
@@ -639,7 +640,11 @@ def end(self, skip_frames: int = 0, duration: Optional[float] = None):
639640
self.autofill_resource_context()
640641
super().end(skip_frames, duration)
641642
tracer = self.transaction.tracer
642-
if not tracer.span_frames_min_duration or self.duration >= tracer.span_frames_min_duration and self.frames:
643+
if (
644+
tracer.span_stack_trace_min_duration >= timedelta(seconds=0)
645+
and self.duration >= tracer.span_stack_trace_min_duration
646+
and self.frames
647+
):
643648
self.frames = tracer.frames_processing_func(self.frames)[skip_frames:]
644649
else:
645650
self.frames = None
@@ -821,11 +826,25 @@ def __init__(self, frames_collector_func, frames_processing_func, queue_func, co
821826
self._ignore_patterns = [re.compile(p) for p in config.transactions_ignore_patterns or []]
822827

823828
@property
824-
def span_frames_min_duration(self) -> Optional[timedelta]:
825-
if self.config.span_frames_min_duration in (timedelta(seconds=-1), None):
826-
return None
829+
def span_stack_trace_min_duration(self) -> timedelta:
830+
if self.config.span_stack_trace_min_duration != timedelta(
831+
seconds=0.005
832+
) or self.config.span_frames_min_duration == timedelta(seconds=0.005):
833+
# No need to check span_frames_min_duration
834+
return self.config.span_stack_trace_min_duration
827835
else:
828-
return self.config.span_frames_min_duration
836+
# span_stack_trace_min_duration is default value and span_frames_min_duration is non-default.
837+
# warn and use span_frames_min_duration
838+
warnings.warn(
839+
"`span_frames_min_duration` is deprecated. Please use `span_stack_trace_min_duration`.",
840+
DeprecationWarning,
841+
)
842+
if self.config.span_frames_min_duration < timedelta(seconds=0):
843+
return timedelta(seconds=0)
844+
elif self.config.span_frames_min_duration == timedelta(seconds=0):
845+
return timedelta(seconds=-1)
846+
else:
847+
return self.config.span_frames_min_duration
829848

830849
def begin_transaction(self, transaction_type, trace_parent=None, start=None, auto_activate=True):
831850
"""

tests/client/transaction_tests.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ def test_transaction_max_spans_dynamic(elasticapm_client):
223223
assert len(spans) == 3
224224

225225

226-
@pytest.mark.parametrize("elasticapm_client", [{"span_frames_min_duration": 20}], indirect=True)
227-
def test_transaction_span_frames_min_duration(elasticapm_client):
226+
@pytest.mark.parametrize("elasticapm_client", [{"span_stack_trace_min_duration": 20}], indirect=True)
227+
def test_transaction_span_stack_trace_min_duration(elasticapm_client):
228228
elasticapm_client.begin_transaction("test_type")
229229
with elasticapm.capture_span("noframes", duration=0.001):
230230
pass
@@ -242,8 +242,8 @@ def test_transaction_span_frames_min_duration(elasticapm_client):
242242
assert spans[1]["stacktrace"] is not None
243243

244244

245-
@pytest.mark.parametrize("elasticapm_client", [{"span_frames_min_durarion_ms": -1}], indirect=True)
246-
def test_transaction_span_frames_min_duration_no_limit(elasticapm_client):
245+
@pytest.mark.parametrize("elasticapm_client", [{"span_stack_trace_min_duration": 0}], indirect=True)
246+
def test_transaction_span_stack_trace_min_duration_no_limit(elasticapm_client):
247247
elasticapm_client.begin_transaction("test_type")
248248
with elasticapm.capture_span("frames"):
249249
pass
@@ -261,8 +261,8 @@ def test_transaction_span_frames_min_duration_no_limit(elasticapm_client):
261261
assert spans[1]["stacktrace"] is not None
262262

263263

264-
def test_transaction_span_frames_min_duration_dynamic(elasticapm_client):
265-
elasticapm_client.config.update(version="1", span_frames_min_duration=20)
264+
def test_transaction_span_stack_trace_min_duration_dynamic(elasticapm_client):
265+
elasticapm_client.config.update(version="1", span_stack_trace_min_duration=20)
266266
elasticapm_client.begin_transaction("test_type")
267267
with elasticapm.capture_span("noframes", duration=0.001):
268268
pass
@@ -279,7 +279,7 @@ def test_transaction_span_frames_min_duration_dynamic(elasticapm_client):
279279
assert spans[1]["name"] == "frames"
280280
assert spans[1]["stacktrace"] is not None
281281

282-
elasticapm_client.config.update(version="1", span_frames_min_duration=-1)
282+
elasticapm_client.config.update(version="1", span_stack_trace_min_duration=0)
283283
elasticapm_client.begin_transaction("test_type")
284284
with elasticapm.capture_span("frames"):
285285
pass
@@ -297,6 +297,46 @@ def test_transaction_span_frames_min_duration_dynamic(elasticapm_client):
297297
assert spans[3]["stacktrace"] is not None
298298

299299

300+
def test_transaction_span_stack_trace_min_duration_overrides_old_config(elasticapm_client):
301+
"""
302+
span_stack_trace_min_duration overrides span_frames_min_duration (which is deprecated)
303+
"""
304+
elasticapm_client.config.update(version="1", span_stack_trace_min_duration=20, span_frames_min_duration=1)
305+
elasticapm_client.begin_transaction("test_type")
306+
with elasticapm.capture_span("noframes", duration=0.01):
307+
pass
308+
with elasticapm.capture_span("frames", duration=0.04):
309+
pass
310+
elasticapm_client.end_transaction("test")
311+
312+
spans = elasticapm_client.events[constants.SPAN]
313+
314+
assert len(spans) == 2
315+
assert spans[0]["name"] == "noframes"
316+
assert "stacktrace" not in spans[0]
317+
318+
assert spans[1]["name"] == "frames"
319+
assert spans[1]["stacktrace"] is not None
320+
321+
# Set span_stack_trace_min_duration to default so it picks up the non-default span_frames_min_duration
322+
elasticapm_client.config.update(version="1", span_stack_trace_min_duration=5, span_frames_min_duration=1)
323+
elasticapm_client.begin_transaction("test_type")
324+
with elasticapm.capture_span("yesframes", duration=0.01):
325+
pass
326+
with elasticapm.capture_span("frames", duration=0.04):
327+
pass
328+
elasticapm_client.end_transaction("test")
329+
330+
spans = elasticapm_client.events[constants.SPAN]
331+
332+
assert len(spans) == 4
333+
assert spans[2]["name"] == "yesframes"
334+
assert spans[2]["stacktrace"] is not None
335+
336+
assert spans[3]["name"] == "frames"
337+
assert spans[3]["stacktrace"] is not None
338+
339+
300340
def test_transaction_keyword_truncation(elasticapm_client):
301341
too_long = "x" * (constants.KEYWORD_MAX_LENGTH + 1)
302342
expected = encoding.keyword_field(too_long)

tests/contrib/django/fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def django_elasticapm_client(request):
5656
client_config = getattr(request, "param", {})
5757
client_config.setdefault("service_name", "app")
5858
client_config.setdefault("secret_token", "secret")
59-
client_config.setdefault("span_frames_min_duration", -1)
59+
client_config.setdefault("span_stack_trace_min_duration", 0)
6060
client_config.setdefault("span_compression_exact_match_max_duration", "0ms")
6161
client_config.setdefault("span_compression_same_kind_max_duration", "0ms")
6262
app = apps.get_app_config("elasticapm")
@@ -85,7 +85,7 @@ def django_sending_elasticapm_client(request, validating_httpserver):
8585
client_config.setdefault("service_name", "app")
8686
client_config.setdefault("secret_token", "secret")
8787
client_config.setdefault("transport_class", "elasticapm.transport.http.Transport")
88-
client_config.setdefault("span_frames_min_duration", -1)
88+
client_config.setdefault("span_stack_trace_min_duration", 0)
8989
client_config.setdefault("span_compression_exact_match_max_duration", "0ms")
9090
client_config.setdefault("span_compression_same_kind_max_duration", "0ms")
9191
client_config.setdefault("exit_span_min_duration", "0ms")

tests/fixtures.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def elasticapm_client(request):
186186
client_config.setdefault("secret_token", "test_key")
187187
client_config.setdefault("central_config", "false")
188188
client_config.setdefault("include_paths", ("*/tests/*",))
189-
client_config.setdefault("span_frames_min_duration", -1)
189+
client_config.setdefault("span_stack_trace_min_duration", 0)
190190
client_config.setdefault("metrics_interval", "0ms")
191191
client_config.setdefault("cloud_provider", False)
192192
client_config.setdefault("span_compression_exact_match_max_duration", "0ms")
@@ -220,7 +220,7 @@ def elasticapm_client_log_file(request):
220220
client_config.setdefault("secret_token", "test_key")
221221
client_config.setdefault("central_config", "false")
222222
client_config.setdefault("include_paths", ("*/tests/*",))
223-
client_config.setdefault("span_frames_min_duration", -1)
223+
client_config.setdefault("span_stack_trace_min_duration", 0)
224224
client_config.setdefault("span_compression_exact_match_max_duration", "0ms")
225225
client_config.setdefault("span_compression_same_kind_max_duration", "0ms")
226226
client_config.setdefault("metrics_interval", "0ms")
@@ -303,7 +303,7 @@ def sending_elasticapm_client(request, validating_httpserver):
303303
client_config.setdefault("service_name", "myapp")
304304
client_config.setdefault("secret_token", "test_key")
305305
client_config.setdefault("transport_class", "elasticapm.transport.http.Transport")
306-
client_config.setdefault("span_frames_min_duration", -1)
306+
client_config.setdefault("span_stack_trace_min_duration", 0)
307307
client_config.setdefault("span_compression_exact_match_max_duration", "0ms")
308308
client_config.setdefault("span_compression_same_kind_max_duration", "0ms")
309309
client_config.setdefault("include_paths", ("*/tests/*",))

tests/utils/stacks/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def test_iter_stack_frames_max_frames():
144144

145145

146146
@pytest.mark.parametrize(
147-
"elasticapm_client", [{"stack_trace_limit": 10, "span_frames_min_duration": -1}], indirect=True
147+
"elasticapm_client", [{"stack_trace_limit": 10, "span_stack_trace_min_duration": 0}], indirect=True
148148
)
149149
def test_iter_stack_frames_max_frames_is_dynamic(elasticapm_client):
150150
def func():

0 commit comments

Comments
 (0)