8000 Support and sample for flask/django · rsliang/botbuilder-python@a56b55f · GitHub
[go: up one dir, main page]

Skip to content

Commit a56b55f

Browse files
committed
Support and sample for flask/django
1 parent 80fe1f0 commit a56b55f

File tree

21 files changed

+506
-8
lines changed

21 files changed

+506
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import sys
4+
import traceback
5+
from applicationinsights import TelemetryClient
6+
from botbuilder.core.bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType
7+
from typing import Dict
8+
9+
class AppinsightsBotTelemetryClient(BotTelemetryClient):
10+
11+
def __init__(self, instrumentation_key:str):
12+
self._instrumentation_key = instrumentation_key
13+
14+
self._context = TelemetryContext()
15+
context.instrumentation_key = self._instrumentation_key
16+
# context.user.id = 'BOTID' # telemetry_channel.context.session.
17+
# context.session.id = 'BOTSESSION'
18+
19+
# set up channel with context
20+
self._channel = TelemetryChannel(context)
21+
# telemetry_channel.context.properties['my_property'] = 'my_value'
22+
23+
self._client = TelemetryClient(self._instrumentation_key, self._channel)
24+
25+
26+
def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dict[str, object]=None,
27+
measurements: Dict[str, object]=None) -> None:
28+
"""
29+
Send information about the page viewed in the application (a web page for instance).
30+
:param name: the name of the page that was viewed.
31+
:param url: the URL of the page that was viewed.
32+
:param duration: the duration of the page view in milliseconds. (defaults to: 0)
33+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
34+
:param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
35+
"""
36+
self._client.track_pageview(name, url, duration, properties, measurements)
37+
38+
def track_exception(self, type_exception: type = None, value : Exception =None, tb : traceback =None,
39+
properties: Dict[str, object]=None, measurements: Dict[str, object]=None) -> None:
40+
"""
41+
Send information about a single exception that occurred in the application.
42+
:param type_exception: the type of the exception that was thrown.
43+
:param value: the exception that the client wants to send.
44+
:param tb: the traceback information as returned by :func:`sys.exc_info`.
45+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
46+
:param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
47+
"""
48+
self._client.track_exception(type_exception, value, tb, properties, measurements)
49+
50+
def track_event(self, name: str, properties: Dict[str, object] = None,
51+
measurements: Dict[str, object] = None) -> None:
52+
"""
53+
Send information about a single event that has occurred in the context of the application.
54+
:param name: the data to associate to this event.
55+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
56+
:param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
57+
"""
58+
self._client.track_event(name, properties, measurements)
59+
60+
def track_metric(self, name: str, value: float, type: TelemetryDataPointType =None,
61+
count: int =None, min: float=None, max: float=None, std_dev: float=None,
62+
properties: Dict[str, object]=None) -> NotImplemented:
63+
"""
64+
Send information about a single metric data point that was captured for the application.
65+
:param name: The name of the metric that was captured.
66+
:param value: The value of the metric that was captured.
67+
:param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`)
68+
:param count: the number of metrics that were aggregated into this data point. (defaults to: None)
69+
:param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None)
70+
:param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None)
71+
:param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None)
72+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
73+
"""
74+
self._client.track_metric(name, value, type, count, min, max, std_dev, properties)
75+
76+
def track_trace(self, name: str, properties: Dict[str, object]=None, severity=None):
77+
"""
78+
Sends a single trace statement.
79+
:param name: the trace statement.\n
80+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n
81+
:param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL
82+
"""
83+
self._client.track_trace(name, properties, severity)
84+
85+
def track_request(self, name: str, url: str, success: bool, start_time: str=None,
86+
duration: int=None, response_code: str =None, http_method: str=None,
87+
properties: Dict[str, object]=None, measurements: Dict[str, object]=None,
88+
request_id: str=None):
89+
"""
90+
Sends a single request that was captured for the application.
91+
:param name: The name for this request. All requests with the same name will be grouped together.
92+
:param url: The actual URL for this request (to show in individual request instances).
93+
:param success: True if the request ended in success, False otherwise.
94+
: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)
95+
:param duration: the number of milliseconds that this request lasted. (defaults to: None)
96+
:param response_code: the response code that this request returned. (defaults to: None)
97+
:param http_method: the HTTP method that triggered this request. (defaults to: None)
98+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
99+
:param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
100+
:param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None)
101+
"""
102+
self._client.track_request(name, url, success, start_time, duration, response_code, http_method, properties,
103+
measurements, request_id)
104+
105+
def track_dependency(self, name:str, data:str, type:str=None, target:str=None, duration:int=None,
106+
success:bool=None, result_code:str=None, properties:Dict[str, object]=None,
107+
measurements:Dict[str, object]=None, dependency_id:str=None):
108+
"""
109+
Sends a single dependency telemetry that was captured for the application.
110+
:param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template.
111+
:param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters.
112+
:param type: 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)
113+
:param target: the target site of a dependency call. Examples are server name, host address. (default to: None)
114+
:param duration: the number of milliseconds that this dependency call lasted. (defaults to: None)
115+
:param success: true if the dependency call ended in success, false otherwise. (defaults to: None)
116+
:param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None)
117+
:param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
118+
:param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
119+
:param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None)
120+
"""
121+
self._client.track_dependency(name, data, type, target, duration, success, result_code, properties,
122+
measurements, dependency_id)
123+

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,36 @@
55
from applicationinsights import TelemetryClient
66
from botbuilder.core.bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType
77
from typing import Dict
8+
from .integration_post_data import IntegrationPostData
89

910
class ApplicationInsightsTelemetryClient(BotTelemetryClient):
10-
11+
1112
def __init__(self, instrumentation_key:str):
1213
self._instrumentation_key = instrumentation_key
1314
self._client = TelemetryClient(self._instrumentation_key)
15+
# Telemetry Processor
16+
def telemetry_processor(data, context):
17+
post_data = IntegrationPostData().activity_json
18+
# Override session and user id
19+
from_prop = post_data['from'] if 'from' in post_data else None
20+
user_id = from_prop['id'] if from_prop != None else None
21+
channel_id = post_data['channelId'] if 'channelId' in post_data else None
22+
conversation = post_data['conversation'] if 'conversation' in post_data else None
23+
conversation_id = conversation['id'] if 'id' in conversation else None
24+
context.user.id = channel_id + user_id
25+
context.session.id = conversation_id
26+
27+
# Additional bot-specific properties
28+
if 'activityId' in post_data:
29+
data.properties["activityId"] = post_data['activityId']
30+
if 'channelId' in post_data:
31+
data.properties["channelId"] = post_data['channelId']
32+
if 'activityType' in post_data:
33+
data.properties["activityType"] = post_data['activityType']
34+
35+
self._client.add_telemetry_processor(telemetry_processor)
1436

15-
37+
1638
def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dict[str, object]=None,
1739
measurements: Dict[str, object]=None) -> None:
1840
"""
@@ -111,3 +133,10 @@ def track_dependency(self, name:str, data:str, type:str=None, target:str=None, d
111133
self._client.track_dependency(name, data, type, target, duration, success, result_code, properties,
112134
measurements, dependency_id)
113135

136+
def flush(self):
137+
"""Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is
138+
being used.
139+
"""
140+
self._client.flush()
141+
142+
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import sys
5+
import gc
6+
import imp
7+
import json
8+
from botbuilder.schema import Activity
9+
10+
class IntegrationPostData:
11+
"""
12+
Retrieve the POST body from the underlying framework:
13+
- Flask
14+
- Django
15+
- (soon Tornado?)
16+
17+
This class:
18+
- Detects framework (currently flask or django)
19+
- Pulls the current request body as a string
20+
21+
Usage:
22+
botdata = BotTelemetryData()
23+
body = botdata.activity_json # Get current request body as json object
24+
activity_id = body[id] # Get the ID from the POST body
25+
"""
26+
def __init__(self):
27+
pass
28+
29+
@property
30+
def activity_json(self) -> json:
31+
body_text = self.get_request_body()
32+
#print(f"ACTIVITY_JSON: Body{body_text}", file=sys.stderr)
33+
body = json.loads(body_text) if body_text != None else None
34+
return body
35+
36+
def get_request_body(self) -> str:
37+
if self.detect_flask():
38+
flask_app = self.get_flask_app()
39+
40+
with flask_app.app_context():
41+
mod = __import__('flask', fromlist=['Flask'])
42+
request = getattr(mod, 'request')
43+
body = self.body_from_WSGI_environ(request.environ)
44+
return body
45+
else:
46+
if self.detect_django():
47+
mod = __import__('django.http', fromlist=['http'])
48+
http_request = getattr(mod, 'HttpRequest')
49+
django_requests = [o for o in gc.get_objects() if isinstance(o, http_request)]
50+
django_request_instances = len(django_requests)
51+
if django_request_instances != 1:
52+
raise Exception(f'Detected {django_request_instances} instances of Django Requests. Expecting 1.')
53+
request = django_requests[0]
54+
body_unicode = request.body.decode('utf-8') if request.method == "POST" else None
55+
return body_unicode
56+
57+
def body_from_WSGI_environ(self, environ):
58+
try:
59+
request_body_size = int(environ.get('CONTENT_LENGTH', 0))
60+
except (ValueError):
61+
request_body_size = 0
62+
request_body = environ['wsgi.input'].read(request_body_size)
63+
return request_body
64+
65+
def detect_flask(self) -> bool:
66+
return "flask" in sys.modules
67+
68+
def detect_django(self) -> bool:
69+
return "django" in sys.modules
70+
71+
def resolve_flask_type(self) -> 'Flask':
72+
mod = __import__('flask', fromlist=['Flask'])
73+
flask_type = getattr(mod, 'Flask')
74+
return flask_type
75+
76+
def get_flask_app(self) -> 'Flask':
77+
flask = [o for o in gc.get_objects() if isinstance(o, self.resolve_flask_type())]
78+
flask_instances = len(flask)
79+
if flask_instances <= 0 or flask_instances > 1:
80+
raise Exception(f'Detected {flask_instances} instances of flask. Expecting 1.')
81+
app = flask[0]
82+
return app

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

Whitespace-only changes.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""
2+
Django settings for django_sample project.
3+
4+
Generated by 'django-admin startproject' using Django 2.2.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/2.2/topics/settings/
8+
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/2.2/ref/settings/
11+
"""
12+
13+
import os
14+
15+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17+
18+
19+
# Quick-start development settings - unsuitable for production
20+
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
21+
22+
# SECURITY WARNING: keep the secret key used in production secret!
23+
SECRET_KEY = 'rf#-23wei#$12uuwh25s=y29zi8-e86a&sfpo#mb6^q&z(q=lu'
24+
25+
# SECURITY WARNING: don't run with debug turned on in production!
26+
DEBUG = True
27+
28+
ALLOWED_HOSTS = []
29+
30+
31+
# Application definition
32+
33+
INSTALLED_APPS = [
34+
'django.contrib.admin',
35+
'django.contrib.auth',
36+
'django.contrib.contenttypes',
37+
'django.contrib.sessions',
38+
'django.contrib.messages',
39+
'django.contrib.staticfiles',
40+
]
41+
42+
MIDDLEWARE = [
43+
'django.middleware.security.SecurityMiddleware',
44+
'django.contrib.sessions.middleware.SessionMiddleware',
45+
'django.middleware.common.CommonMiddleware',
46+
'django.middleware.csrf.CsrfViewMiddleware',
47+
'django.contrib.auth.middleware.AuthenticationMiddleware',
48+
'django.contrib.messages.middleware.MessageMiddleware',
49+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
50+
]
51+
52+
ROOT_URLCONF = 'django_sample.urls'
53+
54+
TEMPLATES = [
55+
{
56+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
57+
'DIRS': [],
58+
'APP_DIRS': True,
59+
'OPTIONS': {
60+
'context_processors': [
61+
'django.template.context_processors.debug',
62+
'django.template.context_processors.request',
63+
'django.contrib.auth.context_processors.auth',
64+
'django.contrib.messages.context_processors.messages',
65+
],
66+
},
67+
},
68+
]
69+
70+
WSGI_APPLICATION = 'django_sample.wsgi.application'
71+
72+
73+
# Database
74+
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
75+
76+
DATABASES = {
77+
'default': {
78+
'ENGINE': 'django.db.backends.sqlite3',
79+
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
80+
}
81+
}
82+
83+
84+
# Password validation
85+
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
86+
87+
AUTH_PASSWORD_VALIDATORS = [
88+
{
89+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
90+
},
91+
{
92+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
93+
},
94+
{
95+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
96+
},
97+
{
98+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
99+
},
100+
]
101+
102+
103+
# Internationalization
104+
# https://docs.djangoproject.com/en/2.2/topics/i18n/
105+
106+
LANGUAGE_CODE = 'en-us'
107+
108+
TIME_ZONE = 'UTC'
109+
110+
USE_I18N = True
111+
112+
USE_L10N = True
113+
114+
USE_TZ = True
115+
116+
117+
# Static files (CSS, JavaScript, Images)
118+
# https://docs.djangoproject.com/en/2.2/howto/static-files/
119+
120+
STATIC_URL = '/static/'

0 commit comments

Comments
 (0)
0