You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
17 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"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 <key> <value> <timestamp>\\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 <key>\\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
}