Обработка исключительных ситуаций - Kesco-m/Kesco.Lib.Log GitHub Wiki

Все исключительные ситуации в программах, разработанных специалистами холдинга должны обрабатываться модулем регистрации ошибок '''Кеsco.Log'''.

'''Kesco.Log.dll''' – модуль регистрации исключений и ошибок в службе поддержки компаний холдинга.

Назначение подсистемы и требования к ней

Система предназначена для сбора данных об ошибках в приложениях холдинга. В случае возникновения исключения в приложении, сопутствующая ошибке информация должна быть собрана и передана в модуль регистрации ошибки:

*сообщение и тип исключительной ситуации; *имя приложения; *имя и версия сборки; *метод приложения, в котором произошла исключительная ситуация; *Stack Trace для всей цепочки исключений с детальной информацией по каждому исключению; *SQL запрос и строку подключения к БД (для исключений, произошедших на стороне SQL сервера);


Все исключительные ситуации и ошибки в программах необходимо выводить пользователю в “дружелюбном”, централизованно реализованном виде:

Архив документов в виде всплывающих popup окон; *в Web-приложениях в виде всплывающих диалоговых модальных окон.


Модуль регистрации ошибок должен обеспечивать сохранение полученной информации. Сообщения отправляются на ящик Служба поддержки. Если отправка письма не удалась, сообщение об ошибке пишется в EventLog журнала Windows. Сообщения, пришедшие на ящик службы поддержки автоматически разбираются (и сортируются) сервисом. Также в зависимости от приоритета возникшей ошибки, KescoExchangeSupportService.

Описание проекта '''Кеsco.Log'''

Исходный код проекта расположен в GIT по адресу https://github.com/Kesco-m/Kesco.Lib.Log.git.

Проект написан в среде VS на языке C#.

Информация об ошибке в процессе разбора цепочки исключений сохраняется в формате XML. Для формирования текста письма в формате HTML/простого текста с отчетом об ошибке используются XSLT шаблоны, являющиеся встроенными ресурсами проекта: '''Letter.xslt''', '''PlainText.xslt'''.

Диаграмма классов

На диаграмме представлены следующие классы:

:public class '''AssemblyIsKescoAttribute''' - Класс, определяющий атрибут сборки. Означает что проект поддерживается программистами Атэк-групп и на основании Stack Trace ошибку требуется локализовывать в сборках, помеченных этим атрибутум.

:public class '''DetailedException''' - Переопределение базового класса Exception. Используется в приложениях холдинга для формирования цепочек исключений, содержащих дополнительную отладочную информацию.

:public class '''LogicalException''' - Переопределение базового класса Exception. Используется в приложениях холдинга для регистрации логических ошибочных ситуаций.

:public class '''Logger''' - Класс, предоставляющий доступ к единственному на приложение статическому объекту регистрации ошибок.

:public class '''LogModule''' - Модуль регистрации исключительных ситуаций.

:internal class '''TaskEventLogReg''' - Класс, используемый при асинхронной регистрации сообщения в EventLog.

:internal class '''TaskMailSend''' - Класс, используемый при асинхронной отправке сообщения через SMTP.

:public enum '''Priority { Error = 1, Alarm = 2, Info = 3 }''' - Перечисление. Определяет приоритеты e-mail сообщений.

'''Error''' - этот приоритет назначается сообщению об исключительной ситуации, которая делает невозможным выполнение определенного действия в приложении (приоритет по умолчанию для '''DetailedException''').

'''Alarm''' – этот приоритет назначается сообщению об исключительной ситуации, которая является критической для нескольких пользователей и делает невозможным работу всего приложения. Сообщения с данным приоритетом в виде SMS отправляются на мобильный телефон дежурного инженера.

'''Info''' - информационное уведомление.

Диаграмма зависимостей классов

Взаимосвязи классов внутри проекта показаны на диаграмме:

Работа с модулем в других приложениях

'''AssemblyIsKescoAttribute''':

Используется в AssemblyInfo.cs приложений для задания соответствующего атрибута сборки: [assembly: AssemblyIsKesco( true )]. Значение атрибута false или его отсутствие означают одно и то же - сборка не рассматривается как внутренний проект холдинга или не представляет интереса с точки зрения отладки (локализации места возникновения ошибки).


'''DetailedException''':

Класс предназначен для сохранения расширенной информации в цепочке исключений и содержит несколько вариантов конструкторов + 1 публичный метод.

:'''public string GetExtendedDetails()'''

Получение строки с расширенными деталями цепочки исключений (доп. информация, SQL данные). Метод может быть использован для получения отладочной информации и отображения ее в интерфейсе пользователя (отображаемые по запросу данные). Для каждого исключения цепочки исключений выводятся следующие свойства:

Тип ошибки: Сообщение ошибки
	Stack Trace: .....
	Details: .....
	Sql Command: .....
	Sql Connection: .....
	Sql Params: .....

:'''Варианты конструкторов DetailedException'''

Все нижеперечисленные конструкторы так или иначе ссылаются на "базовый" конструктор '''public DetailedException( string message, Exception innerException )''', в котором проставляются значения по-умолчанию (см. ниже пример кода). Дополнительные параметры конструктора меняют соответствующие атрибуты '''DetailedException'''.

Для группы конструкторов с SqlCommand отправка письма определяется типом SqlException-а (см. пример ниже).

/// <summary>
/// Констуктор с указанием необходимости отправки письма в службу поддержки с дополнительными отладочными данными.
/// </summary>
/// <param name="message">краткое описание</param>
/// <param name="innerException">объект типа Exception</param>
/// <param name="priority">приоритет исключения</param>
/// <param name="details">дополнительная отладочная информация</param>
/// <param name="sendMail">Флаг-нужно ли отправлять данную ошибку в Службу поддержки</param>
public DetailedException( string message, Exception innerException, Priority priority, string details, bool sendMail )
public DetailedException( string message, Exception innerException, Priority priority, string details )
public DetailedException( string message, Exception innerException, string details, bool sendMail )
public DetailedException( string message, Exception innerException, bool sendMail )
public DetailedException( string message, Exception innerException, Priority priority )
public DetailedException( string message, Exception innerException, string details )
public DetailedException( string message, Exception innerException )
{
	if( innerException is DetailedException )
		priorityLevel = ( innerException as DetailedException ).PriorityLevel;
	else
		priorityLevel = Priority.Error;

	details = "";
	sendMail = true;
}


/// <summary>
/// Констуктор на основании SQL исключения с доп. деталями. Отправка письма определяется классом SQL исключения (12 не отправляется)
/// </summary>
/// <param name="message">краткое описание</param>
/// <param name="innerException">объект типа Exception</param>
/// <param name="sqlCmd">команда к БД - для получения строки подключения к БД и собственно текста sql-комманды</param>
/// <param name="priority">приоритет исключения</param>
/// <param name="details">дополнительная отладочная информация</param>
public DetailedException( string message, Exception innerException, IDbCommand sqlCmd, Priority priority, string details )
public DetailedException( string message, Exception innerException, IDbCommand sqlCmd, string details )
public DetailedException( string message, Exception innerException, IDbCommand sqlCmd, Priority priority )
public DetailedException( string message, Exception innerException, IDbCommand sqlCmd )
{
	this.sqlCmd = sqlCmd;

	// Отправка сообщений для ошибок на SQL Server с кодом 12 не предусмотрена - только вывод информационных сообщений пользователю
	if( innerException is SqlException && ((SqlException)innerException).Class.Equals((byte)12) )
		sendMail = false;
	else
		sendMail = true;
}

/// <summary>
/// Конструктор
/// </summary>
/// <param name="info">Информация необходимация для сериализации</param>
/// <param name="context">Содержимое для сериализации</param>
protected DetailedException(SerializationInfo info, StreamingContext context)

'''LogicalException''':

Класс предназначен для регистрации исключительных ситуаций, связанных с нарушениями каких-то ограничений, либо невозможностью выполнить какие-то действия. Данное исключение не предполагает наличия исключения-основания и использования в цепочке пробрасываемых исключений. Оно предназначено для регистрации ошибки ''здесь-и-сейчас''. Для того чтобы локализовать это ''здесь'' в конструктор передается AssemblyName и название метода (опционально). Предлагается использовать данный класс в следующем виде:

Kesco.Logger.WriteEx( new Kesco.Log.LogicalException(
	"Название исключения (попадет в тему письма с ошибкой)",
	"Детальная информация",
	System.Reflection.Assembly.GetExecutingAssembly().GetName(),
	System.Reflection.MethodBase.GetCurrentMethod().Name ) );

В классе публичными являются только конструкторы. По-умолчанию приоритет назначается = Priority.Error, а в качестве метода, если он не указан, будет '''LogicalException'''. Возможные варианты конструкторов см. ниже:

/// <summary>
/// конструктор LogicalException
/// </summary>
/// <param name="message">краткое описание</param>
/// <param name="details">дополнительная отладочная информация</param>
/// <param name="assembly">информация о сборке (System.Reflection.Assembly.GetExecutingAssembly().GetName)</param>
/// <param name="method">имя метода (System.Reflection.MethodBase.GetCurrentMethod().Name)</param>
/// <param name="priority">приоритет исключения</param>
public LogicalException( string message, string details, AssemblyName assembly, string method, Priority priority )
public LogicalException( string message, string details, AssemblyName assembly, Priority priority )
public LogicalException( string message, string details, AssemblyName assembly, string method )
public LogicalException( string message, string details, AssemblyName assembly )

'''Logger''':

Класс предоставляет статические методы для работы с единственным модулем обработки ошибок для всех сборок, входящих в приложение. Модуль должен инициализироваться единожды в исполняемом приложении.

:'''public static void Init( Kesco.Log.LogModule logModule )'''

Инициализация статического модуля обработки ошибок приложения. В качестве параметра передается уже созданный и проинициализированный объект Kesco.Log.LogModule. Инициализация должна быть выполнена перед попытками записи в лог с использованием статических методов '''WriteEx'''.

:'''public static void WriteEx( Exception ex, bool async )'''

Инициализация записи информации об ошибке в лог. Exception ex - исключение, которое требуется зафиксировать; bool async - выполинть запись асинхронно.

Существует упрощенная версия для вызова асинхронной отправки сообщений: '''public void WriteEx( Exception ex )'''.

'''LogModule''':

:'''public void Init(string smtpServer, string supportEmail)'''

Для работы с модулем регистрации ошибок его требуется инициализировать при старте приложения. Для этого есть публичный метод '''Init'''. При инициализации выполняется настройка параметров SMTP сервера. Названия сборок, разработанных компаниями Атэк-групп, указываются в конструкторе. Настройки должны браться из конфигурационных файлов приложений.

Инициализация модуля логирования должна выглядеть, например, так (пример для Global.asax):

protected void Application_Start( Object sender, EventArgs e )
{
	//первоначальная инициализация приложения
	LogModule log = new LogModule( Settings.appName );
	log.Init( Settings.smtpServer, Settings.email_Support );
	log.OnDispose += new DisposeEventHandler( log_OnDispose );
	Kesco.Logger.Init( log );

	
}

:'''public int State{ get; }'''

Состояние выступает в роли счетчика и принимает только неотрицательные значения. Получение статуса модуля регистрации ошибки. Значения: 0 – LogModule готов к отправке; >0 – сообщения отправляются;

Изменения статуса выполняются с помощью метода: ''private void IncState( int delta )''.

:'''public event DisposeEventHandler OnDispose'''

Событие вызова реинициализации log-модуля. При реинициализации необходимо сделать новый запрос к конфигурационному файлу или реестру для получения необходимых настроек.

	.....
	log.OnDispose += new DisposeEventHandler( log_OnDispose );
	.....

private void log_OnDispose( LogModule sender )
{
	sender.Init( Settings.smtpServer, Settings.email_Support );
}

=== Требования к внешним проектам ===

Если сборка, входящая в проект рассматривается как место возникновения исключительных ситуаций, то она должна быть помечена атрибутом '''AssemblyIsKesco( true )''' в свойствах проекта (в '''AssemblyInfo.cs'''):

[assembly: AssemblyIsKesco(true)]

В этом случае при разборе стека методов, приведших к ошибоке (Stack Trace), в качестве места возникновения ошибки будет определен первый встретившийся метод первой помеченной этим атрибутом сборки. Например:

Stack Trace:
   at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream)
   at System.Data.SqlClient.SqlCommand.ExecuteScalar()			// System.Data
   at Kesco.DBManager.DBModule.ExecuteScalar(String cmdText)		// Kesco.DBManager
   at Kesco.Entity.Save()						// Kesco.Entity
   at shipment.RWNakladnaya.AddDelivery(Boolean noSaveDocument)		// shipment
   at shipment.RWNakladnaya.AddDelivery()
   at shipment.RWDocumentPage.V3_ProcessClientCommand(String name)
   at shipment.RWNakladnaya.V3_ProcessClientCommand(String name)
   at Kesco.Web.V3.Page.V3_ProcessClientRequest()

Если в указанном примере сборка '''Kesco.DBManager''' имеет атрибут '''AssemblyIsKesco(true)''', то она будет определена как место ошибки и методом будет указан '''ExecuteScalar'''.

Если ни '''Kesco.DBManager''', ни '''Kesco.Entity''' такого атрибута не имеют, но у '''shipment''' есть атрибут '''AssemblyIsKesco(true)''', то в качестве сборки и метода с ошибкой будут определены '''shipment''' и '''AddDelivery'''.


Таким образом можно исключать из области рассмотрения те проекты холдинга, которые не являются непосредственно источниками ошибки, а связаны с ними исключительно как передающее звено. В данном примере приложение и сборка '''Kesco.DBManager''' лишь обеспечивает выполнение SQL запроса и возврат полученного DataSet-а. Ошибка же связана с бизнес-логикой на стороне сервера или с некорректным формированием SQL запроса. В любом случае для исправления исключения требуется анализ приложения '''shipment''' (его сборки).

=== Сообщения валидации ===

В результате работы модуля регистрации ошибки в письме могут появиться сообщения валидации, выделенные красным цветом:

*Требуется обрабатывать SQL исключение так, чтобы в обработчик ошибок передавался наследник IDbCommand!

Данное сообщение возникает в случае, если возникшее SqlException было передано модулю обработки ошибок в чистом виде, либо как InnerException обычного исключения. При этом информация об SqlCommand модулю регистрации становится недоступной и в отладочную информацию нельзя включить Sql Command, Sql Connection, Sql Params. Для того чтобы подобная ошибка не возникала SqlException в блоке try-catch следует обрабатывать следующим образом:

throw new Kesco.Log.DetailedException( "Перехватчик исходного SqlException ", sex, SQLcmd );

Данный пример будет работать и в случае, если sex имеет тип отличный от SqlException. Например, Exception.

*Не удалось определить сборку авторства холдинга, в функции которой произошла ошибка!

Ни одна из сборок, фигурирующих в StackTrace цепочки исключений не была помечена атрибутом '''AssemblyIsKesco( true )'''. Из-за этого модуль обработки ошибок посчитал все библиотеки либо системными, либо не преоставляющими отладочной ценности и не смог остановиться ни на одном из методов в цепочке вызовов. Для диагностики причин надо смотреть StackTrace цепочки исключений и, возможно, изменять атрибут '''AssemblyIsKesco( true )''' для каких-то проектов.

Также данное сообщение возникает, если у исключения, по которому формируется письмо в службу поддержки, не было стека вызовов. Это происходит когда исключение не перехватывается блоком try-catch, а создается новый объект и передается методу LogModule.WriteEx():

log.WriteEx( new Kesco.Log.DetailedException( "Тема письма с ошибкой", null, "Отладочная информация, описание нарушения бизнес-логики..." ) );

Обычно такие сообщения создаются в случае нарушения правил бизнес-логики. При этом приложение работает корректно и ошибки с точки зрения технической реализации программы не было. Подобное сообщение (т.е. без стека вызовов, с исключением содержащим только сообщение валидации), пришедшее на ящик службы поддержки, '''не предусматривает заведения ошибки на какой либо програмный продукт'''.

На случай подобных исключений добавлен отдельный класс '''LogicalException''', который позволяет указать модулю обработки ошибок сборку и ее версию + функцию, в которой произошла ошибка, без выкидывания исключений. Пример использования:

Kesco.Logger.WriteEx( new Kesco.Log.LogicalException(
	"Название исключения (попадет в тему письма с ошибкой)",
	"Детальная информация",
	System.Reflection.Assembly.GetExecutingAssembly().GetName(),
	System.Reflection.MethodBase.GetCurrentMethod().Name ) );

== Регистрация ошибок ==

Регистрацию ошибок, возникающих в приложении, желательно выполнять централизовано в одном месте приложения. Для этого используется механизм "пробрасывания" исключения в функции и модули, расположенные выше места ошибки. При пробрасывании предлагается используется класс '''Kesco.Log.DetailedException''', содержащий расширенные данные об исключительной ситуации.

Обработку исключений блоком try-catch и проброс выше необходимо осуществлять в следующих случаях:

*при обработке SQL запросов; *при необходимости передачи дополнительной отладочной информации (клиентских скриптов, заголовков HTTP запроса...); *при возникновении ситуаций, противоречащих бизнес-логике приложения;

Важно: В случае, если исключительная ситуация возникла в "окружении" приложения(при обращении к внешним компонентам), например: *при печати на принтер - кончилась бумага, принтер не найден *при работе с файловой системой - путь/папка не найдена или нет доступа *в приложении возникла ошибки выделения памяти: Out of Memory *при обращении к серверу базы данных: Превышено время ожидания выполнения запроса(Timeout), Transaction deadlock, SQL does not exist.

и т.д. В этих случаях устанавливать приоритет ошибки равным "ExternalError", а пользователю выдавать "понятное" сообщение: например: "Извините, база данных временно недоступна! Попрубуйте позднее!" и т.п.

=== Примеры использования Kesco.Log ===

Пример использования '''Kesco.Log.DetailedException''' при обработке SQL запроса:

private void Example()
{
	SqlDataAdapter da = new SqlDataAdapter("sp_test",Kesco.Settings.DS_document) ;
	da.SelectCommand.CommandType = CommandType.StoredProcedure;
	da.SelectCommand.Parameters.Add("@КодЛица1",506) ;
	DataTable dt = new DataTable();
	try
	{
		......
		da.Fill(dt);
		......
	}
	catch( Exception ex )
	{
		throw new Kesco.Log.DetailedException( "Обработка SQL запроса", ex, da.SelectCommand );
	}
}


В случае если возникшая ошибка не должна повлечь за собой цепочки исключений, можно использовать логирование исключительной ситуации в произвольном месте программы. Например, в случае возникновения ошибки при выполнении какой-то функции, программа должна продолжить работу с параметрами по-умолчанию, но при этом записать в лог отладочную информацию о месте возникновения ошибке и контексте ее возникновения.

Пример вызова функции логирования без передачи исключения выше:

public void ProcessRequest( System.Web.HttpContext context )
{
	try
	{
		.....
		if( context.Request.QueryString[ "date" ] == null )
			date = DateTime.Now;
		.....
		byte[] b = fillDS( date );
		context.Response.ContentType = "application/pdf";
		context.Response.BinaryWrite( b );
	}
	catch( Exception ex )
	{
		string sContext = "Context:\n";
		foreach( string key in context.Request.Headers.AllKeys )
			sContext += String.Format( "[{0}]->[{1}]\n", key, context.Request.Headers[key] );

		Kesco.Logger.WriteEx( new DetailedException( ex.Message, ex, sContext ) );
	}
}


Для Web-приложения централизованную обработку ошибок удобно реализовать в Global.asax:

protected void Application_Error( Object sender, EventArgs e )
{
	Exception ex = Server.GetLastError();
	Kesco.Logger.WriteEx( ex );
}

=== Формат сообщений с информацией об ошибке ===

Письмо с ошибкой должно содержать несколько блоков:

:1. Заголовок

Содержит текст оригинальной ошибки и ее тип. В этой части письма также выводятся сообщения валидации.

:2. Информация о месте возникновения ошибки и контексте ее возникновения.

В письме находится в таблице '''table id="CommonInfo"'''. Строки таблицы содержат пары элементов ключ-значение для соответствующих свойств (см. пример ниже).

:3. Подробная информация с цепочкой возникновения исключений

Блок расположен в таблице '''table id='trace'''' письма. Самые "глубокие" исключения идут первыми, далее - оборачивающие их. Представленные в примере строки повторяются для каждого исключения за исключением строк с '''id='Details|Connection|Command|Parameters''''. Последние выводятся только в том случае, когда соответствующие параметры заданы для DetailedException.

Пример формата письма на ящик службы поддержки:

<html>
<head>...</head>
<body>
	<h1>Сообщение исходной ошибки</h1>
	<h2>Тип исходной ошибки</h2>
	<hr size="1" width="100%">
	<table id="CommonInfo" cellspacing="0" cellpadding="0" width="100%">
		<tr id="AppName">		<td class="head" nowrap>Приложение</td>	<td>Документы: ЖД реестры, накладные</td></tr>
		<tr id="Priority">		<td class="head" nowrap>Проритет</td>	<td>Error</td></tr>
		<tr id="Component">	<td class="head" nowrap>Сборка</td>		<td>Kesco.DBManager</td></tr>
		<tr id="ComponentVersion">	<td class="head" nowrap>Версия сборки</td>	<td>1.0.4427.34264</td></tr>
		<tr id="Function">		<td class="head" nowrap>Функция</td>		<td>ExecuteScalar</td></tr>
		<tr id="BaseDirectory">	<td class="head" nowrap>Base directory</td>	<td>c:/inetpub/wwwroot/Docs/SHIPMENT/</td></tr>
		<tr id="Computer">		<td class="head" nowrap>Компьютер</td>	<td>ktz-letyagin</td></tr>
		<tr id="UserLogin">	<td class="head" nowrap>Пользователь</td>	<td>TEST\letyagin</td></tr>
		<tr id="TimeError">	<td class="head" nowrap>Время ошибки</td>	<td>15.02.2012 07:39:36</td></tr>
	</table>
	<br>
	<hr size="1" width="100%">
	<table id='trace' cellspacing="0" cellpadding="0" width="100%">
		<tr id="Message">		<td class="head" nowrap>Сообщение</td>	<td>...</td></tr>
		<tr id="Type">		<td class="head" nowrap>Тип исключения</td>	<td>...</td></tr>
		<tr id="Component">	<td class="head" nowrap>Версия сборки</td>	<td>...</td></tr>
		<tr id="Method">		<td class="head" nowrap>Метод</td>		<td>...</td></tr>
		<tr id="Details">		<td class="head" nowrap>Details</td>		<td>...</td></tr>
		<tr id="Trace">		<td class="head" nowrap>Stack Trace</td>	<td>...</td></tr>
		<tr id="Connection">	<td class="head" nowrap>Sql Connection</td>	<td>...</td></tr>
		<tr id="Command">		<td class="head" nowrap>Sql Command</td>	<td>...</td></tr>
		<tr id="Parameters">	<td class="head" nowrap>Sql Parameters</td>	<td>...</td></tr>
		<tr>			<td colspan="2"> </td></tr>

		.... повторение блока для всех ошибок в цепочке

	</table>
	....
</body>
</html>


Если по каким-либо причинам отправка письма не удалась, выполняется регистрация в системном журнале Windows. Помимо текстового представления представленного выше письма в журнал регистрации выводится информация о почтовом сервере и отправителя. Пример формата ниже:

Не удалось доставить сообщение об ошибке
SMTP сервер:	smtp.testcom.com.
Email_Support:	[email protected].
Sender:		"ConsoleTEST" <[email protected]>.
Заголовок:	Сообщение исходной ошибки.
Inner Exception:Тип исходной ошибки.
	Текстовое представление письма на службу поддержки.
⚠️ **GitHub.com Fallback** ⚠️