Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,17 @@ To trace Django requests, the agent uses a middleware, `elasticapm.contrib.djang
By default, this middleware is inserted automatically as the first item in `settings.MIDDLEWARES`.
To disable the automatic insertion of the middleware, change this setting to `False`.

[float]
[[config-django-commands-exclude]]
==== `django_commands_exclude`
|============
| Environment | Django | Default
| `ELASTIC_APM_DJANGO_COMMANDS_EXCLUDE` | `DJANGO_COMMANDS_EXCLUDE` | `runserver*,migrate,createsuperuser,\*shell*,testserver`
|============

By default, Elastic APM instruments Django management commands.
You can supply a list of commands that should not be instrumented.
To disable instrumenting of management commands, set it to `*`.
Comment on lines +753 to +763
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you think about making this a whitelist (with globbing included)? That would also make this much safer. (More thoughts in the review body)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A whitelist would definitely be the safest method, but it creates a lot of work for the user. Large Django projects can easily have dozens of management commands, and gain new ones if new 3rd party Django apps are added to the project. Keeping the white list up-to-date could be tedious work.

Another draw-back of a whitelist is that we can't provide a blacklist of commands that we should absolutely not instrument in code.

How about adding a second option, which is a flag, and disable it by default? Add some specific documentation in the Django docs, and include the NOTE there. That way, people will see the note when they read up on how to enable management command instrumentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable to me. 👍


[float]
[[config-generic-environment]]
Expand Down
11 changes: 10 additions & 1 deletion docs/django.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ In order to collect performance metrics,
the agent automatically inserts a middleware at the top of your middleware list
(`settings.MIDDLEWARE` in current versions of Django, `settings.MIDDLEWARE_CLASSES` in some older versions).
To disable the automatic insertion of the middleware,
see <<config-django-autoinsert-middleware,django_autoinsert_middleware>>.
see <<config-django-autoinsert-middleware,`django_autoinsert_middleware`>>.

NOTE: For automatic insertion to work,
your list of middlewares (`settings.MIDDLEWARE` or `settings.MIDDLEWARE_CLASSES`) must be of type `list` or `tuple`.
Expand All @@ -93,6 +93,15 @@ the agent also collects fine grained metrics on template rendering,
database queries, HTTP requests, etc.
You can find more information on what we instrument in the <<automatic-instrumentation, Automatic Instrumentation>> section.

Lastly, the agent will also collect performance data for Django management commands.
You can disable instrumentation for certain commands using the
<<config-django-commands-exclude,`django_commands_exclude`>> setting.
Transactions for management commands can be accessed in the APM app in Kibana by choosing `django_command` in the "transaction type" filter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APM app 😍


NOTE: The agent collects command line arguments as additional metadata for transactions.
If you run a command that contains sensitive data on the command line, like tokens or passwords,
we recommend to exclude that command from instrumentation.

[float]
[[django-instrumenting-custom-python-code]]
===== Instrumenting custom Python code
Expand Down
5 changes: 5 additions & 0 deletions elasticapm/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ class Config(_ConfigBase):
capture_body = _ConfigValue("CAPTURE_BODY", default="off")
async_mode = _BoolConfigValue("ASYNC_MODE", default=True)
instrument_django_middleware = _BoolConfigValue("INSTRUMENT_DJANGO_MIDDLEWARE", default=True)
django_commands_exclude = _ListConfigValue(
"DJANGO_COMMANDS_EXCLUDE",
type=starmatch_to_regex,
default=list(map(starmatch_to_regex, ["runserver*", "migrate", "createsuperuser", "*shell*", "testserver"])),
)
autoinsert_django_middleware = _BoolConfigValue("AUTOINSERT_DJANGO_MIDDLEWARE", default=True)
transactions_ignore_patterns = _ListConfigValue("TRANSACTIONS_IGNORE_PATTERNS", default=[])
service_version = _ConfigValue("SERVICE_VERSION")
Expand Down
61 changes: 61 additions & 0 deletions elasticapm/instrumentation/packages/django/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import sys

from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
from elasticapm.utils import compat


class DjangoCommandInstrumentation(AbstractInstrumentedModule):
name = "django_command"

instrument_list = [("django.core.management", "BaseCommand.execute")]

def call_if_sampling(self, module, method, wrapped, instance, args, kwargs):
from django.apps import apps # import at top level fails if Django is not installed

app = apps.get_app_config("elasticapm.contrib.django")
client = getattr(app, "client", None)
full_name = compat.text_type(instance.__module__)
name = full_name.rsplit(".", 1)[-1]
if not client or any(pattern.match(name) for pattern in client.config.django_commands_exclude):
return wrapped(*args, **kwargs)

transaction = client.begin_transaction("django_command")
transaction.is_sampled = True # always sample transactions
status = "ok"
try:
return wrapped(*args, **kwargs)
except Exception:
status = "failed"
client.capture_exception()
compat.reraise(*sys.exc_info())
finally:
client.end_transaction(full_name, status)
1 change: 1 addition & 0 deletions elasticapm/instrumentation/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"elasticapm.instrumentation.packages.pyodbc.PyODBCInstrumentation",
"elasticapm.instrumentation.packages.django.template.DjangoTemplateInstrumentation",
"elasticapm.instrumentation.packages.django.template.DjangoTemplateSourceInstrumentation",
"elasticapm.instrumentation.packages.django.commands.DjangoCommandInstrumentation",
"elasticapm.instrumentation.packages.urllib.UrllibInstrumentation",
}

Expand Down
32 changes: 31 additions & 1 deletion elasticapm/utils/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,26 @@ def iteritems(d, **kwargs):
def iterlists(d, **kw):
return d.iterlists(**kw)


def exec_(_code_, _globs_=None, _locs_=None):
"""Execute code in a namespace."""
if _globs_ is None:
frame = sys._getframe(1)
_globs_ = frame.f_globals
if _locs_ is None:
_locs_ = frame.f_locals
del frame
elif _locs_ is None:
_locs_ = _globs_
exec("""exec _code_ in _globs_, _locs_""")

exec_(
"""def reraise(tp, value, tb=None):
try:
raise tp, value, tb
finally:
tb = None
"""
)
else:
import io
import queue # noqa F401
Expand Down Expand Up @@ -140,6 +159,17 @@ def iteritems(d, **kwargs):
def iterlists(d, **kw):
return iter(d.lists(**kw))

def reraise(tp, value, tb=None):
try:
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
finally:
value = None
tb = None


def get_default_library_patters():
"""
Expand Down
109 changes: 109 additions & 0 deletions tests/contrib/django/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import pytest # isort:skip

django = pytest.importorskip("django") # isort:skip

from django.core.management import CommandError, call_command

import pytest

from elasticapm.conf import constants
from elasticapm.utils import compat


def test_management_command(django_elasticapm_client):
call_command("eapm_test_command")
transaction = django_elasticapm_client.events[constants.TRANSACTION][0]
assert transaction["type"] == "django_command"
assert transaction["name"] == "tests.contrib.django.testapp.management.commands.eapm_test_command"
assert transaction["result"] == "ok"

spans = django_elasticapm_client.spans_for_transaction(transaction)
assert len(spans) == 1
assert spans[0]["name"] == "yay"


def test_management_command_command_error(django_elasticapm_client):
with pytest.raises(CommandError):
call_command("eapm_test_command", explode="yes")
transaction = django_elasticapm_client.events[constants.TRANSACTION][0]
assert transaction["type"] == "django_command"
assert transaction["name"] == "tests.contrib.django.testapp.management.commands.eapm_test_command"
assert transaction["result"] == "failed"

exception = django_elasticapm_client.events[constants.ERROR][0]
assert exception["culprit"] == "tests.contrib.django.testapp.management.commands.eapm_test_command.handle"
assert exception["exception"]["message"] == "CommandError: oh no"
assert exception["transaction_id"] == transaction["id"]


def test_management_command_other_error(django_elasticapm_client):
with pytest.raises(ZeroDivisionError):
call_command("eapm_test_command", explode="yes, really")
transaction = django_elasticapm_client.events[constants.TRANSACTION][0]
assert transaction["type"] == "django_command"
assert transaction["name"] == "tests.contrib.django.testapp.management.commands.eapm_test_command"
assert transaction["result"] == "failed"

exception = django_elasticapm_client.events[constants.ERROR][0]
assert exception["culprit"] == "tests.contrib.django.testapp.management.commands.eapm_test_command.handle"
assert exception["exception"]["message"].startswith("ZeroDivisionError:")
assert exception["transaction_id"] == transaction["id"]


@pytest.mark.parametrize("django_elasticapm_client", [{"django_commands_exclude": "*"}], indirect=True)
def test_management_command_ignore_all(django_elasticapm_client):
call_command("eapm_test_command")
assert len(django_elasticapm_client.events[constants.TRANSACTION]) == 0


@pytest.mark.parametrize(
"django_elasticapm_client", [{"django_commands_exclude": "eapm_test_command,other_command"}], indirect=True
)
def test_management_command_ignore_exact(django_elasticapm_client):
call_command("eapm_test_command")
assert len(django_elasticapm_client.events[constants.TRANSACTION]) == 0


@pytest.mark.parametrize(
"django_elasticapm_client", [{"django_commands_exclude": "eapm_test_command,other_command"}], indirect=True
)
def test_management_command_ignore_exact(django_elasticapm_client):
call_command("eapm_test_command")
assert len(django_elasticapm_client.events[constants.TRANSACTION]) == 0


@pytest.mark.parametrize(
"django_elasticapm_client", [{"django_commands_exclude": "this_command,other_command"}], indirect=True
)
def test_management_command_ignore_no_match(django_elasticapm_client):
call_command("eapm_test_command")
assert len(django_elasticapm_client.events[constants.TRANSACTION]) == 1
29 changes: 29 additions & 0 deletions tests/contrib/django/testapp/management/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 changes: 29 additions & 0 deletions tests/contrib/django/testapp/management/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from django.core.management.base import BaseCommand, CommandError

from elasticapm import capture_span


class Command(BaseCommand):
help = "Just a test"

def add_arguments(self, parser):
parser.add_argument("--explode", default="no", action="store")

def handle(self, *args, **options):

with capture_span("yay"):
pass

if options["explode"] == "yes":
raise CommandError("oh no")

elif options["explode"] == "yes, really":
nan = 1 / 0