-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
3,921 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/.idea | ||
/vendor | ||
/gauge-exporter | ||
/e2e_tests/.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
test: | ||
go test -v ./... && go build && cd ./e2e_tests && vendor/bin/phpunit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Gauge Exporter | ||
|
||
Gauge Exporter is a Prometheus exporter written in Go that allows you to expose custom gauge metrics to Prometheus. | ||
It provides a simple HTTP API for inputting metrics and serves them in Prometheus format. | ||
|
||
This exporter is specifically designed for use with stateless languages and environments where it is not feasible to maintain metric state in memory, such as in PHP. | ||
In these scenarios, Gauge Exporter provides a way to externally store and aggregate metrics that can't be kept in application memory between requests. | ||
If your application is written in a language that allows for easy in-memory state management, you might find other Prometheus client libraries more suitable for direct instrumentation. | ||
|
||
Gauge Exporter is specifically designed for gauge metrics and does not support counter metrics. | ||
It is optimized for use cases where the metric value can increase, decrease, or be set to a specific point. | ||
If you need to work with cumulative counters that only increase over time, this exporter may not be suitable for your needs. Please, consider StatsD Exporter or Pushgateway. | ||
|
||
## Comparison | ||
|
||
### Gauge Exporter vs Prometheus Pushgateway | ||
Gauge Exporter offers two key advantages over Prometheus Pushgateway. | ||
First, it automatically sets unprovided metric labels to zero, simplifying metric reporting with varying label sets. | ||
Second, it supports Time-to-Live (TTL) based metric expiration, allowing automatic cleanup of stale data. | ||
These features make Gauge Exporter particularly suitable for scenarios with time-sensitive metrics and dynamic labeling requirements, while Pushgateway remains ideal for batch jobs and situations requiring full control over metric persistence. | ||
|
||
## Usage | ||
|
||
### Running the Exporter | ||
|
||
To run the Gauge Exporter, use the following command: | ||
|
||
```bash | ||
./gauge-exporter --listen=0.0.0.0:8181 | ||
``` | ||
|
||
You can customize the listening address and port using the `--listen` flag. | ||
|
||
### Endpoints | ||
|
||
1. `/metrics`: Prometheus metrics endpoint | ||
2. `/gauge/{metric_name}`: Input endpoint for gauge metrics | ||
3. `/version`: Returns the version of the exporter | ||
|
||
### Inputting MetricBag | ||
|
||
To input a metric, send a PUT request to `/gauge/{metric_name}` with the following JSON payload: | ||
|
||
```json | ||
{ | ||
"ttl": 300, | ||
"data": [ | ||
{ | ||
"labels": { | ||
"label1": "value1", | ||
"label2": "value2" | ||
}, | ||
"value": 42.0 | ||
} | ||
], | ||
"system_labels": { | ||
"sys_label1": "sys_value1" | ||
} | ||
} | ||
``` | ||
|
||
- `ttl`: Time-to-live in seconds for the metric | ||
- `data`: Array of metric data points | ||
- `system_labels`: Labels applied to all data points in this request | ||
|
||
### Querying Metrics | ||
|
||
To query the metrics, send a GET request to the `/metrics` endpoint. This will return all the metrics in Prometheus format. | ||
|
||
## Built-in Metrics | ||
|
||
The exporter provides the following built-in metrics: | ||
|
||
- `gauge_exporter_metric_lines_total`: Total number of metric lines stored | ||
- `gauge_exporter_metrics_requests_total`: Total number of metric requests, labeled by status (success/failed) | ||
|
||
## Clients | ||
TBA |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package app | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"gauge-exporter/storage" | ||
"io" | ||
"maps" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/promhttp" | ||
) | ||
|
||
type App struct { | ||
storage *storage.MetricsStorage | ||
listenAddr string | ||
version string | ||
metricsTotal prometheus.Gauge | ||
requestsCount *prometheus.CounterVec | ||
} | ||
|
||
func NewApp(storage *storage.MetricsStorage, listenAddr string, version string) *App { | ||
return &App{ | ||
storage: storage, | ||
listenAddr: listenAddr, | ||
version: version, | ||
metricsTotal: prometheus.NewGauge( | ||
prometheus.GaugeOpts{Name: "gauge_exporter_metric_lines_total"}, | ||
), | ||
requestsCount: prometheus.NewCounterVec( | ||
prometheus.CounterOpts{Name: "gauge_exporter_metrics_requests_total"}, | ||
[]string{"status"}, | ||
), | ||
} | ||
} | ||
|
||
func (app *App) Run() { | ||
mux := http.NewServeMux() | ||
mux.HandleFunc("/metrics", app.outputPrometheusHandler) | ||
mux.HandleFunc("/gauge/", app.inputMetricsHandler) | ||
mux.HandleFunc("/version", app.versionHandler) | ||
|
||
server := &http.Server{ | ||
Addr: app.listenAddr, | ||
Handler: mux, | ||
ReadHeaderTimeout: 3 * time.Second, | ||
WriteTimeout: 10 * time.Second, | ||
IdleTimeout: 120 * time.Second, | ||
} | ||
|
||
err := server.ListenAndServe() | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
func (app *App) outputPrometheusHandler(w http.ResponseWriter, r *http.Request) { | ||
promRegistry := prometheus.NewRegistry() | ||
|
||
for _, metricName := range app.storage.GetAllMetricsNames() { | ||
for _, line := range app.storage.GetMetricLines(metricName) { | ||
if app.storage.IsExpired(metricName) { | ||
app.storage.Delete(metricName) | ||
continue | ||
} | ||
|
||
normalizedName := strings.ReplaceAll(metricName, ".", "_") | ||
|
||
m := prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: normalizedName}, line.GetSortedLabelsKeys()) | ||
m.With(line.GetLabels()).Set(line.Value) | ||
|
||
_ = promRegistry.Register(uncheckedCollector{m}) | ||
} | ||
} | ||
|
||
app.metricsTotal.Set(float64(app.getTotalStorageLines())) | ||
|
||
_ = promRegistry.Register(app.metricsTotal) | ||
_ = promRegistry.Register(app.requestsCount) | ||
|
||
promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{}).ServeHTTP(w, r) | ||
} | ||
|
||
func (app *App) inputMetricsHandler(w http.ResponseWriter, r *http.Request) { | ||
if r.Method != http.MethodPut { | ||
w.WriteHeader(http.StatusMethodNotAllowed) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
|
||
parts := strings.Split(r.URL.Path, "/") | ||
if len(parts) < 3 { | ||
http.Error(w, "Invalid URL", http.StatusBadRequest) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
metricName := parts[2] | ||
if len(metricName) == 0 { | ||
http.Error(w, "Invalid Metric Name", http.StatusBadRequest) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
|
||
var buf bytes.Buffer | ||
_, err := io.Copy(&buf, r.Body) | ||
if err != nil { | ||
http.Error(w, "Error reading request body", http.StatusInternalServerError) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
|
||
var data inputDto | ||
if err := json.Unmarshal(buf.Bytes(), &data); err != nil { | ||
http.Error(w, "Error parsing JSON", http.StatusBadRequest) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
|
||
if data.TTL == 0 || data.Data == nil { | ||
http.Error(w, "Incorrect input format", http.StatusBadRequest) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
|
||
metricLines := make([]*storage.MetricLine, 0, len(data.Data)) | ||
for _, v := range data.Data { | ||
line := storage.NewMetricLine(v.Labels, v.Value) | ||
if line.HasAnyLabel(data.SystemLabels) { | ||
http.Error(w, "Metric labels must not contain any of system_labels", http.StatusBadRequest) | ||
app.incFailedRequestsCount() | ||
return | ||
} | ||
|
||
maps.Copy(v.Labels, data.SystemLabels) | ||
|
||
metricLines = append(metricLines, line) | ||
} | ||
|
||
app.storage.UpdateMetricLines(metricName, data.SystemLabels, metricLines, time.Duration(data.TTL)*time.Second) | ||
app.incSuccessfulRequestsCount() | ||
} | ||
|
||
func (app *App) versionHandler(w http.ResponseWriter, _ *http.Request) { | ||
_, _ = w.Write([]byte(app.version)) | ||
} | ||
|
||
func (app *App) getTotalStorageLines() int { | ||
total := 0 | ||
for _, metricName := range app.storage.GetAllMetricsNames() { | ||
total += len(app.storage.GetMetricLines(metricName)) | ||
} | ||
|
||
return total | ||
} | ||
|
||
func (app *App) incFailedRequestsCount() { | ||
app.requestsCount.WithLabelValues("failed").Inc() | ||
} | ||
|
||
func (app *App) incSuccessfulRequestsCount() { | ||
app.requestsCount.WithLabelValues("success").Inc() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package app | ||
|
||
import "github.com/prometheus/client_golang/prometheus" | ||
|
||
type uncheckedCollector struct { | ||
c prometheus.Collector | ||
} | ||
|
||
func (u uncheckedCollector) Describe(_ chan<- *prometheus.Desc) {} | ||
func (u uncheckedCollector) Collect(ch chan<- prometheus.Metric) { | ||
u.c.Collect(ch) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package app | ||
|
||
type labelsValuesDto struct { | ||
Labels map[string]string `json:"labels"` | ||
Value float64 `json:"value"` | ||
} | ||
|
||
type inputDto struct { | ||
TTL int `json:"ttl"` | ||
Data []labelsValuesDto `json:"data"` | ||
SystemLabels map[string]string `json:"system_labels"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/vendor | ||
/.phpunit.cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"name": "belkacar/gauge-exporter-tests", | ||
"license": "proprietary", | ||
"type": "project", | ||
"minimum-stability": "dev", | ||
"prefer-stable": true, | ||
"require": { | ||
"php": "~8.3.0", | ||
"ext-curl": "*", | ||
"ext-json": "*", | ||
"guzzlehttp/guzzle": "^7.8", | ||
"phpunit/phpunit": "~11.1.0", | ||
"symfony/process": "^7.0", | ||
"belkacar/gauge-exporter-client": "0.2.0" | ||
}, | ||
"repositories": { | ||
"belkacar": { | ||
"type": "composer", | ||
"url": "https://gitlab.belkacar.ru/api/v4/group/php-backend/-/packages/composer/packages.json" | ||
} | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"BelkaCar\\GaugeExporterTests\\": "tests" | ||
} | ||
}, | ||
"config": { | ||
"sort-packages": true | ||
} | ||
} |
Oops, something went wrong.