From ce80f31596f60b13a48c7c463f17a771cbb62332 Mon Sep 17 00:00:00 2001 From: congysu Date: Mon, 30 Sep 2019 15:01:21 -0700 Subject: [PATCH] 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. --- .../applicationinsights/__init__.py | 8 +- .../application_insights_telemetry_client.py | 525 +++++++++--------- .../bot_telemetry_processor.py | 32 ++ .../applicationinsights/django/__init__.py | 29 +- .../django/bot_telemetry_middleware.py | 108 ++-- .../applicationinsights/django/common.py | 136 +++++ .../django/django_telemetry_processor.py | 23 + .../applicationinsights/django/logging.py | 49 ++ .../applicationinsights/django/middleware.py | 291 ++++++++++ .../applicationinsights/flask/__init__.py | 4 +- .../flask/flask_telemetry_processor.py | 22 + .../integration_post_data.py | 53 -- .../applicationinsights/processor/__init__.py | 0 .../processor/telemetry_processor.py | 81 +++ .../django_tests/.gitignore | 1 + .../django_tests/tests.py | 105 ++-- .../django_tests/views.py | 4 +- 17 files changed, 1029 insertions(+), 442 deletions(-) create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/bot_telemetry_processor.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/django_telemetry_processor.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/middleware.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_processor.py delete mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/__init__.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py create mode 100644 libraries/botbuilder-applicationinsights/django_tests/.gitignore diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/__init__.py index 67240e21d..285abdfc1 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/__init__.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/__init__.py @@ -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", +] diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py index 1abd99dcc..ae660eb7b 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py @@ -1,276 +1,249 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Application Insights Telemetry Processor for Bots.""" - -import traceback -from typing import Dict -from applicationinsights import TelemetryClient # pylint: disable=no-name-in-module -from botbuilder.core.bot_telemetry_client import ( - BotTelemetryClient, - Severity, - TelemetryDataPointType, -) -from .integration_post_data import IntegrationPostData - - -def bot_telemetry_processor(data, context) -> bool: - """ Application Insights Telemetry Processor for Bot - Traditional Web user and session ID's don't apply for Bots. This processor - replaces the identifiers to be consistent with Bot Framework's notion of - user and session id's. - - Each event that gets logged (with this processor added) will contain additional - properties. - - The following properties are replaced: - - context.user.id - The user ID that Application Insights uses to identify - a unique user. - - context.session.id - The session ID that APplication Insights uses to - identify a unique session. - - In addition, the additional data properties are added: - - activityId - The Bot Framework's Activity ID which represents a unique - message identifier. - - channelId - The Bot Framework "Channel" (ie, slack/facebook/directline/etc) - - activityType - The Bot Framework message classification (ie, message) - - :param data: Data from Application Insights - :type data: telemetry item - :param context: Context from Application Insights - :type context: context object - :returns: bool -- determines if the event is passed to the server (False = Filtered). - """ - post_data = IntegrationPostData().activity_json - if post_data is None: - # If there is no body (not a BOT request or not configured correctly). - # We *could* filter here, but we're allowing event to go through. - return True - - # Override session and user id - from_prop = post_data["from"] if "from" in post_data else None - user_id = from_prop["id"] if from_prop is not None else None - channel_id = post_data["channelId"] if "channelId" in post_data else None - conversation = post_data["conversation"] if "conversation" in post_data else None - conversation_id = conversation["id"] if "id" in conversation else None - context.user.id = channel_id + user_id - context.session.id = conversation_id - - # Additional bot-specific properties - if "id" in post_data: - data.properties["activityId"] = post_data["id"] - if "channelId" in post_data: - data.properties["channelId"] = post_data["channelId"] - if "type" in post_data: - data.properties["activityType"] = post_data["type"] - return True - - -class ApplicationInsightsTelemetryClient(BotTelemetryClient): - """Application Insights Telemetry Client.""" - - def __init__( - self, instrumentation_key: str, telemetry_client: TelemetryClient = None - ): - self._instrumentation_key = instrumentation_key - self._client = ( - telemetry_client - if telemetry_client is not None - else TelemetryClient(self._instrumentation_key) - ) - # Telemetry Processor - self._client.add_telemetry_processor(bot_telemetry_processor) - - def track_pageview( - self, - name: str, - url: str, - duration: int = 0, - properties: Dict[str, object] = None, - measurements: Dict[str, object] = None, - ) -> None: - """ - Send information about the page viewed in the application (a web page for instance). - :param name: the name of the page that was viewed. - :param url: the URL of the page that was viewed. - :param duration: the duration of the page view in milliseconds. (defaults to: 0) - :param properties: the set of custom properties the client wants attached to this data item. - (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. - (defaults to: None) - """ - self._client.track_pageview(name, url, duration, properties, measurements) - - def track_exception( - self, - exception_type: type = None, - value: Exception = None, - trace: traceback = None, - properties: Dict[str, object] = None, - measurements: Dict[str, object] = None, - ) -> None: - """ - Send information about a single exception that occurred in the application. - :param exception_type: the type of the exception that was thrown. - :param value: the exception that the client wants to send. - :param trace: the traceback information as returned by :func:`sys.exc_info`. - :param properties: the set of custom properties the client wants attached to this data item. - (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. - (defaults to: None) - """ - self._client.track_exception( - exception_type, value, trace, properties, measurements - ) - - def track_event( - self, - name: str, - properties: Dict[str, object] = None, - measurements: Dict[str, object] = None, - ) -> None: - """ - Send information about a single event that has occurred in the context of the application. - :param name: the data to associate to this event. - :param properties: the set of custom properties the client wants attached to this data item. - (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. - (defaults to: None) - """ - self._client.track_event(name, properties=properties, measurements=measurements) - - def track_metric( - self, - name: str, - value: float, - tel_type: TelemetryDataPointType = None, - count: int = None, - min_val: float = None, - max_val: float = None, - std_dev: float = None, - properties: Dict[str, object] = None, - ) -> NotImplemented: - """ - Send information about a single metric data point that was captured for the application. - :param name: The name of the metric that was captured. - :param value: The value of the metric that was captured. - :param tel_type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) - :param count: the number of metrics that were aggregated into this data point. (defaults to: None) - :param min_val: the minimum of all metrics collected that were aggregated into this data point. - (defaults to: None) - :param max_val: the maximum of all metrics collected that were aggregated into this data point. - (defaults to: None) - :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. - (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. - (defaults to: None) - """ - self._client.track_metric( - name, value, tel_type, count, min_val, max_val, std_dev, properties - ) - - def track_trace( - self, name: str, properties: Dict[str, object] = None, severity: Severity = None - ): - """ - Sends a single trace statement. - :param name: the trace statement. - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL - """ - self._client.track_trace(name, properties, severity) - - def track_request( - self, - name: str, - url: str, - success: bool, - start_time: str = None, - duration: int = None, - response_code: str = None, - http_method: str = None, - properties: Dict[str, object] = None, - measurements: Dict[str, object] = None, - request_id: str = None, - ): - """ - Sends a single request that was captured for the application. - :param name: The name for this request. All requests with the same name will be grouped together. - :param url: The actual URL for this request (to show in individual request instances). - :param success: True if the request ended in success, False otherwise. - :param start_time: the start time of the request. The value should look the same as the one returned by - :func:`datetime.isoformat()` (defaults to: None) - :param duration: the number of milliseconds that this request lasted. (defaults to: None) - :param response_code: the response code that this request returned. (defaults to: None) - :param http_method: the HTTP method that triggered this request. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. - (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. - (defaults to: None) - :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None) - """ - self._client.track_request( - name, - url, - success, - start_time, - duration, - response_code, - http_method, - properties, - measurements, - request_id, - ) - - def track_dependency( - self, - name: str, - data: str, - type_name: str = None, - target: str = None, - duration: int = None, - success: bool = None, - result_code: str = None, - properties: Dict[str, object] = None, - measurements: Dict[str, object] = None, - dependency_id: str = None, - ): - """ - Sends a single dependency telemetry that was captured for the application. - :param name: the name of the command initiated with this dependency call. Low cardinality value. - Examples are stored procedure name and URL path template. - :param data: the command initiated by this dependency call. - Examples are SQL statement and HTTP URL with all query parameters. - :param type_name: the dependency type name. Low cardinality value for logical grouping of dependencies and - interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. - (default to: None) - :param target: the target site of a dependency call. Examples are server name, host address. - (default to: None) - :param duration: the number of milliseconds that this dependency call lasted. - (defaults to: None) - :param success: true if the dependency call ended in success, false otherwise. - (defaults to: None) - :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. - (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. - (defaults to: None) - :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None) - """ - self._client.track_dependency( - name, - data, - type_name, - target, - duration, - success, - result_code, - properties, - measurements, - dependency_id, - ) - - def flush(self): - """Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is - being used. - """ - self._client.flush() +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Application Insights Telemetry Client for Bots.""" + +import traceback +from typing import Dict, Callable + +from applicationinsights import TelemetryClient # pylint: disable=no-name-in-module +from botbuilder.core.bot_telemetry_client import ( + BotTelemetryClient, + Severity, + TelemetryDataPointType, +) + +from .bot_telemetry_processor import BotTelemetryProcessor + + +def bot_telemetry_processor(data, context) -> bool: + """Bot Telemetry Processor as a method for backward compatibility. Refer to + callable object :class:`BotTelemetryProcessor` for details. + + :param data: Data from Application Insights + :type data: telemetry item + :param context: Context from Application Insights + :type context: context object + :return: determines if the event is passed to the server (False = Filtered). + :rtype: bool + """ + processor = BotTelemetryProcessor() + return processor(data, context) + + +class ApplicationInsightsTelemetryClient(BotTelemetryClient): + """Application Insights Telemetry Client.""" + + def __init__( + self, + instrumentation_key: str, + telemetry_client: TelemetryClient = None, + telemetry_processor: Callable[[object, object], bool] = None, + ): + self._instrumentation_key = instrumentation_key + self._client = ( + telemetry_client + if telemetry_client is not None + else TelemetryClient(self._instrumentation_key) + ) + # Telemetry Processor + processor = ( + telemetry_processor + if telemetry_processor is not None + else bot_telemetry_processor + ) + self._client.add_telemetry_processor(processor) + + def track_pageview( + self, + name: str, + url: str, + duration: int = 0, + properties: Dict[str, object] = None, + measurements: Dict[str, object] = None, + ) -> None: + """ + Send information about the page viewed in the application (a web page for instance). + :param name: the name of the page that was viewed. + :param url: the URL of the page that was viewed. + :param duration: the duration of the page view in milliseconds. (defaults to: 0) + :param properties: the set of custom properties the client wants attached to this data item. + (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. + (defaults to: None) + """ + self._client.track_pageview(name, url, duration, properties, measurements) + + def track_exception( + self, + exception_type: type = None, + value: Exception = None, + trace: traceback = None, + properties: Dict[str, object] = None, + measurements: Dict[str, object] = None, + ) -> None: + """ + Send information about a single exception that occurred in the application. + :param exception_type: the type of the exception that was thrown. + :param value: the exception that the client wants to send. + :param trace: the traceback information as returned by :func:`sys.exc_info`. + :param properties: the set of custom properties the client wants attached to this data item. + (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. + (defaults to: None) + """ + self._client.track_exception( + exception_type, value, trace, properties, measurements + ) + + def track_event( + self, + name: str, + properties: Dict[str, object] = None, + measurements: Dict[str, object] = None, + ) -> None: + """ + Send information about a single event that has occurred in the context of the application. + :param name: the data to associate to this event. + :param properties: the set of custom properties the client wants attached to this data item. + (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. + (defaults to: None) + """ + self._client.track_event(name, properties=properties, measurements=measurements) + + def track_metric( + self, + name: str, + value: float, + tel_type: TelemetryDataPointType = None, + count: int = None, + min_val: float = None, + max_val: float = None, + std_dev: float = None, + properties: Dict[str, object] = None, + ) -> NotImplemented: + """ + Send information about a single metric data point that was captured for the application. + :param name: The name of the metric that was captured. + :param value: The value of the metric that was captured. + :param tel_type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) + :param count: the number of metrics that were aggregated into this data point. (defaults to: None) + :param min_val: the minimum of all metrics collected that were aggregated into this data point. + (defaults to: None) + :param max_val: the maximum of all metrics collected that were aggregated into this data point. + (defaults to: None) + :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. + (defaults to: None) + :param properties: the set of custom properties the client wants attached to this data item. + (defaults to: None) + """ + self._client.track_metric( + name, value, tel_type, count, min_val, max_val, std_dev, properties + ) + + def track_trace( + self, name: str, properties: Dict[str, object] = None, severity: Severity = None + ): + """ + Sends a single trace statement. + :param name: the trace statement. + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL + """ + self._client.track_trace(name, properties, severity) + + def track_request( + self, + name: str, + url: str, + success: bool, + start_time: str = None, + duration: int = None, + response_code: str = None, + http_method: str = None, + properties: Dict[str, object] = None, + measurements: Dict[str, object] = None, + request_id: str = None, + ): + """ + Sends a single request that was captured for the application. + :param name: The name for this request. All requests with the same name will be grouped together. + :param url: The actual URL for this request (to show in individual request instances). + :param success: True if the request ended in success, False otherwise. + :param start_time: the start time of the request. The value should look the same as the one returned by + :func:`datetime.isoformat()` (defaults to: None) + :param duration: the number of milliseconds that this request lasted. (defaults to: None) + :param response_code: the response code that this request returned. (defaults to: None) + :param http_method: the HTTP method that triggered this request. (defaults to: None) + :param properties: the set of custom properties the client wants attached to this data item. + (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. + (defaults to: None) + :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None) + """ + self._client.track_request( + name, + url, + success, + start_time, + duration, + response_code, + http_method, + properties, + measurements, + request_id, + ) + + def track_dependency( + self, + name: str, + data: str, + type_name: str = None, + target: str = None, + duration: int = None, + success: bool = None, + result_code: str = None, + properties: Dict[str, object] = None, + measurements: Dict[str, object] = None, + dependency_id: str = None, + ): + """ + Sends a single dependency telemetry that was captured for the application. + :param name: the name of the command initiated with this dependency call. Low cardinality value. + Examples are stored procedure name and URL path template. + :param data: the command initiated by this dependency call. + Examples are SQL statement and HTTP URL with all query parameters. + :param type_name: the dependency type name. Low cardinality value for logical grouping of dependencies and + interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. + (default to: None) + :param target: the target site of a dependency call. Examples are server name, host address. + (default to: None) + :param duration: the number of milliseconds that this dependency call lasted. + (defaults to: None) + :param success: true if the dependency call ended in success, false otherwise. + (defaults to: None) + :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. + (defaults to: None) + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. + (defaults to: None) + :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None) + """ + self._client.track_dependency( + name, + data, + type_name, + target, + duration, + success, + result_code, + properties, + measurements, + dependency_id, + ) + + def flush(self): + """Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is + being used. + """ + self._client.flush() diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/bot_telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/bot_telemetry_processor.py new file mode 100644 index 000000000..fa15a3225 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/bot_telemetry_processor.py @@ -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() diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py index b80ed4315..e111813dc 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py @@ -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() diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py index 943a60908..10b4b9b20 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py @@ -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 diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py new file mode 100644 index 000000000..a7f61588c --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py @@ -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 diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/django_telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/django_telemetry_processor.py new file mode 100644 index 000000000..5f3638681 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/django_telemetry_processor.py @@ -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 diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py new file mode 100644 index 000000000..dc36a362b --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py @@ -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 + ) diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/middleware.py new file mode 100644 index 000000000..9c2e67154 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/middleware.py @@ -0,0 +1,291 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import datetime +import inspect +import sys +import time +import uuid + +from applicationinsights.channel import TelemetryContext, contracts +from django.http import Http404 + +from . import common + + +try: + basestring # Python 2 +except NameError: # Python 3 + basestring = (str,) # pylint: disable=invalid-name + +# 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: + """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 ... + 'botbuilder.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 ... + 'botbuilder.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): # pylint: disable=useless-return + # 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 + + 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.track(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 = "" + + 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(): # pylint: disable=invalid-name + 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 isinstance(exception, Http404): + return None + + _, _, tb = sys.exc_info() # pylint: disable=invalid-name + if tb is None or exception is None: + # No actual traceback or exception info, don't bother logging. + return None + + client = common.get_telemetry_client_with_processor( + 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: + def __init__(self, client): + self._baseclient = client + self._client = None + self.request = contracts.RequestData() + self.request.id = str(uuid.uuid4()) + self.context = 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 = common.get_telemetry_client_with_processor( + 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): # pylint: disable=invalid-name + 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) diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py index 192e09d7c..7467c7a98 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT License. """Flask Application Insights package.""" -from .flask_telemetry_middleware import BotTelemetryMiddleware, retrieve_flask_body +from .flask_telemetry_middleware import BotTelemetryMiddleware -__all__ = ["BotTelemetryMiddleware", "retrieve_flask_body"] +__all__ = ["BotTelemetryMiddleware"] diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_processor.py new file mode 100644 index 000000000..301a6bbf9 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_processor.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Telemetry processor for Flask.""" +import sys + +from ..processor.telemetry_processor import TelemetryProcessor +from .flask_telemetry_middleware import retrieve_flask_body + + +class FlaskTelemetryProcessor(TelemetryProcessor): + def can_process(self) -> bool: + return self.detect_flask() + + def get_request_body(self) -> str: + if self.detect_flask(): + return retrieve_flask_body() + return None + + @staticmethod + def detect_flask() -> bool: + """Detects if running in flask.""" + return "flask" in sys.modules diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py deleted file mode 100644 index b8e636002..000000000 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Retrieve the POST request body from underlying web framework.""" - -import sys -import json -from botbuilder.applicationinsights.django import retrieve_bot_body -from botbuilder.applicationinsights.flask import retrieve_flask_body - - -class IntegrationPostData: - """ - Retrieve the POST body from the underlying framework: - - Flask - - Django - - (soon Tornado?) - - This class: - - Detects framework (currently flask or django) - - Pulls the current request body as a string - - Usage: - botdata = BotTelemetryData() - body = botdata.activity_json # Get current request body as json object - activity_id = body[id] # Get the ID from the POST body - """ - - def __init__(self): - pass - - @property - def activity_json(self) -> json: - """Retrieve the request body as json (Activity).""" - body_text = self.get_request_body() - body = json.loads(body_text) if body_text is not None else None - return body - - def get_request_body(self) -> str: # pylint: disable=inconsistent-return-statements - """Retrieve the request body from flask/django middleware component.""" - if self.detect_flask(): - return retrieve_flask_body() - - if self.detect_django(): - # Retrieve from Middleware cache - return retrieve_bot_body() - - def detect_flask(self) -> bool: - """Detects if running in flask.""" - return "flask" in sys.modules - - def detect_django(self) -> bool: - """Detects if running in django.""" - return "django" in sys.modules diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py new file mode 100644 index 000000000..7a15acb16 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +from abc import ABC, abstractmethod + + +class TelemetryProcessor(ABC): + """Application Insights Telemetry Processor base class for Bot""" + + @property + def activity_json(self) -> json: + """Retrieve the request body as json (Activity).""" + body_text = self.get_request_body() + body = json.loads(body_text) if body_text is not None else None + return body + + @abstractmethod + def can_process(self) -> bool: + """Whether the processor can process the request body. + :return: True if the request body can be processed, False otherwise. + :rtype: bool + """ + return False + + @abstractmethod + def get_request_body(self) -> str: # pylint: disable=inconsistent-return-statements + """Retrieve the request body from flask/django middleware component.""" + raise NotImplementedError() + + def __call__(self, data, context) -> bool: + """ Traditional Web user and session ID's don't apply for Bots. This processor + replaces the identifiers to be consistent with Bot Framework's notion of + user and session id's. + + Each event that gets logged (with this processor added) will contain additional + properties. + + The following properties are replaced: + - context.user.id - The user ID that Application Insights uses to identify + a unique user. + - context.session.id - The session ID that APplication Insights uses to + identify a unique session. + + In addition, the additional data properties are added: + - activityId - The Bot Framework's Activity ID which represents a unique + message identifier. + - channelId - The Bot Framework "Channel" (ie, slack/facebook/directline/etc) + - activityType - The Bot Framework message classification (ie, message) + + :param data: Data from Application Insights + :type data: telemetry item + :param context: Context from Application Insights + :type context: context object + :returns: bool -- determines if the event is passed to the server (False = Filtered). + """ + + post_data = self.activity_json + if post_data is None: + # If there is no body (not a BOT request or not configured correctly). + # We *could* filter here, but we're allowing event to go through. + return True + + # Override session and user id + from_prop = post_data["from"] if "from" in post_data else None + user_id = from_prop["id"] if from_prop is not None else None + channel_id = post_data["channelId"] if "channelId" in post_data else None + conversation = ( + post_data["conversation"] if "conversation" in post_data else None + ) + conversation_id = conversation["id"] if "id" in conversation else None + context.user.id = channel_id + user_id + context.session.id = conversation_id + + # Additional bot-specific properties + if "id" in post_data: + data.properties["activityId"] = post_data["id"] + if "channelId" in post_data: + data.properties["channelId"] = post_data["channelId"] + if "type" in post_data: + data.properties["activityType"] = post_data["type"] + return True diff --git a/libraries/botbuilder-applicationinsights/django_tests/.gitignore b/libraries/botbuilder-applicationinsights/django_tests/.gitignore new file mode 100644 index 000000000..e84001e2c --- /dev/null +++ b/libraries/botbuilder-applicationinsights/django_tests/.gitignore @@ -0,0 +1 @@ +aitest diff --git a/libraries/botbuilder-applicationinsights/django_tests/tests.py b/libraries/botbuilder-applicationinsights/django_tests/tests.py index ced7edd80..180aa72b2 100644 --- a/libraries/botbuilder-applicationinsights/django_tests/tests.py +++ b/libraries/botbuilder-applicationinsights/django_tests/tests.py @@ -1,30 +1,24 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os -import sys import logging +import os import django -from django.test import TestCase, Client, modify_settings, override_settings - -from botbuilder.applicationinsights import ( - ApplicationInsightsTelemetryClient, - bot_telemetry_processor, -) -from rest_framework.test import RequestsClient -from applicationinsights import TelemetryClient from applicationinsights.channel import ( - TelemetryChannel, - SynchronousQueue, - SenderBase, - NullSender, AsynchronousSender, + NullSender, + SenderBase, + SynchronousQueue, + TelemetryChannel, ) from applicationinsights.channel.SenderBase import ( DEFAULT_ENDPOINT_URL as DEFAULT_ENDPOINT, ) -from applicationinsights.django import common +from botbuilder.applicationinsights.django import common +from django.test import TestCase, modify_settings, override_settings +from rest_framework.test import RequestsClient + # Note: MIDDLEWARE_CLASSES for django.VERSION <= (1, 10) MIDDLEWARE_NAME = "MIDDLEWARE" @@ -62,7 +56,7 @@ def get_events(self, count): @modify_settings( **{ MIDDLEWARE_NAME: { - "append": "applicationinsights.django.ApplicationInsightsMiddleware", + "append": "botbuilder.applicationinsights.django.ApplicationInsightsMiddleware", "prepend": "botbuilder.applicationinsights.django.BotTelemetryMiddleware", } } @@ -101,12 +95,15 @@ def test_basic_request(self): self.assertEqual(data["responseCode"], 200, "Status code") self.assertEqual(data["success"], True, "Success value") self.assertEqual(data["url"], "http://testserver/", "Request url") - # TODO: Uncomment once we can inject our TelemetryProcessor to add properties - # self.assertEqual(tags["ai.user.id"], 'SLACKFROMID') - # self.assertEqual(tags["ai.session.id"], 'CONVERSATIONID') - # self.assertEqual(data['properties']['channelId'], 'SLACK', "channelId=SLACK") - # self.assertEqual(data['properties']['activityId'], 'bf3cc9a2f5de...', "activityID is set") - # self.assertEqual(data['properties']['activityType'], 'message', "activityType == message") + self.assertEqual(tags["ai.user.id"], "SLACKFROMID") + self.assertEqual(tags["ai.session.id"], "CONVERSATIONID") + self.assertEqual(data["properties"]["channelId"], "SLACK", "channelId=SLACK") + self.assertEqual( + data["properties"]["activityId"], "bf3cc9a2f5de...", "activityID is set" + ) + self.assertEqual( + data["properties"]["activityType"], "message", "activityType == message" + ) def test_bot_event(self): """Tests that traces logged from inside of a view are submitted and parented to the request telemetry item""" @@ -167,12 +164,15 @@ def test_logger(self): self.assertEqual(data["name"], "POST /logger", "Operation name") self.assertEqual(data["url"], "http://testserver/logger", "Request url") self.assertTrue(reqid, "Request id not empty") - # TODO: Uncomment once we can inject our TelemetryProcessor to add properties - # self.assertEqual(tags["ai.user.id"], 'SLACKFROMID') - # self.assertEqual(tags["ai.session.id"], 'CONVERSATIONID') - # self.assertEqual(data['properties']['channelId'], 'SLACK', "channelId=SLACK") - # self.assertEqual(data['properties']['activityId'], 'bf3cc9a2f5de...', "activityID is set") - # self.assertEqual(data['properties']['activityType'], 'message', "activityType == message") + self.assertEqual(tags["ai.user.id"], "SLACKFROMID") + self.assertEqual(tags["ai.session.id"], "CONVERSATIONID") + self.assertEqual(data["properties"]["channelId"], "SLACK", "channelId=SLACK") + self.assertEqual( + data["properties"]["activityId"], "bf3cc9a2f5de...", "activityID is set" + ) + self.assertEqual( + data["properties"]["activityType"], "message", "activityType == message" + ) # Check log event tags = logev["tags"] @@ -184,12 +184,15 @@ def test_logger(self): self.assertEqual(tags["ai.operation.parentId"], reqid, "Parent id") self.assertEqual(data["message"], "Logger message", "Log message") self.assertEqual(data["properties"]["property"], "value", "Property=value") - # TODO: Uncomment once we can inject our TelemetryProcessor to add properties - # self.assertEqual(tags["ai.user.id"], 'SLACKFROMID') - # self.assertEqual(tags["ai.session.id"], 'CONVERSATIONID') - # self.assertEqual(data['properties']['channelId'], 'SLACK', "channelId=SLACK") - # self.assertEqual(data['properties']['activityId'], 'bf3cc9a2f5de...', "activityID is set") - # self.assertEqual(data['properties']['activityType'], 'message', "activityType == message") + self.assertEqual(tags["ai.user.id"], "SLACKFROMID") + self.assertEqual(tags["ai.session.id"], "CONVERSATIONID") + self.assertEqual(data["properties"]["channelId"], "SLACK", "channelId=SLACK") + self.assertEqual( + data["properties"]["activityId"], "bf3cc9a2f5de...", "activityID is set" + ) + self.assertEqual( + data["properties"]["activityType"], "message", "activityType == message" + ) def test_thrower(self): """Tests that unhandled exceptions generate an exception telemetry item parented to the request telemetry item""" @@ -213,12 +216,15 @@ def test_thrower(self): self.assertEqual(data["name"], "POST /thrower", "Request name") self.assertEqual(data["url"], "http://testserver/thrower", "Request url") self.assertTrue(reqid, "Request id not empty") - # TODO: Uncomment once we can inject our TelemetryProcessor to add properties - # self.assertEqual(tags["ai.user.id"], 'SLACKFROMID') - # self.assertEqual(tags["ai.session.id"], 'CONVERSATIONID') - # self.assertEqual(data['properties']['channelId'], 'SLACK', "channelId=SLACK") - # self.assertEqual(data['properties']['activityId'], 'bf3cc9a2f5de...', "activityID is set") - # self.assertEqual(data['properties']['activityType'], 'message', "activityType == message") + self.assertEqual(tags["ai.user.id"], "SLACKFROMID") + self.assertEqual(tags["ai.session.id"], "CONVERSATIONID") + self.assertEqual(data["properties"]["channelId"], "SLACK", "channelId=SLACK") + self.assertEqual( + data["properties"]["activityId"], "bf3cc9a2f5de...", "activityID is set" + ) + self.assertEqual( + data["properties"]["activityType"], "message", "activityType == message" + ) # Check exception event tags = errev["tags"] @@ -234,12 +240,15 @@ def test_thrower(self): self.assertEqual( exc["parsedStack"][0]["method"], "thrower", "Stack frame method name" ) - # TODO: Uncomment once we can inject our TelemetryProcessor to add properties - # self.assertEqual(tags["ai.user.id"], 'SLACKFROMID') - # self.assertEqual(tags["ai.session.id"], 'CONVERSATIONID') - # self.assertEqual(data['properties']['channelId'], 'SLACK', "channelId=SLACK") - # self.assertEqual(data['properties']['activityId'], 'bf3cc9a2f5de...', "activityID is set") - # self.assertEqual(data['properties']['activityType'], 'message', "activityType == message") + self.assertEqual(tags["ai.user.id"], "SLACKFROMID") + self.assertEqual(tags["ai.session.id"], "CONVERSATIONID") + self.assertEqual(data["properties"]["channelId"], "SLACK", "channelId=SLACK") + self.assertEqual( + data["properties"]["activityId"], "bf3cc9a2f5de...", "activityID is set" + ) + self.assertEqual( + data["properties"]["activityType"], "message", "activityType == message" + ) def test_error(self): """Tests that Http404 exception does not generate an exception event @@ -350,7 +359,7 @@ def invoke_post(self, first_level_directory: str): @modify_settings( **{ MIDDLEWARE_NAME: { - "append": "applicationinsights.django.ApplicationInsightsMiddleware" + "append": "botbuilder.applicationinsights.django.ApplicationInsightsMiddleware" } } ) @@ -515,7 +524,7 @@ def test_default_send_interval(self): "version": 1, "handlers": { "appinsights": { - "class": "applicationinsights.django.LoggingHandler", + "class": "botbuilder.applicationinsights.django.LoggingHandler", "level": "INFO", } }, diff --git a/libraries/botbuilder-applicationinsights/django_tests/views.py b/libraries/botbuilder-applicationinsights/django_tests/views.py index 1bc345e89..181ca847c 100644 --- a/libraries/botbuilder-applicationinsights/django_tests/views.py +++ b/libraries/botbuilder-applicationinsights/django_tests/views.py @@ -1,10 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os from rest_framework.decorators import api_view -from applicationinsights import TelemetryClient -from applicationinsights.django import common +from botbuilder.applicationinsights.django import common from botbuilder.applicationinsights import ApplicationInsightsTelemetryClient from django.http import HttpResponse, Http404 from django.template.response import TemplateResponse