1import datetime
2import re
3import uuid
4import applicationinsights
5
6class WSGIApplication(object):
7    """ This class represents a WSGI wrapper that enables request telemetry for existing WSGI applications. The request
8    telemetry will be sent to Application Insights service using the supplied instrumentation key.
9
10    .. code:: python
11
12            from applicationinsights.requests import WSGIApplication
13            from paste.httpserver import serve
14            from pyramid.response import Response
15            from pyramid.view import view_config
16
17            @view_config()
18            def hello(request):
19                return Response('Hello')
20
21            if __name__ == '__main__':
22                from pyramid.config import Configurator
23                config = Configurator()
24                config.scan()
25                app = config.make_wsgi_app()
26
27                # Enable Application Insights middleware
28                app = WSGIApplication('<YOUR INSTRUMENTATION KEY GOES HERE>', app, common_properties={'service': 'hello_world_service'})
29
30                serve(app, host='0.0.0.0')
31    """
32    def __init__(self, instrumentation_key, wsgi_application, *args, **kwargs):
33        """
34        Initialize a new instance of the class.
35
36        Args:
37            instrumentation_key (str). the instrumentation key to use while sending telemetry to the service.\n
38            wsgi_application (func). the WSGI application that we're wrapping.
39        """
40        if not instrumentation_key:
41            raise Exception('Instrumentation key was required but not provided')
42        if not wsgi_application:
43            raise Exception('WSGI application was required but not provided')
44        telemetry_channel = kwargs.pop('telemetry_channel', None)
45        if not telemetry_channel:
46            sender = applicationinsights.channel.AsynchronousSender()
47            queue = applicationinsights.channel.AsynchronousQueue(sender)
48            telemetry_channel = applicationinsights.channel.TelemetryChannel(None, queue)
49        self.client = applicationinsights.TelemetryClient(instrumentation_key, telemetry_channel)
50        self.client.context.device.type = "PC"
51        self._wsgi_application = wsgi_application
52        self._common_properties = kwargs.pop('common_properties', {})
53
54    def flush(self):
55        """Flushes the queued up telemetry to the service.
56        """
57        self.client.flush()
58
59    def __call__(self, environ, start_response):
60        """Callable implementation for WSGI middleware.
61
62        Args:
63            environ (dict). a dictionary containing all WSGI environment properties for this request.\n
64            start_response (func). a function used to store the status, HTTP headers to be sent to the client and optional exception information.
65
66        Returns:
67            (obj). the response to send back to the client.
68        """
69        start_time = datetime.datetime.utcnow()
70        name = environ.get('PATH_INFO') or '/'
71        closure = {'status': '200 OK'}
72        http_method = environ.get('REQUEST_METHOD', 'GET')
73
74        self.client.context.operation.id = str(uuid.uuid4())
75        # operation.parent_id ought to be the request id (not the operation id, but we don't have it yet)
76        self.client.context.operation.name = http_method + ' ' + name
77
78        def status_interceptor(status_string, headers_array, exc_info=None):
79            closure['status'] = status_string
80            start_response(status_string, headers_array, exc_info)
81
82        for part in self._wsgi_application(environ, status_interceptor):
83            yield part
84
85        success = True
86        response_match = re.match(r'\s*(?P<code>\d+)', closure['status'])
87        if response_match:
88            response_code = response_match.group('code')
89            if int(response_code) >= 400:
90                success = False
91        else:
92            response_code = closure['status']
93            success = False
94
95        url = name
96        query_string = environ.get('QUERY_STRING')
97        if query_string:
98            url += '?' + query_string
99
100        scheme = environ.get('wsgi.url_scheme', 'http')
101        host =  environ.get('HTTP_HOST', environ.get('SERVER_NAME', 'unknown'))
102
103        url = scheme + '://' + host + url
104
105        end_time = datetime.datetime.utcnow()
106        duration = int((end_time - start_time).total_seconds() * 1000)
107
108        self.client.track_request(name, url, success, start_time.isoformat() + 'Z', duration, response_code, http_method, self._common_properties)
109