package consul import ( "encoding/json" "fmt" "strconv" "strings" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils" ) // getServiceNodesLabels returns labels for Consul service nodes with given cfg. func getServiceNodesLabels(cfg *apiConfig) []*promutils.Labels { sns := cfg.consulWatcher.getServiceNodesSnapshot() var ms []*promutils.Labels for svc, sn := range sns { for i := range sn { ms = sn[i].appendTargetLabels(ms, svc, cfg.tagSeparator) } } return ms } // ServiceNode is Consul service node. // // See https://www.consul.io/api/health.html#list-nodes-for-service type ServiceNode struct { Service Service Node Node Checks []Check } // Service is Consul service. // // See https://www.consul.io/api/health.html#list-nodes-for-service type Service struct { ID string Service string Address string Namespace string Partition string Port int Tags []string Meta map[string]string } // Node is Consul node. // // See https://www.consul.io/api/health.html#list-nodes-for-service type Node struct { Address string Datacenter string Node string Meta map[string]string TaggedAddresses map[string]string } // Check is Consul check. // // See https://www.consul.io/api/health.html#list-nodes-for-service type Check struct { CheckID string Status string } func parseServiceNodes(data []byte) ([]ServiceNode, error) { var sns []ServiceNode if err := json.Unmarshal(data, &sns); err != nil { return nil, fmt.Errorf("cannot unmarshal ServiceNodes from %q: %w", data, err) } return sns, nil } func (sn *ServiceNode) appendTargetLabels(ms []*promutils.Labels, serviceName, tagSeparator string) []*promutils.Labels { var addr string if sn.Service.Address != "" { addr = discoveryutils.JoinHostPort(sn.Service.Address, sn.Service.Port) } else { addr = discoveryutils.JoinHostPort(sn.Node.Address, sn.Service.Port) } m := promutils.NewLabels(16) m.Add("__address__", addr) m.Add("__meta_consul_address", sn.Node.Address) m.Add("__meta_consul_dc", sn.Node.Datacenter) m.Add("__meta_consul_health", aggregatedStatus(sn.Checks)) m.Add("__meta_consul_namespace", sn.Service.Namespace) m.Add("__meta_consul_partition", sn.Service.Partition) m.Add("__meta_consul_node", sn.Node.Node) m.Add("__meta_consul_service", serviceName) m.Add("__meta_consul_service_address", sn.Service.Address) m.Add("__meta_consul_service_id", sn.Service.ID) m.Add("__meta_consul_service_port", strconv.Itoa(sn.Service.Port)) // We surround the separated list with the separator as well. This way regular expressions // in relabeling rules don't have to consider tag positions. m.Add("__meta_consul_tags", tagSeparator+strings.Join(sn.Service.Tags, tagSeparator)+tagSeparator) // Expose individual tags via __meta_consul_tag_* labels, so users could move all the tags // into the discovered scrape target with the following relabeling rule in the way similar to kubernetes_sd_configs: // // - action: labelmap // regex: __meta_consul_tag_(.+) // // This solves https://stackoverflow.com/questions/44339461/relabeling-in-prometheus for _, tag := range sn.Service.Tags { k := tag v := "" if n := strings.IndexByte(tag, '='); n >= 0 { k = tag[:n] v = tag[n+1:] } m.Add(discoveryutils.SanitizeLabelName("__meta_consul_tag_"+k), v) m.Add(discoveryutils.SanitizeLabelName("__meta_consul_tagpresent_"+k), "true") } for k, v := range sn.Node.Meta { m.Add(discoveryutils.SanitizeLabelName("__meta_consul_metadata_"+k), v) } for k, v := range sn.Service.Meta { m.Add(discoveryutils.SanitizeLabelName("__meta_consul_service_metadata_"+k), v) } for k, v := range sn.Node.TaggedAddresses { m.Add(discoveryutils.SanitizeLabelName("__meta_consul_tagged_address_"+k), v) } ms = append(ms, m) return ms } func aggregatedStatus(checks []Check) string { // The code has been copy-pasted from HealthChecks.AggregatedStatus in Consul var passing, warning, critical, maintenance bool for _, check := range checks { id := check.CheckID if id == "_node_maintenance" || strings.HasPrefix(id, "_service_maintenance:") { maintenance = true continue } switch check.Status { case "passing": passing = true case "warning": warning = true case "critical": critical = true default: return "" } } switch { case maintenance: return "maintenance" case critical: return "critical" case warning: return "warning" case passing: return "passing" default: return "passing" } }