VictoriaMetrics/lib/promscrape/discovery/consul/api.go
Nikolay 0b302d33cb Changes consul discovery api (#921)
* adds consul watch api,
it must reduce load on consul service with blocking wait requests,
changed discoveryClient api with fetchResponseMeta callback.

* small fix

* fix after master merge

* adds watch client at discovery utils

* fixes consul watcher,
changes namings,
fixes data race

* small typo fix

* sanity fix

* fix naming and service node update
2020-12-03 19:52:08 +02:00

163 lines
4.3 KiB
Go

package consul
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
"github.com/VictoriaMetrics/fasthttp"
)
// apiConfig contains config for API server
// with consulWatch service.
type apiConfig struct {
tagSeparator string
consulWatcher *consulWatch
}
var configMap = discoveryutils.NewConfigMap()
func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc, baseDir) })
if err != nil {
return nil, err
}
return v.(*apiConfig), nil
}
func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
token, err := getToken(sdc.Token)
if err != nil {
return nil, err
}
var ba *promauth.BasicAuthConfig
if len(sdc.Username) > 0 {
ba = &promauth.BasicAuthConfig{
Username: sdc.Username,
Password: sdc.Password,
}
token = ""
}
ac, err := promauth.NewConfig(baseDir, ba, token, "", sdc.TLSConfig)
if err != nil {
return nil, fmt.Errorf("cannot parse auth config: %w", err)
}
apiServer := sdc.Server
if apiServer == "" {
apiServer = "localhost:8500"
}
if !strings.Contains(apiServer, "://") {
scheme := sdc.Scheme
if scheme == "" {
scheme = "http"
}
apiServer = scheme + "://" + apiServer
}
client, err := discoveryutils.NewClient(apiServer, ac)
if err != nil {
return nil, fmt.Errorf("cannot create HTTP client for %q: %w", apiServer, err)
}
tagSeparator := ","
if sdc.TagSeparator != nil {
tagSeparator = *sdc.TagSeparator
}
dc, err := getDatacenter(client, sdc.Datacenter)
if err != nil {
return nil, err
}
cw, err := newConsulWatch(client, sdc, dc)
if err != nil {
return nil, fmt.Errorf("cannot start consul watcher: %w", err)
}
cfg := &apiConfig{
tagSeparator: tagSeparator,
consulWatcher: cw,
}
return cfg, nil
}
func getToken(token *string) (string, error) {
if token != nil {
return *token, nil
}
if tokenFile := os.Getenv("CONSUL_HTTP_TOKEN_FILE"); tokenFile != "" {
data, err := ioutil.ReadFile(tokenFile)
if err != nil {
return "", fmt.Errorf("cannot read consul token file %q; probably, `token` arg is missing in `consul_sd_config`? error: %w", tokenFile, err)
}
return string(data), nil
}
t := os.Getenv("CONSUL_HTTP_TOKEN")
// Allow empty token - it shouls work if authorization is disabled in Consul
return t, nil
}
func getDatacenter(client *discoveryutils.Client, dc string) (string, error) {
if dc != "" {
return dc, nil
}
// See https://www.consul.io/api/agent.html#read-configuration
data, err := client.GetAPIResponse("/v1/agent/self")
if err != nil {
return "", fmt.Errorf("cannot query consul agent info: %w", err)
}
a, err := parseAgent(data)
if err != nil {
return "", err
}
return a.Config.Datacenter, nil
}
// returns ServiceNodesState and version index.
func getServiceState(client *discoveryutils.Client, svc, baseArgs string, index uint64) ([]ServiceNode, uint64, error) {
path := fmt.Sprintf("/v1/health/service/%s%s", svc, baseArgs)
data, newIndex, err := getBlockingAPIResponse(client, path, index)
if err != nil {
return nil, index, err
}
sns, err := parseServiceNodes(data)
if err != nil {
return nil, index, err
}
return sns, newIndex, nil
}
// returns consul api response with new index version of object.
// https://www.consul.io/api-docs/features/blocking
func getBlockingAPIResponse(client *discoveryutils.Client, path string, index uint64) ([]byte, uint64, error) {
path += "&index=" + strconv.FormatUint(index, 10)
path = path + fmt.Sprintf("&wait=%s", watchTime)
getMeta := func(resp *fasthttp.Response) {
if ind := resp.Header.Peek("X-Consul-Index"); len(ind) > 0 {
newIndex, err := strconv.ParseUint(string(ind), 10, 64)
if err != nil {
logger.Errorf("failed to parse consul index: %v", err)
return
}
// reset index
// https://www.consul.io/api-docs/features/blocking#implementation-details
if newIndex < 1 {
index = 1
return
}
if index > newIndex {
index = 0
return
}
index = newIndex
}
}
data, err := client.GetBlockingAPIResponse(path, getMeta)
if err != nil {
return nil, index, fmt.Errorf("failed query consul api path=%q, err=%w", path, err)
}
return data, index, nil
}