Кто менял данные, если все ходят под Администратором
База данных - Журнал регистрации
В базе Бухгалтерии за три месяца переписались документы. У части сменились номера, у части — реквизиты. Вопрос стоял простой: кто это сделал. Имени я тут не назову, база чужая. Разберем, чем на такой вопрос вообще отвечают, когда все три привычных источника молчат.
Дальше было три стены подряд.
История версий объекта пуста. Открываю документ, смотрю версии — ничего, как будто его никто не трогал.
Список в журнале обрывается. Дошел до десяти тысяч записей и кончился, а самое раннее изменение было явно раньше.
В журнале во всех строках одно имя: «Администратор». Личная учетная запись есть у каждого разработчика, но обработки запускают под общей, так удобнее. Кто именно сидел под ней в нужный день, журнал не говорит.
Ни одна из трех стен не оказалась тем, чем выглядела. Все цифры ниже сняты 29.09.2026 на живой базе Бухгалтерии 3.0.206, платформа 8.5.1.1343. Период берем с запасом, с 1 января по 29 сентября, если не сказано иное: сузить его всегда успеем, а пропущенное начало искать заново.
Пустая история версий — сама по себе улика
Про пустую историю версий первым делом думают, что версионирование выключено. Иногда так и есть: функциональную опцию «Использовать версионирование объектов» могли снять.
Но есть вариант интереснее, и он сразу сужает круг. Посмотрите в конфигураторе подписку на событие записи, общий модуль ВерсионированиеОбъектовСобытия. Обработчик начинается так:
// Безусловная проверка на ОбменДанными.Загрузка не требуется, т.к. в момент записи // версионируемого объекта при обмене необходимо сохранить его текущую версию в истории версий. Если Источник.ОбменДанными.Загрузка И Источник.ОбменДанными.Отправитель = Неопределено Тогда Возврат; КонецЕсли;
Читается это так: если у объекта выставлен флаг загрузки, а отправитель не задан, версия не пишется вообще. Судя по коду, отправитель заполнен тогда, когда объект приехал из узла обмена; когда флаг выставили руками, чтобы объект записался в обход проверок заполнения, отправителя нет, и подписка выходит на первой же строке.
Если отправитель задан, дело доходит до модуля ВерсионированиеОбъектов, и там стоит второй выход, до вызова, который создает версию:
Если Источник.ОбменДанными.Загрузка Тогда Если Не Источник.Ссылка.Пустая() Тогда ЗаписатьДанныеТекущейВерсии(Источник.Ссылка, Истина); КонецЕсли; Возврат; КонецЕсли;
Имя ЗаписатьДанныеТекущейВерсии обманчиво: новой строки в регистр она не добавляет. Внутри берется НомерПоследнейВерсии без увеличения, набор по этому номеру читается, и содержимое дописывается только туда, где оно пустое. То есть новой записи в истории версий не появляется ни на одном из двух путей.
Если версионирование включено, а у документа, который явно менялся, история версий пуста, его писали с ОбменДанными.Загрузка = Истина: либо обменом, либо кодом, который этот флаг выставил. Сначала все-таки проверьте функциональную опцию: это одна строка, и она снимает половину догадок.
А вот журнал регистрации этот флаг не глушит. Я записал элемент справочника с выставленным флагом и сразу прочитал журнал:
НовыйЭлемент = Справочники.Контрагенты.СоздатьЭлемент(); НовыйЭлемент.Наименование = "Проба журнала"; НовыйЭлемент.ОбменДанными.Загрузка = Истина; НовыйЭлемент.Записать();
Запись в журнале появилась: «Данные. Добавление · Справочник.Контрагенты · Проба журнала». Опыт поставлен на элементе справочника, не на документе, но флаг и подписка тут общие. То есть история версий молчит, а журнал помнит. Значит, копать надо там.
Ограничение в 10 000 записей — не платформенное
Список обрывался не из-за платформы. Откройте в конфигураторе обработку ЖурналРегистрации из Библиотеки стандартных подсистем, модуль формы. Там есть переменная КоличествоПоказываемыхСобытий, в поставке Бухгалтерии 3.0.206 по умолчанию 200, а десять тысяч — это максимум в списке выбора, причем для веб-клиента его еще и режут до тысячи. Ограничение живет в форме, а не в движке.
Платформенный метод ВыгрузитьЖурналРегистрации количество записей вообще не требует. Параметр есть, но он необязательный, и если его не передавать, читается весь период целиком.
Проверка на той же базе, один и тот же отбор, разница только в последнем параметре:
| Вызов | Строк вернулось |
|---|---|
ВыгрузитьЖурналРегистрации(Таблица, Отбор) |
116 595 |
ВыгрузитьЖурналРегистрации(Таблица, Отбор, , , 10000) |
ровно 10 000 |
Чтение всего периода заняло 25,3 секунды на журнале в 349 166 записей. Если журнал у вас на порядок больше, режьте период: время растет вместе с числом прочитанных строк.
Номер сеанса без времени старта ничего не значит
Третья стена — общая учетная запись. Тут журнал дает две зацепки: колонку Компьютер (она заполняется именем машины) и колонку Сеанс. Казалось бы, группируй записи по паре «номер сеанса и компьютер», и разные люди разойдутся по разным строкам.
Так мы и сделали. И получили ерунду: в строке «сеанс 1» оказалось больше тысячи изменений, а в колонке «Приложение» стоял толстый клиент, хотя работа шла в тонком.
Причина в том, что номера сеансов повторяются. Замер ниже показывает это прямо; из него же видно, что номер держится за сеансом, пока тот жив, а после закрытия уходит следующему. За всю историю базы номер ничего не опознает.
Масштаб видно по событиям старта сеанса за одну неделю:
| Значение | |
|---|---|
| стартов сеансов всего | 2 503 |
| различных пар «номер и компьютер» | 317 |
| сколько раз стартовала пара «номер 1 и эта машина» | 582 |
Пятьсот восемьдесят два разных сеанса под одним номером за семь дней, с 22.09.2026 по 29.09.2026. Группировка по номеру складывала их в одну строку, перемешивая работу разных людей и фоновых заданий.
Прикиньте заодно свою: сколько раз за неделю перезапускается клиент, умножьте на число разработчиков. Много выйдет?
Лечится это просто, если знать. У платформы есть событие _$Session$_.Start, которое пишется в журнал при каждом старте сеанса. Читаем его вместе с изменениями данных, сортируем выборку по дате и при проходе запоминаем для каждой пары «номер и компьютер» момент последнего старта. Этот момент становится третьей частью ключа.
Эффект видно на выборке изменений данных за ту же неделю: группировка по паре дает 114 строк, группировка по тройке — 616. Пятьсот два реальных сеанса до этого прятались в чужих строках, и как раз в одной из них терялся документ, который мы искали.
Колонка «Компьютер»
Вот теперь работает и вторая зацепка. Под общей учетной записью именно имя машины отличает одного разработчика от другого. У фоновых заданий эта колонка на проверочной базе за весь период не была заполнена ни разу.
Чем именно правили: путь к обработке в журнале есть
Дальше казалось, что мы уперлись окончательно. Понятно, с какой машины и в какие минуты шла правка. А чем именно правили, журнал знать не может: обработка внешняя, а какая — нигде не записано.
Оказалось, может: искали не в том событии ))
Событие _$Session$_.Start про запущенную обработку не знает ничего: поля Данные и ПредставлениеДанных у него пустые. А есть отдельное событие _$Session$_.ExternalDataProcessorConnect, по-русски «Сеанс. Подключение внешней обработки». И вот у него в поле Данные лежит структура из шести ключей:
Путь = D:\Обработки\ПеренумерацияДокументов.epf Имя = БезопасныйРежим = Нет ПрофильБезопасности = ЗащитаОтОпасныхДействий = Да КодЯзыка =
Полный путь к файлу, вместе с каталогом. Каталог обычно и выдает, чей это файл и откуда его принесли. Видно это и в типовой форме журнала, если задать отбор по событию, — снимок выше снят именно там.
Достается путь так:
Если ТипЗнч(Запись.Данные) = Тип("Структура")
И Запись.Данные.Свойство("Путь") Тогда
Путь = СокрЛП(Строка(Запись.Данные.Путь));
КонецЕсли;
Что тип именно Структура, а не фиксированная, проверено отдельно 29.09.2026 двумя путями: на платформе и через внешнее соединение. Имя ключа можно не угадывать: в Библиотеке стандартных подсистем, общий модуль ЖурналРегистрации, прямо под комментарием с именем этого события перечислены «Путь» и «Код языка».
Дальше остается сложить одно с другим: взять записи о подключении обработок и записи об изменении данных, разложить по сеансам одним и тем же ключом и выстроить по времени. Получается лента вида «в 14:07 сеанс подключил такой-то файл, с 14:07 по 14:12 переписал такие-то документы».
Оговорка, куда же без нее )) Журнал хранит путь к файлу, но не то, что обработка делала. Связь «запустил обработку — переписал документы» выводится из совпадения сеанса и времени, сам журнал ее не подтверждает. Если в одном сеансе запустили две обработки и что-то поправили руками, все три факта встанут рядом, и какой из них чей, придется решать самому.
Две ловушки в отборе, на которых легко сесть
Первая: неизвестное имя события платформа не отвергает. Я был уверен в обратном и даже построил на этом код: перебирал имена событий по одному внутри Попытка, рассчитывая, что неподдерживаемое имя даст исключение. Не дает.
Опыт поставлен на отдельной проверочной базе с коротким журналом, поэтому числа тут на три порядка меньше остальных в статье и с ними не сравниваются. Важно не их значение, а то, что исключения нет ни в одной строке.
| Что передано в отбор «Событие» | Строк вернулось |
|---|---|
_$Data$_.Update |
5 |
_$Data$_.ТакогоИмениНет |
0 |
мусор |
0 |
| пустая строка | 0 |
Ни одно из проверенных имен исключения не дало: на незнакомое имя просто приходит пустая выборка. Это значит, что опечатка в имени события не падает, а тихо превращается в «за период ничего не было». Имена лучше не набирать по памяти: пять имен событий изменения данных лежат литералами в типовой Бухгалтерии, общий модуль ОбработкаНовостейКлиент.
Вторая: без отбора по событиям журнал отдает всё подряд. Кажется, что можно прочитать целиком и разобраться потом. За тот же период приезжает столько:
| Отбор | Строк |
|---|---|
| только изменения данных | 116 595 |
| без отбора по событиям | 349 166 |
Из этих 349 166 почти половина — начала и фиксации транзакций, их 168 507. Еще 24 852 — смены области данных, 6 086 — входы в систему. Изменения данных составляют треть, 33,4 процента. Если не отфильтровать, в отчет о том, кто правил документы, попадут чужие входы в систему и служебные метки, и выглядеть они будут убедительно.
Как отличить документ от служебной записи
Еще одна мелочь, без которой разбор тонет. Изменений данных в журнале много, но подавляющее большинство из них — записи регистров: замеры статистики, серверные оповещения, данные монитора показателей. Документов среди них единицы, а ищут обычно именно документы.
Отличаются они по двум полям сразу. В колонке Метаданные лежит полное имя объекта, и у документа оно начинается с Документ. — например, Документ.СчетФактураВыданный. А колонка ПредставлениеДанных у документа заполнена и содержит его читаемое представление вида «Реализация (акт, накладная, УПД) 0000-000602 от 29.12.2016». У записи регистра она либо пустая, либо содержит одно голое число вроде «155».
ЭтоДокумент = СтрНачинаетсяС(Строка(Запись.Метаданные), "Документ.") И Не ПустаяСтрока(Строка(Запись.ПредставлениеДанных));
Обе проверки нужны именно вместе. Одного «представление непустое» мало: у регистра там может стоять то самое «155», и он пролезет в отбор. Одного Документ. в метаданных тоже мало: так в выборку попадут записи без читаемого представления, и в колонке будет пусто.
Из этого получается отдельная колонка «какие документы записаны в этом сеансе». Вместо тысячи строк про регистры в ней видно две строки про документы, которые и надо было найти.
Попутно: описание объекта лучше склеивать из представления метаданных и представления данных. Из одного представления данных у регистров выйдет голое «155», по которому ничего не понять.
Что собралось: обработка и два урока
Чтобы не собирать эту логику заново при каждом разборе, мы свели ее в небольшую обработку. Она читает журнал за период без ограничения по количеству, режет по стартам сеансов, показывает по каждому сеансу компьютер, время, число изменений, записанные документы и путь к запущенной внешней обработке. Двойной клик по строке разворачивает ленту по минутам.
В базу она ничего не пишет, и это проверено, а не обещано: в коде нет ни одного вызова записи; журнал за окно ее работы содержит только служебные регистры БСП, которые пишутся при входе в программу; и она отработала штатно под активной общей датой запрета изменения, а сама дата запрета в том же сеансе отклоняла запись документа.
Но обработка тут дело десятое. Больше всего времени забрали две вещи, про которые достаточно было знать заранее: что лимит в десять тысяч снимается одним неуказанным параметром и что номер сеанса без времени старта не опознает ничего. В журнале лежит больше, чем типовая форма показывает по умолчанию. Достать это из нее можно: поставьте отбор по событию «Сеанс. Подключение внешней обработки», и путь будет виден прямо в колонке «Сведения о событии». Чего форма не делает — не сводит запуски обработок с изменениями данных по сеансам и не разбирается с повторяющимися номерами. Ради этого и пришлось обходить ее стороной.
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.