Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Metricbeat] Add 'query' metricset for prometheus module #15177

Closed
wants to merge 4 commits into from
Closed

[Metricbeat] Add 'query' metricset for prometheus module #15177

wants to merge 4 commits into from

Conversation

estherk0
Copy link
Contributor

@estherk0 estherk0 commented Dec 18, 2019

Motivation

There is no way to use Prometheus Query using Prometheus collect API (/metrics, /federate)
If Metricbeat can use Prometheus query API, it would be very powerful to collect only what I need.
Refer to the following documentation: https://prometheus.io/docs/prometheus/latest/querying/api/

Purpose

  • Put the resultant to ElasticSearch from Prometheus HTTP query API
  • Can request multiple queries

How to use

  1. Configuration
metricbeat.modules:
      - module: prometheus
        metricsets: ['query']
        period: 10s
        hosts: ["prometheus-operated:9090"]
        paths:
        - name: "cpu_usage"
          path: '/api/v1/query'
          fields:
            'query': 'sum(rate(node_cpu_seconds_total[1m]))'
        - name: "labels"
          path: '/api/v1/labels'
  1. Result
  • From /api/v1/query?query=sum(rate(node_cpu_seconds_total[1m]))
{
  "@timestamp": "2019-12-17T11:57:12.612Z",
  "@metadata": {
    "beat": "metricbeat",
    "type": "_doc",
    "version": "8.0.0"
  },
  "ecs": {
    "version": "1.2.0"
  },
  "host": {
    "name": "lma1"
  },
  "agent": {
    "id": "1b3a4b59-a0ce-43c3-ad65-a69d4110c8ca",
    "version": "8.0.0",
    "type": "metricbeat",
    "ephemeral_id": "8ad6434e-54ad-453d-b5ce-7e0dabb72f0b",
    "hostname": "lma1"
  },
  "service": {
    "address": "prometheus-operated:9090",
    "type": "prometheus"
  },
  "event": {
    "dataset": "prometheus.query",
    "module": "prometheus",
    "duration": 143009798
  },
  "metricset": {
    "name": "query",
    "period": 10000
  },
  "prometheus": {
    "query": {
      "mem_usage": {
        "status": "success",
        "data": {
          "resultType": "vector",
          "result": [
            {
              "metric": {},
              "reconciledValue": {
                "unixtimestamp": 1576583832.753000,
                "value": "2880242405376"
              }
            }
          ]
        }
      }
    }
  }
}

Please review this and give me feedback. :)

@estherk0 estherk0 requested a review from a team as a code owner December 18, 2019 06:35
@elasticmachine
Copy link
Collaborator

Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?

1 similar comment
@elasticmachine
Copy link
Collaborator

Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?

@ChrsMark ChrsMark added Team:Integrations Label for the Integrations team [zube]: In Review review labels Dec 18, 2019
@exekias
Copy link
Contributor

exekias commented Dec 18, 2019

Thank you for opening this @jabbukka!! 🎉 . Could you please explain how would you use this besides the given examples? I try to understand the general use case.

Also I wonder, would it make sense to store only the last value? How do you plan to query this data?

@estherk0
Copy link
Contributor Author

estherk0 commented Dec 19, 2019

Thank you for opening this @jabbukka!! 🎉 . Could you please explain how would you use this besides the given examples? I try to understand the general use case.

@exekias Sorry for the lack of explanation. 😥
I'm trying to create a monitoring dashboard using ElasticSearch + Kibana + Canvas onKubernetes + OpenStack Cluster environment. And Prometheus collects a lot of metric data for Kubernetes and OpenStack. Therefore, I've decided to import Prometheus' metric into ElasticSearch.
(I just want to store the query result for monitoring to Elasticsearch. It doesn't need to store all raw data...)

However, it was difficult to calculate metrics with Prometheus raw data in ElasticSearch using ES Query. Because Prometheus data model are based on time series.

For examples, Histogram data model uses histogram_quantile() function to calculate quantiles.
I believe it makes much more easier if I can use PromQL to integrates Prometheus Data.

Also I wonder, would it make sense to store only the last value? How do you plan to query this data?

the last value is representative for the last query result. i. e) cpu usage rate for last 1 minute.
It's for the real-time metric data to report, not dumping the raw data.
Below is the example for configuration.

        [...]
        period: 1m
        paths:
        - name: "cpu_usage_1m"  
          fields:
             // cluster1's cpu usage for last 1 minute
            'query': 'sum(rate(node_cpu_seconds_total{cluster="cluster1",stat~="iowait"}[1m]))'
        - name: "top5_busiest_instaces"
           fields:
             // top 5 instances
             'query': 'topk(5,sum(rate(http_requests_total[1m]))by(instance))'
        [...]

I hope this made things a bit more clear.
It also related to this issue: Metricbeat and advanced Prometheus queries

// Vector [ <unix_timestamp>, "<query_result>" ] is not acceptable for Elasticsearch.
// Because there are two types in one array.
// So change Vector to Object { unixtimestamp: "<unix_timestamp", value: "query_result" }
if res.Data.ResultType == "vector" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to come up with a good conversion for all result types. I think we can strip down much of the info we get in the response to store only timestamp + results. What do you think about doing something like this?:

  • For scalar:
    • Store timestamp under event tiemstamp (mb.Event.Timestamp) (this will be reported as @timestamp in the final event)
    • Store value under prometheus.query.<query_name> (ie: prometheus.query.http_request in your example)
  • For string:
    • Store timestamp under event tiemstamp (mb.Event.Timestamp)
    • Store value under prometheus.query.<query_name>
  • For vector:
    • Create an event for each metric
    • Store timestamp under event timestamp (mb.Event.Timestamp)
    • Store value under prometheus.query.<query_name>
    • Store labels under prometheus.labels
  • For matrix (range vector):
    • Create an event for each metric and (timestamp, vlue) pairs
    • Store timestamp under event timestamp (mb.Event.Timestamp)
    • Store value under prometheus.query.<query_name>
    • Store labels under prometheus.labels

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I'll try to apply this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm applying the suggested changes on code. Please review commit #c511e95faf

@ChrsMark
Copy link
Member

ChrsMark commented Mar 17, 2020

@jabbukka apologies for having it stalled all that time. Could you rebase it on top of the latest master so as to resolve the conflicts and see what is missing? It would be nice to have it in soon.

  • On metricbeat/include/list_common.go you will need to import the modules like "github.com/elastic/beats/v7/metricbeat/module/.... It should be fixed if you run again make update
  • On metricbeat/module/prometheus/_meta/config.yml there is a new metricset that is already added.
  • On metricbeat/module/prometheus/fields.go it should be fixed by running again make update

Also all imports should be defined with v7 like github.com/elastic/beats/v7/libbeat/common.

@@ -0,0 +1,32 @@
[
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All files under tesdata directory are used for testing. In this a docs.plain file is needed which then will be used as a testing input. See for example https://github.com/elastic/beats/blob/a174425cb45c406a748b290dd2ef892c731cdac7/metricbeat/module/prometheus/collector/_meta/testdata/docs.plain.

)

func TestData(t *testing.T) {
mbtest.TestDataFiles(t, "prometheus", "query")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this actually doing anything? As mentioned in a previous comment a docs.plain is required to be used as input in the tests.

Vectors [][]interface{} `json:"values"`
}

func (m *MetricSet) parseResponse(body []byte, pathConfig PathConfig) ([]mb.Event, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any special reason for parseResponse to be a method of Metricset? If not the following should be fine:

Suggested change
func (m *MetricSet) parseResponse(body []byte, pathConfig PathConfig) ([]mb.Event, error) {
func parseResponse(body []byte, pathConfig PathConfig) ([]mb.Event, error) {

type PathConfig struct {
Path string `config:"path"`
Fields common.MapStr `config:"fields"`
Name string `config:"name"`
Copy link
Member

@ChrsMark ChrsMark Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to call it MetricName QueryName(and metric_name query_name) so as to be more specific.

// Validate for Prometheus "query" metricset config
func (p PathConfig) Validate() error {
if p.Name == "" {
return errors.New("`namespace` can not be empty in path configuration")
Copy link
Member

@ChrsMark ChrsMark Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace can be confusing here. We had better call it metric_namequery_name.

if err := json.Unmarshal(body, &arrayBody); err != nil {
return nil, "", errors.Wrap(err, "Failed to parse api response")
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we check if the query was sucessful?
Sth like:

	if arrayBody.Status == "error" {
		return nil, "", errors.Errorf("Failed to query")
	}


// Config for "query" metricset
type Config struct {
Paths []PathConfig `config:"paths"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a name like Queries would be better here. wdyt?


// PathConfig is used to make a API request.
type PathConfig struct {
Path string `config:"path"`
Copy link
Member

@ChrsMark ChrsMark Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Query here?

// PathConfig is used to make a API request.
type PathConfig struct {
Path string `config:"path"`
Fields common.MapStr `config:"fields"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe QueryParams?


events, parseErr := m.parseResponse(body, pathConfig)
if parseErr != nil {
return err
Copy link
Member

@ChrsMark ChrsMark Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are in a loop I would say that we don't have to return but to log the error and continue to next query, sth like:

if parseErr != nil {
  m.Logger().Debug("error parsing response for ", pathConfig.Name, ": ", parseErr)
  reporter.Error(errors.Wrap(err, "error parsing response"))
  continue
}

I would do the same if FetchResponse() return error too.

* Remove array from response body.  Original response body has an array (Vector type). It makes difficult
to query with Elasticsearch QL.
New data schema:
  "prometheus": {
    "query": {
      "mem_usage": {
        "status": "success",
        "data": {
          "resultType": "vector",
          "result": [
            {
              "metric": {},
              "reconciledValue": {
                "unixtimestamp": 1.576751116531e+09,
                "value": "2947656593408"
              }
            }
          ]
        }
      }
    }
  }
* Prometheus API returns "string" type for query result. But actual
result type is a number.
* Make it easy to use ES SQL
import (
"errors"

"github.com/elastic/beats/libbeat/common"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/v7/libbeat/common"

"strconv"
"time"

"github.com/elastic/beats/libbeat/common"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/v7/libbeat/common"

"time"

"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/metricbeat/mb"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/elastic/beats/metricbeat/mb"
"github.com/elastic/beats/v7/metricbeat/mb"

import (
"io/ioutil"

"github.com/elastic/beats/libbeat/common"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/v7/libbeat/common"

"io/ioutil"

"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/metricbeat/helper"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/elastic/beats/metricbeat/helper"
"github.com/elastic/beats/v7/metricbeat/helper"


"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/metricbeat/helper"
"github.com/elastic/beats/metricbeat/mb"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/elastic/beats/metricbeat/mb"
"github.com/elastic/beats/v7/metricbeat/mb"


func (m *MetricSet) getURL(path string, queryMap common.MapStr) string {
queryStr := mb.QueryParams(queryMap).String()
return "http://" + m.BaseMetricSet.Host() + path + "?" + queryStr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "http://" + m.BaseMetricSet.Host() + path + "?" + queryStr
return m.http.GetURI() + path + "?" + queryStr

In order to have the URI already set properly on module initialisation you will need to initialise Metricset like:

const (
	defaultScheme = "http"
)

var (
	hostParser = parse.URLHostParserBuilder{
		DefaultScheme: defaultScheme,
	}.Build()
)

func init() {
	mb.Registry.MustAddMetricSet("prometheus", "query", New,
		mb.WithHostParser(hostParser),
	)
}

@ChrsMark
Copy link
Member

@jabbukka I think that the requested changes might need a decent amount of work and testing and since we are moving forward to code freeze would you mind if I do the changes-cleanup for you on a different PR while keeping your commits ( in this, in the merge commit you will be the co-author) Let me know!

@estherk0
Copy link
Contributor Author

@ChrsMark Sure! No problem. :)

@ChrsMark
Copy link
Member

Hey @jabbukka ! This feature has been merged! Here is the commit on master: 7c82034.

Thank you for proposing and implementing this feature!

@ChrsMark ChrsMark closed this Mar 23, 2020
@zube zube bot removed the [zube]: In Review label Mar 23, 2020
@ChrsMark ChrsMark added [zube]: In Review Team:Platforms Label for the Integrations - Platforms team and removed review labels Mar 23, 2020
@elasticmachine
Copy link
Collaborator

Pinging @elastic/integrations-platforms (Team:Platforms)

@zube zube bot added review Team:Platforms Label for the Integrations - Platforms team and removed Team:Platforms Label for the Integrations - Platforms team [zube]: In Review review labels Mar 23, 2020
@estherk0
Copy link
Contributor Author

@ChrsMark I'm so glad to contribute it! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Team:Integrations Label for the Integrations team Team:Platforms Label for the Integrations - Platforms team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants