File: middleware.py

package info (click to toggle)
python-applicationinsights 0.11.10-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 876 kB
  • sloc: python: 5,948; makefile: 151; sh: 77
file content (276 lines) | stat: -rw-r--r-- 10,855 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276

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

try:
    basestring     # Python 2
except NameError:  # Python 3
    basestring = (str, )

# Pick a function to measure time; starting with 3.3, time.monotonic is available.
try:
    TIME_FUNC = time.monotonic
except AttributeError:
    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 hasattr(request, '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):
        if hasattr(request, 'appinsights'):
            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)
            if hasattr(view_func, "__name__"):
                name = view_func.__name__
            elif hasattr(view_func, "__class__") and hasattr(view_func.__class__, "__name__"):
                name = view_func.__class__.__name__
            else:
                name = "<unknown>"

            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 response

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(arg):
    if isinstance(arg, basestring):
        return arg
    if isinstance(arg, int):
        return str(arg)
    return repr(arg)