Пользователь
0,0
рейтинг
12 октября 2011 в 19:47

Разработка → Логирование в Java / quick start

Java*
В ходе моей работы в компании DataArt я, в числе прочего, занимаюсь менторской деятельностью. В частности это включает в себя проверку учебных заданий сделанных практикантами. В последнее время в заданиях наметилась тенденция «странного» использования логеров. Мы с коллегами решили включить в текст задания ссылку на статью с описанием java logging best practices, но оказалось, что такой статьи в которой бы просто и без лишних деталей на практике объяснялось бы как надо писать в лог на Java, вот так вот с ходу не находится.

Данная статья не содержит каких-то откровений, в ней не рассматриваются тонкости какого либо из многочисленных java logging frameworks. Здесь рассказываю как записать в лог так, чтобы это не вызвало удивления у Ваших коллег, основная цель написания включить ее в список обязательного чтения для практикантов. Если все еще интересно, читайте дальше


Несколько разъяснений.
  • Весь код примеров использует java.util.logging framework. Вопрос «Какой из фреймворков логирования ниболее кошерен» я оставлю за кадром. Скажу только что до java.util.logging проще всего дотянуться ибо он уже идет вместе с JRE и на самом деле рассказанное в данной статье с минимальными косметическими правками верно для подавляющего большинства систем логирования.
  • В целом рецепты приведенные в данной статье не являются единственно верными, есть моменты о которых можно поспорить, но в целом эти рецепты используются многие годы, многими разработчиками, во многих проектах и они достаточно хороши чтобы им следовать если у Вас нет каких-то совсем уже серьезных возражений.
  • В статье не рассматриваются такие «продвинутые» топики как:
    • Конфигурирование уровней для отдельных логеров
    • Форматирования логов
    • Асинхронное логирование
    • Создание собственных уровней логирования в Log4J
    • Контекстное логирование
    • И многое другое
  • Слово logging я пишу по русски как логирование с одной буквой «г» в основном потом, что такой вариант перевода чаще встречается
  • Советы, что, с каким уровнем логировать я пожалуй тоже оставлю за кадром т.к. тут все сильно зависит от приложения, условий эксплуатации, отношений с заказчиком и т.п. тонких вещей.


Пример №1


Хорошо


public class SomeClass {
	    
	    private static Logger log = Logger.getLogger(SomeClass.class.getName());
	    
	    public void someMethod()
	    {
	        log.info("Some message");
	    }      
...


  1. Логер это статическое поле класса инициализируемое при загрузке класса, имеет простое, короткое имя, важно чтобы во всех Ваших классах переменная логера называлась одинаково (это диктуется общим правилом, одинаковые вещи в программе должны делаться одинаковым образом).
  2. В качестве имени логера я использую имя класса, на самом деле это не единственный способ, можно пытаться организовать какую-то свою иерархию логирования (например transport layer/app layer для подсистем имеющих дело с обменом данными), но как показывает практика выдумывать и главное потом неукоснительно следовать такой иерархии крайне сложно, а вариант с именами логеров совпадающими с именами классов весьма хорош и используется в 99% проектов
  3. Здесь для записи в лог я использую короткий метод .info, а не более общий метод .log, так много лаконичнее
  4. Имя логера берется как SomeClass.class.getName(), а не как «com.dataart.demo.java.logging.SomeClass», оба способа по идее одинаковы, но первый защищает Вас от сюрпризов при рефакторинге имени/пакета класса


Плохо

public class SomeClass {
	    public void someMethod()
	    {
	         Logger.getLogger("com.dataart.demo.java.logging.SomeClass").log(Level.INFO,"Some message");
	    }    
...  


По сути тоже самое но букв больше и читается не так легко.

Замечание между примерами

Вы наверное обратили внимание, что все сообщения в примерах на английском языке. Это не случайно. Дело в том, что даже если все-все кто работает и будет работать с Вашим кодом говорят по русски, есть вероятность, что Вам придется просматривать лог сообщения на удаленном компьютере например через ssh при этом в большом количестве случаев Вы увидите примерно такое сообщение "????, ???? ?????!!!!" (я безусловно знаю что через ssh можно протащить русские буквы, но вот почему-то далеко не всегда все оказывается настроенным должным образом).
Или даже на локальной машине в cmd вы можете увидеть что вот такое:
INFO: ╨Ъ╨░╨║╨╛╨╡-╤В╨╛ ╤Б╨╛╨╛╨▒╤Й╨╡╨╜╨╕╨╡ ╨▓ ╨╗╨╛╨│

С этим безусловно тоже можно бороться. Но не всегда легко объяснить заказчику на том конце телефонной трубки, как сделать так чтобы вместо крякозябр были видны русские буквы.
Совет: Пишите лог сообщения на английском языке, ну или в крайнем случае латинскими буквами.

Пример №2

Хорошо



	       try {
	            throw new Exception("Some exception");
	        } catch (Exception ex) {
	            log.log(Level.SEVERE, "Exception: ", ex);
	        }
	        //В стандартной лог конфигурации вы это сообщение не увидите
	        log.fine("some minor, debug message");

	        /*
	          Иногда вывод лог сообщений требует достаточно больших ресурсов (например
	          дамп какого-то пакета данных и т.п.).
	          В таких случаях стоит проверить выведется ли в лог сообщение для этого уровня
	          логирования
	        */
	        if (log.isLoggable(Level.FINE)) {
	            log.fine("Some CPU consuming message: " + prepareCPUConsumingLogMessage());
	        }


  1. Если Вам необходимо залогировать исключение, для этого служит метод .log(level,message,exception)
  2. Если вы специально не настроили конфигурацию лог системы, сообщения с уровнем ниже info, например fine выводиться не будут. Но писать их по крайней мере для важных частей системы стоит. Когда что-то пойдет не так, Вы настроите более подробный уровень логирования и увидите много интересного.
  3. Слишком много лог сообщений, даже если они физически не пишутся в лог файл из-за своего слишком маленького уровня, могут существенно замедлить выполнение программы. Особенно если для подготовки самого сообщения надо потратить много ресурсов. Для этого есть метод .isLoggable(level) — он позволяет узнать пропустит ли текущая конфигурация логера данное сообщение


Плохо


	       try {
	            throw new Exception("Some exception");
	        } catch (Exception ex) {
	            log.severe("Exception: " + ex.toString() );
	        }
                log.fine("Some CPU consuming message: " + itTakes500MillisecondsToPrepageThisMessage());
	


Если логировать только ex.toString(), то потом Вы не сможете понять в какой строке изначально сработало исключение.

Пример №3


Логер надо конфигурировать. Есть конфигурация по умолчанию она выводит в консоль все сообщения с уровнем INFO и выше. Она достаточно хороша, для разработки из IDE, но для реального приложения ее обычно неплохо бы подправить.

Какие тут есть варианты

По умолчанию: Файл logging.properties для уровня INFO, вывод в консоль

#Console handler
handlers= java.util.logging.ConsoleHandler
.level=INFO


Делаем логирование более подробным выводим еще и сообщения уровня FINE

#Console handler
handlers= java.util.logging.ConsoleHandler
.level=FINE
java.util.logging.ConsoleHandler.level = FINE


Что мы тут сделали
  • Установили уровень FINE для корневого логера, просто чтобы сообщения пролезали внутрь лог системы.
  • И сказали что все что пролезет через лог систему надо выводить на консоль от уровня FINE и выше.


Выводим лог сообщения куда-то еще


Чем плох вывод на консоль? Консоль это по сути дела старый добрый stderr. Что это значит:
  • Если приложение запускается с помощью javaw Вы вообще ничего не увидите.
  • Если вывод идет в консоль и нужное вам сообщение промелькнуло 4 часа назад буфер консоли его уже съел, информация пропала.
  • Если вывод консоли направлен в файл java com.yourcompanyname.EntryClass 2>>application_log.txt и приложение работает не останавливаясь несколько недель — файл будет весьма и весьма большим, рискуя занять весь диск.


Чтобы решить эти проблемы был придуман java.util.logging.FileHandler — хэндлер который выводит лог сообщения в файл. При этом он умеет ротировать файлы, т.е. после достижения максимально допустимого размера, он дописывает в файл текщуее лог сообщение и открывает новый файл с инкрементальным префиксом. И так по кругу. Например

handlers= java.util.logging.FileHandler
java.util.logging.FileHandler.pattern = application_log.txt
java.util.logging.FileHandler.limit = 50
java.util.logging.FileHandler.count = 7
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter


создаст вот такие файлы (последняя колонка — размер в байтах)

application_log.txt.0                                                │ 0
application_log.txt.1                                                │ 79
application_log.txt.2                                                │ 79
application_log.txt.3                                                │ 676
application_log.txt.4                                                │ 87
application_log.txt.5                                                │ 114


Мы указали максимальный размер 50 байтов, в реальной жизни надо скорее указывать не меньше мегабайта, например вот так (я знаю, что 1000000 это чуть меньше мегабайта, но кому охота по памяти писать 1048576, если суть дела это фактически не меняет)
java.util.logging.FileHandler.limit = 1000000


В примере, как мы видим, файлы получились больше 50 байт потому что размер по сути округляется вверх до последнего целого лог сообщения. Т.е. если Вы укажете размер 1 байт и запишете лог сообщение размером в 1000 байт то размер файла станет 1000 байт и после этого лог сообщения файл закроется и откроется следующий.

copy & paste конфиг для реальной жизни, его вполне хватает для большинства service, console и desktop приложений.

handlers= java.util.logging.FileHandler

java.util.logging.FileHandler.pattern = application_log.txt
java.util.logging.FileHandler.limit = 1000000
java.util.logging.FileHandler.count = 5
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter


Последняя часть магии

Ну и последнее о чем осталось рассказать — как собственно сконфигурировать логер из файла свойств. Есть два способа:
  1. Из командной строки запуска приложения
  2. В первых строчках кода Вашего приложения


Первый чуть более правильный ибо он декларативный и работает сразу, до того как начал работать код Вашего приложения.
Вот так

java Djava.util.logging.config.file=logging.properties com.dataart.application.ClassName

Но к сожалению менять строку запуска не всегда можно или не всегда удобно. Второй способ тоже неплохо работает.


    public static void main(String[] args) {
        try {
            LogManager.getLogManager().readConfiguration(
                    MainApplicationEntryClass.class.getResourceAsStream("/logging.properties"));
        } catch (IOException e) {
            System.err.println("Could not setup logger configuration: " + e.toString());
        }
        .....


  • Здесь MainApplicationEntryClass — это класс — точка входа в Ваше приложение, видимо имя класса у Вас будет другое
  • Сам файл logging.properties как правило в таких случаях кладется в корень иерархии классов и выглядит это например вот так



Что осталось за кадром

В реальной жизни как минимум половина всех Java приложений это web приложения. Сама техничка логирования в них совершенно не отличается от изложенного выше. Ну может быть за тем исключением что разные сервера приложений могут использовать разные библиотеки логирования такие например как:
  • Log4J
  • JULI logger (строго говоря это не вполне самостоятельный фреймворк, а своего рода надстройка над java.util.logging)
  • SLF4J
  • Commons Logging


Соответственно несколько отличается настройка и имена методов. Но сам принцип меняется мало. Конкретные особенности как правило хорошо описаны в документации на сам сервер приложений, например
Denis Tsyplakov @Semenych
карма
182,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (26)

  • 0
    > Выводим лог сообщения куда-то еще

    Здесь еще можно добавить, что возможно логирование в syslog. Что довольно удобно в некоторых случаях.
    Например вот так:
    blog.dutchworks.nl/2010/01/14/logging-to-the-syslog-from-a-java-application/
    • +1
      Не в статье которая по большому счету заточена для начинающих про syslog я писать поостерегусь. Ибо все таки "/dev/log" есть не у всех.
  • –2
    За статью спасибо!
    Я раньше такие статьи заставлял junior-ов писать :-)
  • +6
    Тема не раскрыта, ИМХО. Больше всего недопонимания у народа, которым я объяснял логгинг в таких фремворках — вызывает матрица логгеров и аппендеров. И это как раз самая базовая и самая мощная фича. Про это надо в первую очередь рассказывать.

    Фразы java logging best practices и java.util.logging framework не сочетаются — опять ИМХО. java.util.logging framework не рекомендуется к использованию (по многим причинам), а вместо нее нужна обертка типа slf4j, и реализация поприличнее, чем JUL — тогда можно говорить о best practices. Иначе в реальной жизни оно так-же применимо, как Hello World.
    • 0
      См начало статьи:
      Весь код примеров использует java.util.logging framework. Вопрос «Какой из фреймворков логирования ниболее кошерен» я оставлю за кадром. Скажу только что до java.util.logging проще всего дотянуться ибо он уже идет вместе с JRE и на самом деле рассказанное в данной статье с минимальными
      И поверьте писать код вполне можно не понимая матрицу логеров и апендеров. Достаточно чтобы хотя бы один человек в команде это понимал.
      • 0
        сорри, промазал с ответом. См. чуть ниже.
  • +1
    Я это видел, конечно. И код писать можно. Но на best practices оно не тянет — максимум на beginners tutorial, чтобы научиться делать хоть как-нибудь, а потом уже переучиваться делать правильно (так почему сразу не учить, как делать правильно?).

    Хотя, пожалуй, я слишком придираюсь.

    В общем, статья написана очень неплохо. Если добавить пару вещей — (1) ту самую матрицу логгеров/аппендеров, и (2) объяснить, что хотя для примеров используется JUL из коробки, в реальной жизни его применять не стоит по ряду причин (мешанина логгеров в сторонних библиотеках требует использования обертки над логгером и перенаправления части логгинг фреймворков в один реально используемый; ограниченность возможностей JUL; неудобство использования в ряде мест — довольно критично, на самом деле) — тогда будет ок.
    • 0
      Не флейма ради. Я достаточно давно (фактически с момента выхода 1.4) использую JUL. Вроде пока больших проблем на замечал, ну кроме иногда необходимой некоторой магии с настройкой логеров сторонних библиотек.
      Тут хочется ехидно спросить «что я делаю не так». Но на самом деле действительно интересно, что я упускаю? Может быть в моих приложениях есть какая-то засада о которой я не знаю?
      В принципе то и Tomcat на JUL перешел.
      • +1
        Не флейма ради, а токмо пользы для :-)

        Например, с JUL мы пишем:

        log.fine( "Let's display some object in debug: " + myObject );


        Но конкатенация строк при выключенных отладочных логах может убить производительность, поэтому мы пишем так:

        if( log.isLoggable( Level.FINE ) ) {
        log.fine( "Let's display some object in debug: " + myObject );
        }


        Но если функция prepareCPUConsumingLogMessage() вернет null, всем будет плохо. Поэтому мы пишем так:

        if( log.isLoggable( Level.FINE ) ) {
        log.fine( "Let's display some object in debug: " + (myObject != null ? myObject.toString() : "null") );
        }


        Теперь сравним с SLF4J:

        log.debug( "Let's display some object in debug: {}", myObject );


        Профит:

        1. Значительно более компактно и понятно.

        2. Проверка logLevel может быть опущена в большинстве мест, кроме самых критичных по быстродействию, и тех, где вызывается prepareCPUConsumingLogMessage().

        3. Проверка нулей выполняется за вас — это очень важно, так как зачастую из-за этих нулей включение логгирования может прервать выполнение функции (которая сама по себе может и была защищена от нулей, но не в отладочном сообшении).
        • 0
          Поторопился —

          Но если функция prepareCPUConsumingLogMessage() вернет null,

          нужно читать как

          Но если объект вернет myObject,
          • 0
            вот блин… ну почему нельзя комменты редактировать?!

            нужно читать как

            Но если объект myObject вернет null
          • +1
            Спасибо, я понял. Это да. Перегруженных методов у стандартного логера не хватает.

            Возможно то, что меня устраивает JUL является следствием приверженности бритве Оккама и моего стиля написания лог сообщений.
            На конкатенацию двух-трех строк я обычно не обращаю внимания, особенно если где-то рядом есть обращение к БД или сетевой обмен.
            По поводу null
            public class TestNull {
                public static void main(String[] args) {
                    String s = null;
                    System.out.println("!"+s);
                }
            }

            выведет
            !null

            В целом согласен — сочетания Formatter и логера в одном флаконе весьма удобно.
            На самом деле к JUL у меня есть две серьезные претензии
            1. отсутствие контекстного логирования например /псевдокод/
            log.context(remoteHostAddress).log("Transport packet received:\n" + Util.dumpBytes(packetBytes));

            делаем так, а потом собираем по всем логерам срез относящийся только к данному хосту. Как мне помнится Log4J такое позволяет
            2. LogBags — это когда мы выделяем одну условную единицу обработки например запроса и пишем туда все подряд с разными уровнями. Но в лог файл это попадает только если максимальный уровень в LogBag не меньше указанного, например warning. Так мы имеем не очень большой файл логов, но зато все-все сообщения включая уровень finest.

            По молодости я пытался что-то такое использовать, но потом плюнул ибо заставить всю команду ровно и консистентно писать в лог не реально. А если даже один джуниор начинает писать как попало (например забьет на контекст), все эти стройные штуки ломаются нафиг. И сейчас остановился на следующей позиции. Да JUL не идеален, иногда он громоздок, но он стандартен и проще один раз научить всех им пользоваться, чем мучиться с солянкой из разных логеров в головах разработчиков.
            Мне к сожалению довелось видеть проект в котором 3 разных разработчика, каждый использовали свой и только свой, единственно рассово-верный логер, периодически рефакторя код друг друга :-)
            • 0
              logbags на самом деле не обязательно. Если таки использовать context (по крайней мере, в критических участках), то например, logback позволяет с помощью MDCFilter-a логгировать отладочные сообщения только от одного залогиненного пользователя (или по какому другому критерию), оставляя всех остальных пользователей в INFO.

              Можно даже прикрутить такую штуку без использования контекста, если есть на что опереться в thread local storage, чтобы получить ассоциацию с конкретной сессией.

              В JUL (и ему подобных вещах, в свое время полу-скопированных Sun-ом «ради стандартизации»), меня раздражает то, что они не развиваются.

              Спасибо за подсказку с конкатенированием строк — тут меня С/С++ бэкграунд подводит — у меня фобия насчет нулевых указателей :-).
              • +1
                logbag нужен вот для чего — например (почти реальная ситуация) у меня приходят пакеты по сети, я их сложным образом потрошу и раздают другим частям системы. Каждый пакет может порождать скажем 500 потенциально полезных сообщений. Пакеты валятся со скоростью 10 штук в сек. Общий максимальный объем текста логов на один пакет скажем 50-100 кб.
                Система mission critical и при взрыве неприятностях с одной из 200 установок у меня будут спрашивать каждую мелочь, когда получил, что было в полученном пакете, когда обработал, какой пакет (в байтах) по сети отдал и когда, когда получил подтверждение о том, что дошло и какие байты были в подтверждении.
                Контекст безусловно рулит позволяет отфильтровать относящееся к конкретной установке (по позывному), но блин за сутки это ~20Гб текста, а историю надо хранить хотя бы за три месяца.
                Конечно есть log rotate + gzip и все такое, но это безумство логов тоже как-то хочется ограничить

                В этой ситуации очень удобна система при которой нормально пишутся только info сообщения и все-все сообщения если при обработке пакета был хотя бы один warning.

                Но, надо сказать, что проект сильно не типичный, обычно такого геморроя нет и хватает обычных средств :-)
                • 0
                  да, интересный сценарий — запись всего только в случае ошибки — это очень полезно
  • +1
    Хорошая вводная статья. Хотелось бы обратить внимание на еще один, архи-важный момент: логирование в библиотеках отличается коренным образом от логирования в конечном приложении.

    В идеале библиотека (коих в приложении может быть больше чем одна штука) не навязывает мне использование какой-либо конкретной реализации логирования, а пользуется нейтральным интерфейсом, оставляя выбор реализации за конечным приложением (именно эту возможность дают SLF4J и динозавр commons logging).

    И уж совсем печально видеть порой библиотеки, которые не только берут ответственность в выборе реализации логирования на себя, но вдобавок еще и перенимают на себя всю конфигурацию.

    Пожалуйста, избегайте подобные фокусы. Помните, каждый раз, когда вы компилируете такой код, где-то умирает маленький котенок.
    • +1
      Согласен, момент весьма тонкий. К счастью джуниоры (основная аудитория статьи) не часто пишут библиотеки. Да и чего уже там греха таить, в нашей коммерческой практике очень редко (к моему сожалению) приходится писать библиотеки для широкого использования. Честно говоря я вообще ни одного такого проекта не помню.
      Да, делать jar для своего же проекта — это часто. А вот так чтобы «для кого-то не знаю для кого и какой у него будет логер» — даже не слышал чтобы у нас кто-то такое заказывал. Увы!
      • 0
        Но согласитесь, даже для проектов внутри Вашей организации: сегодня вы знаете, что везде используется java.util.logging, но что будет завтра — этого не знает никто. Вот захочется вам через пару лет 10 кратного перформанса или особой реализации с учетом специфики Java EE или OSGi или что-то еще… а у вас везде JUL.
        • 0
          Ну у нас не in-home а outsoursing, так что все достаточно стабильное и относительно короткоживущее. Ну и опять же исходники всегда есть.
          Опять же если смотреть правде в глаза в 50% случаев logging framework нам дан как свыше заказчиком. В оставшихся 50% продиктован религиозными предпочтениями тим лида.
          Я например всегда исповедовал принцип «делай как проще, все равно рано или поздно жизнь изменится и придется переделывать, а простое переделывать проще и не так обидно».
  • 0
    The Simple Logging Facade for Java or (SLF4J) -> s/SL4J/SLF4j/g
  • 0
    Статья неплохая, но увы! — в ней разбираются только простейшие практики организации и настройки логирования. При этом за кадром остались, возможно, самые принципиально важные вещи: что, когда и как логировать.
    Было бы очень интересно, например, узнать опыт DataArt в части
    — шаблонов строк лога (чтобы grep-апь потом было легче)
    — правил, регламентирующих обязательность логирования того или иного (например, исключения должны попадать в лог всегда или только при каких-то определенных обстоятельствах)
    — взаимоотношения try-catch-throw и практик ведения логов (например, логировать ли что-нибудь при перевыбросе исключений)
    — подходов к разруливанию «вложенного логирования» (когда часто используемый метод-утилита что-то логирует, и это иногда помогает, а иногда мешает)
    — одновременного ведения нескольких логов (разбиваемых, например, по уровню) — хорошо это или плохо, помогает или нет и т.п.
    — вопросов синхронизации лога на клиентской и серверной сторонах приложения

    ну и других подобных аспектов.
    • 0
      Таки да, это такая специальная статья в которой разбираются только основы. Ибо такой специальной статьи от других авторов я найти не смог, все норовят матрицу логеров разобрать или еще что-то такое замороченное.
      Нам была нужна простая статья про простые вещи. Вот она.
      • 0
        Возможно то, о чем я упомянул, — это не самые «простые вещи», но определенно необходимые любому новичку (да и не только), потому что с ними он столкнется ровно в тот самый момент, как только начнет использовать логгер.
        Понимаю, что такую готовую статью найти трудно — потому и поинтересовался опытом DataArt. :)
        • 0
          см ниже, вопрос имеет много ответов и писать на эту тему статью на хабре не хочется. Может быть в корпоративном блоге.
  • 0
    Странно что никто ещё не написал. Сообщение об ошибке всегда должно быть осмысленным. Нельзя писать
    } catch (Exception ex) {
    log.log(Level.SEVERE, "Exception: ", ex);
    }

    Это то же самое что писать комментарии в стиле
    // Create new long varibale and assign current timestamp to it
    Long time = new Long(System.currentTimeMillis());

    То что это Exception и так видно, что именно сломалось-то не понятно! Реальность такова, что в проектах всегда есть Exception'ы и когда что-то сломалось нужно уметь отделить в логе те, которые имеют отношение к поломке. Сравните, Вы видите в логе
    ERROR 2011-10-13 16:43:56,643 [ProxyUtils] Exception: NumberFormatException

    Относится ли это к проблеме, ради которой Вас разбудили в 4 утра? И сравните с таким логом:
    ERROR 2011-10-13 16:43:56,643 [ProxyUtils] Failed to parse user's input: NumberFormatException
    ERROR 2011-10-13 16:43:56,648 [ProxyUtils] Failed to load proxy configuration: NullPointerException

    Сразу понятно какая из строк относится к проблеме «всё сломалось, сайт не работает», не так ли?
    • 0
      Ну в оригинале было
      log.log(Level.SEVERE, «Bla-bla-bla: », ex);

      Так что ничего удивительного :-)

      Боюсь, что если я напишу что и как надо логировать и какой текст лучше писать, это будет очередной holy war. Не уверен, что конструктивная дискуссия на эту тему получится.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.