...

Логгер

Тема в разделе "Скрипты и разработка", создана пользователем kirillovykh, 28 май 2024.

Метки:
  1. kirillovykh

    kirillovykh Участник

    Бизнес кейс
    Отлавливать ошибки в методах api и обработчиках событий пользовательских модулей

    Постановка
    Пользовательский модуль для создания логов и приложение для хранения

    Решение
    Основное назначение – логирование в методах api и обработчиках событий пользовательских модулей, т.к. в данных местах не предусмотрено инструментов пользовательской отладки/обработки ошибок. Также возможно использовать в решении для дебага.

    Состав решения:
    • Приложение Журнал логов. Рекомендуется создавать приложение в специализированном для администрирования разделе. Например, создать раздел «Сервисный раздел».
    [​IMG]
    • Модуль Логгер
    [​IMG]
    [​IMG]

    Описание решения:
    Контекст приложения Журнал логовов:
    • Описание – текст сообщения журнала
    • Уровень – категория уровня логирования
    • Связанный элемент приложения – произвольное приложение
    • Поставщик – категория поставщика логирования
    Контекст настроек модуля:
    • Сообщение журнала – приложение типа Журнал логов
    • Настройки – таблица, свойства – Поставщик (строка), Уровень/Включено – таблица с названием уровня логирования и чекбоксом

    Таблица настроек модуля с минимальной кастомизацией, формируется динамически на основе значений категорий Уровень и Поставщик приложения Журнал логов. Тем самым отредактировав значения категорий, достаточно зайти в настройки модуля, включить необходимые уровни логирования и сохранить.
    [​IMG]
    Код:
    
    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.
    Обработка тела запроса:
    1. Проверка корректности полученных данных
    2. Проверка включения уровня лога для полученного местонахождения
    3. Создание элемента приложения Журнал логов
    4. При ошибке создания отправка сообщения в ленту связанного элемента приложения при его наличии.

    Листинг логгера
    Код:
    
    /**
    * Произвольное приложение
    */
    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)}`
        );
    }

    Последнее редактирование: 28 май 2024
  2. asup

    asup Новичок

    Пожалуйста распишете подробнее. Особенно создание приложения "Журнал логовов" и настройки модуля "Логгер".
    1. Что такое location?
    2. Как была создана таблица Настройки в модуле "Логгер"? Опишите подробней.
    3. Как и где создать функцию onInit()?
    4. Где и как использовать Листинг логгера?