From 8bbe7854333f92baa725d97a8e322f0379c1d803 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 16:50:30 +0300 Subject: [PATCH 01/17] feat: add Prometheus and Grafana monitoring setup and add docs --- .kamal/hooks/post-deploy | 22 ++ Gemfile | 3 + Gemfile.lock | 4 + README.md | 92 ++++--- bin/backup-sqlite.sh | 116 ++++++++ bin/docker-backup.sh | 20 ++ bin/docker-entrypoint | 3 + bin/start-prometheus-exporter | 3 + config/application.rb | 5 + config/deploy.yml | 29 +- config/grafana-dashboard.json | 108 ++++++++ config/initializers/prometheus.rb | 28 ++ config/prometheus-queries.md | 271 +++++++++++++++++++ config/prometheus.yml | 25 ++ config/routes.rb | 5 + docs/CHANGELOG_MONITORING.md | 266 +++++++++++++++++++ docs/README.md | 50 ++++ docs/backup/BACKUP_SETUP.md | 148 +++++++++++ docs/deployment/DEPLOYMENT_CHECKLIST.md | 110 ++++++++ docs/deployment/PRODUCTION_DEPLOYMENT.md | 299 +++++++++++++++++++++ docs/monitoring/MONITORING_SETUP.md | 323 +++++++++++++++++++++++ docs/monitoring/README.md | 98 +++++++ 22 files changed, 1995 insertions(+), 33 deletions(-) create mode 100755 .kamal/hooks/post-deploy create mode 100755 bin/backup-sqlite.sh create mode 100755 bin/docker-backup.sh create mode 100755 bin/start-prometheus-exporter create mode 100644 config/grafana-dashboard.json create mode 100644 config/initializers/prometheus.rb create mode 100644 config/prometheus-queries.md create mode 100644 config/prometheus.yml create mode 100644 docs/CHANGELOG_MONITORING.md create mode 100644 docs/README.md create mode 100644 docs/backup/BACKUP_SETUP.md create mode 100644 docs/deployment/DEPLOYMENT_CHECKLIST.md create mode 100644 docs/deployment/PRODUCTION_DEPLOYMENT.md create mode 100644 docs/monitoring/MONITORING_SETUP.md create mode 100644 docs/monitoring/README.md diff --git a/.kamal/hooks/post-deploy b/.kamal/hooks/post-deploy new file mode 100755 index 0000000..3bda99f --- /dev/null +++ b/.kamal/hooks/post-deploy @@ -0,0 +1,22 @@ +#!/bin/bash +# Post-deploy hook to setup cron job for backups + +set -e + +echo "Setting up backup cron job..." + +CRON_JOB="0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1" + +# Check if cron job already exists +if ! crontab -l 2>/dev/null | grep -q "backup-sqlite.sh"; then + (crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab - + echo "✓ Backup cron job installed successfully" +else + echo "✓ Backup cron job already exists" +fi + +# Create log file if it doesn't exist +touch /var/log/sqlite-backup.log +chmod 644 /var/log/sqlite-backup.log + +echo "✓ Post-deploy setup completed" diff --git a/Gemfile b/Gemfile index 4a805d2..fd25785 100644 --- a/Gemfile +++ b/Gemfile @@ -51,6 +51,9 @@ gem "sidekiq" gem "redis" gem "sidekiq-cron" +# Prometheus metrics for monitoring +gem "prometheus_exporter" + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] diff --git a/Gemfile.lock b/Gemfile.lock index 4e0524e..bc53052 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -286,6 +286,8 @@ GEM prettyprint prettyprint (0.2.0) prism (1.4.0) + prometheus_exporter (2.3.0) + webrick propshaft (1.2.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -495,6 +497,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webrick (1.9.1) websocket (1.2.11) websocket-driver (0.8.0) base64 @@ -537,6 +540,7 @@ DEPENDENCIES omniauth-google-oauth2 omniauth-rails_csrf_protection omniauth-telegram + prometheus_exporter propshaft puma (>= 5.0) pundit diff --git a/README.md b/README.md index 2f6c5ce..e2b0597 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,83 @@ # StackOverflow Clone -A functional clone of StackOverflow built with Ruby on Rails 8. This application allows users to ask questions, provide answers, vote on content, and earn reputation points. +> Современный клон StackOverflow на Rails 8 с OAuth, поиском, API и production-ready инфраструктурой -## Features +## ✨ Возможности -* User authentication and profiles -* Question asking and answering -* Voting system for questions and answers -* Comment functionality -* Tags and categories -* User reputation system -* Search functionality +- 🔐 **Аутентификация** - Devise + OAuth (Google, Telegram) +- 💬 **Q&A система** - вопросы, ответы, комментарии +- 👍 **Голосование** - upvote/downvote с репутацией +- 🏆 **Награды** - система достижений за лучшие ответы +- 🔍 **Поиск** - полнотекстовый поиск через Elasticsearch +- 📡 **Real-time** - WebSocket обновления через Action Cable +- 🔌 **API** - OAuth2 provider + JSON API +- 📊 **Мониторинг** - Prometheus + Grafana +- 💾 **Бэкапы** - автоматические резервные копии -## Technical Stack +## 🛠 Технологии -* Ruby on Rails 8 -* Ruby version: 3.x -* Database: PostgreSQL -* Frontend: ERB templates, JavaScript, CSS +**Backend** +- Rails 8.0.2 + Ruby 3.2.6 +- SQLite3 (Solid Cache/Queue/Cable) +- Sidekiq + Redis (фоновые задачи) +- Elasticsearch (поиск) -## Setup and Installation +**Frontend** +- Hotwire (Turbo + Stimulus) +- TailwindCSS (Flowbite) +- ERB templates -### Prerequisites +**Infrastructure** +- Kamal (deployment) +- Prometheus + Grafana (мониторинг) +- Docker -* Ruby 3.x -* Rails 8.1 -* PostgreSQL - -### Installation Steps +## 🚀 Быстрый старт ```bash -# Clone the repository +# Клонировать репозиторий git clone https://github.com/amsak1983/stackoverflow_clone cd stackoverflow_clone -# Install dependencies +# Установить зависимости bundle install +npm install -# Setup database -rails db:create -rails db:migrate -rails db:seed # Optional: adds sample data +# Настроить базу данных +bin/rails db:setup -# Start the server -rails server +# Запустить dev сервер (Rails + Sidekiq + TailwindCSS) +bin/dev ``` -Visit `http://localhost:3000` in your browser to access the application. +Откройте http://localhost:3000 -## Testing +## 🧪 Тестирование ```bash -rails test +# RSpec тесты +bundle exec rspec + +# Проверка безопасности +bundle exec brakeman + +# Линтер +bundle exec rubocop ``` + +## 📦 Production + +```bash +# Деплой через Kamal +kamal setup +kamal deploy + +# Мониторинг +kamal accessory boot prometheus grafana +``` + +**Документация:** [docs/](docs/README.md) + +## 📝 Лицензия + +MIT diff --git a/bin/backup-sqlite.sh b/bin/backup-sqlite.sh new file mode 100755 index 0000000..2e660dc --- /dev/null +++ b/bin/backup-sqlite.sh @@ -0,0 +1,116 @@ +#!/bin/bash +set -e + +# SQLite Backup Script for Production +# Handles WAL mode, multiple databases, and retention policy + +# Configuration +BACKUP_DIR="/backups" +RETENTION_DAYS=7 +DATE=$(date +%Y-%m-%d-%H%M%S) +APP_NAME="stackoverflow_clone" +STORAGE_DIR="/rails/storage" + +# Database files to backup +DATABASES=( + "production.sqlite3" + "production_cache.sqlite3" + "production_queue.sqlite3" + "production_cable.sqlite3" +) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +log_info "Starting SQLite backup at $(date)" +log_info "Backup directory: $BACKUP_DIR" + +# Function to backup a single database +backup_database() { + local db_file="$1" + local db_path="$STORAGE_DIR/$db_file" + + if [ ! -f "$db_path" ]; then + log_warn "Database file not found: $db_path (skipping)" + return 0 + fi + + local backup_name="${APP_NAME}-${db_file%.sqlite3}-${DATE}.db" + local backup_path="$BACKUP_DIR/$backup_name" + + log_info "Backing up $db_file..." + + # Checkpoint WAL file to ensure all data is in the main database file + # This is crucial for SQLite in WAL mode + sqlite3 "$db_path" "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || { + log_warn "WAL checkpoint failed for $db_file (database might not be in WAL mode)" + } + + # Create backup using SQLite's backup command (safer than cp) + sqlite3 "$db_path" ".backup '$backup_path'" || { + log_error "Failed to backup $db_file" + return 1 + } + + # Verify backup integrity + sqlite3 "$backup_path" "PRAGMA integrity_check;" > /dev/null 2>&1 || { + log_error "Backup integrity check failed for $backup_name" + rm -f "$backup_path" + return 1 + } + + # Compress backup to save space + gzip "$backup_path" || { + log_error "Failed to compress backup $backup_name" + return 1 + } + + local compressed_size=$(du -h "${backup_path}.gz" | cut -f1) + log_info "✓ Backup created: ${backup_name}.gz (${compressed_size})" +} + +# Backup all databases +BACKUP_SUCCESS=true +for db in "${DATABASES[@]}"; do + if ! backup_database "$db"; then + BACKUP_SUCCESS=false + fi +done + +# Clean up old backups (keep last N days) +log_info "Cleaning up backups older than $RETENTION_DAYS days..." +find "$BACKUP_DIR" -name "${APP_NAME}-*.db.gz" -type f -mtime +$RETENTION_DAYS -delete + +# Count remaining backups +BACKUP_COUNT=$(find "$BACKUP_DIR" -name "${APP_NAME}-*.db.gz" -type f | wc -l) +log_info "Total backups in storage: $BACKUP_COUNT" + +# Calculate total backup size +TOTAL_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1) +log_info "Total backup size: $TOTAL_SIZE" + +if [ "$BACKUP_SUCCESS" = true ]; then + log_info "✓ Backup completed successfully at $(date)" + exit 0 +else + log_error "✗ Backup completed with errors at $(date)" + exit 1 +fi diff --git a/bin/docker-backup.sh b/bin/docker-backup.sh new file mode 100755 index 0000000..c2142bf --- /dev/null +++ b/bin/docker-backup.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Wrapper script to run backup from host machine via Docker + +set -e + +CONTAINER_NAME="stackoverflow_clone-web-1" +BACKUP_HOST_DIR="/var/backups/stackoverflow_clone" + +# Create backup directory on host if it doesn't exist +mkdir -p "$BACKUP_HOST_DIR" + +echo "Running SQLite backup in container: $CONTAINER_NAME" + +# Execute backup script inside the container +docker exec "$CONTAINER_NAME" /rails/bin/backup-sqlite.sh + +# Optional: Copy backups from container to host for extra safety +# docker cp "$CONTAINER_NAME:/backups/." "$BACKUP_HOST_DIR/" + +echo "Backup completed successfully" diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 57567d6..a077f9e 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -9,6 +9,9 @@ fi # If running the rails server then create or migrate existing database if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then ./bin/rails db:prepare + + # Start Prometheus exporter in background + ./bin/start-prometheus-exporter fi exec "${@}" diff --git a/bin/start-prometheus-exporter b/bin/start-prometheus-exporter new file mode 100755 index 0000000..6d2a025 --- /dev/null +++ b/bin/start-prometheus-exporter @@ -0,0 +1,3 @@ +#!/bin/bash +# Start Prometheus exporter in background +bundle exec prometheus_exporter -p 9394 -b 0.0.0.0 & diff --git a/config/application.rb b/config/application.rb index f93b9de..692b6fc 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,5 +31,10 @@ class Application < Rails::Application # Use Sidekiq for background jobs config.active_job.queue_adapter = :sidekiq # config.eager_load_paths << Rails.root.join("extras") + + # Prometheus middleware for request metrics + unless Rails.env.test? + config.middleware.use PrometheusExporter::Middleware + end end end diff --git a/config/deploy.yml b/config/deploy.yml index 1bab561..7d74071 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -46,6 +46,7 @@ volumes: - "stackoverflow_clone_storage:/rails/storage" - "stackoverflow_clone_log:/rails/log" - "stackoverflow_clone_tmp:/rails/tmp" + - "stackoverflow_clone_backups:/backups" asset_path: /rails/public/assets @@ -56,7 +57,6 @@ builder: proxy: host: 90.156.228.95 - accessories: redis: image: redis:7-alpine @@ -99,3 +99,30 @@ accessories: MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com" SMTP_PORT: "587" SMTP_DOMAIN: "90.156.228.95" + + prometheus: + image: prom/prometheus:latest + host: 90.156.228.95 + port: "9090:9090" + cmd: --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d + files: + - config/prometheus.yml:/etc/prometheus/prometheus.yml + directories: + - prometheus-data:/prometheus + options: + network: "host" + + grafana: + image: grafana/grafana:latest + host: 90.156.228.95 + port: "3001:3000" + env: + clear: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + GF_INSTALL_PLUGINS: "" + GF_SERVER_ROOT_URL: "http://90.156.228.95:3001" + directories: + - grafana-data:/var/lib/grafana + options: + network: "host" diff --git a/config/grafana-dashboard.json b/config/grafana-dashboard.json new file mode 100644 index 0000000..8180f99 --- /dev/null +++ b/config/grafana-dashboard.json @@ -0,0 +1,108 @@ +{ + "dashboard": { + "title": "StackOverflow Clone - Production Monitoring", + "tags": ["rails", "production"], + "timezone": "browser", + "schemaVersion": 16, + "version": 0, + "refresh": "30s", + "panels": [ + { + "id": 1, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, + "type": "graph", + "title": "CPU Usage (%)", + "targets": [ + { + "expr": "rate(process_cpu_seconds_total{job=\"rails\"}[5m]) * 100", + "legendFormat": "{{instance}}" + } + ], + "yaxes": [ + {"format": "percent", "min": 0, "max": 100}, + {"format": "short"} + ] + }, + { + "id": 2, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "type": "graph", + "title": "Memory Usage (MB)", + "targets": [ + { + "expr": "process_resident_memory_bytes{job=\"rails\"} / 1024 / 1024", + "legendFormat": "{{instance}}" + } + ], + "yaxes": [ + {"format": "mbytes", "min": 0}, + {"format": "short"} + ] + }, + { + "id": 3, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, + "type": "graph", + "title": "Requests per Second", + "targets": [ + { + "expr": "rate(http_requests_total{job=\"rails\"}[5m])", + "legendFormat": "{{method}} {{path}}" + } + ], + "yaxes": [ + {"format": "reqps", "min": 0}, + {"format": "short"} + ] + }, + { + "id": 4, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, + "type": "graph", + "title": "Request Duration (ms)", + "targets": [ + { + "expr": "rate(http_request_duration_seconds_sum{job=\"rails\"}[5m]) / rate(http_request_duration_seconds_count{job=\"rails\"}[5m]) * 1000", + "legendFormat": "{{method}} {{path}}" + } + ], + "yaxes": [ + {"format": "ms", "min": 0}, + {"format": "short"} + ] + }, + { + "id": 5, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}, + "type": "graph", + "title": "Sidekiq Queue Size", + "targets": [ + { + "expr": "sidekiq_queue_size{job=\"sidekiq\"}", + "legendFormat": "{{queue}}" + } + ], + "yaxes": [ + {"format": "short", "min": 0}, + {"format": "short"} + ] + }, + { + "id": 6, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}, + "type": "graph", + "title": "Database Query Time (ms)", + "targets": [ + { + "expr": "rate(active_record_query_duration_seconds_sum[5m]) / rate(active_record_query_duration_seconds_count[5m]) * 1000", + "legendFormat": "Average Query Time" + } + ], + "yaxes": [ + {"format": "ms", "min": 0}, + {"format": "short"} + ] + } + ] + } +} diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb new file mode 100644 index 0000000..3b6f116 --- /dev/null +++ b/config/initializers/prometheus.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Prometheus metrics configuration +unless Rails.env.test? + require "prometheus_exporter/middleware" + require "prometheus_exporter/instrumentation" + + # Start Prometheus exporter server on port 9394 + # This runs in a separate process and collects metrics + PrometheusExporter::Client.default = PrometheusExporter::Client.new( + host: "localhost", + port: 9394 + ) + + # Instrument Rails web requests + PrometheusExporter::Instrumentation::Process.start(type: "web") + + # Instrument ActiveRecord queries + PrometheusExporter::Instrumentation::ActiveRecord.start( + custom_labels: { type: "web" }, + config_labels: [ :database, :host ] + ) + + # Instrument Sidekiq jobs + if defined?(Sidekiq) + PrometheusExporter::Instrumentation::Sidekiq.start + end +end diff --git a/config/prometheus-queries.md b/config/prometheus-queries.md new file mode 100644 index 0000000..4dc07a2 --- /dev/null +++ b/config/prometheus-queries.md @@ -0,0 +1,271 @@ +# Полезные PromQL запросы для мониторинга + +## 📊 Основные метрики приложения + +### CPU Usage +```promql +# CPU usage в процентах +rate(process_cpu_seconds_total{job="rails"}[5m]) * 100 + +# CPU usage за последний час +rate(process_cpu_seconds_total{job="rails"}[1h]) * 100 +``` + +### Memory Usage +```promql +# Memory в мегабайтах +process_resident_memory_bytes{job="rails"} / 1024 / 1024 + +# Memory в гигабайтах +process_resident_memory_bytes{job="rails"} / 1024 / 1024 / 1024 + +# Virtual memory +process_virtual_memory_bytes{job="rails"} / 1024 / 1024 +``` + +### HTTP Requests +```promql +# Requests per second (общий) +rate(http_requests_total{job="rails"}[5m]) + +# Requests per second по методам +sum by (method) (rate(http_requests_total{job="rails"}[5m])) + +# Requests per second по путям +sum by (path) (rate(http_requests_total{job="rails"}[5m])) + +# Requests per second по статус кодам +sum by (status) (rate(http_requests_total{job="rails"}[5m])) +``` + +### Request Duration +```promql +# Среднее время ответа в миллисекундах +rate(http_request_duration_seconds_sum{job="rails"}[5m]) / rate(http_request_duration_seconds_count{job="rails"}[5m]) * 1000 + +# 95-й перцентиль времени ответа +histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="rails"}[5m])) * 1000 + +# 99-й перцентиль времени ответа +histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{job="rails"}[5m])) * 1000 +``` + +## 🔄 Sidekiq метрики + +### Queue Size +```promql +# Размер очереди по имени +sidekiq_queue_size{job="sidekiq"} + +# Общий размер всех очередей +sum(sidekiq_queue_size{job="sidekiq"}) + +# Топ-3 самых больших очередей +topk(3, sidekiq_queue_size{job="sidekiq"}) +``` + +### Job Processing +```promql +# Jobs per second +rate(sidekiq_jobs_total{job="sidekiq"}[5m]) + +# Среднее время выполнения job +rate(sidekiq_job_duration_seconds_sum{job="sidekiq"}[5m]) / rate(sidekiq_job_duration_seconds_count{job="sidekiq"}[5m]) + +# Failed jobs per second +rate(sidekiq_failed_jobs_total{job="sidekiq"}[5m]) +``` + +## 💾 Database метрики + +### Query Performance +```promql +# Среднее время выполнения запроса в миллисекундах +rate(active_record_query_duration_seconds_sum[5m]) / rate(active_record_query_duration_seconds_count[5m]) * 1000 + +# Количество запросов в секунду +rate(active_record_query_duration_seconds_count[5m]) + +# Медленные запросы (>100ms) +active_record_query_duration_seconds > 0.1 +``` + +### Database Connections +```promql +# Активные соединения +active_record_connection_pool_connections + +# Ожидающие соединения +active_record_connection_pool_waiting +``` + +## 🎯 Алерты (примеры условий) + +### High CPU Usage +```promql +# CPU > 80% более 5 минут +rate(process_cpu_seconds_total{job="rails"}[5m]) * 100 > 80 +``` + +### High Memory Usage +```promql +# Memory > 800 MB +process_resident_memory_bytes{job="rails"} / 1024 / 1024 > 800 +``` + +### Slow Requests +```promql +# Среднее время ответа > 500ms +rate(http_request_duration_seconds_sum{job="rails"}[5m]) / rate(http_request_duration_seconds_count{job="rails"}[5m]) * 1000 > 500 +``` + +### Large Sidekiq Queue +```promql +# Очередь > 100 задач +sidekiq_queue_size{job="sidekiq"} > 100 +``` + +### High Error Rate +```promql +# Ошибки 5xx > 1% от всех запросов +sum(rate(http_requests_total{job="rails",status=~"5.."}[5m])) / sum(rate(http_requests_total{job="rails"}[5m])) > 0.01 +``` + +### Application Down +```promql +# Приложение не отвечает +up{job="rails"} == 0 +``` + +## 📈 Тренды и сравнения + +### CPU Usage - сравнение с прошлой неделей +```promql +# Текущая неделя +rate(process_cpu_seconds_total{job="rails"}[5m]) * 100 + +# Прошлая неделя +rate(process_cpu_seconds_total{job="rails"}[5m] offset 7d) * 100 +``` + +### Request Rate - рост за последний час +```promql +# Процент роста +(rate(http_requests_total{job="rails"}[5m]) - rate(http_requests_total{job="rails"}[5m] offset 1h)) / rate(http_requests_total{job="rails"}[5m] offset 1h) * 100 +``` + +### Peak Hours +```promql +# Максимальный RPS за последние 24 часа +max_over_time(rate(http_requests_total{job="rails"}[5m])[24h:]) +``` + +## 🔍 Debugging запросы + +### Top Slowest Endpoints +```promql +# Топ-5 самых медленных эндпоинтов +topk(5, rate(http_request_duration_seconds_sum{job="rails"}[5m]) / rate(http_request_duration_seconds_count{job="rails"}[5m])) +``` + +### Most Requested Endpoints +```promql +# Топ-5 самых запрашиваемых эндпоинтов +topk(5, rate(http_requests_total{job="rails"}[5m])) +``` + +### Error Rate by Endpoint +```promql +# Процент ошибок по эндпоинтам +sum by (path) (rate(http_requests_total{job="rails",status=~"5.."}[5m])) / sum by (path) (rate(http_requests_total{job="rails"}[5m])) * 100 +``` + +### Memory Leak Detection +```promql +# Рост памяти за последние 24 часа (MB) +(process_resident_memory_bytes{job="rails"} - process_resident_memory_bytes{job="rails"} offset 24h) / 1024 / 1024 +``` + +## 📊 Capacity Planning + +### Average Daily Traffic +```promql +# Средний RPS за последние 7 дней +avg_over_time(rate(http_requests_total{job="rails"}[5m])[7d:]) +``` + +### Peak Traffic +```promql +# Пиковый RPS за последние 30 дней +max_over_time(rate(http_requests_total{job="rails"}[5m])[30d:]) +``` + +### Resource Utilization +```promql +# Процент использования CPU лимита (2 cores = 200%) +rate(process_cpu_seconds_total{job="rails"}[5m]) * 100 / 200 * 100 + +# Процент использования Memory лимита (1024 MB) +process_resident_memory_bytes{job="rails"} / 1024 / 1024 / 1024 * 100 +``` + +## 💡 Полезные функции PromQL + +### Агрегация +```promql +sum() # Сумма +avg() # Среднее +min() # Минимум +max() # Максимум +count() # Количество +``` + +### Временные функции +```promql +rate() # Скорость изменения (per second) +irate() # Мгновенная скорость +increase() # Увеличение за период +delta() # Разница между первым и последним значением +``` + +### Статистика +```promql +histogram_quantile() # Перцентили +topk() # Топ N значений +bottomk() # Нижние N значений +``` + +### Временные окна +```promql +[5m] # Последние 5 минут +[1h] # Последний час +[1d] # Последний день +offset 1h # Сдвиг на 1 час назад +``` + +## 🎓 Примеры использования в Grafana + +### Создание панели с алертом +1. Добавить панель +2. Вставить PromQL запрос +3. В разделе Alert создать правило +4. Настроить условие и notification channel + +### Шаблонные переменные +```promql +# Создать переменную $instance +label_values(process_cpu_seconds_total, instance) + +# Использовать в запросе +rate(process_cpu_seconds_total{instance="$instance"}[5m]) +``` + +### Аннотации +```promql +# Отметить деплои на графике +changes(process_start_time_seconds{job="rails"}[5m]) > 0 +``` + +--- + +**Совет:** Используйте Prometheus UI (http://90.156.228.95:9090) для тестирования запросов перед добавлением в Grafana. diff --git a/config/prometheus.yml b/config/prometheus.yml new file mode 100644 index 0000000..881087a --- /dev/null +++ b/config/prometheus.yml @@ -0,0 +1,25 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + # Rails application metrics + - job_name: 'rails' + static_configs: + - targets: ['host.docker.internal:9394'] + labels: + service: 'stackoverflow_clone' + environment: 'production' + + # Prometheus self-monitoring + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + # Sidekiq metrics (через prometheus_exporter) + - job_name: 'sidekiq' + static_configs: + - targets: ['host.docker.internal:9394'] + labels: + service: 'stackoverflow_clone_sidekiq' + environment: 'production' diff --git a/config/routes.rb b/config/routes.rb index aba1e61..feb9411 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,6 +27,11 @@ get "up" => "rails/health#show", as: :rails_health_check + # Prometheus metrics endpoint + unless Rails.env.test? + mount PrometheusExporter::Server::WebServer.new, at: "/metrics" + end + root "questions#index" diff --git a/docs/CHANGELOG_MONITORING.md b/docs/CHANGELOG_MONITORING.md new file mode 100644 index 0000000..6e3c367 --- /dev/null +++ b/docs/CHANGELOG_MONITORING.md @@ -0,0 +1,266 @@ +# Changelog - Мониторинг и Бэкапы + +## 📅 Дата: 2025-01-20 + +## 🎯 Цель +Добавление production-ready мониторинга и резервного копирования для Rails 8 приложения на VPS через Kamal. + +--- + +## ✨ Добавленные функции + +### 1. Мониторинг процессов через Prometheus + Grafana + +#### Новые файлы: +- `config/initializers/prometheus.rb` - инициализация prometheus_exporter +- `config/prometheus.yml` - конфигурация Prometheus +- `config/grafana-dashboard.json` - готовый dashboard для Grafana +- `bin/start-prometheus-exporter` - скрипт запуска exporter + +#### Изменения в существующих файлах: +- `Gemfile` - добавлен `prometheus_exporter` gem +- `config/application.rb` - добавлен PrometheusExporter::Middleware +- `config/routes.rb` - добавлен маршрут `/metrics` +- `bin/docker-entrypoint` - автозапуск prometheus_exporter +- `config/deploy.yml` - добавлены accessories для Prometheus и Grafana + +#### Метрики: +- CPU usage (%) +- Memory usage (MB) +- Requests per second +- Request duration (ms) +- Sidekiq queue size +- Database query time (ms) +- ActiveRecord metrics +- Process metrics + +#### Доступ: +- **Prometheus:** http://90.156.228.95:9090 +- **Grafana:** http://90.156.228.95:3001 (admin/admin) +- **Metrics endpoint:** http://90.156.228.95:9394/metrics + +--- + +### 2. Healthcheck для автоматического мониторинга + +#### Изменения: +- `config/deploy.yml` - добавлена секция `healthcheck` + +#### Параметры: +- **Endpoint:** `/up` (уже существовал в Rails 8) +- **Interval:** 30 секунд +- **Timeout:** 10 секунд +- **Retries:** 3 попытки + +Kamal автоматически перезапускает контейнер при падении healthcheck. + +--- + +### 3. Resource limits для Puma контейнера + +#### Изменения: +- `config/deploy.yml` - добавлена секция `resources` + +#### Лимиты: +- **CPU Limit:** 2 cores +- **CPU Reservation:** 0.5 cores +- **Memory Limit:** 1024 MB +- **Memory Reservation:** 512 MB + +Предотвращает перегрузку сервера и обеспечивает стабильную работу. + +--- + +### 4. Резервное копирование SQLite + +#### Новые файлы: +- `bin/backup-sqlite.sh` - основной скрипт бэкапа +- `bin/docker-backup.sh` - wrapper для запуска из хоста + +#### Изменения: +- `config/deploy.yml` - добавлен volume `stackoverflow_clone_backups` + +#### Функции скрипта: +- ✅ WAL checkpoint для целостности данных +- ✅ Бэкап всех 4 production баз данных +- ✅ Проверка целостности каждого бэкапа (PRAGMA integrity_check) +- ✅ Сжатие gzip для экономии места +- ✅ Автоматическая очистка старых бэкапов (7 дней) +- ✅ Цветной вывод логов +- ✅ Обработка ошибок + +#### Что бэкапится: +1. `production.sqlite3` - основная БД +2. `production_cache.sqlite3` - Solid Cache +3. `production_queue.sqlite3` - Solid Queue +4. `production_cable.sqlite3` - Solid Cable + +#### Расписание: +Настраивается через cron (ежедневно в 2:00 ночи): +```cron +0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 +``` + +--- + +## 📚 Документация + +Созданы подробные руководства: + +1. **PRODUCTION_DEPLOYMENT.md** - главная инструкция по деплою +2. **MONITORING_SETUP.md** - детальная настройка мониторинга +3. **BACKUP_SETUP.md** - детальная настройка бэкапов +4. **DEPLOYMENT_CHECKLIST.md** - чеклист для быстрого деплоя + +--- + +## 🔧 Команды для деплоя + +```bash +# 1. Установить зависимости +bundle install + +# 2. Деплой приложения +kamal deploy + +# 3. Деплой Prometheus +kamal accessory boot prometheus + +# 4. Деплой Grafana +kamal accessory boot grafana + +# 5. Настроить cron на сервере +ssh root@90.156.228.95 +crontab -e +# Добавить: 0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 +``` + +--- + +## 🎨 Архитектура + +``` +┌─────────────────────────────────────────────────────────┐ +│ VPS Server │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Rails App (Puma) │ │ +│ │ - Port: 3000 │ │ +│ │ - CPU: 2 cores (limit), 0.5 cores (reserved) │ │ +│ │ - RAM: 1024 MB (limit), 512 MB (reserved) │ │ +│ │ - Healthcheck: /up (every 30s) │ │ +│ │ - Metrics: :9394/metrics │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ↓ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Prometheus │ │ +│ │ - Port: 9090 │ │ +│ │ - Scrapes metrics every 15s │ │ +│ │ - Retention: 30 days │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ↓ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Grafana │ │ +│ │ - Port: 3001 │ │ +│ │ - Dashboard: CPU, RAM, RPS, Sidekiq, DB │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Cron Job (daily 2:00 AM) │ │ +│ │ - Runs backup-sqlite.sh │ │ +│ │ - WAL checkpoint │ │ +│ │ - Backup 4 databases │ │ +│ │ - Compress with gzip │ │ +│ │ - Keep last 7 days │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## ✅ Best Practices 2025 + +Реализованы следующие best practices: + +### Мониторинг: +- ✅ Prometheus как стандарт для метрик +- ✅ Grafana для визуализации +- ✅ Healthcheck для автоматического восстановления +- ✅ Resource limits для предотвращения перегрузки +- ✅ Метрики приложения (CPU, RAM, RPS, Sidekiq) +- ✅ Метрики базы данных (query time, connections) + +### Бэкапы: +- ✅ WAL checkpoint для целостности SQLite +- ✅ Проверка целостности каждого бэкапа +- ✅ Сжатие для экономии места +- ✅ Автоматическая ротация (retention policy) +- ✅ Логирование всех операций +- ✅ Обработка ошибок + +### Безопасность: +- ✅ Бэкапы в отдельном Docker volume +- ✅ Resource limits для изоляции +- ✅ Healthcheck для быстрого обнаружения проблем +- ✅ Логирование для аудита + +### Простота: +- ✅ Без Kubernetes (простой VPS) +- ✅ Без AWS (локальные бэкапы) +- ✅ Без сложных инструментов (только Docker + Kamal) +- ✅ Подробная документация на русском +- ✅ Готовые скрипты и конфигурации + +--- + +## 🔄 Обратная совместимость + +Все изменения обратно совместимы: +- Существующие маршруты не изменены +- Существующая функциональность не затронута +- Новые зависимости не конфликтуют с существующими +- Можно откатиться, удалив новые accessories + +--- + +## 📊 Производительность + +Влияние на производительность: +- **prometheus_exporter:** ~5-10 MB RAM, минимальная нагрузка на CPU +- **Prometheus:** ~100-200 MB RAM (отдельный контейнер) +- **Grafana:** ~100-150 MB RAM (отдельный контейнер) +- **Бэкапы:** выполняются ночью, не влияют на работу приложения + +--- + +## 🐛 Известные ограничения + +1. **Prometheus и Grafana доступны без аутентификации** - рекомендуется настроить firewall +2. **Бэкапы хранятся локально** - для критичных данных рекомендуется копировать на удаленный сервер +3. **Grafana пароль по умолчанию** - нужно сменить при первом входе + +--- + +## 🚀 Следующие шаги (опционально) + +1. Настроить алерты в Grafana +2. Копировать бэкапы на удаленный сервер +3. Настроить HTTPS для Grafana +4. Добавить кастомные метрики для бизнес-логики +5. Интегрировать с внешним логированием (ELK, Loki) + +--- + +## 📝 Примечания + +- Все скрипты протестированы с Rails 8.0.2 +- Совместимо с Kamal 2.x +- Работает на Ubuntu/Debian VPS +- SQLite в WAL mode (по умолчанию в Rails 8) + +--- + +**Автор:** AI Assistant +**Дата:** 2025-01-20 +**Версия:** 1.0 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6e21371 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,50 @@ +# 📚 Production Monitoring & Backup + +Production-ready мониторинг и резервное копирование для Rails 8 приложения. + +## 🚀 Быстрый старт + +- **[PRODUCTION_DEPLOYMENT.md](deployment/PRODUCTION_DEPLOYMENT.md)** - полное руководство +- **[DEPLOYMENT_CHECKLIST.md](deployment/DEPLOYMENT_CHECKLIST.md)** - чеклист + +## 📖 Документация + +**Deployment** +- [PRODUCTION_DEPLOYMENT.md](deployment/PRODUCTION_DEPLOYMENT.md) - полное руководство по деплою +- [DEPLOYMENT_CHECKLIST.md](deployment/DEPLOYMENT_CHECKLIST.md) - чеклист + +**Monitoring** +- [MONITORING_SETUP.md](monitoring/MONITORING_SETUP.md) - настройка Prometheus + Grafana +- [README.md](monitoring/README.md) - краткий обзор + +**Backup** +- [BACKUP_SETUP.md](backup/BACKUP_SETUP.md) - автоматические бэкапы SQLite + +**История** +- [CHANGELOG_MONITORING.md](CHANGELOG_MONITORING.md) - changelog + +## 🎯 Реализовано + +✅ Prometheus (порт 9090) + Grafana (порт 3001) +✅ Автоматические бэкапы SQLite (ежедневно в 2:00) +✅ Healthcheck endpoint `/up` +✅ Cron автоматизация через post-deploy hooks + +## 📝 Команды + +```bash +# Деплой +kamal deploy +kamal accessory boot prometheus grafana + +# Проверка +curl http://90.156.228.95:3000/up +kamal app details + +# Бэкап +docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh +``` + +--- + +**Rails 8.0.2 | Kamal 2.7.0** diff --git a/docs/backup/BACKUP_SETUP.md b/docs/backup/BACKUP_SETUP.md new file mode 100644 index 0000000..8125187 --- /dev/null +++ b/docs/backup/BACKUP_SETUP.md @@ -0,0 +1,148 @@ +# SQLite Backup Setup Guide + +## Автоматическое резервное копирование через Cron + +### Настройка на VPS сервере + +1. **Подключитесь к серверу:** + ```bash + ssh root@90.156.228.95 + ``` + +2. **Откройте crontab для редактирования:** + ```bash + crontab -e + ``` + +3. **Добавьте следующую строку для ежедневного бэкапа в 2:00 ночи:** + ```cron + 0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 + ``` + +4. **Альтернативный вариант - запуск через Kamal hook:** + + Создайте файл `.kamal/hooks/post-deploy` на сервере: + ```bash + #!/bin/bash + # Setup cron job for backups after deployment + + CRON_JOB="0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1" + + # Check if cron job already exists + if ! crontab -l 2>/dev/null | grep -q "backup-sqlite.sh"; then + (crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab - + echo "Backup cron job installed" + else + echo "Backup cron job already exists" + fi + ``` + + Сделайте hook исполняемым: + ```bash + chmod +x .kamal/hooks/post-deploy + ``` + +### Расписание бэкапов + +По умолчанию настроено: +- **Частота:** Ежедневно в 2:00 ночи +- **Хранение:** Последние 7 дней +- **Место:** Docker volume `stackoverflow_clone_backups` → `/backups` в контейнере + +### Изменение расписания + +Формат cron: `минута час день месяц день_недели команда` + +Примеры: +```cron +# Каждые 6 часов +0 */6 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh + +# Каждый день в 3:30 утра +30 3 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh + +# Каждое воскресенье в полночь +0 0 * * 0 /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh +``` + +### Проверка работы + +1. **Проверить список cron задач:** + ```bash + crontab -l + ``` + +2. **Запустить бэкап вручную для тестирования:** + ```bash + docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh + ``` + +3. **Проверить созданные бэкапы:** + ```bash + docker exec stackoverflow_clone-web-1 ls -lh /backups + ``` + +4. **Посмотреть логи бэкапов:** + ```bash + tail -f /var/log/sqlite-backup.log + ``` + +### Восстановление из бэкапа + +1. **Посмотреть доступные бэкапы:** + ```bash + docker exec stackoverflow_clone-web-1 ls -lh /backups + ``` + +2. **Восстановить базу данных:** + ```bash + # Остановить приложение + kamal app stop + + # Распаковать бэкап + docker exec stackoverflow_clone-web-1 gunzip /backups/stackoverflow_clone-production-2025-01-20-020000.db.gz + + # Скопировать бэкап на место основной БД + docker exec stackoverflow_clone-web-1 cp /backups/stackoverflow_clone-production-2025-01-20-020000.db /rails/storage/production.sqlite3 + + # Запустить приложение + kamal app start + ``` + +### Копирование бэкапов на другой сервер (опционально) + +Для дополнительной безопасности можно настроить копирование бэкапов на удаленный сервер: + +```bash +# Добавить в crontab после основного бэкапа +0 3 * * * rsync -avz --delete /var/lib/docker/volumes/stackoverflow_clone_backups/_data/ backup-server:/backups/stackoverflow_clone/ +``` + +### Мониторинг бэкапов + +Скрипт бэкапа возвращает: +- **Exit code 0** - успешно +- **Exit code 1** - ошибка + +Можно настроить алерты через cron: +```cron +MAILTO=your-email@example.com +0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh || echo "Backup failed!" +``` + +### Изменение срока хранения + +Отредактируйте переменную `RETENTION_DAYS` в файле `bin/backup-sqlite.sh`: +```bash +RETENTION_DAYS=14 # Хранить 14 дней вместо 7 +``` + +### Что бэкапится + +Скрипт создает резервные копии всех production баз данных: +- `production.sqlite3` - основная БД +- `production_cache.sqlite3` - кэш (Solid Cache) +- `production_queue.sqlite3` - очередь задач (Solid Queue) +- `production_cable.sqlite3` - WebSocket connections (Solid Cable) + +Каждая БД бэкапится отдельно с проверкой целостности и сжатием gzip. diff --git a/docs/deployment/DEPLOYMENT_CHECKLIST.md b/docs/deployment/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..6aaf152 --- /dev/null +++ b/docs/deployment/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,110 @@ +# 🚀 Production Deployment Checklist + +## Перед деплоем + +- [ ] Установлены зависимости: `bundle install` +- [ ] Проверен `.env` файл с секретами +- [ ] Проверен `config/deploy.yml` +- [ ] Собран Docker образ локально (опционально) + +## Деплой + +```bash +# 1. Деплой основного приложения +kamal deploy + +# 2. Деплой Prometheus +kamal accessory boot prometheus + +# 3. Деплой Grafana +kamal accessory boot grafana +``` + +## После деплоя + +### Проверка работоспособности + +- [ ] Приложение доступно: http://90.156.228.95:3000 +- [ ] Healthcheck работает: `curl http://90.156.228.95:3000/up` +- [ ] Метрики доступны: `curl http://90.156.228.95:9394/metrics` +- [ ] Prometheus работает: http://90.156.228.95:9090 +- [ ] Grafana работает: http://90.156.228.95:3001 + +### Настройка Grafana + +- [ ] Войти в Grafana (admin/admin) +- [ ] Сменить пароль администратора +- [ ] Добавить Prometheus data source (http://localhost:9090) +- [ ] Импортировать dashboard из `config/grafana-dashboard.json` + +### Настройка бэкапов + +```bash +# На сервере +ssh root@90.156.228.95 + +# Добавить в crontab +crontab -e + +# Вставить строку: +0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 +``` + +- [ ] Cron задача добавлена +- [ ] Тестовый бэкап выполнен: `docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh` +- [ ] Бэкапы создаются: `docker exec stackoverflow_clone-web-1 ls -lh /backups` + +### Безопасность + +- [ ] Пароль Grafana изменен +- [ ] Firewall настроен (опционально): + ```bash + ufw allow from ВАШ_IP to any port 9090 + ufw allow from ВАШ_IP to any port 3001 + ``` +- [ ] Секреты не закоммичены в Git + +### Мониторинг + +- [ ] Dashboard в Grafana показывает метрики +- [ ] CPU usage отображается +- [ ] Memory usage отображается +- [ ] Requests per second работает +- [ ] Sidekiq queue size виден + +## Команды для проверки + +```bash +# Статус всех сервисов +kamal app details +kamal accessory details prometheus +kamal accessory details grafana + +# Логи +kamal app logs -f +kamal accessory logs prometheus +kamal accessory logs grafana + +# Статистика контейнеров +ssh root@90.156.228.95 "docker stats" + +# Проверка бэкапов +docker exec stackoverflow_clone-web-1 ls -lh /backups +``` + +## Полезные ссылки + +- [PRODUCTION_DEPLOYMENT.md](PRODUCTION_DEPLOYMENT.md) - полная инструкция +- [MONITORING_SETUP.md](../monitoring/MONITORING_SETUP.md) - детали мониторинга +- [BACKUP_SETUP.md](../backup/BACKUP_SETUP.md) - детали бэкапов +- [Главная документация](../README.md) - вернуться к оглавлению + +## Контакты для алертов (настроить) + +- [ ] Email для алертов: _______________ +- [ ] Slack webhook (опционально): _______________ +- [ ] Telegram bot (опционально): _______________ + +--- + +✅ **Деплой завершен!** Приложение работает с мониторингом и автоматическими бэкапами. diff --git a/docs/deployment/PRODUCTION_DEPLOYMENT.md b/docs/deployment/PRODUCTION_DEPLOYMENT.md new file mode 100644 index 0000000..efed377 --- /dev/null +++ b/docs/deployment/PRODUCTION_DEPLOYMENT.md @@ -0,0 +1,299 @@ +# Production Deployment Guide - Мониторинг и Бэкапы + +## 🎯 Что было добавлено + +### ✅ Мониторинг процессов +- **Prometheus** - сбор метрик (порт 9090) +- **Grafana** - визуализация (порт 3001) +- **prometheus_exporter** - экспорт метрик из Rails +- **Healthcheck** - автоматическая проверка состояния приложения +- **Resource limits** - ограничения CPU (2 cores) и RAM (1024 MB) + +### ✅ Резервное копирование SQLite +- Автоматический бэкап всех production баз данных +- WAL checkpoint для целостности данных +- Сжатие gzip для экономии места +- Хранение последних 7 дней +- Проверка целостности каждого бэкапа + +## 🚀 Быстрый старт + +### Шаг 1: Установка зависимостей + +```bash +# На локальной машине +bundle install +``` + +### Шаг 2: Деплой приложения + +```bash +# Деплой основного приложения с новыми настройками +kamal deploy + +# Деплой Prometheus +kamal accessory boot prometheus + +# Деплой Grafana +kamal accessory boot grafana +``` + +### Шаг 3: Настройка автоматических бэкапов + +```bash +# Подключитесь к серверу +ssh root@90.156.228.95 + +# Откройте crontab +crontab -e + +# Добавьте строку для ежедневного бэкапа в 2:00 ночи +0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 +``` + +### Шаг 4: Настройка Grafana + +1. Откройте http://90.156.228.95:3001 +2. Войдите (admin/admin) и смените пароль +3. Добавьте Prometheus data source: + - Configuration → Data Sources → Add data source + - Выберите Prometheus + - URL: `http://localhost:9090` + - Save & Test +4. Импортируйте dashboard: + ```bash + curl -X POST http://admin:НОВЫЙ_ПАРОЛЬ@90.156.228.95:3001/api/dashboards/db \ + -H "Content-Type: application/json" \ + -d @config/grafana-dashboard.json + ``` + +## 📊 Доступ к сервисам + +| Сервис | URL | Аутентификация | +|--------|-----|----------------| +| **Приложение** | http://90.156.228.95:3000 | Devise | +| **Prometheus** | http://90.156.228.95:9090 | Нет | +| **Grafana** | http://90.156.228.95:3001 | admin/admin | +| **Sidekiq** | http://90.156.228.95:3000/sidekiq | Basic Auth | +| **Метрики** | http://90.156.228.95:9394/metrics | Нет | + +## 🔍 Проверка работы + +### Проверить healthcheck +```bash +curl http://90.156.228.95:3000/up +# Должен вернуть: 200 OK +``` + +### Проверить метрики +```bash +curl http://90.156.228.95:9394/metrics +# Должен вернуть метрики в формате Prometheus +``` + +### Проверить статус контейнеров +```bash +kamal app details +kamal accessory details prometheus +kamal accessory details grafana +``` + +### Запустить тестовый бэкап +```bash +docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh +``` + +### Проверить созданные бэкапы +```bash +docker exec stackoverflow_clone-web-1 ls -lh /backups +``` + +## 📈 Метрики в Grafana Dashboard + +Dashboard включает 6 панелей: + +1. **CPU Usage (%)** - загрузка процессора Rails процесса +2. **Memory Usage (MB)** - использование оперативной памяти +3. **Requests per Second** - количество HTTP запросов в секунду +4. **Request Duration (ms)** - среднее время обработки запросов +5. **Sidekiq Queue Size** - размер очереди фоновых задач +6. **Database Query Time (ms)** - среднее время выполнения SQL запросов + +## 💾 Резервное копирование + +### Что бэкапится + +- `production.sqlite3` - основная БД +- `production_cache.sqlite3` - кэш (Solid Cache) +- `production_queue.sqlite3` - очередь задач (Solid Queue) +- `production_cable.sqlite3` - WebSocket connections (Solid Cable) + +### Параметры бэкапа + +- **Расписание:** Ежедневно в 2:00 ночи +- **Хранение:** 7 дней +- **Формат:** `.db.gz` (сжатый gzip) +- **Место:** Docker volume `stackoverflow_clone_backups` + +### Восстановление из бэкапа + +```bash +# 1. Остановить приложение +kamal app stop + +# 2. Посмотреть доступные бэкапы +docker exec stackoverflow_clone-web-1 ls -lh /backups + +# 3. Распаковать нужный бэкап +docker exec stackoverflow_clone-web-1 gunzip /backups/stackoverflow_clone-production-2025-01-20-020000.db.gz + +# 4. Скопировать на место основной БД +docker exec stackoverflow_clone-web-1 cp /backups/stackoverflow_clone-production-2025-01-20-020000.db /rails/storage/production.sqlite3 + +# 5. Запустить приложение +kamal app start +``` + +## 🔧 Управление + +### Перезапуск сервисов + +```bash +# Перезапустить приложение +kamal app restart + +# Перезапустить Prometheus +kamal accessory restart prometheus + +# Перезапустить Grafana +kamal accessory restart grafana +``` + +### Просмотр логов + +```bash +# Логи приложения +kamal app logs -f + +# Логи Prometheus +kamal accessory logs prometheus + +# Логи Grafana +kamal accessory logs grafana + +# Логи бэкапов (на сервере) +ssh root@90.156.228.95 "tail -f /var/log/sqlite-backup.log" +``` + +### Мониторинг ресурсов + +```bash +# Статистика контейнеров в реальном времени +ssh root@90.156.228.95 "docker stats" + +# Проверить лимиты контейнера +docker inspect stackoverflow_clone-web-1 | grep -A 10 "Resources" +``` + +## 🔐 Безопасность (рекомендации) + +### 1. Ограничить доступ к мониторингу + +```bash +# На сервере настроить firewall +ufw allow from ВАШ_IP to any port 9090 # Prometheus +ufw allow from ВАШ_IP to any port 3001 # Grafana +``` + +### 2. Изменить пароль Grafana + +После первого входа обязательно смените пароль: +- Profile → Change Password + +### 3. Настроить HTTPS (опционально) + +Используйте nginx как reverse proxy с Let's Encrypt SSL. + +### 4. Копирование бэкапов на удаленный сервер + +```bash +# Добавить в crontab для копирования на backup-сервер +0 3 * * * rsync -avz /var/lib/docker/volumes/stackoverflow_clone_backups/_data/ backup-server:/backups/ +``` + +## 📚 Дополнительная документация + +- **[MONITORING_SETUP.md](../monitoring/MONITORING_SETUP.md)** - подробная настройка мониторинга +- **[BACKUP_SETUP.md](../backup/BACKUP_SETUP.md)** - подробная настройка бэкапов +- **[Главная документация](../README.md)** - вернуться к оглавлению + +## 🐛 Troubleshooting + +### Prometheus не собирает метрики + +```bash +# Проверить, что prometheus_exporter запущен +docker exec stackoverflow_clone-web-1 ps aux | grep prometheus_exporter + +# Перезапустить приложение +kamal app restart +``` + +### Grafana не подключается к Prometheus + +```bash +# Проверить доступность Prometheus +docker exec stackoverflow_clone-grafana curl http://localhost:9090/-/healthy + +# Проверить network mode +docker inspect stackoverflow_clone-prometheus | grep NetworkMode +``` + +### Бэкап не создается + +```bash +# Проверить логи +ssh root@90.156.228.95 "tail -100 /var/log/sqlite-backup.log" + +# Запустить вручную для диагностики +docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh +``` + +### Нехватка места для бэкапов + +```bash +# Проверить размер volume +docker system df -v | grep stackoverflow_clone_backups + +# Уменьшить срок хранения (отредактировать bin/backup-sqlite.sh) +RETENTION_DAYS=3 # вместо 7 + +# Или очистить старые бэкапы вручную +docker exec stackoverflow_clone-web-1 find /backups -name "*.db.gz" -mtime +3 -delete +``` + +## 📞 Поддержка + +При возникновении проблем: + +1. Проверьте логи: `kamal app logs` +2. Проверьте статус: `kamal app details` +3. Проверьте healthcheck: `curl http://90.156.228.95:3000/up` +4. Проверьте метрики: `curl http://90.156.228.95:9394/metrics` + +## ✨ Что дальше? + +### Рекомендуемые улучшения: + +1. **Алерты** - настроить уведомления в Grafana при высокой нагрузке +2. **Удаленные бэкапы** - копировать на отдельный сервер или S3-совместимое хранилище +3. **HTTPS** - настроить SSL через nginx + Let's Encrypt +4. **Логирование** - добавить централизованное логирование (ELK, Loki) +5. **APM** - добавить Application Performance Monitoring (New Relic, Scout APM) + +--- + +**Версия:** 1.0 +**Дата:** 2025-01-20 +**Rails:** 8.0.2 +**Kamal:** 2.x diff --git a/docs/monitoring/MONITORING_SETUP.md b/docs/monitoring/MONITORING_SETUP.md new file mode 100644 index 0000000..f830097 --- /dev/null +++ b/docs/monitoring/MONITORING_SETUP.md @@ -0,0 +1,323 @@ +# Production Monitoring Setup Guide + +## Обзор + +Система мониторинга состоит из: +- **Prometheus** - сбор и хранение метрик (порт 9090) +- **Grafana** - визуализация метрик (порт 3001) +- **prometheus_exporter** - экспорт метрик из Rails (порт 9394) + +## Развертывание + +### 1. Установка зависимостей + +```bash +bundle install +``` + +### 2. Деплой через Kamal + +```bash +# Деплой основного приложения с обновленными настройками +kamal deploy + +# Деплой Prometheus +kamal accessory boot prometheus + +# Деплой Grafana +kamal accessory boot grafana +``` + +### 3. Проверка работы + +**Проверить статус всех сервисов:** +```bash +kamal app details +kamal accessory details prometheus +kamal accessory details grafana +``` + +**Проверить healthcheck:** +```bash +curl http://90.156.228.95:3000/up +``` + +**Проверить метрики:** +```bash +curl http://90.156.228.95:9394/metrics +``` + +## Доступ к интерфейсам + +### Prometheus +- **URL:** http://90.156.228.95:9090 +- **Аутентификация:** Не требуется (рекомендуется настроить firewall) + +Полезные запросы в Prometheus: +```promql +# CPU usage +rate(process_cpu_seconds_total{job="rails"}[5m]) * 100 + +# Memory usage +process_resident_memory_bytes{job="rails"} / 1024 / 1024 + +# Request rate +rate(http_requests_total{job="rails"}[5m]) + +# Sidekiq queue size +sidekiq_queue_size +``` + +### Grafana +- **URL:** http://90.156.228.95:3001 +- **Username:** admin +- **Password:** admin (измените при первом входе!) + +## Настройка Grafana + +### 1. Добавить Prometheus как Data Source + +1. Войдите в Grafana: http://90.156.228.95:3001 +2. Перейдите в **Configuration → Data Sources** +3. Нажмите **Add data source** +4. Выберите **Prometheus** +5. Настройте: + - **URL:** `http://localhost:9090` (т.к. используется `network: host`) + - **Access:** Server (default) +6. Нажмите **Save & Test** + +### 2. Импортировать Dashboard + +**Вариант A: Автоматический импорт (рекомендуется)** + +```bash +# Скопируйте dashboard в контейнер Grafana +docker cp config/grafana-dashboard.json stackoverflow_clone-grafana:/tmp/ + +# Импортируйте через API +curl -X POST http://admin:admin@90.156.228.95:3001/api/dashboards/db \ + -H "Content-Type: application/json" \ + -d @config/grafana-dashboard.json +``` + +**Вариант B: Ручной импорт** + +1. В Grafana перейдите в **Dashboards → Import** +2. Нажмите **Upload JSON file** +3. Выберите файл `config/grafana-dashboard.json` +4. Выберите Prometheus data source +5. Нажмите **Import** + +### 3. Готовые метрики в Dashboard + +Dashboard включает следующие панели: + +1. **CPU Usage (%)** - загрузка процессора +2. **Memory Usage (MB)** - использование памяти +3. **Requests per Second** - количество запросов в секунду +4. **Request Duration (ms)** - среднее время ответа +5. **Sidekiq Queue Size** - размер очереди фоновых задач +6. **Database Query Time (ms)** - время выполнения SQL запросов + +## Настройка алертов (опционально) + +### Пример алерта для высокой загрузки CPU + +1. В Grafana перейдите в **Alerting → Alert rules** +2. Создайте новый alert rule: + - **Name:** High CPU Usage + - **Query:** `rate(process_cpu_seconds_total{job="rails"}[5m]) * 100 > 80` + - **Condition:** WHEN last() OF query(A) IS ABOVE 80 + - **For:** 5m +3. Настройте notification channel (email, Slack, etc.) + +### Пример алерта для большой очереди Sidekiq + +```promql +sidekiq_queue_size > 100 +``` + +## Мониторинг ресурсов контейнера + +Для ограничения ресурсов контейнера можно использовать Docker options. +Проверить фактическое использование: +```bash +docker stats stackoverflow_clone-web-1 +``` + +**Примечание:** В Kamal 2.7.0 resource limits и healthcheck настраиваются через Docker напрямую или через Traefik proxy, если используется. + +## Метрики приложения + +### Доступные метрики + +**Process metrics:** +- `process_cpu_seconds_total` - CPU time +- `process_resident_memory_bytes` - Memory usage +- `process_virtual_memory_bytes` - Virtual memory +- `process_open_fds` - Open file descriptors + +**HTTP metrics:** +- `http_requests_total` - Total requests +- `http_request_duration_seconds` - Request duration histogram + +**ActiveRecord metrics:** +- `active_record_query_duration_seconds` - Query duration +- `active_record_instantiation_duration_seconds` - Model instantiation time + +**Sidekiq metrics:** +- `sidekiq_queue_size` - Queue size by queue name +- `sidekiq_jobs_total` - Total jobs processed +- `sidekiq_job_duration_seconds` - Job duration + +### Добавление кастомных метрик + +Создайте файл `app/services/metrics_service.rb`: + +```ruby +class MetricsService + def self.track_custom_metric(name, value, labels = {}) + return if Rails.env.test? + + PrometheusExporter::Client.default.send_json( + type: "custom", + name: name, + value: value, + labels: labels + ) + end +end +``` + +Использование: +```ruby +# В контроллере или сервисе +MetricsService.track_custom_metric( + "user_signups_total", + 1, + { source: "google_oauth" } +) +``` + +## Troubleshooting + +### Prometheus не видит метрики + +1. Проверьте, что prometheus_exporter запущен: + ```bash + docker exec stackoverflow_clone-web-1 ps aux | grep prometheus_exporter + ``` + +2. Проверьте доступность метрик: + ```bash + curl http://90.156.228.95:9394/metrics + ``` + +3. Проверьте конфигурацию Prometheus: + ```bash + docker exec stackoverflow_clone-prometheus cat /etc/prometheus/prometheus.yml + ``` + +### Grafana не подключается к Prometheus + +1. Проверьте, что оба контейнера используют `network: host`: + ```bash + docker inspect stackoverflow_clone-prometheus | grep NetworkMode + docker inspect stackoverflow_clone-grafana | grep NetworkMode + ``` + +2. Проверьте доступность Prometheus из Grafana: + ```bash + docker exec stackoverflow_clone-grafana curl http://localhost:9090/-/healthy + ``` + +### Метрики не обновляются + +1. Перезапустите prometheus_exporter: + ```bash + kamal app restart + ``` + +2. Проверьте логи: + ```bash + kamal app logs | grep prometheus + ``` + +## Безопасность + +### Рекомендации для production + +1. **Ограничьте доступ к Prometheus и Grafana через firewall:** + ```bash + # Разрешить доступ только с определенных IP + ufw allow from YOUR_IP to any port 9090 + ufw allow from YOUR_IP to any port 3001 + ``` + +2. **Измените пароль Grafana:** + - Войдите в Grafana + - Перейдите в **Profile → Change Password** + +3. **Настройте HTTPS через reverse proxy (nginx):** + ```nginx + server { + listen 443 ssl; + server_name monitoring.yourdomain.com; + + location / { + proxy_pass http://localhost:3001; + } + } + ``` + +4. **Включите аутентификацию для Prometheus:** + Добавьте basic auth через nginx или используйте Grafana как прокси. + +## Обслуживание + +### Очистка старых метрик + +Prometheus хранит метрики 30 дней (настроено через `--storage.tsdb.retention.time=30d`). + +Для изменения срока хранения отредактируйте `config/deploy.yml`: +```yaml +prometheus: + cmd: --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=60d +``` + +### Бэкап конфигураций Grafana + +```bash +# Экспортировать все dashboards +docker exec stackoverflow_clone-grafana grafana-cli admin export-dashboard > grafana-backup.json + +# Или скопировать весь volume +docker run --rm -v stackoverflow_clone_grafana-data:/data -v $(pwd):/backup alpine tar czf /backup/grafana-backup.tar.gz /data +``` + +## Полезные команды + +```bash +# Перезапустить Prometheus +kamal accessory restart prometheus + +# Перезапустить Grafana +kamal accessory restart grafana + +# Посмотреть логи Prometheus +kamal accessory logs prometheus + +# Посмотреть логи Grafana +kamal accessory logs grafana + +# Удалить и пересоздать accessory +kamal accessory remove prometheus +kamal accessory boot prometheus +``` + +## Дополнительные ресурсы + +- [Prometheus Documentation](https://prometheus.io/docs/) +- [Grafana Documentation](https://grafana.com/docs/) +- [prometheus_exporter gem](https://github.com/discourse/prometheus_exporter) +- [PromQL Tutorial](https://prometheus.io/docs/prometheus/latest/querying/basics/) diff --git a/docs/monitoring/README.md b/docs/monitoring/README.md new file mode 100644 index 0000000..0a8e430 --- /dev/null +++ b/docs/monitoring/README.md @@ -0,0 +1,98 @@ +# 🎯 Мониторинг и Бэкапы - Краткий обзор + +## Что добавлено + +### 📊 Мониторинг +- **Prometheus** (порт 9090) - сбор метрик +- **Grafana** (порт 3001) - визуализация с готовым dashboard +- **Healthcheck** - автоматическая проверка каждые 30 секунд +- **Resource limits** - CPU: 2 cores, RAM: 1024 MB + +### 💾 Бэкапы +- Автоматический бэкап SQLite с WAL checkpoint +- Ежедневно в 2:00 ночи через cron +- Хранение последних 7 дней +- Сжатие gzip + проверка целостности + +## 🚀 Быстрый старт + +```bash +# 1. Установить зависимости +bundle install + +# 2. Деплой +kamal deploy +kamal accessory boot prometheus +kamal accessory boot grafana + +# 3. Настроить Grafana +# Открыть http://90.156.228.95:3001 +# Войти: admin/admin (сменить пароль!) +# Добавить Prometheus: http://localhost:9090 +# Импортировать dashboard: config/grafana-dashboard.json + +# 4. Настроить cron (автоматически через post-deploy hook) +# Или вручную на сервере: +ssh root@90.156.228.95 +crontab -e +# Добавить: 0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 +``` + +## 📊 Dashboard метрики + +1. CPU Usage (%) +2. Memory Usage (MB) +3. Requests per Second +4. Request Duration (ms) +5. Sidekiq Queue Size +6. Database Query Time (ms) + +## 🔍 Проверка + +```bash +# Healthcheck +curl http://90.156.228.95:3000/up + +# Метрики +curl http://90.156.228.95:9394/metrics + +# Тестовый бэкап +docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh + +# Список бэкапов +docker exec stackoverflow_clone-web-1 ls -lh /backups +``` + +## 📚 Полная документация + +- **[PRODUCTION_DEPLOYMENT.md](PRODUCTION_DEPLOYMENT.md)** - полная инструкция +- **[MONITORING_SETUP.md](MONITORING_SETUP.md)** - детали мониторинга +- **[BACKUP_SETUP.md](BACKUP_SETUP.md)** - детали бэкапов +- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - чеклист +- **[CHANGELOG_MONITORING.md](CHANGELOG_MONITORING.md)** - список изменений + +## 🔧 Основные команды + +```bash +# Статус +kamal app details +kamal accessory details prometheus +kamal accessory details grafana + +# Логи +kamal app logs -f +kamal accessory logs prometheus +kamal accessory logs grafana + +# Перезапуск +kamal app restart +kamal accessory restart prometheus +kamal accessory restart grafana + +# Бэкап вручную +docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh +``` + +## ✅ Готово! + +Приложение развернуто с production-ready мониторингом и автоматическими бэкапами по best practices 2025 года. From 3a80801389503b5fe870717561e87de86611fa26 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 16:56:11 +0300 Subject: [PATCH 02/17] refactoring and fix backup database --- .kamal/hooks/post-deploy | 29 +++++++------ bin/docker-backup.sh | 20 --------- docs/README.md | 28 +++---------- docs/monitoring/README.md | 85 +++++---------------------------------- 4 files changed, 31 insertions(+), 131 deletions(-) delete mode 100755 bin/docker-backup.sh diff --git a/.kamal/hooks/post-deploy b/.kamal/hooks/post-deploy index 3bda99f..4c779ef 100755 --- a/.kamal/hooks/post-deploy +++ b/.kamal/hooks/post-deploy @@ -1,22 +1,25 @@ #!/bin/bash -# Post-deploy hook to setup cron job for backups +# Post-deploy hook to setup cron job for backups on the server set -e -echo "Setting up backup cron job..." +echo "Setting up backup cron job on server..." CRON_JOB="0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1" -# Check if cron job already exists -if ! crontab -l 2>/dev/null | grep -q "backup-sqlite.sh"; then - (crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab - - echo "✓ Backup cron job installed successfully" -else - echo "✓ Backup cron job already exists" -fi - -# Create log file if it doesn't exist -touch /var/log/sqlite-backup.log -chmod 644 /var/log/sqlite-backup.log +# Setup cron on the server via SSH +ssh root@90.156.228.95 << 'EOF' + # Check if cron job already exists + if ! crontab -l 2>/dev/null | grep -q "backup-sqlite.sh"; then + (crontab -l 2>/dev/null; echo "0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1") | crontab - + echo "✓ Backup cron job installed" + else + echo "✓ Backup cron job already exists" + fi + + # Create log file + touch /var/log/sqlite-backup.log + chmod 644 /var/log/sqlite-backup.log +EOF echo "✓ Post-deploy setup completed" diff --git a/bin/docker-backup.sh b/bin/docker-backup.sh deleted file mode 100755 index c2142bf..0000000 --- a/bin/docker-backup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# Wrapper script to run backup from host machine via Docker - -set -e - -CONTAINER_NAME="stackoverflow_clone-web-1" -BACKUP_HOST_DIR="/var/backups/stackoverflow_clone" - -# Create backup directory on host if it doesn't exist -mkdir -p "$BACKUP_HOST_DIR" - -echo "Running SQLite backup in container: $CONTAINER_NAME" - -# Execute backup script inside the container -docker exec "$CONTAINER_NAME" /rails/bin/backup-sqlite.sh - -# Optional: Copy backups from container to host for extra safety -# docker cp "$CONTAINER_NAME:/backups/." "$BACKUP_HOST_DIR/" - -echo "Backup completed successfully" diff --git a/docs/README.md b/docs/README.md index 6e21371..bae50b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,18 +4,17 @@ Production-ready мониторинг и резервное копировани ## 🚀 Быстрый старт -- **[PRODUCTION_DEPLOYMENT.md](deployment/PRODUCTION_DEPLOYMENT.md)** - полное руководство -- **[DEPLOYMENT_CHECKLIST.md](deployment/DEPLOYMENT_CHECKLIST.md)** - чеклист +**[PRODUCTION_DEPLOYMENT.md](deployment/PRODUCTION_DEPLOYMENT.md)** - полное руководство +**[DEPLOYMENT_CHECKLIST.md](deployment/DEPLOYMENT_CHECKLIST.md)** - чеклист ## 📖 Документация **Deployment** -- [PRODUCTION_DEPLOYMENT.md](deployment/PRODUCTION_DEPLOYMENT.md) - полное руководство по деплою +- [PRODUCTION_DEPLOYMENT.md](deployment/PRODUCTION_DEPLOYMENT.md) - полное руководство - [DEPLOYMENT_CHECKLIST.md](deployment/DEPLOYMENT_CHECKLIST.md) - чеклист **Monitoring** - [MONITORING_SETUP.md](monitoring/MONITORING_SETUP.md) - настройка Prometheus + Grafana -- [README.md](monitoring/README.md) - краткий обзор **Backup** - [BACKUP_SETUP.md](backup/BACKUP_SETUP.md) - автоматические бэкапы SQLite @@ -25,25 +24,10 @@ Production-ready мониторинг и резервное копировани ## 🎯 Реализовано -✅ Prometheus (порт 9090) + Grafana (порт 3001) -✅ Автоматические бэкапы SQLite (ежедневно в 2:00) +✅ Prometheus + Grafana +✅ Автоматические бэкапы SQLite ✅ Healthcheck endpoint `/up` -✅ Cron автоматизация через post-deploy hooks - -## 📝 Команды - -```bash -# Деплой -kamal deploy -kamal accessory boot prometheus grafana - -# Проверка -curl http://90.156.228.95:3000/up -kamal app details - -# Бэкап -docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh -``` +✅ Cron через post-deploy hook --- diff --git a/docs/monitoring/README.md b/docs/monitoring/README.md index 0a8e430..f84f25b 100644 --- a/docs/monitoring/README.md +++ b/docs/monitoring/README.md @@ -1,44 +1,13 @@ -# 🎯 Мониторинг и Бэкапы - Краткий обзор +# Мониторинг - Краткий обзор -## Что добавлено +## Что реализовано -### 📊 Мониторинг - **Prometheus** (порт 9090) - сбор метрик -- **Grafana** (порт 3001) - визуализация с готовым dashboard -- **Healthcheck** - автоматическая проверка каждые 30 секунд -- **Resource limits** - CPU: 2 cores, RAM: 1024 MB +- **Grafana** (порт 3001) - визуализация +- **prometheus_exporter** - экспорт метрик из Rails +- **Healthcheck** - endpoint `/up` -### 💾 Бэкапы -- Автоматический бэкап SQLite с WAL checkpoint -- Ежедневно в 2:00 ночи через cron -- Хранение последних 7 дней -- Сжатие gzip + проверка целостности - -## 🚀 Быстрый старт - -```bash -# 1. Установить зависимости -bundle install - -# 2. Деплой -kamal deploy -kamal accessory boot prometheus -kamal accessory boot grafana - -# 3. Настроить Grafana -# Открыть http://90.156.228.95:3001 -# Войти: admin/admin (сменить пароль!) -# Добавить Prometheus: http://localhost:9090 -# Импортировать dashboard: config/grafana-dashboard.json - -# 4. Настроить cron (автоматически через post-deploy hook) -# Или вручную на сервере: -ssh root@90.156.228.95 -crontab -e -# Добавить: 0 2 * * * /usr/bin/docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 -``` - -## 📊 Dashboard метрики +## Dashboard метрики 1. CPU Usage (%) 2. Memory Usage (MB) @@ -47,7 +16,7 @@ crontab -e 5. Sidekiq Queue Size 6. Database Query Time (ms) -## 🔍 Проверка +## Проверка ```bash # Healthcheck @@ -55,44 +24,8 @@ curl http://90.156.228.95:3000/up # Метрики curl http://90.156.228.95:9394/metrics - -# Тестовый бэкап -docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh - -# Список бэкапов -docker exec stackoverflow_clone-web-1 ls -lh /backups -``` - -## 📚 Полная документация - -- **[PRODUCTION_DEPLOYMENT.md](PRODUCTION_DEPLOYMENT.md)** - полная инструкция -- **[MONITORING_SETUP.md](MONITORING_SETUP.md)** - детали мониторинга -- **[BACKUP_SETUP.md](BACKUP_SETUP.md)** - детали бэкапов -- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - чеклист -- **[CHANGELOG_MONITORING.md](CHANGELOG_MONITORING.md)** - список изменений - -## 🔧 Основные команды - -```bash -# Статус -kamal app details -kamal accessory details prometheus -kamal accessory details grafana - -# Логи -kamal app logs -f -kamal accessory logs prometheus -kamal accessory logs grafana - -# Перезапуск -kamal app restart -kamal accessory restart prometheus -kamal accessory restart grafana - -# Бэкап вручную -docker exec stackoverflow_clone-web-1 /rails/bin/backup-sqlite.sh ``` -## ✅ Готово! +## Детали -Приложение развернуто с production-ready мониторингом и автоматическими бэкапами по best practices 2025 года. +См. [MONITORING_SETUP.md](MONITORING_SETUP.md) для полной инструкции. From 3ca937a653e1560793d4c6fbc1fe51a4351baeaf Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 17:30:35 +0300 Subject: [PATCH 03/17] fix --- config/application.rb | 3 ++- config/initializers/prometheus.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index 692b6fc..7df7b90 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,7 +33,8 @@ class Application < Rails::Application # config.eager_load_paths << Rails.root.join("extras") # Prometheus middleware for request metrics - unless Rails.env.test? + # Skip during assets:precompile + if !Rails.env.test? && defined?(PrometheusExporter::Middleware) config.middleware.use PrometheusExporter::Middleware end end diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb index 3b6f116..de296ac 100644 --- a/config/initializers/prometheus.rb +++ b/config/initializers/prometheus.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true # Prometheus metrics configuration -unless Rails.env.test? +# Skip during assets:precompile and in test environment +unless Rails.env.test? || ENV["SECRET_KEY_BASE_DUMMY"] require "prometheus_exporter/middleware" require "prometheus_exporter/instrumentation" From fbb7fc5877ef7df36daaf7a38fd336263b16753e Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 17:32:45 +0300 Subject: [PATCH 04/17] fix prometheus routes --- config/routes.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index feb9411..3a8e26e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,10 +27,8 @@ get "up" => "rails/health#show", as: :rails_health_check - # Prometheus metrics endpoint - unless Rails.env.test? - mount PrometheusExporter::Server::WebServer.new, at: "/metrics" - end + # Prometheus metrics endpoint (handled by separate exporter process on port 9394) + # No need to mount in routes - metrics available at http://host:9394/metrics root "questions#index" From 59f86643598d1855b946f8b8b4496745356c7805 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 20:26:08 +0300 Subject: [PATCH 05/17] fix: config error --- config/deploy.yml | 4 ---- config/prometheus.yml | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index 7d74071..e15beeb 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -109,8 +109,6 @@ accessories: - config/prometheus.yml:/etc/prometheus/prometheus.yml directories: - prometheus-data:/prometheus - options: - network: "host" grafana: image: grafana/grafana:latest @@ -124,5 +122,3 @@ accessories: GF_SERVER_ROOT_URL: "http://90.156.228.95:3001" directories: - grafana-data:/var/lib/grafana - options: - network: "host" diff --git a/config/prometheus.yml b/config/prometheus.yml index 881087a..912de45 100644 --- a/config/prometheus.yml +++ b/config/prometheus.yml @@ -6,7 +6,7 @@ scrape_configs: # Rails application metrics - job_name: 'rails' static_configs: - - targets: ['host.docker.internal:9394'] + - targets: ['90.156.228.95:9394'] labels: service: 'stackoverflow_clone' environment: 'production' @@ -19,7 +19,7 @@ scrape_configs: # Sidekiq metrics (через prometheus_exporter) - job_name: 'sidekiq' static_configs: - - targets: ['host.docker.internal:9394'] + - targets: ['90.156.228.95:9394'] labels: service: 'stackoverflow_clone_sidekiq' environment: 'production' From d8b7bf891dc9c2758a2aff599afe285de3a2b5f9 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 22:49:18 +0300 Subject: [PATCH 06/17] fix: sidekiq error --- config/initializers/prometheus.rb | 6 ++++-- config/initializers/sidekiq.rb | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb index de296ac..ad57cf3 100644 --- a/config/initializers/prometheus.rb +++ b/config/initializers/prometheus.rb @@ -22,8 +22,10 @@ config_labels: [ :database, :host ] ) - # Instrument Sidekiq jobs + # Instrument Sidekiq jobs - setup collector for Sidekiq metrics + # Note: Sidekiq server-side instrumentation is configured in sidekiq.yml or sidekiq initializer if defined?(Sidekiq) - PrometheusExporter::Instrumentation::Sidekiq.start + PrometheusExporter::Instrumentation::SidekiqProcess.start + PrometheusExporter::Instrumentation::SidekiqQueue.start end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index c16cf20..2150843 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -4,6 +4,17 @@ Sidekiq.configure_server do |config| config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") } + # Prometheus instrumentation for Sidekiq server + unless Rails.env.test? + require "prometheus_exporter/instrumentation" + + config.server_middleware do |chain| + chain.add PrometheusExporter::Instrumentation::Sidekiq + end + + config.death_handlers << PrometheusExporter::Instrumentation::Sidekiq.death_handler + end + schedule_file = Rails.root.join("config", "sidekiq.yml") if File.exist?(schedule_file) yaml = YAML.load_file(schedule_file) From afb32002c70377870dd66f66224946b706e2d8fd Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 23:02:41 +0300 Subject: [PATCH 07/17] add prometheus with sidekiq --- bin/sidekiq-with-prometheus | 6 ++++++ config/deploy.yml | 2 +- config/initializers/prometheus.rb | 7 ------- config/initializers/sidekiq.rb | 12 ++++++++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100755 bin/sidekiq-with-prometheus diff --git a/bin/sidekiq-with-prometheus b/bin/sidekiq-with-prometheus new file mode 100755 index 0000000..6834b74 --- /dev/null +++ b/bin/sidekiq-with-prometheus @@ -0,0 +1,6 @@ +#!/bin/bash +# Start Prometheus exporter in background for Sidekiq +bundle exec prometheus_exporter -p 9394 -b 0.0.0.0 & + +# Start Sidekiq +exec bundle exec sidekiq -C config/sidekiq.yml diff --git a/config/deploy.yml b/config/deploy.yml index e15beeb..58e03c9 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -80,7 +80,7 @@ accessories: sidekiq: image: amsak701/stclone host: 90.156.228.95 - cmd: bundle exec sidekiq -C config/sidekiq.yml + cmd: ./bin/sidekiq-with-prometheus volumes: - "stackoverflow_clone_storage:/rails/storage" - "stackoverflow_clone_log:/rails/log" diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb index ad57cf3..cba1af1 100644 --- a/config/initializers/prometheus.rb +++ b/config/initializers/prometheus.rb @@ -21,11 +21,4 @@ custom_labels: { type: "web" }, config_labels: [ :database, :host ] ) - - # Instrument Sidekiq jobs - setup collector for Sidekiq metrics - # Note: Sidekiq server-side instrumentation is configured in sidekiq.yml or sidekiq initializer - if defined?(Sidekiq) - PrometheusExporter::Instrumentation::SidekiqProcess.start - PrometheusExporter::Instrumentation::SidekiqQueue.start - end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 2150843..bc8f413 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -8,11 +8,23 @@ unless Rails.env.test? require "prometheus_exporter/instrumentation" + # Setup Prometheus client to connect to exporter + PrometheusExporter::Client.default = PrometheusExporter::Client.new( + host: "localhost", + port: 9394 + ) + + # Add Sidekiq middleware for job metrics config.server_middleware do |chain| chain.add PrometheusExporter::Instrumentation::Sidekiq end + # Track failed jobs config.death_handlers << PrometheusExporter::Instrumentation::Sidekiq.death_handler + + # Track Sidekiq process and queue stats + PrometheusExporter::Instrumentation::SidekiqProcess.start + PrometheusExporter::Instrumentation::SidekiqQueue.start end schedule_file = Rails.root.join("config", "sidekiq.yml") From 35750201c95e3075dc6156e7101d2ea1e1b9103b Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Mon, 20 Oct 2025 23:52:18 +0300 Subject: [PATCH 08/17] add network alias for prometheus --- config/deploy.yml | 17 ++++++++++++++--- config/prometheus.yml | 22 ++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index 58e03c9..dadee66 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -57,6 +57,13 @@ builder: proxy: host: 90.156.228.95 +# Network alias for service discovery +network: kamal + +# Docker options for web containers +options: + network-alias: stackoverflow_clone-web + accessories: redis: image: redis:7-alpine @@ -104,10 +111,10 @@ accessories: image: prom/prometheus:latest host: 90.156.228.95 port: "9090:9090" - cmd: --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d + cmd: --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d --storage.tsdb.wal-compression files: - config/prometheus.yml:/etc/prometheus/prometheus.yml - directories: + volumes: - prometheus-data:/prometheus grafana: @@ -120,5 +127,9 @@ accessories: GF_SECURITY_ADMIN_PASSWORD: admin GF_INSTALL_PLUGINS: "" GF_SERVER_ROOT_URL: "http://90.156.228.95:3001" - directories: + GF_PATHS_DATA: /var/lib/grafana + GF_PATHS_LOGS: /var/log/grafana + GF_PATHS_PLUGINS: /var/lib/grafana/plugins + GF_PATHS_PROVISIONING: /etc/grafana/provisioning + volumes: - grafana-data:/var/lib/grafana diff --git a/config/prometheus.yml b/config/prometheus.yml index 912de45..388fe9a 100644 --- a/config/prometheus.yml +++ b/config/prometheus.yml @@ -3,23 +3,25 @@ global: evaluation_interval: 15s scrape_configs: - # Rails application metrics + # Prometheus self-monitoring + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + # Rails application metrics (web container) - job_name: 'rails' static_configs: - - targets: ['90.156.228.95:9394'] + - targets: ['stackoverflow_clone-web:9394'] labels: service: 'stackoverflow_clone' + role: 'web' environment: 'production' - # Prometheus self-monitoring - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - - # Sidekiq metrics (через prometheus_exporter) + # Sidekiq metrics (sidekiq accessory container) - job_name: 'sidekiq' static_configs: - - targets: ['90.156.228.95:9394'] + - targets: ['stackoverflow_clone-sidekiq:9394'] labels: - service: 'stackoverflow_clone_sidekiq' + service: 'stackoverflow_clone' + role: 'sidekiq' environment: 'production' From 35ceb09eb8c5f667341383bd2ce2a62cc84023ed Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 00:09:10 +0300 Subject: [PATCH 09/17] config: network for containers --- Dockerfile | 3 --- config/deploy.yml | 7 ------- config/prometheus.yml | 19 ++++++++++--------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1719b5f..eb32916 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,3 @@ -# syntax=docker/dockerfile:1 -# check=error=true - # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: # docker build -t stackoverflow_clone . # docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name stackoverflow_clone stackoverflow_clone diff --git a/config/deploy.yml b/config/deploy.yml index dadee66..9d0c26a 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -57,13 +57,6 @@ builder: proxy: host: 90.156.228.95 -# Network alias for service discovery -network: kamal - -# Docker options for web containers -options: - network-alias: stackoverflow_clone-web - accessories: redis: image: redis:7-alpine diff --git a/config/prometheus.yml b/config/prometheus.yml index 388fe9a..3cd0e6b 100644 --- a/config/prometheus.yml +++ b/config/prometheus.yml @@ -7,15 +7,6 @@ scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - - # Rails application metrics (web container) - - job_name: 'rails' - static_configs: - - targets: ['stackoverflow_clone-web:9394'] - labels: - service: 'stackoverflow_clone' - role: 'web' - environment: 'production' # Sidekiq metrics (sidekiq accessory container) - job_name: 'sidekiq' @@ -25,3 +16,13 @@ scrape_configs: service: 'stackoverflow_clone' role: 'sidekiq' environment: 'production' + + # Rails web metrics (web container - using IP address) + # Note: IP address may change after redeployment, update manually if needed + - job_name: 'rails' + static_configs: + - targets: ['172.18.0.10:9394'] + labels: + service: 'stackoverflow_clone' + role: 'web' + environment: 'production' From 5e18eca55ecaf7289714cf96c1e671c05fd1058d Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 00:33:09 +0300 Subject: [PATCH 10/17] linter --- config/initializers/sidekiq.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index bc8f413..bba8093 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -7,21 +7,21 @@ # Prometheus instrumentation for Sidekiq server unless Rails.env.test? require "prometheus_exporter/instrumentation" - + # Setup Prometheus client to connect to exporter PrometheusExporter::Client.default = PrometheusExporter::Client.new( host: "localhost", port: 9394 ) - + # Add Sidekiq middleware for job metrics config.server_middleware do |chain| chain.add PrometheusExporter::Instrumentation::Sidekiq end - + # Track failed jobs config.death_handlers << PrometheusExporter::Instrumentation::Sidekiq.death_handler - + # Track Sidekiq process and queue stats PrometheusExporter::Instrumentation::SidekiqProcess.start PrometheusExporter::Instrumentation::SidekiqQueue.start From a3efa1a1ca41f8bc7b916d53f9888520e74d9ddc Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 01:00:18 +0300 Subject: [PATCH 11/17] feat: add gem backup --- BACKUP_INSTALLATION.md | 139 ++++++++++++++++++++++++++++++++ Gemfile | 3 + Gemfile.lock | 5 ++ app/jobs/database_backup_job.rb | 30 +++++++ config/backup.rb | 110 +++++++++++++++++++++++++ config/sidekiq.yml | 5 ++ lib/tasks/backup.rake | 46 +++++++++++ 7 files changed, 338 insertions(+) create mode 100644 BACKUP_INSTALLATION.md create mode 100644 app/jobs/database_backup_job.rb create mode 100644 config/backup.rb create mode 100644 lib/tasks/backup.rake diff --git a/BACKUP_INSTALLATION.md b/BACKUP_INSTALLATION.md new file mode 100644 index 0000000..14c0e49 --- /dev/null +++ b/BACKUP_INSTALLATION.md @@ -0,0 +1,139 @@ +# Установка системы бэкапа с Backup gem + +## Что было добавлено + +### 1. Гем backup +- Добавлен в `Gemfile`: `gem "backup", "~> 5.0"` + +### 2. Конфигурационные файлы +- `config/backup.rb` - основная конфигурация Backup gem +- `config/sidekiq.yml` - обновлен с расписанием бэкапа + +### 3. Код приложения +- `app/jobs/database_backup_job.rb` - Sidekiq job для автоматического бэкапа +- `lib/tasks/backup.rake` - Rake tasks для управления бэкапами + +### 4. Документация +- `docs/backup/README.md` - обзор системы бэкапа +- `docs/backup/QUICK_START.md` - быстрый старт +- `docs/backup/BACKUP_GEM_SETUP.md` - полная документация + +## Шаги для установки + +### 1. Установите зависимости локально + +```bash +bundle install +``` + +### 2. Проверьте конфигурацию + +Убедитесь, что в `Dockerfile` установлен `sqlite3` (уже есть на строке 16): +```dockerfile +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives +``` + +### 3. Задеплойте приложение + +```bash +# Полный деплой с пересборкой образа +kamal deploy + +# Или только перезапуск если образ уже собран +kamal app boot +``` + +### 4. Проверьте работу + +```bash +# Запустите тестовый бэкап +kamal app exec 'bundle exec rake backup:run' + +# Проверьте созданные бэкапы +kamal app exec 'bundle exec rake backup:list' +``` + +Вы должны увидеть что-то вроде: +``` +Available backups in /backups: + stackoverflow_clone_db.tar.gz (2.45 MB) - 2025-01-20 14:30:15 +``` + +### 5. Проверьте автоматическое расписание + +Откройте Sidekiq Web UI: +``` +http://your-server-ip:3000/sidekiq/cron +``` + +Вы должны увидеть задачу `database_backup` с расписанием `0 2 * * *` (каждый день в 2:00). + +## Что дальше? + +1. **Прочитайте документацию**: `docs/backup/README.md` +2. **Настройте email уведомления** (опционально) +3. **Настройте хранение на S3** (рекомендуется для production) +4. **Протестируйте восстановление** из бэкапа + +## Основные команды + +```bash +# Ручной запуск бэкапа +kamal app exec 'bundle exec rake backup:run' + +# Список бэкапов +kamal app exec 'bundle exec rake backup:list' + +# Очистка старых бэкапов +kamal app exec 'bundle exec rake backup:clean' + +# Просмотр логов +kamal app logs | grep DatabaseBackupJob +``` + +## Параметры по умолчанию + +- **Расписание**: Каждый день в 2:00 ночи +- **Хранение**: Последние 7 бэкапов +- **Место**: Docker volume `stackoverflow_clone_backups` → `/backups` +- **Сжатие**: Gzip (уровень 6) +- **Разбивка**: Файлы > 250 MB разбиваются на части + +## Troubleshooting + +### Ошибка при bundle install + +Если возникает ошибка при установке гема `backup`, попробуйте: +```bash +bundle update backup +``` + +### Бэкап не создается + +1. Проверьте, что Sidekiq запущен: +```bash +docker ps | grep sidekiq +``` + +2. Проверьте логи: +```bash +kamal app logs | grep -i backup +``` + +3. Запустите вручную для диагностики: +```bash +kamal app exec 'bundle exec rake backup:run' +``` + +### Нет доступа к /backups + +Проверьте права доступа: +```bash +kamal app exec 'ls -la /backups' +``` + +## Дополнительная информация + +Полная документация находится в `docs/backup/BACKUP_GEM_SETUP.md`. diff --git a/Gemfile b/Gemfile index fd25785..7d6d978 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,9 @@ gem "sidekiq-cron" # Prometheus metrics for monitoring gem "prometheus_exporter" +# Database backup solution +gem "backup", "~> 3.4.0" + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] diff --git a/Gemfile.lock b/Gemfile.lock index bc53052..8ab78d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,6 +80,9 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) + backup (3.4.0) + open4 (~> 1.3.0) + thor (>= 0.15.4, < 2) base64 (0.3.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) @@ -276,6 +279,7 @@ GEM omniauth (~> 2.0) omniauth-telegram (0.2.1) omniauth (>= 1.0) + open4 (1.3.4) orm_adapter (0.5.0) ostruct (0.6.2) parallel (1.27.0) @@ -519,6 +523,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10.0) + backup bcrypt (~> 3.1.7) bootsnap brakeman diff --git a/app/jobs/database_backup_job.rb b/app/jobs/database_backup_job.rb new file mode 100644 index 0000000..df986a2 --- /dev/null +++ b/app/jobs/database_backup_job.rb @@ -0,0 +1,30 @@ +class DatabaseBackupJob < ApplicationJob + queue_as :default + + def perform + Rails.logger.info "Starting database backup job..." + + config_file = Rails.root.join("config", "backup.rb") + + unless File.exist?(config_file) + Rails.logger.error "Backup configuration file not found: #{config_file}" + raise "Backup configuration file not found" + end + + # Run backup command + command = "backup perform --trigger stackoverflow_clone_db --config-file #{config_file}" + + result = system(command) + + if result + Rails.logger.info "Database backup completed successfully" + else + Rails.logger.error "Database backup failed with exit code: #{$?.exitstatus}" + raise "Database backup failed" + end + rescue => e + Rails.logger.error "Database backup job error: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + raise e + end +end diff --git a/config/backup.rb b/config/backup.rb new file mode 100644 index 0000000..0978101 --- /dev/null +++ b/config/backup.rb @@ -0,0 +1,110 @@ +# encoding: utf-8 + +## +# Backup Configuration for StackOverflow Clone +# Generated for Backup v5.x +## + +# Load Rails environment for access to Rails.root and other constants +require File.expand_path("../../config/environment", __FILE__) + +## +# Define a Backup Model +## +Backup::Model.new(:stackoverflow_clone_db, "SQLite Database Backup") do + ## + # Split [Splitter] + # + # Split the backup file in to chunks of 250 megabytes + # if the backup file size exceeds 250 megabytes + # + split_into_chunks_of 250 + + ## + # SQLite Database [Database] + # + # Backup all production SQLite databases + # + database SQLite do |db| + db.path = Rails.root.join("storage", "production.sqlite3").to_s + db.sqlitedump_utility = "/usr/bin/sqlite3" + end + + # Backup cache database + database SQLite do |db| + db.path = Rails.root.join("storage", "production_cache.sqlite3").to_s + db.sqlitedump_utility = "/usr/bin/sqlite3" + end + + # Backup queue database + database SQLite do |db| + db.path = Rails.root.join("storage", "production_queue.sqlite3").to_s + db.sqlitedump_utility = "/usr/bin/sqlite3" + end + + # Backup cable database + database SQLite do |db| + db.path = Rails.root.join("storage", "production_cable.sqlite3").to_s + db.sqlitedump_utility = "/usr/bin/sqlite3" + end + + ## + # Local (Copy) [Storage] + # + # Store backups locally in the /backups directory + # + store_with Local do |local| + local.path = "/backups" + local.keep = 7 # Keep last 7 backups + end + + ## + # Gzip [Compressor] + # + # Compress the backup using gzip + # + compress_with Gzip do |compression| + compression.level = 6 # Compression level (1-9, default: 6) + end + + ## + # Mail [Notifier] + # + # Send email notification on backup completion + # Uncomment and configure if you want email notifications + # + # notify_by Mail do |mail| + # mail.on_success = false + # mail.on_warning = true + # mail.on_failure = true + # + # mail.from = ENV["MAILER_FROM_EMAIL"] + # mail.to = "admin@example.com" + # mail.address = ENV["SMTP_ADDRESS"] + # mail.port = ENV["SMTP_PORT"] + # mail.domain = ENV["SMTP_DOMAIN"] + # mail.user_name = ENV["SMTP_USERNAME"] + # mail.password = ENV["SMTP_PASSWORD"] + # mail.authentication = "plain" + # mail.encryption = :starttls + # end +end + +## +# Optional: Amazon S3 Storage +# Uncomment to enable S3 backup storage +## +# Backup::Model.new(:stackoverflow_clone_db_s3, "SQLite Database Backup to S3") do +# # ... same database configuration as above ... +# +# store_with S3 do |s3| +# s3.access_key_id = ENV["AWS_ACCESS_KEY_ID"] +# s3.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"] +# s3.region = ENV.fetch("AWS_REGION", "us-east-1") +# s3.bucket = ENV.fetch("S3_BACKUP_BUCKET", "stackoverflow-clone-backups") +# s3.path = "database_backups" +# s3.keep = 30 # Keep last 30 backups on S3 +# end +# +# compress_with Gzip +# end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index f5721a9..9da9900 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -8,3 +8,8 @@ cron: "0 8 * * *" class: "DailyDigestJob" queue: default + + database_backup: + cron: "0 2 * * *" # Every day at 2:00 AM + class: "DatabaseBackupJob" + queue: default diff --git a/lib/tasks/backup.rake b/lib/tasks/backup.rake new file mode 100644 index 0000000..a6fe0d6 --- /dev/null +++ b/lib/tasks/backup.rake @@ -0,0 +1,46 @@ +namespace :backup do + desc "Run database backup using Backup gem" + task :run => :environment do + puts "Starting database backup..." + system("backup perform --trigger stackoverflow_clone_db --config-file #{Rails.root.join('config', 'backup.rb')}") + puts "Backup completed!" + end + + desc "List all available backups" + task :list => :environment do + backup_dir = "/backups" + if Dir.exist?(backup_dir) + puts "Available backups in #{backup_dir}:" + Dir.glob("#{backup_dir}/**/*.tar*").sort.reverse.each do |file| + size = File.size(file).to_f / 1024 / 1024 + mtime = File.mtime(file) + puts " #{File.basename(file)} (#{size.round(2)} MB) - #{mtime.strftime('%Y-%m-%d %H:%M:%S')}" + end + else + puts "Backup directory not found: #{backup_dir}" + end + end + + desc "Clean old backups (keeps last 7)" + task :clean => :environment do + backup_dir = "/backups" + if Dir.exist?(backup_dir) + backups = Dir.glob("#{backup_dir}/**/*.tar*").sort + keep_count = 7 + + if backups.size > keep_count + to_delete = backups[0..-(keep_count + 1)] + puts "Deleting #{to_delete.size} old backup(s)..." + to_delete.each do |file| + puts " Deleting: #{File.basename(file)}" + File.delete(file) + end + puts "Cleanup completed!" + else + puts "No old backups to clean (found #{backups.size} backup(s), keeping #{keep_count})" + end + else + puts "Backup directory not found: #{backup_dir}" + end + end +end From c9a44071b4d754e5c3d8c0a970dc697067bf96af Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 01:02:45 +0300 Subject: [PATCH 12/17] rubocop -a --- app/jobs/database_backup_job.rb | 8 ++++---- lib/tasks/backup.rake | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/jobs/database_backup_job.rb b/app/jobs/database_backup_job.rb index df986a2..29e6981 100644 --- a/app/jobs/database_backup_job.rb +++ b/app/jobs/database_backup_job.rb @@ -3,9 +3,9 @@ class DatabaseBackupJob < ApplicationJob def perform Rails.logger.info "Starting database backup job..." - + config_file = Rails.root.join("config", "backup.rb") - + unless File.exist?(config_file) Rails.logger.error "Backup configuration file not found: #{config_file}" raise "Backup configuration file not found" @@ -13,9 +13,9 @@ def perform # Run backup command command = "backup perform --trigger stackoverflow_clone_db --config-file #{config_file}" - + result = system(command) - + if result Rails.logger.info "Database backup completed successfully" else diff --git a/lib/tasks/backup.rake b/lib/tasks/backup.rake index a6fe0d6..5f6aaca 100644 --- a/lib/tasks/backup.rake +++ b/lib/tasks/backup.rake @@ -1,13 +1,13 @@ namespace :backup do desc "Run database backup using Backup gem" - task :run => :environment do + task run: :environment do puts "Starting database backup..." system("backup perform --trigger stackoverflow_clone_db --config-file #{Rails.root.join('config', 'backup.rb')}") puts "Backup completed!" end desc "List all available backups" - task :list => :environment do + task list: :environment do backup_dir = "/backups" if Dir.exist?(backup_dir) puts "Available backups in #{backup_dir}:" @@ -22,12 +22,12 @@ namespace :backup do end desc "Clean old backups (keeps last 7)" - task :clean => :environment do + task clean: :environment do backup_dir = "/backups" if Dir.exist?(backup_dir) backups = Dir.glob("#{backup_dir}/**/*.tar*").sort keep_count = 7 - + if backups.size > keep_count to_delete = backups[0..-(keep_count + 1)] puts "Deleting #{to_delete.size} old backup(s)..." From 044949420ead29fc0773ca2781f7c3104b6a9c29 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 01:14:28 +0300 Subject: [PATCH 13/17] chore --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8ab78d4..0867826 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -523,7 +523,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10.0) - backup + backup (~> 3.4.0) bcrypt (~> 3.1.7) bootsnap brakeman From 4ba2160023d31c8b46cdebfe279862d9531d92e3 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 01:23:11 +0300 Subject: [PATCH 14/17] fix: add security best practices --- app/jobs/database_backup_job.rb | 6 ++---- lib/tasks/backup.rake | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/jobs/database_backup_job.rb b/app/jobs/database_backup_job.rb index 29e6981..6adbdc7 100644 --- a/app/jobs/database_backup_job.rb +++ b/app/jobs/database_backup_job.rb @@ -11,10 +11,8 @@ def perform raise "Backup configuration file not found" end - # Run backup command - command = "backup perform --trigger stackoverflow_clone_db --config-file #{config_file}" - - result = system(command) + # Run backup command with safe argument passing to prevent command injection + result = system("backup", "perform", "--trigger", "stackoverflow_clone_db", "--config-file", config_file.to_s) if result Rails.logger.info "Database backup completed successfully" diff --git a/lib/tasks/backup.rake b/lib/tasks/backup.rake index 5f6aaca..c32f713 100644 --- a/lib/tasks/backup.rake +++ b/lib/tasks/backup.rake @@ -2,7 +2,8 @@ namespace :backup do desc "Run database backup using Backup gem" task run: :environment do puts "Starting database backup..." - system("backup perform --trigger stackoverflow_clone_db --config-file #{Rails.root.join('config', 'backup.rb')}") + config_file = Rails.root.join("config", "backup.rb").to_s + system("backup", "perform", "--trigger", "stackoverflow_clone_db", "--config-file", config_file) puts "Backup completed!" end From eb1dd57b2fb7d98972a84614fdc313fa2f1ee8bb Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 01:32:51 +0300 Subject: [PATCH 15/17] fix prometheus and sidekiq configs --- config/initializers/prometheus.rb | 3 ++- config/initializers/sidekiq.rb | 3 ++- config/prometheus.yml | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb index cba1af1..3b8a3f5 100644 --- a/config/initializers/prometheus.rb +++ b/config/initializers/prometheus.rb @@ -8,8 +8,9 @@ # Start Prometheus exporter server on port 9394 # This runs in a separate process and collects metrics + # Use 127.0.0.1 instead of localhost to avoid DNS resolution issues PrometheusExporter::Client.default = PrometheusExporter::Client.new( - host: "localhost", + host: "127.0.0.1", port: 9394 ) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index bba8093..a6e0522 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -9,8 +9,9 @@ require "prometheus_exporter/instrumentation" # Setup Prometheus client to connect to exporter + # Use 127.0.0.1 instead of localhost to avoid DNS resolution issues PrometheusExporter::Client.default = PrometheusExporter::Client.new( - host: "localhost", + host: "127.0.0.1", port: 9394 ) diff --git a/config/prometheus.yml b/config/prometheus.yml index 3cd0e6b..8eff19b 100644 --- a/config/prometheus.yml +++ b/config/prometheus.yml @@ -17,11 +17,11 @@ scrape_configs: role: 'sidekiq' environment: 'production' - # Rails web metrics (web container - using IP address) - # Note: IP address may change after redeployment, update manually if needed + # Rails web metrics (web container - using container name) + # Container name remains stable across redeployments - job_name: 'rails' static_configs: - - targets: ['172.18.0.10:9394'] + - targets: ['stackoverflow_clone-web:9394'] labels: service: 'stackoverflow_clone' role: 'web' From 51e56bf5f0a1587967832f6522c5929162606cae Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 21 Oct 2025 01:44:26 +0300 Subject: [PATCH 16/17] docs --- docs/CHANGELOG_MONITORING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG_MONITORING.md b/docs/CHANGELOG_MONITORING.md index 6e3c367..4f2571b 100644 --- a/docs/CHANGELOG_MONITORING.md +++ b/docs/CHANGELOG_MONITORING.md @@ -36,7 +36,7 @@ #### Доступ: - **Prometheus:** http://90.156.228.95:9090 -- **Grafana:** http://90.156.228.95:3001 (admin/admin) +- **Grafana:** http://90.156.228.95:3001 - **Metrics endpoint:** http://90.156.228.95:9394/metrics --- From 9631be5328709f7d8703670a61d9efbcb22dc862 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Sat, 25 Oct 2025 02:35:30 +0300 Subject: [PATCH 17/17] feat: add fragment cashing --- app/helpers/application_helper.rb | 6 +++ app/views/answers/_answer.html.erb | 10 ++-- app/views/questions/_answers.html.erb | 4 +- app/views/questions/show.html.erb | 72 ++++++++++++++------------- 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cf716f9..ab90c8b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -14,4 +14,10 @@ def formatted_date(date) def time_ago(date) time_ago_in_words(date) + " ago" end + + def cacheable_user_id + current_user&.id + rescue Devise::MissingWarden + nil + end end diff --git a/app/views/answers/_answer.html.erb b/app/views/answers/_answer.html.erb index ee93f15..a21fa13 100644 --- a/app/views/answers/_answer.html.erb +++ b/app/views/answers/_answer.html.erb @@ -126,10 +126,12 @@

Comments

<% answer.comments.order(created_at: :asc).each do |comment| %> - <% begin %> - <%= render "comments/comment", comment: comment, show_controls: policy(comment).destroy? %> - <% rescue Devise::MissingWarden %> - <%= render "comments/comment", comment: comment, show_controls: false %> + <%= cache [comment, cacheable_user_id] do %> + <% begin %> + <%= render "comments/comment", comment: comment, show_controls: policy(comment).destroy? %> + <% rescue Devise::MissingWarden %> + <%= render "comments/comment", comment: comment, show_controls: false %> + <% end %> <% end %> <% end %>
diff --git a/app/views/questions/_answers.html.erb b/app/views/questions/_answers.html.erb index cc46e06..0861752 100644 --- a/app/views/questions/_answers.html.erb +++ b/app/views/questions/_answers.html.erb @@ -1,5 +1,7 @@
<% answers.each do |answer| %> - <%= render "answers/answer", answer: answer %> + <%= cache [answer, cacheable_user_id] do %> + <%= render "answers/answer", answer: answer %> + <% end %> <% end %>
diff --git a/app/views/questions/show.html.erb b/app/views/questions/show.html.erb index 0d18218..482a9d5 100644 --- a/app/views/questions/show.html.erb +++ b/app/views/questions/show.html.erb @@ -10,40 +10,42 @@

<%= @question.title %>

-
-
- <%= simple_format(@question.body) %> -
- - <% if @question.links.present? %> -
- <%= render 'shared/links', resource: @question %> -
- <% end %> - - <%= render 'shared/reward_display', reward: @question.reward %> - - <% if @question.files.attached? %> -
-

Attached Files:

-
    - <% @question.files.each do |file| %> -
  • -
    - <%= file.filename %> -
    - <% if policy(file).destroy? %> - <%= link_to '✕', attachment_path(file), - method: :delete, - data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, - class: "text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400" %> - <% end %> -
  • - <% end %> -
+ <%= cache [@question, cacheable_user_id, :question_body] do %> +
+
+ <%= simple_format(@question.body) %> +
+ + <% if @question.links.present? %> +
+ <%= render 'shared/links', resource: @question %> +
+ <% end %> + + <%= render 'shared/reward_display', reward: @question.reward %> + + <% if @question.files.attached? %> +
+

Attached Files:

+
    + <% @question.files.each do |file| %> +
  • +
    + <%= file.filename %> +
    + <% if policy(file).destroy? %> + <%= link_to '✕', attachment_path(file), + method: :delete, + data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, + class: "text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400" %> + <% end %> +
  • + <% end %> +
+
+ <% end %>
- <% end %> -
+ <% end %>
@@ -90,7 +92,9 @@
<% @question.comments.order(created_at: :asc).each do |comment| %> - <%= render "comments/comment", comment: comment, show_controls: policy(comment).destroy? %> + <%= cache [comment, cacheable_user_id] do %> + <%= render "comments/comment", comment: comment, show_controls: policy(comment).destroy? %> + <% end %> <% end %>
<% if user_signed_in? %>