Skip to content

Commit ce80f31

Browse files
committed
Django: Application Insights accepts TelemetryProcessor or TelemetryClient
* fork django middleware from application insights repo. * django middleware now accepts a TelemetryClient that is aware of telemetry processor * enable test verifications for the middleware in django tests. * format with black.
1 parent 65f73d0 commit ce80f31

File tree

17 files changed

+1029
-442
lines changed

17 files changed

+1029
-442
lines changed

libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,11 @@
99
ApplicationInsightsTelemetryClient,
1010
bot_telemetry_processor,
1111
)
12+
from .bot_telemetry_processor import BotTelemetryProcessor
1213

13-
__all__ = ["ApplicationInsightsTelemetryClient", "bot_telemetry_processor"]
14+
15+
__all__ = [
16+
"ApplicationInsightsTelemetryClient",
17+
"BotTelemetryProcessor",
18+
"bot_telemetry_processor",
19+
]

libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py

Lines changed: 249 additions & 276 deletions
Large diffs are not rendered by default.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Application Insights Telemetry Processor for Bots."""
4+
from typing import List
5+
6+
from .django.django_telemetry_processor import DjangoTelemetryProcessor
7+
from .flask.flask_telemetry_processor import FlaskTelemetryProcessor
8+
from .processor.telemetry_processor import TelemetryProcessor
9+
10+
11+
class BotTelemetryProcessor(TelemetryProcessor):
12+
"""Application Insights Telemetry Processor for Bot"""
13+
14+
def __init__(self, processors: List[TelemetryProcessor] = None):
15+
self._processors: List[TelemetryProcessor] = [
16+
DjangoTelemetryProcessor(),
17+
FlaskTelemetryProcessor(),
18+
] if processors is None else processors
19+
20+
def can_process(self) -> bool:
21+
for processor in self._processors:
22+
if processor.can_process():
23+
return True
24+
25+
return False
26+
27+
def get_request_body(self) -> str:
28+
for inner in self._processors:
29+
if inner.can_process():
30+
return inner.get_request_body()
31+
32+
return super().get_request_body()
Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
# Copyright (c) Microsoft Corporation. All rights reserved.
2-
# Licensed under the MIT License.
3-
"""Django Application Insights package."""
4-
5-
from .bot_telemetry_middleware import BotTelemetryMiddleware, retrieve_bot_body
6-
7-
__all__ = ["BotTelemetryMiddleware", "retrieve_bot_body"]
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Django Application Insights package."""
4+
5+
from . import common
6+
from .bot_telemetry_middleware import BotTelemetryMiddleware
7+
from .logging import LoggingHandler
8+
from .middleware import ApplicationInsightsMiddleware
9+
10+
11+
__all__ = [
12+
"BotTelemetryMiddleware",
13+
"ApplicationInsightsMiddleware",
14+
"LoggingHandler",
15+
"create_client",
16+
]
17+
18+
19+
def create_client():
20+
"""Returns an :class:`applicationinsights.TelemetryClient` instance using the instrumentation key
21+
and other settings found in the current Django project's `settings.py` file."""
22+
return common.create_client()
Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,56 @@
1-
# Copyright (c) Microsoft Corporation. All rights reserved.
2-
# Licensed under the MIT License.
3-
"""Bot Telemetry Middleware."""
4-
5-
from threading import current_thread
6-
7-
# Map of thread id => POST body text
8-
_REQUEST_BODIES = {}
9-
10-
11-
def retrieve_bot_body():
12-
""" retrieve_bot_body
13-
Retrieve the POST body text from temporary cache.
14-
The POST body corresponds with the thread id and should resides in
15-
cache just for lifetime of request.
16-
"""
17-
result = _REQUEST_BODIES.pop(current_thread().ident, None)
18-
return result
19-
20-
21-
class BotTelemetryMiddleware:
22-
"""
23-
Save off the POST body to later populate bot-specific properties to
24-
add to Application Insights.
25-
26-
Example activating MIDDLEWARE in Django settings:
27-
MIDDLEWARE = [
28-
# Ideally add somewhere near top
29-
'botbuilder.applicationinsights.django.BotTelemetryMiddleware',
30-
...
31-
]
32-
"""
33-
34-
def __init__(self, get_response):
35-
self.get_response = get_response
36-
37-
def __call__(self, request):
38-
self.process_request(request)
39-
return self.get_response(request)
40-
41-
def process_request(self, request) -> bool:
42-
"""Process the incoming Django request."""
43-
# Bot Service doesn't handle anything over 256k
44-
# TODO: Add length check
45-
body_unicode = (
46-
request.body.decode("utf-8") if request.method == "POST" else None
47-
)
48-
# Sanity check JSON
49-
if body_unicode is not None:
50-
# Integration layer expecting just the json text.
51-
_REQUEST_BODIES[current_thread().ident] = body_unicode
52-
return True
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Bot Telemetry Middleware."""
4+
5+
from threading import current_thread
6+
7+
8+
# Map of thread id => POST body text
9+
_REQUEST_BODIES = {}
10+
11+
12+
def retrieve_bot_body():
13+
""" retrieve_bot_body
14+
Retrieve the POST body text from temporary cache.
15+
The POST body corresponds with the thread id and should resides in
16+
cache just for lifetime of request.
17+
"""
18+
19+
result = _REQUEST_BODIES.get(current_thread().ident, None)
20+
return result
21+
22+
23+
class BotTelemetryMiddleware:
24+
"""
25+
Save off the POST body to later populate bot-specific properties to
26+
add to Application Insights.
27+
28+
Example activating MIDDLEWARE in Django settings:
29+
MIDDLEWARE = [
30+
# Ideally add somewhere near top
31+
'botbuilder.applicationinsights.django.BotTelemetryMiddleware',
32+
...
33+
]
34+
"""
35+
36+
def __init__(self, get_response):
37+
self.get_response = get_response
38+
39+
def __call__(self, request):
40+
self.process_request(request)
41+
response = self.get_response(request)
42+
_REQUEST_BODIES.pop(current_thread().ident, None)
43+
return response
44+
45+
def process_request(self, request) -> bool:
46+
"""Process the incoming Django request."""
47+
# Bot Service doesn't handle anything over 256k
48+
# TODO: Add length check
49+
body_unicode = (
50+
request.body.decode("utf-8") if request.method == "POST" else None
51+
)
52+
# Sanity check JSON
53+
if body_unicode is not None:
54+
# Integration layer expecting just the json text.
55+
_REQUEST_BODIES[current_thread().ident] = body_unicode
56+
return True
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Common utilities for Django middleware."""
4+
import collections
5+
6+
from applicationinsights import TelemetryClient
7+
from applicationinsights.channel import (
8+
AsynchronousQueue,
9+
AsynchronousSender,
10+
NullSender,
11+
SynchronousQueue,
12+
TelemetryChannel,
13+
)
14+
from django.conf import settings
15+
16+
from ..processor.telemetry_processor import TelemetryProcessor
17+
from .django_telemetry_processor import DjangoTelemetryProcessor
18+
19+
20+
ApplicationInsightsSettings = collections.namedtuple(
21+
"ApplicationInsightsSettings",
22+
[
23+
"ikey",
24+
"channel_settings",
25+
"use_view_name",
26+
"record_view_arguments",
27+
"log_exceptions",
28+
],
29+
)
30+
31+
ApplicationInsightsChannelSettings = collections.namedtuple(
32+
"ApplicationInsightsChannelSettings", ["send_interval", "send_time", "endpoint"]
33+
)
34+
35+
36+
def load_settings():
37+
if hasattr(settings, "APPLICATION_INSIGHTS"):
38+
config = settings.APPLICATION_INSIGHTS
39+
elif hasattr(settings, "APPLICATIONINSIGHTS"):
40+
config = settings.APPLICATIONINSIGHTS
41+
else:
42+
config = {}
43+
44+
if not isinstance(config, dict):
45+
config = {}
46+
47+
return ApplicationInsightsSettings(
48+
ikey=config.get("ikey"),
49+
use_view_name=config.get("use_view_name", False),
50+
record_view_arguments=config.get("record_view_arguments", False),
51+
log_exceptions=config.get("log_exceptions", True),
52+
channel_settings=ApplicationInsightsChannelSettings(
53+
endpoint=config.get("endpoint"),
54+
send_interval=config.get("send_interval"),
55+
send_time=config.get("send_time"),
56+
),
57+
)
58+
59+
60+
saved_clients = {} # pylint: disable=invalid-name
61+
saved_channels = {} # pylint: disable=invalid-name
62+
63+
64+
def get_telemetry_client_with_processor(
65+
key: str, channel: TelemetryChannel, telemetry_processor: TelemetryProcessor = None
66+
) -> TelemetryClient:
67+
"""Gets a telemetry client instance with a telemetry processor.
68+
69+
:param key: instrumentation key
70+
:type key: str
71+
:param channel: Telemetry channel
72+
:type channel: TelemetryChannel
73+
:param telemetry_processor: use an existing telemetry processor from caller.
74+
:type telemetry_processor: TelemetryProcessor
75+
:return: a telemetry client with telemetry processor.
76+
:rtype: TelemetryClient
77+
"""
78+
client = TelemetryClient(key, channel)
79+
processor = (
80+
telemetry_processor
81+
if telemetry_processor is not None
82+
else DjangoTelemetryProcessor()
83+
)
84+
client.add_telemetry_processor(processor)
85+
return client
86+
87+
88+
def create_client(aisettings=None, telemetry_processor: TelemetryProcessor = None):
89+
global saved_clients, saved_channels # pylint: disable=invalid-name, global-statement
90+
91+
if aisettings is None:
92+
aisettings = load_settings()
93+
94+
if aisettings in saved_clients:
95+
return saved_clients[aisettings]
96+
97+
channel_settings = aisettings.channel_settings
98+
99+
if channel_settings in saved_channels:
100+
channel = saved_channels[channel_settings]
101+
else:
102+
sender = AsynchronousSender(service_endpoint_uri=channel_settings.endpoint)
103+
104+
if channel_settings.send_time is not None:
105+
sender.send_time = channel_settings.send_time
106+
if channel_settings.send_interval is not None:
107+
sender.send_interval = channel_settings.send_interval
108+
109+
queue = AsynchronousQueue(sender)
110+
channel = TelemetryChannel(None, queue)
111+
saved_channels[channel_settings] = channel
112+
113+
ikey = aisettings.ikey
114+
if ikey is None:
115+
return dummy_client("No ikey specified", telemetry_processor)
116+
117+
client = get_telemetry_client_with_processor(
118+
aisettings.ikey, channel, telemetry_processor
119+
)
120+
saved_clients[aisettings] = client
121+
return client
122+
123+
124+
def dummy_client(
125+
reason: str, telemetry_processor: TelemetryProcessor = None
126+
): # pylint: disable=unused-argument
127+
"""Creates a dummy channel so even if we're not logging telemetry, we can still send
128+
along the real object to things that depend on it to exist"""
129+
130+
sender = NullSender()
131+
queue = SynchronousQueue(sender)
132+
channel = TelemetryChannel(None, queue)
133+
client = get_telemetry_client_with_processor(
134+
"00000000-0000-0000-0000-000000000000", channel, telemetry_processor
135+
)
136+
return client
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Telemetry processor for Django."""
4+
import sys
5+
6+
from ..processor.telemetry_processor import TelemetryProcessor
7+
from .bot_telemetry_middleware import retrieve_bot_body
8+
9+
10+
class DjangoTelemetryProcessor(TelemetryProcessor):
11+
def can_process(self) -> bool:
12+
return self.detect_django()
13+
14+
def get_request_body(self) -> str:
15+
if self.detect_django():
16+
# Retrieve from Middleware cache
17+
return retrieve_bot_body()
18+
return None
19+
20+
@staticmethod
21+
def detect_django() -> bool:
22+
"""Detects if running in django."""
23+
return "django" in sys.modules
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from applicationinsights import logging
4+
5+
from . import common
6+
7+
8+
class LoggingHandler(logging.LoggingHandler):
9+
"""This class is a LoggingHandler that uses the same settings as the Django middleware to configure
10+
the telemetry client. This can be referenced from LOGGING in your Django settings.py file. As an
11+
example, this code would send all Django log messages--WARNING and up--to Application Insights:
12+
13+
.. code:: python
14+
15+
LOGGING = {
16+
'version': 1,
17+
'disable_existing_loggers': False,
18+
'handlers': {
19+
# The application insights handler is here
20+
'appinsights': {
21+
'class': 'applicationinsights.django.LoggingHandler',
22+
'level': 'WARNING'
23+
}
24+
},
25+
'loggers': {
26+
'django': {
27+
'handlers': ['appinsights'],
28+
'level': 'WARNING',
29+
'propagate': True,
30+
}
31+
}
32+
}
33+
34+
# You will need this anyway if you're using the middleware.
35+
# See the middleware documentation for more information on configuring
36+
# this setting:
37+
APPLICATION_INSIGHTS = {
38+
'ikey': '00000000-0000-0000-0000-000000000000'
39+
}
40+
"""
41+
42+
def __init__(self, *args, **kwargs):
43+
client = common.create_client()
44+
new_kwargs = {}
45+
new_kwargs.update(kwargs)
46+
new_kwargs["telemetry_channel"] = client.channel
47+
super(LoggingHandler, self).__init__(
48+
client.context.instrumentation_key, *args, **new_kwargs
49+
)

0 commit comments

Comments
 (0)