Pull to refresh

Comments 63

Не очень понятно, какую задачу вы решаете, и зачем вам для этого числовые коды. Чтобы "ориентироваться, откуда сообщение появилось" есть call stack и caller information. Для всего остального есть структурное логирование.

call stack и caller information
— это если речь об ошибках? Не всегда сообщения являются ошибками и отражаются в логах. Обратите внимание, я говорю о сообщениях, а не только об ошибках.

Зато теперь очень просто общаться с пользователями:
— Назовите код сообщения?
— 172.
— Ок. Причина в следующем…

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

Я предпочитаю ставить обращение ко мне пользователей на «числовые» рельсы, чтобы было минимум субъективности. Да и пользователь, когда видит код сообщения, то у него больше уверенности, что это сообщение имеет чёткое объяснение и он его в 99% случаев точно получит.

Это моё субъективное отношение. Я ведь не против остальных методов.

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

Нет, это если речь о любой записи.


Зато теперь очень просто общаться с пользователями:

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


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


А ведь тексты сообщений бывают и одинаковые.

Ровно с той же вероятностью, с которой бывают одинаковые числовые коды.


Однако считаю, что начать текст сообщение надо с чего-то конкретного.

Зачем? Это вопрос, с которого я начал, и я считаю его очень важным: зачем надо начинать текст сообщения "с чего-то конкретного"?


И описанным методом сгенерировать такое число, надеюсь, будет очень просто.

… а еще эти коды можно генерить полностью автоматически во время сборки приложения.

а еще эти коды можно генерить полностью автоматически во время сборки приложения
Муторно будет с разными версиями приложения, особенно в разных ветках. Сделать же код ошибки «стабильным» — тяжело.

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

А это зависит от модели развертывания в первую очередь.


Можно добавлять в код номер сборки, но там свои заморочки.

Да нет, достаточно любой разговор с пользователем начинать с фразы "назовите точную версию из About".

А это зависит от модели развертывания в первую очередь.
Так я и говорю, муторно.

достаточно любой разговор с пользователем начинать с фразы «назовите точную версию из About»
Это если у приложения есть About. Это дополнительная нагрузка на (вероятно) технически неграмотного юзера. Это необходимость разработчику открыть релиз систему -> найти версию кода по версии билда -> каким-то образом найти сообщение, которое получило наш код. Муторно это.

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

то разработчик по номеру отдаст ответ не по той версии, и он не подойдет

Во-первых, с чего вдруг смысл сообщения меняется от номера версии приложения. Очень плохо.

Во-вторых. Ответ может не подойти даже если вы говорите об одной и той же версии. Неужели тупик?
Во-первых, с чего вдруг смысл сообщения меняется от номера версии приложения.
Друг, мы тут автоматику обсуждаем. Авто-присваивание кодов. И в такой ситуации очень непросто соблюсти условие, когда одно конкретное сообщение будет иметь постоянный код в разных версиях.

lair говорит правильные вещи (в контексте данной ветки).
Лично мне совсем не очевидна эта проблема. Может вам немного развернуть ответ? Лично я на первый взгляд считаю, что автогенерация кодов сообщений сама по себе плохая идея, но, возможно, что я понимаю её в другом контексте, чем вы. Вы не уточните?
Представь себе проект, условный Web API. 10 контроллеров, 20 сервисов, 30 репозиториев. Всего 100 исключений на проект. Запустили мы релиз 1.0, все исключения во время билда получили свой номер. То есть, в кодовой базе эти номера не отразились.

Приходит клиент, хочет фичу. Мы ввели новый контроллер, сервис, перименовали один из старых. Билдим релиз 2.0. Нумерация исключений поменялась. Если раньше исключение в ImportantController имело номер 32, сейчас оно имеет номер 43.

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

Какие выходы? Либо зашивать в апп номер версии (через конфиг или константу) и вставлять его в сообщения, либо зашивать коды в VCS. Номер версии автоматический, не сильно упрощает гуглеж кодов, удлиняет коды, может быть полезен при логах. Зашивка кода в VCS тоже может породить конфликты при слияниях, автоматизируется при желании, придется продумывать вопросы вроде «что делать, если мы немного изменили сообщение?».
Я не очень понял в чём тогда логика? Вот прямо сейчас я ищу ошибки так: image

Если код сообщения меняется во время билда, то как мне искать причину в исходниках, если указанная мной цепочка разорвана?
Зашивка кода в VCS тоже может породить конфликты при слияниях
Попробую предложить начальное административное решение, что перед началом работы каждому участвующему сотруднику выделить диапазон кодов — тебе 0-1000, тебе 1001-2000 и т.д. Поверьте, тыщу кодов можно дооолго «расходовать». Я полтора года пишу одну программу и у меня дошло до 480. Кончился диапазон — вот тебе новый. Понятно, что не панацея, вон у микрософта какие коды. Но мы ведь не windows пишем.))) Хотя от этого утверждения качество не должно быть на последнем месте.
Если код сообщения меняется во время билда, то как мне искать причину в исходниках, если указанная мной цепочка разорвана?
Зашиваем в код ошибки номер версии. Вместо "_267" будет $"{version}{separator}_267". Дальше пишем тулзу, которая по номеру сборки определяет commitID, скачивает нужную версию репозитория и открывает IDE в файле, где найден код _267. Решение сильно спорное!

Вопрос: если оно спорное, почему о нем зашла речь?

Представьте себе, у вас есть ошибка: "_267 Не могу удалить документ 12345". На следующий релиз она превращается в "_267 Не могу удалить документ {documentId}". Затем вы добавляете "_267 Не могу удалить документ {documentId}. Причина: {reason}." Через полгода кто-то добавляет "_267 Модуль {module} не может удалить документ {documentId}. Причина: {reason}."

Еще через два года кто-то пытается решить баг и видит сообщение "_267 Не могу удалить документ 12345". Открывает IDE, ищет код _267, и удивляется: ни слова о модуле, никаких данных о причине.

Или сценарий: открывает ваш коллега IDE, ищет _267 — а такого кода нет. Удалили его, после миграции на новый движок.

Автокоды с зашивкой версии такие сценарии покрывают. Но ручная вставка кодов с настройкой логера (чтоб логер версии сам вставлял) будет проще.
Представьте себе, у вас есть ошибка: "_267 Не могу удалить документ 12345". На следующий релиз она превращается в "_267 Не могу удалить документ {documentId}". Затем вы добавляете "_267 Не могу удалить документ {documentId}. Причина: {reason}." Через полгода кто-то добавляет "_267 Модуль {module} не может удалить документ {documentId}. Причина: {reason}."
Ну это совсем неправильно!!! Если вы решили поменять сообщение, то меняйте и код! Почему вы решили поменять сообщение и не поменять код?! (Понимаю, что видение использования инструмента бывает разная у всех, но не сочтите за придирку, что я считаю своё предложение очевидным — меняете смысл операции — меняете код).
Нет цели экономить коды. Пишите новые. Поменяли сообщение — берите новый код.(если считаете, что действительно смысл сообщения поменялся).
А вот добавить номер версии не проблема — я предлагал кастомизировать функцию mfem() на свой вкус. Это также означало, что в неё можно зашить и номер версии.
Ну это совсем неправильно!!!
Ну зачем же так?

Если вы решили поменять сообщение, то меняйте и код!… Поменяли сообщение — берите новый код.(если считаете, что действительно смысл сообщения поменялся)
Кажется, вы слегка изменили свою позицию, пока писали ответ. Я позволю себе продолжить: «для анализа вообще багов неважно, как часто вы ренумеруете коды, если ваш код привязан к номеру коммита». Можете заводить новый код хоть в каждом коммите — главное, чего вы добиваетесь:
  1. По номеру ошибки восстановить кодовую базу
  2. Найти файл с кодом ошибки в этой базе.


Но это верно только для анализа багов. Для других участников вашей команды больше кодов — больше проблем.
  1. Чем больше информации вы зашиваете в код, тем сложнее юзерам диктовать вам эти коды. Чувствуете разницу между "_267" и «12.34.12.34545~~~_267»?
  2. Надо думать о документации.
  3. Насколько я представляю, иметь стабильные коды лучше для саппорта: «О, у вас ошибка 403? Сейчас мы с вами перелогинимся, и все заработает!»


Получается цепочка:
  1. Для разбора багов хочется иметь инструмент навигации по сообщениям об ошибках. Чтоб получил баг-репорт с ошибкой — и сразу нужный файл открыл.
  2. Для этого каждое сообщение мы нумеруем.
  3. Поддерживать нумерацию стабильной — трудно. Изменения логики и рефакторинг могут изменить нумерацию.
  4. Приходится мириться с нестабильностью нумерации. Чтобы выполнять пункт 1, начинаем искать способ определения не только файла с ошибкой, но и его версии.
  5. Нумерация ошибок может помочь не только в поиске багов. Идеал — держать нумерацию стабильной.
Кажется, вы слегка изменили свою позицию, пока писали ответ.
Вы правильно заметили, только связали не с тем фактом. Изначально была задача получить сквозную нумерацию. Нумерация ошибкой и исключений в программе — это просто пример. Поэтому я не меняю свою точку зрения, а подгоняю этот лайфхак под возникающие изменения в задаче нумерации ошибок. Изначально-то не стояло задачи нумерации ошибок.
Вполне может быть, что нумерация ошибок в таком виде хорошо годится для одномодульного приложения, которую пишет один программист и вне этого кейса уже требуется доработка такого подхода. Кроме того предложенный способ внедрения кода сообщения не годится для автоматической обработки исключений. Там нужен другой подход. Да, нужно приложить усилия, чтобы адаптировать такой подход. Но хотя бы есть какой-то элементарный бесплатный способ сквозной нумерации, который не требует каких-то внешних библиотек и сред для несложной утилиты или сервиса, микросервиса. Вот когда потребуется что-то более сложное — ок. Тогда объективно инструмент не подходит и вы берёте что-то тяжеловеснее.
Во-первых, с чего вдруг смысл сообщения меняется от номера версии приложения.

С того, что логика приложения меняется.

Муторно будет с разными версиями приложения
Рад, что мы друг друга поняли.

Можно «зашить» версию в код ошибки. Будет код ошибки вроде "${code}~~~{version}###{message}", где code=$"{fileNumber}.{line}". Тогда не нужны About, нет нагрузки на юзера, можно автоматизировать открытие нужного файла по коду ошибки, можно зашить стек как цепочку кодов. Только вот кто даст времени\денег на такое удовольствие?
Будет код ошибки вроде "${code}~~~{version}###{message}", где code=$"{fileNumber}.{line}".

Возьмите уже структурное логирование.


Только вот кто даст времени\денег на такое удовольствие?

Тот же человек, которому нужно сокращение расходов на поддержку. Если такого нет, то и с кодами можно не заморачиваться.

Только вот кто даст времени\денег на такое удовольствие?
Тот же человек, которому нужно сокращение расходов на поддержку. Если такого нет, то и с кодами можно не заморачиваться.

Разумно.
У вас больше опыта работы с большими проектами. Оцените примерно затраты на код\тестирование\согласование для 2 вариантов: авторского и с автогенерацией кодов (пускай даже Guid-ов).

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

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

зачем надо начинать текст сообщения «с чего-то конкретного»?
А с чего вы советуете начать?

«Критикуешь — предлагай», помните такой принцип?
Вы ведь не возражаете против такого неотъемлемого права пользователя на получение дополнительной информации?

Я, для начала, возражаю, что это "неотъемлемое право".


А с чего вы советуете начать?

Я советую начать с конкретной формулировки задачи, которую вы решаете. Я, собственно, с этого начал свой первый комментарий, и так и не получил ответа до сих пор.

Я, для начала, возражаю, что это «неотъемлемое право».
А я вот против вашего возражения. У пользователя есть такое право. Ведь пользователем может являться и ваш коллега, который вместе с вами пишет код за соседним компом и может у вас спросить, «что за сообщение с номером 172?» Вы предлагаете отказать ему в ответе?

Я советую начать с конкретной формулировки задачи, которую вы решаете.
Вы точно уверены, что не понимаете задачу?
У пользователя есть такое право

Где и кем оно зафиксировано?


Вы точно уверены, что не понимаете задачу?

Да, совершенно.

Где и кем оно зафиксировано?
Оно зафиксировано в маленькой кнопочке, которую обычно встраивают в программу. Называется она по разному, например, «Оставить вопрос», «свяжитесь с нами», «support» и т.д. Не?
Тут как в ресторане — лучше пусть пользователь расскажет вам, что у него есть вопрос, чем расскажет своим знакомым, какая у вас плохая программа. Маркетинг, рынок и всё такое. Хотите, чтобы вашу программу покупали — не нарушайте права пользователей на получение ответов на их вопросы. Да, бывают, скажем так, альтернативные пользователи, которые хотят иногда странного. Но они бывают везде. И поэтому для достижения результата с максимальной скоростью нужны цыфры. Так проще.

Вы точно уверены, что не понимаете задачу?

Да, совершенно.

Ok. А какую проблему c вашей точки зрения я решаю? Давайте начнём с того, что вы поняли из того, что прочитали?
Оно зафиксировано в маленькой кнопочке, которую обычно встраивают в программу.

Это не право. Это возможность, которую разработчик решил предоставить своим пользователям (или решил не предоставлять). И если решил предоставлять — то на тех условиях, на которых он счел нужным.


А какую проблему c вашей точки зрения я решаю?

Понятия не имею.


Давайте начнём с того, что вы поняли из того, что прочитали?

Я об этом уже писал в первом комментарии. Вы пишете: "при получении очередного сообщения в runtime стало уже трудно ориентироваться откуда оно появилось". Для этой проблемы (в C#) есть типовые решения, которые вы игнорируете.

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

Это два разных ответа, вообще-то.


Понятия не имею

Это первый, на вопрос "А какую проблему c вашей точки зрения я решаю?"


Вы пишете: "при получении очередного сообщения в runtime стало уже трудно ориентироваться откуда оно появилось".

А это — второй, на вопрос "что вы поняли из того, что прочитали?"

Я знаю что я написал. Вы не ответили на вопрос, что вы поняли из того что прочитали. Вы просто скопировали текст. Это совершенно не объясняет того, что вы поняли.

Отнюдь. Я вполне ответил на этот вопрос. Если вы чего-то из моего ответа не понимаете...

Я так понимаю, что вы не решаете задачи, а просто выражаете своё мнение, что вам что-то непонятно. У вас есть право озвучить, что вам что-то непонятно. Однако. Неправильно рассчитывать на то, что если вы сами не будете прикладывать усилий, чтобы понять, то ожидать, что по какой-то причине другой человек вам обязан что-то разжёвывать и доказывать — это ваше личное, глубокое заблуждение.

Лёгкий троллинг иногда приемлем, но не надо выходить за рамки приличия.

Вам же нечего сказать по делу? Не так ли? Если есть, то будьте любезны находиться в рамках технического обсуждения, а не переходить на личности.
Я так понимаю, что вы не решаете задачи

Вы понимаете неправильно. Я как раз обычно решаю задачи, просто прежде чем их решать, я уточняю, какую же задачу надо решить.


Вам же нечего сказать по делу?

Ровно наоборот. Мне вполне есть, что сказать "по делу", и я даже уже сказал приличную часть из этого.


будьте любезны находиться в рамках технического обсуждения

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

просто прежде чем их решать,
Я УЖЕ решил задачу. И меня решение устраивает. Вы хотите убедить меня, что оно не устраивает вас? Ок. Продолжайте искать своё решение. Я не против.
Техническое обсуждение начинается с постановки задачи
Чёткой постановки задачи не было. Но это не значит, что нет задачи. Так понятнее?
Я УЖЕ решил задачу. И меня решение устраивает.

Это неудивительно.


Чёткой постановки задачи не было. Но это не значит, что нет задачи.

Но это может значить, что решали не ту задачу, которую нужно.

С этого бы и начали, что постановки задачи на было, к чему весь этот цирк на n комментариев?

А я с этого и начал в самом первом комментарии: "Не очень понятно, какую задачу вы решаете". И потом еще раз повторил: "Я советую начать с конкретной формулировки задачи, которую вы решаете".

Мой комментарий автору предназначался, видимо в мобильной версии промахнулся)
Как автор статьи я должен с уважением относиться ко всем пользователей, которые обратились с вопросом. И хотя бы быть вежливым. Всё-таки я давал обещание хабру в этом. )))
зачем надо начинать текст сообщения "с чего-то конкретного"?

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


Плюс коды ошибок проще гуглить. Вот всю прошлую неделю я пытался починить Sharepoint — и по русскоязычным сообщениям об ошибках я не мог найти ничего вразумительного. Под конец я даже, к стыду своему, начал переходить по ссылкам на qaru от отчаянья...

Локализация.

О, вот это реально хороший пойнт. Но он, как уже говорилось, прекрасно решается без человеческого участия, автогенерацией кодов при сборке приложения.


(впрочем, въедливости ради: этот пойнт опирается на подразумеваемую, но не зафиксированную ситуацию, при которой у нас информация поступает от пользователя, а не… другими, более эффективными способами)

Неа, автогенерация — плохая идея, ведь надо поддерживать согласованность кодов в разных версиях программы.

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

А вот это как раз предположение, что (а) согласованность поддерживать надо и (б) нет другого идентификатора, по которому можно поддерживать согласованность (девелоперского сообщения об ошибке, например). Эти предположения могут быть верными, но не всегда, и если они включены в задачу, их надо явно озвучивать.

Конечно же согласованность поддерживать надо. Когда одно и то же сообщение имеет номер E123 в одной версии программы, и номер E152 в другой — по этому номеру больше нельзя найти ничего полезного.


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

Когда одно и то же сообщение имеет номер E123 в одной версии программы, и номер E152 в другой — по этому номеру больше нельзя найти ничего полезного.

Ну то есть теперь нас еще и волнует возможность гуглить ошибки?


Но ладно, допустим. Что делать, если некое сообщение об ошибке потеряло актуальность? Никогда больше не использовать этот код?


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

Отнюдь. Если девелоперские сообщения стабильны, то есть больше одного способа сделать из них коды при сборке исходников.

Ну то есть теперь нас еще и волнует возможность гуглить ошибки?

Не теперь, а с самого начала. Перечитайте моё сообщение, с которого началась эта ветка.


Что делать, если некое сообщение об ошибке потеряло актуальность? Никогда больше не использовать этот код?

Да, это будет правильным решением.


Если девелоперские сообщения стабильны, то есть больше одного способа сделать из них коды при сборке исходников.

Например? Я вижу только один способ: хеширование. Но это мне кажется так себе затеей.

Не теперь, а с самого начала. Перечитайте моё сообщение, с которого началась эта ветка.

Я имею в виду, что это еще одно неявное требование, которое нуждается в фиксации.


Да, это будет правильным решением.

Интересно, как это гарантировать. Безотносительно способа генерации кодов, я имею в виду.


Например?

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


Но это мне кажется так себе затеей.

Почему?

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

А как избежать вставки очередного сообщения в середину списка?


Почему?

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

А как избежать вставки очередного сообщения в середину списка?

Использованием списка от прошлого билда.


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

Это зависит от процесса сборки, а не от того, как генерить код. Иными словами, эта проблема будет (или не будет) всегда, когда код сообщения генерится автоматически, а не задается разработчиком явно.


А ещё код сообщения теперь меняется от незначительных правок, вроде исправления опечатки.

Я же начал с того, что чтобы это работало, сообщения должны быть стабильны. Если они не стабильны, работать не будет.

Использованием списка от прошлого билда.

Ага, вот уже билды перестали быть воспроизводимыми.


Иными словами, эта проблема будет (или не будет) всегда, когда код сообщения генерится автоматически, а не задается разработчиком явно.

Именно так и есть.

Ага, вот уже билды перестали быть воспроизводимыми.

Зависит от того, как вы храните список от прошлого билда.


Именно так и есть.

… и это осознанный выбор между удобством в одном месте и удобством в другом.

Согласен с lair, но добавил бы ещё, что можно использовать разные типы exceptions (как стандартные, так и свои) для разных мест в коде, чтоб потом «ориентироваться откуда оно появилось». И вообще решение автора смотрится достаточно громоздко, запутанно, в команде из нескольких человек — будет вызывать конфликты.
можно использовать разные типы exceptions
Обратите внимание, что речь не только об Exceptions. Значит не все сообщения могут попасть в логи.

в команде из нескольких человек — будет вызывать конфликты
А если не использовать такого подхода, то конфликтов не будет? Это выходит за рамки технических решений. Но ведь чтобы конфликтов не было к ним нужно готовиться, а не применять разные методики без проверки на удобство или граничные условия.
Автоматизируй это, парень. С Roslyn проверяй, используется ли Message в исключении, и указывается ли в нем код. Если нет — показывай Warning, в предлагаемом фиксе добавляй значение к Enum, добавляй новое значение в Message. Тогда твои коды будут добавляться в Enum и конструктор исключения двумя клавишами: Ctrl+Enter, Enter.

Edge Case — когда сообщение генерируется не в конструкторе исключения, тут придется поработать.

Если будешь такое реализовывать\открывать — стучись, помогу.
Cпасибо. Буду иметь в виду.
Можешь какой-нибудь скрин сделать? Я с Roslyn не работал.

Я бы вообще хотел разработать такой шаблон кода, который бы мне сразу генерировал следующий номер, а не я его «выдумывал». Вообще был бы крутяк. Сейчас чуть глянул — может действительно Roslyn может решить эту задачу?
Я прямо прозрел! Спасибо за инфу!
Почему бы сразу не мелочиться и не использовать GUID-ы в качестве кода ошибок? Практически гарантированная уникальность, простота генерации (Tools — Create GUID), никаких коллизий в ветках.
— Юзер, назовите код ошибки.
— Дядя, там 36 символов!


В остальном — отличное решение, респект!

Сверните в base32, будет меньше 30.

Если Вы в состоянии посчитать количество символов в этом ужасе, то уж точно легко их назовёте.
простота генерации (Tools — Create GUID)

В решарпере сниппет есть, не выходя из кода вообще.

Простите, навеяло
2 15 42
42 15
37 08 5
20 20 20!
Sign up to leave a comment.

Articles