// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Connector; using Microsoft.Bot.Schema; using Microsoft.Bot.Schema.Teams; using Newtonsoft.Json; namespace Microsoft.Bot.Builder { /// /// Uses a object to log incoming, outgoing, updated, or deleted message activities. /// public class TelemetryLoggerMiddleware : IMiddleware { /// /// Initializes a new instance of the class. /// /// The telemetry client to send telemetry events to. /// `true` to include personally identifiable information; otherwise, `false`. public TelemetryLoggerMiddleware(IBotTelemetryClient telemetryClient, bool logPersonalInformation = false) { TelemetryClient = telemetryClient ?? new NullBotTelemetryClient(); LogPersonalInformation = logPersonalInformation; } /// /// Gets a value indicating whether to include personal information that came from the user. /// /// `true` to include personally identifiable information; otherwise, `false`. /// /// If true, personal information is included in calls to the telemetry client's /// method; /// otherwise this information is filtered out. /// public bool LogPersonalInformation { get; } /// /// Gets The telemetry client to send telemetry events to. /// /// /// The this middleware uses to log events. /// [JsonIgnore] public IBotTelemetryClient TelemetryClient { get; } /// /// Logs events for incoming, outgoing, updated, or deleted message activities, using the . /// /// The context object for this turn. /// The delegate to call to continue the bot middleware pipeline. /// A cancellation token that can be used by other objects /// or threads to receive notice of cancellation. /// A task that represents the work queued to execute. /// /// public virtual async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken) { BotAssert.ContextNotNull(context); // log incoming activity at beginning of turn if (context.Activity != null) { var activity = context.Activity; // Log Bot Message Received await OnReceiveActivityAsync(activity, cancellationToken).ConfigureAwait(false); } // hook up onSend pipeline context.OnSendActivities(async (ctx, activities, nextSend) => { // run full pipeline var responses = await nextSend().ConfigureAwait(false); foreach (var activity in activities) { await OnSendActivityAsync(activity, cancellationToken).ConfigureAwait(false); } return responses; }); // hook up update activity pipeline context.OnUpdateActivity(async (ctx, activity, nextUpdate) => { // run full pipeline var response = await nextUpdate().ConfigureAwait(false); await OnUpdateActivityAsync(activity, cancellationToken).ConfigureAwait(false); return response; }); // hook up delete activity pipeline context.OnDeleteActivity(async (ctx, reference, nextDelete) => { // run full pipeline await nextDelete().ConfigureAwait(false); var deleteActivity = new Activity { Type = ActivityTypes.MessageDelete, Id = reference.ActivityId, } .ApplyConversationReference(reference, isIncoming: false) .AsMessageDeleteActivity(); await OnDeleteActivityAsync((Activity)deleteActivity, cancellationToken).ConfigureAwait(false); }); if (nextTurn != null) { await nextTurn(cancellationToken).ConfigureAwait(false); } } /// /// Uses the telemetry client's /// method to /// log telemetry data when a message is received from the user. /// The event name is . /// /// Current activity sent from user. /// A cancellation token that can be used by other objects /// or threads to receive notice of cancellation. /// A task that represents the work queued to execute. protected virtual async Task OnReceiveActivityAsync(Activity activity, CancellationToken cancellation) { TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgReceiveEvent, await FillReceiveEventPropertiesAsync(activity).ConfigureAwait(false)); return; } /// /// Uses the telemetry client's /// method to /// log telemetry data when the bot sends the user a message. It uses the telemetry client's /// The event name is . /// /// Current activity sent from user. /// A cancellation token that can be used by other objects /// or threads to receive notice of cancellation. /// A task that represents the work queued to execute. protected virtual async Task OnSendActivityAsync(Activity activity, CancellationToken cancellation) { TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgSendEvent, await FillSendEventPropertiesAsync(activity).ConfigureAwait(false)); return; } /// /// Uses the telemetry client's /// method to /// log telemetry data when the bot updates a message it sent previously. /// The event name is . /// /// Current activity sent from user. /// A cancellation token that can be used by other objects /// or threads to receive notice of cancellation. /// A task that represents the work queued to execute. protected virtual async Task OnUpdateActivityAsync(Activity activity, CancellationToken cancellation) { TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgUpdateEvent, await FillUpdateEventPropertiesAsync(activity).ConfigureAwait(false)); return; } /// /// Uses the telemetry client's /// method to /// log telemetry data when the bot deletes a message it sent previously. /// The event name is . /// /// Current activity sent from user. /// A cancellation token that can be used by other objects /// or threads to receive notice of cancellation. /// A task that represents the work queued to execute. protected virtual async Task OnDeleteActivityAsync(Activity activity, CancellationToken cancellation) { TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgDeleteEvent, await FillDeleteEventPropertiesAsync(activity).ConfigureAwait(false)); return; } /// /// Fills event properties for the event. /// If the is true, filters out the sender's name and the /// message's text and speak fields. /// /// The message activity sent from user. /// Additional properties to add to the event. /// The properties and their values to log when a message is received from the user. protected Task> FillReceiveEventPropertiesAsync(Activity activity, Dictionary additionalProperties = null) { if (activity == null) { return Task.FromResult(new Dictionary()); } var properties = new Dictionary() { { TelemetryConstants.FromIdProperty, activity.From?.Id }, { TelemetryConstants.ConversationNameProperty, activity.Conversation?.Name }, { TelemetryConstants.LocaleProperty, activity.Locale }, { TelemetryConstants.RecipientIdProperty, activity.Recipient?.Id }, { TelemetryConstants.RecipientNameProperty, activity.Recipient?.Name }, { TelemetryConstants.ActivityTypeProperty, activity.Type }, { TelemetryConstants.ConversationIdProperty, activity.Conversation?.Id }, { TelemetryConstants.ActivityIdProperty, activity.Id }, }; // Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples if (LogPersonalInformation) { if (!string.IsNullOrWhiteSpace(activity.From?.Name)) { properties.Add(TelemetryConstants.FromNameProperty, activity.From?.Name); } if (!string.IsNullOrWhiteSpace(activity.Text)) { properties.Add(TelemetryConstants.TextProperty, activity.Text); } if (!string.IsNullOrWhiteSpace(activity.Speak)) { properties.Add(TelemetryConstants.SpeakProperty, activity.Speak); } } PopulateAdditionalChannelProperties(activity, properties); // Additional Properties can override "stock" properties. if (additionalProperties != null) { return Task.FromResult(additionalProperties.Concat(properties) .GroupBy(kv => kv.Key) .ToDictionary(g => g.Key, g => g.First().Value)); } return Task.FromResult(properties); } /// /// Fills event properties for the event. /// If the is true, filters out the recipient's name and the /// message's text and speak fields. /// /// The user's activity to which the bot is responding. /// Additional properties to add to the event. /// The properties and their values to log when the bot sends the user a message. protected Task> FillSendEventPropertiesAsync(Activity activity, Dictionary additionalProperties = null) { if (activity == null) { return Task.FromResult(new Dictionary()); } var properties = new Dictionary() { { TelemetryConstants.ReplyActivityIDProperty, activity.ReplyToId }, { TelemetryConstants.RecipientIdProperty, activity.Recipient?.Id }, { TelemetryConstants.ConversationNameProperty, activity.Conversation?.Name }, { TelemetryConstants.LocaleProperty, activity.Locale }, { TelemetryConstants.ActivityTypeProperty, activity.Type }, { TelemetryConstants.ConversationIdProperty, activity.Conversation?.Id }, { TelemetryConstants.ActivityIdProperty, activity.Id }, }; // Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples if (LogPersonalInformation) { if (!string.IsNullOrWhiteSpace(activity.Recipient.Name)) { properties.Add(TelemetryConstants.RecipientNameProperty, activity.Recipient.Name); } if (!string.IsNullOrWhiteSpace(activity.Text)) { properties.Add(TelemetryConstants.TextProperty, activity.Text); } if (!string.IsNullOrWhiteSpace(activity.Speak)) { properties.Add(TelemetryConstants.SpeakProperty, activity.Speak); } if (activity.Attachments != null && activity.Attachments.Any()) { properties.Add(TelemetryConstants.AttachmentsProperty, JsonConvert.SerializeObject(activity.Attachments, new JsonSerializerSettings { MaxDepth = null })); } } // Additional Properties can override "stock" properties. if (additionalProperties != null) { return Task.FromResult(additionalProperties.Concat(properties) .GroupBy(kv => kv.Key) .ToDictionary(g => g.Key, g => g.First().Value)); } return Task.FromResult(properties); } /// /// Fills event properties for the event. /// If the is true, filters out the message's text field. /// /// Last activity sent from user. /// Additional properties to add to the event. /// The properties and their values to log when the bot updates a message it sent previously. protected Task> FillUpdateEventPropertiesAsync(Activity activity, Dictionary additionalProperties = null) { if (activity == null) { return Task.FromResult(new Dictionary()); } var properties = new Dictionary() { { TelemetryConstants.RecipientIdProperty, activity.Recipient?.Id }, { TelemetryConstants.ConversationIdProperty, activity.Conversation?.Id }, { TelemetryConstants.ConversationNameProperty, activity.Conversation?.Name }, { TelemetryConstants.LocaleProperty, activity.Locale }, { TelemetryConstants.ActivityTypeProperty, activity.Type }, { TelemetryConstants.ActivityIdProperty, activity.Id }, }; // Use the LogPersonalInformation flag to toggle logging PII data, text is a common example if (LogPersonalInformation && !string.IsNullOrWhiteSpace(activity.Text)) { properties.Add(TelemetryConstants.TextProperty, activity.Text); } // Additional Properties can override "stock" properties. if (additionalProperties != null) { return Task.FromResult(additionalProperties.Concat(properties) .GroupBy(kv => kv.Key) .ToDictionary(g => g.Key, g => g.First().Value)); } return Task.FromResult(properties); } /// /// Fills event properties for the event. /// /// The Activity object deleted by bot. /// Additional properties to add to the event. /// The properties and their values to log when the bot deletes a message it sent previously. #pragma warning disable CA1822 // Mark members as static (can't change this without breaking binary compat) protected Task> FillDeleteEventPropertiesAsync(IMessageDeleteActivity activity, Dictionary additionalProperties = null) #pragma warning restore CA1822 // Mark members as static { if (activity == null) { return Task.FromResult(new Dictionary()); } var properties = new Dictionary() { { TelemetryConstants.RecipientIdProperty, activity.Recipient?.Id }, { TelemetryConstants.ConversationIdProperty, activity.Conversation?.Id }, { TelemetryConstants.ConversationNameProperty, activity.Conversation?.Name }, { TelemetryConstants.ActivityTypeProperty, activity.Type }, { TelemetryConstants.ActivityIdProperty, activity.Id }, }; // Additional Properties can override "stock" properties. if (additionalProperties != null) { return Task.FromResult(additionalProperties.Concat(properties) .GroupBy(kv => kv.Key) .ToDictionary(g => g.Key, g => g.First().Value)); } return Task.FromResult(properties); } private static void PopulateAdditionalChannelProperties(Activity activity, Dictionary properties) { switch (activity.ChannelId) { case Channels.Msteams: var teamsChannelData = activity.GetChannelData(); properties.Add("TeamsTenantId", teamsChannelData?.Tenant?.Id); properties.Add("TeamsUserAadObjectId", activity.From?.AadObjectId); if (teamsChannelData?.Team != null) { properties.Add("TeamsTeamInfo", JsonConvert.SerializeObject(teamsChannelData.Team, new JsonSerializerSettings { MaxDepth = null })); } break; } } } }