вторник, 13 апреля 2010 г.

Отчёт использования CRM пользователями системы

Один из моих клиентов пожелал иметь возможность просматривать лог работы пользователей в CRM. Я знал, что MVP Дэвид Дженнавей (David Jennaway) создал такое решение основываясь на логах IIS. Но как я не бился - у меня не получилось заставить IIS записывать лог в базу. Также данный подход не будет работать в сценарие IFD, так как данная схема аутентификации (на уровне форм) не позволит нам отличить одного пользователя от другого.

Оставив эту идею я занялся изобретением своего велосипеда.

Решение будет состоять из 3-ёх частей:
1. База, в которую будет записываться информация о работе пользователей в системе.
2. Плагин, который будет выполнять запись этой информации (его необходимо будет зарегистрировать на все наиболее часто используемые сообщения - Execute, RetrieveMultiple, Retrieve, Create, Update, Delete).
3. Разработка отчёта, который будет отображать данные.

База. Я решил не использовать базу CRM для хранения информации о работе пользователей по причине быстродействия. После создания базы я выполнил создание таблицы, в которую сохранялись бы данные. Можно использовать следующий скрипт для создания такой таблицы:

CREATE TABLE [dbo].[UserLog](
[UserId] [uniqueidentifier] NULL,
[UserName] [varchar](max) NULL,
[OrgName] [varchar](max) NULL,
[RecordDateTime] [datetime] NULL,
[SourceHost] [varchar](max) NULL
) ON [PRIMARY]

GO


Плагин

Задача возлагаемая на плагин - получение информации о пользователе, организации и IP адресе компьютера, с которого был выполнен вход в CRM. Его код:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;
using System.Data.SqlClient;
using System.Web;

namespace UserActionsLogger
{
public class UALogger : IPlugin
{

#region Privates

private readonly string _connectionString = string.Empty;

#endregion Privates

#region CTOR

public UALogger(string config, string secureConfig)
{
_connectionString = config;
}

#endregion CTOR

#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
//Проверка на то, что автором вызова является приложение, а не асинхронный сервис или вебсервис
if (context.CallerOrigin == CallerOrigin.AsyncService ||
context.CallerOrigin == CallerOrigin.WebServiceApi)
return;

try
{
//Получение IP ПК, с которого выполнен вход в CRM
string hostname = string.Empty;
HttpContext webContext = HttpContext.Current;
if (webContext != null)
hostname = webContext.Request.UserHostName;
if (webContext != null && hostname != string.Empty)
hostname = webContext.Request.UserHostAddress;

//Получение имени пользователя для лога
Guid curentUserId = context.UserId;
ICrmService crmservice = context.CreateCrmService(true);
systemuser su = (systemuser)crmservice.Retrieve(EntityName.systemuser.ToString(), curentUserId, new ColumnSet(new string[] {"fullname"}));
string username = su.fullname;

//Отсеивание системных учётных записей
if (username.ToUpper() == "SYSTEM" ||
username.ToUpper() == "INTEGRATION")
return;

//Запись данных в лог
using (SqlConnection connection = new SqlConnection(_connectionString))
{
connection.Open();

using (SqlCommand cmd = new SqlCommand("", connection))
{
cmd.CommandText = "Insert Into UserLog(UserId, UserName, OrgName, RecordDateTime, SourceHost) Values(@UserId, @UserName, @OrgName, @recordDateTime, @SourceHost)";
cmd.Parameters.AddWithValue("@UserId", curentUserId);
cmd.Parameters.AddWithValue("@UserName", username);
cmd.Parameters.AddWithValue("@OrgName", context.OrganizationName);
cmd.Parameters.AddWithValue("@RecordDateTime", DateTime.Now);
cmd.Parameters.AddWithValue("@SourceHost", hostname);

cmd.ExecuteNonQuery();
}

connection.Close();
}
}
catch { }
}

#endregion IPlugin Members
}
}


В параметре config плагина необходимо указать строку подключения к базе с логами. Пример регистрации шага на сообщение Execute:



Отчёт
Поскольку в CRM нет понятия сессионности, то очень непросто понять когда же пользователь перестал работать в CRM. Для того, чтобы обойти эту проблему был использован метод из выше указанной статьи - разбивка анализируемого интервала на отрезки с определённой частотой. Если на такой интервал существовала хоть одна запись в логе, принималось, что пользователь работал в системе.

Параметрами для отчёта были выбраны интервал анализа работы пользователей в системе и интервал дробления (5, 10, 15, 20, 30, 60 минут). Далее представлено "сердце" отчёта - запрос:

--Создание временной таблицы, 
--которая содержит все анализируемые интервалы
Create Table #TimeTable(StartDate DateTime, EndDate DateTime)

--Заполнение данной таблицы при помощи цикла
while @StartDate < @EndDate
Begin
Insert Into #TimeTable
Values(@StartDate, DATEADD(minute, @Delta, @StartDate))

Set @StartDate = DATEADD(minute, @Delta, @StartDate)
End

--Получение данных для отчёта
Select
Distinct
t.StartDate
,t.EndDate
,UserName
,SourceHost
From
#TimeTable t
Inner Join UserLog u
on u.RecordDateTime > t.StartDate
And u.RecordDateTime <= t.EndDate

--Удаление временной таблицы
Drop Table #TimeTable


Результат можно увидеть на скриншоте:

Исходный код плагина и отчёта:

3 комментария:

  1. Интересно, спасибо. Надо попробовать у себя

    ОтветитьУдалить
  2. Привет.
    Расточительно на каждый INSERT делать отдельное соединение к БД. Может использовать connection pool ?

    ОтветитьУдалить
  3. Спасибо за комментарий.
    Можно и синглтон сделать, но на момент когда реализовывалось решение - это было некритично.

    ОтветитьУдалить