diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..78eb298 --- /dev/null +++ b/.air.toml @@ -0,0 +1,10 @@ +root = "." # Корень проекта +tmp_dir = "tmp" + +[build] + binary = "app" # Имя исполняемого файла + cmd = "go build -o app main.go" + include_ext = ["go", "tpl", "tmpl", "html"] + +[run] + cmd = "./app" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..e63ee1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.devcontainer/ +tmp/ \ No newline at end of file diff --git a/README.md b/README.md index 000854b..9178a4d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# Exporter - +# Exporter \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d797c85 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + exporter: + image: cosmtrek/air:v1.61.7 + volumes: + - .:/app + ports: + - "9101:9101" + command: air -c .air.toml + + prometheus: + image: prom/prometheus:v3.1.0 + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d9e8950 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module prometheus_exporter + +go 1.23.5 + +require github.com/prometheus/client_golang v1.20.5 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d5318cf --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5319009 --- /dev/null +++ b/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "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"` // "gauge", "counter" и т.д. + Metrics map[string]any `json:"metrics"` +} + +// Экспортёр метрик +type MetricsExporter struct { + gaugeMetrics map[string]*prometheus.GaugeVec + counterMetrics map[string]*prometheus.CounterVec + mu sync.Mutex // Защита от одновременного доступа +} + +// Создаём новый экспортёр +func NewMetricsExporter() *MetricsExporter { + return &MetricsExporter{ + gaugeMetrics: make(map[string]*prometheus.GaugeVec), + counterMetrics: make(map[string]*prometheus.CounterVec), + } +} + +// Обновление или создание метрики +func (me *MetricsExporter) UpdateMetric(name string, value interface{}) { + 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} + + if request.Type == "gauge" { + // Регистрируем Gauge, если он ещё не существует + if _, exists := me.gaugeMetrics[fullMetricName]; !exists { + gaugeVec := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: fullMetricName, + Help: fmt.Sprintf("Metric %s from %s", metricName, request.ID), + }, + labels, + ) + customRegistry.MustRegister(gaugeVec) + me.gaugeMetrics[fullMetricName] = gaugeVec + } + // Обновляем значение Gauge + me.gaugeMetrics[fullMetricName].WithLabelValues(labelValues...).Set(floatVal) + + } else if request.Type == "counter" { + // Регистрируем Counter, если он ещё не существует + if _, exists := me.counterMetrics[fullMetricName]; !exists { + counterVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: fullMetricName, + Help: fmt.Sprintf("Metric %s from %s", metricName, request.ID), + }, + labels, + ) + customRegistry.MustRegister(counterVec) + me.counterMetrics[fullMetricName] = counterVec + } + // Инкрементируем Counter + me.counterMetrics[fullMetricName].WithLabelValues(labelValues...).Add(floatVal) + } + } +} + +// Обработчик для приёма JSON +func (me *MetricsExporter) JSONHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed) + return + } + + // Читаем тело запроса + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + defer r.Body.Close() + + // Парсим JSON + var data MetricRequest + if err := json.Unmarshal(body, &data); err != nil { + http.Error(w, "Invalid JSON format", http.StatusBadRequest) + return + } + + // Обновляем метрики + me.UpdateMetric(name, value) + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Metrics updated")) +} + +func main() { + exporter := NewMetricsExporter() + + // Используем кастомный реестр в обработчике /metrics + http.Handle("/metrics", promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{})) + http.HandleFunc("/update", exporter.JSONHandler) // Обработчик для приёма JSON + + port := ":9101" + fmt.Println("Starting server on port", port) + if err := http.ListenAndServe(port, nil); err != nil { + fmt.Println("Error starting server:", err) + } +} diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..6139fdf --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 1s # Интервал сбора метрик + +scrape_configs: + - job_name: "exporter" + static_configs: + - targets: ["exporter:9101"] # Сервис экспортёра diff --git a/test_data.txt b/test_data.txt new file mode 100644 index 0000000..d2be418 --- /dev/null +++ b/test_data.txt @@ -0,0 +1,11 @@ +curl -X POST -H "Content-Type: application/json" -d '{ + "id": "mock_api_2", + "name": "Mock /ping", + "url": "http://127.0.0.1:8081/ping", + "method": "GET", + "type": "gauge", + "metrics": { + "response_time": 120.5, + "status_code": 200, + } +}' http://localhost:9101/update