← Назад к вопросам

HTTP сервис с Prometheus метриками

1.0 Junior🔥 121 комментариев
#Основы Go

Условие

Напишите HTTP сервис, который слушает входящие запросы, преобразует их в запрос к PostgreSQL, выполняет запрос и возвращает ответ клиенту.

Требования

  1. Ограничить максимальное количество одновременных коннектов к БД
  2. Добавить Prometheus метрики на вызовы:
    • Количество запросов
    • Время выполнения запросов
    • Количество ошибок

Сигнатура

type Server struct {
    db         *sql.DB
    maxConns   int
    // ваши поля
}

func NewServer(db *sql.DB, maxConns int) *Server
func (s *Server) HandleQuery(w http.ResponseWriter, r *http.Request)

Это реальное тестовое задание

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

HTTP сервис с Prometheus метриками - реальное решение

Описание задачи

Нужно реализовать HTTP сервис, который:

  • Принимает запросы от клиентов
  • Выполняет SQL запросы к PostgreSQL
  • Ограничивает количество одновременных соединений
  • Собирает Prometheus метрики
  • Возвращает результаты в JSON

Архитектурное решение

Компоненты:

  1. Семафор (semaphore) - ограничение одновременных коннектов
  2. Prometheus metrics - счетчики и гистограммы
  3. Middleware - сбор метрик для каждого запроса
  4. Connection pooling - встроено в database/sql

Реализация

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
    
    _ "github.com/lib/pq"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    queryCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_queries_total",
            Help: "Total number of HTTP queries processed",
        },
        []string{"status"},
    )
    
    queryDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_query_duration_seconds",
            Help:    "Time spent processing queries",
            Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1},
        },
        []string{"operation"},
    )
    
    dbErrors = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "db_errors_total",
            Help: "Total number of database errors",
        },
        []string{"error_type"},
    )
    
    activeConnections = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_db_connections",
            Help: "Number of active database connections",
        },
    )
)

func init() {
    prometheus.MustRegister(queryCounter)
    prometheus.MustRegister(queryDuration)
    prometheus.MustRegister(dbErrors)
    prometheus.MustRegister(activeConnections)
}

type Server struct {
    db          *sql.DB
    maxConns    int
    semaphore   chan struct{}
    activeCount int32
    mu          sync.Mutex
}

func NewServer(db *sql.DB, maxConns int) *Server {
    return &Server{
        db:        db,
        maxConns:  maxConns,
        semaphore: make(chan struct{}, maxConns),
    }
}

type QueryRequest struct {
    Query string        `json:"query"`
    Args  []interface{} `json:"args"`
}

type QueryResponse struct {
    Columns []string        `json:"columns"`
    Rows    [][]interface{} `json:"rows"`
    Count   int             `json:"count"`
}

func (s *Server) HandleQuery(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    
    var req QueryRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
        queryCounter.WithLabelValues("error").Inc()
        dbErrors.WithLabelValues("parse_error").Inc()
        return
    }
    
    s.semaphore <- struct{}{}
    defer func() { <-s.semaphore }()
    
    s.mu.Lock()
    s.activeCount++
    activeConnections.Set(float64(s.activeCount))
    s.mu.Unlock()
    
    defer func() {
        s.mu.Lock()
        s.activeCount--
        activeConnections.Set(float64(s.activeCount))
        s.mu.Unlock()
    }()
    
    ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
    defer cancel()
    
    rows, err := s.db.QueryContext(ctx, req.Query, req.Args...)
    if err != nil {
        http.Error(w, fmt.Sprintf("Query error: %v", err), http.StatusInternalServerError)
        queryCounter.WithLabelValues("error").Inc()
        dbErrors.WithLabelValues("query_error").Inc()
        queryDuration.WithLabelValues("query").Observe(time.Since(start).Seconds())
        return
    }
    defer rows.Close()
    
    columns, err := rows.Columns()
    if err != nil {
        http.Error(w, fmt.Sprintf("Columns error: %v", err), http.StatusInternalServerError)
        dbErrors.WithLabelValues("columns_error").Inc()
        return
    }
    
    var result QueryResponse
    result.Columns = columns
    result.Rows = make([][]interface{}, 0)
    
    for rows.Next() {
        values := make([]interface{}, len(columns))
        valuePtrs := make([]interface{}, len(columns))
        for i := range columns {
            valuePtrs[i] = &values[i]
        }
        
        if err := rows.Scan(valuePtrs...); err != nil {
            http.Error(w, fmt.Sprintf("Scan error: %v", err), http.StatusInternalServerError)
            dbErrors.WithLabelValues("scan_error").Inc()
            return
        }
        
        result.Rows = append(result.Rows, values)
    }
    
    result.Count = len(result.Rows)
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(result)
    
    queryCounter.WithLabelValues("success").Inc()
    queryDuration.WithLabelValues("query").Observe(time.Since(start).Seconds())
}

func main() {
    dsn := "postgres://user:password@localhost/dbname?sslmode=disable"
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)
    
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
    
    server := NewServer(db, 20)
    
    http.HandleFunc("/query", server.HandleQuery)
    http.Handle("/metrics", promhttp.Handler())
    
    fmt.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Ключевые компоненты

1. Семафор через канал:

semaphore := make(chan struct{}, maxConns)
s.semaphore <- struct{}{}
<-s.semaphore

2. Prometheus метрики:

  • queryCounter - счетчик успешных/неудачных запросов
  • queryDuration - гистограмма времени выполнения
  • dbErrors - счетчик ошибок по типам
  • activeConnections - текущее количество активных соединений

3. Connection pooling:

db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

Анализ сложности

  • Time: O(n) где n - количество строк результата
  • Space: O(n) для хранения результата
  • Concurrency: O(maxConns) одновременных запросов

Best Practices

  1. Используй context.WithTimeout - всегда ставь таймауты на БД запросы
  2. Настраивай пул соединений - SetMaxOpenConns, SetMaxIdleConns
  3. Собирай метрики - Prometheus для мониторинга в production
  4. Обработка ошибок - разные типы ошибок требуют разных действий
  5. Graceful shutdown - закрывай БД перед выходом