Встановлення рівня журналу повідомлення під час виконання в 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.