Skip to content

Commit

Permalink
CIRC-10033: Implement PromQL metadata queries (#106)
Browse files Browse the repository at this point in the history
* CIRC-10033: Implement PromQL metadata queries

* CIRC-10033: Lint fixes
  • Loading branch information
dhaifley committed Mar 21, 2023
1 parent 026c493 commit df6c674
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ to [Semantic Versioning](http://semver.org/) rules.

## [Next Release]

## [v1.13.8] - 2023-03-21

* add: Adds PromQL support for metadata queries via PromQLMetadataQuery().

## [v1.13.7] - 2023-03-17

* add: Adds PromQL support for series queries via PromQLSeriesQuery().
Expand Down Expand Up @@ -493,6 +497,7 @@ writing to histogram endpoints.
any delay, once started. Created: 2019-03-12. Fixed: 2019-03-13.

[Next Release]: https://github.com/circonus-labs/gosnowth
[v1.13.8]: https://github.com/circonus-labs/gosnowth/releases/tag/v1.13.8
[v1.13.7]: https://github.com/circonus-labs/gosnowth/releases/tag/v1.13.7
[v1.13.6]: https://github.com/circonus-labs/gosnowth/releases/tag/v1.13.6
[v1.13.5]: https://github.com/circonus-labs/gosnowth/releases/tag/v1.13.5
Expand Down
191 changes: 191 additions & 0 deletions promql.go
Original file line number Diff line number Diff line change
Expand Up @@ -1021,3 +1021,194 @@ func (sc *SnowthClient) PromQLLabelValuesQueryContext(ctx context.Context,

return r, nil
}

// PromQLMetadataQuery values represent Prometheus queries for series metadata.
// These values are accepted as strings and will accept the same values as
// would be passed to the prometheus /api/v1/metadata endpoint.
type PromQLMetadataQuery struct {
Limit string `json:"limit,omitempty"`
Metric string `json:"metric,omitempty"`
AccountID string `json:"account_id,omitempty"`
}

// PromQLMetadataQuery returns all time series metadata for a specified metric.
func (sc *SnowthClient) PromQLMetadataQuery(query *PromQLMetadataQuery,
nodes ...*SnowthNode,
) (*PromQLResponse, error) {
return sc.PromQLMetadataQueryContext(context.Background(), query, nodes...)
}

// PromQLMetadataQueryContext is the context aware version of PromQLMetadataQuery.
func (sc *SnowthClient) PromQLMetadataQueryContext(
ctx context.Context,
query *PromQLMetadataQuery,
nodes ...*SnowthNode,
) (*PromQLResponse, error) {
var node *SnowthNode

if len(nodes) > 0 && nodes[0] != nil {
node = nodes[0]
} else {
node = sc.GetActiveNode()
}

if node == nil {
return nil, fmt.Errorf("unable to get active node")
}

if query == nil {
return nil, fmt.Errorf("invalid PromQL metadata query: null")
}

aID := int64(0)

opts := &FindTagsOptions{
Activity: 0,
Latest: 0,
}

q := "and(__name:*)"

if query.Metric != "" {
mt, err := ConvertSeriesSelector(query.Metric)
if err != nil {
return nil, fmt.Errorf("invalid PromQL metadata query: "+
"invalid metric selector: %s: %w", query.Metric, err)
}

q = mt
}

if query.Limit != "" {
i, err := strconv.ParseInt(query.Limit, 10, 64)
if err != nil {
return nil,
fmt.Errorf("invalid PromQL metadata query: invalid limit: %v",
query.Limit)
}

opts.Limit = i
}

if query.AccountID != "" {
i, err := strconv.ParseInt(query.AccountID, 10, 64)
if err != nil {
return nil,
fmt.Errorf("invalid PromQL series query: invalid account_id: %v",
query.AccountID)
}

aID = i
}

res, err := sc.FindTagsContext(ctx, aID, q, opts, node)
if err != nil {
r := &PromQLResponse{
Status: "error",
ErrorType: "database",
Error: err.Error(),
}

rErr := &PromQLError{
Status: r.Status,
ErrorType: r.ErrorType,
Err: r.Error,
}

return r, rErr
}

r := &PromQLResponse{
Status: "success",
}

data := map[string][]map[string]string{}

for _, fti := range res.Items {
name := fti.MetricName

m := map[string]string{
"__name": name,
"__name__": name,
"type": fti.Type,
"unit": "",
"help": "",
}

mn, err := ParseMetricName(name)
if err != nil {
r.Warnings = append(r.Warnings, fmt.Errorf(
"unable to parse metric name: %s: %w", name, err).Error())
}

if mn != nil {
name = mn.Name

m["__name__"] = name

for _, st := range mn.StreamTags {
m[st.Category] = st.Value
}
}

for _, ct := range fti.CheckTags {
cat := ct

val := ""

parts := strings.SplitN(cat, ":", 2)

if len(parts) > 0 {
cat = parts[0]
}

if strings.HasPrefix(cat, `b"`) &&
strings.HasSuffix(cat, `"`) {
cat = strings.Trim(cat[1:], `"`)

b, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding,
bytes.NewBufferString(cat)))
if err != nil {
r.Warnings = append(r.Warnings, fmt.Errorf(
"unable to parse base64 tag category: %w", err).Error())
}

if b != nil {
cat = string(b)
}
}

if len(parts) > 1 {
val = parts[1]
}

if strings.HasPrefix(val, `b"`) &&
strings.HasSuffix(val, `"`) {
val = strings.Trim(val[1:], `"`)

b, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding,
bytes.NewBufferString(val)))
if err != nil {
r.Warnings = append(r.Warnings, fmt.Errorf(
"unable to parse base64 tag value: %w", err).Error())
}

if b != nil {
val = string(b)
}
}

m[cat] = val
}

if _, ok := data[name]; !ok {
data[name] = []map[string]string{}
}

data[name] = append(data[name], m)
}

r.Data = data

return r, nil
}
67 changes: 67 additions & 0 deletions promql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,3 +557,70 @@ func TestPromQLLabelValuesQuery(t *testing.T) {
t.Errorf("Expected data: test, got: %v", ds[0])
}
}

func TestPromQLMetadataQuery(t *testing.T) {
t.Parallel()

ms := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request,
) {
if r.RequestURI == "/state" {
_, _ = w.Write([]byte(stateTestData))

return
}

if r.RequestURI == "/stats.json" {
_, _ = w.Write([]byte(statsTestData))

return
}

w.Header().Set("X-Snowth-Search-Result-Count", "1")
_, _ = w.Write([]byte(promQLSeriesTestData))

return
}))

defer ms.Close()

sc, err := NewClient(context.Background(),
&Config{Servers: []string{ms.URL}})
if err != nil {
t.Fatal("Unable to create snowth client", err)
}

u, err := url.Parse(ms.URL)
if err != nil {
t.Fatal("Invalid test URL")
}

node := &SnowthNode{url: u}

res, err := sc.PromQLMetadataQuery(&PromQLMetadataQuery{
Limit: "2",
Metric: "test",
AccountID: "1",
}, node)
if err != nil {
t.Fatal(err)
}

if res.Data == nil {
t.Fatalf("Expected data, got: %v", res.Data)
}

dm, ok := res.Data.(map[string][]map[string]string)
if !ok {
t.Fatalf("Invalid type for result data: %T", res.Data)
}

if len(dm) != 1 {
t.Fatalf("Expected data length: 1, got: %v", len(dm))
}

if dm["test"][0]["type"] != "numeric,histogram" {
t.Errorf("Expected type: numeric,histogram, got: %v",
dm["test"][0]["type"])
}
}

0 comments on commit df6c674

Please sign in to comment.