Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions data_sources_prometheus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package flashduty

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)

// ReadPrometheusLabelValues lists the values of one label from a
// Prometheus-compatible data source, through the Monitors proxy.
//
// Unlike the generated typed endpoints, the response body is the data
// source's native Prometheus HTTP API payload: it is not wrapped in the
// Flashduty {request_id, error, data} envelope, so decoding goes straight
// into PrometheusLabelValuesResponse. A non-2xx status can come from either
// side of the proxy — the platform itself (plain text, raised before the
// data source is reached) or the data source (its own JSON error shape,
// distinct from Flashduty's) — so the raw body is carried as-is on the
// returned error's Message rather than parsed as a Flashduty error.
//
// API: GET /monit/prometheus/api/v1/label/{label_name}/values (monit-prometheus-read-label-values).
func (s *DataSourcesService) ReadPrometheusLabelValues(ctx context.Context, dataSourceID uint64, labelName string) (*PrometheusLabelValuesResponse, *Response, error) {
if dataSourceID == 0 {
return nil, nil, fmt.Errorf("flashduty: data source id is required")
}
labelName = strings.TrimSpace(labelName)
if labelName == "" {
return nil, nil, fmt.Errorf("flashduty: label name is required")
}

path := "/monit/prometheus/api/v1/label/" + url.PathEscape(labelName) + "/values"
httpReq, err := s.client.newRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
httpReq.Header.Set("X-DSID", strconv.FormatUint(dataSourceID, 10))

httpResp, err := s.client.client.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("flashduty: request to %s failed: %v", sanitizeURL(httpReq.URL), sanitizeError(err))
}
defer func() { _ = httpResp.Body.Close() }()

resp := &Response{Response: httpResp, RequestID: httpResp.Header.Get("Flashcat-Request-Id")}
resp.RateLimit = parseRateLimit(httpResp.Header)

raw, err := io.ReadAll(io.LimitReader(httpResp.Body, maxResponseBodySize))
if err != nil {
return nil, resp, fmt.Errorf("flashduty: reading response body: %w", err)
}

if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
apiErr := &ErrorResponse{Response: httpResp, Message: strings.TrimSpace(string(raw)), RequestID: resp.RequestID}
return nil, resp, asAPIError(apiErr, resp.RateLimit)
}

out := new(PrometheusLabelValuesResponse)
if len(bytes.TrimSpace(raw)) > 0 {
if err := json.Unmarshal(raw, out); err != nil {
return nil, resp, fmt.Errorf("flashduty: decoding response into %T (request_id %s): %w", out, resp.RequestID, err)
}
}
return out, resp, nil
}
73 changes: 73 additions & 0 deletions data_sources_prometheus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package flashduty

import (
"context"
"io"
"net/http"
"testing"
)

func TestReadPrometheusLabelValuesSendsPathAndHeader(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("method = %s", r.Method)
}
if got := r.URL.EscapedPath(); got != "/monit/prometheus/api/v1/label/job%20name/values" {
t.Errorf("escaped path = %s", got)
}
if got := r.URL.Query().Get("app_key"); got != "KEY" {
t.Errorf("app_key = %q", got)
}
if got := r.Header.Get("X-DSID"); got != "42" {
t.Errorf("X-DSID = %q", got)
}
w.Header().Set("Flashcat-Request-Id", "RIDL")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, `{"status":"success","data":["api","db","worker"]}`)
})

out, resp, err := c.DataSources.ReadPrometheusLabelValues(context.Background(), 42, "job name")
if err != nil {
t.Fatalf("ReadPrometheusLabelValues error: %v", err)
}
if resp == nil || resp.StatusCode != http.StatusOK || resp.RequestID != "RIDL" {
t.Fatalf("response meta = %+v", resp)
}
if out == nil || out.Status != "success" || len(out.Data) != 3 || out.Data[1] != "db" {
t.Fatalf("decoded response = %+v", out)
}
}

func TestReadPrometheusLabelValuesSurfacesNonEnvelopeErrorBody(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(w, "missing header: X-DSID")
})

out, resp, err := c.DataSources.ReadPrometheusLabelValues(context.Background(), 42, "job")
if err == nil {
t.Fatal("expected an error")
}
if out != nil {
t.Fatalf("expected nil result on error, got %+v", out)
}
if resp == nil || resp.StatusCode != http.StatusBadRequest {
t.Fatalf("response meta = %+v", resp)
}
if got := err.Error(); got == "" {
t.Fatal("expected a non-empty error message")
}
}

func TestReadPrometheusLabelValuesValidatesInputs(t *testing.T) {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("request should not be sent")
})

if _, _, err := c.DataSources.ReadPrometheusLabelValues(context.Background(), 0, "job"); err == nil {
t.Fatal("expected empty data source id error")
}
if _, _, err := c.DataSources.ReadPrometheusLabelValues(context.Background(), 42, ""); err == nil {
t.Fatal("expected empty label name error")
}
}
14 changes: 14 additions & 0 deletions diagnostics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 105 additions & 0 deletions models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading