Auditoría de datos en Microsoft CRM

El otro día trabajando con un cliente de Plain Concepts me plantearon una cuestión sobre Microsoft CRM bastante interesante ¿Incluye de serie Microsoft CRM algún mecanismo que nos permita auditar las modificaciones de datos en el CRM? Como muchos ya sabéis, Microsoft CRM 3.0 no incluye ningún mecanismo para realizar un log de modificaciones sobre los datos del CRM… Pero sí incluye un magnífico sistema de CallOuts que nos permite crear un sencillo log de modificaciones en una hora o menos.


Ya se os está ocurriendo como hacerlo ¿No? Bueno la idea es sencilla, creamos un CallOut que maneje todos los tipos de métodos de post callout (PostCreate, PostAssing, PostUpdate, PostDelete, etc…) y que se encargue de escribir la información en un fichero de log por ejemplo. ¿Por qué post callout? Pues porque cuando Microsoft CRM dispara un post callout la información ya ha sido tratada por la plataforma, lo que quiere decir que la modificación es en firme, mientras que en los pre callouts no tienes garantizado que el cambio porque el sistema todavía no ha procesado la operación.


Además hay otro aspecto básico que debemos de tener muy en cuenta al trabajar con los callouts. Los callouts se ejecutan siempre de forma síncrona con el proceso del Servidor de Microsoft CRM, tanto pre como post callouts, lo que quiere decir que el hilo de ejecución va a esperar a que el callout termine para seguir. Esto, en ocasiones es aceptable e incluso necesario (pe: cancelar una operación en un precallout), pero en muchos otros casos (sobre todo en post callouts) no es aceptable ya que podemos estar realizando llamadas a Web Services o escribiendo en ficheros lo que ralentizaría el interfaz de usuario del CRM, llegando incluso a poder provocar time outs. Por lo tanto deberemos lanzar los procesos “pesados” en otro hilo de ejecución.


Un ejemplo de código para este callout podría ser el siguiente, pero antes de nada os aviso que lo acabo de escribir y ni siquiera he tenido ocasión de probarlo. Así que no hay ninguna garantía de que funcione correctamente, si lo probáis o lo mejoráis hacédmelo saber dejando un comentario. Por cierto, lo he escrito utilizando la plantilla de proyecto para callouts.


 







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114


using System;
using System.Text;
using Microsoft.Crm.Callout;
using System.Threading;
using System.IO;
using System.Diagnostics;

namespace PlainConcepts.CRMTools.Logging
{
public class LogCallout : CrmCalloutBase
{

public override void PostAssign(CalloutUserContext userContext, CalloutEntityContext entityContext,
string preImageEntityXml, string postImageEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.Assign, userContext.UserId.ToString(), preImageEntityXml, postImageEntityXml);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}

public override void PostCreate(CalloutUserContext userContext, CalloutEntityContext entityContext,
string postImageEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.Create, userContext.UserId.ToString(), postImageEntityXml, null);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}


public override void PostDelete(CalloutUserContext userContext, CalloutEntityContext entityContext,
string preImageEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.Delete, userContext.UserId.ToString(), preImageEntityXml, null);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}


public override void PostDeliver(CalloutUserContext userContext, CalloutEntityContext entityContext,
string postImageEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.Deliver, userContext.UserId.ToString(), postImageEntityXml, null);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}

public override void PostMerge(CalloutUserContext userContext, CalloutEntityContext entityContext,
string preImageMasterEntityXml, string preImageSubordinateEntityXml, string postImageMasterEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.Merge, userContext.UserId.ToString(), preImageMasterEntityXml + ” “ +
preImageSubordinateEntityXml, postImageMasterEntityXml);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}

public override void PostSetState(CalloutUserContext userContext, CalloutEntityContext entityContext,
string preImageEntityXml, string postImageEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.SetState, userContext.UserId.ToString(), preImageEntityXml, postImageEntityXml);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}

public override void PostUpdate(CalloutUserContext userContext, CalloutEntityContext entityContext,
string preImageEntityXml, string postImageEntityXml)
{
LogInfo log =
new LogInfo(OperationsType.Update, userContext.UserId.ToString(), preImageEntityXml, postImageEntityXml);

ThreadPool.QueueUserWorkItem(new WaitCallback(WriteToFile.Write), log);
}

}

class WriteToFile
{
static object sync;

public static void Write(Object logInfo)
{
Monitor.Enter(sync);
String path = DateTime.Now.Month + “-“ + DateTime.Now.Day + “_MSCRM.log”;
StreamWriter sw;
try
{
sw = File.AppendText(path);
sw.WriteLine(logInfo.ToString());
sw.Close();
}
catch (IOException e)
{
if (!System.Diagnostics.EventLog.SourceExists(“CRMDataMonitor”))
System.Diagnostics.EventLog.CreateEventSource(
“CRMDataMonitor”, “Application”);

EventLog.WriteEntry(“CRMDataMonitor”,
“Error trying to write the log: n” + e.Message + “n” + e.StackTrace);
}
finally
{
Monitor.Exit(sync);
}
}
}

class LogInfo
{
public LogInfo(OperationsType opType, String userId, String preImage, String postImage)
{
this.opType = opType;
this.userId = userId;
this.preImage = preImage;
this.postImage = postImage;
}

public DateTime time = DateTime.Now;
public OperationsType opType;
public String userId;
public String preImage;
public String postImage;

public override string ToString()
{
return time + “t” + opType + “t” + userId + “t” +
preImage + “t” + ((postImage != null) ? postImage : “”);
}

}

enum OperationsType
{
Assign, Create, Delete, Deliver, Merge, SetState, Update
}
}


Como se ve en el código he utilizado el ApplicationPool para lanzar procesos en background que realicen las escrituras en el fichero de log. También he utilizado una clase estática que contiene el método estático que ejecutan los threads para escribir, lo he hecho de esta forma para asegurar bloqueos con Monitor y así evitar problemas de concurrencia en los accesos a los ficheros de log. Digo ficheros, porque como podéis comprobar el nombre del fichero de log contiene el mes y el día para que cada día se cree un fichero distinto. Además he declarado la clase LogInfo que contiene la información para escribir una entrada de log, y redefinido el ToString() para facilitar la escritura.


Una vez compilado el callout sólo faltaría desplegarlo en el servidor CRM siguiendo los pasos que se explican en el SDK. Recordad que tenemos que registrar el callout para todos los eventos tipo post de todas las entidades que queramos monitorizar. Por ejemplo para registrarlo para el post create de la entidad Cuenta.







1
2
3
4
5
6
7
  <callout entity=”account” event=”PostCreate”>
<subscription assembly=”callout.dll”
class=”PlainConcepts.CRMTools.Logging.LogCallout”
onerror=”ignore”>
<postvalue>@all</postvalue>
</subscription>
</callout>


Como posibles mejoras se me ocurre un par de ellas interesantes: Utilizar el nombre de usuario en vez del GUID, para ello habría que recuperarlo a través de los servicios web. Y hacer el log sobre una BBDD en vez de un fichero. Espero vuestros comentarios sobre el tema.


Un Saludo,


Marco Amoedo

3 comentarios en “Auditor&#237;a de datos en Microsoft CRM”

  1. Excelente post Marco.
    Solo una sugerencia: por qué no crear el log usando las clases a tal proposito System.Diagnostics para tirar las trazas. Así sería muy facil que mediante configuración el usuario eligiese la salida, el nivel de logeo etc… Resumiendo, así se podria usar todo el potencial del traceo de System.Diagnostics.

    Entiendo que tu post es un ejemplo, pero tiro aqúi mi puntada. Seguro que esto hecho con tiempo y paciencia tendría mucho uso.

  2. Gracias Rodrigo, me apunto lo de System.Diagnostics. La verdad es que no se me haba ocurrido incluirlo en el ejemplo, pero sera una de las mejoras fundamentales, y casi obligatoria.
    Y como esto es probable que lo tengamos que desarrollar para uno de los proyectos de Microsoft CRM que tenmos entre manos, prometo volverlo a publicar cuando este ms elaborado.

  3. Hola a todos.

    ¿Alguien creo un log de modificaciones de una entidad que sea
    consultable?
    O sea, que no sea simplemente un texto, sino que permita realizarle
    consultas a ese log.
    Una especie de base de datos o algo por el estilo.

    ¿Me podrían por favor aportar ideas al respecto?

    Tambien quiero saber en cuanto afecta a la performance del CRM agregar la opcion de logs como comenta este post.

    Gracias y saludos,
    Raúl

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *