elasticapm/contrib/django/management/commands/elasticapm.py (233 lines of code) (raw):

# 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 __future__ import absolute_import import logging import sys import urllib.parse from django.conf import settings from django.core.management.base import BaseCommand from django.core.management.color import color_style from django.utils import termcolors from elasticapm.contrib.django.client import DjangoClient try: from django.core.management.base import OutputWrapper except ImportError: OutputWrapper = None blue = termcolors.make_style(opts=("bold",), fg="blue") cyan = termcolors.make_style(opts=("bold",), fg="cyan") green = termcolors.make_style(fg="green") magenta = termcolors.make_style(opts=("bold",), fg="magenta") red = termcolors.make_style(opts=("bold",), fg="red") white = termcolors.make_style(opts=("bold",), fg="white") yellow = termcolors.make_style(opts=("bold",), fg="yellow") class TestException(Exception): pass class ColoredLogger(object): def __init__(self, stream) -> None: self.stream = stream self.errors = [] self.color = color_style() def log(self, level, *args, **kwargs) -> None: style = kwargs.pop("style", self.color.NOTICE) msg = " ".join((level.upper(), args[0] % args[1:], "\n")) if OutputWrapper is None: self.stream.write(msg) else: self.stream.write(msg, style_func=style) def error(self, *args, **kwargs) -> None: kwargs["style"] = red self.log("error", *args, **kwargs) self.errors.append((args,)) def warning(self, *args, **kwargs) -> None: kwargs["style"] = yellow self.log("warning", *args, **kwargs) def info(self, *args, **kwargs) -> None: kwargs["style"] = green self.log("info", *args, **kwargs) CONFIG_EXAMPLE = """ You can set it in your settings file: ELASTIC_APM = { 'SERVICE_NAME': '<YOUR-SERVICE-NAME>', 'SECRET_TOKEN': '<YOUR-SECRET-TOKEN>', } or with environment variables: $ export ELASTIC_APM_SERVICE_NAME="<YOUR-SERVICE-NAME>" $ export ELASTIC_APM_SECRET_TOKEN="<YOUR-SECRET-TOKEN>" $ python manage.py elasticapm check """ class Command(BaseCommand): arguments = ( (("-s", "--service-name"), {"default": None, "dest": "service_name", "help": "Specifies the service name."}), (("-t", "--token"), {"default": None, "dest": "secret_token", "help": "Specifies the secret token."}), ) args = "test check" def add_arguments(self, parser) -> None: parser.add_argument("subcommand") for args, kwargs in self.arguments: parser.add_argument(*args, **kwargs) def handle(self, *args, **options): if "subcommand" in options: subcommand = options["subcommand"] else: return self.handle_command_not_found("No command specified.") if subcommand not in self.dispatch: self.handle_command_not_found('No such command "%s".' % subcommand) else: self.dispatch.get(subcommand, self.handle_command_not_found)(self, subcommand, **options) def handle_test(self, command, **options): """Send a test error to APM Server""" # can't be async for testing class LogCaptureHandler(logging.Handler): def __init__(self, level=logging.NOTSET) -> None: self.logs = [] super(LogCaptureHandler, self).__init__(level) def handle(self, record) -> None: self.logs.append(record) handler = LogCaptureHandler() logger = logging.getLogger("elasticapm.transport") logger.addHandler(handler) config = {"async_mode": False} for key in ("service_name", "secret_token"): if options.get(key): config[key] = options[key] client = DjangoClient(**config) client.error_logger = ColoredLogger(self.stderr) client.logger = ColoredLogger(self.stderr) self.write( "Trying to send a test error to APM Server using these settings:\n\n" "SERVICE_NAME:\t%s\n" "SECRET_TOKEN:\t%s\n" "SERVER:\t\t%s\n\n" % (client.config.service_name, client.config.secret_token, client.config.server_url) ) try: raise TestException("Hi there!") except TestException: client.capture_exception() client.close() if not handler.logs: self.write( "Success! We tracked the error successfully! \n" "You should see it in the APM app in Kibana momentarily. \n" 'Look for "TestException: Hi there!" in the Errors tab of the %s app' % client.config.service_name ) else: self.write("Oops. That didn't work. The following error occured: \n\n", red) for entry in handler.logs: self.write(entry.getMessage(), red) def handle_check(self, command, **options): """Check your settings for common misconfigurations""" passed = True client = DjangoClient(metrics_interval="0ms") if not client.config.enabled: return True def is_set(x): return x and x != "None" # check if org/app is set: if is_set(client.config.service_name): self.write("Service name is set, good job!", green) else: passed = False self.write("Configuration errors detected!", red, ending="\n\n") self.write(" * SERVICE_NAME not set! ", red, ending="\n") self.write(CONFIG_EXAMPLE) # secret token is optional but recommended if not is_set(client.config.secret_token): self.write(" * optional SECRET_TOKEN not set", yellow, ending="\n") self.write("") server_url = client.config.server_url if server_url: parsed_url = urllib.parse.urlparse(server_url) if parsed_url.scheme.lower() in ("http", "https"): # parse netloc, making sure people did not supply basic auth if "@" in parsed_url.netloc: credentials, _, path = parsed_url.netloc.rpartition("@") passed = False self.write("Configuration errors detected!", red, ending="\n\n") if ":" in credentials: self.write(" * SERVER_URL cannot contain authentication " "credentials", red, ending="\n") else: self.write( " * SERVER_URL contains an unexpected at-sign!" " This is usually used for basic authentication, " "but the colon is left out", red, ending="\n", ) else: self.write("SERVER_URL {0} looks fine".format(server_url), green) # secret token in the clear not recommended if is_set(client.config.secret_token) and parsed_url.scheme.lower() == "http": self.write(" * SECRET_TOKEN set but server not using https", yellow, ending="\n") else: self.write( " * SERVER_URL has scheme {0} and we require " "http or https!".format(parsed_url.scheme), red, ending="\n", ) passed = False else: self.write("Configuration errors detected!", red, ending="\n\n") self.write(" * SERVER_URL appears to be empty", red, ending="\n") passed = False self.write("") # check if we're disabled due to DEBUG: if settings.DEBUG: if getattr(settings, "ELASTIC_APM", {}).get("DEBUG"): self.write( "Note: even though you are running in DEBUG mode, we will " 'send data to the APM Server, because you set ELASTIC_APM["DEBUG"] to ' "True. You can disable ElasticAPM while in DEBUG mode like this" "\n\n", yellow, ) self.write( " ELASTIC_APM = {\n" ' "DEBUG": False,\n' " # your other ELASTIC_APM settings\n" " }" ) else: self.write( "Looks like you're running in DEBUG mode. ElasticAPM will NOT " "gather any data while DEBUG is set to True.\n\n", red, ) self.write( "If you want to test ElasticAPM while DEBUG is set to True, you" " can force ElasticAPM to gather data by setting" ' ELASTIC_APM["DEBUG"] to True, like this\n\n' " ELASTIC_APM = {\n" ' "DEBUG": True,\n' " # your other ELASTIC_APM settings\n" " }" ) passed = False else: self.write("DEBUG mode is disabled! Looking good!", green) self.write("") # check if middleware is set, and if it is at the first position middleware_attr = "MIDDLEWARE" if getattr(settings, "MIDDLEWARE", None) is not None else "MIDDLEWARE_CLASSES" middleware = list(getattr(settings, middleware_attr)) try: pos = middleware.index("elasticapm.contrib.django.middleware.TracingMiddleware") if pos == 0: self.write("Tracing middleware is configured! Awesome!", green) else: self.write("Tracing middleware is configured, but not at the first position\n", yellow) self.write("ElasticAPM works best if you add it at the top of your %s setting" % middleware_attr) except ValueError: self.write("Tracing middleware not configured!", red) self.write( "\n" "Add it to your %(name)s setting like this:\n\n" " %(name)s = (\n" ' "elasticapm.contrib.django.middleware.TracingMiddleware",\n' " # your other middleware classes\n" " )\n" % {"name": middleware_attr} ) self.write("") if passed: self.write("Looks like everything should be ready!", green) else: self.write("Please fix the above errors.", red) self.write("") client.close() return passed def handle_command_not_found(self, message) -> None: self.write(message, red, ending="") self.write(" Please use one of the following commands:\n\n", red) self.write("".join(" * %s\t%s\n" % (k.ljust(8), v.__doc__) for k, v in self.dispatch.items())) self.write("\n") argv = self._get_argv() self.write("Usage:\n\t%s elasticapm <command>" % (" ".join(argv[: argv.index("elasticapm")]))) def write(self, msg, style_func=None, ending=None, stream=None) -> None: """ wrapper around self.stdout/stderr to ensure Django 1.4 compatibility """ if stream is None: stream = self.stdout if OutputWrapper is None: ending = "\n" if ending is None else ending msg += ending stream.write(msg) else: stream.write(msg, style_func=style_func, ending=ending) def _get_argv(self): """allow cleaner mocking of sys.argv""" return sys.argv dispatch = {"test": handle_test, "check": handle_check}