Пишем свою систему логирования при помощи WCF
- modified:
- reading: 7 minutes
Предположим у нас есть задание: написать систему логирования, которая в реальном времени отсылает логи слушателям, причем информация может быть конфиденциальная и нужно избежать возможности кражи информации.
Первое правило, о котором нужно не забыть при написании системы логирования – это то, что система логирования не должна влиять на производительность основного приложения, либо влияние должно быть сведено к минимуму. Что можно использовать? Так как я подразумеваю, что слушатели могут быть удаленные, то можно использовать WCF для подключения. Для защиты информации сделаем следующее: при подключении клиента – клиент будет генерировать пару ключей RSA, и открытый ключ будет передавать на сервер. Сервер при помощи открытого ключа будет шифровать сообщение и отправлять клиенту, а так как закрытым ключом владеет только клиент, то и прочитать данное сообщение сможет только клиент. Для усовершенствования можно еще сделать так, чтобы при начале связи сервер генерировал пару ключей и отправлял клиенту открытый ключ, при помощи которого клиент будет шифровать свой открытый ключ (чтобы не было возможности подменить информацию клиенту). Есть другой вариант – использовать сертификаты при установки соединения, но это требует дополнительных затрат времени на развертывание.
Итак, опишем интерфейсы, которые будут использоваться в WCF соединении
[ServiceContract(SessionMode = SessionMode.Required,
CallbackContract = typeof(IWCFLoggerOutputCallback))]
internal interface IWCFLoggerOutput
{
[OperationContract(IsOneWay = true)]
void RegisterListener();
/// <summary>
/// log with encrypt
/// </summary>
/// <param name="parameters">Send public key to encrypt</param>
[OperationContract(IsOneWay = true)]
void RegisterSecureListener(RSAParameters parameters);
[OperationContract(IsOneWay = true, IsTerminating = true)]
void CloseChannel();
}
/// <summary>
/// Support just unicode messages
/// </summary>
internal interface IWCFLoggerOutputCallback
{
[OperationContract(IsOneWay = true)]
void Log(byte[] message);
}
Таким образом у нас будут клиентские методы для регистрации канала, один метод для незащищенного канала, один метод для регистрации защищенного канала с передачей открытого ключа, а так же интерфейс Callback – метод Log который будет вызываться на сервере для передачи логируемого сообщения. В нашем случае ServiceContract как раз и определяет поведение соединения, что оно должно быть сессионным, а для Callback использоваться интерфейс IWCFLoggerOutputCallback. Дальше реализуем эти интерфейсы для клиентской части. Класс WCFClient
internal class WCFClient : DuplexClientBase<IWCFLoggerOutput>
{
public IWCFLoggerOutputCallback Callback { get; set; }
public WCFClient(WCFListener listener, string endPoint)
: base(new InstanceContext(listener), ServiceHelper.GetDefaultBinding(), new EndpointAddress(endPoint))
{
}
public void RegisterListener()
{
Channel.RegisterListener();
}
public void RegisterListener(RSAParameters parameters)
{
Channel.RegisterSecureListener(parameters);
}
public void CloseChannel()
{
try
{
Channel.CloseChannel();
}
// TODO: Use log4net to log this exceptions
catch (Exception)
{
Abort();
}
}
}
public static class ServiceHelper
{
// TODO: This should be customizing
public static Binding GetDefaultBinding()
{
return new NetTcpBinding();
}
}
Использовать я буду обычный NetTcpBinding для соединения. Так же напишем класс WCFListener, который и будет являться основным классом для установления соединения с сервером:
public class WCFListener : IWCFLoggerOutputCallback, IDisposable
{
private RSAParameters? RSAParameters { get; set; }
private WCFClient Client { get; set; }
/// <summary>
/// Create new Listener instance
/// </summary>
/// <param name="endPoint">Server url</param>
/// <param name="fSecurity">Need to open secury channel</param>
public WCFListener(string endPoint, bool fSecurity)
{
Client = new WCFClient(this, endPoint);
if (fSecurity)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
RSAParameters = rsa.ExportParameters(true);
Client.RegisterListener(rsa.ExportParameters(false));
}
}
else
{
Client.RegisterListener();
}
}
public bool IsSecure
{
get { return RSAParameters.HasValue; }
}
// TODO: Change Action delegate to friendly name delegate
public event Action<string> Log;
void IWCFLoggerOutputCallback.Log(byte[] message)
{
if (IsSecure)
{
message = RSAHelper.Decrypt(message, RSAParameters.Value);
}
//TODO: customize encoding
string sMessage = Encoding.Unicode.GetString(message);
if (Log != null)
Log(sMessage);
}
public void Dispose()
{
Client.CloseChannel();
Client.Close();
}
}
У нас теперь есть клиентская часть, осталось написать сервер, реализовав поведение вышеописанных интерфейсов:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession
, ConcurrencyMode = ConcurrencyMode.Multiple)]
internal class WCFLoggerOutputService : IWCFLoggerOutput
{
private IWCFLoggerOutputCallback Listener { get; set; }
private RSAParameters? RSAParameters { get; set;}
public bool IsSecure
{
get { return RSAParameters.HasValue; }
}
public void RegisterListener()
{
Listener = OperationContext.Current.GetCallbackChannel<IWCFLoggerOutputCallback>();
WCFOutput.RegisterOnLog(Log);
}
public void RegisterSecureListener(RSAParameters parameters)
{
RSAParameters = parameters;
RegisterListener();
}
private bool Log(string message)
{
try
{
byte[] bData = Encoding.Unicode.GetBytes(message);
if (IsSecure)
{
bData = RSAHelper.Encrypt(bData, RSAParameters.Value);
}
Listener.Log(bData);
return true;
}
// TODO: log error with log4net
catch(Exception)
{
return false;
}
}
public void CloseChannel()
{
WCFOutput.UnRegisterOnLog(Log);
}
}
Поведение класса WCFLogerOutputService очень простое, при регистрации канала он регистрирует свой метод Log в класс WCFOutput, который имеет уже более сложную логику:
internal class WCFOutput : IOutput, IDisposable
{
private ServiceHost _host;
private object _stackLocker = new object();
private readonly Queue<string> _queue = new Queue<string>();
private readonly Timer _timer ;
private WCFOutput()
{
_timer = new Timer(OnTimerLogger);
LaunchTimer();
}
#region Static, singlton
private static readonly object _locker = new object();
private static WCFOutput _output;
public static IOutput GetOutput()
{
if (_output == null)
{
lock (_locker)
{
if (_output == null)
{
_output = new WCFOutput();
_output.OpenHost();
}
}
}
return _output;
}
#endregion
#region WCF implementation
~WCFOutput()
{
Dispose();
}
public void Dispose()
{
CloseHost();
}
private void OpenHost()
{
CloseHost();
_host = new ServiceHost(typeof(WCFLoggerOutputService));
// TODO: Url param must be customize
_host.AddServiceEndpoint(typeof(IWCFLoggerOutput), ServiceHelper.GetDefaultBinding()
, "net.tcp://localhost:2222/Logger/");
_host.Open();
}
private void CloseHost()
{
if (_host != null)
{
_host.Close();
_host = null;
}
}
#endregion
#region Register client listeners
private static object _eventLocker = new object();
public delegate bool LoggerAction(string message);
private static event LoggerAction OnLog;
public static void RegisterOnLog(LoggerAction action)
{
lock (_eventLocker)
{
OnLog += action;
}
}
public static void UnRegisterOnLog(LoggerAction action)
{
lock (_eventLocker)
{
OnLog -= action;
}
}
#endregion
#region IOutput interface
//TODO: Write thread safe queue with unlimited write and limited read
public void PutMessage(string msg)
{
lock (_stackLocker)
{
_queue.Enqueue(msg);
}
}
#endregion
private void OnTimerLogger(object obj)
{
while (ContainsLogs())
{
string msg = GetNextMsg();
lock (_eventLocker)
{
if (OnLog != null)
{
IList<LoggerAction> badDelegates = new List<LoggerAction>();
foreach (LoggerAction action in OnLog.GetInvocationList())
{
try
{
if (!action(msg))
{
badDelegates.Add(action);
}
}
//TODO: use log4net to log this error
catch (Exception)
{
badDelegates.Add(action);
}
}
foreach (var action in badDelegates)
{
OnLog -= action;
}
}
}
}
LaunchTimer();
}
private string GetNextMsg()
{
lock (_stackLocker)
{
return _queue.Dequeue();
}
}
private bool ContainsLogs()
{
lock (_stackLocker)
{
return _queue.Count > 0;
}
}
private void LaunchTimer()
{
_timer.Change(100, Timeout.Infinite);
}
}
На самом деле класс WCFOutput получился даже немного перегруженный. Итак, что же в нем есть? Для хранения сообщений для логирования я буду использовать Queue<string> (очередь), так же реализую паттерн синглтон с этим классом, так как мне не нужно больше одного сервера логирования в приложении (#region Static, singlton). При создании экземпляра он будет открывать Host для прослушивания подключения клиентов (#region WCF implementation), там же зашит путь по которому он будет прослушивать. Дальше я реализовал event OnLog, к которому будут подключаться сессии клиентов, и которым будут передаваться сообщения. Метод PutMessage служит для того, чтобы класть сообщения в очередь лога. Именно, я захотел сразу сделать прототип так, чтобы логика отправки сообщений клиентам была в отдельном потоке, чтобы ни в коем случае не тормозить основной поток. И по таймеру вызывается метод OnTimerLogger, который берет сообщения из очереди и отправляет их клиентам, у меня сразу есть проверка на то, что сообщения отправляются, чтобы выкидывать отвалившихся клиентов.
Так же у меня присутствует несколько Wrapper’ов с идей того, что систему WCF лога можно заменить на другую, без ковыряния уже использующую эту систему приложения. В результате напишем небольшой регрессионный тест:
static ILogger _logger;
[TestFixtureSetUp]
public void SetUpTests()
{
_logger = LoggerFactory.GetLogger();
}
/// <summary>
/// This test show that listener can work with many clients (one secure, other not secure)
/// </summary>
[Test]
public void many_listeners_with_secure()
{
WCFListener[] listeners = new WCFListener[10];
for (int i = 0; i < listeners.Length; i++)
{
listeners[i] = new WCFListener("net.tcp://localhost:2222/Logger/", i%2 == 1);
listeners[i].Log += big_test_listener_Log;
}
counter = 0;
_logger.Log("Big test");
Thread.Sleep(5000);
Assert.AreEqual(counter, listeners.Length);
for (int i = 0; i < listeners.Length; i++)
{
listeners[i].Dispose();
}
}
public static int counter = 0;
private object _lock = new object();
void big_test_listener_Log(string obj)
{
lock (_lock)
{
counter++;
Console.WriteLine(obj);
}
}
В методе SetUpTests – инициализируем наш сервер логирования, в методе тестирования – создадим 10 слушателей (с шифрованием сообщений и без).
Скачать пример: LogSystem.zip. Там находится библиотека-прототип, библиотека с тестами, а так же два консольных приложения – примеры сервера и клиента.
Важные замечания к этому прототипу которые нужно учесть при использовании: (а) RSACryptoServiceProvider не лучший вариант для шифрования, взамен его лучше использовать DESCryptoServiceProvider или AESCryptoServiceProvider; (б) в WCF есть возможность использования SSL, потому иногда можно обеспечить безопасность на транспортном уровне, но это потребует временных затрат на разворачивание инфраструктуры, но в каких-то задачах - это будет лучшим решением; (в) были замечания по поводу реализации паттерна синглтон, дело в том, что работать данная реализация будет нормально в CLR - .NET 2.0, проблемы могут быть на I64 архитектурах - тогда спасет volatile перед объявлением статической переменной, а так же могут быть проблемы в реализованном по стандарту ECMA CLR, есть вероятность, что это Mono. Лучшим вариантом тогда будет простая инициализация поля в объявлении.