Using Azure Application Insights with aiohttp
In the last months I wrote an alternative Python SDK for Azure Storage using aiohttp, to take advantage of asynchronous web requests. Recently I did the same for Azure Application Insights. For those who don’t know it, Application Insights is a monitoring service that enables collecting telemetry data from applications, such as custom events and traces, unhandled exceptions, web requests, page views and much more.
It provides live stream of all metrics and possibility to receive alerts when desired, for example email notifications when the average response time for web requests is higher than x seconds in the last y minutes.
As it happens for Azure Storage, Microsoft provides an official Python SDK for Application Insights, but it is designed to work with Python 2.x and 3.x, therefore not using asyncio
infrastructure for event loops and not optimal for application using asyncio
. For this reason, I wrote an alternative library and published it in GitHub and PyPi, with name asynapplicationinsights
.
The library can be installed using pip:
pip install asynapplicationinsights
This post describes how the library was written. To know more about Application Insights, I recommend the following article from Microsoft:
1. Analysing the Application Insights REST api
The first step was to collect information about the Application Insights REST api, to know what parameters must be sent to collect telemetry. I didn’t find this information inside MSDN documentation, so I did the following:
- installed the official Python SDK from Microsoft in a python virtual environment
- detected the function making web requests to Application Insights in the source code, and modified it to log request bodies on file system
- used the library, calling methods to log events, requests, traces, etc.
I could also have sniffed traffic generated by the official .NET SDK for Application Insights, but I personally find easier to just edit a couple of lines in Python.
The file to change in the official SDK is: applicationinsights/channel/SenderBase.py
, below a fragment of code showing which lines were added to log request bodies.
def send(self, data_to_send):
""" Immediately sends the data passed in to :func:`service_endpoint_uri`. If the service request fails, the
passed in items are pushed back to the :func:`queue`.
Args:
data_to_send (Array): an array of :class:`contracts.Envelope` objects to send to the service.
"""
request_payload = json.dumps([ a.write() for a in data_to_send ])
# added lines, logs request payload for each object type:
for item in data_to_send:
name = item.name
with open(name + '.json', mode='wt') as body_log:
body_log.write(json.dumps([item.write()], indent=2))
For example, the payload to log a web request has the following structure:
[
{
"ver": 1,
"name": "Microsoft.ApplicationInsights.Request",
"time": "2018-06-23T22:56:17.393658Z",
"sampleRate": 100.0,
"iKey": "<INSTRUMENTATION KEY HERE>",
"tags": {
"ai.device.id": "device_id",
"ai.device.locale": "en_US",
"ai.device.osVersion": "#25-Ubuntu SMP Wed May 23 18:02:16 UTC 2018",
"ai.device.type": "Other",
"ai.internal.sdkVersion": "py3:0.11.4"
},
"data": {
"baseType": "RequestData",
"baseData": {
"ver": 2,
"id": "dc0d1d70-41dc-4216-892c-c043f0575dd8",
"name": "AddToFavorites",
"duration": "00:00:00.125",
"responseCode": 201,
"success": true,
"url": "/api/v1/favorites"
}
}
}
]
And the payload to log a trace event the following:
[
{
"ver": 1,
"name": "Microsoft.ApplicationInsights.Event",
"time": "2018-06-23T22:56:17.392930Z",
"sampleRate": 100.0,
"iKey": "<INSTRUMENTATION KEY HERE>",
"tags": {
"ai.device.id": "device_id",
"ai.device.locale": "en_US",
"ai.device.osVersion": "#25-Ubuntu SMP Wed May 23 18:02:16 UTC 2018",
"ai.device.type": "Other",
"ai.internal.sdkVersion": "py3:0.11.4"
},
"data": {
"baseType": "EventData",
"baseData": {
"ver": 2,
"name": "Fart",
"properties": {
"foo": "foo"
},
"measurements": {
"speed": 60.5
}
}
}
}
]
It is sufficient to make HTTP POST requests to https://dc.services.visualstudio.com/v2/track
endpoint, with valid body and instrumentation key, to collect information in Application Insights. The instrumentation key is a GUID assigned to a given instance of Application Insights service.
Additional information about these data structures and context tags can be found here in GitHub:
- https://github.com/Microsoft/ApplicationInsights-Home/blob/master/EndpointSpecs/Schemas/Bond/
- https://github.com/Microsoft/ApplicationInsights-Home/blob/master/EndpointSpecs/Schemas/Docs/ContextTagKeys.md
When information are logged to Application Insights, they can be explored using a REST api or visually on Azure Portal. Pictures below show the convenient graphs (failed requests, server response time, page view load time, server requests) and the analytics portal, supporting user defined queries.
2. Writing the SDK for asyncio
The second step was to write code to make asynchronous web requests to Application Insights REST api. For the implementation I used aiohttp
library and its HTTP client. My library uses a queue and flushes messages when the queue has 500 items in memory, or when explicitly required. This follows the same features of the official Python SDK from Microsoft, with the difference that is using asyncio. This client can be used in any kind of application, even in console applications having access to the internet. Examples for using this asynchronous client are included in the asynapplicationinsights GitHub repository, like in the code below.
import uuid
import asyncio
import unittest
from datetime import datetime, timedelta
from asynapplicationinsights.telemetry import AsyncTelemetryClient
from asynapplicationinsights.channel.aiohttpchannel import AiohttpTelemetryChannel
from random import randint
instrumentation_key = '<YOUR_INSTRUMENTATION_KEY>'
# This is just an example, for more information see the GitHub repository
# https://github.com/RobertoPrevato/asynapplicationinsights
#
class TestAi(unittest.TestCase):
def test_write_event(self):
loop = asyncio.get_event_loop()
async def go():
client = AsyncTelemetryClient(instrumentation_key,
AiohttpTelemetryChannel())
await client.track_event('Example',
measurements={
'speed': 55.5
})
await client.track_trace('Foo', {'foo': 'power'}, 2)
for _ in range(10):
await asyncio.sleep(0.005)
await client.track_metric('Something',
randint(0, 200))
await client.track_request(str(uuid.uuid4()),
'Example',
'http://localhost:44666/like/123456',
True,
datetime.utcnow() + timedelta(seconds=-10),
duration=125,
response_code=200,
http_method='GET'
)
try:
x = 100 / 0
except:
await client.track_exception()
await client.flush()
await client.dispose()
loop.run_until_complete(go())
3. A middleware for aiohttp server
Finally, I included an implementation of middleware for aiohttp
servers, to track web requests and unhandled exceptions. The middleware also logs how much time is spent inside the code for each request, so it is useful to detect bottlenecks. Instructions for use are included in a dedicated page of the project wiki.
As a final note, for single page applications (SPA), the server side middleware should be used together with the official JavaScript SDK from Microsoft because certain kinds of telemetries are designed to be coming from browsers (for example, PageView, not implemented in my asynapplicationinsights
). In case you are wondering, requests are not logged twice in this case: the JavaScript library is not generating logs of Request type.
Conclusions
Using the Application Insights api directly can be useful in some scenarios, so knowing its data structures and the shape of requests can be useful. Writing a custom client helped me learning many things I didn’t know about Application Insights, for example how users and sessions are logged and displayed on the Azure portal.