Бизнес кейс
Отлавливать ошибки в методах api и обработчиках событий пользовательских модулей
Постановка
Пользовательский модуль для создания логов и приложение для хранения
Решение
Основное назначение – логирование в методах api и обработчиках событий пользовательских модулей, т.к. в данных местах не предусмотрено инструментов пользовательской отладки/обработки ошибок. Также возможно использовать в решении для дебага.
Состав решения:
- Приложение Журнал логов. Рекомендуется создавать приложение в специализированном для администрирования разделе. Например, создать раздел «Сервисный раздел».
Описание решения:
Контекст приложения Журнал логовов:
- Описание – текст сообщения журнала
- Уровень – категория уровня логирования
- Связанный элемент приложения – произвольное приложение
- Поставщик – категория поставщика логирования
Контекст настроек модуля:
- Сообщение журнала – приложение типа Журнал логов
- Настройки – таблица, свойства – Поставщик (строка), Уровень/Включено – таблица с названием уровня логирования и чекбоксом
Таблица настроек модуля с минимальной кастомизацией, формируется динамически на основе значений категорий Уровень и Поставщик приложения Журнал логов. Тем самым отредактировав значения категорий, достаточно зайти в настройки модуля, включить необходимые уровни логирования и сохранить.
Код:
async function onInit(): Promise<void> {
const sets_table = Context.fields.sets_table.create();
for (let location of Context.fields.log_item.app.fields.location.data.variants) {
const row = sets_table.insert();
const old_row = Context.data.sets_table?.find(x => x.location === location.code);
row.location = location.code;
if (!old_row) {
const table = Context.fields.sets_table.fields.level_sets_table.create();
Context.fields.log_item.app.fields.level.data.variants.map(x => table.insert().level = x.code);
row.level_sets_table = table;
continue;
}
for (let level of Context.fields.log_item.app.fields.level.data.variants) {
const level_row = row.level_sets_table.insert();
const old_level_row = old_row.level_sets_table.find(x => x.level === level.code);
level_row.level = level.code;
if (old_level_row) {
level_row.enabled = old_level_row.enabled;
}
}
}
Context.data.sets_table = sets_table;
}
Дополнительно возможно динамически создавать дерево папок приложения, для удобства хранения логов.
Для корректного создания лога в теле запроса необходимо указать location, level и message, необязательный параметр ref_item.
Обработка тела запроса:
- Проверка корректности полученных данных
- Проверка включения уровня лога для полученного местонахождения
- Создание элемента приложения Журнал логов
- При ошибке создания отправка сообщения в ленту связанного элемента приложения при его наличии.
Листинг логгера
Код:
/**
* Произвольное приложение
*/
type MetaRefItem = {
-readonly [K in keyof RefItem as NonFunctionKeys<RefItem, K>]: RefItem[K]
};
type NonFunctionKeys<T, K extends keyof T> = T[K] extends () => any ? never : K;
type Location = keyof typeof Namespace.params.fields.log_item.app.fields.location.variants;
type Level = keyof typeof Namespace.params.fields.log_item.app.fields.level.variants;
type LogData = {
location: Location,
level: Level,
message: string,
ref_item?: MetaRefItem,
}
type LogLevelData = {
location: Location,
message: string,
ref_item?: MetaRefItem,
}
type LogDataKeys = keyof LogData;
type LogLevelDataKeys = keyof LogLevelData;
const logger_app = Namespace.params.fields.log_item.app;
let log_data: LogData | undefined;
async function log(req: HttpApiRequest): Promise<HttpResponse | void> {
try {
if (typeof req?.body !== 'string') {
throw new Error('Empty request body');
}
log_data = JSON.parse(req.body);
if (!isLogData(log_data)) {
throw new Error('Not Log Message');
}
if (!isEnabled(log_data.location, log_data.level)) {
return;
}
await createLogItem(log_data.location, log_data.level, log_data.message, log_data.ref_item);
}
catch (err) {
await createLogItem('logger', 'error', `${err.message}: log_data: ${JSON.stringify(log_data)}`, log_data?.ref_item);
}
}
//#region Log by level
async function logError(req: HttpApiRequest): Promise<HttpResponse | void> {
try {
if (typeof req?.body !== 'string') {
throw new Error('Empty request body');
}
log_data = JSON.parse(req.body);
if (!isLogLevelData(log_data)) {
throw new Error('Not Log Level Message');
}
if (!isEnabled(log_data.location, 'error')) {
return;
}
await createLogItem(log_data.location, 'error', log_data.message, log_data.ref_item);
}
catch (err) {
await createLogItem('logger', 'error', `${err.message}: ${logError.name}_data: ${JSON.stringify(log_data)}`, log_data?.ref_item);
}
}
async function logWarning(req: HttpApiRequest): Promise<HttpResponse | void> {
try {
if (typeof req?.body !== 'string') {
throw new Error('Empty request body');
}
log_data = JSON.parse(req.body);
if (!isLogLevelData(log_data)) {
throw new Error('Not Log Level Message');
}
if (!isEnabled(log_data.location, 'error')) {
return;
}
await createLogItem(log_data.location, 'warn', log_data.message, log_data.ref_item);
}
catch (err) {
await createLogItem('logger', 'error', `${err.message}: ${logWarning.name}_data: ${JSON.stringify(log_data)}`, log_data?.ref_item);
}
}
async function logInformation(req: HttpApiRequest): Promise<HttpResponse | void> {
...
}
async function logDebug(req: HttpApiRequest): Promise<HttpResponse | void> {
...
}
//#endregion
//#region Supporting methods
async function createLogItem(location: Location, level: Level, message: string, ref_item?: MetaRefItem) {
try {
const log_item = logger_app.create();
log_item.data.location = Namespace.params.fields.log_item.app.fields.location.variants[location];
log_item.data.level = Namespace.params.fields.log_item.app.fields.level.variants[level];
log_item.data.description = message;
log_item.data.application_item = ref_item as TRefItem;
log_item.data.status = Namespace.params.fields.log_item.app.fields.status.variants.unprocessed;
await log_item.save();
}
catch (err) {
if (!ref_item) {
return;
}
const ref_item_obj = new RefItem(ref_item.namespace, ref_item.code, ref_item.id);
const item: ApplicationItem<any, any> = await ref_item_obj.fetch();
await item.sendMessage(`Error: Writing ${level} log message`, `${err.message}: log_data: ${JSON.stringify(log_data)}`);
}
}
function isEnabled(location: Location, level: Level) {
return !!Namespace.params.data.sets_table.find(x => x.location == location && x.level_sets_table.find(y => y.level == level)?.enabled);
}
function isLogData(value: any): value is LogData {
if (!isObject(value)) {
return false;
}
const keys: LogDataKeys[] = ['location', 'level', 'message'];
const obj_keys = Object.keys(value);
if (keys.some(x => obj_keys.indexOf(x) == -1 || !value[x])) {
return false;
}
if (!isLocation(value[keys[0]]) || !isLevel(value[keys[1]])) {
return false;
}
return true;
}
function isLogLevelData(value: any): value is LogLevelData {
if (!isObject(value)) {
return false;
}
const keys: LogLevelDataKeys[] = ['location', 'message'];
const obj_keys = Object.keys(value);
if (keys.some(x => obj_keys.indexOf(x) == -1 || !value[x])) {
return false;
}
if (!isLocation(value[keys[0]])) {
return false;
}
return true;
}
function isObject(value: any) {
return Object.prototype.toString.call(value) === '[object Object]';
}
function isLocation(value: any): value is Location {
switch (value as Location) {
case 'all':
case 'out_messaging':
case 'in_messaging':
case 'deals':
return true;
default:
return false;
}
}
function isLevel(value: any): value is Level {
switch (value as Level) {
case 'error':
case 'warn':
case 'info':
case 'debug':
case 'log':
return true;
default:
return false;
}
}
//#endregion
Для методов настроена внешняя авторизация, поэтому при создании запроса необходимо указывать токен пользователя.
Пример создания лога
Код:
async function logError(message: string, event?: CstmEvent) {
const body: LogLevelData = {
location: Namespace.params.fields.log_item.app.fields.location.variants.out_messaging.code,
message: message,
ref_item: event?.__item,
};
const response = await fetch(`${System.getBaseUrl()}/api/extensions/018e9932-69e4-7e20-86fd-cdfb34f17eab/script/log_error`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${Namespace.params.data.logger_token}`,
},
body: JSON.stringify(body),
});
if (response.status < 300) {
return;
}
if (!event?.__item) {
return;
}
const ref_item = new RefItem(event.__item.namespace, event.__item.code, event.__item.id);
const item: ApplicationItem<any, any> = await ref_item.fetch();
await item.sendMessage(
'Error: Writing log message',
`${message} at ${logError.name}\n at ${Namespace.code}:\n response: ${response.status} ${await response.text()}, log_data: ${JSON.stringify(body)}, event: ${JSON.stringify(event)}`
);
}