Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@
ApplicationInsightsTelemetryClient,
bot_telemetry_processor,
)
from .bot_telemetry_processor import BotTelemetryProcessor

__all__ = ["ApplicationInsightsTelemetryClient", "bot_telemetry_processor"]

__all__ = [
"ApplicationInsightsTelemetryClient",
"BotTelemetryProcessor",
"bot_telemetry_processor",
]

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Application Insights Telemetry Processor for Bots."""
from typing import List

from .django.django_telemetry_processor import DjangoTelemetryProcessor
from .flask.flask_telemetry_processor import FlaskTelemetryProcessor
from .processor.telemetry_processor import TelemetryProcessor


class BotTelemetryProcessor(TelemetryProcessor):
"""Application Insights Telemetry Processor for Bot"""

def __init__(self, processors: List[TelemetryProcessor] = None):
self._processors: List[TelemetryProcessor] = [
DjangoTelemetryProcessor(),
FlaskTelemetryProcessor(),
] if processors is None else processors

def can_process(self) -> bool:
for processor in self._processors:
if processor.can_process():
return True

return False

def get_request_body(self) -> str:
for inner in self._processors:
if inner.can_process():
return inner.get_request_body()

return super().get_request_body()
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Django Application Insights package."""

from .bot_telemetry_middleware import BotTelemetryMiddleware, retrieve_bot_body

__all__ = ["BotTelemetryMiddleware", "retrieve_bot_body"]
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Django Application Insights package."""

from . import common
from .bot_telemetry_middleware import BotTelemetryMiddleware
from .logging import LoggingHandler
from .middleware import ApplicationInsightsMiddleware


__all__ = [
"BotTelemetryMiddleware",
"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()
Original file line number Diff line number Diff line change
@@ -1,52 +1,56 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Bot Telemetry Middleware."""

from threading import current_thread

# Map of thread id => POST body text
_REQUEST_BODIES = {}


def retrieve_bot_body():
""" retrieve_bot_body
Retrieve the POST body text from temporary cache.
The POST body corresponds with the thread id and should resides in
cache just for lifetime of request.
"""
result = _REQUEST_BODIES.pop(current_thread().ident, None)
return result


class BotTelemetryMiddleware:
"""
Save off the POST body to later populate bot-specific properties to
add to Application Insights.

Example activating MIDDLEWARE in Django settings:
MIDDLEWARE = [
# Ideally add somewhere near top
'botbuilder.applicationinsights.django.BotTelemetryMiddleware',
...
]
"""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
self.process_request(request)
return self.get_response(request)

def process_request(self, request) -> bool:
"""Process the incoming Django request."""
# Bot Service doesn't handle anything over 256k
# TODO: Add length check
body_unicode = (
request.body.decode("utf-8") if request.method == "POST" else None
)
# Sanity check JSON
if body_unicode is not None:
# Integration layer expecting just the json text.
_REQUEST_BODIES[current_thread().ident] = body_unicode
return True
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Bot Telemetry Middleware."""

from threading import current_thread


# Map of thread id => POST body text
_REQUEST_BODIES = {}


def retrieve_bot_body():
""" retrieve_bot_body
Retrieve the POST body text from temporary cache.
The POST body corresponds with the thread id and should resides in
cache just for lifetime of request.
"""

result = _REQUEST_BODIES.get(current_thread().ident, None)
return result


class BotTelemetryMiddleware:
"""
Save off the POST body to later populate bot-specific properties to
add to Application Insights.

Example activating MIDDLEWARE in Django settings:
MIDDLEWARE = [
# Ideally add somewhere near top
'botbuilder.applicationinsights.django.BotTelemetryMiddleware',
...
]
"""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
self.process_request(request)
response = self.get_response(request)
_REQUEST_BODIES.pop(current_thread().ident, None)
return response

def process_request(self, request) -> bool:
"""Process the incoming Django request."""
# Bot Service doesn't handle anything over 256k
# TODO: Add length check
body_unicode = (
request.body.decode("utf-8") if request.method == "POST" else None
)
# Sanity check JSON
if body_unicode is not None:
# Integration layer expecting just the json text.
_REQUEST_BODIES[current_thread().ident] = body_unicode
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Common utilities for Django middleware."""
import collections

from applicationinsights import TelemetryClient
from applicationinsights.channel import (
AsynchronousQueue,
AsynchronousSender,
NullSender,
SynchronousQueue,
TelemetryChannel,
)
from django.conf import settings

from ..processor.telemetry_processor import TelemetryProcessor
from .django_telemetry_processor import DjangoTelemetryProcessor


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 = {} # pylint: disable=invalid-name
saved_channels = {} # pylint: disable=invalid-name


def get_telemetry_client_with_processor(
key: str, channel: TelemetryChannel, telemetry_processor: TelemetryProcessor = None
) -> TelemetryClient:
"""Gets a telemetry client instance with a telemetry processor.

:param key: instrumentation key
:type key: str
:param channel: Telemetry channel
:type channel: TelemetryChannel
:param telemetry_processor: use an existing telemetry processor from caller.
:type telemetry_processor: TelemetryProcessor
:return: a telemetry client with telemetry processor.
:rtype: TelemetryClient
"""
client = TelemetryClient(key, channel)
processor = (
telemetry_processor
if telemetry_processor is not None
else DjangoTelemetryProcessor()
)
client.add_telemetry_processor(processor)
return client


def create_client(aisettings=None, telemetry_processor: TelemetryProcessor = None):
global saved_clients, saved_channels # pylint: disable=invalid-name, global-statement

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:
sender = AsynchronousSender(service_endpoint_uri=channel_settings.endpoint)

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 = AsynchronousQueue(sender)
channel = TelemetryChannel(None, queue)
saved_channels[channel_settings] = channel

ikey = aisettings.ikey
if ikey is None:
return dummy_client("No ikey specified", telemetry_processor)

client = get_telemetry_client_with_processor(
aisettings.ikey, channel, telemetry_processor
)
saved_clients[aisettings] = client
return client


def dummy_client(
reason: str, telemetry_processor: TelemetryProcessor = None
): # pylint: disable=unused-argument
"""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 = NullSender()
queue = SynchronousQueue(sender)
channel = TelemetryChannel(None, queue)
client = get_telemetry_client_with_processor(
"00000000-0000-0000-0000-000000000000", channel, telemetry_processor
)
return client
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Telemetry processor for Django."""
import sys

from ..processor.telemetry_processor import TelemetryProcessor
from .bot_telemetry_middleware import retrieve_bot_body


class DjangoTelemetryProcessor(TelemetryProcessor):
def can_process(self) -> bool:
return self.detect_django()

def get_request_body(self) -> str:
if self.detect_django():
# Retrieve from Middleware cache
return retrieve_bot_body()
return None

@staticmethod
def detect_django() -> bool:
"""Detects if running in django."""
return "django" in sys.modules
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from applicationinsights import logging

from . import common


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