Бизнес кейс
Отлавливать ошибки в методах api и обработчиках событий пользовательских модулей
Постановка
Пользовательский модуль для создания логов и приложение для хранения
Решение
Основное назначение – логирование в методах api и обработчиках событий пользовательских модулей, т.к. в данных местах не предусмотрено инструментов пользовательской отладки/обработки ошибок. Также возможно использовать в решении для дебага.
Состав решения:
- Приложение Журнал логов. Рекомендуется создавать приложение в специализированном для администрирования разделе. Например, создать раздел «Сервисный раздел».
![[IMG]](https://community.elma365.com/ru/assets/bG9nX21lc3NhZ2VfYXBwMTcxNjg4MzExOA==.png)
![[IMG]](https://community.elma365.com/ru/assets/bG9nZ2VyMTcxNjg4MzIzMw==.png)
![[IMG]](https://community.elma365.com/ru/assets/bG9nZ2VyIDIxNzE2ODgzMjk3.png)
Описание решения:
Контекст приложения Журнал логовов:
- Описание – текст сообщения журнала
- Уровень – категория уровня логирования
- Связанный элемент приложения – произвольное приложение
- Поставщик – категория поставщика логирования
Контекст настроек модуля:
- Сообщение журнала – приложение типа Журнал логов
- Настройки – таблица, свойства – Поставщик (строка), Уровень/Включено – таблица с названием уровня логирования и чекбоксом
Таблица настроек модуля с минимальной кастомизацией, формируется динамически на основе значений категорий Уровень и Поставщик приложения Журнал логов. Тем самым отредактировав значения категорий, достаточно зайти в настройки модуля, включить необходимые уровни логирования и сохранить.
![[IMG]](https://community.elma365.com/ru/assets/bG9nZ2VyIHNldHMgMTE3MTY4ODY5MDE=.png)
	Код:
	
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)}`
    );
}