├── Файлы конфигурации ├── MethodsWithParameter.sh └── Vulnerabilities.sh ├── README.md ├── open-data-processor.ps1 ├── Технологический журнал ├── LogFilesWithoutBOM.sh ├── ExceptionsNumberByMinutes.sh ├── FrequentTimeoutContexts.sh ├── LongestTransactions.sh ├── CallsTopByMemoryPeak.sh ├── LongestLockWaitsByRegions.sh ├── LongestLockWaitsByContext.sh ├── FrequentEvents.sh ├── ExceptionDescriptions.sh ├── FrequentExceptions.sh ├── LocksByConnectIDAndRegions.sh ├── FrequentExceptions.py ├── LongestQueries.sh └── LongestQueries.py ├── Взаимодействие с публикацией ├── http-service-get.ps1 └── http-service-post.ps1 ├── LICENSE └── Обслуживание базы данных └── CheckDBErrors.sh /Файлы конфигурации/MethodsWithParameter.sh: -------------------------------------------------------------------------------- 1 | # Скрипт ищет в файлах конфигурации методы (процедуры или функции), у которых есть параметр 2 | # с определенным именем (ниже — параметр DecimalPlacesFor). 3 | 4 | # Собственно поиск. Ищем и процедуры, и функции. 5 | # 6 | grep --with-filename '^[Procedure|Function].*(.*DecimalPlacesFor.*Export' */Ext/Module.bsl | 7 | 8 | # Отрезаем все лишнее, чтобы на вывод ушли только названия модулей и найденных в них методов. 9 | # 10 | perl -pe 's/\/Ext\/Module.bsl:(Procedure|Function) /\./g;s/\(.*Export.*//g' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Скрипты для 1С 2 | 3 | [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) 4 | 5 | Скрипты сгруппированы по области применения. Например, в директории «Технологический журнал» — скрипты для анализа собранного ТЖ, а в директории «Файлы конфигурации» — скрипты, работающие с выгрузкой конфигурации в файлы. 6 | 7 | Внутри скриптов много комментариев (иногда их больше, чем кода). Это не из любви к графомании — просто так намного легче понять цель и логику скрипта, вернувшись к нему спустя продолжительное время. -------------------------------------------------------------------------------- /open-data-processor.ps1: -------------------------------------------------------------------------------- 1 | # Открывает тонкий клиент 1С с указанным параметром и запускает определенную обработку. 2 | 3 | $appPath = "C:\Program Files\1cv8\8.3.17.1989\bin\1cv8c.exe" 4 | $epfPath = "D:\UsefulDataProcessor.epf" 5 | $outPath = "D:\UsefulDataProcessor Output.txt" 6 | $lParams = "Option1;Option2" 7 | 8 | $ArgumentsList = 9 | "ENTERPRISE", 10 | "/S `"localhost\21117-deleteme`"", 11 | "/N Administrator", 12 | "/P Password", 13 | "/Execute `"$epfPath`"", 14 | "/C `"$lParams`"", 15 | "/DisableStartupDialogs", 16 | "/out `"$outPath`" -NoTruncate" 17 | 18 | Start-Process $appPath -ArgumentList $ArgumentsList -NoNewWindow -PassThru -Wait | Out-Null -------------------------------------------------------------------------------- /Технологический журнал/LogFilesWithoutBOM.sh: -------------------------------------------------------------------------------- 1 | # Определяет наличие UTF-8 BOM в файлах ТЖ. 2 | # 3 | # Выводит сообщение, из в файле лога отсутствует маркер последовательности байтов. Практического применения нет — 4 | # это просто способ убедиться, что BOM действительно присутствует в каждом файле лога ТЖ. 5 | # 6 | # Удалить BOM из потока данных можно, например, вставив такую инструкцию в конвейер: 7 | # perl -pe 's/\xef\xbb\xbf//g' | 8 | # 9 | # Альтернативный вариант — заранее удалить BOM из файлов лога, с которыми вы сейчас работаете: 10 | # find . -name "*.log" -print0 | xargs -0 sed 's/^\xef\xbb\xbf//' -i 11 | # 12 | 13 | find -name "*.log" | while read file; do [ "`head -c3 -- "$file"`" != $'\xef\xbb\xbf' ] && echo "No BOM here: $file"; done -------------------------------------------------------------------------------- /Взаимодействие с публикацией/http-service-get.ps1: -------------------------------------------------------------------------------- 1 | # Скрипт делает GET-запрос к HTTP-сервису InternalAPI, 2 | # вызывая метод User с параметром UserLogin. 3 | 4 | $URL = "http://localhost/DevFBERP" # Адрес публикации 5 | $user = 'Administrator' # Логин пользователя 6 | $pass = '' # Пароль пользователя 7 | 8 | $apiUrl = $url + "/hs/InternalAPI" 9 | $pair = "$($user):$($pass)" 10 | $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) 11 | $basicAuthValue = "Basic $encodedCreds" 12 | 13 | $answer = Invoke-WebRequest -uri "$apiUrl/User?UserLogin=Administrator" ` 14 | -Method Get ` 15 | -headers @{'Authorization' = $basicAuthValue; 'Content-Type'= 'application/json'} 16 | 17 | $result = ConvertFrom-Json -InputObject $answer 18 | 19 | $result -------------------------------------------------------------------------------- /Взаимодействие с публикацией/http-service-post.ps1: -------------------------------------------------------------------------------- 1 | # Скрипт делает POST-запрос к HTTP-сервису InternalAPI, 2 | # вызывая метод UpdateUser с параметрами UserLogin и UserPassword. 3 | 4 | $URL = "http://localhost/DevFBERP" # Адрес публикации 5 | $user = 'Administrator' # Логин пользователя 6 | $pass = '' # Пароль пользователя 7 | 8 | $apiUrl = $url + "/hs/InternalAPI" 9 | $pair = "$($user):$($pass)" 10 | $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) 11 | $basicAuthValue = "Basic $encodedCreds" 12 | 13 | $answer = Invoke-WebRequest -uri "$apiUrl/UpdateUser" ` 14 | -Method Post ` 15 | -headers @{'Authorization' = $basicAuthValue; 'Content-Type'= 'application/json'} ` 16 | -body "{'UserLogin':'Cashier', 'UserPassword':'1234'}" 17 | 18 | $result = ConvertFrom-Json -InputObject $answer 19 | 20 | $result -------------------------------------------------------------------------------- /Технологический журнал/ExceptionsNumberByMinutes.sh: -------------------------------------------------------------------------------- 1 | # Топ минут с максимальным количеством ошибок на базе события EXCP. 2 | 3 | # Получаем имя файла с событием EXCP и его строку. 4 | # 5 | grep -oP ".*,EXCP," */*.log | 6 | 7 | # Удаляем из потока данных UTF-8 BOM. 8 | # 9 | perl -pe 's/\xef\xbb\xbf//g' | 10 | 11 | # Из имени файла берем час, из строки — минуту. Остальное выкидываем. 12 | # Другой вариант: sed -r 's/.*([0-9][0-9]).log:([0-9][0-9]).*/\1:\2/' 13 | # 14 | # На этом этапе удобно менять регулярное выражение под конкретную задачу. 15 | # Например, можно не читать минуты (тогда расчет будет по часам) 16 | # или читать месяц и день (если анализируется длительный период). 17 | # 18 | grep -oP "\d\d.log:\d\d" | 19 | sed 's/.log//' | 20 | 21 | # Сортируем результат (чтобы утитила uniq правильно сгруппировала), группируем, 22 | # потом снова сортируем — уже по количеству найденных совпадений. 23 | # 24 | sort | 25 | uniq -c | 26 | sort -rn > ExceptionsNumberByMinutes.txt -------------------------------------------------------------------------------- /Технологический журнал/FrequentTimeoutContexts.sh: -------------------------------------------------------------------------------- 1 | cat rphost_*/*.log | 2 | 3 | # Удаляем из потока данных UTF-8 BOM. 4 | # 5 | perl -pe 's/\xef\xbb\xbf//g' | 6 | 7 | # Утилита gawk разделяет лог по метке времени события и выполняет переданный ей скрипт. 8 | # 9 | gawk -F'Context=' -vRS='[0-9]+:[0-9]+.[0-9]+-[0-9]+,' ' 10 | { 11 | # Отбираем только события TTIMEOUT. 12 | # 13 | if ( $0 ~ "^TTIMEOUT.*" ) { 14 | 15 | Context = $2; 16 | 17 | gsub("\n", "", Context); 18 | 19 | Timeouts[Context] += 1; 20 | 21 | } 22 | }; 23 | 24 | END { 25 | 26 | for (Context in Timeouts) { 27 | print Timeouts[Context] " timeouts " Context; 28 | } 29 | 30 | } 31 | ' | 32 | 33 | sort -rn | 34 | head -n 1000 | 35 | 36 | # Мы заменяли переводы строк на подстроку , чтобы результат можно было отсортировать (sort) и усечь (head). 37 | # Теперь можно сделать обратную замену — тогда результат будет удобнее читать. 38 | # 39 | sed -r "s//\n/g" > FrequentTimeoutContexts.txt -------------------------------------------------------------------------------- /Технологический журнал/LongestTransactions.sh: -------------------------------------------------------------------------------- 1 | # Топ длительных транзакций на базе события SDBL. 2 | # 3 | # Выводятся транзакции, которые шли дольше 20 секунд (это время ожидания возможности установления блокировки 4 | # по умолчанию). Для каждой выводится сначала продолжительность в секундах, потом — текст события. 5 | 6 | cat rphost_*/*.log | 7 | 8 | # Удаляем из потока данных UTF-8 BOM. 9 | # 10 | perl -pe 's/\xef\xbb\xbf//g' | 11 | 12 | # Заменяем первый дефис (отделяющий время начала события от его длительности) на запятую. Тогда, если задать запятую 13 | # как разделитель полей, можно будет выделить продолжительность события без дополнительных ухищрений: и до, и после 14 | # значения продолжительности будет запятая. 15 | # 16 | sed -r 's/-/,/' | 17 | 18 | gawk -F',' '{ 19 | 20 | # Проверяем, что событие — SDBL + в нем есть указатель на завершение или откат транзакции + оно шло от 20 секунд. 21 | # 22 | if ($0 ~ ".*,SDBL,.*Func=(Commit|Rollback)Transaction.*" && $2 >= 20000000) 23 | { 24 | printf "%.3f %s\n", $2 / 1000000, $0 25 | } 26 | 27 | }' | 28 | 29 | sort -rn | 30 | head -n 100 > LongestTransactions.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Обслуживание базы данных/CheckDBErrors.sh: -------------------------------------------------------------------------------- 1 | # Поиск ошибок в выводе команды DBCC CHECKDB(). 2 | # 3 | # Задачи: 4 | # 5 | # 1. Ищет все объекты с ошибками и помещает в файл DBCC CHECKDB Errors.txt; 6 | # 2. Имена таблиц с ошибками помещает в файл DBCC CHECKDB Tables.txt. 7 | # 8 | # Удобен для быстрого анализа поврежденной базы без скроллинга лога: можно быстро оценить, 9 | # какие объекты были повреждены и каков характер повреждений. Список таблиц можно вставить 10 | # в обработку поиска метаданных и сопоставить его в объектами метаданных конфигурации. 11 | # 12 | 13 | cat "DBCC CHECKDB.txt" | 14 | 15 | gawk -F'\n' -vRS='DBCC results for ' '{ 16 | 17 | # Если следующая строка начинается с «There are», то ошибок для объекта нет. 18 | # 19 | if ($2 !~ "^There are .*") 20 | { 21 | IsTable = match($1, /^._.*\./) != 0 22 | 23 | if (IsTable) 24 | { 25 | # Отрезаем от имени таблицы кавычки и точку в конце. 26 | # 27 | Table = substr($1, 2, length($1) - 3) 28 | 29 | Tables[Table] = Table 30 | } 31 | 32 | print $0 "\n" > "DBCC CHECKDB Errors.txt" 33 | } 34 | }; 35 | 36 | END { 37 | 38 | asort(Tables) 39 | 40 | for (Table in Tables) 41 | { 42 | print Tables[Table] > "DBCC CHECKDB Tables.txt" 43 | } 44 | 45 | }' -------------------------------------------------------------------------------- /Файлы конфигурации/Vulnerabilities.sh: -------------------------------------------------------------------------------- 1 | # Скрипт ищет в файлах конфигурации некоторые потенциальные уязвимости — айпишники, ссылки, 2 | # e-mail'ы и роли с правом на интерактивное удаление чего-либо. 3 | 4 | # Поиск адресов электронной почты в текстах программных модулей. 5 | # 6 | # Логика: ищем все BSL-файлы, а в них — e-mail по шаблону. 7 | # Минусы: находятся e-mail'ы разработчиков. 8 | # 9 | find . -name "*.bsl" -print0 | xargs -0 egrep "[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+" > InlineEmails.txt 10 | 11 | # Поиск URL-адресов в текстах программных модулей. 12 | # 13 | # Логика: ищем все BSL-файлы, а в них — URL по шаблону. 14 | # Минусы: под регулярку попадают пространства XDTO-пакетов. 15 | # 16 | find . -name "*.bsl" -print0 | xargs -0 egrep "(ftp|http|https):\/\/[^ \"]+" > InlineURLs.txt 17 | 18 | # Поиск IP-адресов в текстах программных модулей. 19 | # 20 | # Логика: ищем все BSL-файлы, а в них — IP по шаблону. 21 | # Минусы: под регулярку попадают версии конфигурации. 22 | # 23 | find . -name "*.bsl" -print0 | xargs -0 egrep "([0-9]{1,3}[\.]){3}[0-9]{1,3}" > InlineIPs.txt 24 | 25 | # Поиск ролей с правом интерактивного удаления. 26 | # 27 | # Логика: ищет в XML-файлах ролей значение «true» на следующей строке после строки права интерактивного удаления. 28 | # 29 | grep -A1 "InteractiveDelete" Roles/*/Ext/Rights.xml | grep "true" > InteractiveDelete.txt -------------------------------------------------------------------------------- /Технологический журнал/CallsTopByMemoryPeak.sh: -------------------------------------------------------------------------------- 1 | # Топ вызовов с максимальным расходом памяти в пике. Строится по событиям CALL (собираемые свойства — Context и MemoryPeak). 2 | 3 | cat rphost_*/*.log | 4 | 5 | # Удаляем из потока данных UTF-8 BOM. 6 | # 7 | sed -r "s/\xef\xbb\xbf//g" | 8 | 9 | # Делим события по фрагменту с временем и продолжительностью. Так в одну запись попадет даже многострочный контекст. 10 | # 11 | gawk -vRS="[0-9]+:[0-9]+.[0-9]+-[0-9]+," '{ 12 | 13 | # Нас интересуют только вызовы с контекстом. 14 | # 15 | if ($0 ~ ".*Context=.*") { 16 | 17 | Line = $0 18 | 19 | gsub("\n", "", Line) # Переводы строк помешают сортировке. 20 | gsub("^.*,Context=", "", Line) # Удалим ненужные данные. 21 | 22 | # Если разделить получившуюся строку по такой подстроке, то получится массив 23 | # из двух элементов: контекстом и значением пика по памяти. 24 | # 25 | split(Line, Data, ",MemoryPeak=") 26 | 27 | Context = Data[1] 28 | MemoryPeak = Data[2] 29 | 30 | Contexts[Context] += MemoryPeak 31 | 32 | } 33 | 34 | }; END { 35 | 36 | for (Context in Contexts) { 37 | print Contexts[Context] " " Context 38 | } 39 | 40 | }' | 41 | 42 | sort -rn | 43 | head -n 10 | 44 | 45 | sed -r "s//\n/g" > CallsTopByMemoryPeak.txt -------------------------------------------------------------------------------- /Технологический журнал/LongestLockWaitsByRegions.sh: -------------------------------------------------------------------------------- 1 | # Топ ожиданий на блокировках на базе событий TLOCK. 2 | # 3 | # Блокировки группируются по области; для каждой выводится общее время ожидания, среднее время ожидания 4 | # и количество установок. Сортировка — по общему времени ожидания. 5 | 6 | # Управляемые блокировки возникают только на rphost'ах. 7 | # 8 | cat rphost_*/*.log | 9 | 10 | # Удаляем из потока данных UTF-8 BOM. 11 | # 12 | perl -pe 's/\xef\xbb\xbf//g' | 13 | 14 | # Фильтруем TLOCK'и, у которых заполнено свойство WaitConnections (т.е. платформа реально ждала возможности установить 15 | # управляемую блокировку, а не просто потратила какое-то время на её создание). 16 | # 17 | grep -E ',TLOCK,.*WaitConnections=[0-9]+' | 18 | 19 | # Вырезаем из информации о событии все данные, кроме его продолжительности и областей. 20 | # 21 | sed -r 's/.*-([0-9]+),TLOCK,.*Regions=(.*),Locks=.*/\1 \2/' | 22 | 23 | # Если областей несколько — они будут заключены в одинарные кавычки. Удалим их. 24 | # 25 | sed -r "s/'(.*)'/\1/" | 26 | 27 | gawk '{ 28 | 29 | DurationByRegions[$2] += $1; 30 | QuantityByRegions[$2] += 1; 31 | 32 | }; END { 33 | 34 | for (Regions in DurationByRegions) { 35 | 36 | TotalQuantity = QuantityByRegions[Regions]; 37 | TotalDuration = DurationByRegions[Regions] / 1000000; 38 | AverageDuration = DurationByRegions[Regions] / QuantityByRegions[Regions] / 1000000; 39 | 40 | printf "%.3f seconds total, %.3f seconds on average, %d locks: %s\n", TotalDuration, AverageDuration, TotalQuantity, Regions; 41 | 42 | } 43 | 44 | }' | 45 | 46 | sort -rn | 47 | head -n 100 > LongestLockWaitsByRegions.txt -------------------------------------------------------------------------------- /Технологический журнал/LongestLockWaitsByContext.sh: -------------------------------------------------------------------------------- 1 | # Топ ожиданий на блокировках на базе событий TLOCK. 2 | # 3 | # Блокировки группируются по контексту; для каждой выводится общее время ожидания, среднее время ожидания 4 | # и количество установок. Сортировка — по общему времени ожидания. 5 | 6 | # Управляемые блокировки возникают только на rphost'ах. 7 | # 8 | cat rphost_*/*.log | 9 | 10 | # Удаляем из потока данных UTF-8 BOM. 11 | # 12 | perl -pe 's/\xef\xbb\xbf//g' | 13 | 14 | gawk -F',Context=' -vRS='[0-9]+:[0-9]+.[0-9]+-' '{ 15 | 16 | # Фильтруем TLOCK, у которых заполнено свойство WaitConnections (т.е. платформа реально ждала 17 | # возможности установить управляемую блокировку, а не просто потратила время на её создание). 18 | # 19 | if ( $0 ~ ".*,TLOCK,.*WaitConnections=[0-9]+.*" ) { 20 | 21 | # Мы разделили события по времени начала, так что продолжительность — это строка до запятой. 22 | # 23 | Duration = substr($0, 0, index($0, ",") - 1); 24 | 25 | # Заменим переводы строк на макрос, чтобы впоследствии утилиты sort и head отработали верно. 26 | # 27 | gsub("\n", "", $2); 28 | 29 | DurationByContext[$2] += Duration; 30 | LocksByContexts[$2] += 1; 31 | 32 | } 33 | 34 | }; END { 35 | 36 | for (Context in DurationByContext) { 37 | 38 | TotalLocks = LocksByContexts[Context]; 39 | TotalDuration = DurationByContext[Context] / 1000000; 40 | AverageDuration = DurationByContext[Context] / LocksByContexts[Context] / 1000000; 41 | 42 | printf "%.3f seconds total, %.3f seconds on average, %d locks:%s\n", TotalDuration, AverageDuration, TotalLocks, Context; 43 | } 44 | 45 | }' | 46 | 47 | sort -rn | 48 | head -n 100 | 49 | 50 | sed -r "s//\n/g" > LongestLockWaitsByContext.txt -------------------------------------------------------------------------------- /Технологический журнал/FrequentEvents.sh: -------------------------------------------------------------------------------- 1 | # Топ событий ТЖ. 2 | # 3 | # Выводит события технологического журнала (от наиболее частотного к наименее частотному). Для каждого 4 | # события выводится количество воспроизведений. 5 | # 6 | # Большого практического смысла скрипт не имеет, но может использоваться для получения общей картины 7 | # или как болванка для других, более специфических задач. 8 | # 9 | 10 | cat */*.log | 11 | 12 | # Удаляем из потока данных UTF-8 BOM. 13 | # 14 | perl -pe 's/\xef\xbb\xbf//g' | 15 | 16 | # Теперь нам нужно найти все имена событий и определить, сколько раз каждое событие происходит. Используем gawk. 17 | # 18 | # Установим настройки. Во-первых, запись каждого события в файле ТЖ начинается с его времени и продолжительности 19 | # (например, 14:55.636001-1). Сделаем такую подстроку разделитем записей (ключ -vRS). Теперь каждая запись будет 20 | # содержать текст события полностью (информация, переводы строк, другие спецсимвы — в общем, всё, кроме времени 21 | # события и его продолжительности). 22 | # 23 | # Во-вторых, имя события — если оно есть — помещается в строке события первым (так как все до него мы сделали разделителем 24 | # строк). Если сделать запятую разделителем полей (ключ -F), то первое же поле будет указывать на имя события. 25 | # 26 | # Скрипт работает в два этапа. На первом мы коллекционируем уникальные имена событий и считаем, сколько раз каждое событие 27 | # было зафиксировано. На втором — для каждого найденного уникального события выводим количество совпадений и имя события. 28 | # 29 | gawk -F',' -vRS='[0-9]+:[0-9]+.[0-9]+-[0-9]+,' ' 30 | { 31 | if (length($1) > 0) 32 | EventCounters[$1] += 1; 33 | }; 34 | 35 | END { 36 | for (EventName in EventCounters) 37 | print EventCounters[EventName] " " EventName 38 | }' | 39 | 40 | # Сортируем события по частотности и выводим результат в файл. 41 | # 42 | sort -rn > FrequentEvents.txt -------------------------------------------------------------------------------- /Технологический журнал/ExceptionDescriptions.sh: -------------------------------------------------------------------------------- 1 | # Список описаний исключений с группировкой по наименованию исключения. 2 | # 3 | 4 | cat rphost_*/*.log | 5 | 6 | # Удаляем из потока данных UTF-8 BOM. 7 | # 8 | perl -pe 's/\xef\xbb\xbf//g' | 9 | 10 | # Теперь нам нужно найти события исключений (EXCP), прочитать из них описание (Descr) и определить, сколько раз 11 | # среди исключений попадается каждое описание. С этой задачей хорошо справляется gawk. 12 | # 13 | # Установим настройки. Каждое событие начинается с времени и продолжительности (например, 14:55.636001-1). 14 | # Сделаем такую подстроку разделитем записей. Теперь каждая запись будет содержать событие полностью (со всеми 15 | # переводами строк и другими спецсимволами). 16 | # 17 | # Разделителем полей поставим запятую, но по факту это не пригодится: нам нужно анализировать два поля события, 18 | # Exception и Descr. Последний всегда идет в конце события, а вот положение Exception может разниться в зависимости 19 | # от конкретного исключения. 20 | # 21 | # Скрипт работает в два этапа. На первом мы формируем массив двойной вложенности, ключи которого — значения Exception. 22 | # Значения массива — массивы с ключом, равным описанию, и значением, равным количеством нахождения этого описания. 23 | # 24 | gawk -F',' -vRS='[0-9]+:[0-9]+.[0-9]+-[0-9]+,' ' 25 | 26 | function GetException(Input) { 27 | 28 | gsub("^.*Exception=", "", Input); 29 | gsub(",Descr=.*$", "", Input); 30 | 31 | return Input; 32 | 33 | } 34 | 35 | function GetDescription(Input) { 36 | 37 | gsub("^.*Descr=", "", Input); 38 | 39 | return Input; 40 | 41 | } 42 | 43 | { 44 | if ( $0 ~ "^EXCP,.*" ) { 45 | 46 | Exception = GetException($0); 47 | Description = GetDescription($0); 48 | 49 | ExceptionDescriptions[Exception][Description] += 1; 50 | 51 | } 52 | }; 53 | 54 | END { 55 | for (Exception in ExceptionDescriptions) { 56 | 57 | print "---"; 58 | print "EXCEPTION: " Exception; 59 | print "---\n"; 60 | 61 | for (Description in ExceptionDescriptions[Exception]) { 62 | 63 | EventsNumber = ExceptionDescriptions[Exception][Description] 64 | 65 | print "Description (" EventsNumber " events)\n"; 66 | print Description; 67 | 68 | } 69 | 70 | } 71 | 72 | }' > ExceptionDescriptions.txt -------------------------------------------------------------------------------- /Технологический журнал/FrequentExceptions.sh: -------------------------------------------------------------------------------- 1 | # Топ частотных ошибок на базе события EXCP. 2 | # 3 | # Для каждого исключения выводится количество срабатываний и описание. 4 | # 5 | # Более простые варианты решения той же задачи: 6 | # 7 | # grep -ho ",EXCP,.*Descr=.*" */*.log | perl -pe "s/,EXCP,.,.*Descr=/Descr=/g" | uniq -c | sort -rn 8 | # grep -hoP ",EXCP,.*\KDescr=.*" */*.log | uniq -c | sort -rn 9 | # 10 | # Проблема с ними том, что описание у EXCP может быть многострочным — то есть, варианты выше 11 | # будут терять часть данных, нужных для расследования проблемы. 12 | 13 | cat */*.log | 14 | 15 | # Удаляем из потока данных UTF-8 BOM. 16 | # 17 | perl -pe 's/\xef\xbb\xbf//g' | 18 | 19 | # На этом этапе удобно удалить информацию, которая не нужна для анализа — тогда некоторые ошибки могут быть сгруппированы. 20 | # Например, замена ниже превратит строки «server_addr=(23)[::1]:49427» в «server_addr=(00)[::0]:00000»: 21 | # 22 | # sed -r "s/server_addr=\(.*:[0-9]+/server_addr=(00)[::0]:00000/g" | 23 | 24 | # Теперь нам нужно найти события исключений (EXCP), прочитать из них описание (Descr) и определить, сколько раз 25 | # среди исключений попадается каждое описание. С этой задачей хорошо справляется gawk. 26 | # 27 | # Установим настройки. Во-первых, каждое событие начинается с времени (например, 14:55.636001-). Сделаем 28 | # такую подстроку разделитем записей. Теперь каждая запись будет содержать событие полностью (со всеми 29 | # переводами строк и другими спецсимволами). 30 | # 31 | # Во-вторых, описание ошибки — если оно есть — помещается в строке события последним, после подстроки «Descr=». 32 | # Сделаем её разделителем полей. Теперь каждая запись будет иметь два поля: первое — cтрока события до описания, 33 | # второе — собственно, описание. 34 | # 35 | # Скрипт работает в два этапа. На первом мы коллекционируем уникальные значения описания для события исключения 36 | # и считаем, сколько раз каждое описание было найдено. На втором — для каждого найденного уникального описания 37 | # выводим количество найденных совпадений и текст описания. 38 | # 39 | gawk -F'Descr=' -vRS='[0-9]+:[0-9]+.[0-9]+-' ' 40 | 41 | function Description(StringAfterDescription) 42 | { 43 | gsub("\n+$", "", StringAfterDescription); 44 | gsub("\n", "", StringAfterDescription); 45 | 46 | return StringAfterDescription; 47 | } 48 | 49 | { 50 | if ( $1 ~ "^.*,EXCP,.*" ) { 51 | 52 | Grouping = Description($2); 53 | Quantity[Grouping] += 1; 54 | 55 | } 56 | }; 57 | 58 | END { 59 | for (Grouping in Quantity) 60 | print Quantity[Grouping] " " Grouping "" 61 | }' | 62 | 63 | sort -rn | 64 | head -n 100 | 65 | 66 | # Мы заменили переводы строк на подстроку — чтобы результат можно было отсортировать (sort) 67 | # и усечь (head). Теперь можно сделать обратную замену. 68 | # 69 | sed -r "s//\n/g" > FrequentExceptions.txt -------------------------------------------------------------------------------- /Технологический журнал/LocksByConnectIDAndRegions.sh: -------------------------------------------------------------------------------- 1 | # Поиск попыток наложений управляемых блокировок по номеру соединения и области блокировки. 2 | # 3 | # Скрипт принимает два параметра: 4 | # 1. номер соединения (t:connectID) 5 | # 2. область наложения блокировки (Regions) 6 | # 7 | # Пример вызова: 8 | # bash LocksByConnectIDAndRegions.sh 80283 InfoRg55705 9 | # 10 | 11 | # Для удобства дальнейшей работы добавим в вывод имена файлов и номера строк. 12 | # Привычную утилиту cat невозможно заставить сделать это, поэтому придется 13 | # пойти на жульничество: используем поиск любого символа с помощью grep, 14 | # которая как раз умеет то, что нам нужно. 15 | # 16 | # Есть более адекватные способы добиться нужного результата, но этот, пожалуй, 17 | # самый короткий из них. 18 | # 19 | grep --with-filename --line-number . rphost_*/*.log | 20 | 21 | # Во-первых, удалим из потока данных BOM. Во-вторых, в дальнейшем мы будем работать 22 | # с отдельными событиями и нам потребуется их сортировать. Во-вторых, переводы строк 23 | # помешают использовать утилиту sort, так что заменим их на подстроку . 24 | # 25 | perl -pe 's/\xef\xbb\xbf//g; s/\r\n//g' | 26 | 27 | # Найдем в потоке события. Для этого отловим строго конкретную последовательность 28 | # символов: имя файла, двоеточие, номер строки, двоеточие (это все было добавлено 29 | # grep'ом), а потом — стандартная строка начала события в ТЖ. Перед каждым найденным 30 | # результатом добавим маркер начала события . 31 | # 32 | # Помимо этого, вытащим дату/время события и вынесем его в начало строки: 33 | # тогда sort сможет с этим работать. 34 | # 35 | perl -pe 's/(rphost_[0-9]+\/([0-9]+)\.log:[0-9]+:([0-9]+):([0-9]+\.[0-9]+)-[0-9]+)/\2\3\4 \1/g' | 36 | 37 | # Разделим поток на отдельные события с помощью маркера и проанализируем: 38 | # подходят эти события под условия из параметров скрипта или нет. Если подходят — 39 | # отдельными строками для каждого выведем поля Locks и alreadyLocked (если есть). 40 | # 41 | gawk -vCONNECTID=$1 -vREGIONS=$2 -vLB='' -vRS='' ' 42 | 43 | function GetFieldValue(FieldValues, BeginsWith) 44 | { 45 | Result = ""; 46 | 47 | BeginsWithLength = length(BeginsWith); 48 | 49 | for (FieldValuesKey in FieldValues) { 50 | 51 | Value = FieldValues[FieldValuesKey]; 52 | 53 | ValueBeginsWith = substr(Value, 1, BeginsWithLength); 54 | 55 | if (ValueBeginsWith == BeginsWith) { 56 | Result = Value; 57 | break; 58 | } 59 | 60 | } 61 | 62 | return Result; 63 | } 64 | 65 | { 66 | if ( $0 ~ ".*TLOCK,.*,t:connectID=" CONNECTID ",.*,Regions=" REGIONS ".*" ) 67 | { 68 | split($0, FieldValues, ","); 69 | 70 | Locks = GetFieldValue(FieldValues, "Locks="); 71 | alreadyLocked = GetFieldValue(FieldValues, "alreadyLocked="); 72 | 73 | print $0 "\t" Locks LB "\t" alreadyLocked LB; 74 | } 75 | }' | 76 | 77 | # Сортируем получившиеся события по времени — от самых ранних к самым поздним. 78 | # 79 | sort --ignore-leading-blanks --numeric-sort | 80 | 81 | # Заменяем обратно на переводы строк, чтобы результат был похож на исходный ТЖ. 82 | # 83 | perl -pe 's//\r\n/g' > LocksByConnectIDAndRegions_$1_$2.txt -------------------------------------------------------------------------------- /Технологический журнал/FrequentExceptions.py: -------------------------------------------------------------------------------- 1 | # Топ пояснений к исключительным ситуациям. 2 | # 3 | # Строится на базе значения свойства Descr у событий EXCP. Для каждого уникального пояснения 4 | # выводится количество повторений. 5 | 6 | import os 7 | import re 8 | import glob 9 | import time 10 | 11 | # Выводит время работы скрипта в секундах. 12 | # 13 | def print_time(): 14 | 15 | seconds = time.time() - start_time 16 | 17 | print("--- %s seconds ---" % seconds) 18 | 19 | # Читает файлы ТЖ и считает повторения для каждого уникального пояснения. 20 | # 21 | def get_descriptions(): 22 | 23 | # Определяет, является ли строка первой для какого-то события. 24 | # 25 | def is_event_first_line(): 26 | 27 | result = re.match('[0-9]{2}:[0-9]{2}.[0-9]+-[0-9]+,.+,', line) 28 | 29 | return result != None 30 | 31 | # Извлекает пояснение из строк события и увеличивает соответствующий счётчик повторений. 32 | # 33 | def add_description(event_lines): 34 | 35 | if len(event_lines) > 0: 36 | 37 | event = "\n".join(event_lines) 38 | 39 | event_fields = event.split(',Descr=') 40 | 41 | if len(event_fields) == 2: 42 | description = event_fields[1] 43 | else: 44 | description = '' 45 | 46 | if descriptions.get(description) == None: 47 | descriptions[description] = 0 48 | 49 | descriptions[description] = descriptions[description] + 1 50 | 51 | script_dirname = os.path.dirname(__file__) 52 | script_dirpath = os.path.abspath(script_dirname) 53 | search_template = os.path.join(script_dirpath, '*', '*.log') 54 | 55 | log_filenames = glob.iglob(search_template, recursive = True) 56 | descriptions = {} 57 | 58 | for log_filename in log_filenames: 59 | 60 | log_file = open(log_filename, 'r', encoding = 'utf-8-sig') 61 | event_lines = [] 62 | 63 | while True: 64 | 65 | line = log_file.readline() 66 | 67 | if not line: 68 | break 69 | 70 | if is_event_first_line(): 71 | 72 | add_description(event_lines) 73 | 74 | event_lines = [] 75 | 76 | if line.find(',EXCP,') != -1: 77 | event_lines.append(line) 78 | 79 | elif len(event_lines) > 0: 80 | 81 | event_lines.append(line) 82 | 83 | add_description(event_lines) 84 | 85 | log_file.close() 86 | 87 | return descriptions 88 | 89 | # Сортирует пояснения по количеству повторений и выводит в файл. 90 | # 91 | def sort_and_write_descriptions(): 92 | 93 | sorted_keys = sorted(descriptions, key = descriptions.get, reverse = True) 94 | output_file = open('FrequentExceptions.txt', 'w', encoding = 'utf-8-sig') 95 | 96 | for sorted_key in sorted_keys: 97 | 98 | description = sorted_key.strip() 99 | number = descriptions[sorted_key] 100 | 101 | line = "\n\n--- {} items ---\n\n{}".format(number, description) 102 | output_file.write(line) 103 | 104 | output_file.close() 105 | 106 | start_time = time.time() 107 | 108 | descriptions = get_descriptions() 109 | 110 | sort_and_write_descriptions() 111 | 112 | print_time() -------------------------------------------------------------------------------- /Технологический журнал/LongestQueries.sh: -------------------------------------------------------------------------------- 1 | # Топ длительных запросов к MSSQL. 2 | # 3 | # Запросы группируются по тексту (поле Sql) и контексту (поле Context). Если текст или контекст 4 | # отсутствуют в записи о событии, группировка будет по тем данным, что есть — например, только 5 | # по запросу или только по контексту. Если нет ни запроса, ни контекста, группировка будет 6 | # выполнена по пустой строке. 7 | # 8 | # Для каждого выводится суммарное время выполнения, среднее и максимальное, а также общее число 9 | # выполнений. Сортировка — по суммарному времени, от большего к меньшему. 10 | # 11 | 12 | cat */*.log | 13 | 14 | # Удаляем из потока данных UTF-8 BOM. 15 | # 16 | perl -pe 's/\xef\xbb\xbf//g' | 17 | 18 | # Сразу удаляем строки с параметрами запросов — они помешают нормальной группировке. 19 | # Вообще это логичнее делать внутри скрипта gawk, но я не придумал, как. 20 | # 21 | sed -r "/^p_[0-9]+:/d" | 22 | 23 | # Утилита gawk разделяет лог по метке времени события и выполняет переданный ей скрипт. 24 | # 25 | gawk -vRS='[0-9]+:[0-9]+.[0-9]+-' ' 26 | 27 | function GetDuration(Input) { 28 | 29 | # Мы разделили события по времени их начала, поэтому набор цифр до запятой — это продолжительность события. 30 | # 31 | return int(substr(Input, 0, index(Input, ",") - 1)); 32 | 33 | } 34 | 35 | function GetGroup(Input) { 36 | 37 | # Определим, какие составляющие группировки есть в тексте события. Нас интересует либо Sql, 38 | # либо Context, а лучше и то, и другое. 39 | 40 | if (index(Input, "Sql=") != 0) { 41 | 42 | Result = Input; 43 | 44 | gsub("^.*Sql=", "Sql=", Result); 45 | 46 | } 47 | else if (index(Input, "Context=") != 0) { 48 | 49 | Result = Input; 50 | 51 | gsub("^.*Context=", "Context=", Result); 52 | 53 | } 54 | else { 55 | 56 | # Если в событии нет ни текста запроса, ни контекста выполнения — 57 | # группируем по пустой строке. 58 | # 59 | Result = ""; 60 | 61 | } 62 | 63 | if (Result != "") { 64 | 65 | gsub("\n+$", "", Result); # удалим двойные переводы строк 66 | gsub("\n", "", Result); # заменим переводы строк на 67 | gsub("#tt[0-9]+", "#tt", Result); # нормализуем имена временных таблиц 68 | gsub("Rows=[0-9]+,RowsAffected=[0-9]+,", "", Result); # удалим данные, не нужные для анализа 69 | 70 | } 71 | 72 | return Result; 73 | 74 | } 75 | 76 | { 77 | # Отбираем только события DBMSSQL. 78 | # 79 | if ( $0 ~ "^[0-9]+,DBMSSQL.*" ) { 80 | 81 | Duration = GetDuration($0); 82 | Group = GetGroup($0); 83 | 84 | QueriesDuration[Group] += Duration; 85 | QueriesExecuted[Group] += 1; 86 | 87 | if (Duration > MaxQueriesDuration[Group]) { 88 | MaxQueriesDuration[Group] = Duration; 89 | } 90 | 91 | } 92 | }; 93 | 94 | END { 95 | 96 | for (Group in QueriesDuration) { 97 | 98 | executedTotal = QueriesExecuted[Group]; 99 | durationMax = MaxQueriesDuration[Group] / 1000000; 100 | durationTotal = QueriesDuration[Group] / 1000000; 101 | durationAverage = QueriesDuration[Group] / QueriesExecuted[Group] / 1000000; 102 | 103 | printf "%.3f seconds total, %.3f seconds on average, %.3f seconds at maximum, %d executions%s\n", durationTotal, durationAverage, durationMax, executedTotal, Group; 104 | 105 | } 106 | 107 | } 108 | ' | 109 | 110 | sort -rn | 111 | head -n 1000 | 112 | 113 | # Мы заменяли переводы строк на подстроку , чтобы результат можно было отсортировать (sort) и усечь (head). 114 | # Теперь можно сделать обратную замену — тогда результат будет удобнее читать. 115 | # 116 | sed -r "s//\n/g" > LongestQueries.txt -------------------------------------------------------------------------------- /Технологический журнал/LongestQueries.py: -------------------------------------------------------------------------------- 1 | # Скрипт извлекает события DBMSSQL из ТЖ 1С и группирует их запросу и контексту. 2 | # 3 | # Тексты запросов нормализуются: параметры удаляются, имена временные таблицы заменяются на #tt. 4 | # Для каждого уникального запроса выводится количество выполнений, суммарная и средняя длительность 5 | # и максимальная продолжительность одного запроса. 6 | # 7 | # Во время работы скрипт выводит путь к обрабатываемому в данный момент файлу, его объём 8 | # и общий прогресс выполнения (сколько файлов обработано из общего количества). 9 | # 10 | # После завершения работы скрипт выводит время в секундах, затраченное на анализ. 11 | 12 | import os 13 | import re 14 | import glob 15 | import time 16 | 17 | from pathlib import Path 18 | 19 | def get_log_filenames(): 20 | 21 | script_dirname = os.path.dirname(__file__) 22 | script_dirpath = os.path.abspath(script_dirname) 23 | 24 | path_template = os.path.join(script_dirpath, 'rphost_*', '*.log') 25 | log_filenames = glob.iglob(path_template, recursive = True) 26 | 27 | return list(log_filenames) 28 | 29 | def print_information(): 30 | 31 | def get_log_file_size(): 32 | 33 | stat_result = Path(log_filename).stat() 34 | 35 | size_in_bytes = stat_result.st_size 36 | size_in_megabytes = size_in_bytes / 1024 / 1024 37 | 38 | return size_in_megabytes 39 | 40 | log_file_size = get_log_file_size() 41 | 42 | print('%s (%.3f MB), %d / %d' % (log_filename, log_file_size, current_log_number, log_filenames_number)) 43 | print('%d unique queries collected' % len(queries)) 44 | print() 45 | 46 | def extract_log_file_queries(): 47 | 48 | def process_event_lines(): 49 | 50 | duration = int(event_lines[0].split('-')[1].split(',')[0]) 51 | 52 | sql = "".join(event_lines) 53 | sql = re.sub('.*,Sql=', 'Sql=', sql) 54 | sql = sql.strip() 55 | 56 | if queries.get(sql) == None: 57 | 58 | queries[sql] = { 59 | 'counter': 0, 60 | 'total_duration': 0, 61 | 'max_duration': 0, 62 | } 63 | 64 | sql_stat = queries[sql] 65 | 66 | sql_stat['total_duration'] = sql_stat['total_duration'] + duration 67 | sql_stat['counter'] = sql_stat['counter'] + 1 68 | 69 | if duration > sql_stat['max_duration']: 70 | sql_stat['max_duration'] = duration 71 | 72 | queries[sql] = sql_stat 73 | 74 | def is_event_first_line(): 75 | 76 | result = re.match('[0-9]{2}:[0-9]{2}.[0-9]+-[0-9]+,[A-Z]+,', line) 77 | 78 | return result != None 79 | 80 | def get_event_name(): 81 | 82 | return line.split(',')[1] 83 | 84 | def add_event_line(): 85 | 86 | result = re.match('p_[0-9]+:.*', line) 87 | 88 | if result == None: 89 | 90 | new_line = re.sub('#tt[0-9]+', '#tt', line) 91 | 92 | event_lines.append(new_line) 93 | 94 | log_file = open(log_filename, 'r', encoding = 'utf-8-sig') 95 | event_lines = [] 96 | 97 | while True: 98 | 99 | line = log_file.readline() 100 | 101 | if not line: 102 | break 103 | 104 | if is_event_first_line(): 105 | 106 | if len(event_lines) > 0: 107 | process_event_lines() 108 | 109 | event_lines = [] 110 | 111 | if get_event_name() == 'DBMSSQL': 112 | add_event_line() 113 | 114 | elif len(event_lines) > 0: 115 | add_event_line() 116 | 117 | if len(event_lines) > 0: 118 | process_event_lines() 119 | 120 | log_file.close() 121 | 122 | def sort_and_write_queries(): 123 | 124 | sorted_queries = sorted(queries.items(), key = lambda kv: kv[1]['total_duration'], reverse = True) 125 | output_file = open('LongestQueries.txt', 'w', encoding = 'utf-8-sig') 126 | 127 | for sorted_query in sorted_queries: 128 | 129 | sql = sorted_query[0].strip() 130 | duration = sorted_query[1]['total_duration'] / 1000000 131 | 132 | max_duration = sorted_query[1]['max_duration'] / 1000000 133 | repetition = sorted_query[1]['counter'] 134 | avg_duration = sorted_query[1]['total_duration'] / repetition / 1000000 135 | 136 | line = "\n\n--- %d queries with %.3f total duration (max duration: %.3f, avg duration: %.3f) ---\n\n%s" % (repetition, duration, max_duration, avg_duration, sql) 137 | output_file.write(line) 138 | 139 | output_file.close() 140 | 141 | def print_running_time(): 142 | 143 | seconds = time.time() - start_time 144 | 145 | print("--- %.3f seconds ---" % seconds) 146 | 147 | start_time = time.time() 148 | queries = {} 149 | 150 | log_filenames = get_log_filenames() 151 | 152 | log_filenames_number = len(log_filenames) 153 | current_log_number = 0 154 | 155 | for log_filename in log_filenames: 156 | 157 | current_log_number += 1 158 | 159 | print_information() 160 | extract_log_file_queries() 161 | 162 | sort_and_write_queries() 163 | 164 | print_running_time() --------------------------------------------------------------------------------