add Dockerfile

rc
SavelyG 2025-02-05 14:14:21 +00:00
parent f836ae401b
commit 391f703dec
6 changed files with 227 additions and 191 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.devcontainer/
tests/
*.md
Dockerfile
.dockerignore
.gitignore
.git

1
.gitignore vendored
View File

@ -1,2 +1 @@
.devcontainer/ .devcontainer/
tmp/

21
Dockerfile Normal file
View File

@ -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"]

View File

@ -1,195 +1,22 @@
package main package main
import ( import (
"encoding/json" "exporter/internal/app"
"fmt"
"io"
"log" "log"
"net/http" "net/http"
"os" "os"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "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() { func main() {
log.SetOutput(os.Stdout) // Логируем в стандартный вывод log.SetOutput(os.Stdout) // Логируем в стандартный вывод
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
exporter := NewMetricsExporter() exporter := app.NewMetricsExporter()
// Используем кастомный реестр в обработчике /metrics // Используем кастомный реестр в обработчике /metrics
http.Handle("/metrics", promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{})) http.Handle("/metrics", promhttp.HandlerFor(app.CustomRegistry, promhttp.HandlerOpts{}))
http.HandleFunc("/update", exporter.JSONHandler) // Обработчик для приёма JSON http.HandleFunc("/update", exporter.JSONHandler) // Обработчик для приёма JSON
port := ":9101" port := ":9101"
@ -199,4 +26,4 @@ func main() {
} }
} }
//TODO: сделать переменные окружения, настроить канал, дописать тесты //TODO: сделать переменные окружения, настроить канал, дописать юнит тесты, добавить интеграционные тесты

181
internal/app/app.go Normal file
View File

@ -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)
}

View File

@ -1,8 +1,9 @@
package main package test
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"exporter/internal/app"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -15,12 +16,12 @@ import (
// Тест: успешное обновление метрик через /update // Тест: успешное обновление метрик через /update
func TestJSONHandler_ValidRequest(t *testing.T) { func TestJSONHandler_ValidRequest(t *testing.T) {
exporter := NewMetricsExporter() exporter := app.NewMetricsExporter()
server := httptest.NewServer(http.HandlerFunc(exporter.JSONHandler)) server := httptest.NewServer(http.HandlerFunc(exporter.JSONHandler))
defer server.Close() defer server.Close()
// JSON-запрос // JSON-запрос
requestData := MetricRequest{ requestData := app.MetricRequest{
ID: "test_api", ID: "test_api",
Name: "Test API", Name: "Test API",
URL: "http://localhost/api", URL: "http://localhost/api",
@ -48,7 +49,7 @@ func TestJSONHandler_ValidRequest(t *testing.T) {
// Тест: успешная обработка некорректного JSON // Тест: успешная обработка некорректного JSON
func TestJSONHandler_InvalidJSON(t *testing.T) { func TestJSONHandler_InvalidJSON(t *testing.T) {
exporter := NewMetricsExporter() exporter := app.NewMetricsExporter()
server := httptest.NewServer(http.HandlerFunc(exporter.JSONHandler)) server := httptest.NewServer(http.HandlerFunc(exporter.JSONHandler))
defer server.Close() defer server.Close()
@ -60,9 +61,9 @@ func TestJSONHandler_InvalidJSON(t *testing.T) {
// Тест: проверка создания и обновления Gauge метрики // Тест: проверка создания и обновления Gauge метрики
func TestUpdateMetric_Gauge(t *testing.T) { func TestUpdateMetric_Gauge(t *testing.T) {
exporter := NewMetricsExporter() exporter := app.NewMetricsExporter()
request := MetricRequest{ request := app.MetricRequest{
ID: "test", ID: "test",
Name: "Test Gauge", Name: "Test Gauge",
URL: "http://test.com", URL: "http://test.com",
@ -77,16 +78,16 @@ func TestUpdateMetric_Gauge(t *testing.T) {
exporter.UpdateMetric(request) exporter.UpdateMetric(request)
// Проверяем, что метрика существует // Проверяем, что метрика существует
metric, exists := exporter.gaugeMetrics["vks_test_load"] metric, exists := exporter.GaugeMetrics["vks_test_load"]
assert.True(t, exists, "Метрика должна быть зарегистрирована") assert.True(t, exists, "Метрика должна быть зарегистрирована")
assert.NotNil(t, metric, "Метрика не должна быть nil") assert.NotNil(t, metric, "Метрика не должна быть nil")
} }
// Тест: проверка создания и увеличения Counter метрики // Тест: проверка создания и увеличения Counter метрики
func TestUpdateMetric_Counter(t *testing.T) { func TestUpdateMetric_Counter(t *testing.T) {
exporter := NewMetricsExporter() exporter := app.NewMetricsExporter()
request := MetricRequest{ request := app.MetricRequest{
ID: "test", ID: "test",
Name: "Test Counter", Name: "Test Counter",
URL: "http://test.com", URL: "http://test.com",
@ -102,7 +103,7 @@ func TestUpdateMetric_Counter(t *testing.T) {
exporter.UpdateMetric(request) exporter.UpdateMetric(request)
// Проверяем, что метрика существует // Проверяем, что метрика существует
metric, exists := exporter.counterMetrics["vks_test_requests"] metric, exists := exporter.CounterMetrics["vks_test_requests"]
assert.True(t, exists, "Метрика должна быть зарегистрирована") assert.True(t, exists, "Метрика должна быть зарегистрирована")
assert.NotNil(t, metric, "Метрика не должна быть nil") assert.NotNil(t, metric, "Метрика не должна быть nil")
} }
@ -114,10 +115,10 @@ func TestMetricsEndpoint(t *testing.T) {
Name: "test_metric", Name: "test_metric",
Help: "Test metric for /metrics endpoint", Help: "Test metric for /metrics endpoint",
}) })
customRegistry.MustRegister(gauge) app.CustomRegistry.MustRegister(gauge)
gauge.Set(50.5) gauge.Set(50.5)
server := httptest.NewServer(promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{})) server := httptest.NewServer(promhttp.HandlerFor(app.CustomRegistry, promhttp.HandlerOpts{}))
defer server.Close() defer server.Close()
// Делаем GET-запрос к /metrics // Делаем GET-запрос к /metrics