Встановлення рівня журналу повідомлення під час виконання в slf4j


100

При використанні log4j Logger.log(Priority p, Object message)метод доступний і може використовуватися для реєстрації повідомлення на рівні журналу, визначеному під час виконання. Ми використовуємо цей факт і цю пораду для перенаправлення stderr до реєстратора на певному рівні журналу.

slf4j не має загального log()методу, який я можу знайти. Чи означає це, що немає способу здійснити вищезазначене?


4
Схоже, існує певна дискусія щодо додавання цього до slf4j 2.0 у список розсилки розробників: qos.ch/pipermail/slf4j-dev/2010-March/002865.html
Едвард Дейл

1
погляньте на Маркер, це спеціальні дані, які ви можете передати в ланцюжок журналів.
tuxSlayer

1
@tuxSlayer Ви можете, будь ласка, розробити, як використовувати маркер у цьому випадку?
Жалюгідна змінна

Можливо, це не найкраща ідея для "ведення журналу", але ви можете використовувати декілька маркерів для входу в журнал "пріоритет" (високий | низький | нормальний, інформація | попереджати | фатальний) і використовувати фільтрацію в режимі входу в систему або спеціальне додаток для споживання маркерів і записів журналу приводу. на окремі канали (інформація про журнал, фатальна електронна пошта тощо). Однак більш прямим способом є створення фасаду для цього, як було зазначено у відповідях нижче.
tuxSlayer

2
Ця функція повинна бути частиною slf4j 2.0. jira.qos.ch/browse/SLF4J-124 Детальну інформацію та можливий slf4j 1.xобхід див. у моїй відповіді .
slartidan

Відповіді:


47

З цим немає ніякого способу slf4j.

Я думаю, що причиною відсутності цієї функціональності є те, що побудувати Levelтип поруч із неможливимslf4j який може бути ефективно відображений у Levelтип (або еквівалентний) тип, який використовується у всіх можливих реалізаціях журналів за фасадом. Крім того, дизайнери вирішили, що ваш варіант використання занадто незвичний, щоб виправдати витрати на його підтримку.

Що стосується @ ripper234 «s споживчого випадку (модульне тестування), я думаю , що прагматичне рішення змінити модульний тест (и) з працею дротяного знання про те , що система реєстрації знаходиться за SLF4J фасаду ... при виконанні модульних тестів.


9
Насправді немає потрібного картографування. Існує п'ять рівнів, які вже неявно визначені методами в org.slf4j.Logger: налагодження, помилка, інформація, слід, попередження.
Едвард Дейл

1
І питання закрилися як недійсні. Наскільки я це розумію, це навмисний вибір дизайну.
ripper234

9
@ ripper234 - Я не думаю, що ваша помилка вирішила ту саму проблему, що і оригінальне запитання scompt.com. Ви запитали про налаштування рівня базової системи реєстрації через API SLF4J. Те, що було після scompt.com, - це загальний метод «журналу» в API SLF4J, який приймає рівень журналу повідомлення як параметр.
Річард Фірн

1
+1 @RichardFearn І не можна скасувати оновлення коментарів через 60 секунд, мех . Тим часом запит на функцію існує: bugzilla.slf4j.org/show_bug.cgi?id=133
січня

3
Посилання RFE більше не вирішуються. Зараз є відповідні посилання: jira.qos.ch/browse/SLF4J-124 та jira.qos.ch/browse/SLF4J-197 ... і обидва закриті. Прочитайте коментарі до обґрунтування.
Стівен С

27

Річард Ферн має правильну ідею, тому я написав повний клас на основі його коду скелета. Сподіваємось, досить короткий, щоб розмістити тут. Скопіюйте та вставте для задоволення. Мені, мабуть, слід додати ще якусь магічну заклик: "Цей код випускається у загальнодоступне надбання"

import org.slf4j.Logger;

public class LogLevel {

    /**
     * Allowed levels, as an enum. Import using "import [package].LogLevel.Level"
     * Every logging implementation has something like this except SLF4J.
     */

    public static enum Level {
        TRACE, DEBUG, INFO, WARN, ERROR
    }

    /**
     * This class cannot be instantiated, why would you want to?
     */

    private LogLevel() {
        // Unreachable
    }

    /**
     * Log at the specified level. If the "logger" is null, nothing is logged.
     * If the "level" is null, nothing is logged. If the "txt" is null,
     * behaviour depends on the SLF4J implementation.
     */

    public static void log(Logger logger, Level level, String txt) {
        if (logger != null && level != null) {
            switch (level) {
            case TRACE:
                logger.trace(txt);
                break;
            case DEBUG:
                logger.debug(txt);
                break;
            case INFO:
                logger.info(txt);
                break;
            case WARN:
                logger.warn(txt);
                break;
            case ERROR:
                logger.error(txt);
                break;
            }
        }
    }

    /**
     * Log at the specified level. If the "logger" is null, nothing is logged.
     * If the "level" is null, nothing is logged. If the "format" or the "argArray"
     * are null, behaviour depends on the SLF4J-backing implementation.
     */

    public static void log(Logger logger, Level level, String format, Object[] argArray) {
        if (logger != null && level != null) {
            switch (level) {
            case TRACE:
                logger.trace(format, argArray);
                break;
            case DEBUG:
                logger.debug(format, argArray);
                break;
            case INFO:
                logger.info(format, argArray);
                break;
            case WARN:
                logger.warn(format, argArray);
                break;
            case ERROR:
                logger.error(format, argArray);
                break;
            }
        }
    }

    /**
     * Log at the specified level, with a Throwable on top. If the "logger" is null,
     * nothing is logged. If the "level" is null, nothing is logged. If the "format" or
     * the "argArray" or the "throwable" are null, behaviour depends on the SLF4J-backing
     * implementation.
     */

    public static void log(Logger logger, Level level, String txt, Throwable throwable) {
        if (logger != null && level != null) {
            switch (level) {
            case TRACE:
                logger.trace(txt, throwable);
                break;
            case DEBUG:
                logger.debug(txt, throwable);
                break;
            case INFO:
                logger.info(txt, throwable);
                break;
            case WARN:
                logger.warn(txt, throwable);
                break;
            case ERROR:
                logger.error(txt, throwable);
                break;
            }
        }
    }

    /**
     * Check whether a SLF4J logger is enabled for a certain loglevel. 
     * If the "logger" or the "level" is null, false is returned.
     */

    public static boolean isEnabledFor(Logger logger, Level level) {
        boolean res = false;
        if (logger != null && level != null) {
            switch (level) {
            case TRACE:
                res = logger.isTraceEnabled();
                break;
            case DEBUG:
                res = logger.isDebugEnabled();
                break;
            case INFO:
                res = logger.isInfoEnabled();
                break;
            case WARN:
                res = logger.isWarnEnabled();
                break;
            case ERROR:
                res = logger.isErrorEnabled();
                break;
            }
        }
        return res;
    }
}

Це було б простіше у використанні з варіаційним параметром args (Object ...).
Anonymousmoose

"org.slf4j.Logger" має досить багато підписів методів реєстрації, які не обробляються у вищевказаному класі, тому розширення, ймовірно, є гарантованим: slf4j.org/api/org/slf4j/Logger.html
Девід Тонхофер

1
Я думаю, що ця реалізація додасть небажаних змін. Під час використання виклику logger.info (...) реєстратор має доступ до класу та методу виклику, і він може бути доданий до запису журналу автоматично. Тепер із цією реалізацією журнал викликів (реєстратор, рівень, txt) створить запис журналу, який завжди матиме той самий виклик: Loglevel.log. Маю рацію?
Домін

@Domin Привіт, ви маєте на увазі, реєстратор може перевірити поточний стек викликів, а потім витягнути останній запис для автоматичного ведення журналу, що тут не так? В принципі, так, але насправді стек буде ще більше після цього, поки фактичне повідомлення не буде виписано (зокрема, потрібно повернути повідомлення в якийсь момент, а потім власне додаток). Я думаю, що це повинна роль апендера, щоб викинути нецікаві лінії стека, щоб ви могли адаптувати його, щоб викинути все до та включити виклик до цього класу Loglevel.
Девід Тонхофер

@David, Так, ти маєш рацію :-). Я не впевнений, що це завдання для додатка, тому що в цьому випадку ви визначаєте жорстку залежність між додатком і реєстратором ... але ... це рішення. Спасибі Давидові
Доміну

14

Спробуйте переключитися на режим "Зворотний зв'язок" і використовувати

ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(Level.toLevel("info"));

Я вважаю, що це буде єдиний виклик до зворотного зв'язку, а решта вашого коду залишиться незмінною. Для зворотного зв'язку використовується SLF4J, і міграція буде безболісною, просто потрібно змінити конфігураційні файли xml.

Не забудьте повернути рівень журналу після того, як ви закінчите.


Я вже використовував захищений від зворотного зв'язку slf4j, і це миттєво дозволило мені очистити мої тестові одиниці. Дякую!
Ламбарт

2
Це було моє перше -1, дякую. Я вважаю, ви помиляєтесь. Для зворотного зв'язку використовується SLF4J, тому відповідь є актуальною.
Αλέκος

3
@AlexandrosGelbessis Вам слід перечитати питання. Було запропоновано метод, який міг програматизувати одне повідомлення журналу на будь-якому рівні. Ви змінюєте рівень кореневого реєстратора для всіх повідомлень, а не лише для одного.
січня

12

Ви можете реалізувати це за допомогою лямбда Java 8.

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

public class LevelLogger {
    private static final Logger LOGGER = LoggerFactory.getLogger(LevelLogger.class);
    private static final Map<Level, LoggingFunction> map;

    static {
        map = new HashMap<>();
        map.put(Level.TRACE, (o) -> LOGGER.trace(o));
        map.put(Level.DEBUG, (o) -> LOGGER.debug(o));
        map.put(Level.INFO, (o) -> LOGGER.info(o));
        map.put(Level.WARN, (o) -> LOGGER.warn(o));
        map.put(Level.ERROR, (o) -> LOGGER.error(o));
    }

    public static void log(Level level, String s) {
        map.get(level).log(s);
    }

    @FunctionalInterface
    private interface LoggingFunction {
        public void log(String arg);
    }
}

Ну так ... але тепер вам потрібно змінити свою кодову базу, щоб використовувати цей API, а також або замість slf4j. Якщо ви використовуєте його замість slf4j 1) він, ймовірно, повинен бути багатшим, 2) багато (принаймні) імпорту потрібно змінити, і 3) цей новий шар перед slf4j додає додаткових накладних витрат.
Стівен C

4
Пам’ятайте також, що коли ви переходите до цього рішення, клас, який здійснює фактичну реєстрацію, не буде реєструватися (тому що реєстратор ініціалізований з LevelLogger), що не дуже добре, оскільки це, як правило, дуже корисна інформація.
Dormouse

6

Це можна зробити за допомогою enumі допоміжного методу:

enum LogLevel {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR,
}

public static void log(Logger logger, LogLevel level, String format, Object[] argArray) {
    switch (level) {
        case TRACE:
            logger.trace(format, argArray);
            break;
        case DEBUG:
            logger.debug(format, argArray);
            break;
        case INFO:
            logger.info(format, argArray);
            break;
        case WARN:
            logger.warn(format, argArray);
            break;
        case ERROR:
            logger.error(format, argArray);
            break;
    }
}

// example usage:
private static final Logger logger = ...
final LogLevel level = ...
log(logger, level, "Something bad happened", ...);

Ви можете додати інші варіанти log, скажімо, якщо вам потрібні загальні еквіваленти 1-параметру SLF4J або 2-параметра warn/ error/ тощо. методи.


3
Правда, але мета slf4j - не писати обгортки журналів.
djjeck

5
Метою SLF4J є надання абстракції для різних каркасів ведення журналу. Якщо ця абстракція не забезпечує саме те, що вам потрібно, у вас немає іншого вибору, як написати допоміжний метод. Єдиною іншою альтернативою є внести такий метод, як той, який я відповів на проект SLF4J.
Річард Фірн

Я погоджуюся, але в цьому випадку є такі застереження, як, наприклад, ви б більше не змогли надати номер файлу та рядка, якщо б ви не здійснили ще один спосіб вирішення цього питання. У цьому випадку я затримався б з log4j, поки рамка не підтримала функцію - що в підсумку відбулося через розширення, дивіться останню відповідь Роберта Елліота.
djjeck


3

Мені просто потрібно було щось подібне і придумали:

@RequiredArgsConstructor //lombok annotation
public enum LogLevel{

    TRACE(l -> l::trace),
    INFO (l -> l::info),
    WARN (l -> l::warn),
    ERROR(l -> l::error);

    private final Function<Logger, Consumer<String>> function;

    public void log(Logger logger, String message) {
        function.apply(logger).accept(message);
    }
}

використання:

    LogLevel level = LogLevel.TRACE;
    level.log(logger, "message");

Журнал передається під час виклику, тому інформація про клас повинна бути в порядку, і вона добре працює з анотацією @ Slf4j lombok.


Дуже дякую за цей дивовижний підхід - я опублікував подібну відповідь, виходячи з вашої ідеї.
slartidan

DEBUGвідсутній як постійний.
slartidan

Це рішення завжди буде входити LogLevelяк клас і logяк метод, що робить журнали менш значущими.
slartidan

2

Це НЕ можливо визначити рівень протоколювання в sjf4j 1.xз коробки. Але є slf4j надію 2.0виправити цю проблему . У 2.0 може виглядати так:

// POTENTIAL 2.0 SOLUTION
import org.slf4j.helpers.Util;
import static org.slf4j.spi.LocationAwareLogger.*;

// does not work with slf4j 1.x
Util.log(logger, DEBUG_INT, "hello world!");

Тим часом для slf4j 1.x ви можете скористатися цим способом:

Скопіюйте цей клас у свій класний шлях:

import org.slf4j.Logger;
import java.util.function.Function;

public enum LogLevel {

    TRACE(l -> l::trace, Logger::isTraceEnabled),
    DEBUG(l -> l::debug, Logger::isDebugEnabled),
    INFO(l -> l::info, Logger::isInfoEnabled),
    WARN(l -> l::warn, Logger::isWarnEnabled),
    ERROR(l -> l::error, Logger::isErrorEnabled);

    interface LogMethod {
        void log(String format, Object... arguments);
    }

    private final Function<Logger, LogMethod> logMethod;
    private final Function<Logger, Boolean> isEnabledMethod;

    LogLevel(Function<Logger, LogMethod> logMethod, Function<Logger, Boolean> isEnabledMethod) {
        this.logMethod = logMethod;
        this.isEnabledMethod = isEnabledMethod;
    }

    public LogMethod prepare(Logger logger) {
        return logMethod.apply(logger);
    }

    public boolean isEnabled(Logger logger) {
        return isEnabledMethod.apply(logger);
    }
}

Тоді ви можете використовувати його так:

Logger logger = LoggerFactory.getLogger(Application.class);

LogLevel level = LogLevel.ERROR;
level.prepare(logger).log("It works!"); // just message, without parameter
level.prepare(logger).log("Hello {}!", "world"); // with slf4j's parameter replacing

try {
    throw new RuntimeException("Oops");
} catch (Throwable t) {
    level.prepare(logger).log("Exception", t);
}

if (level.isEnabled(logger)) {
    level.prepare(logger).log("logging is enabled");
}

Це виведе журнал таким чином:

[main] ERROR Application - It works!
[main] ERROR Application - Hello world!
[main] ERROR Application - Exception
java.lang.RuntimeException: Oops
    at Application.main(Application.java:14)
[main] ERROR Application - logging is enabled

Чи варто того?

  • ProВін зберігає розташування вихідного коду (назви класів, назви методів, номери рядків вказуватимуть на ваше код)
  • ProВи можете легко визначити змінні , параметри та типи повернення якLogLevel
  • ProВаш бізнес-код залишається коротким і легким для читання, і не потрібно додаткові залежності .

Вихідний код як мінімальний приклад розміщується на GitHub .


Примітка: LogMethodдля роботи з класами поза пакетом інтерфейс повинен бути загальнодоступним. Крім цього, він працює за призначенням. Дякую!
andrebrait

1

З API slf4j неможливо динамічно змінювати рівень журналу, але ви можете налаштувати зворотний зв'язок (якщо ви використовуєте це) самостійно. У цьому випадку створіть заводський клас для свого реєстратора та реалізуйте кореневий реєстратор із потрібною конфігурацією.

LoggerContext loggerContext = new LoggerContext();
ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);

// Configure appender
final TTLLLayout layout = new TTLLLayout();
layout.start(); // default layout of logging messages (the form that message displays 
// e.g. 10:26:49.113 [main] INFO com.yourpackage.YourClazz - log message

final LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<>();
encoder.setCharset(StandardCharsets.UTF_8);
encoder.setLayout(layout);

final ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
appender.setContext(loggerContext);
appender.setEncoder(encoder);
appender.setName("console");
appender.start();

root.addAppender(appender);

Після налаштування кореневого реєстратора (достатньо лише одного разу) ви можете делегувати отримання нового реєстратора

final ch.qos.logback.classic.Logger logger = loggerContext.getLogger(clazz);

Не забудьте використати те саме loggerContext .

Зміна рівня журналу легко зробити за допомогою кореневого реєстратора, що дається з loggerContext.

root.setLevel(Level.DEBUG);

1

Підтвердити відповідь Ондрей Скопек

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.slf4j.LoggerFactory;

var rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(Level.TRACE);

Ви отримаєте результат:

2020-05-14 14: 01: 16,644 TRACE [] [oakcmMetrics] Тестовий працівник Зареєстрований показник з ім'ям MetricName [ім'я = загальний буфер-час очікування, група = показники виробника, опис = Загальний час, коли заявник очікує на виділення місця ., теги = {client-id = виробник-2}]


0

Я щойно стикався з подібною потребою. У моєму випадку slf4j налаштований за допомогою адаптера реєстрації Java (jdk14). Використовуючи наступний фрагмент коду, мені вдалося змінити рівень налагодження під час виконання:

Logger logger = LoggerFactory.getLogger("testing");
java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger("testing");
julLogger.setLevel(java.util.logging.Level.FINE);
logger.debug("hello world");

1
Як і інші відповіді, це не стосується оригінального питання, це інша проблема.
E-Riz

0

Виходячи з відповіді Massimo virgilio, мені також вдалося це зробити з slf4j-log4j, використовуючи самоаналіз. HTH.

Logger LOG = LoggerFactory.getLogger(MyOwnClass.class);

org.apache.logging.slf4j.Log4jLogger LOGGER = (org.apache.logging.slf4j.Log4jLogger) LOG;

try {
    Class loggerIntrospected = LOGGER.getClass();
    Field fields[] = loggerIntrospected.getDeclaredFields();
    for (int i = 0; i < fields.length; i++) {
        String fieldName = fields[i].getName();
        if (fieldName.equals("logger")) {
            fields[i].setAccessible(true);
            org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) fields[i].get(LOGGER);
            loggerImpl.setLevel(Level.DEBUG);
        }
    }
} catch (Exception e) {
    System.out.println("ERROR :" + e.getMessage());
}

0

Ось лямбда-рішення не настільки зручне в користуванні, як @Paul Croarkin в один спосіб (рівень ефективно проходить двічі). Але я думаю, що: (а) користувач повинен передати реєстратор; та (b) AFAIU в оригінальному запитанні не було прохання зручного способу для всюди в додатку, лише ситуація з невеликим звичаєм всередині бібліотеки.

package test.lambda;
import java.util.function.*;
import org.slf4j.*;

public class LoggerLambda {
    private static final Logger LOG = LoggerFactory.getLogger(LoggerLambda.class);

    private LoggerLambda() {}

    public static void log(BiConsumer<? super String, ? super Object[]> logFunc, Supplier<Boolean> logEnabledPredicate, 
            String format, Object... args) {
        if (logEnabledPredicate.get()) {
            logFunc.accept(format, args);
        }
    }

    public static void main(String[] args) {
        int a = 1, b = 2, c = 3;
        Throwable e = new Exception("something went wrong", new IllegalArgumentException());
        log(LOG::info, LOG::isInfoEnabled, "a = {}, b = {}, c = {}", a, b, c);

        // warn(String, Object...) instead of warn(String, Throwable), but prints stacktrace nevertheless
        log(LOG::warn, LOG::isWarnEnabled, "error doing something: {}", e, e);
    }
}

Оскільки slf4j дозволяє Throwable (чий стек слід записувати) всередині парами varargs , я думаю, немає потреби в перевантаженні logдопоміжного методу для інших споживачів, ніж (String, Object[]).


0

Мені вдалося це зробити для прив'язки JDK14, спочатку запитавши екземпляр SLF4J Logger, а потім встановивши рівень на прив'язку - ви можете спробувати це для прив'язки Log4J.

private void setLevel(Class loggerClass, java.util.logging.Level level) {
  org.slf4j.LoggerFactory.getLogger(loggerClass);
  java.util.logging.Logger.getLogger(loggerClass.getName()).setLevel(level);
}

0

Я використовую метод імпорту модулів ch.qos.logback, а потім наберіть примірник журналу slf4j Logger до ch.qos.logback.classic.Logger. Цей екземпляр включає метод setLevel ().

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;

Logger levelSet = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

// Now you can set the desired logging-level
levelSet.setLevel( Level.OFF );

Щоб дізнатись можливі рівні журналу, ви можете підірвати клас ch.qos.logback, щоб побачити всі можливі значення для рівня :

prompt$ javap -cp logback-classic-1.2.3.jar ch.qos.logback.classic.Level

Результати такі:

{
   // ...skipping
   public static final ch.qos.logback.classic.Level OFF;
   public static final ch.qos.logback.classic.Level ERROR;
   public static final ch.qos.logback.classic.Level WARN;
   public static final ch.qos.logback.classic.Level INFO;
   public static final ch.qos.logback.classic.Level DEBUG;
   public static final ch.qos.logback.classic.Level TRACE;
   public static final ch.qos.logback.classic.Level ALL;
}

-2

використовуючи інтроспекцію Java, ви можете це зробити, наприклад:

private void changeRootLoggerLevel(int level) {

    if (logger instanceof org.slf4j.impl.Log4jLoggerAdapter) {
        try {
            Class loggerIntrospected = logger.getClass();
            Field fields[] = loggerIntrospected.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                String fieldName = fields[i].getName();
                if (fieldName.equals("logger")) {
                    fields[i].setAccessible(true);
                    org.apache.log4j.Logger loggerImpl = (org.apache.log4j.Logger) fields[i]
                            .get(logger);

                    if (level == DIAGNOSTIC_LEVEL) {
                        loggerImpl.setLevel(Level.DEBUG);
                    } else {
                        loggerImpl.setLevel(org.apache.log4j.Logger.getRootLogger().getLevel());
                    }

                    // fields[i].setAccessible(false);
                }
            }
        } catch (Exception e) {
            org.apache.log4j.Logger.getLogger(LoggerSLF4JImpl.class).error("An error was thrown while changing the Logger level", e);
        }
    }

}

5
Це прямо стосується log4j, а не slf4j загалом
Thorbjørn Ravn Andersen

-6

ні, він має ряд методів, info (), debug (), warn () тощо тощо (це замінює поле пріоритету)

подивіться на http://www.slf4j.org/api/org/slf4j/Logger.html для повного api Logger.


вибачте, я бачу, що ви зараз просите. ні, немає загального способу зміни рівня журналу під час виконання, але ви можете легко реалізувати допоміжний метод зі статусом комутатора.
chris

Так, але тоді ви повинні зробити це один раз для кожної перевантаженої версії методу "log".
Ендрю Лебедь
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.