Введение
Появившаяся необходимость выгрузки ошибок из 1С в Sentry была обусловлена потребностью удобного мониторинга ошибок и пользовательского опыта вне 1С. Такая потребность возникла у одного крупного клиента нашей компании, для которого нужно было придумать решение. Сложилась следующая ситуация: в службу технической поддержки регулярно поступали обращения с жалобами на скорость работы 1С, а в силу отсутствия мониторинга пользовательского опыта расследование инцидентов было затруднено, также нельзя было отследить динамику улучшения/ухудшения работы системы. Для решения описанных проблем был выбран Sentry.
В вопросе реализации выгрузки ошибок статья не несет глобальной новизны (все статьи по данной тематике перечислены в списке источников), но представленное решение имеет свое исполнение. Схожая и полезная статья[1] на эту тему написана Владиславом Журавским, за что автору огромная благодарность. Помимо мониторинга ошибок, Sentry предоставляет возможность расчета такого показателя, как Apdex, суть которого будет описана далее. Было решено воспользоваться, в том числе, и данным функционалом Sentry, что уже более ново в контексте интеграции 1С и Sentry (во всяком случае, по данной теме сходу материалов найти не удалось).
Рассматриваемое решение может быть неидеальным, но оно работоспособно и дает определенный функционал. В статье подробно пояснены не все тонкости разработки решения, а только наиболее важные (по мнению автора).
Что такое Sentry
Sentry - инструмент для сбора и анализа ошибок. Есть возможность указания большого количества дополнительной информации для каждой ошибки: пользователь, окружение, клиент и сервер и многое другое. Процесс создания проекта в Sentry рассматриваться не будет, как и процесс получения DSN токена, эта информация есть в статье [1].
Рисунок 1. Пример списка ошибок и иных сообщений в Sentry
На рис.1 показан пример списка ошибок (и других сообщений) в Sentry. Видно, что есть возможность настраивания фильтров для отбора нужной информации. Также уже можно заметить некоторые плюсы от использования данного инструмента. Например, указано количество возникновения каждой ошибки и количество пользователей, у которых она когда-либо возникала.
Рисунок 2. Пример списка событий для расчета Apdex в Sentry
На данном рисунке проиллюстрирован список событий для расчета Apdex. APDEX (Application Performance Index) — это индекс производительности, который отражает, насколько пользователи удовлетворены скоростью работы информационной системы[2]. Есть возможность внутри каждого события увидеть список событий данного типа (с возможностью раскрыть информацию о конкретном событии), а также ознакомиться с графиками Apdex и длительности события в различные временные промежутки. Это продемонстрировано на рис. 3.
Рисунок 3. Пример списка событий конкретного вида для расчета Apdex в Sentry
На данном рисунке можно увидеть некоторую техническую информацию в разрезе процентного соотношения от общего количества таких событий. Например, окружение (прод, стейдж или тест), операционную систему и имя компьютера.
Рисунок 4. Пример данных конкретного события для расчета Apdex в Sentry
На рисунке выше продемонстрированы данные, полученные для конкретного события. Указаны даты начала и окончания события в Unix формате в стандарте UTC в миллисекундах, длительность в миллисекундах, пользователь, база, версии платформы и конфигурации и т.д. Т.к. в примере использованы данные для события открытия формы списка справочника сотрудников, то у пользователя (пользователя Sentry) есть подробная информация о том, когда и у кого была открыта упомянутая форма, как быстро открылось окно и другая полезная информация.
Реализация интеграции на стороне 1С
Как и для большинства доработок в 1С, было принято решение воспользоваться дополнительным расширением. Главным преимуществом применения доп. расширений при доработках в 1С является отсутствие необходимости снимать основную конфигурацию с поддержки. Кроме того расширение крайне просто установить в нужную базу или несколько баз. В расширение были добавлены некоторые объекты и есть несколько заимствованных из основной конфигурации.
Были добавлены 3 общих модуля: sentry_Синхронизация, sentry_RestApi, sentry_ОбщегоНазначенияСервер. Подсистема sentry_Интеграция. Роль sentry_ОсновнаяРоль для настройки прав доступа к расширению. Общая картинка для упомянутой подсистемы. Справочник НастройкиСинхронизацииСsentry для настройки интеграции. И 3 системных перечисления: sentry_РежимыРаботы, sentry_УровниЖурналаРегистрации, sentry_Окружение. Заимствованные объекты: справочник КлючевыеОперации и РС (регистр сведений) ЗамерыВремени.
Для справочника НастройкиСинхронизацииСsentry были добавлены следующие реквизиты:
- Сервер (Строка) - хранит адрес Sentry для обмена данными;
- Токен (Строка) - хранит токен Sentry для обмена данными;
- НомерПроекта (Строка) - хранит идентификатор проекта Sentry для обмена данными;
- ЗащищенноеСоединение (Булево) - признак защищенного соединения;
- Порт (Строка) - номер порта при обмене данными;
- ОбменЗапущен (Булево) - признак того, что обмен с Sentry запущен;
- ИдентификаторФоновогоЗадания (УникальныйИдентификатор) - идентификатор текущего выполняемого фонового задания для данной настройки интеграции;
- РежимРаботы (ПеречислениеСсылка.sentry_РежимыРаботы) - хранит режим работы для данной настройки. Доступны значения: ПолнаяВыгрузка, ВыгрузкаЗаПериод, ВыгружатьТолькоНовыеПослеЗапускаОбмена;
- НачалоПериодаЖурналРегистрации (Дата) - дата начала выгрузки данных об ошибках при режиме работы ВыгрузкаЗаПериод;
- ОкончаниеПериодаЖурналРегистрации (Дата) - дата окончания выгрузки данных об ошибках при режиме работы ВыгрузкаЗаПериод;
- ВыгружатьЖурналРегистрации (Булево) - признак того, необходимо выгружать ошибки;
- ВыгружатьЗамерыВремени (Булево) - признак того, необходимо выгружать замеры времени;
- НачалоПериодаЗамерыВремени (Дата) - дата начала выгрузки данных о замерах времени при режиме работы ВыгрузкаЗаПериод;
- ОкончаниеПериодаЗамерыВремени (Дата) - дата окончания выгрузки данных о замерах времени при режиме работы ВыгрузкаЗаПериод;
- Окружение (ПеречислениеСсылка.sentry_Окружение) - информация об окружении, доступные значения: Прод, Стейдж, Тест.
Дополнительно была добавлена табличная часть с единственным реквизитом УровеньЖурналаРегистрации (ПеречислениеСсылка.sentry_УровниЖурналаРегистрации) с возможными значениями: Ошибка, Предупреждение, Информация, Примечание. Табличная часть необходима для возможности указания пользователем типа информации выгружаемой из журнала регистрации.
Важный момент про Сервер, Токен и НомерПроекта будет раскрыт далее. Для данного справочника были созданы 3 формы: ФормаЭлемента, ФормаСписка, ФормаВыполнениеСинхронизации. На рис. 5 продемонстрирована форма элемента справочника.
Рисунок 5. Форма настройки в 1С интеграции с Sentry
Назначение полей формы можно понять из описания реквизитов выше. На форме присутствуют кнопки запуска и выключения обмена, а также кнопка проверки соединения с Sentry. Элементы формы отображаются в зависимости от необходимости их отображения. Эта логика отражена в модуле формы.
Модуль формы
ФормаЭлемента справочника
НастройкиСинхронизацииСsentry
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ОбновитьОтображениеЭлементовФормы(Истина);
КонецПроцедуры
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
УжеЕстьТакаяНастройка = ПроверитьНастройкуНаДубль(Объект.Ссылка, Объект.Сервер, Объект.Токен, Объект.НомерПроекта);
Если УжеЕстьТакаяНастройка Тогда
ОбщегоНазначенияКлиент.СообщитьПользователю("Настройка с такими параметрами уже существует. Создать еще одну нельзя.");
Отказ = Истина;
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
ОбновитьФормуСписка();
ОбновитьОтображениеЭлементовФормы();
КонецПроцедуры
#КонецОбласти
#Область КомандыФормы
&НаКлиенте
Процедура УдалитьНастройку(Команда)
Оповещение = Новый ОписаниеОповещения("ПослеЗакрытияВопросаОбУдалении", ЭтотОбъект);
ПоказатьВопрос(Оповещение, "Настройка обмена будет удалена. Продолжить?",РежимДиалогаВопрос.ДаНет, 0);
КонецПроцедуры
&НаКлиенте
Процедура ПроверитьСоединение(Команда)
Ошибка = Ложь;
Если НЕ ЗначениеЗаполнено(Объект.Сервер) Тогда
Ошибка = Истина;
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Не заполнен адрес sentry.");
КонецЕсли;
Если НЕ ЗначениеЗаполнено(Объект.Токен) Тогда
Ошибка = Истина;
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Не заполнен токен.");
КонецЕсли;
Если НЕ ЗначениеЗаполнено(Объект.Токен) Тогда
Ошибка = Истина;
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Не заполнен номер проекта.");
КонецЕсли;
Если НЕ Ошибка Тогда
Если СоединениеУстановленоУспешно() Тогда
ОбщегоНазначенияКлиент.СообщитьПользователю("Соединение с ресурсом " + Объект.Сервер + " установлено успешно.");
Иначе
ОбщегоНазначенияКлиент.СообщитьПользователю("Соединение с ресурсом " + Объект.Сервер + " не установлено.");
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ВыключитьОбмен(Команда)
ВыключитьОбменНаСервере();
ОбновитьФормуСписка();
ОбновитьОтображениеЭлементовФормы();
КонецПроцедуры
&НаКлиенте
Процедура ЗапуститьОбмен(Команда)
Если sentry_ОбщегоНазначенияСервер.РасширениеВБезопасномРежиме() Тогда
ПоказатьПредупреждение(, "Расширение модуля в безопасном режиме. Продолжение невозможно", 10);
Возврат;
КонецЕсли;
Если ЗапуститьОбменНаСервере() Тогда
ПараметрыФормы = Новый Структура;
ПараметрыФормы.Вставить("НастройкаОбмена", Объект.Ссылка);
ПараметрыФормы.Вставить("Родитель", Объект.Ссылка);
ОбновитьФормуСписка();
ОбновитьОтображениеЭлементовФормы();
Оповещение = Новый ОписаниеОповещения("ОбновитьФормуИФормуСписка", ЭтотОбъект);
ОткрытьФорму("Справочник.НастройкиСинхронизацииСsentry.Форма.ФормаВыполнениеСинхронизации", ПараметрыФормы, , Объект,,, Оповещение);
Иначе
ОбновитьФормуСписка();
ОбновитьОтображениеЭлементовФормы();
КонецЕсли;
КонецПроцедуры
#КОнецОбласти
#Область УдалениеНастройки
// Продолжение УдалитьНастройку.
//
&НаКлиенте
Процедура ПослеЗакрытияВопросаОбУдалении(Результат, Параметры) Экспорт
Если Результат = КодВозвратаДиалога.Нет Тогда
Возврат;
Иначе
УдалитьНастройкуСервер(Объект.Ссылка);
ЭтаФорма.Закрыть();
ОбновитьФормуСписка();
КонецЕсли;
КонецПроцедуры
// Удалаяет элемент справочника НастройкиСинхронизации.НастройкиСинхронизацииСsentry.
//
// Параметры:
// НастройкаОбмена - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку обмена.
//
&НаСервереБезКонтекста
Процедура УдалитьНастройкуСервер(НастройкаОбмена)
УстановитьПривилегированныйРежим(Истина);
НастройкаОбмена.ПолучитьОбъект().Удалить();
УстановитьПривилегированныйРежим(Ложь);
КонецПроцедуры
#КонецОбласти
#Область ВключениеВыключениеОбмена
// Инициализирует выполнение обмена.
//
&НаСервере
Функция ЗапуститьОбменНаСервере()
Если НЕ Объект.ВыгружатьЖурналРегистрации И НЕ Объект.ВыгружатьЗамерыВремени Тогда
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Не выбраны данные для выгрузки.");
Возврат Ложь;
КонецЕсли;
Если НЕ Объект.РежимРаботы = Перечисления.sentry_РежимыРаботы.ВыгружатьТолькоНовыеПослеЗапускаОбмена Тогда
Если Объект.РежимРаботы = Перечисления.sentry_РежимыРаботы.ВыгрузкаЗаПериод
И (((НЕ ЗначениеЗаполнено(Объект.НачалоПериодаЖурналРегистрации) ИЛИ НЕ ЗначениеЗаполнено(Объект.ОкончаниеПериодаЖурналРегистрации))
И Объект.ВыгружатьЖурналРегистрации)
ИЛИ ((НЕ ЗначениеЗаполнено(Объект.НачалоПериодаЗамерыВремени) ИЛИ НЕ ЗначениеЗаполнено(Объект.ОкончаниеПериодаЗамерыВремени))
И Объект.ВыгружатьЗамерыВремени)) Тогда
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Не заполнены периоды выгрузки.");
Возврат Ложь;
КонецЕсли;
Объект.ОбменЗапущен = Истина;
ЭтотОбъект.Записать();
Возврат Истина;
Иначе
НайденноеФоновоеЗадание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Объект.ИдентификаторФоновогоЗадания);
Если НайденноеФоновоеЗадание = Неопределено
ИЛИ НЕ НайденноеФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Активно Тогда
Объект.ОбменЗапущен = Истина;
ЭтотОбъект.Записать();
Объект.ИдентификаторФоновогоЗадания = ЗапуститьВРежимеРеальногоВремениВФоне();
КонецЕсли;
ЭтотОбъект.Записать();
КонецЕсли;
Возврат Ложь;
КонецФункции
// Производит запуск обмена в режиме рального времени и возращает идентификатор фонового задания.
//
// Возвращаемое значение:
// УникальныйИдентификатор - если статус задания = "Выполняется", то содержит идентификатор запущенного фонового задания.
//
&НаСервере
Функция ЗапуститьВРежимеРеальногоВремениВФоне()
ПараметрыСинхронизации = Новый Структура;
ПараметрыСинхронизации.Вставить("НастройкаОбмена" , Объект.Ссылка);
УстановитьПривилегированныйРежим(Истина);
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(УникальныйИдентификатор);
ПараметрыВыполнения.НаименованиеФоновогоЗадания = НСтр("ru = 'Выполнение синхронизации с sentry'");
ПараметрыВыполнения.ОжидатьЗавершение = 0.1;
ФоновоеЗадание = ДлительныеОперации.ВыполнитьВФоне("sentry_Синхронизация.ВыполнитьОбменДаннымиВРежимеРеальногоВремениПоФоновомуЗаданию",
ПараметрыСинхронизации, ПараметрыВыполнения);
Возврат ФоновоеЗадание.ИдентификаторЗадания;
КонецФункции
// Изменяет реквизиты ОбменЗапущен и ИдентификаторФоновогоЗадания.
//
&НаСервере
Процедура ВыключитьОбменНаСервере()
Объект.ОбменЗапущен = Ложь;
Объект.ИдентификаторФоновогоЗадания = Неопределено;
ЭтотОбъект.Записать();
КонецПроцедуры
#КонецОбласти
#Область ОбновлениеФормИЭлементовФормы
// Обновляет формы элемента и списка справочника НастройкиСинхронизацииСsentry.
//
&НаКлиенте
Процедура ОбновитьФормуИФормуСписка(Результат, ДополнительныеПараметры) Экспорт
ОбновитьФормуСписка();
ОбновитьОтображениеЭлементовФормы();
КонецПроцедуры
// Обновляет форму списка справочника НастройкиСинхронизацииСsentry.
//
&НаКлиенте
Процедура ОбновитьФормуСписка()
ФормаСписка = ПолучитьФорму("Справочник.НастройкиСинхронизацииСsentry.Форма.ФормаСписка");
ФормаСписка.Элементы.Список.Обновить();
КонецПроцедуры
// Обновляет видимость элемнтов формы.
//
// Параметры:
// Открытие - Булево - признак того, что форма была только что открыта.
//
Процедура ОбновитьОтображениеЭлементовФормы(Открытие=Ложь)
Если Объект.Ссылка.Пустая() И Открытие Тогда
Элементы.ГруппаОбменЗапущен.Видимость = Ложь;
Элементы.ВыключитьОбмен.Видимость = Ложь;
Иначе
DSN = "https://" + Объект.Токен + "@" + Объект.Сервер + "/" + объект.НомерПроекта;
Если Объект.ОбменЗапущен Тогда
Элементы.ГруппаОбменЗапущен.Видимость = Истина;
Элементы.ОбменЗапущен.Видимость = Истина;
Элементы.ОбменЗапущенНадпись.Видимость = Истина;
Элементы.ОбменНеЗапущен.Видимость = Ложь;
Элементы.ОбменНеЗапущенНадпись.Видимость = Ложь;
Элементы.ЗапуститьОбменВРеальномВремени.Видимость = Ложь;
Элементы.ВыключитьОбмен.Видимость = Истина;
Иначе
Элементы.ГруппаОбменЗапущен.Видимость = Истина;
Элементы.ОбменЗапущен.Видимость = Ложь;
Элементы.ОбменЗапущенНадпись.Видимость = Ложь;
Элементы.ОбменНеЗапущен.Видимость = Истина;
Элементы.ОбменНеЗапущенНадпись.Видимость = Истина;
Элементы.ЗапуститьОбменВРеальномВремени.Видимость = Истина;
Элементы.ВыключитьОбмен.Видимость = Ложь;
КонецЕсли;
КонецЕсли;
Если ЗначениеЗаполнено(Объект.Токен) И ЗначениеЗаполнено(Объект.Сервер) И ЗначениеЗаполнено(Объект.НомерПроекта) Тогда
Элементы.ПроверитьСоединение.Доступность = Истина;
Элементы.ПроверитьСоединение.ОтображениеПодсказки = ОтображениеПодсказки.Нет;
Элементы.ЗапуститьОбменВРеальномВремени.Доступность = Истина;
Иначе
Элементы.ПроверитьСоединение.Доступность = Ложь;
Элементы.ПроверитьСоединение.ОтображениеПодсказки = ОтображениеПодсказки.Кнопка;
Элементы.ЗапуститьОбменВРеальномВремени.Доступность = Ложь;
КонецЕсли;
Если Объект.РежимРаботы = Перечисления.sentry_РежимыРаботы.ВыгрузкаЗаПериод Тогда
Если Объект.ВыгружатьЖурналРегистрации Тогда
Элементы.ГруппаПериодЖурналРегистрации.Видимость = Истина;
Иначе
Элементы.ГруппаПериодЖурналРегистрации.Видимость = Ложь;
КонецЕсли;
Если Объект.ВыгружатьЗамерыВремени Тогда
Элементы.ГруппаПериодЗамерыВремени.Видимость = Истина;
Иначе
Элементы.ГруппаПериодЗамерыВремени.Видимость = Ложь;
КонецЕсли;
Иначе
Элементы.ГруппаПериодЖурналРегистрации.Видимость = Ложь;
Элементы.ГруппаПериодЗамерыВремени.Видимость = Ложь;
КонецЕсли;
КонецПроцедуры
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
&НаКлиенте
Процедура DSNПриИзменении(Элемент)
ПозицияПоследнегоСлеша = СтрНайти(Элемент.ТекстРедактирования, "/", НаправлениеПоиска.СКонца,,1);
ПозицияСобаки = СтрНайти(Элемент.ТекстРедактирования, "@");
Объект.Токен = Сред(Элемент.ТекстРедактирования, 9, ПозицияСобаки - 9);
Объект.Сервер = Сред(Элемент.ТекстРедактирования, ПозицияСобаки + 1, ПозицияПоследнегоСлеша - (ПозицияСобаки + 1));
Объект.НомерПроекта = Сред(Элемент.ТекстРедактирования, ПозицияПоследнегоСлеша + 1);
Объект.ЗащищенноеСоединение = Истина;
Объект.Порт = 443;
ОбновитьОтображениеЭлементовФормы();
КонецПроцедуры
&НаКлиенте
Процедура РежимРаботыПриИзменении(Элемент)
ОбновитьОтображениеЭлементовФормы();
КонецПроцедуры
&НаКлиенте
Процедура ВыгружаемыеУровниЖурналаРегистрацииПередОкончаниемРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
Если Объект.ВыгружаемыеУровниЖурналаРегистрации.Количество() > 1 Тогда
ДобавленнаяСтрокаТЧ = Объект.ВыгружаемыеУровниЖурналаРегистрации[Объект.ВыгружаемыеУровниЖурналаРегистрации.Количество()-1];
ЗначениеДобавленнойСтрокиТЧ = ДобавленнаяСтрокаТЧ.УровеньЖурналаРегистрации;
Дубли = Объект.ВыгружаемыеУровниЖурналаРегистрации.НайтиСтроки(Новый Структура("УровеньЖурналаРегистрации", ЗначениеДобавленнойСтрокиТЧ));
Если Дубли.Количество() > 1 Тогда
Сообщить("Этот уровень журнала регистрации уже добавлен.");
Объект.ВыгружаемыеУровниЖурналаРегистрации.Удалить(ДобавленнаяСтрокаТЧ);
Отказ = Истина;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
#КонецОбласти
#Область Прочее
// Проверяет, есть ли еще элементы справочника НастройкиСинхронизацииСsentry
// с такими же значениями реквизитов Сервер, НомерПроекта, Токен.
//
// Параметры:
// Ссылка - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку.
// Сервер - Строка - содержит адрес sentry.
// Токен - Строка - содержит токен для sentry.
// НомерПроекта - Строка - содержит номер проекта sentry.
//
// Возвращаемое значение:
// Булево - если Истина, значит элемент с такими реквизитами уже существует.
//
Функция ПроверитьНастройкуНаДубль(Ссылка, Сервер, Токен, НомерПроекта)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НастройкиСинхронизацииСsentry.Ссылка КАК Ссылка
|ИЗ
| Справочник.НастройкиСинхронизацииСsentry КАК НастройкиСинхронизацииСsentry
|ГДЕ
| НастройкиСинхронизацииСsentry.Ссылка <> &Ссылка
| И НастройкиСинхронизацииСsentry.Токен = &Токен
| И НастройкиСинхронизацииСsentry.НомерПроекта = &НомерПроекта
| И НастройкиСинхронизацииСsentry.Сервер = &Сервер";
Запрос.УстановитьПараметр("Сервер", Сервер);
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("Токен", Токен);
Запрос.УстановитьПараметр("НомерПроекта", НомерПроекта);
УстановитьПривилегированныйРежим(Истина);
РезультатЗапроса = Запрос.Выполнить();
УстановитьПривилегированныйРежим(Ложь);
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Если РезультатЗапроса.Пустой() Тогда
Возврат Ложь;
Иначе
Возврат Истина;
КонецЕсли;
КонецФункции
// Проверяет успешность установления соединения с ресурсом. Также сообщает пользователю
// о наличии некоторых ошибок.
//
// Возвращаемое значение:
// Булево - если соединение было установлено, возвращает Истина, иначе Ложь.
//
&НаСервере
Функция СоединениеУстановленоУспешно()
СистемнаяИнформация = Новый("СистемнаяИнформация");
СтруктураДанных = Новый Структура;
СтруктураДанных.Вставить("Дата", ТекущаяДатаСеанса());
СтруктураДанных.Вставить("Транзакция", "");
СтруктураДанных.Вставить("РабочийСервер", sentry_ОбщегоНазначенияСервер.ПолучитьСерверИнформационнойБазы());
СтруктураДанных.Вставить("Комментарий", "");
СтруктураДанных.Вставить("ПредставлениеМетаданных", "");
СтруктураДанных.Вставить("ПредставлениеСобытия", "Проверка установки соединения");
СтруктураДанных.Вставить("Компьютер", "");
СтруктураДанных.Вставить("ИмяПриложения", "");
СтруктураДанных.Вставить("ПредставлениеПриложения", "");
СтруктураДанных.Вставить("ПредставлениеДанных", "");
СтруктураДанных.Вставить("сеанс", "");
СтруктураДанных.Вставить("ВерсияПриложения", СистемнаяИнформация.ВерсияПриложения);
СтруктураДанных.Вставить("соединение", "");
СтруктураДанных.Вставить("ИмяПользователя", Строка(Пользователи.ТекущийПользователь()));
СтруктураТестовойОшибки = sentry_ОбщегоНазначенияСервер.СформироватьТелоСообщенияSentry(СтруктураДанных, "Информация", sentry_ОбщегоНазначенияСервер.ПолучитьОкружение(Объект.Окружение));
ДанныеСSentry = sentry_RestApi.ОтправкаДанных(sentry_Синхронизация.СформироватьСтруктуруНастроек(Объект.Ссылка), СтруктураТестовойОшибки, Истина);
Если ЗначениеЗаполнено(ДанныеСSentry) Тогда
КодСостояния = ДанныеСSentry.Получить("КодСостояния");
Если НЕ ЗначениеЗаполнено(КодСостояния) Тогда
Возврат Ложь;
Иначе
Если КодСостояния = 200 Тогда
Возврат Истина;
Иначе
Возврат Ложь;
КонецЕсли;
КонецЕсли;
Иначе
Возврат Ложь;
КонецЕсли;
КонецФункции
#КонецОбласти
Необходимо упомянуть важные аспекты работы модуля выше. Добавлена проверка создания дублирующих настроек интеграции (функция ПроверитьНастройкуНаДубль), т.е. нет возможности создать несколько подключений к одному и тому же проекту одного и того же Sentry. Есть возможность проверки установки соединения с Sentry путем отправки тестовой ошибки. Формирование и процесс отправки данных об ошибке будет описан позже. Элементы формы, которые не нужны при текущих настройках скрываются и вновь отображаются при соответствующих настройках, все это описано в процедуре ОбновитьОтображениеЭлементовФормы. У пользователя есть возможность указывать различные уровни журнала регистрации, т.е. без дублей, что отражено в обработчике события формы ВыгружаемыеУровниЖурналаРегистрацииПередОкончаниемРедактирования. Также все изменения формы в результате своих действий пользователь будет видеть сразу и форму не надо обновлять или использовать кнопку записи.
Стоит более подробно описать работу с DSN токеном. Он состоит из 3-х частей: токена, адреса Sentry и идентификатор проекта(в представленном коде переменная названа НомерПроекта).
Шаблон DSN токена: https://<токен>@<адрес Sentry>/<идентификатор проекта>.
Пример DSN токена: https://4df6cc9d87bgw6363353c051e23d@sentry.mycompany.ru/46.
По коду модуля формы видно, что при окончании редактирования DSN токена пользователем он разделяется на указанные выше части и записывается в соответствующие реквизиты.
Выгрузка данных в Sentry во всех трех режимах происходит с помощью фонового задания, т.е. пользователю не нужно ждать, пока все данные выгрузятся в Sentry, он может и дальше работать с 1С. Для режимов ПолнаяВыгрузка и ВыгрузкаЗаПериод используется форма ФормаВыполнениеСинхронизации, чтобы выводить пользователю информацию о ходе выгрузки данных. Сейчас выводятся только 2 сообщения: о начале и об окончании выгрузки данных. При необходимости логирование можно дополнить. Для режима работы ВыгружатьТолькоНовыеПослеЗапускаОбмена эта форма не используется, т.к. без выключения обмена пользователем этот процесс нельзя считать конечным, также не имеет смысла на глазах у пользователя логировать процессы неизвестной длительности. На рис. 6 представлена форма ФормаВыполнениеСинхронизации справочника НастройкиСинхронизацииСsentry.
Рисунок 6. Форма выполнения синхронизации с Sentry
Модуль формы
ФормаВыполнениеСинхронизации справочника
НастройкиСинхронизацииСsentry
#Область ОбработчикиСобытий
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
НастройкаОбмена = Параметры.НастройкаОбмена;
Родитель = Параметры.Родитель;
КонецПроцедуры
&НаКлиенте
Процедура ПриОткрытии(Отказ)
ПодключитьОбработчикОжидания("ЗапуститьВыполнениеОбмена", 0.1, Истина);
ПодключитьОбработчикОжидания("ОбновлениеИнформацииОбмена", 1);
КонецПроцедуры
#КонецОбласти
#Область Обмен
// Инициализирует выполнение обмена данными.
//
&НаКлиенте
Процедура ЗапуститьВыполнениеОбмена()
ДобавитьЕдиничнуюЗаписьВПротокол("Начало обмена");
ПараметрыОбмена = Новый Структура;
ПараметрыОбмена.Вставить("НастройкаОбмена" , НастройкаОбмена);
ДлительнаяОперация = ВыполнитьОбменСSentryНаСервере(ПараметрыОбмена);
ИдентификаторЗадания = ДлительнаяОперация.ИдентификаторЗадания;
ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
ПараметрыОжидания.ВыводитьОкноОжидания = Ложь;
ОписаниеОповещения = Новый ОписаниеОповещения("ПриЗавершенииОперацииЗагрузки", ЭтотОбъект);
ДлительныеОперацииКлиент.ОжидатьЗавершение(ДлительнаяОперация, ОписаниеОповещения, ПараметрыОжидания);
КонецПроцедуры
// Создает фоновое задание для выполнения обмена.
//
// ПараметрыОбмена - Структура:
// * НастройкаОбмена - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку обмена.
//
&НаСервере
Функция ВыполнитьОбменСSentryНаСервере(ПараметрыОбмена)
УстановитьПривилегированныйРежим(Истина);
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(УникальныйИдентификатор);
ПараметрыВыполнения.НаименованиеФоновогоЗадания = НСтр("ru = 'Выполнение обмена с Sentry'");
Результат = ДлительныеОперации.ВыполнитьВФоне("sentry_Синхронизация.ВыполнитьОбменДаннымиПоФоновомуЗаданию", ПараметрыОбмена, ПараметрыВыполнения);
Возврат Результат;
КонецФункции
// Обновляет информацию о состоянии обмена для пользователя.
//
&НаКлиенте
Процедура ОбновлениеИнформацииОбмена()
ОбновлениеИнформацииОбменаСервер();
ДлинаПротокола = СтрДлина(Протокол);
Если ДлинаПротокола > 0 Тогда
Элементы.Протокол.ТекущаяОбласть = ТабличныйПротокол.Области.Найти("Информация");
КонецЕсли;
Если ЗавершенаСинхронизация Тогда
ДобавитьЕдиничнуюЗаписьВПротокол("Завершение обмена");
ОтключитьОбработчикОжидания("ОбновлениеИнформацииОбмена");
КонецЕсли;
КонецПроцедуры
// Обновляет информацию о состоянии обмена для пользователя.
//
&НаСервере
Процедура ОбновлениеИнформацииОбменаСервер(Сообщения = Неопределено)
Если НЕ ЗначениеЗаполнено(Протокол) Тогда
ВыведенныйТекст = ТабличныйПротокол.НайтиТекст("Начало обмена").Текст;
Если ЗначениеЗаполнено(ВыведенныйТекст) Тогда
ПерваяЗаписьТекст = Лев(ВыведенныйТекст, СтрНайти(ВыведенныйТекст, Символы.ПС)-1);
ТабличныйПротокол.Очистить();
ДобавитьЕдиничнуюЗаписьВПротокол(?(ЗначениеЗаполнено(ПерваяЗаписьТекст), ПерваяЗаписьТекст, ВыведенныйТекст), Ложь);
КонецЕсли;
КонецЕсли;
Макет = Справочники.НастройкиСинхронизацииСsentry.ПолучитьМакет("МакетЛога");
Если Сообщения = Неопределено Тогда
Задания = ФоновыеЗадания.ПолучитьФоновыеЗадания(Новый Структура("УникальныйИдентификатор", ИдентификаторЗадания));
Если Задания.Количество() = 0 Тогда
Возврат;
КонецЕсли;
Задание = Задания[0];
Сообщения = Задание.ПолучитьСообщенияПользователю(Истина);
КонецЕсли;
Для Каждого Сообщение Из Сообщения Цикл
ТекстСообщения = Сообщение.Поле + ?(ЗначениеЗаполнено(Сообщение.Поле), " -- ", "") + Сообщение.Текст;
Протокол = Протокол + ТекстСообщения + Символы.ПС;
Если (Сообщение.ПутьКДанным = "КритическаяОшибка" ИЛИ Сообщение.ПутьКДанным = "Критическая ошибка")
И ЗначениеЗаполнено(Сообщение.КлючДанных) Тогда
ОбластьМакета = Макет.ПолучитьОбласть("КритическаяОшибкаСсылка");
ИначеЕсли (Сообщение.ПутьКДанным = "КритическаяОшибка" ИЛИ Сообщение.ПутьКДанным = "Критическая ошибка")
И НЕ ЗначениеЗаполнено(Сообщение.КлючДанных) Тогда
ОбластьМакета = Макет.ПолучитьОбласть("КритическаяОшибка");
ИначеЕсли Сообщение.ПутьКДанным = "Ошибка" И ЗначениеЗаполнено(Сообщение.КлючДанных) Тогда
ОбластьМакета = Макет.ПолучитьОбласть("ОшибкаСсылка");
ИначеЕсли Сообщение.ПутьКДанным = "Ошибка" И НЕ ЗначениеЗаполнено(Сообщение.КлючДанных) Тогда
ОбластьМакета = Макет.ПолучитьОбласть("Ошибка");
ИначеЕсли Сообщение.ПутьКДанным = "Информация" И ЗначениеЗаполнено(Сообщение.КлючДанных) Тогда
ОбластьМакета = Макет.ПолучитьОбласть("ИнформацияСсылка");
ИначеЕсли Сообщение.ПутьКДанным = "Информация" И НЕ ЗначениеЗаполнено(Сообщение.КлючДанных) Тогда
ОбластьМакета = Макет.ПолучитьОбласть("Информация");
Иначе
ОбластьМакета = Макет.ПолучитьОбласть("Информация");
КонецЕсли;
ОбластьМакета.Параметры.ТекстСообщения = ТекстСообщения;
ОбластьМакета.Параметры.СсылкаНаОбъект = Сообщение.КлючДанных;
ТабличныйПротокол.Вывести(ОбластьМакета);
КонецЦикла;
КонецПроцедуры
// Добавляет запись с информацией о состоянии обмена.
//
// Параметры:
// Сообщение - Строка - текст сообщения для пользователя.
// УказыватьВремя - Булево - если Истина, то будет указано время сообщения.
//
&НаСервере
Процедура ДобавитьЕдиничнуюЗаписьВПротокол(Сообщение, УказыватьВремя=Истина)
Макет = Справочники.НастройкиСинхронизацииСsentry.ПолучитьМакет("МакетЛога");
ТекстСообщения = ?(УказыватьВремя, Формат(ТекущаяДатаСеанса(),"ДЛФ=T") + " -- " + Сообщение, Сообщение);
Протокол = Протокол + ТекстСообщения + Символы.ПС;
ОбластьМакета= Макет.ПолучитьОбласть("Информация");
ОбластьМакета.Параметры.ТекстСообщения = ТекстСообщения;
ТабличныйПротокол.Вывести(ОбластьМакета);
КонецПроцедуры
// Является продолжением ЗапуститьВыполнениеОбмена.
//
&НаКлиенте
Процедура ПриЗавершенииОперацииЗагрузки(Результат, ДополнительныеПараметры) Экспорт
Элементы.ГруппаКнопки.Видимость = Истина;
Элементы.КнопкаЗакрыть.Видимость = Истина;
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
Данные = ПолучитьИзВременногоХранилища(Результат.АдресРезультата);
Если ЗначениеЗаполнено(Данные) И НЕ СтрЧислоВхождений(Протокол, Символы.ПС) - 1 = Данные.Количество() Тогда
Протокол = "";
КонецЕсли;
Если Протокол = "" Тогда
Если ТипЗнч(Данные) = Тип("ФиксированныйМассив") Тогда
ОбновлениеИнформацииОбменаСервер(Данные);
КонецЕсли;
КонецЕсли;
Если ЗначениеЗаполнено(Результат.ПодробноеПредставлениеОшибки) Тогда
ОписаниеОшибки = Результат.ПодробноеПредставлениеОшибки;
Элементы.СтраницыФормы.ТекущаяСтраница = Элементы.СтраницаОшибка;
Иначе
ПоказатьОповещениеПользователя(
СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(
НСтр("ru = '%1 ""%2""'"),
Формат(sentry_ОбщегоНазначенияСервер.ПолучитьТекущуюДатуСеанса(), "ДЛФ=DT"),
НастройкаОбмена)
,,
НСтр("ru = 'Обмен с sentry завершен'"),
БиблиотекаКартинок.Информация32);
Элементы.НадписьВыполняетсяОбмен.Заголовок = "Обмен завершен";
Элементы.КартинкаВыполняетсяОбмен.Видимость = Ложь;
Элементы.КнопкаЗакрыть.КнопкаПоУмолчанию = Истина;
КонецЕсли;
ОбновлениеИнформацииОбмена();
ЗавершенаСинхронизация = Истина;
ИзменитьРодителяНаСервере();
ОбновитьФормы();
КонецПроцедуры
#КонецОбласти
#Область ОбновлениеРодителя
//Обновляет открытую родительскую форму и форму списка, откуда она была открыта.
//
&НаКлиенте
Процедура ОбновитьФормы()
ОткрытыеОкна = ПолучитьОкна();
Для Каждого ОткрытоеОкно Из ОткрытыеОкна Цикл
Если СтрНайти(ОткрытоеОкно.Заголовок, "(Настройки синхронизации с sentry)") > 0 Тогда
Форма = ОткрытоеОкно.Содержимое[0];
Форма.Прочитать();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
// Устанавливает для настройки синхронизации из которой была открыта данная форма значение реквизита
// ОбменЗапущен в Ложь после выполнения обмена.
//
&НаСервере
Процедура ИзменитьРодителяНаСервере()
ОбъектРодитель = Родитель.ПолучитьОбъект();
ОбъектРодитель.ОбменЗапущен = Ложь;
ОбъектРодитель.Записать();
КонецПроцедуры
#КонецОбласти
В модуле формы выше отсутствуют какие-либо важные моменты, которые требуют подробного разбора. Он отвечает за запуск фонового задания и обновление форм, в т.ч. за отображение логов пользователю. Вывод сообщений пользователю реализован с помощью макета МакетЛога.
Общий модуль
sentry_Синхронизация
#Область ОбменДанными
// Выполняет обмен данными по фоновому заданию.
//
// Параметры:
// ПараметрыОбмена - Структура:
// * НастройкаОбмена - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку обмена.
// АдресРезультата - УникальныйИдентификатор, Строка - содержит адрес с результатом выполнения фонового задания(необязательный).
//
Процедура ВыполнитьОбменДаннымиПоФоновомуЗаданию(ПараметрыОбмена, АдресРезультата=Неопределено) Экспорт
НастройкаОбмена = ПараметрыОбмена.НастройкаОбмена;
СтруктураНастроек = СформироватьСтруктуруНастроек(НастройкаОбмена);
ВыполнитьОбменДанными(СтруктураНастроек);
ПоместитьВоВременноеХранилище(ПолучитьСообщенияПользователю(Истина), АдресРезультата);
КонецПроцедуры
// Выполняет обмен данными по фоновому заданию при режиме обмена ВыгружатьТолькоНовыеПослеЗапускаОбмена.
//
// Параметры:
// ПараметрыОбмена - Структура:
// * НастройкаОбмена - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку обмена.
// АдресРезультата - УникальныйИдентификатор, Строка - содержит адрес с результатом выполнения фонового задания(необязательный).
//
Процедура ВыполнитьОбменДаннымиВРежимеРеальногоВремениПоФоновомуЗаданию(ПараметрыОбмена, АдресРезультата=Неопределено) Экспорт
НастройкаОбмена = ПараметрыОбмена.НастройкаОбмена;
ДатаНачалаВыгрузкиНовых = ТекущаяДатаСеанса();
СтруктураНастроек = СформироватьСтруктуруНастроек(НастройкаОбмена);
Пока НастройкаОбмена.ОбменЗапущен Цикл
УстановитьПривилегированныйРежим(Истина);
НастройкаСинхронизацииОбъект = НастройкаОбмена.ПолучитьОбъект();
УстановитьПривилегированныйРежим(Ложь);
Если НастройкаСинхронизацииОбъект = Неопределено Тогда
Прервать;
КонецЕсли;
ВыполнитьОбменДанными(СтруктураНастроек, ДатаНачалаВыгрузкиНовых);
КонецЦикла;
ПоместитьВоВременноеХранилище(ПолучитьСообщенияПользователю(Истина), АдресРезультата);
КонецПроцедуры
// Является продолжением ВыполнитьОбменДаннымиПоФоновомуЗаданию, ВыполнитьОбменДаннымиВРежимеРеальногоВремениПоФоновомуЗаданию.
// Инициализирует процесс обмена данными.
//
// Параметры:
// СтруктураНастроек - см. СформироватьСтруктуруНастроек.
// ДатаНачалаВыгрузкиНовых - Дата - Заполняется только при режиме ВыгружатьТолькоНовыеПослеЗапускаОбмена. Значение по умолчанию Неопределено.
//
Процедура ВыполнитьОбменДанными(СтруктураНастроек, ДатаНачалаВыгрузкиНовых=Неопределено) Экспорт
Если СтруктураНастроек = Неопределено Тогда
Возврат;
КонецЕсли;
Если СтруктураНастроек.ВыгружатьЖурналРегистрации Тогда
sentry_ВыгрузитьЖурналРегистрации(СтруктураНастроек, ДатаНачалаВыгрузкиНовых);
КонецЕсли;
Если СтруктураНастроек.ВыгружатьЗамерыВремени Тогда
ВыгрузитьЗамерыВремени(СтруктураНастроек, ДатаНачалаВыгрузкиНовых);
КонецЕсли;
КонецПроцедуры
#конецОбласти
#Область СтруктураНастроек
// Возвращает структуру настроек по настройке обмена.
//
// Параметры:
// НастройкаОбмена - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку обмена.
//
// Возвращаемое значение:
// Структура - данные о настройке синхронизации. Свойства структуры:
// * НастройкаОбмена - СправочникСсылка.НастройкиСинхронизацииСsentry - содержит ссылку на настройку обмена.
// * Сервер - Строка - содержит адрес sentry для обмена данными.
// * НомерПроекта - Строка - содержит номер проекта sentry для обмена данными.
// * Токен - Строка - содержит токен sentry для обмена данными.
// * ЗащищенноеСоединение - Булево - содержит признак защищенности соединения при обмене данными.
// * Порт - Число - содержит номер порта для обмена данными.
// * ОбменЗапущен - Булево - если Истина, когда для текущей настройки выполняется фоновое задание для
// обмена данными, иначе в ручном режиме.
// * ИдентификаторФоновогоЗадания - УникальныйИдентификатор - содержит идентификатор фонового задания.
// * НачалоПериодаЖурналРегистрации - Дата - дата начала выгрузки данных из журнала регистрации.
// * ОкончаниеПериодаЖурналРегистрации - Дата - дата окончания выгрузки данных из журнала регистрации.
// * НачалоПериодаЗамерыВремени - Дата - дата начала выгрузки данных замеров времени.
// * ОкончаниеПериодаЗамерыВремени - Дата - дата окончания выгрузки данных замеров времени.
// * ВыгружатьЖурналРегистрации - Булево - если Истина, тогда будут выгружены данные из журнала регистрации.
// * ВыгружатьЗамерыВремени - Булево - если Истина, тогда будут выгружены данные замеров времени.
// * РежимРаботы - ПеречислениеСсылка.sentry_РежимыРаботы - содержит информацию о режиме работы обмена.
// * ВыгружаемыеУровниЖурналаРегистрации- Массив - содержит список выгружаемых уровней журнала регистрации.
// Элементами являются значения УровеньЖурналаРегистрации.
// * Окружение - ПеречислениеСсылка.sentry_Окружение - содержит информаци об окружении.
//
Функция СформироватьСтруктуруНастроек(НастройкаОбмена) Экспорт
СтруктураНастроек = Новый Структура;
СтруктураНастроек.Вставить("НастройкаОбмена", НастройкаОбмена);
СтруктураНастроек.Вставить("Сервер", НастройкаОбмена.Сервер);
СтруктураНастроек.Вставить("НомерПроекта", НастройкаОбмена.НомерПроекта);
СтруктураНастроек.Вставить("Токен", НастройкаОбмена.Токен);
СтруктураНастроек.Вставить("ЗащищенноеСоединение", НастройкаОбмена.ЗащищенноеСоединение);
СтруктураНастроек.Вставить("Порт", НастройкаОбмена.Порт);
СтруктураНастроек.Вставить("ОбменЗапущен", НастройкаОбмена.ОбменЗапущен);
СтруктураНастроек.Вставить("ИдентификаторФоновогоЗадания", НастройкаОбмена.ИдентификаторФоновогоЗадания);
СтруктураНастроек.Вставить("НачалоПериодаЖурналРегистрации", НастройкаОбмена.НачалоПериодаЖурналРегистрации);
СтруктураНастроек.Вставить("ОкончаниеПериодаЖурналРегистрации", НастройкаОбмена.ОкончаниеПериодаЖурналРегистрации);
СтруктураНастроек.Вставить("НачалоПериодаЗамерыВремени", НастройкаОбмена.НачалоПериодаЗамерыВремени);
СтруктураНастроек.Вставить("ОкончаниеПериодаЗамерыВремени", НастройкаОбмена.ОкончаниеПериодаЗамерыВремени);
СтруктураНастроек.Вставить("ВыгружатьЖурналРегистрации", НастройкаОбмена.ВыгружатьЖурналРегистрации);
СтруктураНастроек.Вставить("ВыгружатьЗамерыВремени", НастройкаОбмена.ВыгружатьЗамерыВремени);
СтруктураНастроек.Вставить("РежимРаботы", НастройкаОбмена.РежимРаботы);
СтруктураНастроек.Вставить("ВыгружаемыеУровниЖурналаРегистрации", sentry_ОбщегоНазначенияСервер.ПолучитьУровниЖурналаРегистрации(НастройкаОбмена.ВыгружаемыеУровниЖурналаРегистрации.ВыгрузитьКолонку("УровеньЖурналаРегистрации")));
СтруктураНастроек.Вставить("Окружение", НастройкаОбмена.Окружение);
Возврат СтруктураНастроек;
КонецФункции
#КонецОбласти
#Область ВыгрузкаДанных
// Производит формирование и выгрузку данных в sentry из журнала регистрации.
//
// Параметры:
// СтруктураНастроек - см. СформироватьСтруктуруНастроек.
// ДатаНачалаВыгрузкиНовых - Дата - Заполняется только при режиме ВыгружатьТолькоНовыеПослеЗапускаОбмена. Значение по умолчанию Неопределено.
//
Процедура sentry_ВыгрузитьЖурналРегистрации(СтруктураНастроек, ДатаНачалаВыгрузкиНовых=Неопределено)
СохранитьПоследнююДату = Ложь;
Окружение = sentry_ОбщегоНазначенияСервер.ПолучитьОкружение(СтруктураНастроек.Окружение);
Для Каждого УровеньЖурнала Из СтруктураНастроек.ВыгружаемыеУровниЖурналаРегистрации Цикл
ОтборЖурнала = Новый Структура();
ОтборЖурнала.Вставить("Уровень", УровеньЖурнала);
Если СтруктураНастроек.РежимРаботы = Перечисления.sentry_РежимыРаботы.ВыгрузкаЗаПериод Тогда
ОтборЖурнала.Вставить("ДатаНачала", СтруктураНастроек.НачалоПериодаЖурналРегистрации);
ОтборЖурнала.Вставить("ДатаОкончания", СтруктураНастроек.ОкончаниеПериодаЖурналРегистрации);
ИначеЕсли СтруктураНастроек.РежимРаботы = Перечисления.sentry_РежимыРаботы.ВыгружатьТолькоНовыеПослеЗапускаОбмена Тогда
ОтборЖурнала.Вставить("ДатаНачала", ДатаНачалаВыгрузкиНовых-1);
КонецЕсли;
СобытияЖурналаТекущийДень = Новый ТаблицаЗначений;
ВыгрузитьЖурналРегистрации(СобытияЖурналаТекущийДень,ОтборЖурнала,,,);
СобытияЖурналаТекущийДень.Сортировать("Дата Возр");
Для Каждого СтрокаЖурнала Из СобытияЖурналаТекущийДень Цикл
Тело = sentry_ОбщегоНазначенияСервер.СформироватьТелоСообщенияSentry(СтрокаЖурнала, УровеньЖурнала, Окружение);
Ответ = sentry_RestApi.ОтправкаДанных(СтруктураНастроек, Тело);
СохранитьПоследнююДату = Истина;
КонецЦикла;
Если СохранитьПоследнююДату Тогда
ДатаНачалаВыгрузкиНовых = СтрокаЖурнала.Дата + 1;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
// Производит формирование и выгрузку данных в sentry замеров времени.
//
// Параметры:
// СтруктураНастроек - см. СформироватьСтруктуруНастроек.
// ДатаНачалаВыгрузкиНовых - Дата - Заполняется только при режиме ВыгружатьТолькоНовыеПослеЗапускаОбмена. Значение по умолчанию Неопределено.
//
Процедура ВыгрузитьЗамерыВремени(СтруктураНастроек, ДатаНачалаВыгрузкиНовых=Неопределено)
СохранитьПоследнююДату = Ложь;
Если НЕ ЗначениеЗаполнено(ДатаНачалаВыгрузкиНовых) Тогда
ДатаНачала = СтруктураНастроек.НачалоПериодаЗамерыВремени;
ДатаОкончания = СтруктураНастроек.ОкончаниеПериодаЗамерыВремени;
Иначе
ДатаНачала = ДатаНачалаВыгрузкиНовых;
ДатаОкончания = Неопределено;
КонецЕсли;
ВыборкаЗамеровВремени = sentry_ОбщегоНазначенияСервер.ПолучитьВыборкуЗамеровВремени(УниверсальноеВремя(ДатаНачала, "Europe/Moscow"),
?(ЗначениеЗаполнено(ДатаОкончания), УниверсальноеВремя(ДатаОкончания, "Europe/Moscow"), Неопределено));
Пока ВыборкаЗамеровВремени.Следующий() Цикл
МассивJson = sentry_ОбщегоНазначенияСервер.СформироватьМассимвJsonСЗамерамиВремени(ВыборкаЗамеровВремени, СтруктураНастроек,
sentry_ОбщегоНазначенияСервер.ПолучитьОкружение(СтруктураНастроек.Окружение));
Ответ = sentry_RestApi.ОтправкаДанных(СтруктураНастроек, МассивJson);
СохранитьПоследнююДату = Истина;
КонецЦикла;
Если СохранитьПоследнююДату Тогда
ДатаНачалаВыгрузкиНовых = МестноеВремя(ВыборкаЗамеровВремени.ДатаЗаписи, "Europe/Moscow") + 1;
КонецЕсли;
КонецПроцедуры
#КонецОбласти
Модуль sentry_Синхронизация имеет несколько важных особенностей работы. Он отвечает за старт работы фоновых заданий, именно с него начинается исполнение фонового задания. Процедуры ВыполнитьОбменДаннымиПоФоновомуЗаданию и ВыполнитьОбменДаннымиВРежимеРеальногоВремениПоФоновомуЗаданию отличаются тем, что первая используется в режимах работы ПолнаяВыгрузка и ВыгрузкаЗаПериод, а вторая при ВыгружатьТолькоНовыеПослеЗапускаОбмена. Вторая процедура будет исполнятся до тех пор, пока настройка, из которой был запущен обмен, существует и в ней не активирована кнопка выключения обмена. Естественно, код прекратит свое исполнение при возникновении ошибок в коде или при удалении фонового задания.
Необходимо уделить внимание функции СформироватьСтруктуруНастроек, т.к. она имеет важную роль. Дело в том, что по ходу исполнения кода необходимо обращаться к различным данным настройки обмена. Вдобавок, это происходит в цикле. Если в текущей ситуации обращаться к информации, которую хранит реквизит справочника, через точку, то это увеличит нагрузку на систему. Такой метод получения данных создает много запросов к БД, также идет получение не только запрашиваемого реквизита, но и всех остальных. По этой тематике много информации на просторах интернета, где самостоятельно можно наглядно увидеть, как это работает на стороне СУБД. Чтобы решить проблему, описанную выше, значения реквизитов справочника записываются в структуру и хранятся в переменной, что избавляет от необходимости обращения к БД.
Получение данных об ошибках происходит с помощью выгрузки данных из журнала регистрации посредством использования процедуры встроенного языка ВыгрузитьЖурналРегистрации. Есть возможность настроить отбор, отбор происходит только по уровню журнала и датам начала и окончания, если они заданы. В режиме работы ВыгружатьТолькоНовыеПослеЗапускаОбмена дата начала отбора уменьшается на секунду. Это необходимо, когда в момент формирования предыдущей выгрузки была добавлена запись в ЖР, которая в выгрузку не попала. Выгрузка замеров времени ничем примечательным не отличается и формируется по данным РС ЗамерыВремени.
Общий модуль
sentry_ОбщегоНазначенияСервер
#Область ФормированиеДанных
// Формирует тело сообщения для отправки в sentry.
//
// Параметры:
// СтрокаЖурнала - СтрокаТаблицыЗначений - содержит данные строки журнала регистрации.
// УровеньЖурналаSentry - УровеньЖурналаРегистрации - содержит уровень журнала регистрации.
// Окружение - Строка - см. ПолучитьОкружение.
//
// Возвращаемое значение:
// Структура - данные строки журнала регистрации. Структура в себе может содержать другие структуры,
// соответствия и массивы. Ключ является именем поля, а значение его значением.
//
Функция СформироватьТелоСообщенияSentry(СтрокаЖурнала, УровеньЖурналаSentry, Окружение) Экспорт
СистемнаяИнформация = Новый("СистемнаяИнформация");
TimeStamp = СтрокаЖурнала.Дата;
ИмяБазы = ПолучитьИмяИнформационнойБазы();
Тело = Новый Структура();
Тело.Вставить("timestamp", TimeStamp);
Тело.Вставить("logger", "1C_Sentry_logger");
Тело.Вставить("platform", "Other"); //нельзя указать 1С, у него только фиксированный список платформ
Тело.Вставить("level", КонвертироватьУровень(Строка(УровеньЖурналаSentry)));
Тело.Вставить("transaction", СтрокаЖурнала.Транзакция);
Тело.Вставить("server_name", СтрокаЖурнала.РабочийСервер);
Тело.Вставить("release", Метаданные.Версия);
Тело.Вставить("dist", ИмяБазы); //Подставлять имя базы
Тело.Вставить("environment", Окружение); //Тут важно определять это рабочая база или нет
Тело.Вставить("message", СтрокаЖурнала.Комментарий);
Тело.Вставить("tags", Новый Соответствие);
Тело["tags"].Вставить("context", Строка(Метаданные.РежимСовместимости));
Тело.Вставить("extra", Новый Соответствие);
Тело["extra"].Вставить("some_other_value", "foo bar"); //Можно добавить что-нибудь свое как доп. информацию
//По ключу "exception" идет группировка ошибок
Тело.Вставить("exception", Новый Соответствие);
МодульОшибки = "";
Если ПустаяСтрока(МодульОшибки) Тогда
МодульОшибки = СтрокаЖурнала.ПредставлениеМетаданных;
КонецЕсли;
Тело["exception"].Вставить("values", Новый Массив);
Тело["exception"]["values"].Добавить(Новый Структура("type, value", СтрокаЖурнала.ПредставлениеСобытия, МодульОшибки));
Тело.Вставить("contexts", Новый Соответствие);
Тело["contexts"].Вставить("device", Новый Соответствие);
Тело["contexts"]["device"].вставить("type", "device");
Тело["contexts"]["device"].вставить("name", СтрокаЖурнала.Компьютер);
Тело["contexts"]["device"].вставить("family", "PC");
Тело["contexts"].Вставить("os", Новый Соответствие);
Тело["contexts"]["os"].вставить("name", "windows"); //Информация о компьютере не вытягивается, но возможно будет
Тело["contexts"].Вставить("app", Новый Соответствие);
Тело["contexts"]["app"].вставить("app_name", СтрокаЖурнала.ИмяПриложения);
Тело["contexts"]["app"].вставить("build_type", СтрокаЖурнала.ПредставлениеПриложения);
Тело["contexts"]["app"].вставить("app_version", СистемнаяИнформация.ВерсияПриложения);
Тело["contexts"].Вставить("session_data", Новый Соответствие);
Тело["contexts"]["session_data"].вставить("session", СтрокаЖурнала.сеанс);
Тело["contexts"]["session_data"].вставить("connection", СтрокаЖурнала.соединение);
Data = Новый Структура;
Data.Вставить("metadata", СтрокаЖурнала.ПредставлениеМетаданных);
Data.Вставить("record", СтрокаЖурнала.ПредставлениеДанных);
Тело.Вставить("breadcrumbs", Новый Соответствие);
Тело["breadcrumbs"].Вставить("values", Новый Массив);
Тело["breadcrumbs"]["values"].Добавить(Новый Структура("timestamp, message, category, data", TimeStamp, СтрокаЖурнала.Комментарий, СтрокаЖурнала.ПредставлениеСобытия, Data));
Тело.Вставить("user", Новый Соответствие);
Тело["user"].Вставить("id", СтрокаЖурнала.ИмяПользователя);
Тело["user"].Вставить("username", СтрокаЖурнала.ИмяПользователя);
Тело["user"].Вставить("ip_address", СтрокаЖурнала.Компьютер);
Возврат Тело;
КонецФункции
// Конвертирует массив перечислений sentry_УровниЖурналаРегистрации в массив с элементами типа УровеньЖурналаРегистрации.
//
// Параметры:
// МассивПеречисленийУровней - Массив - содержит список уровней журнала регистрации(без повторений) типа sentry_УровниЖурналаРегистрации.
//
// Возвращаемое значение:
// Массив - список уровней журнала регистрации с типом УровеньЖурналаРегистрации.
//
Функция ПолучитьУровниЖурналаРегистрации(МассивПеречисленийУровней) Экспорт
Результат = Новый Массив;
Для Каждого ПеречислениеУровня Из МассивПеречисленийУровней Цикл
Если ПеречислениеУровня = Перечисления.sentry_УровниЖурналаРегистрации.Ошибка Тогда
Результат.Добавить(УровеньЖурналаРегистрации.Ошибка);
ИначеЕсли ПеречислениеУровня = Перечисления.sentry_УровниЖурналаРегистрации.Информация Тогда
Результат.Добавить(УровеньЖурналаРегистрации.Информация);
ИначеЕсли ПеречислениеУровня = Перечисления.sentry_УровниЖурналаРегистрации.Предупреждение Тогда
Результат.Добавить(УровеньЖурналаРегистрации.Предупреждение);
ИначеЕсли ПеречислениеУровня = Перечисления.sentry_УровниЖурналаРегистрации.Примечание Тогда
Результат.Добавить(УровеньЖурналаРегистрации.Примечание);
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
// Конвертирует наименование уровня журнала регистрации в наименование присутствующее в sentry.
//
// Параметры:
// Уровень - Строка - наименование уровня журнала регистрации.
//
// Возвращаемое значение:
// Строка - нименование уровня ошибки доступное в sentry.
//
Функция КонвертироватьУровень(Уровень) Экспорт
Если Уровень = "Ошибка" Тогда
Возврат "error";
ИначеЕсли Уровень = "Информация" ИЛИ Уровень = "Примечание" Тогда
Возврат "info";
ИначеЕсли Уровень = "Предупреждение" Тогда
Возврат "warning";
КонецЕсли;
КонецФункции
// Формирует сообщение для sentry с замерами времени.
//
// Параметры:
// СтрокаВыборкиЗамеровВремени - Произвольный - строка выборки из результата запроса запроса к РегистрСведений.ЗамерыВремени.
// СтруктураНастроек - см. sentry_Синхронизация.СформироватьСтруктуруНастроек.
// Окружение - см. ПолучитьОкружение.
//
// Возвращаемое значение:
// Массив - содержит в себе структуры с данными замера времени(данные для одного сообщения). Ключ является именем поля, а значение его значением.
//
Функция СформироватьМассимвJsonСЗамерамиВремени(СтрокаВыборкиЗамеровВремени, СтруктураНастроек, Окружение) Экспорт
trace_id = СтрЗаменить(XMLСтрока(Новый УникальныйИдентификатор), "-", "");
event_id = СтрЗаменить(XMLСтрока(Новый УникальныйИдентификатор), "-", "");
span_id = Лев(СтрЗаменить(XMLСтрока(Новый УникальныйИдентификатор), "-", ""), 16);
МассивСтруктур = Новый Массив;
ИмяБазы = sentry_ОбщегоНазначенияСервер.ПолучитьИмяИнформационнойБазы();
СистемнаяИнформация = Новый СистемнаяИнформация;
ВерсияОС = СистемнаяИнформация.ВерсияОС;
ВерсияПлатформы = СистемнаяИнформация.ВерсияПриложения;
ВерсияКонфигурации = Метаданные.Версия;
НазваниеКонфигурации = Метаданные.Представление();
Сервер1С = ИмяКомпьютера();
СтруктураРасширения = ПолучитьИмяИВерсиюРасширения();
СтруктураВерсий = Новый Структура;
СтруктураВерсий.Вставить("name", "1С: " + ИмяБазы);
СтруктураВерсий.Вставить("version", "platform: " + ВерсияПлатформы + ", configuration: " + ВерсияКонфигурации);
СтруктураВерсийРасширения = Новый Структура;
СтруктураВерсийРасширения.Вставить("name", "sentry.1c");//СтруктураРасширения.Имя);
СтруктураВерсийРасширения.Вставить("version", "4.5.0");//СтруктураРасширения.Версия);
Структураtrace = Новый Структура;
Структураtrace.Вставить("trace_id", trace_id);
Структураtrace.Вставить("sample_rate", "1.0");
Структураtrace.Вставить("transaction", Строка(СтрокаВыборкиЗамеровВремени.КлючеваяОперация));
Структураtrace.Вставить("public_key", СтруктураНастроек.Токен);
Структураtrace.Вставить("environment", Окружение);
Структураtrace.Вставить("sampled", Истина);
СтруктураСобытия = Новый Структура;
СтруктураСобытия.Вставить("event_id", event_id);
СтруктураСобытия.Вставить("sent_at", XMLСтрока(ТекущаяДатаСеанса())+"Z");
СтруктураСобытия.Вставить("dsn", "https://"+СтруктураНастроек.Токен+"@"+СтруктураНастроек.Сервер+"/"+СтруктураНастроек.НомерПроекта);
СтруктураСобытия.Вставить("sdk", СтруктураВерсийРасширения);
СтруктураСобытия.Вставить("trace", Структураtrace);
МассивСтруктур.Добавить(СтруктураСобытия);
МассивСтруктур.Добавить(Новый Структура("type, content_type", "transaction", "application/json"));
СтруктураОС = Новый Структура();
СтруктураОС.Вставить("name", ВерсияОС);
СтруктураПользователя = Новый Структура;
СтруктураПользователя.Вставить("fullName", СтрокаВыборкиЗамеровВремени.Пользователь);
Структураtrace = Новый Структура;
Структураtrace.Вставить("trace_id", trace_id);
Структураtrace.Вставить("span_id", span_id);
Структураtrace.Вставить("op", "ui");
Структураtrace.Вставить("status", ?(СтрокаВыборкиЗамеровВремени.ВыполненСОшибкой, "error", "ok"));
Структураtrace.Вставить("data", СтруктураПользователя);
Структураtrace.Вставить("runtime", СтруктураВерсий);
СтруктураКонтекста = Новый Структура();
СтруктураКонтекста.Вставить("os", СтруктураОС);
СтруктураКонтекста.Вставить("trace", Структураtrace);
СтруктураЗамеров = Новый Структура();
СтруктураЗамеров.Вставить("platform", НазваниеКонфигурации);
СтруктураЗамеров.Вставить("sdk", СтруктураВерсийРасширения);
СтруктураЗамеров.Вставить("transaction", Строка(СтрокаВыборкиЗамеровВремени.КлючеваяОперация));
СтруктураЗамеров.Вставить("server_name", Сервер1С);
СтруктураЗамеров.Вставить("environment", Окружение);
СтруктураЗамеров.Вставить("contexts", СтруктураКонтекста);
СтруктураЗамеров.Вставить("start_timestamp", КонвертироватьДатуВunixФормат(СтрокаВыборкиЗамеровВремени.ДатаНачалаЗамера));
СтруктураЗамеров.Вставить("timestamp", КонвертироватьДатуВunixФормат(СтрокаВыборкиЗамеровВремени.ДатаОкончания));
СтруктураЗамеров.Вставить("transaction_info", Новый Структура("source", "custom"));
МассивСтруктур.Добавить(СтруктураЗамеров);
Возврат МассивСтруктур;
КонецФункции
// Возвращает выборку из результатов запроса к РегистрСведений.ЗамерыВремени.
//
// Параметры:
// ДатаНачала - Дата, Неопределено - дата начала отбора данных. Параметр не заполняется при отборе данных за все время.
// ДатаОкончания - Дата, Неопределено - дата окончания отбора данных. Параметр не заполняется при отборе данных за все время или
// при отборе всех данных начиния с ДатаНачала.
//
// Возвращаемое значение:
// ВыборкаИзРезультатаЗапроса - данные выборки из результата запроса к РегистрСведений.ЗамерыВремени.
//
Функция ПолучитьВыборкуЗамеровВремени(ДатаНачала, ДатаОкончания) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗамерыВремени.КлючеваяОперация КАК КлючеваяОперация,
| ЗамерыВремени.ДатаНачалаЗамера КАК ДатаНачалаЗамера,
| ЗамерыВремени.ДатаОкончания КАК ДатаОкончания,
| ЗамерыВремени.Пользователь КАК Пользователь,
| ЗамерыВремени.ВыполненСОшибкой КАК ВыполненСОшибкой,
| ЗамерыВремени.ДатаЗаписи КАК ДатаЗаписи
|ИЗ
| РегистрСведений.ЗамерыВремени КАК ЗамерыВремени
|ГДЕ
| ЗамерыВремени.ДатаЗаписи МЕЖДУ &ДатаНачала И &ДатаОкончания";
Если ЗначениеЗаполнено(ДатаНачала) И ЗначениеЗаполнено(ДатаОкончания) Тогда
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);
ИначеЕсли ЗначениеЗаполнено(ДатаНачала) Тогда
Запрос.Текст = СтрЗаменить(Запрос.Текст, "ЗамерыВремени.ДатаЗаписи МЕЖДУ &ДатаНачала И &ДатаОкончания", "ЗамерыВремени.ДатаЗаписи >= &ДатаНачала");
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Иначе
Запрос.Текст = СтрЗаменить(Запрос.Текст, "ГДЕ" + Символы.ПС + "ЗамерыВремени.ДатаЗаписи МЕЖДУ &ДатаНачала И &ДатаОкончания", "");
КонецЕсли;
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Возврат ВыборкаДетальныеЗаписи;
КонецФункции
// Конвертирует значение перечисления sentry_Окружение в строку.
//
// Параметры:
// ОкружениеПеречислением - ПеречислениеСсылка.sentry_Окружение - окружение перечислением.
//
// Возвращаемое значение:
// Строка - окружение строкой.
//
Функция ПолучитьОкружение(ОкружениеПеречислением) Экспорт
Если ОкружениеПеречислением = Перечисления.sentry_Окружение.Прод Тогда
Возврат "Prod";
ИначеЕсли ОкружениеПеречислением = Перечисления.sentry_Окружение.Стейдж Тогда
Возврат "Stage";
ИначеЕсли ОкружениеПеречислением = Перечисления.sentry_Окружение.Тест Тогда
Возврат "Test";
КонецЕсли;
КонецФункции
#КонецОбласти
#Область ПолучениеДанныхИнформационнойБазы
// Возвращает имя информационной базы. Подходит для файловой базы и для клиент-серверной.
//
// Возвращаемое значение:
// Строка - имя информационной базы.
//
Функция ПолучитьИмяИнформационнойБазы() Экспорт
СтрокаСоединения = СтрокаСоединенияИнформационнойБазы();
НСтр(СтрокаСоединенияИнформационнойБазы(), "Ref");
МнСтрокаСоединения = СтрЗаменить(СтрокаСоединения, ";", Символы.ПС);
ИмяБазы = СтрЗаменить(СтрЗаменить(СтрПолучитьСтроку(МнСтрокаСоединения, 2), "Ref=", ""), """", "");
Если НЕ ЗначениеЗаполнено(ИмяБазы) Тогда
ПоследнийСлеш = СтрНайти(СтрокаСоединения, "\", НаправлениеПоиска.СКонца);
ИмяБазы = Сред(СтрокаСоединения, ПоследнийСлеш+1, СтрДлина(СтрокаСоединения)-ПоследнийСлеш-2);
КонецЕсли;
Возврат ИмяБазы;
КонецФункции
// Возвращает сервер информационной базы. Подходит для файловой базы и для клиент-серверной.
//
// Возвращаемое значение:
// Строка - сервер информационной базы. Для файловой базы будет возвращено имя компьютера.
//
Функция ПолучитьСерверИнформационнойБазы() Экспорт
СтрокаСоединения = СтрокаСоединенияИнформационнойБазы();
НСтр(СтрокаСоединенияИнформационнойБазы(), "Srvr");
МнСтрокаСоединения = СтрЗаменить(СтрокаСоединения, ";", Символы.ПС);
Сервер = СтрЗаменить(СтрЗаменить(СтрПолучитьСтроку(МнСтрокаСоединения, 1), "Srvr=", ""), """", "");
Если НЕ ЗначениеЗаполнено(Сервер) ИЛИ СтрНайти(МнСтрокаСоединения, "Srvr") = 0 Тогда
СтрокаИнформационнойБазы = ПолучитьТекущийСеансИнформационнойБазы();
ПозицияЗапятойДо = СтрНайти(СтрокаИнформационнойБазы, ",", НаправлениеПоиска.СКонца,, 4);
ПозицияЗапятойПосле = СтрНайти(СтрокаИнформационнойБазы, ",", НаправлениеПоиска.СКонца,, 3);
Сервер = СокрЛП(Сред(СтрокаИнформационнойБазы, ПозицияЗапятойДо+1, ПозицияЗапятойПосле-ПозицияЗапятойДо-1));
КонецЕсли;
Возврат Сервер;
КонецФункции
#КонецОбласти
#Область РаботаСДатами
// Конвертирует универсальную дату в миллисекундах в дату unix формата в миллисекундах.
//
// Параметры:
// ДатаВМиллисекундах - число - универсальная дата в миллисекундах в UTC, начиная с 01.01.0001 00:00:00.
//
// Возвращаемое значение:
// Число - unix дата в миллисекундах.
//
Функция КонвертироватьДатуВunixФормат(ДатаВМиллисекундах) Экспорт
ДатаБезМиллисекунд = Цел(Число(ДатаВМиллисекундах)/1000);
Миллисекунды = Число(ДатаВМиллисекундах)/1000-ДатаБезМиллисекунд;
unixtime = Число(Формат(Дата(1,1,1,3,0,0) + ДатаБезМиллисекунд - Дата(1970,1,1,0,0,0), "ЧГ=0")) + Миллисекунды;
Возврат unixtime;
КонецФункции
// см. ТекущаяДатаСеанса.
//
Функция ПолучитьТекущуюДатуСеанса() Экспорт
Возврат ТекущаяДатаСеанса();
КонецФункции
#КонецОбласти
#Область РаботаСРасширением
// Определяет используется ли расширение в безопасном режиме.
//
// Возвращаемое значение:
// Булево - признак использования расширения в безопасном режиме.
//
Функция РасширениеВБезопасномРежиме() Экспорт
Если ОбщегоНазначения.РазделениеВключено() Тогда
Возврат Ложь;
КонецЕсли;
Результат = Ложь;
УстановитьПривилегированныйРежим(Истина);
НайденныеРасширения = РасширенияКонфигурации.Получить(Новый Структура("Имя", ПолучитьИмяРасширения()));
УстановитьПривилегированныйРежим(Ложь);
Если НайденныеРасширения.Количество() > 0 Тогда
Результат = НайденныеРасширения[0].БезопасныйРежим;
КонецЕсли;
Возврат Результат;
КонецФункции
// Возвращает имя расширения.
//
// Возвращаемое значение:
// Строка - имя расширения.
//
Функция ПолучитьИмяРасширения() Экспорт
Возврат "ИнтеграцияС_sentry";
КонецФункции
// Возвращает имя и версию данного расширения.
//
// Возвращаемое значение:
// Строка - имя и версия данного расширения.
//
Функция ПолучитьИмяИВерсиюРасширения() Экспорт
Для Каждого Расширение Из РасширенияКонфигурации.Получить() Цикл
Если Расширение.Имя = "ИнтеграцияС_sentry" Тогда
Возврат Новый Структура("Имя, Версия", Расширение.Имя, Расширение.Версия);
КонецЕсли;
КонецЦикла;
КонецФункции
#КонецОбласти
Естественно, самым важным является формирование сообщения для Sentry, за которое отвечает функция СформироватьТелоСообщенияSentry при выгрузке журнала регистрации. Итоговое сообщение является структурой. Ключ структуры - имя поля, а значение - его значение. Ниже представлено назначение каждого поля[1]:
- timestamp - содержит дату события;
- logger - имя логера, можно задать по своему усмотрению;
- platform - тут обязательно указывается Other, т.к. в Sentry в списке доступных платформ нет 1С;
- level - уровень ошибки. У Sentry свой список уровней (их больше, чем в 1С) ошибок, поэтому было сопоставлено следующим образом: в 1С Ошибка - в Sentry error, в 1С Информация - в Sentry info, в 1С Примечание - в Sentry info, в 1С Предупреждение - в Sentry warning;
- transaction - транзакция, в этом поле передается транзакция из журнала регистрации;
- server_name - имя сервера;
- release - версия конфигурации;
- dist - имя базы;
- environment - окружение (Prod, Stage, Test);
- message - комментарий из журнала регистрации;
- tags - произвольные теги. Здесь было решено через поле context передавать режим совместимости;
- extra - через это поле можно передавать любую дополнительную информацию;
- exception - содержит информацию о возникшем событии и модулю, в котором оно возникло. По данному полю происходит группировка событий;
- contexts - контекст события. Тут передается информация о компьютере пользователя (т.е. клиента), операционной системе, приложении (фоновое задание, тонкий клиент и т.д.), версии платформы, сеансе и соединении;
- breadcrumbs - перечень вызовов перед возникновением ошибки;
- user - информация о пользователе.
Сообщение с информацией о замерах времени формируется похожим образом. В этом случае итоговые данные представлены массивом, который обязательно должен содержать 3 элемента: структура с информацией о замерах времени, структура с данными о событии и структура с информацией о типе данных в сообщении. В структуре события заполняются следующие поля:
- event_id- идентификатор события;
- sent_at - дата отправки данной информации. Указывается в формате UTC, поэтому к XML строке прибавляется символ "Z", при ином формате Sentry не воспримет отправленную информацию;
- dsn - DSN токен;
- sdk - Software Development Kit, сюда было решено передавать свое наименование и версию;
- trace - информация о трассировке. Содержит поля:
- trace_id - идентификатор трассировки. С его помощью можно отследить последовательность пользовательских операций (открытий окон в 1С), но в текущем решении эта возможность не используется, т.к. в РС ЗамерыВремени нет связи между записями и не отследить последовательность открытия окон;
- sample_rate - частота выборки для отправки данных. Данное число характеризует, какая часть данных отправляется. 1- все данные, а, например, 0.1 - 10% случайно выбранных данных. В контексте решаемой задачи этот параметр неважен, поэтому используется значение по умолчанию - 1;
- transaction - содержит наименование операции;
- public_key - ключ из DSN токена;
- environment - окружение;
- sampled - признак выборки данных.
В структуре с типом данных сообщения всего 2 поля:
- type - тип события, для замеров времени указывается "transaction";
- content_type - тип передаваемых данных, в данном случае "application/json".
Структура с данными о замерах времени содержит поля:
- platform - название конфигурации;
- sdk - такое же поле, как и в первой структуре;
- transaction - такое же поле, как и в первой структуре;
- server_name - сервер 1С;
- environment - окружение;
- contexts - содержит информацию о версии ОС и поле trace с полями:
- trace_id - такое же поле, как и в первой структуре;
- span_id - схож с идентификатором трассировки, но позволяет отслеживать последовательность рабочих процессов приложения. В данном решении этот функционал также не используется;
- op - тип операции, указывается "ui", т.к. замеры времени относятся к интерфейсу;
- status - содержит информация о том, с ошибкой или без была выполнена операция;
- data - содержит информация о пользователе;
- runtime - содержит имя базы и версии платформы и конфигурации.
- start_timestamp - дата начала события (открытия окна) в формате Unix в стандарте UTC;
- timestamp - дата окончания события (открытия окна) в формате Unix в стандарте UTC;
- transaction_info - информация о транзакции, в которой указан источник.
Передача дат начала и окончания события имеет особенность. В РС ЗамервВремени эти даты хранятся в универсальном формате в миллисекундах, а в Sentry они должны передаваться в Unix формате. Под вторым форматом понимается количество секунд (в текущем случае миллисекунд) прошедших с начала 1970-го года (01.01.1970). В 1С эти даты подразумевают количество секунд (в текущем случае миллисекунд) прошедших от рождества Христова (т.е. с 01.01.0001). Никаких штатных методов конвертации таких дат из одного формата в другой найдено не было, поэтому была написана функция КонвертироватьДатуВunixФормат, которая конвертирует универсальную дату в миллисекундах в 1С в дату Unix формата стандарта UTC.
Общий модуль
sentry_RestApi
#Область Общее
// Возвращает HTTP соединение с указанным в структуре параметра ресурсом.
//
// Параметры:
// СтруктураНастроек - Структура - см. sentry_Синхронизация.СформироватьСтруктуруНастроек.
//
// Возвращаемое значение:
// HTTPСоединение, Неопределено - содержит HTTP соединение. Возвращается Неопределено, если соединение
// не удалось установить, например, если не заполнен сервер.
//
Функция УстановитьСоединениеССервером(СтруктураНастроек) Экспорт
Попытка
Если СтруктураНастроек.Сервер = "" Тогда
Возврат Неопределено;
КонецЕсли;
Если НЕ СтруктураНастроек.ЗащищенноеСоединение Тогда
ssl = Неопределено;
Иначе
ssl = Новый ЗащищенноеСоединениеOpenSSL(
Неопределено,
Неопределено);
КонецЕсли;
Соединение = Новый HTTPСоединение(СтруктураНастроек.Токен + "@" + СтруктураНастроек.Сервер,, Неопределено,,,30, ssl);
Исключение
лТекстОшибки = НСтр("ru = 'Не удалось установить соединение с сервером'") + СтруктураНастроек.Сервер + ":" + Строка(СтруктураНастроек.Порт)
+ НСтр("ru = '.Проверьте правильность токена.'");
ОбщегоНазначения.СообщитьПользователю(лТекстОшибки);
ОбщегоНазначения.СообщитьПользователю(ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
Соединение = Неопределено;
КонецПопытки;
Возврат Соединение;
КонецФункции
// см. ПрочитатьJSON.
//
Функция ПрочитатьJSONНаСервере(JSON, ПрочитатьВСоответствие) Экспорт
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(JSON);
Возврат ПрочитатьJSON(ЧтениеJSON, ПрочитатьВСоответствие);
КонецФункции
#КонецОбласти
#Область РаботаСКодировками
// см. РаскодироватьСтроку.
//
Функция РаскодироватьСтрокуСервер(ТелоHTTPЗапроса) Экспорт
Возврат РаскодироватьСтроку(ТелоHTTPЗапроса, СпособКодированияСтроки.URLВКодировкеURL);
КонецФункции
// см. КодироватьСтроку.
//
Функция ЗакодироватьСтрокуСервер(ЗначениеСтроки) Экспорт
Возврат КодироватьСтроку(XMLСтрока(?(ТипЗнч(ЗначениеСтроки) = Тип("Булево"), Число(ЗначениеСтроки), ЗначениеСтроки)), СпособКодированияСтроки.КодировкаURL, "UTF8");
КонецФункции
// Раскодирует строку ответа от сервера.
//
// Параметры:
// URL - Строка - строка для раскодировки.
//
// Возвращаемое значение:
// Строка - содержит раскодированную строку.
//
Функция РаскодироватьJSON(URL) Экспорт
Результат = URL;
СписокСимволов = Новый СписокЗначений;
СписокСимволов.Добавить("\u0430", "а");
СписокСимволов.Добавить("\u0431", "б");
СписокСимволов.Добавить("\u0432", "в");
СписокСимволов.Добавить("\u0433", "г");
СписокСимволов.Добавить("\u0434", "д");
СписокСимволов.Добавить("\u0435", "е");
СписокСимволов.Добавить("\u0451", Символ(1105));
СписокСимволов.Добавить("\u0436", "ж");
СписокСимволов.Добавить("\u0437", "з");
СписокСимволов.Добавить("\u0438", "и");
СписокСимволов.Добавить("\u0439", "й");
СписокСимволов.Добавить("\u043a", "к");
СписокСимволов.Добавить("\u043b", "л");
СписокСимволов.Добавить("\u043c", "м");
СписокСимволов.Добавить("\u043d", "н");
СписокСимволов.Добавить("\u043e", "о");
СписокСимволов.Добавить("\u043f", "п");
СписокСимволов.Добавить("\u0440", "р");
СписокСимволов.Добавить("\u0441", "с");
СписокСимволов.Добавить("\u0442", "т");
СписокСимволов.Добавить("\u0443", "у");
СписокСимволов.Добавить("\u0444", "ф");
СписокСимволов.Добавить("\u0445", "х");
СписокСимволов.Добавить("\u0446", "ц");
СписокСимволов.Добавить("\u0447", "ч");
СписокСимволов.Добавить("\u0448", "ш");
СписокСимволов.Добавить("\u0449", "щ");
СписокСимволов.Добавить("\u044a", "ъ");
СписокСимволов.Добавить("\u044b", "ы");
СписокСимволов.Добавить("\u044c", "ь");
СписокСимволов.Добавить("\u044d", "э");
СписокСимволов.Добавить("\u044e", "ю");
СписокСимволов.Добавить("\u044f", "я");
СписокСимволов.Добавить("\u0410", "А");
СписокСимволов.Добавить("\u0411", "Б");
СписокСимволов.Добавить("\u0412", "В");
СписокСимволов.Добавить("\u0413", "Г");
СписокСимволов.Добавить("\u0414", "Д");
СписокСимволов.Добавить("\u0415", "Е");
СписокСимволов.Добавить("\u0401", Символ(1025));
СписокСимволов.Добавить("\u0416", "Ж");
СписокСимволов.Добавить("\u0417", "З");
СписокСимволов.Добавить("\u0418", "И");
СписокСимволов.Добавить("\u0419", "Й");
СписокСимволов.Добавить("\u041a", "К");
СписокСимволов.Добавить("\u041b", "Л");
СписокСимволов.Добавить("\u041c", "М");
СписокСимволов.Добавить("\u041d", "Н");
СписокСимволов.Добавить("\u041e", "О");
СписокСимволов.Добавить("\u041f", "П");
СписокСимволов.Добавить("\u0420", "Р");
СписокСимволов.Добавить("\u0421", "С");
СписокСимволов.Добавить("\u0422", "Т");
СписокСимволов.Добавить("\u0423", "У");
СписокСимволов.Добавить("\u0424", "Ф");
СписокСимволов.Добавить("\u0425", "Х");
СписокСимволов.Добавить("\u0426", "Ц");
СписокСимволов.Добавить("\u0427", "Ч");
СписокСимволов.Добавить("\u0428", "Ш");
СписокСимволов.Добавить("\u0429", "Щ");
СписокСимволов.Добавить("\u042a", "Ъ");
СписокСимволов.Добавить("\u042b", "Ы");
СписокСимволов.Добавить("\u042c", "Ь");
СписокСимволов.Добавить("\u042d", "Э");
СписокСимволов.Добавить("\u042e", "Ю");
СписокСимволов.Добавить("\u042f", "Я");
СписокСимволов.Добавить("\u0022", "'");
СписокСимволов.Добавить("\u003E", ">");
СписокСимволов.Добавить("\u003е", ">");
СписокСимволов.Добавить("\u003C", "<");
СписокСимволов.Добавить("\u003c", "<");
Для Каждого текЭлемент Из СписокСимволов Цикл
Результат = СтрЗаменить(Результат, текЭлемент.Значение, текЭлемент.Представление);
КонецЦикла;
Возврат Результат;
КонецФункции
#КонецОбласти
#Область ОтправкаДанных
// Отправляет данные на указанный сервер и возвращает ответ от сервера.
// Отправка/получение данных осуществляется с помощью JSON, т.е. методом POST.
//
// Параметры:
// СтруктураНастроек - Структура - см. sentry_Синхронизация.СформироватьСтруктуруНастроек.
// Поля - Структура, Массив - соержит данные для отправки в sentry. Внутри массива/структуры
// могут быть другие структуры или соответствия. Ключ является именем поля,
// а значение его значением.
// ДобавлятьКодОтвета - Булево - если Истина, то будет еще возвращаться и код ответа от сервера. По умолчанию Ложь.
//
//
// Возвращаемое значение:
// Массив, Соответствие, Булево, Неопределено - содержит JSON считанный в Соответствие. При возникновении ошибок и невозможности
// получения ответа от сервера возвращается Неопределено.
//
Функция ОтправкаДанных(СтруктураНастроек, Поля=Неопределено, ДобавлятьКодОтвета=Ложь) Экспорт
Соединение = УстановитьСоединениеССервером(СтруктураНастроек);
Если Соединение = Неопределено Тогда
Возврат Неопределено;
КонецЕсли;
HTTPЗапрос = Новый HTTPЗапрос;
HTTPЗапрос.АдресРесурса = "/api/" + СтруктураНастроек.НомерПроекта + "/store/";
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json;charset=utf-8");
HTTPЗапрос.Заголовки.Вставить("X-Sentry-Auth", "Sentry sentry_version=7,sentry_key=" + СтруктураНастроек.Токен);
HTTPЗапрос.Заголовки.Вставить("Authorization", "Bearer " + СтруктураНастроек.Токен);
Если ТипЗнч(Поля) = Тип("Структура") Тогда
тЗаписьJSON = Новый ЗаписьJSON;
тПараметрыJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет, " ", Истина);
тЗаписьJSON.УстановитьСтроку(тПараметрыJSON);
ЗаписатьJSON(тЗаписьJSON, Поля);
strJSON = тЗаписьJSON.Закрыть();
Иначе
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/x-sentry-envelope");
ПерваяИтерация = Истина;
Для Каждого СтруктураПолей Из Поля Цикл
Если ПерваяИтерация Тогда
ПерваяИтерация = Ложь;
тЗаписьJSON = Новый ЗаписьJSON;
тПараметрыJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет, " ", Истина);
тЗаписьJSON.УстановитьСтроку(тПараметрыJSON);
ЗаписатьJSON(тЗаписьJSON, СтруктураПолей);
strJSON = тЗаписьJSON.Закрыть();
Иначе
тЗаписьJSON = Новый ЗаписьJSON;
тПараметрыJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет, " ", Истина);
тЗаписьJSON.УстановитьСтроку(тПараметрыJSON);
ЗаписатьJSON(тЗаписьJSON, СтруктураПолей);
strJSON = strJSON + Символы.ПС + тЗаписьJSON.Закрыть();
КонецЕсли;
КонецЦикла;
КонецЕсли;
HTTPЗапрос.УстановитьТелоИзСтроки(strJSON);
ТелоЗапросаДляЛога = strJSON;
Попытка
Ответ = Соединение.ОтправитьДляОбработки(HTTPЗапрос);
ОтветСтрокой = Ответ.ПолучитьТелоКакСтроку();
ОтветСПорталаДляЛога = РаскодироватьJSON(ОтветСтрокой);
Исключение
КонецПопытки;
Попытка
лСтруктураОтвета = ПрочитатьJSONНаСервере(ОтветСтрокой, Истина);
Если ДобавлятьКодОтвета Тогда
лСтруктураОтвета.Вставить("КодСостояния", Ответ.КодСостояния);
КонецЕсли;
Исключение
Возврат Неопределено;
КонецПопытки;
Возврат лСтруктураОтвета;
КонецФункции
#КонецОбласти
Последним важным модулем является модуль по отправке данных в Sentry. Стоит отметить 3 важных заголовка, без которых соединение установлено не будет. В заголовке X-Sentry-Auth обязательно необходимо указать версию Sentry и ключ из DSN токена. В заголовке Authorization нужно указать также ключ из DSN токена. Важно, что при отправке ошибок в заголовке Content-Type указывается "application/json;charset=utf-8", а при отправке замеров времени "application/x-sentry-envelope". Структуры для отправки ошибок сразу записываются в json и отправляются, а структуры из массива при отправке замеров времени записываются поочередно в json с помощью конкатенации и потом данные отправляются.
Заключение
Было разработано расширение для отправки ошибок и других уровней журнала из журнала регистрации в 1С в Sentry. Также был реализован функционал для выгрузки данных из РС ЗамерыВремени с целью мониторинга пользовательского опыта в 1С. В статье описаны ключевые моменты при разработке представленного расширения, а самой информации достаточно для самостоятельной разработки такой же или похожей интеграции. Расширение можно считать полностью законченным и готовым к использованию. Представленное решение есть возможность расширить с точки зрения функционала и функционала, используемого с помощью Sentry.
Решение подходит для типовых конфигураций, т.к. журнал регистрации и РС ЗамерыВремени являются штатными механизмами. Тестировалось на платформе 8.3.25.1374, конфигурация Зарплата и управление персоналом, редакция 3.1 (3.1.30.108), база файловая. Стоит добавить, что обновления и поддержка расширения не планируются (на текущий момент), но приветствуются предложения и комментарии. Статья может быть дополнена.
Список источников
- Владислав Журавский.Cбор и анализ ошибок при помощи Sentry, или как упростить жизнь себе и пользователям: [Электронный ресурс]., 2020 // URL: //infostart.ru/1c/articles/1307327/. (Дата обращения: 28.12.2024).
- Введение в APDEX – с чего начинают оптимизацию профессионалы: [Электронный ресурс]., 2021 // URL: https://xn----1-bedvffifm4g.xn--p1ai/news/2021-12-03-apdex/. (Дата обращения: 28.12.2024).
- Андрей Крапивин. Часовой на страже логов: [Электронный ресурс]., 2020 // URL: //infostart.ru/pm/1178723/. (Дата обращения: 28.12.2024).
- Александр Маликов. Использование Sentry в контексте розничной сети: [Электронный ресурс]., 2022 // URL: //infostart.ru/1c/articles/1678579/. (Дата обращения: 28.12.2024).