8000 Telemetry Middleware (#293) · sherlock666/botbuilder-python@9381c1b · GitHub
[go: up one dir, main page]

Skip to content

Commit 9381c1b

Browse files
davetaaxelsrz
authored andcommitted
Telemetry Middleware (microsoft#293)
* Add Telemetry Logger
1 parent 76176da commit 9381c1b

File tree

6 files changed

+573
-1
lines changed

6 files changed

+573
-1
lines changed

libraries/botbuilder-core/botbuilder/core/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
from .state_property_accessor import StatePropertyAccessor
3131
from .state_property_info import StatePropertyInfo
3232
from .storage import Storage, StoreItem, calculate_change_hash
33+
from .telemetry_constants import TelemetryConstants
34+
from .telemetry_logger_constants import TelemetryLoggerConstants
35+
from .telemetry_logger_middleware import TelemetryLoggerMiddleware
3336
from .turn_context import TurnContext
3437
from .user_state import UserState
3538
from .user_token_provider import UserTokenProvider
@@ -65,6 +68,9 @@
6568
"StatePropertyInfo",
6669
"Storage",
6770
"StoreItem",
71+
"TelemetryConstants",
72+
"TelemetryLoggerConstants",
73+
"TelemetryLoggerMiddleware",
6874
"TopIntent",
6975
"TurnContext",
7076
"UserState",

libraries/botbuilder-core/botbuilder/core/middleware_set.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class Middleware(ABC):
1212
@abstractmethod
13-
def on_process_request(
13+
async def on_process_request(
1414
self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
1515
):
1616
pass
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License
3+
4+
5+
class TelemetryConstants:
6+
"""Telemetry logger property names."""
7+
8+
CHANNEL_ID_PROPERTY: str = "channelId"
9+
CONVERSATION_ID_PROPERTY: str = "conversationId"
10+
CONVERSATION_NAME_PROPERTY: str = "conversationName"
11+
DIALOG_ID_PROPERTY: str = "dialogId"
12+
FROM_ID_PROPERTY: str = "fromId"
13+
FROM_NAME_PROPERTY: str = "fromName"
14+
LOCALE_PROPERTY: str = "locale"
15+
RECIPIENT_ID_PROPERTY: str = "recipientId"
16+
RECIPIENT_NAME_PROPERTY: str = "recipientName"
17+
REPLY_ACTIVITY_ID_PROPERTY: str = "replyActivityId"
18+
TEXT_PROPERTY: str = "text"
19+
SPEAK_PROPERTY: str = "speak"
20+
USER_ID_PROPERTY: str = "userId"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License
3+
4+
5+
class TelemetryLoggerConstants:
6+
"""The Telemetry Logger Event names."""
7+
8+
# The name of the event when a new message is received from the user.
9+
BOT_MSG_RECEIVE_EVENT: str = "BotMessageReceived"
10+
11+
# The name of the event when logged when a message is sent from the bot to the user.
12+
BOT_MSG_SEND_EVENT: str = "BotMessageSend"
13+
14+
# The name of the event when a message is updated by the bot.
15+
BOT_MSG_UPDATE_EVENT: str = "BotMessageUpdate"
16+
17+
# The name of the event when a message is deleted by the bot.
18+
BOT_MSG_DELETE_EVENT: str = "BotMessageDelete"
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Middleware Component for logging Activity messages."""
4+
5+
from typing import Awaitable, Callable, List, Dict
6+
from botbuilder.schema import Activity, ConversationReference, ActivityTypes
7+
from .bot_telemetry_client import BotTelemetryClient
8+
from .bot_assert import BotAssert
9+
from .middleware_set import Middleware
10+
from .null_telemetry_client import NullTelemetryClient
11+
from .turn_context import TurnContext
12+
from .telemetry_constants import TelemetryConstants
13+
from .telemetry_logger_constants import TelemetryLoggerConstants
14+
15+
16+
# pylint: disable=line-too-long
17+
class TelemetryLoggerMiddleware(Middleware):
18+
"""Middleware for logging incoming, outgoing, updated or deleted Activity messages."""
19+
20+
def __init__(
21+
self, telemetry_client: BotTelemetryClient, log_personal_information: bool
22+
) -> None:
23+
super(TelemetryLoggerMiddleware, self).__init__()
24+
self._telemetry_client = telemetry_client or NullTelemetryClient()
25+
self._log_personal_information = log_personal_information
26+
27+
@property
28+
def telemetry_client(self) -> BotTelemetryClient:
29+
"""Gets the currently configured BotTelemetryClient."""
30+
return self._telemetry_client
31+
32+
@property
33+
def log_personal_information(self) -> bool:
34+
""" Gets a value indicating whether determines whether to log personal
35+
information that came from the user."""
36+
return self._log_personal_information
37+
38+
# pylint: disable=arguments-differ
39+
async def on_process_request(
40+
self, context: TurnContext, logic_fn: Callable[[TurnContext], Awaitable]
41+
) -> None:
42+
"""Logs events based on incoming and outgoing activities using
43+
BotTelemetryClient base class
44+
45+
:param turn_context: The context object for this turn.
46+
:param logic: Callable to continue the bot middleware pipeline
47+
48+
:return: None
49+
"""
50+
BotAssert.context_not_none(context)
51+
52+
# Log incoming activity at beginning of turn
53+
if context.activity:
54+
activity = context.activity
55+
# Log Bot Message Received
56+
await self.on_receive_activity(activity)
57+
58+
# hook up onSend pipeline
59+
# pylint: disable=unused-argument
60+
async def send_activities_handler(
61+
ctx: TurnContext,
62+
activities: List[Activity],
63+
next_send: Callable[[], Awaitable[None]],
64+
):
65+
# Run full pipeline
66+
responses = await next_send()
67+
for activity in activities:
68+
await self.on_send_activity(activity)
69+
return responses
70+
71+
context.on_send_activities(send_activities_handler)
72+
73+
# hook up update activity pipeline
74+
async def update_activity_handler(
75+
ctx: TurnContext, activity: Activity, next_update: Callable[[], Awaitable]
76+
):
77+
# Run full pipeline
78+
response = await next_update()
79+
await self.on_update_activity(activity)
80+
return response
81+
82+
context.on_update_activity(update_activity_handler)
83+
84+
# hook up delete activity pipeline
85+
async def delete_activity_handler(
86+
ctx: TurnContext,
87+
reference: ConversationReference,
88+
next_delete: Callable[[], Awaitable],
89+
):
90+
# Run full pipeline
91+
await next_delete()
92+
93+
delete_msg = Activity(
94+
type=ActivityTypes.message_delete, id=reference.activity_id
95+
)
96+
deleted_activity: Activity = TurnContext.apply_conversation_reference(
97+
delete_msg, reference, False
98+
)
99+
await self.on_delete_activity(deleted_activity)
100+
101+
context.on_delete_activity(delete_activity_handler)
102+
103+
if logic_fn:
104+
await logic_fn()
105+
106+
async def on_receive_activity(self, activity: Activity) -> None:
107+
"""Invoked when a message is received from the user.
108+
Performs logging of telemetry data using the BotTelemetryClient.track_event() method.
109+
This event name used is "BotMessageReceived".
110+
:param activity: Current activity sent from user.
111+
"""
112+
self.telemetry_client.track_event(
113+
TelemetryLoggerConstants.BOT_MSG_RECEIVE_EVENT,
114+
await self.fill_receive_event_properties(activity),
115+
)
116+
117+
async def on_send_activity(self, activity: Activity) -> None:
118+
"""Invoked when the bot sends a message to the user.
119+
Performs logging of telemetry data using the BotTelemetryClient.track_event() method.
120+
This event name used is "BotMessageSend".
121+
:param activity: Current activity sent from bot.
122+
"""
123+
self.telemetry_client.track_event(
124+
TelemetryLoggerConstants.BOT_MSG_SEND_EVENT,
125+
await self.fill_send_event_properties(activity),
126+
)
127+
128+
async def on_update_activity(self, activity: Activity) -> None:
129+
"""Invoked when the bot updates a message.
130+
Performs logging of telemetry data using the BotTelemetryClient.track_event() method.
131+
This event name used is "BotMessageUpdate".
132+
:param activity: Current activity sent from user.
133+
"""
134+
self.telemetry_client.track_event(
135+
TelemetryLoggerConstants.BOT_MSG_UPDATE_EVENT,
136+
await self.fill_update_event_properties(activity),
137+
)
138+
139+
async def on_delete_activity(self, activity: Activity) -> None:
140+
"""Invoked when the bot deletes a message.
141+
Performs logging of telemetry data using the BotTelemetryClient.track_event() method.
142+
This event name used is "BotMessageDelete".
143+
:param activity: Current activity sent from user.
144+
"""
145+
self.telemetry_client.track_event(
146+
TelemetryLoggerConstants.BOT_MSG_DELETE_EVENT,
147+
await self.fill_delete_event_properties(activity),
148+
)
149+
150+
async def fill_receive_event_properties(
151+
self, activity: Activity, additional_properties: Dict[str, str] = None
152+
) -> Dict[str, str]:
153+
"""Fills the event properties for the BotMessageReceived.
154+
Adheres to the LogPersonalInformation flag to filter Name, Text and Speak properties.
155+
:param activity: activity sent from user.
156+
:param additional_properties: Additional properties to add to the event.
157+
Additional properties can override "stock" properties.
158+
159+
:return: A dictionary that is sent as "Properties" to
160+
BotTelemetryClient.track_event method for the BotMessageReceived event.
161+
"""
162+
properties = {
163+
TelemetryConstants.FROM_ID_PROPERTY: activity.from_property.id,
164+
TelemetryConstants.CONVERSATION_NAME_PROPERTY: activity.conversation.name,
165+
TelemetryConstants.LOCALE_PROPERTY: activity.locale,
166+
TelemetryConstants.RECIPIENT_ID_PROPERTY: activity.recipient.id,
167+
TelemetryConstants.RECIPIENT_NAME_PROPERTY: activity.from_property.name,
168+
}
169+
170+
if self.log_personal_information:
171+
if activity.from_property.name and activity.from_property.name.strip():
172+
properties[
173+
TelemetryConstants.FROM_NAME_PROPERTY
174+
] = activity.from_property.name
175+
if activity.text and activity.text.strip():
176+
properties[TelemetryConstants.TEXT_PROPERTY] = activity.text
177+
if activity.speak and activity.speak.strip():
178+
properties[TelemetryConstants.SPEAK_PROPERTY] = activity.speak
179+
180+
# Additional properties can override "stock" properties
181+
if additional_properties:
182+
for prop in additional_properties:
183+
properties[prop.key] = prop.value
184+
185+
return properties
186+
187+
async def fill_send_event_properties(
188+
self, activity: Activity, additional_properties: Dict[str, str] = None
189+
) -> Dict[str, str]:
190+
"""Fills the event properties for the BotMessageSend.
191+
These properties are logged when an activity message is sent by the Bot to the user.
192+
:param activity: activity sent from user.
193+
:param additional_properties: Additional properties to add to the event.
194+
Additional properties can override "stock" properties.
195+
196+
:return: A dictionary that is sent as "Properties" to the
197+
BotTelemetryClient.track_event method for the BotMessageSend event.
198+
"""
199+
properties = {
200+
TelemetryConstants.REPLY_ACTIVITY_ID_PROPERTY: activity.reply_to_id,
201+
TelemetryConstants.RECIPIENT_ID_PROPERTY: activity.recipient.id,
202+
TelemetryConstants.CONVERSATION_NAME_PROPERTY: activity.conversation.name,
203+
TelemetryConstants.LOCALE_PROPERTY: activity.locale,
204+
}
205+
206+
# Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples
207+
if self.log_personal_information:
208+
if activity.from_property.name and activity.from_property.name.strip():
209+
properties[
210+
TelemetryConstants.FROM_NAME_PROPERTY
211+
] = activity.from_property.name
212+
if activity.text and activity.text.strip():
213+
properties[TelemetryConstants.TEXT_PROPERTY] = activity.text
214+
if activity.speak and activity.speak.strip():
215+
properties[TelemetryConstants.SPEAK_PROPERTY] = activity.speak
216+
217+
# Additional properties can override "stock" properties
218+
if additional_properties:
219+
for prop in additional_properties:
220+
properties[prop.key] = prop.value
221+
222+
return properties
223+
224+
async def fill_update_event_properties(
225+
self, activity: Activity, additional_properties: Dict[str, str] = None
226+
) -> Dict[str, str]:
227+
"""Fills the event properties for the BotMessageUpdate.
228+
These properties are logged when an activity message is updated by the Bot.
229+
For example, if a card is interacted with by the use, and the card needs
230+
to be updated to reflect some interaction.
231+
:param activity: activity sent from user.
232+
:param additional_properties: Additional properties to add to the event.
233+
Additional properties can override "stock" properties.
234+
235+
:return: A dictionary that is sent as "Properties" to the
236+
BotTelemetryClient.track_event method for the BotMessageUpdate event.
237+
"""
238+
properties = {
239+
TelemetryConstants.RECIPIENT_ID_PROPERTY: activity.recipient.id,
240+
TelemetryConstants.CONVERSATION_ID_PROPERTY: activity.conversation.id,
241+
TelemetryConstants.CONVERSATION_NAME_PROPERTY: activity.conversation.name,
242+
TelemetryConstants.LOCALE_PROPERTY: activity.locale,
243+
}
244+
245+
# Use the LogPersonalInformation flag to toggle logging PII data, text is a common examples
246+
if self.log_personal_information:
247+
if activity.text and activity.text.strip():
248+
properties[TelemetryConstants.TEXT_PROPERTY] = activity.text
249+
250+
# Additional properties can override "stock" properties
251+
if additional_properties:
252+
for prop in additional_properties:
253+
properties[prop.key] = prop.value
254+
255+
return properties
256+
257+
async def fill_delete_event_properties(
258+
self, activity: Activity, additional_properties: Dict[str, str] = None
259+
) -> Dict[str, str]:
260+
"""Fills the event properties for the BotMessageDelete.
261+
These properties are logged when an activity message is deleted by the Bot.
262+
:param activity: activity sent from user.
263+
:param additional_properties: Additional properties to add to the event.
264+
Additional properties can override "stock" properties.
265+
266+
:return: A dictionary that is sent as "Properties" to the
267+
BotTelemetryClient.track_event method for the BotMessageUpdate event.
268+
"""
269+
properties = {
270+
TelemetryConstants.RECIPIENT_ID_PROPERTY: activity.recipient.id,
271+
TelemetryConstants.CONVERSATION_ID_PROPERTY: activity.conversation.id,
272+
TelemetryConstants.CONVERSATION_NAME_PROPERTY: activity.conversation.name,
273+
}
274+
275+
# Additional properties can override "stock" properties
276+
if additional_properties:
277+
for prop in additional_properties:
278+
properties[prop.key] = prop.value
279+
280+
return properties

0 commit comments

Comments
 (0)
0