diff --git a/.travis.yml b/.travis.yml index 043f6f5..3fffe2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,13 @@ python: script: - python setup.py sdist - python setup.py test +- django_tests/all_tests.sh deploy: provider: pypi user: BogdanBe password: secure: Vy3FxFJuBkkZzqozmdT7l3eMNRvGfuDXXlBy3mCfiO3XP7eYId/MJ2ftnkEqYrNoObJlWTQ3vIQpZaO/lE6KIaHO5uT1T+9Fv7E2FLIoJ0+JOV+dy219LfpARojOA5Mf+tQyGffqUyQ2zFkYnFQ/Mo1Av7wPLKGzncbUmwbrGoE= + distributions: "sdist bdist_wheel" on: tags: true repo: Microsoft/AppInsights-Python diff --git a/README.md b/README.md index a7873c6..dd30631 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This project extends the Application Insights API surface to support Python. [Ap ## Requirements ## -Python 2.7 and Python 3.4 are currently supported by this module. +Python >=2.7 and Python >=3.4 are currently supported by this module. For opening the project in Microsoft Visual Studio you will need [Python Tools for Visual Studio](http://pytools.codeplex.com/). @@ -187,3 +187,109 @@ def hello_world(): if __name__ == '__main__': app.run() ``` + +**Integrating with Django** + +Place the following in your `settings.py` file: + +```python +# If on Django < 1.10 +MIDDLEWARE_CLASSES = [ + # ... or whatever is below for you ... + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # ... or whatever is above for you ... + 'applicationinsights.django.ApplicationInsightsMiddleware', # Add this middleware to the end +] + +# If on Django >= 1.10 +MIDDLEWARE = [ + # ... or whatever is below for you ... + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # ... or whatever is above for you ... + 'applicationinsights.django.ApplicationInsightsMiddleware', # Add this middleware to the end +] + +APPLICATION_INSIGHTS = { + # (required) Your Application Insights instrumentation key + 'ikey': "00000000-0000-0000-0000-000000000000", + + # (optional) By default, request names are logged as the request method + # and relative path of the URL. To log the fully-qualified view names + # instead, set this to True. Defaults to False. + 'use_view_name': True, + + # (optional) To log arguments passed into the views as custom properties, + # set this to True. Defaults to False. + 'record_view_arguments': True, + + # (optional) Exceptions are logged by default, to disable, set this to False. + 'log_exceptions': False, + + # (optional) Events are submitted to Application Insights asynchronously. + # send_interval specifies how often the queue is checked for items to submit. + # send_time specifies how long the sender waits for new input before recycling + # the background thread. + 'send_interval': 1.0, # Check every second + 'send_time': 3.0, # Wait up to 3 seconds for an event + + # (optional, uncommon) If you must send to an endpoint other than the + # default endpoint, specify it here: + 'endpoint': "https://dc.services.visualstudio.com/v2/track", +} +``` + +This will log all requests and exceptions to the instrumentation key +specified in the `APPLICATION_INSIGHTS` setting. In addition, an +`appinsights` property will be placed on each incoming `request` object in +your views. This will have the following properties: + +* `client`: This is an instance of the `applicationinsights.TelemetryClient` + type, which will submit telemetry to the same instrumentation key, and + will parent each telemetry item to the current request. +* `request`: This is the `applicationinsights.channel.contracts.RequestData` + instance for the current request. You can modify properties on this + object during the handling of the current request. It will be submitted + when the request has finished. +* `context`: This is the `applicationinsights.channel.TelemetryContext` + object for the current ApplicationInsights sender. + +You can also hook up logging to Django. For example, to log all builtin +Django warnings and errors, use the following logging configuration in +`settings.py`: + +```python +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + # The application insights handler is here + 'appinsights': { + 'class': 'applicationinsights.django.LoggingHandler', + 'level': 'WARNING' + } + }, + 'loggers': { + 'django': { + 'handlers': ['appinsights'], + 'level': 'WARNING', + 'propagate': True, + } + } +} +``` + +See Django's [logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) +for more information. diff --git a/applicationinsights/TelemetryClient.py b/applicationinsights/TelemetryClient.py index b9d02cd..227e574 100644 --- a/applicationinsights/TelemetryClient.py +++ b/applicationinsights/TelemetryClient.py @@ -168,17 +168,20 @@ class TelemetryClient(object): self._channel.write(data, self._context) - def track_trace(self, name, properties=None): + def track_trace(self, name, properties=None, severity=None): """Sends a single trace statement. Args: name (str). the trace statement.\n - properties (dict). the set of custom properties the client wants attached to this data item. (defaults to: None) + properties (dict). the set of custom properties the client wants attached to this data item. (defaults to: None)\n + severity (str). the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL """ data = channel.contracts.MessageData() data.message = name or NULL_CONSTANT_STRING if properties: data.properties = properties + if severity is not None: + data.severity_level = channel.contracts.MessageData.PYTHON_LOGGING_LEVELS.get(severity) self._channel.write(data, self._context) diff --git a/applicationinsights/channel/NullSender.py b/applicationinsights/channel/NullSender.py new file mode 100644 index 0000000..1d40307 --- /dev/null +++ b/applicationinsights/channel/NullSender.py @@ -0,0 +1,11 @@ +from .SenderBase import SenderBase + +class NullSender(SenderBase): + """A sender class that does not send data. Useful for debug mode, when + telemetry may not be desired, with no changes to the object model. + """ + def __init__(self, *args, **kwargs): + super(NullSender, self).__init__("nil-endpoint", *args, **kwargs) + + def send(self, data): + pass diff --git a/applicationinsights/channel/SenderBase.py b/applicationinsights/channel/SenderBase.py index c31116e..29f6462 100644 --- a/applicationinsights/channel/SenderBase.py +++ b/applicationinsights/channel/SenderBase.py @@ -25,6 +25,7 @@ class SenderBase(object): self._service_endpoint_uri = service_endpoint_uri self._queue = None self._send_buffer_size = 100 + self._timeout = 10 @property def service_endpoint_uri(self): @@ -63,6 +64,21 @@ class SenderBase(object): """ return self._queue + @property + def send_timeout(self): + """Time in seconds that the sender should wait before giving up.""" + return self._timeout + + @send_timeout.setter + def send_timeout(self, seconds): + """Configures the timeout in seconds the sender waits for a response for the server. + + Args: + seconds(float). Timeout in seconds. + """ + + self._send_buffer_size = seconds + @queue.setter def queue(self, value): """The queue that this sender is draining. While :class:`SenderBase` doesn't implement any means of doing @@ -115,7 +131,7 @@ class SenderBase(object): request = HTTPClient.Request(self._service_endpoint_uri, bytearray(request_payload, 'utf-8'), { 'Accept': 'application/json', 'Content-Type' : 'application/json; charset=utf-8' }) try: - response = HTTPClient.urlopen(request) + response = HTTPClient.urlopen(request, timeout=self._timeout) status_code = response.getcode() if 200 <= status_code < 300: return @@ -127,4 +143,4 @@ class SenderBase(object): # Add our unsent data back on to the queue for data in data_to_send: - self._queue.put(data) \ No newline at end of file + self._queue.put(data) diff --git a/applicationinsights/channel/TelemetryChannel.py b/applicationinsights/channel/TelemetryChannel.py index 3d86b36..4ad7dd3 100644 --- a/applicationinsights/channel/TelemetryChannel.py +++ b/applicationinsights/channel/TelemetryChannel.py @@ -101,9 +101,9 @@ class TelemetryChannel(object): if not properties: properties = {} data.properties = properties - for key, value in local_context.properties: + for key in local_context.properties: if key not in properties: - properties[key] = value + properties[key] = local_context.properties[key] envelope.data.base_data = data self._queue.put(envelope) diff --git a/applicationinsights/channel/__init__.py b/applicationinsights/channel/__init__.py index 9aaf9ac..889365c 100644 --- a/applicationinsights/channel/__init__.py +++ b/applicationinsights/channel/__init__.py @@ -6,4 +6,5 @@ from .SynchronousQueue import SynchronousQueue from .SynchronousSender import SynchronousSender from .TelemetryChannel import TelemetryChannel from .TelemetryContext import TelemetryContext -from . import contracts \ No newline at end of file +from .NullSender import NullSender +from . import contracts diff --git a/applicationinsights/channel/contracts/MessageData.py b/applicationinsights/channel/contracts/MessageData.py index 17cd4c7..c30c083 100644 --- a/applicationinsights/channel/contracts/MessageData.py +++ b/applicationinsights/channel/contracts/MessageData.py @@ -1,5 +1,6 @@ import collections import copy +import logging from .Utils import _write_complex_object class MessageData(object): @@ -9,6 +10,19 @@ class MessageData(object): DATA_TYPE_NAME = 'MessageData' + PYTHON_LOGGING_LEVELS = { + 'DEBUG': 0, + 'INFO': 1, + 'WARNING': 2, + 'ERROR': 3, + 'CRITICAL': 4, + logging.DEBUG: 0, + logging.INFO: 1, + logging.WARNING: 2, + logging.ERROR: 3, + logging.CRITICAL: 4 + } + _defaults = collections.OrderedDict([ ('ver', 2), ('message', None), diff --git a/applicationinsights/django/__init__.py b/applicationinsights/django/__init__.py new file mode 100644 index 0000000..3ce7cc4 --- /dev/null +++ b/applicationinsights/django/__init__.py @@ -0,0 +1,10 @@ +from .middleware import ApplicationInsightsMiddleware +from .logging import LoggingHandler +from . import common + +__all__ = ['ApplicationInsightsMiddleware', 'LoggingHandler', 'create_client'] + +def create_client(): + """Returns an :class:`applicationinsights.TelemetryClient` instance using the instrumentation key + and other settings found in the current Django project's `settings.py` file.""" + return common.create_client() diff --git a/applicationinsights/django/common.py b/applicationinsights/django/common.py new file mode 100644 index 0000000..7f945f2 --- /dev/null +++ b/applicationinsights/django/common.py @@ -0,0 +1,85 @@ +import collections + +from django.conf import settings +import applicationinsights + +ApplicationInsightsSettings = collections.namedtuple("ApplicationInsightsSettings", [ + "ikey", + "channel_settings", + "use_view_name", + "record_view_arguments", + "log_exceptions"]) + +ApplicationInsightsChannelSettings = collections.namedtuple("ApplicationInsightsChannelSettings", [ + "send_interval", + "send_time", + "endpoint"]) + +def load_settings(): + if hasattr(settings, "APPLICATION_INSIGHTS"): + config = settings.APPLICATION_INSIGHTS + elif hasattr(settings, "APPLICATIONINSIGHTS"): + config = settings.APPLICATIONINSIGHTS + else: + config = {} + + if not isinstance(config, dict): + config = {} + + return ApplicationInsightsSettings( + ikey=config.get("ikey"), + use_view_name=config.get("use_view_name", False), + record_view_arguments=config.get("record_view_arguments", False), + log_exceptions=config.get("log_exceptions", True), + channel_settings=ApplicationInsightsChannelSettings( + endpoint=config.get("endpoint"), + send_interval=config.get("send_interval"), + send_time=config.get("send_time"))) + +saved_clients = {} +saved_channels = {} + +def create_client(aisettings=None): + global saved_clients, saved_channels + + if aisettings is None: + aisettings = load_settings() + + if aisettings in saved_clients: + return saved_clients[aisettings] + + channel_settings = aisettings.channel_settings + + if channel_settings in saved_channels: + channel = saved_channels[channel_settings] + else: + if channel_settings.endpoint is not None: + sender = applicationinsights.channel.AsynchronousSender(service_endpoint_uri=channel_settings.endpoint) + else: + sender = applicationinsights.channel.AsynchronousSender() + + if channel_settings.send_time is not None: + sender.send_time = channel_settings.send_time + if channel_settings.send_interval is not None: + sender.send_interval = channel_settings.send_interval + + queue = applicationinsights.channel.AsynchronousQueue(sender) + channel = applicationinsights.channel.TelemetryChannel(None, queue) + saved_channels[channel_settings] = channel + + ikey = aisettings.ikey + if ikey is None: + return dummy_client("No ikey specified") + + client = applicationinsights.TelemetryClient(aisettings.ikey, channel) + saved_clients[aisettings] = client + return client + +def dummy_client(reason): + """Creates a dummy channel so even if we're not logging telemetry, we can still send + along the real object to things that depend on it to exist""" + + sender = applicationinsights.channel.NullSender() + queue = applicationinsights.channel.SynchronousQueue(sender) + channel = applicationinsights.channel.TelemetryChannel(None, queue) + return applicationinsights.TelemetryClient("00000000-0000-0000-0000-000000000000", channel) diff --git a/applicationinsights/django/logging.py b/applicationinsights/django/logging.py new file mode 100644 index 0000000..17baa00 --- /dev/null +++ b/applicationinsights/django/logging.py @@ -0,0 +1,45 @@ + +from . import common +from applicationinsights import logging + +import sys + +class LoggingHandler(logging.LoggingHandler): + """This class is a LoggingHandler that uses the same settings as the Django middleware to configure + the telemetry client. This can be referenced from LOGGING in your Django settings.py file. As an + example, this code would send all Django log messages--WARNING and up--to Application Insights: + + .. code:: python + + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + # The application insights handler is here + 'appinsights': { + 'class': 'applicationinsights.django.LoggingHandler', + 'level': 'WARNING' + } + }, + 'loggers': { + 'django': { + 'handlers': ['appinsights'], + 'level': 'WARNING', + 'propagate': True, + } + } + } + + # You will need this anyway if you're using the middleware. + # See the middleware documentation for more information on configuring + # this setting: + APPLICATION_INSIGHTS = { + 'ikey': '00000000-0000-0000-0000-000000000000' + } + """ + def __init__(self, *args, **kwargs): + client = common.create_client() + new_kwargs = {} + new_kwargs.update(kwargs) + new_kwargs['telemetry_channel'] = client.channel + super(LoggingHandler, self).__init__(client.context.instrumentation_key, *args, **new_kwargs) diff --git a/applicationinsights/django/middleware.py b/applicationinsights/django/middleware.py new file mode 100644 index 0000000..667a1b6 --- /dev/null +++ b/applicationinsights/django/middleware.py @@ -0,0 +1,274 @@ + +import datetime +import inspect +import sys +import time +import uuid + +from django.http import Http404 + +import applicationinsights +from applicationinsights.channel import contracts +from . import common + +# Pick a function to measure time; starting with 3.3, time.monotonic is available. +if sys.version_info >= (3, 3): + TIME_FUNC = time.monotonic +else: + TIME_FUNC = time.time + +class ApplicationInsightsMiddleware(object): + """This class is a Django middleware that automatically enables request and exception telemetry. Django versions + 1.7 and newer are supported. + + To enable, add this class to your settings.py file in MIDDLEWARE_CLASSES (pre-1.10) or MIDDLEWARE (1.10 and newer): + + .. code:: python + + # If on Django < 1.10 + MIDDLEWARE_CLASSES = [ + # ... or whatever is below for you ... + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # ... or whatever is above for you ... + 'applicationinsights.django.ApplicationInsightsMiddleware', # Add this middleware to the end + ] + + # If on Django >= 1.10 + MIDDLEWARE = [ + # ... or whatever is below for you ... + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # ... or whatever is above for you ... + 'applicationinsights.django.ApplicationInsightsMiddleware', # Add this middleware to the end + ] + + And then, add the following to your settings.py file: + + .. code:: python + + APPLICATION_INSIGHTS = { + # (required) Your Application Insights instrumentation key + 'ikey': "00000000-0000-0000-0000-000000000000", + + # (optional) By default, request names are logged as the request method + # and relative path of the URL. To log the fully-qualified view names + # instead, set this to True. Defaults to False. + 'use_view_name': True, + + # (optional) To log arguments passed into the views as custom properties, + # set this to True. Defaults to False. + 'record_view_arguments': True, + + # (optional) Exceptions are logged by default, to disable, set this to False. + 'log_exceptions': False, + + # (optional) Events are submitted to Application Insights asynchronously. + # send_interval specifies how often the queue is checked for items to submit. + # send_time specifies how long the sender waits for new input before recycling + # the background thread. + 'send_interval': 1.0, # Check every second + 'send_time': 3.0, # Wait up to 3 seconds for an event + + # (optional, uncommon) If you must send to an endpoint other than the + # default endpoint, specify it here: + 'endpoint': "https://dc.services.visualstudio.com/v2/track", + } + + Once these are in place, each request will have an `appinsights` object placed on it. + This object will have the following properties: + + * `client`: This is an instance of the :class:`applicationinsights.TelemetryClient` type, which will + submit telemetry to the same instrumentation key, and will parent each telemetry item to the current + request. + * `request`: This is the :class:`applicationinsights.channel.contracts.RequestData` instance for the + current request. You can modify properties on this object during the handling of the current request. + It will be submitted when the request has finished. + * `context`: This is the :class:`applicationinsights.channel.TelemetryContext` object for the current + ApplicationInsights sender. + + These properties will be present even when `DEBUG` is `True`, but it may not submit telemetry unless + `debug_ikey` is set in `APPLICATION_INSIGHTS`, above. + """ + def __init__(self, get_response=None): + self.get_response = get_response + + # Get configuration + self._settings = common.load_settings() + self._client = common.create_client(self._settings) + + # Pre-1.10 handler + def process_request(self, request): + # Populate context object onto request + addon = RequestAddon(self._client) + data = addon.request + context = addon.context + request.appinsights = addon + + # Basic request properties + data.start_time = datetime.datetime.utcnow().isoformat() + "Z" + data.http_method = request.method + data.url = request.build_absolute_uri() + data.name = "%s %s" % (request.method, request.path) + context.operation.name = data.name + context.operation.id = data.id + context.location.ip = request.META.get('REMOTE_ADDR', '') + context.user.user_agent = request.META.get('HTTP_USER_AGENT', '') + + # User + if request.user is not None and not request.user.is_anonymous and request.user.is_authenticated: + context.user.account_id = request.user.get_short_name() + + # Run and time the request + addon.start_stopwatch() + return None + + # Pre-1.10 handler + def process_response(self, request, response): + addon = request.appinsights + duration = addon.measure_duration() + + data = addon.request + context = addon.context + + # Fill in data from the response + data.duration = addon.measure_duration() + data.response_code = response.status_code + data.success = response.status_code < 400 or response.status_code == 401 + + # Submit and return + self._client.channel.write(data, context) + return response + + # 1.10 and up... + def __call__(self, request): + self.process_request(request) + response = self.get_response(request) + self.process_response(request, response) + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + if not hasattr(request, "appinsights"): + return None + + data = request.appinsights.request + context = request.appinsights.context + + # Operation name is the method + url by default (set in __call__), + # If use_view_name is set, then we'll look up the name of the view. + if self._settings.use_view_name: + mod = inspect.getmodule(view_func) + name = view_func.__name__ + if mod: + opname = "%s %s.%s" % (data.http_method, mod.__name__, name) + else: + opname = "%s %s" % (data.http_method, name) + data.name = opname + context.operation.name = opname + + # Populate the properties with view arguments + if self._settings.record_view_arguments: + for i, arg in enumerate(view_args): + data.properties['view_arg_' + str(i)] = arg_to_str(arg) + + for k, v in view_kwargs.items(): + data.properties['view_arg_' + k] = arg_to_str(v) + + return None + + def process_exception(self, request, exception): + if not self._settings.log_exceptions: + return None + + if type(exception) is Http404: + return None + + _, _, tb = sys.exc_info() + if tb is None or exception is None: + # No actual traceback or exception info, don't bother logging. + return None + + client = applicationinsights.TelemetryClient(self._client.context.instrumentation_key, self._client.channel) + if hasattr(request, 'appinsights'): + client.context.operation.parent_id = request.appinsights.request.id + + client.track_exception(type(exception), exception, tb) + + return None + + def process_template_response(self, request, response): + if hasattr(request, 'appinsights') and hasattr(response, 'template_name'): + data = request.appinsights.request + data.properties['template_name'] = response.template_name + + return None + +class RequestAddon(object): + def __init__(self, client): + self._baseclient = client + self._client = None + self.request = contracts.RequestData() + self.request.id = str(uuid.uuid4()) + self.context = applicationinsights.channel.TelemetryContext() + self.context.instrumentation_key = client.context.instrumentation_key + self.context.operation.id = self.request.id + self._process_start_time = None + + @property + def client(self): + if self._client is None: + # Create a client that submits telemetry parented to the request. + self._client = applicationinsights.TelemetryClient(self.context.instrumentation_key, self._baseclient.channel) + self._client.context.operation.parent_id = self.context.operation.id + + return self._client + + def start_stopwatch(self): + self._process_start_time = TIME_FUNC() + + def measure_duration(self): + end_time = TIME_FUNC() + return ms_to_duration(int((end_time - self._process_start_time) * 1000)) + +def ms_to_duration(n): + duration_parts = [] + for multiplier in [1000, 60, 60, 24]: + duration_parts.append(n % multiplier) + n //= multiplier + + duration_parts.reverse() + duration = "%02d:%02d:%02d.%03d" % tuple(duration_parts) + if n: + duration = "%d.%s" % (n, duration) + + return duration + +def arg_to_str_3(arg): + if isinstance(arg, str): + return arg + if isinstance(arg, int): + return str(arg) + return repr(arg) + +def arg_to_str_2(arg): + if isinstance(arg, str) or isinstance(arg, unicode): + return arg + if isinstance(arg, int): + return str(arg) + return repr(arg) + +if sys.version_info < (3, 0): + arg_to_str = arg_to_str_2 +else: + arg_to_str = arg_to_str_3 diff --git a/applicationinsights/logging/LoggingHandler.py b/applicationinsights/logging/LoggingHandler.py index a9b10b9..ed0bdf7 100644 --- a/applicationinsights/logging/LoggingHandler.py +++ b/applicationinsights/logging/LoggingHandler.py @@ -106,4 +106,4 @@ class LoggingHandler(logging.Handler): # if we don't simply format the message and send the trace formatted_message = self.format(record) - self.client.track_trace(formatted_message, properties=properties) + self.client.track_trace(formatted_message, properties=properties, severity=record.levelname) diff --git a/django_tests/all_tests.sh b/django_tests/all_tests.sh new file mode 100755 index 0000000..0306277 --- /dev/null +++ b/django_tests/all_tests.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +if [ -z $PYTHON ]; then + PYTHON=$(which python) +fi + +cd $(dirname $0) +BASEDIR=$(pwd) + +# Django/python compatibility matrix... +if $PYTHON -c "import sys; sys.exit(0 if sys.version_info < (3, 0) else 1)"; then + # Django2.0 won't support Python2 + DJANGO_VERSIONS='1.7.11 1.8.18 1.9.13 1.10.7 1.11' +elif $PYTHON -c "import sys; sys.exit(0 if sys.version_info < (3, 5) else 1)"; then + DJANGO_VERSIONS='1.7.11 1.8.18 1.9.13 1.10.7 1.11' +else + # python3.5 dropped html.parser.HtmlParserError versions older than Django1.8 won't work + DJANGO_VERSIONS='1.8.18 1.9.13 1.10.7 1.11' +fi + +# For each Django version... +for v in $DJANGO_VERSIONS +do + echo "" + echo "***" + echo "*** Running tests for Django $v" + echo "***" + echo "" + + # Create new directory + TMPDIR=$(mktemp -d) + function cleanup + { + rm -rf $TMPDIR + exit $1 + } + + trap cleanup EXIT SIGINT + + # Create virtual environment + virtualenv -p $PYTHON $TMPDIR/env + + # Install Django version + application insights + . $TMPDIR/env/bin/activate + pip install Django==$v || exit $? + cd $BASEDIR/.. + python setup.py install || exit $? + + # Run tests + cd $BASEDIR + bash ./run_test.sh || exit $? + + # Remove venv + deactivate + rm -rf $TMPDIR +done diff --git a/django_tests/run_test.sh b/django_tests/run_test.sh new file mode 100755 index 0000000..6dc1ae9 --- /dev/null +++ b/django_tests/run_test.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# It is expected at this point that django and applicationinsights are both installed into a +# virtualenv. +django_version=$(python -c "import django ; print('.'.join(map(str, django.VERSION[0:2])))") +test $? -eq 0 || exit 1 + +# Create a new temporary work directory +TMPDIR=$(mktemp -d) +SRCDIR=$(pwd) +function cleanup +{ + cd $SRCDIR + rm -rf $TMPDIR + exit $1 +} +trap cleanup EXIT SIGINT +cd $TMPDIR + +# Set up Django project +django-admin startproject aitest +cd aitest +cp $SRCDIR/views.py aitest/views.py +cp $SRCDIR/tests.py aitest/tests.py +cp $SRCDIR/urls.py aitest/urls.py + +./manage.py test +exit $? diff --git a/django_tests/tests.py b/django_tests/tests.py new file mode 100644 index 0000000..3495745 --- /dev/null +++ b/django_tests/tests.py @@ -0,0 +1,338 @@ +import logging + +import django +from django.test import TestCase, Client, modify_settings, override_settings + +from applicationinsights import TelemetryClient +from applicationinsights.channel import TelemetryChannel, SynchronousQueue, SenderBase, NullSender, AsynchronousSender +from applicationinsights.django import common + +if django.VERSION > (1, 10): + MIDDLEWARE_NAME = "MIDDLEWARE" +else: + MIDDLEWARE_NAME = "MIDDLEWARE_CLASSES" + +TEST_IKEY = '12345678-1234-5678-9012-123456789abc' +TEST_ENDPOINT = 'https://test.endpoint/v2/track' +DEFAULT_ENDPOINT = AsynchronousSender().service_endpoint_uri + +class AITestCase(TestCase): + def plug_sender(self): + # Reset saved objects + common.saved_clients = {} + common.saved_channels = {} + + # Create a client and mock out the sender + client = common.create_client() + sender = MockSender() + client._channel = TelemetryChannel(None, SynchronousQueue(sender)) + self.events = sender.events + self.channel = client.channel + + def get_events(self, count): + self.channel.flush() + self.assertEqual(len(self.events), count, "Expected %d event(s) in queue (%d actual)" % (count, len(self.events))) + if count == 1: + return self.events[0] + return self.events + +@modify_settings(**{MIDDLEWARE_NAME: {'append': 'applicationinsights.django.ApplicationInsightsMiddleware'}}) +@override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY}) +class MiddlewareTests(AITestCase): + def setUp(self): + self.plug_sender() + + def test_basic_request(self): + """Tests that hitting a simple view generates a telemetry item with the correct properties""" + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + + event = self.get_events(1) + tags = event['tags'] + data = event['data']['baseData'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(event['iKey'], TEST_IKEY) + self.assertEqual(tags['ai.operation.name'], 'GET /', "Operation name") + self.assertEqual(data['name'], 'GET /', "Request name") + self.assertEqual(data['responseCode'], 200, "Status code") + self.assertEqual(data['success'], True, "Success value") + self.assertEqual(data['httpMethod'], 'GET', "HTTP Method") + self.assertEqual(data['url'], 'http://testserver/', "Request url") + + def test_logger(self): + """Tests that traces logged from inside of a view are submitted and parented to the request telemetry item""" + response = self.client.get('/logger') + self.assertEqual(response.status_code, 200) + + logev, reqev = self.get_events(2) + + # Check request event (minimal, since we validate this elsewhere) + tags = reqev['tags'] + data = reqev['data']['baseData'] + reqid = tags['ai.operation.id'] + self.assertEqual(reqev['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(data['id'], reqid, "Request id") + self.assertEqual(data['name'], 'GET /logger', "Operation name") + self.assertEqual(data['url'], 'http://testserver/logger', "Request url") + + self.assertTrue(reqid, "Request id not empty") + + # Check log event + tags = logev['tags'] + data = logev['data']['baseData'] + self.assertEqual(logev['name'], 'Microsoft.ApplicationInsights.Message', "Event type") + self.assertEqual(logev['iKey'], TEST_IKEY) + self.assertEqual(tags['ai.operation.parentId'], reqid, "Parent id") + self.assertEqual(data['message'], 'Logger message', "Log message") + self.assertEqual(data['properties']['property'], 'value', "Property=value") + + def test_thrower(self): + """Tests that unhandled exceptions generate an exception telemetry item parented to the request telemetry item""" + with self.assertRaises(ValueError): + self.client.get('/thrower') + + errev, reqev = self.get_events(2) + + # Check request event + tags = reqev['tags'] + data = reqev['data']['baseData'] + reqid = tags['ai.operation.id'] + self.assertEqual(reqev['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(reqev['iKey'], TEST_IKEY) + self.assertEqual(data['id'], reqid, "Request id") + self.assertEqual(data['responseCode'], 500, "Response code") + self.assertEqual(data['success'], False, "Success value") + self.assertEqual(data['name'], 'GET /thrower', "Request name") + self.assertEqual(data['url'], 'http://testserver/thrower', "Request url") + + self.assertTrue(reqid, "Request id not empty") + + # Check exception event + tags = errev['tags'] + data = errev['data']['baseData'] + self.assertEqual(errev['name'], 'Microsoft.ApplicationInsights.Exception', "Event type") + self.assertEqual(tags['ai.operation.parentId'], reqid, "Exception parent id") + self.assertEqual(len(data['exceptions']), 1, "Exception count") + exc = data['exceptions'][0] + self.assertEqual(exc['typeName'], 'ValueError', "Exception type") + self.assertEqual(exc['hasFullStack'], True, "Has full stack") + self.assertEqual(exc['parsedStack'][0]['method'], 'thrower', "Stack frame method name") + + def test_error(self): + """Tests that Http404 exception does not generate an exception event + and the request telemetry item properly logs the failure""" + response = self.client.get("/errorer") + self.assertEqual(response.status_code, 404) + + event = self.get_events(1) + tags = event['tags'] + data = event['data']['baseData'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(tags['ai.operation.name'], 'GET /errorer', "Operation name") + self.assertEqual(data['responseCode'], 404, "Status code") + self.assertEqual(data['success'], False, "Success value") + self.assertEqual(data['url'], 'http://testserver/errorer', "Request url") + + def test_no_view_arguments(self): + """Tests that view id logging is off by default""" + self.plug_sender() + response = self.client.get('/getid/24') + self.assertEqual(response.status_code, 200) + + event = self.get_events(1) + data = event['data']['baseData'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertTrue('properties' not in data or 'view_arg_0' not in data['properties']) + + def test_no_view(self): + """Tests that requests to URLs not backed by views are still logged""" + response = self.client.get('/this/view/does/not/exist') + self.assertEqual(response.status_code, 404) + + event = self.get_events(1) + tags = event['tags'] + data = event['data']['baseData'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(data['responseCode'], 404, "Status code") + self.assertEqual(data['success'], False, "Success value") + self.assertEqual(data['name'], 'GET /this/view/does/not/exist', "Request name") + self.assertEqual(data['url'], 'http://testserver/this/view/does/not/exist', "Request url") + + def test_401_success(self): + """Tests that a 401 status code is considered successful""" + response = self.client.get("/returncode/401") + self.assertEqual(response.status_code, 401) + + event = self.get_events(1) + tags = event['tags'] + data = event['data']['baseData'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(tags['ai.operation.name'], 'GET /returncode/401', "Operation name") + self.assertEqual(data['responseCode'], 401, "Status code") + self.assertEqual(data['success'], True, "Success value") + self.assertEqual(data['url'], 'http://testserver/returncode/401', "Request url") + +@modify_settings(**{MIDDLEWARE_NAME: {'append': 'applicationinsights.django.ApplicationInsightsMiddleware'}}) +class RequestSettingsTests(AITestCase): + # This type needs to plug the sender during the test -- doing it in setUp would have nil effect + # because each method's override_settings wouldn't have happened by then. + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'use_view_name': True}) + def test_use_view_name(self): + """Tests that request names are set to URLs when use_operation_url=True""" + self.plug_sender() + self.client.get('/') + event = self.get_events(1) + self.assertEqual(event['data']['baseData']['name'], 'GET aitest.views.home', "Request name") + self.assertEqual(event['tags']['ai.operation.name'], 'GET aitest.views.home', "Operation name") + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'use_view_name': False}) + def test_use_view_name_off(self): + """Tests that request names are set to view names when use_operation_url=False""" + self.plug_sender() + self.client.get('/') + event = self.get_events(1) + self.assertEqual(event['data']['baseData']['name'], 'GET /', "Request name") + self.assertEqual(event['tags']['ai.operation.name'], 'GET /', "Operation name") + + @override_settings(APPLICATION_INSIGHTS=None) + def test_appinsights_still_supplied(self): + """Tests that appinsights is still added to requests even if APPLICATION_INSIGHTS is unspecified""" + # This uses request.appinsights -- it will crash if it's not there. + response = self.client.get('/logger') + self.assertEqual(response.status_code, 200) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'record_view_arguments': True}) + def test_view_id(self): + """Tests that view arguments are logged when record_view_arguments=True""" + self.plug_sender() + response = self.client.get('/getid/24') + self.assertEqual(response.status_code, 200) + + event = self.get_events(1) + props = event['data']['baseData']['properties'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(props['view_arg_0'], '24', "View argument") + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'log_exceptions': False}) + def test_log_exceptions_off(self): + """Tests that exceptions are not logged when log_exceptions=False""" + self.plug_sender() + with self.assertRaises(ValueError): + response = self.client.get('/thrower') + + event = self.get_events(1) + data = event['data']['baseData'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Request', "Event type") + self.assertEqual(data['responseCode'], 500, "Response code") + +class SettingsTests(TestCase): + def setUp(self): + # Just clear out any cached objects + common.saved_clients = {} + common.saved_channels = {} + + def test_no_app_insights(self): + """Tests that events are swallowed when APPLICATION_INSIGHTS is unspecified""" + client = common.create_client() + self.assertTrue(type(client.channel.sender) is NullSender) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY}) + def test_default_endpoint(self): + """Tests that the default endpoint is used when endpoint is unspecified""" + client = common.create_client() + self.assertEqual(client.channel.sender.service_endpoint_uri, DEFAULT_ENDPOINT) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'endpoint': TEST_ENDPOINT}) + def test_overridden_endpoint(self): + """Tests that the endpoint is used when specified""" + client = common.create_client() + self.assertEqual(client.channel.sender.service_endpoint_uri, TEST_ENDPOINT) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'send_time': 999}) + def test_send_time(self): + """Tests that send_time is propagated to sender""" + client = common.create_client() + self.assertEqual(client.channel.sender.send_time, 999) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY, 'send_interval': 999}) + def test_send_interval(self): + """Tests that send_interval is propagated to sender""" + client = common.create_client() + self.assertEqual(client.channel.sender.send_interval, 999) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY}) + def test_default_send_time(self): + """Tests that send_time is equal to the default when it is unspecified""" + client = common.create_client() + self.assertEqual(client.channel.sender.send_time, AsynchronousSender().send_time) + + @override_settings(APPLICATION_INSIGHTS={'ikey': TEST_IKEY}) + def test_default_send_interval(self): + """Tests that send_interval is equal to the default when it is unspecified""" + client = common.create_client() + self.assertEqual(client.channel.sender.send_interval, AsynchronousSender().send_interval) + + +@override_settings(LOGGING={ + 'version': 1, + 'handlers': { + 'appinsights': { + 'class': 'applicationinsights.django.LoggingHandler', + 'level': 'INFO', + } + }, + 'loggers': { + __name__: { + 'handlers': ['appinsights'], + 'level': 'INFO', + } + } +}, APPLICATION_INSIGHTS={'ikey': TEST_IKEY}) +class LoggerTests(AITestCase): + def setUp(self): + self.plug_sender() + + def test_log_error(self): + """Tests an error trace telemetry is properly sent""" + django.setup() + logger = logging.getLogger(__name__) + msg = "An error log message" + logger.error(msg) + + event = self.get_events(1) + data = event['data']['baseData'] + props = data['properties'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Message', "Event type") + self.assertEqual(event['iKey'], TEST_IKEY) + self.assertEqual(data['message'], msg, "Log message") + self.assertEqual(data['severityLevel'], 3, "Severity level") + self.assertEqual(props['fileName'], 'tests.py', "Filename property") + self.assertEqual(props['level'], 'ERROR', "Level property") + self.assertEqual(props['module'], 'tests', "Module property") + + def test_log_info(self): + """Tests an info trace telemetry is properly sent""" + django.setup() + logger = logging.getLogger(__name__) + msg = "An info message" + logger.info(msg) + + event = self.get_events(1) + data = event['data']['baseData'] + props = data['properties'] + self.assertEqual(event['name'], 'Microsoft.ApplicationInsights.Message', "Event type") + self.assertEqual(event['iKey'], TEST_IKEY) + self.assertEqual(data['message'], msg, "Log message") + self.assertEqual(data['severityLevel'], 1, "Severity level") + self.assertEqual(props['fileName'], 'tests.py', "Filename property") + self.assertEqual(props['level'], 'INFO', "Level property") + self.assertEqual(props['module'], 'tests', "Module property") + +class MockSender(SenderBase): + def __init__(self): + SenderBase.__init__(self, DEFAULT_ENDPOINT) + self.events = [] + + def send(self, data): + self.events.extend(a.write() for a in data) diff --git a/django_tests/urls.py b/django_tests/urls.py new file mode 100644 index 0000000..0146527 --- /dev/null +++ b/django_tests/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls import include, url +from django.contrib import admin + +from . import views + +urlpatterns = [ + # Examples: + # url(r'^$', 'aitest.views.home', name='home'), + # url(r'^blog/', include('blog.urls')), + + url(r'^admin/', include(admin.site.urls)), + url(r'^$', views.home, name='home'), + url(r'^logger$', views.logger, name='logger'), + url(r'^thrower$', views.thrower, name='thrower'), + url(r'^errorer$', views.errorer, name='errorer'), + url(r'^getid/([0-9]+)$', views.getid, name='getid'), + url(r'^returncode/([0-9]+)$', views.returncode, name='returncode'), +] diff --git a/django_tests/views.py b/django_tests/views.py new file mode 100644 index 0000000..0b5ad12 --- /dev/null +++ b/django_tests/views.py @@ -0,0 +1,23 @@ +from django.http import HttpResponse, Http404 + +def home(request): + return HttpResponse("Welcome home") + +def logger(request): + request.appinsights.client.track_trace("Logger message", {"property": "value"}) + return HttpResponse("We logged a message") + +def thrower(request): + raise ValueError("This is an unexpected exception") + +def errorer(request): + raise Http404("This is a 404 error") + +def echoer(request): + return HttpResponse(request.appinsights.request.id) + +def getid(request, id): + return HttpResponse(str(id)) + +def returncode(request, id): + return HttpResponse("Status code set to %s" % id, status=int(id)) diff --git a/doc/applicationinsights.channel.contracts.rst b/doc/applicationinsights.channel.contracts.rst index 7fdc00b..5051021 100644 --- a/doc/applicationinsights.channel.contracts.rst +++ b/doc/applicationinsights.channel.contracts.rst @@ -1,9 +1,6 @@ .. toctree:: - :maxdepth: 2 :hidden: - applicationinsights - applicationinsights.channel.contracts module ============================================ diff --git a/doc/applicationinsights.django.rst b/doc/applicationinsights.django.rst new file mode 100644 index 0000000..0a1a41f --- /dev/null +++ b/doc/applicationinsights.django.rst @@ -0,0 +1,21 @@ +.. toctree:: + :maxdepth: 2 + :hidden: + +applicationinsights.django module +================================= + +ApplicationInsightsMiddleware class +----------------------------------- +.. autoclass:: applicationinsights.django.ApplicationInsightsMiddleware + :members: + :member-order: groupwise + :inherited-members: + +LoggingHandler class +-------------------- +.. autoclass:: applicationinsights.django.LoggingHandler + +create_client function +---------------------- +.. autofunction:: applicationinsights.django.create_client diff --git a/doc/applicationinsights.exceptions.rst b/doc/applicationinsights.exceptions.rst index eef1a5d..d9383b5 100644 --- a/doc/applicationinsights.exceptions.rst +++ b/doc/applicationinsights.exceptions.rst @@ -2,8 +2,6 @@ :maxdepth: 2 :hidden: - applicationinsights.requests - applicationinsights.exceptions module ===================================== diff --git a/doc/applicationinsights.logging.rst b/doc/applicationinsights.logging.rst index 1bb4e16..5c5a9c3 100644 --- a/doc/applicationinsights.logging.rst +++ b/doc/applicationinsights.logging.rst @@ -2,8 +2,6 @@ :maxdepth: 2 :hidden: - applicationinsights.exceptions - applicationinsights.logging module ================================== diff --git a/doc/applicationinsights.requests.rst b/doc/applicationinsights.requests.rst index ad0ead9..b91a6b1 100644 --- a/doc/applicationinsights.requests.rst +++ b/doc/applicationinsights.requests.rst @@ -2,8 +2,6 @@ :maxdepth: 2 :hidden: - applicationinsights.channel - applicationinsights.requests module =================================== diff --git a/doc/applicationinsights.rst b/doc/applicationinsights.rst index b509c9b..ae01d17 100644 --- a/doc/applicationinsights.rst +++ b/doc/applicationinsights.rst @@ -1,12 +1,15 @@ -.. toctree:: - :maxdepth: 2 - :hidden: - - applicationinsights.logging - applicationinsights module ========================== +.. toctree:: + :maxdepth: 1 + + applicationinsights.channel + applicationinsights.logging + applicationinsights.requests + applicationinsights.django + applicationinsights.exceptions + TelemetryClient class ---------------------- .. autoclass:: applicationinsights.TelemetryClient diff --git a/doc/index.rst b/doc/index.rst index 5a9d709..7392b2e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -26,6 +26,7 @@ Application Insights SDK for Python * :ref:`Advanced logging configuration ` * :ref:`Logging unhandled exceptions ` * :ref:`Logging requests ` + * :ref:`Integrating with Django ` This project extends the Application Insights API surface to support Python. `Application Insights `__ is a service that allows developers to keep their application available, performing and succeeding. This Python module will allow you to send telemetry of various kinds (event, trace, exception, etc.) to the Application Insights service where they can be visualized in the Azure Portal. @@ -300,4 +301,107 @@ Once installed, you can send telemetry to Application Insights. Here are a few s # run the application if __name__ == '__main__': - app.run() \ No newline at end of file + app.run() + +.. _usage-sample-16: + +**Integrating with Django** + +Place the following in your `settings.py` file: + +.. code:: python + + # If on Django < 1.10 + MIDDLEWARE_CLASSES = [ + # ... or whatever is below for you ... + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # ... or whatever is above for you ... + 'applicationinsights.django.ApplicationInsightsMiddleware', # Add this middleware to the end + ] + + # If on Django >= 1.10 + MIDDLEWARE = [ + # ... or whatever is below for you ... + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # ... or whatever is above for you ... + 'applicationinsights.django.ApplicationInsightsMiddleware', # Add this middleware to the end + ] + + APPLICATION_INSIGHTS = { + # (required) Your Application Insights instrumentation key + 'ikey': "00000000-0000-0000-0000-000000000000", + + # (optional) By default, request names are logged as the fully-qualified + # name of the view. To disable this behavior, specify: + 'use_operation_url': True, + + # (optional) By default, arguments to views are tracked as custom + # properties. To disable this, specify: + 'record_view_arguments': False, + + # (optional) Events are submitted to Application Insights asynchronously. + # send_interval specifies how often the queue is checked for items to submit. + # send_time specifies how long the sender waits for new input before recycling + # the background thread. + 'send_interval': 1.0, # Check every second + 'send_time': 3.0, # Wait up to 3 seconds for an event + + # (optional, uncommon) If you must send to an endpoint other than the + # default endpoint, specify it here: + 'endpoint': "https://dc.services.visualstudio.com/v2/track", + } + +This will log all requests and exceptions to the instrumentation key +specified in the `APPLICATION_INSIGHTS` setting. In addition, an +`appinsights` property will be placed on each incoming `request` object in +your views. This will have the following properties: + +* `client`: This is an instance of the :class:`applicationinsights.TelemetryClient` type, which will + submit telemetry to the same instrumentation key, and will parent each telemetry item to the current + request. +* `request`: This is the :class:`applicationinsights.channel.contracts.RequestData` instance for the + current request. You can modify properties on this object during the handling of the current request. + It will be submitted when the request has finished. +* `context`: This is the :class:`applicationinsights.channel.TelemetryContext` object for the current + ApplicationInsights sender. + +You can also hook up logging to Django. For example, to log all builtin +Django warnings and errors, use the following logging configuration in +`settings.py`: + +.. code:: python + + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + # The application insights handler is here + 'appinsights': { + 'class': 'applicationinsights.django.LoggingHandler', + 'level': 'WARNING' + } + }, + 'loggers': { + 'django': { + 'handlers': ['appinsights'], + 'level': 'WARNING', + 'propagate': True, + } + } + } + +See Django's logging documentation for more information: +https://docs.djangoproject.com/en/1.11/topics/logging/ diff --git a/tests/applicationinsights_tests/TestTelemetryClient.py b/tests/applicationinsights_tests/TestTelemetryClient.py index ebfa2e9..67f4f3d 100644 --- a/tests/applicationinsights_tests/TestTelemetryClient.py +++ b/tests/applicationinsights_tests/TestTelemetryClient.py @@ -67,9 +67,9 @@ class TestTelemetryClient(unittest.TestCase): queue = channel.SynchronousQueue(sender) client = TelemetryClient('99999999-9999-9999-9999-999999999999', channel.TelemetryChannel(context=None, queue=queue)) client.context.device = None - client.track_trace('test', { 'foo': 'bar' }) + client.track_trace('test', { 'foo': 'bar' }, severity='WARNING') client.flush() - expected = '{"ver": 1, "name": "Microsoft.ApplicationInsights.Message", "time": "TIME_PLACEHOLDER", "sampleRate": 100.0, "iKey": "99999999-9999-9999-9999-999999999999", "tags": {"ai.internal.sdkVersion": "SDK_VERSION_PLACEHOLDER"}, "data": {"baseType": "MessageData", "baseData": {"ver": 2, "message": "test", "properties": {"foo": "bar"}}}}' + expected = '{"ver": 1, "name": "Microsoft.ApplicationInsights.Message", "time": "TIME_PLACEHOLDER", "sampleRate": 100.0, "iKey": "99999999-9999-9999-9999-999999999999", "tags": {"ai.internal.sdkVersion": "SDK_VERSION_PLACEHOLDER"}, "data": {"baseType": "MessageData", "baseData": {"ver": 2, "message": "test", "severityLevel": 2, "properties": {"foo": "bar"}}}}' sender.data.time = 'TIME_PLACEHOLDER' sender.data.tags['ai.internal.sdkVersion'] = 'SDK_VERSION_PLACEHOLDER' actual = json.dumps(sender.data.write()) diff --git a/tests/applicationinsights_tests/logging_tests/TestLoggingHandler.py b/tests/applicationinsights_tests/logging_tests/TestLoggingHandler.py index e1ccd67..a927ae2 100644 --- a/tests/applicationinsights_tests/logging_tests/TestLoggingHandler.py +++ b/tests/applicationinsights_tests/logging_tests/TestLoggingHandler.py @@ -46,14 +46,14 @@ class TestLoggingHandler(unittest.TestCase): logger, sender = self._setup_logger() expected = [ - (logger.debug, 'debug message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 'simple_logger - DEBUG - debug message'), - (logger.info, 'info message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 'simple_logger - INFO - info message'), - (logger.warn, 'warn message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 'simple_logger - WARNING - warn message'), - (logger.error, 'error message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 'simple_logger - ERROR - error message'), - (logger.critical, 'critical message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 'simple_logger - CRITICAL - critical message') + (logger.debug, 'debug message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 0, 'simple_logger - DEBUG - debug message'), + (logger.info, 'info message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 1, 'simple_logger - INFO - info message'), + (logger.warn, 'warn message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 2, 'simple_logger - WARNING - warn message'), + (logger.error, 'error message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 3, 'simple_logger - ERROR - error message'), + (logger.critical, 'critical message', 'Microsoft.ApplicationInsights.Message', 'test', 'MessageData', 4, 'simple_logger - CRITICAL - critical message') ] - for logging_function, logging_parameter, envelope_type, ikey, data_type, message in expected: + for logging_function, logging_parameter, envelope_type, ikey, data_type, severity_level, message in expected: logging_function(logging_parameter) data = sender.data[0][0] sender.data = [] @@ -61,6 +61,7 @@ class TestLoggingHandler(unittest.TestCase): self.assertEqual(ikey, data.ikey) self.assertEqual(data_type, data.data.base_type) self.assertEqual(message, data.data.base_data.message) + self.assertEqual(severity_level, data.data.base_data.severity_level) def test_log_exception_works_as_expected(self): logger, sender = self._setup_logger()