No Image

Как правильно читать логи

СОДЕРЖАНИЕ
6 просмотров
16 декабря 2019

Что такое логи?

Краткая справка из Википедии:

Файл регистрации, протокол, журнал или лог (англ. log) — файл с записями о событиях в хронологическом порядке. Различают регистрацию внешних событий и протоколирование работы самой программы – источника записей (хотя часто всё записывается в единый файл). Например, в лог-файлы веб-сервера записывается информация, откуда пришёл тот либо иной посетитель, когда и сколько времени он провел на сайте, что там смотрел и скачивал, какой у него браузер и какой IP-адрес у его компьютера.

Для чего нужны логи?

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

Как включить запись логов?

Обычно, по умолчанию, для экономии дискового пространства ведение логов на хостинге выключено.

Приведу включение записи на примере панели управления хостингом Таймвеб.

В панели управления переходим во вкладку «Логи», выбираем из выпадающего сайта нужный (если их несколько) и активируем ползунок «Лог доступа (access_log)»

Примерно через час, когда накопится достаточное количество записей, переходим в директорию сайта и скачиваем файл.

Открываем в любом текстовом редакторе (на примере второй столбец, с адресом сайта закрашен).

Разберем для примера строку № 49

site.ru – адрес нашего сайта

85.93.93.102 – IP-адрес посетителя, с датой и вренем посещения, а также его часовой пояс

GET – тип запроса, возможен вариант POST, чем отличается GET и POST можно почитать в сети, если говорить упрощено, GET – получение данных, POST – отправка, например авторизация

page/2 – к какой странице было обращение, в нашем случае это site.ru/ page/2

HTTP/1.0 – протокол, по которому пришел посетитель

200 – код ответа сервера, более подробную информацию о кодах состояния можно прочитать в Википедии – https://ru.wikipedia.org/wiki/Список_кодов_состояния_HTTP

80195 – количество байт полученных посетителем

Linguee Bot (http://www.linguee.com/bot; bot@linguee.com) – данные о посетителе, тут также может быть информация о браузере, операционной системе, устройстве и так далее

Даже при беглом взгляде видно, что с адреса 85.93.93.102 идет множество запросов, обращение было как раз по чрезмерной нагрузке на сайт, как только адрес бота Linguee Bot был запрещен, нагрузка практически сразу вернулась в норму.

И все за несколько минут, благодаря логам; без них на выяснение причины понадобилось бы гораздо больше времени. Также были замечены обращения по адресам, содержавшим вставки типа – xd0xbexd1x82xd0xb7 …

Кто занимается «лечением» сайтов, знает, что подобные запросы могут создавать запредельные нагрузки на сервер.

Иногда самые простые методы – самые действенные, а защита на уровне сервера самая надежная.

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

26 июля 2009 г.

Как читать лог-файлы

Стек вызовов в EurekaLog (текстовый вид)

Стек вызовов в EurekaLog (вид в EurekaLog Viewer)

Стек вызовов в FastMM

Стек вызовов в JCL

Стек вызовов в Delphi (View/Debug Windows/Call Stack)

Итак, как вы можете видеть, в какой бы утилите не был создан стек вызовов – он почти всегда содержит эквивалентные сведения (ну, в примерах выше приведены разные программы, так что не пытайтесь сравнивать эти стеки вызовов 😉 ).

Во-первых, обычно первым элементом в строке идёт сам адрес кода. Это числа вида 00488ABC или 488ABC (с обрезанными нулями). В стиле Delphi это будет выглядеть как $00488ABC – т.е. указатель на место в памяти, а именно: на код. Вы можете использовать эти адреса и вручную (например, если другая информация отсутствует): запустите программу, вызовите меню Search/Goto address (программа должна стоять на паузе) и введите $00488ABC – и вы попадёте ровно в это место (при условии, что адрес не изменился при перезапуске программы).

Почти всегда рядом с адресом указывается имя исполняемого модуля, которому он принадлежит. Например, Project8.exe или user32.dll . По этому признаку можно быстро отделить “свой код” от системного. Впрочем, сам вид адреса зачастую говорит об этом. К примеру, exe практически всегда загружается по адресу $400000 – следовательно, адреса вида $488ABC (близкие к $400000 ) почти наверняка принадлежат нашей программе. А вот системные DLL обычно загружаются по верхним адресам. Так что адреса вида $75B94911 скорее всего принадлежат не нам. Промежуточные адреса (типа $58C3C0 ) обычно принадлежат сторонним DLL (нашим или не нашим, но не системным). Окей, это весьма грубое и не совсем точное описание – но это всего лишь введение для новичков, так что не придирайтесь 😉

Иногда вместе с адресами указывается смещение относительно начала исполняемого модуля или сегмента кода – например 87A8C для адреса 00488A8C в логе JCL ( $00400000 + $1000 + $87A8C = $00488A8C ). Такая информация не слишком часто нужна, но, если вы понимаете в этом, то включайте (выше приведён полный вывод, а вообще он настраиваемый) – лишним не будет. Тут также может пригодиться дополнительная информация в лог-файле: список модулей с дескриптором каждого.

Но достаточно про эти сухие цифры – уж их-то вы будете использовать редко 🙂 Потому что при наличии отладочной информации, ассоциированной с этим адресом, рядом будет указана более дружественная информация. А именно: имя модуля (unit), класса, метода или функции и номер строки. Иногда часть информации может отсутствовать – это зависит от детальности самой информации. Мы уже обсуждали это здесь и здесь.

Собственно, эта информация говорит сама за себя: вот вам Delphi-вый модуль, вот вам метод и даже строка в нём, где возникла ошибка (для первой строки в стеке вызовов) или произошёл вызов подпрограммы (для всех остальных строк). Единственное, что требует комментария: номера строк. Часто вместе с абсолютными номерами строк (отсчёт от начала файла) указываются смещения в строках (и иногда даже в байтах) самой строки от начала процедуры, которой она принадлежит. Например, пусть у нас есть такой код:
Слева указывается обычная нумерация строк, как она есть в редакторе кода – абсолютная, от начала файла. Так вот, номера строк для DoActions2 может выглядеть как “ 48[2] ” (стиль EurekaLog) или “ Line 48, "Unit1.pas" + 1 ” (стиль JCL). Что следует читать как: это 48-я строка в файле или вторая строка в процедуре Test.

Читайте также:  Как подключить вай фай к старому телевизору

Зачем нужны эти относительные смещения? Ну, они чрезвычайно удобны при чтении стека вызовов, если ваши исходники поменялись. Например, вы добавили процедуру до процедуры Test. Сама процедура Test сместилась ниже на 27 строк. Понятно, что теперь строка 48 принадлежит совершенно другому коду. Однако, вы все ещё можете найти DoActions2 , если вы посмотрите на имя процедуры в стеке вызовов (“ Test ”) и отсчитаете 2 строки от её начала.

Кстати, EurekaLog также умеет смотреть в папку __history (доступна только в новых версиях Delphi), чтобы извлечь оттуда наиболее подходящую версию исходника для просмотра (это происходит когда вы дважды щёлкаете по строке в стеке вызовов в EurekaLog Viewer).

Вот несколько моментов, которые надо иметь ввиду, когда вы читаете стек вызовов.
Во-первых, в одном файле баг-отчёта может быть несколько отчётов об ошибках. Во-вторых, даже в одном отчёте может быть перечислено несколько стеков вызовов: по одному на каждый поток. Так что, прежде чем приступать к анализу стека, – убедитесь, что вы собрались читать нужный 😉

Как правило, все стеки вызова следует читать снизу-вверх: в самом низу находится процедура, которую вызвали первой. Выше – процедура, вызванная из стартовой, ещё выше – ещё один вложенный вызов и т.д., пока в самом верху вы не увидите текущее место. Часто это и будет именно место возникновения ошибки (но не всегда – см. ниже), а вся прочая часть стека вызовов показывает, как мы в это место пришли.

Первые (снизу) несколько процедур в стеке вызовов обычно являются системными или принадлежат RTL Delphi и, как правило, редко представляют интерес. Часто информация о них отображается лишь частично – ввиду отсутствия отладочной информации. Также иногда стек вызовов имеет ограничение в глубину, т.е. не может содержать более N элементов. В этом случае нижняя часть и вовсе обрезается.

С другой стороны, в верхней части стека вызовов кроме собственно места ошибки иногда могут идти упоминания и служебных процедур – это зависит от того, в каком месте (на какой стадии раскрутки) обнаруживается проблема и строится стек вызовов. К примеру, там могут упоминаться процедуры обработки исключений или вызовы GetMem и т.п.

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

Далее, надо также иметь ввиду способ, которым строится стек. Их два: по фреймам вызовов (frame-based) и raw-метод. Первый метод строит стек, опираясь на последовательность фреймов, которые добавляются в стек при большинстве вызовов процедур. Обычно этот метод даёт хорошие результаты, если только у вас нет большого числа коротких процедур – для них стековые фреймы, как правило, не создаются (хотя вы и можете это исправить, включая опцию “Stack Frames”). Благодаря тому, что метод смотрит только на информацию о реальных вызовах – он довольно точен и быстр, т.к. не смотрит всю информацию в стеке, а просто проходит по цепочке фреймов, каждый из которых ссылается на предыдущий.

Raw-метод работает иначе: он просто сканирует весь стек, пытаясь найти в нём адреса возврата. Действительно, создаётся или нет фрейм вызова – это вопрос. Но вот адрес возврата-то кладётся в стек всегда. Вот их-то и пытается найти raw-метод. Для этого он берёт каждое число в стеке и пытается определить: похоже это на адрес возврата или нет? Поэтому raw-метод применяется только в сочетании с некоторой эвристикой. В зависимости от качества этой эвристики, построенные стеки вызовов могут значительно отличаться. К примеру, можно смотреть на то, указывает ли значение на сегмент кода, есть ли для него отладочная информация и т.п.

К чему я вас этим гружу? Да к тому, что чтение стека вызова будет зависеть от того, каким методом он построен. И вам лучше бы знать это до того, как вы приступите к анализу отчётов.
Какая есть разница? Ну, по описанию можно и самому сообразить: frame-based метод может пропускать вызовы, если для них не создаются фреймы (как правило, это очень короткие процедуры) и вовсе застопориться, если хотя бы один фрейм был повреждён. С другой стороны, очевидно, что raw-метод во многом “агрессивнее” frame-based метода: уж он почти наверняка не пропустит вызов (если только он не отсёкся эвристикой), но может добавлять в стек вызовов лишние элементы – так называемые “ложные срабатывания”.

Поэтому, анализируя стек и видя странные вещи (“ну не может по моему коду эта процедура вызываться отсюда!”) – помните о методе, которым построен стек и либо предполагайте отсутствие метода в стеке (для frame-based), либо предполагайте лишнюю (ложную) запись (для raw). Особенно нужно помнить об этой разнице, если вы используете какой-либо метод получения стека вызова в самой программе – например, хотите получить имя вызывавшей вас процедуры. Не всегда (хотя и чаще всего) в стеке вызовов вызывающий будет следовать второй строкой.

EurekaLog версии 6 использует только raw-метод (поддержка frame-based планируется для версии 7). FastMM и JCL поддерживают оба метода, переключение между которыми осуществляется в опциях (последние версии по-умолчанию используют frame-based метод).

В основном тут хочется сказать такую вещь: ошибка не обязательно сидит в том месте, куда указывает стек вызова! Эту простую истину почему-то не понимает огромное количество народа.

Ну, например, у вас в программе есть один из этих проклятых багов порчи памяти: вы что-то делаете и попутно портите какую-то свою память. Код с багом может выполниться на ура, без ошибок. А ошибка возникнет много позже, при выполнении совершенно другого кода. У вас будет Access Violation, и стек вызовов будет указывать на совершенно невинный код.

Читайте также:  Как зайти в даркнет через обычный браузер

Просто анализируйте ситуацию, чтобы не свалить всё на ни в чём не повинный код.

Более того, некоторые типы отчётов возможно получить именно не в момент ошибки. Речь, конечно же, идёт не об исключениях (уж их-то мы ловим сразу), а о проблемах с памятью (утечки и порча памяти). Например, очевидно, что менеджер памяти не может проверять весь пул памяти на корректность при каждой операции в программе: “а уж не собирается ли вот эта инструкция кода сейчас затереть нужную память?” Более того, это невозможно даже для выполнения только во время обращения к менеджеру памяти (т.е. к GetMem / FreeMem ). Т.к. памяти выделяется много, управляющих структур ещё больше – и если сканировать их все при каждом обращении к менеджеру памяти, то производительность вашей программы упадёт до нуля.

Поэтому, менеджер памяти анализирует проблемы только по текущему контексту. Например, просим мы его освободить память: он проверит, уж не повреждён ли служебный заголовок у этого блока. Заметьте: только у этого, а не у всех. Далее, просим мы его выделить память: он пробегается по списку условно-свободных блоков и находит один. Прежде чем вернуть его вызывающему, менеджер памяти поверяет: а уж не писал ли кто чего в этот блок памяти, пока он был свободным?

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

Примеры? Ну, давайте посмотрим на FastMM (функциональность EurekaLog скромнее, поэтому с ней вы разберётесь по аналогии). Каждое сообщение FastMM начинается как “FastMM has detected an error during a ” – т.е. “FastMM обнаружил ошибку при ”. Далее указывается при какой же операции он нашёл ошибку: GetMem , FreeMem , ReallocMem или сканировании при поиске. Затем идёт собственно ошибка. Вот примеры ошибок:

  • “The block header has been corrupted”
  • “The block footer has been corrupted”
  • “FastMM detected that a block has been modified after being freed”
  • “An attempt has been made to free/reallocate an unallocated block”

Ну и другие, более редкие сообщения, типа вызова виртуального метода удалённого объекта. Смотрите: сообщения говорят о конкретных проблемах: FastMM обнаружил, что заголовок (header) или заглушка (footer) блока повреждены, запись в свободный блок или попытку повторного освобождения памяти. Ниоткуда не следует, что проблема произошла именно в этот момент! В текущий момент времени менеджер памяти просто обнаружил проблему. Сама проблема возникла ранее.

С целью помочь поймать проблему FastMM может добавлять в отчёт не только стек вызовов к текущему моменту, но и стек вызовов для последних успешных операций с блоком памяти (что означает, что проблема сидит где-то между двумя моментами). Например, если FastMM говорит о том, что он обнаружил, что свободный блок кто-то изменил, то это значит, что:

  1. Вы выделили блок (FastMM покажет этот стек вызовов).
  2. Вы освободили блок (FastMM покажет этот стек вызовов).
  3. Какой-то код испортил память, ошибочно записывая в этот свободный блок.
  4. Вы вызвали GetMem. В этот момент FastMM обнаруживает изменение в блоке и трубит тревогу (FastMM покажет этот стек вызовов).

В отчёте будет три (!) стека вызова для одной проблемы и ни один из них не будет представлять указание на проблему: проблема будет сидеть где-то между вторым и последним стеками вызовов.

Короче говоря: просто немного используйте свою голову (читайте сообщения, а не ломитесь проверять код по стекам вызовов) и всё будет отлично 😉

Как искать причину такой проблемы – об этом, в следующий раз.

Помимо стека вызовов, в баг-отчёте есть ещё и другая информация, которая является вспомогательной. Из неё можно извлечь подсказки к поиску причины ошибки. Наиболее важными частями является информация о CPU и регистрах, а также дампы памяти. По ним зачастую можно определить состояние переменных в момент ошибки и другие полезные сведения. Конечно же, оперирование с этой информацией требует знания ассемблера, но это действительно мощный инструмент.

Впрочем, дампы памяти могут помочь даже неподкованному в ассемблере программисту. Например, дамп утечки в FastMM расскажет о том, какие данные лежали в этой памяти. И увидев, скажем, строку вместо данных объекта, мы сможем понять, откуда она взялась и проверить связанный с ней код.

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

Ну, например, странный AV на, казалось бы, ровном месте при вызове функции. Посмотрев на поле версии ОС, вы увидите, что программа была запущена в Windows 2000, в которой вы её не проверяли. А копнув глубже, вы обнаружите, что проблема была в том, что используемая вами функция не существует в Windows 2000 (вы динамически импортировали функцию, не проверяя на ошибки).

Пора поговорить про удобную работу с логами, тем более что в Windows есть масса неочевидных инструментов для этого. Например, Log Parser, который порой просто незаменим.

В статье не будет про серьезные вещи вроде Splunk и ELK (Elasticsearch + Logstash + Kibana). Сфокусируемся на простом и бесплатном.

До появления PowerShell можно было использовать такие утилиты cmd как find и findstr. Они вполне подходят для простой автоматизации. Например, когда мне понадобилось отлавливать ошибки в обмене 1С 7.7 я использовал в скриптах обмена простую команду:

Она позволяла получить в файле fail.txt все ошибки обмена. Но если было нужно что-то большее, вроде получения информации о предшествующей ошибке, то приходилось создавать монструозные скрипты с циклами for или использовать сторонние утилиты. По счастью, с появлением PowerShell эти проблемы ушли в прошлое.

Основным инструментом для работы с текстовыми журналами является командлет Get-Content, предназначенный для отображения содержимого текстового файла. Например, для вывода журнала сервиса WSUS в консоль можно использовать команду:

Читайте также:  Как пользоваться ноутбуком для чайников

Для вывода последних строк журнала существует параметр Tail, который в паре с параметром Wait позволит смотреть за журналом в режиме онлайн. Посмотрим, как идет обновление системы командой:


Смотрим за ходом обновления Windows.

Если же нам нужно отловить в журналах определенные события, то поможет командлет Select-String, который позволяет отобразить только строки, подходящие под маску поиска. Посмотрим на последние блокировки Windows Firewall:


Смотрим, кто пытается пролезть на наш дедик.

При необходимости посмотреть в журнале строки перед и после нужной, можно использовать параметр Context. Например, для вывода трех строк после и трех строк перед ошибкой можно использовать команду:

Оба полезных командлета можно объединить. Например, для вывода строк с 45 по 75 из netlogon.log поможет команда:

Журналы системы ведутся в формате .evtx, и для работы с ними существуют отдельные командлеты. Для работы с классическими журналами («Приложение», «Система», и т.д.) используется Get-Eventlog. Этот командлет удобен, но не позволяет работать с остальными журналами приложений и служб. Для работы с любыми журналами, включая классические, существует более универсальный вариант ― Get-WinEvent. Остановимся на нем подробнее.

Для получения списка доступных системных журналов можно выполнить следующую команду:


Вывод доступных журналов и информации о них.

Для просмотра какого-то конкретного журнала нужно лишь добавить его имя. Для примера получим последние 20 записей из журнала System командой:


Последние записи в журнале System.

Для получения определенных событий удобнее всего использовать хэш-таблицы. Подробнее о работе с хэш-таблицами в PowerShell можно прочитать в материале Technet about_Hash_Tables.

Для примера получим все события из журнала System с кодом события 1 и 6013.

В случае если надо получить события определенного типа ― предупреждения или ошибки, ― нужно использовать фильтр по важности (Level). Возможны следующие значения:

  • 0 ― всегда записывать;
  • 1 ― критический;
  • 2 ― ошибка;
  • 3 ― предупреждение;
  • 4 ― информация;
  • 5 ― подробный (Verbose).

Собрать хэш-таблицу с несколькими значениями важности одной командой так просто не получится. Если мы хотим получить ошибки и предупреждения из системного журнала, можно воспользоваться дополнительной фильтрацией при помощи Where-Object:


Ошибки и предупреждения журнала System.

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

Подробнее почитать про работу обоих командлетов для работы с системными журналами можно в документации PowerShell:

PowerShell ― механизм удобный и гибкий, но требует знания синтаксиса и для сложных условий и обработки большого количества файлов потребует написания полноценных скриптов. Но есть вариант обойтись всего-лишь SQL-запросами при помощи замечательного Log Parser.

Утилита Log Parser появилась на свет в начале «нулевых» и с тех пор успела обзавестись официальной графической оболочкой. Тем не менее актуальности своей она не потеряла и до сих пор остается для меня одним из самых любимых инструментов для анализа логов. Загрузить утилиту можно в Центре Загрузок Microsoft, графический интерфейс к ней ― в галерее Technet. О графическом интерфейсе чуть позже, начнем с самой утилиты.

О возможностях Log Parser уже рассказывалось в материале «LogParser — привычный взгляд на непривычные вещи», поэтому я начну с конкретных примеров.

Для начала разберемся с текстовыми файлами ― например, получим список подключений по RDP, заблокированных нашим фаерволом. Для получения такой информации вполне подойдет следующий SQL-запрос:

Посмотрим на результат:


Смотрим журнал Windows Firewall.

Разумеется, с полученной таблицей можно делать все что угодно ― сортировать, группировать. Насколько хватит фантазии и знания SQL.

Log Parser также прекрасно работает с множеством других источников. Например, посмотрим откуда пользователи подключались к нашему серверу по RDP.

Работать будем с журналом TerminalServices-LocalSessionManagerOperational.

Не со всеми журналами Log Parser работает просто так ― к некоторым он не может получить доступ. В нашем случае просто скопируем журнал из %SystemRoot%System32WinevtLogsMicrosoft-Windows-TerminalServices-LocalSessionManager%4Operational.evtx в %temp% est.evtx.

Данные будем получать таким запросом:


Смотрим, кто и когда подключался к нашему серверу терминалов.

Особенно удобно использовать Log Parser для работы с большим количеством файлов журналов ― например, в IIS или Exchange. Благодаря возможностям SQL можно получать самую разную аналитическую информацию, вплоть до статистики версий IOS и Android, которые подключаются к вашему серверу.

В качестве примера посмотрим статистику количества писем по дням таким запросом:

Если в системе установлены Office Web Components, загрузить которые можно в Центре загрузки Microsoft, то на выходе можно получить красивую диаграмму.


Выполняем запрос и открываем получившуюся картинку…


Любуемся результатом.

Следует отметить, что после установки Log Parser в системе регистрируется COM-компонент MSUtil.LogQuery. Он позволяет делать запросы к движку утилиты не только через вызов LogParser.exe, но и при помощи любого другого привычного языка. В качестве примера приведу простой скрипт PowerShell, который выведет 20 наиболее объемных файлов на диске С.

Ознакомиться с документацией о работе компонента можно в материале Log Parser COM API Overview на портале SystemManager.ru.

Благодаря этой возможности для облегчения работы существует несколько утилит, представляющих из себя графическую оболочку для Log Parser. Платные рассматривать не буду, а вот бесплатную Log Parser Studio покажу.


Интерфейс Log Parser Studio.

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

Вторая особенность ― возможность экспорта запроса в скрипт PowerShell.

В качестве примера посмотрим, как будет работать выборка ящиков, отправляющих больше всего писем:


Выборка наиболее активных ящиков.

При этом можно выбрать куда больше типов журналов. Например, в «чистом» Log Parser существуют ограничения по типам входных данных, и отдельного типа для Exchange нет ― нужно самостоятельно вводить описания полей и пропуск заголовков. В Log Parser Studio нужные форматы уже готовы к использованию.

Помимо Log Parser, с логами можно работать и при помощи возможностей MS Excel, которые упоминались в материале «Excel вместо PowerShell». Но максимального удобства можно достичь, подготавливая первичный материал при помощи Log Parser с последующей обработкой его через Power Query в Excel.

Приходилось ли вам использовать какие-либо инструменты для перелопачивания логов? Поделитесь в комментариях.

Комментировать
6 просмотров
Комментариев нет, будьте первым кто его оставит

Это интересно
No Image Компьютеры
0 комментариев
No Image Компьютеры
0 комментариев
No Image Компьютеры
0 комментариев
Adblock detector