402 строки
20 KiB
C#
402 строки
20 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// Uses a <see cref="IBotTelemetryClient"/> object to log incoming, outgoing, updated, or deleted message activities.
|
|
/// </summary>
|
|
public class TelemetryLoggerMiddleware : IMiddleware
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="TelemetryLoggerMiddleware"/> class.
|
|
/// </summary>
|
|
/// <param name="telemetryClient">The telemetry client to send telemetry events to.</param>
|
|
/// <param name="logPersonalInformation">`true` to include personally identifiable information; otherwise, `false`.</param>
|
|
public TelemetryLoggerMiddleware(IBotTelemetryClient telemetryClient, bool logPersonalInformation = false)
|
|
{
|
|
TelemetryClient = telemetryClient ?? new NullBotTelemetryClient();
|
|
LogPersonalInformation = logPersonalInformation;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether to include personal information that came from the user.
|
|
/// </summary>
|
|
/// <value>`true` to include personally identifiable information; otherwise, `false`.</value>
|
|
/// <remarks>
|
|
/// If true, personal information is included in calls to the telemetry client's
|
|
/// <see cref="IBotTelemetryClient.TrackEvent(string, IDictionary{string, string}, IDictionary{string, double})"/> method;
|
|
/// otherwise this information is filtered out.
|
|
/// </remarks>
|
|
public bool LogPersonalInformation { get; }
|
|
|
|
/// <summary>
|
|
/// Gets The telemetry client to send telemetry events to.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The <see cref="IBotTelemetryClient"/> this middleware uses to log events.
|
|
/// </value>
|
|
[JsonIgnore]
|
|
public IBotTelemetryClient TelemetryClient { get; }
|
|
|
|
/// <summary>
|
|
/// Logs events for incoming, outgoing, updated, or deleted message activities, using the <see cref="TelemetryClient"/>.
|
|
/// </summary>
|
|
/// <param name="context">The context object for this turn.</param>
|
|
/// <param name="nextTurn">The delegate to call to continue the bot middleware pipeline.</param>
|
|
/// <param name="cancellationToken">A cancellation token that can be used by other objects
|
|
/// or threads to receive notice of cancellation.</param>
|
|
/// <returns>A task that represents the work queued to execute.</returns>
|
|
/// <seealso cref="ITurnContext"/>
|
|
/// <seealso cref="Bot.Schema.IActivity"/>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the telemetry client's
|
|
/// <see cref="IBotTelemetryClient.TrackEvent(string, IDictionary{string, string}, IDictionary{string, double})"/> method to
|
|
/// log telemetry data when a message is received from the user.
|
|
/// The event name is <see cref="TelemetryLoggerConstants.BotMsgReceiveEvent"/>.
|
|
/// </summary>
|
|
/// <param name="activity">Current activity sent from user.</param>
|
|
/// <param name="cancellation">A cancellation token that can be used by other objects
|
|
/// or threads to receive notice of cancellation.</param>
|
|
/// <returns>A task that represents the work queued to execute.</returns>
|
|
protected virtual async Task OnReceiveActivityAsync(Activity activity, CancellationToken cancellation)
|
|
{
|
|
TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgReceiveEvent, await FillReceiveEventPropertiesAsync(activity).ConfigureAwait(false));
|
|
return;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the telemetry client's
|
|
/// <see cref="IBotTelemetryClient.TrackEvent(string, IDictionary{string, string}, IDictionary{string, double})"/> method to
|
|
/// log telemetry data when the bot sends the user a message. It uses the telemetry client's
|
|
/// The event name is <see cref="TelemetryLoggerConstants.BotMsgSendEvent"/>.
|
|
/// </summary>
|
|
/// <param name="activity">Current activity sent from user.</param>
|
|
/// <param name="cancellation">A cancellation token that can be used by other objects
|
|
/// or threads to receive notice of cancellation.</param>
|
|
/// <returns>A task that represents the work queued to execute.</returns>
|
|
protected virtual async Task OnSendActivityAsync(Activity activity, CancellationToken cancellation)
|
|
{
|
|
TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgSendEvent, await FillSendEventPropertiesAsync(activity).ConfigureAwait(false));
|
|
return;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the telemetry client's
|
|
/// <see cref="IBotTelemetryClient.TrackEvent(string, IDictionary{string, string}, IDictionary{string, double})"/> method to
|
|
/// log telemetry data when the bot updates a message it sent previously.
|
|
/// The event name is <see cref="TelemetryLoggerConstants.BotMsgUpdateEvent"/>.
|
|
/// </summary>
|
|
/// <param name="activity">Current activity sent from user.</param>
|
|
/// <param name="cancellation">A cancellation token that can be used by other objects
|
|
/// or threads to receive notice of cancellation.</param>
|
|
/// <returns>A task that represents the work queued to execute.</returns>
|
|
protected virtual async Task OnUpdateActivityAsync(Activity activity, CancellationToken cancellation)
|
|
{
|
|
TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgUpdateEvent, await FillUpdateEventPropertiesAsync(activity).ConfigureAwait(false));
|
|
return;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the telemetry client's
|
|
/// <see cref="IBotTelemetryClient.TrackEvent(string, IDictionary{string, string}, IDictionary{string, double})"/> method to
|
|
/// log telemetry data when the bot deletes a message it sent previously.
|
|
/// The event name is <see cref="TelemetryLoggerConstants.BotMsgDeleteEvent"/>.
|
|
/// </summary>
|
|
/// <param name="activity">Current activity sent from user.</param>
|
|
/// <param name="cancellation">A cancellation token that can be used by other objects
|
|
/// or threads to receive notice of cancellation.</param>
|
|
/// <returns>A task that represents the work queued to execute.</returns>
|
|
protected virtual async Task OnDeleteActivityAsync(Activity activity, CancellationToken cancellation)
|
|
{
|
|
TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgDeleteEvent, await FillDeleteEventPropertiesAsync(activity).ConfigureAwait(false));
|
|
return;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills event properties for the <see cref="TelemetryLoggerConstants.BotMsgReceiveEvent"/> event.
|
|
/// If the <see cref="LogPersonalInformation"/> is true, filters out the sender's name and the
|
|
/// message's text and speak fields.
|
|
/// </summary>
|
|
/// <param name="activity">The message activity sent from user.</param>
|
|
/// <param name="additionalProperties">Additional properties to add to the event.</param>
|
|
/// <returns>The properties and their values to log when a message is received from the user.</returns>
|
|
protected Task<Dictionary<string, string>> FillReceiveEventPropertiesAsync(Activity activity, Dictionary<string, string> additionalProperties = null)
|
|
{
|
|
if (activity == null)
|
|
{
|
|
return Task.FromResult(new Dictionary<string, string>());
|
|
}
|
|
|
|
var properties = new Dictionary<string, string>()
|
|
{
|
|
{ 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills event properties for the <see cref="TelemetryLoggerConstants.BotMsgSendEvent"/> event.
|
|
/// If the <see cref="LogPersonalInformation"/> is true, filters out the recipient's name and the
|
|
/// message's text and speak fields.
|
|
/// </summary>
|
|
/// <param name="activity">The user's activity to which the bot is responding.</param>
|
|
/// <param name="additionalProperties">Additional properties to add to the event.</param>
|
|
/// <returns>The properties and their values to log when the bot sends the user a message.</returns>
|
|
protected Task<Dictionary<string, string>> FillSendEventPropertiesAsync(Activity activity, Dictionary<string, string> additionalProperties = null)
|
|
{
|
|
if (activity == null)
|
|
{
|
|
return Task.FromResult(new Dictionary<string, string>());
|
|
}
|
|
|
|
var properties = new Dictionary<string, string>()
|
|
{
|
|
{ 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills event properties for the <see cref="TelemetryLoggerConstants.BotMsgUpdateEvent"/> event.
|
|
/// If the <see cref="LogPersonalInformation"/> is true, filters out the message's text field.
|
|
/// </summary>
|
|
/// <param name="activity">Last activity sent from user.</param>
|
|
/// <param name="additionalProperties">Additional properties to add to the event.</param>
|
|
/// <returns>The properties and their values to log when the bot updates a message it sent previously.</returns>
|
|
protected Task<Dictionary<string, string>> FillUpdateEventPropertiesAsync(Activity activity, Dictionary<string, string> additionalProperties = null)
|
|
{
|
|
if (activity == null)
|
|
{
|
|
return Task.FromResult(new Dictionary<string, string>());
|
|
}
|
|
|
|
var properties = new Dictionary<string, string>()
|
|
{
|
|
{ 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills event properties for the <see cref="TelemetryLoggerConstants.BotMsgDeleteEvent"/> event.
|
|
/// </summary>
|
|
/// <param name="activity">The Activity object deleted by bot.</param>
|
|
/// <param name="additionalProperties">Additional properties to add to the event.</param>
|
|
/// <returns>The properties and their values to log when the bot deletes a message it sent previously.</returns>
|
|
#pragma warning disable CA1822 // Mark members as static (can't change this without breaking binary compat)
|
|
protected Task<Dictionary<string, string>> FillDeleteEventPropertiesAsync(IMessageDeleteActivity activity, Dictionary<string, string> additionalProperties = null)
|
|
#pragma warning restore CA1822 // Mark members as static
|
|
{
|
|
if (activity == null)
|
|
{
|
|
return Task.FromResult(new Dictionary<string, string>());
|
|
}
|
|
|
|
var properties = new Dictionary<string, string>()
|
|
{
|
|
{ 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<string, string> properties)
|
|
{
|
|
switch (activity.ChannelId)
|
|
{
|
|
case Channels.Msteams:
|
|
var teamsChannelData = activity.GetChannelData<TeamsChannelData>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|