From 391f703dec168378bb125f5036394d6bb46030ce Mon Sep 17 00:00:00 2001 From: SavelyG Date: Wed, 5 Feb 2025 14:14:21 +0000 Subject: [PATCH] add Dockerfile --- .dockerignore | 7 ++ .gitignore | 3 +- Dockerfile | 21 +++++ cmd/main.go | 181 +----------------------------------- internal/app/app.go | 181 ++++++++++++++++++++++++++++++++++++ tests/unit/exporter_test.go | 25 ++--- 6 files changed, 227 insertions(+), 191 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 internal/app/app.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..25da975 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.devcontainer/ +tests/ +*.md +Dockerfile +.dockerignore +.gitignore +.git \ No newline at end of file diff --git a/.gitignore b/.gitignore index e63ee1a..c2efc81 100755 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -.devcontainer/ -tmp/ \ No newline at end of file +.devcontainer/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e299a09 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.23.5 as builder + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . . + +RUN go build -o /app/main ./cmd + +FROM golang:1.23.5-alpine + +WORKDIR /app + +COPY --from=builder /app/main . + +EXPOSE 9101 + +CMD ["./main"] diff --git a/cmd/main.go b/cmd/main.go index b692611..089f24c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,195 +1,22 @@ package main import ( - "encoding/json" - "fmt" - "io" + "exporter/internal/app" "log" "net/http" "os" - "sync" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) -// Создаём кастомный реестр -var customRegistry = prometheus.NewRegistry() - -// Структура JSON -type MetricRequest struct { - ID string `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - Method string `json:"method"` - Type string `json:"type"` - Metrics map[string]float64 `json:"metrics"` -} - -// Экспортёр метрик -type MetricsExporter struct { - gaugeMetrics map[string]*prometheus.GaugeVec - counterMetrics map[string]*prometheus.CounterVec - histogramMetrics map[string]*prometheus.HistogramVec - summaryMetrics map[string]*prometheus.SummaryVec - mu sync.Mutex // Защита от одновременного доступа -} - -// Создаём новый экспортёр -func NewMetricsExporter() *MetricsExporter { - return &MetricsExporter{ - gaugeMetrics: make(map[string]*prometheus.GaugeVec), - counterMetrics: make(map[string]*prometheus.CounterVec), - histogramMetrics: make(map[string]*prometheus.HistogramVec), - summaryMetrics: make(map[string]*prometheus.SummaryVec), - } -} - -// Обновление или создание метрики -func (me *MetricsExporter) UpdateMetric(request MetricRequest) { - me.mu.Lock() - defer me.mu.Unlock() - - for metricName, value := range request.Metrics { - // Уникальное имя метрики - fullMetricName := fmt.Sprintf("vks_%s_%s", request.ID, metricName) - - // Лейблы - labels := []string{"name", "url", "method"} - labelValues := []string{request.Name, request.URL, request.Method} - - // Обработка метрик в зависимости от типа - switch request.Type { - case "gauge": - me.updateGauge(fullMetricName, labels, labelValues, value) - case "counter": - me.updateCounter(fullMetricName, labels, labelValues, value) - case "histogram": - me.updateHistogram(fullMetricName, labels, labelValues, value) - case "summary": - me.updateSummary(fullMetricName, labels, labelValues, value) - default: - log.Printf("Неподдерживаемый тип метрики: %s\n", request.Type) - } - } -} - -// Обновление `Gauge` метрик -func (me *MetricsExporter) updateGauge(name string, labels []string, labelValues []string, value float64) { - if _, exists := me.gaugeMetrics[name]; !exists { - gaugeVec := prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: name, - Help: fmt.Sprintf("Gauge metric %s", name), - }, - labels, - ) - customRegistry.MustRegister(gaugeVec) - me.gaugeMetrics[name] = gaugeVec - } - me.gaugeMetrics[name].WithLabelValues(labelValues...).Set(value) - log.Printf("Gauge обновлён: %s = %f\n", name, value) -} - -// Обновление `Counter` метрик -func (me *MetricsExporter) updateCounter(name string, labels []string, labelValues []string, value float64) { - if value < 0 { - log.Printf("Ошибка: Counter %s не может быть отрицательным\n", name) - return - } - if _, exists := me.counterMetrics[name]; !exists { - counterVec := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: name, - Help: fmt.Sprintf("Counter metric %s", name), - }, - labels, - ) - customRegistry.MustRegister(counterVec) - me.counterMetrics[name] = counterVec - } - me.counterMetrics[name].WithLabelValues(labelValues...).Add(value) - log.Printf("Counter обновлён: %s += %f\n", name, value) -} - -// Обновление `Histogram` метрик -func (me *MetricsExporter) updateHistogram(name string, labels []string, labelValues []string, value float64) { - if _, exists := me.histogramMetrics[name]; !exists { - histogramVec := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: name, - Help: fmt.Sprintf("Histogram metric %s", name), - Buckets: prometheus.LinearBuckets(10, 10, 10), // Пример диапазонов - }, - labels, - ) - customRegistry.MustRegister(histogramVec) - me.histogramMetrics[name] = histogramVec - } - me.histogramMetrics[name].WithLabelValues(labelValues...).Observe(value) - log.Printf("Histogram обновлён: %s = %f\n", name, value) -} - -// Обновление `Summary` метрик -func (me *MetricsExporter) updateSummary(name string, labels []string, labelValues []string, value float64) { - if _, exists := me.summaryMetrics[name]; !exists { - summaryVec := prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Name: name, - Help: fmt.Sprintf("Summary metric %s", name), - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, // Пример целей - }, - labels, - ) - customRegistry.MustRegister(summaryVec) - me.summaryMetrics[name] = summaryVec - } - me.summaryMetrics[name].WithLabelValues(labelValues...).Observe(value) - log.Printf("Summary обновлён: %s = %f\n", name, value) -} - -// Обработчик для приёма JSON -func (me *MetricsExporter) JSONHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - log.Printf("Неверный метод: %s (ожидался POST)", r.Method) - http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed) - return - } - - // Читаем тело запроса - body, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Ошибка чтения тела запроса: %s\n", err) - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - defer r.Body.Close() - - // Парсим JSON - var request MetricRequest - if err := json.Unmarshal(body, &request); err != nil { - log.Printf("Ошибка парсинга JSON: %s\nТело запроса: %s\n", err, string(body)) - http.Error(w, "Invalid JSON format", http.StatusBadRequest) - return - } - - // Обновляем метрики - log.Printf("Обновление метрик для запроса ID: %s\n", request.ID) - me.UpdateMetric(request) - - w.WriteHeader(http.StatusOK) - w.Write([]byte("Metrics updated")) - log.Printf("Метрики обновлены успешно для ID: %s\n", request.ID) -} - func main() { log.SetOutput(os.Stdout) // Логируем в стандартный вывод log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) - exporter := NewMetricsExporter() + exporter := app.NewMetricsExporter() // Используем кастомный реестр в обработчике /metrics - http.Handle("/metrics", promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{})) + http.Handle("/metrics", promhttp.HandlerFor(app.CustomRegistry, promhttp.HandlerOpts{})) http.HandleFunc("/update", exporter.JSONHandler) // Обработчик для приёма JSON port := ":9101" @@ -199,4 +26,4 @@ func main() { } } -//TODO: сделать переменные окружения, настроить канал, дописать тесты +//TODO: сделать переменные окружения, настроить канал, дописать юнит тесты, добавить интеграционные тесты diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..fcc5bd0 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,181 @@ +package app + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +// Создаём кастомный реестр +var CustomRegistry = prometheus.NewRegistry() + +// Структура JSON +type MetricRequest struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Method string `json:"method"` + Type string `json:"type"` + Metrics map[string]float64 `json:"metrics"` +} + +// Экспортёр метрик +type MetricsExporter struct { + GaugeMetrics map[string]*prometheus.GaugeVec + CounterMetrics map[string]*prometheus.CounterVec + HistogramMetrics map[string]*prometheus.HistogramVec + SummaryMetrics map[string]*prometheus.SummaryVec + mu sync.Mutex // Защита от одновременного доступа +} + +// Создаём новый экспортёр +func NewMetricsExporter() *MetricsExporter { + return &MetricsExporter{ + GaugeMetrics: make(map[string]*prometheus.GaugeVec), + CounterMetrics: make(map[string]*prometheus.CounterVec), + HistogramMetrics: make(map[string]*prometheus.HistogramVec), + SummaryMetrics: make(map[string]*prometheus.SummaryVec), + } +} + +// Обновление или создание метрики +func (me *MetricsExporter) UpdateMetric(request MetricRequest) { + me.mu.Lock() + defer me.mu.Unlock() + + for metricName, value := range request.Metrics { + // Уникальное имя метрики + fullMetricName := fmt.Sprintf("vks_%s_%s", request.ID, metricName) + + // Лейблы + labels := []string{"name", "url", "method"} + labelValues := []string{request.Name, request.URL, request.Method} + + // Обработка метрик в зависимости от типа + switch request.Type { + case "gauge": + me.updateGauge(fullMetricName, labels, labelValues, value) + case "counter": + me.updateCounter(fullMetricName, labels, labelValues, value) + case "histogram": + me.updateHistogram(fullMetricName, labels, labelValues, value) + case "summary": + me.updateSummary(fullMetricName, labels, labelValues, value) + default: + log.Printf("Неподдерживаемый тип метрики: %s\n", request.Type) + } + } +} + +// Обновление `Gauge` метрик +func (me *MetricsExporter) updateGauge(name string, labels []string, labelValues []string, value float64) { + if _, exists := me.GaugeMetrics[name]; !exists { + gaugeVec := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: name, + Help: fmt.Sprintf("Gauge metric %s", name), + }, + labels, + ) + CustomRegistry.MustRegister(gaugeVec) + me.GaugeMetrics[name] = gaugeVec + } + me.GaugeMetrics[name].WithLabelValues(labelValues...).Set(value) + log.Printf("Gauge обновлён: %s = %f\n", name, value) +} + +// Обновление `Counter` метрик +func (me *MetricsExporter) updateCounter(name string, labels []string, labelValues []string, value float64) { + if value < 0 { + log.Printf("Ошибка: Counter %s не может быть отрицательным\n", name) + return + } + if _, exists := me.CounterMetrics[name]; !exists { + counterVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: name, + Help: fmt.Sprintf("Counter metric %s", name), + }, + labels, + ) + CustomRegistry.MustRegister(counterVec) + me.CounterMetrics[name] = counterVec + } + me.CounterMetrics[name].WithLabelValues(labelValues...).Add(value) + log.Printf("Counter обновлён: %s += %f\n", name, value) +} + +// Обновление `Histogram` метрик +func (me *MetricsExporter) updateHistogram(name string, labels []string, labelValues []string, value float64) { + if _, exists := me.HistogramMetrics[name]; !exists { + histogramVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: name, + Help: fmt.Sprintf("Histogram metric %s", name), + Buckets: prometheus.LinearBuckets(10, 10, 10), // Пример диапазонов + }, + labels, + ) + CustomRegistry.MustRegister(histogramVec) + me.HistogramMetrics[name] = histogramVec + } + me.HistogramMetrics[name].WithLabelValues(labelValues...).Observe(value) + log.Printf("Histogram обновлён: %s = %f\n", name, value) +} + +// Обновление `Summary` метрик +func (me *MetricsExporter) updateSummary(name string, labels []string, labelValues []string, value float64) { + if _, exists := me.SummaryMetrics[name]; !exists { + summaryVec := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: name, + Help: fmt.Sprintf("Summary metric %s", name), + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, // Пример целей + }, + labels, + ) + CustomRegistry.MustRegister(summaryVec) + me.SummaryMetrics[name] = summaryVec + } + me.SummaryMetrics[name].WithLabelValues(labelValues...).Observe(value) + log.Printf("Summary обновлён: %s = %f\n", name, value) +} + +// Обработчик для приёма JSON +func (me *MetricsExporter) JSONHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + log.Printf("Неверный метод: %s (ожидался POST)", r.Method) + http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed) + return + } + + // Читаем тело запроса + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("Ошибка чтения тела запроса: %s\n", err) + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + defer r.Body.Close() + + // Парсим JSON + var request MetricRequest + if err := json.Unmarshal(body, &request); err != nil { + log.Printf("Ошибка парсинга JSON: %s\nТело запроса: %s\n", err, string(body)) + http.Error(w, "Invalid JSON format", http.StatusBadRequest) + return + } + + // Обновляем метрики + log.Printf("Обновление метрик для запроса ID: %s\n", request.ID) + me.UpdateMetric(request) + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Metrics updated")) + log.Printf("Метрики обновлены успешно для ID: %s\n", request.ID) +} diff --git a/tests/unit/exporter_test.go b/tests/unit/exporter_test.go index 82411ed..84d285a 100644 --- a/tests/unit/exporter_test.go +++ b/tests/unit/exporter_test.go @@ -1,8 +1,9 @@ -package main +package test import ( "bytes" "encoding/json" + "exporter/internal/app" "io" "net/http" "net/http/httptest" @@ -15,12 +16,12 @@ import ( // Тест: успешное обновление метрик через /update func TestJSONHandler_ValidRequest(t *testing.T) { - exporter := NewMetricsExporter() + exporter := app.NewMetricsExporter() server := httptest.NewServer(http.HandlerFunc(exporter.JSONHandler)) defer server.Close() // JSON-запрос - requestData := MetricRequest{ + requestData := app.MetricRequest{ ID: "test_api", Name: "Test API", URL: "http://localhost/api", @@ -48,7 +49,7 @@ func TestJSONHandler_ValidRequest(t *testing.T) { // Тест: успешная обработка некорректного JSON func TestJSONHandler_InvalidJSON(t *testing.T) { - exporter := NewMetricsExporter() + exporter := app.NewMetricsExporter() server := httptest.NewServer(http.HandlerFunc(exporter.JSONHandler)) defer server.Close() @@ -60,9 +61,9 @@ func TestJSONHandler_InvalidJSON(t *testing.T) { // Тест: проверка создания и обновления Gauge метрики func TestUpdateMetric_Gauge(t *testing.T) { - exporter := NewMetricsExporter() + exporter := app.NewMetricsExporter() - request := MetricRequest{ + request := app.MetricRequest{ ID: "test", Name: "Test Gauge", URL: "http://test.com", @@ -77,16 +78,16 @@ func TestUpdateMetric_Gauge(t *testing.T) { exporter.UpdateMetric(request) // Проверяем, что метрика существует - metric, exists := exporter.gaugeMetrics["vks_test_load"] + metric, exists := exporter.GaugeMetrics["vks_test_load"] assert.True(t, exists, "Метрика должна быть зарегистрирована") assert.NotNil(t, metric, "Метрика не должна быть nil") } // Тест: проверка создания и увеличения Counter метрики func TestUpdateMetric_Counter(t *testing.T) { - exporter := NewMetricsExporter() + exporter := app.NewMetricsExporter() - request := MetricRequest{ + request := app.MetricRequest{ ID: "test", Name: "Test Counter", URL: "http://test.com", @@ -102,7 +103,7 @@ func TestUpdateMetric_Counter(t *testing.T) { exporter.UpdateMetric(request) // Проверяем, что метрика существует - metric, exists := exporter.counterMetrics["vks_test_requests"] + metric, exists := exporter.CounterMetrics["vks_test_requests"] assert.True(t, exists, "Метрика должна быть зарегистрирована") assert.NotNil(t, metric, "Метрика не должна быть nil") } @@ -114,10 +115,10 @@ func TestMetricsEndpoint(t *testing.T) { Name: "test_metric", Help: "Test metric for /metrics endpoint", }) - customRegistry.MustRegister(gauge) + app.CustomRegistry.MustRegister(gauge) gauge.Set(50.5) - server := httptest.NewServer(promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{})) + server := httptest.NewServer(promhttp.HandlerFor(app.CustomRegistry, promhttp.HandlerOpts{})) defer server.Close() // Делаем GET-запрос к /metrics