Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# Practice107
# Practice106
84 changes: 84 additions & 0 deletions SerovAA/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Password Generator — микросервисное веб-приложение

## Описание проекта

Генератор паролей с микросервисной архитектурой.
Состоит из пяти компонентов:

- **Nginx** — обратный прокси и точка входа (порт 8080)
- **Frontend** — статический HTML/JS интерфейс
- **Backend (Flask)** — API для создания задач генерации паролей
- **Worker** — фоновый сервис, выполняющий длительную генерацию
- **Redis** — брокер задач и хранилище результатов

## Запуск

bash
- docker compose up -d

## Доступные endpoints
- Веб-приложение: http://localhost:8080
- Prometheus метрики: http://localhost:8080/metrics (или http://localhost:9090/targets)
- Grafana: http://localhost:3000 (логин admin / admin)

## Мониторинг в Grafana
- После входа в Grafana:
- Перейдите в Dashboards → Password Generator Monitoring.
- Вы увидите график скорости генерации паролей, счётчик запросов и логи сервисов.

## Собственная метрика
В бэкенде реализован счётчик generate_requests_total. Он увеличивается при каждом POST /api/generate и доступен по /metrics в формате Prometheus.

## Сбор логов
Логи всех контейнеров собираются через Promtail и отправляются в Loki. В Grafana можно фильтровать логи по сервису (service="backend").

## Остановить:

bash
- docker compose down

## Взаимодействие сервисов
- Пользователь вводит параметры пароля (длина, цифры, спецсимволы) и нажимает «Generate».
- Frontend отправляет POST-запрос на /api/generate в Backend.
- Backend создаёт задачу в Redis и возвращает task_id.
- Worker забирает задачу из очереди, генерирует пароль (с имитацией задержки 2 сек) и сохраняет результат.
- Frontend каждую секунду опрашивает /api/result/<task_id> и отображает пароль.

## Проверка работоспособности (curl)
Создать задачу на генерацию пароля:

bash
curl -X POST http://localhost:8080/api/generate \
-H "Content-Type: application/json" \
-d '{"length": 16, "use_digits": true, "use_special": true}'
Получить результат по task_id (подставьте полученный идентификатор):

bash
- curl http://localhost:8080/api/result/<task_id>

## CI/CD Pipeline
При создании pull request в ветку main автоматически запускается GitHub Actions workflow:

- Установка зависимостей для Backend и Worker
- Запуск тестов (pytest) для обоих сервисов
- Проверка сборки Docker Compose

Статус CI: https://github.com/SoftwareEngineering2026/Practice106/actions/workflows/ci-serovaa.yml/badge.svg

## Локальный запуск тестов
* bash
* pip install pytest
* pytest tests/

## Особенности реализации
✅ Единая точка входа — Nginx (порт 8080)

✅ Все сервисы общаются через внутреннюю сеть Docker

✅ Длительные задачи вынесены в отдельный Worker (не блокируют Backend)

✅ Полностью микросервисная архитектура

✅ Запуск одной командой docker compose up

✅ Автоматическое CI-тестирование при Pull Request
6 changes: 6 additions & 0 deletions SerovAA/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
42 changes: 42 additions & 0 deletions SerovAA/backend/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os
import json
import uuid
import redis
from flask import Flask, request, jsonify
from prometheus_client import Counter, generate_latest, REGISTRY

app = Flask(__name__)

# Собственная метрика: счётчик запросов на генерацию
GENERATE_REQUESTS = Counter('generate_requests_total', 'Total number of password generation requests')

redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'redis'), port=6379, db=0)

@app.route('/api/generate', methods=['POST'])
def generate():
GENERATE_REQUESTS.inc() # увеличиваем счётчик
data = request.json
task_id = str(uuid.uuid4())
task_data = {
'status': 'pending',
'length': data['length'],
'use_digits': data['use_digits'],
'use_special': data['use_special']
}
redis_client.set(f"task:{task_id}", json.dumps(task_data))
redis_client.lpush('password_tasks', task_id)
return jsonify({'task_id': task_id})

@app.route('/api/result/<task_id>', methods=['GET'])
def result(task_id):
task_data = redis_client.get(f"task:{task_id}")
if task_data:
return jsonify(json.loads(task_data))
return jsonify({'status': 'not_found'}), 404

@app.route('/metrics', methods=['GET'])
def metrics():
return generate_latest(REGISTRY), 200, {'Content-Type': 'text/plain'}

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
3 changes: 3 additions & 0 deletions SerovAA/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Flask==2.3.3
redis==5.0.1
prometheus-client==0.19.0
91 changes: 91 additions & 0 deletions SerovAA/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
version: '3.8'

services:
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./frontend:/usr/share/nginx/html
depends_on:
- backend
networks:
- app-network

backend:
build: ./backend
environment:
- REDIS_HOST=redis
depends_on:
- redis
networks:
- app-network
labels:
- "com.docker.compose.service=backend"

worker:
build: ./worker
environment:
- REDIS_HOST=redis
depends_on:
- redis
networks:
- app-network
labels:
- "com.docker.compose.service=worker"

redis:
image: redis:alpine
networks:
- app-network
labels:
- "com.docker.compose.service=redis"

prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
networks:
- app-network

loki:
image: grafana/loki:latest
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- ./loki/loki-config.yaml:/etc/loki/loki-config.yaml
ports:
- "3100:3100"
networks:
- app-network

promtail:
image: grafana/promtail:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./promtail/promtail-config.yaml:/etc/promtail/promtail-config.yaml
command: -config.file=/etc/promtail/promtail-config.yaml
depends_on:
- loki
networks:
- app-network

grafana:
image: grafana/grafana:latest
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
ports:
- "3000:3000"
depends_on:
- prometheus
- loki
networks:
- app-network

networks:
app-network:
driver: bridge
73 changes: 73 additions & 0 deletions SerovAA/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Password Generator</title>
<style>
body { font-family: Arial; max-width: 500px; margin: 50px auto; padding: 20px; }
input, button { margin: 10px 0; padding: 8px; }
.result { margin-top: 20px; padding: 10px; background: #f0f0f0; }
.status { color: gray; }
</style>
</head>
<body>
<h1>🔐 Password Generator</h1>

<label>Length: <input type="number" id="length" value="12" min="6" max="32"></label><br>
<label><input type="checkbox" id="digits" checked> Digits</label><br>
<label><input type="checkbox" id="special" checked> Special chars</label><br>

<button onclick="generatePassword()">Generate</button>
<button onclick="checkStatus()">Check Status</button>

<div class="result">
<strong>Password:</strong> <span id="password">-</span><br>
<strong>Task ID:</strong> <span id="taskId">-</span><br>
<strong>Status:</strong> <span id="status" class="status">Idle</span>
</div>

<script>
async function generatePassword() {
const length = document.getElementById('length').value;
const useDigits = document.getElementById('digits').checked;
const useSpecial = document.getElementById('special').checked;

const response = await fetch('/api/generate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({length, use_digits: useDigits, use_special: useSpecial})
});
const data = await response.json();
document.getElementById('taskId').innerText = data.task_id;
document.getElementById('status').innerText = 'Processing...';
document.getElementById('password').innerText = '-';

// Poll for result
pollResult(data.task_id);
}

async function pollResult(taskId) {
const interval = setInterval(async () => {
const response = await fetch(`/api/result/${taskId}`);
const data = await response.json();
if (data.status === 'completed') {
document.getElementById('password').innerText = data.password;
document.getElementById('status').innerText = 'Completed!';
clearInterval(interval);
} else if (data.status === 'failed') {
document.getElementById('status').innerText = 'Failed';
clearInterval(interval);
}
}, 1000);
}

async function checkStatus() {
const taskId = document.getElementById('taskId').innerText;
if (taskId !== '-') {
const response = await fetch(`/api/result/${taskId}`);
const data = await response.json();
document.getElementById('status').innerText = data.status;
}
}
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions SerovAA/grafana/provisioning/dashboards/dashboard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: 1

providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"title": "Password Generator Monitoring",
"panels": [
{
"title": "Generate Requests Rate",
"type": "graph",
"targets": [
{
"expr": "rate(generate_requests_total[1m])",
"legendFormat": "req/s"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}
},
{
"title": "Total Generate Requests",
"type": "stat",
"targets": [
{
"expr": "generate_requests_total"
}
],
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0}
},
{
"title": "Logs (Loki)",
"type": "logs",
"targets": [
{
"expr": "{service=\"backend\"} or {service=\"worker\"}",
"refId": "A"
}
],
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 8}
}
]
}
12 changes: 12 additions & 0 deletions SerovAA/grafana/provisioning/datasources/datasources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: 1

datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
25 changes: 25 additions & 0 deletions SerovAA/loki/loki-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
auth_enabled: false

server:
http_listen_port: 3100

common:
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
replication_factor: 1

schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h

storage_config:
filesystem:
directory: /loki/chunks
Loading