Merge pull request #41 from Microsoft/develop

Merging develop into master in preparation for release
This commit is contained in:
Alex Bulankou 2017-08-11 15:20:28 -07:00 коммит произвёл GitHub
Родитель bb15f9fada 324d3a578a
Коммит 6dc7f98025
27 изменённых файлов: 1183 добавлений и 33 удалений

Просмотреть файл

@ -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

108
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.

Просмотреть файл

@ -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)

Просмотреть файл

@ -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

Просмотреть файл

@ -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)
self._queue.put(data)

Просмотреть файл

@ -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)

Просмотреть файл

@ -6,4 +6,5 @@ from .SynchronousQueue import SynchronousQueue
from .SynchronousSender import SynchronousSender
from .TelemetryChannel import TelemetryChannel
from .TelemetryContext import TelemetryContext
from . import contracts
from .NullSender import NullSender
from . import contracts

Просмотреть файл

@ -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),

Просмотреть файл

@ -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()

Просмотреть файл

@ -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)

Просмотреть файл

@ -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)

Просмотреть файл

@ -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

Просмотреть файл

@ -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)

56
django_tests/all_tests.sh Executable file
Просмотреть файл

@ -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

28
django_tests/run_test.sh Executable file
Просмотреть файл

@ -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 $?

338
django_tests/tests.py Normal file
Просмотреть файл

@ -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)

18
django_tests/urls.py Normal file
Просмотреть файл

@ -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'),
]

23
django_tests/views.py Normal file
Просмотреть файл

@ -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))

Просмотреть файл

@ -1,9 +1,6 @@
.. toctree::
:maxdepth: 2
:hidden:
applicationinsights
applicationinsights.channel.contracts module
============================================

Просмотреть файл

@ -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

Просмотреть файл

@ -2,8 +2,6 @@
:maxdepth: 2
:hidden:
applicationinsights.requests
applicationinsights.exceptions module
=====================================

Просмотреть файл

@ -2,8 +2,6 @@
:maxdepth: 2
:hidden:
applicationinsights.exceptions
applicationinsights.logging module
==================================

Просмотреть файл

@ -2,8 +2,6 @@
:maxdepth: 2
:hidden:
applicationinsights.channel
applicationinsights.requests module
===================================

Просмотреть файл

@ -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

Просмотреть файл

@ -26,6 +26,7 @@ Application Insights SDK for Python
* :ref:`Advanced logging configuration <usage-sample-13>`
* :ref:`Logging unhandled exceptions <usage-sample-14>`
* :ref:`Logging requests <usage-sample-15>`
* :ref:`Integrating with Django <usage-sample-16>`
This project extends the Application Insights API surface to support Python. `Application
Insights <http://azure.microsoft.com/en-us/services/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()
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/

Просмотреть файл

@ -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())

Просмотреть файл

@ -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()