{ "cells": [ { "cell_type": "markdown", "id": "6bc66b73", "metadata": {}, "source": [ "# Клиент для отправки метрик #" ] }, { "cell_type": "markdown", "id": "454171d9", "metadata": {}, "source": [ "## Хранение метрик ##\n", "\n", "Если вы разрабатываете настоящий проект, у которого есть большое количество пользователей, то необходимо наблюдать за всеми процессами, происходящими в нем. Для этого нужно смотреть за численными показателями в проекте. Показатели могут быть самыми разными - количество запросов к вашему приложению, время ответа вашего сервиса на каждый запрос, количество пользователей в сутки, и т.д. Эти всевозможные численные показатели мы будем называть метриками.\n", "\n", "Для сбора, хранения и отображения подобных метрик существуют готовые решения, например `Graphite`, `InfluxDB`. Мы в рамках курса разработаем свою систему для сбора метрик - сервер и клиент.\n", "\n", "В этом блоке мы начнем с разработки клиента для отправки подобных метрик на сервер, где они хранятся, и могут быть запрошены в любой момент времени. Затем в качестве финального задания в шестом блоке вам будет предложено реализовать и сам сервер." ] }, { "cell_type": "markdown", "id": "08938207", "metadata": {}, "source": [ "## Протокол взаимодействия ##\n", "\n", "Итак, в этом блоке вам необходимо разработать сетевую программу-клиент, при помощи которой можно отправлять различные метрики на сервер. Клиент и сервер должны взаимодействовать между собой по простому текстовому протоколу через TCP сокеты. Текстовый протокол имеет главное преимущество – он наглядный – можно просмотреть диалог взаимодействия клиентской и серверной стороны без использования дополнительных инструментов." ] }, { "cell_type": "markdown", "id": "77376f2a", "metadata": {}, "source": [ "Прежде чем реализовывать клиентское приложение давайте рассмотрим взаимодействие между клиентом и сервером на конкретных примерах." ] }, { "cell_type": "markdown", "id": "80bace3a", "metadata": {}, "source": [ "Предположим, необходимо собирать метрики о работе операционной системы: `cpu` (загрузка процессора), `memory usage` (потребление памяти), `disk usage` (потребление места на жестком диске), `network usage` (статистика сетевых интерфейсов) и т.д. Это понадобится для контроля загрузки серверов и прогноза по расширению парка железа компании - проще говоря для мониторинга.\n", "\n", "Пусть у нас имеется в наличии два сервера `huginn` и `muninn`. Мы будем получать загрузку центрального процессора на сервере и отправлять метрику с названием `имя_сервера.cpu`\n", "\n", "```\n", "client -> server: put huginn.cpu 10.6 1642667947\\n\n", "server -> client: ok\\n\\n\n", "client -> server: put muninn.cpu 15.3 1642667959\\n\n", "server -> client: ok\\n\\n\n", "```\n", "\n", "Чтобы отправить метрику на сервер, вы отправляете в TCP-соединение строку вида:\n", "\n", "```\n", "put huginn.cpu 10.6 1642667947\\n\n", "```\n", "\n", "Ключевое слово `put` означает команду отправки метрики. За ней через пробел следует название (имя) самой метрики, например `huginn.cpu`, далее опять через пробел значение метрики, и через еще один пробел временная метка `unix timestamp`. Таким образом, во время `1642667947` значение метрики `huginn.cpu` было равно `10.6`. Наконец, команда заканчивается символом переноса строки `\\n`.\n", "\n", "В ответ на эту команду `put` сервер присылает уведомление об успешном сохранении метрики в виде строки:\n", "\n", "```\n", "ok\\n\\n\n", "```\n", "\n", "Два переноса строки в данном случае означают маркер конца сообщения от сервера клиенту." ] }, { "cell_type": "markdown", "id": "a090a6c9", "metadata": {}, "source": [ "## Команды ##\n", "\n", "Необходимо реализовать две команды:\n", "\n", "`put` - для сохранения метрик на сервере.\n", "\n", "`get` - для получения метрик.\n", "\n", "Формат команды `put` для отправки метрик — это строка вида:\n", "\n", "```\n", "put \\n\n", "```\n", "\n", "Успешный ответ от сервера:\n", "\n", "```\n", "ok\\n\\n\n", "```\n", "\n", "Ошибка сервера:\n", "\n", "```\n", "error\\nwrong command\\n\\n\n", "```\n", "\n", "Обратите внимание на то, что за каждым ответом сервера указано два символа `\\n`. В качестве значения метрики `value` используется вещественное число.\n", "\n", "Данные нужно не только отправлять на сервер, но и запрашивать их. Это может потребоваться для визуализации и анализа нужных метрик в определенные промежутки времени.\n", "\n", "Формат команды `get` для получения метрик — это строка вида:\n", "\n", "```\n", "get \\n\n", "```\n", "\n", "В качестве ключа можно указывать символ `*`, для этого символа будут возвращены все доступные метрики. В данном задании мы никак не ограничиваем количество метрик, которые должен вернуть сервер – сервер должен возвращать все метрики, удовлетворяющие ключу.\n", "\n", "Успешный ответ от сервера:\n", "\n", "```\n", "ok\\nhuginn.cpu 10.5 1642667947\\nmuninn.cpu 15.3 1642667959\\n\\n\n", "```\n", "\n", "Если ни одна метрика не удовлетворяет условиям поиска, то вернется ответ:\n", "\n", "```\n", "ok\\n\\n\n", "```\n", "\n", "Обратите внимание, что каждая успешная операция начинается с `\"ok\"`, а за ответом сервера всегда указано два символа `\\n`." ] }, { "cell_type": "markdown", "id": "f37c929b", "metadata": {}, "source": [ "## Реализация клиента. ##\n", "\n", "Необходимо реализовать класс `Client`, в котором будет инкапсулировано соединение с сервером, клиентский сокет и методы для получения и отправки метрик на сервер. В конструктор класса `Client` должна передаваться адресная пара хост и порт, а также необязательный аргумент `timeout` (`timeout=None` по умолчанию). У класса `Client` должно быть 2 метода: `put` и `get`, соответствующих протоколу выше.\n", "\n", "Пример вызова клиента для отправки метрик и затем их получения:" ] }, { "cell_type": "code", "execution_count": 1, "id": "6f42bcf1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'huginn.cpu': [(1642667947, 0.5), (1642667948, 2.0), (1642667948, 0.5)], 'muninn.cpu': [(1642667950, 3.0), (1642667951, 4.0)], 'muninn.memory': [(1642748845, 4200000.0)]}\n" ] } ], "source": [ "from client import Client\n", "\n", "client = Client(\"127.0.0.1\", 8888, timeout=15)\n", "\n", "client.put(\"huginn.cpu\", 0.5, timestamp=1642667947)\n", "client.put(\"huginn.cpu\", 2.0, timestamp=1642667948)\n", "client.put(\"huginn.cpu\", 0.5, timestamp=1642667948)\n", "\n", "client.put(\"muninn.cpu\", 3, timestamp=1642667950)\n", "client.put(\"muninn.cpu\", 4, timestamp=1642667951)\n", "client.put(\"muninn.memory\", 4200000)\n", "\n", "print(client.get(\"*\"))" ] }, { "cell_type": "markdown", "id": "4acccf13", "metadata": {}, "source": [ "Клиент получает данные в текстовом виде, метод `get` должен возвращать словарь с полученными ключами с сервера. Значением ключа в словаре является список кортежей `[(timestamp, metric_value), ...]`, отсортированный по `timestamp` от меньшего к большему. Значение timestamp должно быть преобразовано к целому числу `int`. Значение метрики `metric_value` нужно преобразовать к числу с плавающей точкой `float`.\n", "\n", "Метод `put` принимает первым аргументом название метрики, вторым численное значение, третьим - необязательный именованный аргумент `timestamp`. Если пользователь вызвал метод `put` без аргумента `timestamp`, то клиент автоматически должен подставить текущее время в команду `put - int(time.time())`\n", "\n", "Метод `put` не возвращает ничего в случае успешной отправки и выбрасывает исключение `ClientError` в случае неуспешной.\n", "\n", "Метод `get` принимает первым аргументом имя метрики, значения которой мы хотим выгрузить. Также вместо имени метрики можно использовать символ `*`, о котором говорилось в описании протокола.\n", "\n", "Метод `get` возвращает словарь с метриками (смотрите ниже пример) в случае успешного получения ответа от сервера и выбрасывает исключение `ClientError` в случае неуспешного.\n", "\n", "Пример возвращаемого значения при успешном вызове `client.get(\"huginn.cpu\")`:\n", "\n", "```python\n", "{\n", " 'huginn.cpu': [\n", " (1642667947, 0.5),\n", " (1642667948, 0.5)\n", " ]\n", "}\n", "```\n", "\n", "Пример возвращаемого значения при успешном вызове `client.get(\"*\")`:\n", "\n", "```python\n", "{\n", " 'huginn.cpu': [\n", " (1642667947, 0.5),\n", " (1642667948, 0.5)\n", " ],\n", " 'muninn.cpu': [\n", " (1642667950, 3.0),\n", " (1642667951, 4.0)\n", " ],\n", " 'muninn.memory': [\n", " (1642668557, 4200000.0)\n", " ]\n", "}\n", "```\n", "\n", "Если в ответ на get-запрос сервер вернул положительный ответ `ok\\n\\n`, но без данных (то есть данных по запрашиваемому ключу нет), то метод `get` клиентадолжен вернуть пустой словарь:" ] }, { "cell_type": "code", "execution_count": 2, "id": "c83223c7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{}\n" ] } ], "source": [ "print(client.get(\"non_existing_key\"))" ] }, { "cell_type": "markdown", "id": "cc53e7c0", "metadata": {}, "source": [ "Обратите внимание, что сервер хранит данные с максимальным разрешением в одну секунду. Это означает, что если в одну и ту же секунду отправить две одинаковые метрики, то будет сохранено только одно значение, которое было обработано последним. Все остальные значения будут перезаписаны.\n", "\n", "Итак, вам необходимо предоставить модуль с классом `Client`, исключением `ClientError`. В этом классе `Client` должны быть доступны методы `get` и `put` с описанной выше сигнатурой. При вызове методов `get` и `put` клиент должен посылать сообщения в TCP-соединение с сервером в соответствии с описанным текстовым протоколом, получать ответ от сервера, преобразовывать его в удобный для использования формат, описанный выше.\n", "\n", "Код клиента неудобно разрабатывать и отлаживать без сервера. Для удобства тестирования во время разработки кода клиента мы разработали `unittest`-ты.\n", "\n", "[test_client.py](test_client.py \"test_client.py\")\n", "\n", "Используйте данный `unittest` для проверки работы Вашего клиента для отправки метрик. Это ускорит процесс разработки клиента и упростит отладку." ] }, { "cell_type": "code", "execution_count": 16, "id": "91854869", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ ".....\r\n", "----------------------------------------------------------------------\r\n", "Ran 5 tests in 0.001s\r\n", "\r\n", "OK\r\n" ] } ], "source": [ "! python -m unittest test_client.py" ] }, { "cell_type": "markdown", "id": "62e177a7", "metadata": {}, "source": [ "Успехов при выполнении задания!" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }