diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b291cf2bcd..7f7cec9cda 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,8 +1,10 @@
/web/ui @juliusv
/web/ui/module @juliusv @nexucis
-/storage/remote @csmarchbanks @cstyan @bwplotka @tomwilkie
-/storage/remote/otlptranslator @gouthamve @jesusvazquez
+/storage/remote @cstyan @bwplotka @tomwilkie
+/storage/remote/otlptranslator @aknuds1 @jesusvazquez
/discovery/kubernetes @brancz
/tsdb @jesusvazquez
/promql @roidelapluie
/cmd/promtool @dgl
+/documentation/prometheus-mixin @metalmatze
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index f4d17b3596..bb4e2d24c9 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,4 +1,4 @@
-blank_issues_enabled: false
+blank_issues_enabled: true
contact_links:
- name: Prometheus Community Support
url: https://prometheus.io/community/
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 95df72dd0e..cf90177b1d 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,4 +1,8 @@
-
-
-
-
-
-{{ end }}
-
-{{/* LHS menu, should be passed . */}}
-{{ define "menu" }}
-
-{{ end }}
-
-{{/* Helper, pass (args . path name) */}}
-{{ define "_menuItem" }}
-
-{{ end }}
-
diff --git a/console_libraries/prom.lib b/console_libraries/prom.lib
deleted file mode 100644
index d7d436f947..0000000000
--- a/console_libraries/prom.lib
+++ /dev/null
@@ -1,138 +0,0 @@
-{{/* vim: set ft=html: */}}
-{{/* Load Prometheus console library JS/CSS. Should go in */}}
-{{ define "prom_console_head" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-{{ end }}
-
-{{/* Top of all pages. */}}
-{{ define "head" -}}
-
-
-
-{{ template "prom_console_head" }}
-
-
-{{ template "navbar" . }}
-
-{{ template "menu" . }}
-{{ end }}
-
-{{ define "__prom_query_drilldown_noop" }}{{ . }}{{ end }}
-{{ define "humanize" }}{{ humanize . }}{{ end }}
-{{ define "humanizeNoSmallPrefix" }}{{ if and (lt . 1.0) (gt . -1.0) }}{{ printf "%.3g" . }}{{ else }}{{ humanize . }}{{ end }}{{ end }}
-{{ define "humanize1024" }}{{ humanize1024 . }}{{ end }}
-{{ define "humanizeDuration" }}{{ humanizeDuration . }}{{ end }}
-{{ define "humanizePercentage" }}{{ humanizePercentage . }}{{ end }}
-{{ define "humanizeTimestamp" }}{{ humanizeTimestamp . }}{{ end }}
-{{ define "printf.1f" }}{{ printf "%.1f" . }}{{ end }}
-{{ define "printf.3g" }}{{ printf "%.3g" . }}{{ end }}
-
-{{/* prom_query_drilldown (args expr suffix? renderTemplate?)
-Displays the result of the expression, with a link to /graph for it.
-
-renderTemplate is the name of the template to use to render the value.
-*/}}
-{{ define "prom_query_drilldown" }}
-{{ $expr := .arg0 }}{{ $suffix := (or .arg1 "") }}{{ $renderTemplate := (or .arg2 "__prom_query_drilldown_noop") }}
-{{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{ else }}-{{ end }}
-{{ end }}
-
-{{ define "prom_path" }}/consoles/{{ .Path }}?{{ range $param, $value := .Params }}{{ $param }}={{ $value }}&{{ end }}{{ end }}"
-
-{{ define "prom_right_table_head" }}
-
-
-{{ end }}
-{{ define "prom_right_table_tail" }}
-
-
-{{ end }}
-
-{{/* RHS table head, pass job name. Should be used after prom_right_table_head. */}}
-{{ define "prom_right_table_job_head" }}
-
- | {{ . }} |
- {{ template "prom_query_drilldown" (args (printf "sum(up{job='%s'})" .)) }} / {{ template "prom_query_drilldown" (args (printf "count(up{job='%s'})" .)) }} |
-
-
- | CPU |
- {{ template "prom_query_drilldown" (args (printf "avg by(job)(irate(process_cpu_seconds_total{job='%s'}[5m]))" .) "s/s" "humanizeNoSmallPrefix") }} |
-
-
- | Memory |
- {{ template "prom_query_drilldown" (args (printf "avg by(job)(process_resident_memory_bytes{job='%s'})" .) "B" "humanize1024") }} |
-
-{{ end }}
-
-
-{{ define "prom_content_head" }}
-
-
-{{ template "prom_graph_timecontrol" . }}
-{{ end }}
-{{ define "prom_content_tail" }}
-
-
-{{ end }}
-
-{{ define "prom_graph_timecontrol" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{{ end }}
-
-{{/* Bottom of all pages. */}}
-{{ define "tail" }}
-
-
-{{ end }}
diff --git a/consoles/index.html.example b/consoles/index.html.example
deleted file mode 100644
index c725d30dea..0000000000
--- a/consoles/index.html.example
+++ /dev/null
@@ -1,28 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
-Overview
-These are example consoles for Prometheus.
-
-These consoles expect exporters to have the following job labels:
-
-
- | Exporter |
- Job label |
-
-
- | Node Exporter |
- node |
-
-
- | Prometheus |
- prometheus |
-
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/consoles/node-cpu.html b/consoles/node-cpu.html
deleted file mode 100644
index d6c515d2dd..0000000000
--- a/consoles/node-cpu.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
-
- | CPU(s): {{ template "prom_query_drilldown" (args (printf "scalar(count(count by (cpu)(node_cpu_seconds_total{job='node',instance='%s'})))" .Params.instance)) }} |
-
-{{ range printf "sum by (mode)(irate(node_cpu_seconds_total{job='node',instance='%s'}[5m])) * 100 / scalar(count(count by (cpu)(node_cpu_seconds_total{job='node',instance='%s'})))" .Params.instance .Params.instance | query | sortByLabel "mode" }}
-
- | {{ .Labels.mode | title }} CPU |
- {{ .Value | printf "%.1f" }}% |
-
-{{ end }}
- | Misc |
-
- | Processes Running |
- {{ template "prom_query_drilldown" (args (printf "node_procs_running{job='node',instance='%s'}" .Params.instance) "" "humanize") }} |
-
-
- | Processes Blocked |
- {{ template "prom_query_drilldown" (args (printf "node_procs_blocked{job='node',instance='%s'}" .Params.instance) "" "humanize") }} |
-
-
- | Forks |
- {{ template "prom_query_drilldown" (args (printf "irate(node_forks_total{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }} |
-
-
- | Context Switches |
- {{ template "prom_query_drilldown" (args (printf "irate(node_context_switches_total{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }} |
-
-
- | Interrupts |
- {{ template "prom_query_drilldown" (args (printf "irate(node_intr_total{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }} |
-
-
- | 1m Loadavg |
- {{ template "prom_query_drilldown" (args (printf "node_load1{job='node',instance='%s'}" .Params.instance)) }} |
-
-
-
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
- Node CPU - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}
-
- CPU Usage
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/consoles/node-disk.html b/consoles/node-disk.html
deleted file mode 100644
index ffff41b797..0000000000
--- a/consoles/node-disk.html
+++ /dev/null
@@ -1,78 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
-
- | Disks |
-
-{{ range printf "node_disk_io_time_seconds_total{job='node',instance='%s'}" .Params.instance | query | sortByLabel "device" }}
- {{ .Labels.device }} |
-
- | Utilization |
- {{ template "prom_query_drilldown" (args (printf "irate(node_disk_io_time_seconds_total{job='node',instance='%s',device='%s'}[5m]) * 100" .Labels.instance .Labels.device) "%" "printf.1f") }} |
-
-
- | Throughput |
- {{ template "prom_query_drilldown" (args (printf "irate(node_disk_read_bytes_total{job='node',instance='%s',device='%s'}[5m]) + irate(node_disk_written_bytes_total{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device .Labels.instance .Labels.device) "B/s" "humanize") }} |
-
-
- | Avg Read Time |
- {{ template "prom_query_drilldown" (args (printf "irate(node_disk_read_time_seconds_total{job='node',instance='%s',device='%s'}[5m]) / irate(node_disk_reads_completed_total{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device .Labels.instance .Labels.device) "s" "humanize") }} |
-
-
- | Avg Write Time |
- {{ template "prom_query_drilldown" (args (printf "irate(node_disk_write_time_seconds_total{job='node',instance='%s',device='%s'}[5m]) / irate(node_disk_writes_completed_total{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device .Labels.instance .Labels.device) "s" "humanize") }} |
-
-{{ end }}
-
- | Filesystem Fullness |
-
-{{ define "roughlyNearZero" }}
-{{ if gt .1 . }}~0{{ else }}{{ printf "%.1f" . }}{{ end }}
-{{ end }}
-{{ range printf "node_filesystem_size_bytes{job='node',instance='%s'}" .Params.instance | query | sortByLabel "mountpoint" }}
-
- | {{ .Labels.mountpoint }} |
- {{ template "prom_query_drilldown" (args (printf "100 - node_filesystem_avail_bytes{job='node',instance='%s',mountpoint='%s'} / node_filesystem_size_bytes{job='node'} * 100" .Labels.instance .Labels.mountpoint) "%" "roughlyNearZero") }} |
-
-{{ end }}
-
-
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
- Node Disk - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}
-
- Disk I/O Utilization
-
-
- Filesystem Usage
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/consoles/node-overview.html b/consoles/node-overview.html
deleted file mode 100644
index 92f53ba871..0000000000
--- a/consoles/node-overview.html
+++ /dev/null
@@ -1,121 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
- | Overview |
-
- | User CPU |
- {{ template "prom_query_drilldown" (args (printf "sum(irate(node_cpu_seconds_total{job='node',instance='%s',mode='user'}[5m])) * 100 / count(count by (cpu)(node_cpu_seconds_total{job='node',instance='%s'}))" .Params.instance .Params.instance) "%" "printf.1f") }} |
-
-
- | System CPU |
- {{ template "prom_query_drilldown" (args (printf "sum(irate(node_cpu_seconds_total{job='node',instance='%s',mode='system'}[5m])) * 100 / count(count by (cpu)(node_cpu_seconds_total{job='node',instance='%s'}))" .Params.instance .Params.instance) "%" "printf.1f") }} |
-
-
- | Memory Total |
- {{ template "prom_query_drilldown" (args (printf "node_memory_MemTotal_bytes{job='node',instance='%s'}" .Params.instance) "B" "humanize1024") }} |
-
-
- | Memory Free |
- {{ template "prom_query_drilldown" (args (printf "node_memory_MemFree_bytes{job='node',instance='%s'}" .Params.instance) "B" "humanize1024") }} |
-
-
- | Network |
-
-{{ range printf "node_network_receive_bytes_total{job='node',instance='%s',device!='lo'}" .Params.instance | query | sortByLabel "device" }}
-
- | {{ .Labels.device }} Received |
- {{ template "prom_query_drilldown" (args (printf "irate(node_network_receive_bytes_total{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device) "B/s" "humanize") }} |
-
-
- | {{ .Labels.device }} Transmitted |
- {{ template "prom_query_drilldown" (args (printf "irate(node_network_transmit_bytes_total{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device) "B/s" "humanize") }} |
-
-{{ end }}
-
- | Disks |
-
-{{ range printf "node_disk_io_time_seconds_total{job='node',instance='%s',device!~'^(md\\\\d+$|dm-)'}" .Params.instance | query | sortByLabel "device" }}
-
- | {{ .Labels.device }} Utilization |
- {{ template "prom_query_drilldown" (args (printf "irate(node_disk_io_time_seconds_total{job='node',instance='%s',device='%s'}[5m]) * 100" .Labels.instance .Labels.device) "%" "printf.1f") }} |
-
-{{ end }}
-{{ range printf "node_disk_io_time_seconds_total{job='node',instance='%s'}" .Params.instance | query | sortByLabel "device" }}
-
- | {{ .Labels.device }} Throughput |
- {{ template "prom_query_drilldown" (args (printf "irate(node_disk_read_bytes_total{job='node',instance='%s',device='%s'}[5m]) + irate(node_disk_written_bytes_total{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device .Labels.instance .Labels.device) "B/s" "humanize") }} |
-
-{{ end }}
-
- | Filesystem Fullness |
-
-{{ define "roughlyNearZero" }}
-{{ if gt .1 . }}~0{{ else }}{{ printf "%.1f" . }}{{ end }}
-{{ end }}
-{{ range printf "node_filesystem_size_bytes{job='node',instance='%s'}" .Params.instance | query | sortByLabel "mountpoint" }}
-
- | {{ .Labels.mountpoint }} |
- {{ template "prom_query_drilldown" (args (printf "100 - node_filesystem_avail_bytes{job='node',instance='%s',mountpoint='%s'} / node_filesystem_size_bytes{job='node'} * 100" .Labels.instance .Labels.mountpoint) "%" "roughlyNearZero") }} |
-
-{{ end }}
-
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
- Node Overview - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}
-
- CPU Usage
-
-
-
- Disk I/O Utilization
-
-
-
- Memory
-
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/consoles/node.html b/consoles/node.html
deleted file mode 100644
index 9a37544eee..0000000000
--- a/consoles/node.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
-
- | Node |
- {{ template "prom_query_drilldown" (args "sum(up{job='node'})") }} / {{ template "prom_query_drilldown" (args "count(up{job='node'})") }} |
-
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
-Node
-
-
-
- | Node |
- Up |
- CPU Used |
- Memory Available |
-
-{{ range query "up{job='node'}" | sortByLabel "instance" }}
-
- | {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Labels.instance }} |
- Yes{{ else }} class="alert-danger">No{{ end }} |
- {{ template "prom_query_drilldown" (args (printf "100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{job='node',mode='idle',instance='%s'}[5m])))" .Labels.instance) "%" "printf.1f") }} |
- {{ template "prom_query_drilldown" (args (printf "node_memory_MemFree_bytes{job='node',instance='%s'} + node_memory_Cached_bytes{job='node',instance='%s'} + node_memory_Buffers_bytes{job='node',instance='%s'}" .Labels.instance .Labels.instance .Labels.instance) "B" "humanize1024") }} |
-
-{{ else }}
-| No nodes found. |
-{{ end }}
-
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/consoles/prometheus-overview.html b/consoles/prometheus-overview.html
deleted file mode 100644
index 08e027de06..0000000000
--- a/consoles/prometheus-overview.html
+++ /dev/null
@@ -1,96 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
-
- | Overview |
-
-
- | CPU |
- {{ template "prom_query_drilldown" (args (printf "irate(process_cpu_seconds_total{job='prometheus',instance='%s'}[5m])" .Params.instance) "s/s" "humanizeNoSmallPrefix") }} |
-
-
- | Memory |
- {{ template "prom_query_drilldown" (args (printf "process_resident_memory_bytes{job='prometheus',instance='%s'}" .Params.instance) "B" "humanize1024") }} |
-
-
- | Version |
- {{ with query (printf "prometheus_build_info{job='prometheus',instance='%s'}" .Params.instance) }}{{. | first | label "version"}}{{end}} |
-
-
-
- | Storage |
-
-
- | Ingested Samples |
- {{ template "prom_query_drilldown" (args (printf "irate(prometheus_tsdb_head_samples_appended_total{job='prometheus',instance='%s'}[5m])" .Params.instance) "/s" "humanizeNoSmallPrefix") }} |
-
-
- | Head Series |
- {{ template "prom_query_drilldown" (args (printf "prometheus_tsdb_head_series{job='prometheus',instance='%s'}" .Params.instance) "" "humanize") }} |
-
-
- | Blocks Loaded |
- {{ template "prom_query_drilldown" (args (printf "prometheus_tsdb_blocks_loaded{job='prometheus',instance='%s'}" .Params.instance) "" "humanize") }} |
-
-
- | Rules |
-
-
- | Evaluation Duration |
- {{ template "prom_query_drilldown" (args (printf "irate(prometheus_evaluator_duration_seconds_sum{job='prometheus',instance='%s'}[5m]) / irate(prometheus_evaluator_duration_seconds_count{job='prometheus',instance='%s'}[5m])" .Params.instance .Params.instance) "" "humanizeDuration") }} |
-
-
- | Notification Latency |
- {{ template "prom_query_drilldown" (args (printf "irate(prometheus_notifications_latency_seconds_sum{job='prometheus',instance='%s'}[5m]) / irate(prometheus_notifications_latency_seconds_count{job='prometheus',instance='%s'}[5m])" .Params.instance .Params.instance) "" "humanizeDuration") }} |
-
-
- | Notification Queue |
- {{ template "prom_query_drilldown" (args (printf "prometheus_notifications_queue_length{job='prometheus',instance='%s'}" .Params.instance) "" "humanize") }} |
-
-
- | HTTP Server |
-
-{{ range printf "prometheus_http_request_duration_seconds_count{job='prometheus',instance='%s'}" .Params.instance | query | sortByLabel "handler" }}
-
- | {{ .Labels.handler }} |
- {{ template "prom_query_drilldown" (args (printf "irate(prometheus_http_request_duration_seconds_count{job='prometheus',instance='%s',handler='%s'}[5m])" .Labels.instance .Labels.handler) "/s" "humanizeNoSmallPrefix") }} |
-
-{{ end }}
-
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
-
-
Prometheus Overview - {{ .Params.instance }}
-
-
Ingested Samples
-
-
-
-
HTTP Server
-
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/consoles/prometheus.html b/consoles/prometheus.html
deleted file mode 100644
index e0d026376d..0000000000
--- a/consoles/prometheus.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{{ template "head" . }}
-
-{{ template "prom_right_table_head" }}
-
- | Prometheus |
- {{ template "prom_query_drilldown" (args "sum(up{job='prometheus'})") }} / {{ template "prom_query_drilldown" (args "count(up{job='prometheus'})") }} |
-
-{{ template "prom_right_table_tail" }}
-
-{{ template "prom_content_head" . }}
-Prometheus
-
-
-
- | Prometheus |
- Up |
- Ingested Samples |
- Memory |
-
-{{ range query "up{job='prometheus'}" | sortByLabel "instance" }}
-
- | {{ .Labels.instance }} |
- Yes{{ else }} class="alert-danger">No{{ end }} |
- {{ template "prom_query_drilldown" (args (printf "irate(prometheus_tsdb_head_samples_appended_total{job='prometheus',instance='%s'}[5m])" .Labels.instance) "/s" "humanizeNoSmallPrefix") }} |
- {{ template "prom_query_drilldown" (args (printf "process_resident_memory_bytes{job='prometheus',instance='%s'}" .Labels.instance) "B" "humanize1024")}} |
-
-{{ else }}
-| No devices found. |
-{{ end }}
-
-
-{{ template "prom_content_tail" . }}
-
-{{ template "tail" }}
diff --git a/discovery/README.md b/discovery/README.md
index 19b579b399..4c06608625 100644
--- a/discovery/README.md
+++ b/discovery/README.md
@@ -234,6 +234,11 @@ type Config interface {
type DiscovererOptions struct {
Logger log.Logger
+
+ // A registerer for the Discoverer's metrics.
+ Registerer prometheus.Registerer
+
+ HTTPClientOptions []config.HTTPClientOption
}
```
diff --git a/discovery/aws/ec2.go b/discovery/aws/ec2.go
index 86d76627e1..a44912481a 100644
--- a/discovery/aws/ec2.go
+++ b/discovery/aws/ec2.go
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"net"
+ "strconv"
"strings"
"time"
@@ -30,6 +31,7 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -40,28 +42,29 @@ import (
)
const (
- ec2Label = model.MetaLabelPrefix + "ec2_"
- ec2LabelAMI = ec2Label + "ami"
- ec2LabelAZ = ec2Label + "availability_zone"
- ec2LabelAZID = ec2Label + "availability_zone_id"
- ec2LabelArch = ec2Label + "architecture"
- ec2LabelIPv6Addresses = ec2Label + "ipv6_addresses"
- ec2LabelInstanceID = ec2Label + "instance_id"
- ec2LabelInstanceLifecycle = ec2Label + "instance_lifecycle"
- ec2LabelInstanceState = ec2Label + "instance_state"
- ec2LabelInstanceType = ec2Label + "instance_type"
- ec2LabelOwnerID = ec2Label + "owner_id"
- ec2LabelPlatform = ec2Label + "platform"
- ec2LabelPrimarySubnetID = ec2Label + "primary_subnet_id"
- ec2LabelPrivateDNS = ec2Label + "private_dns_name"
- ec2LabelPrivateIP = ec2Label + "private_ip"
- ec2LabelPublicDNS = ec2Label + "public_dns_name"
- ec2LabelPublicIP = ec2Label + "public_ip"
- ec2LabelRegion = ec2Label + "region"
- ec2LabelSubnetID = ec2Label + "subnet_id"
- ec2LabelTag = ec2Label + "tag_"
- ec2LabelVPCID = ec2Label + "vpc_id"
- ec2LabelSeparator = ","
+ ec2Label = model.MetaLabelPrefix + "ec2_"
+ ec2LabelAMI = ec2Label + "ami"
+ ec2LabelAZ = ec2Label + "availability_zone"
+ ec2LabelAZID = ec2Label + "availability_zone_id"
+ ec2LabelArch = ec2Label + "architecture"
+ ec2LabelIPv6Addresses = ec2Label + "ipv6_addresses"
+ ec2LabelInstanceID = ec2Label + "instance_id"
+ ec2LabelInstanceLifecycle = ec2Label + "instance_lifecycle"
+ ec2LabelInstanceState = ec2Label + "instance_state"
+ ec2LabelInstanceType = ec2Label + "instance_type"
+ ec2LabelOwnerID = ec2Label + "owner_id"
+ ec2LabelPlatform = ec2Label + "platform"
+ ec2LabelPrimaryIPv6Addresses = ec2Label + "primary_ipv6_addresses"
+ ec2LabelPrimarySubnetID = ec2Label + "primary_subnet_id"
+ ec2LabelPrivateDNS = ec2Label + "private_dns_name"
+ ec2LabelPrivateIP = ec2Label + "private_ip"
+ ec2LabelPublicDNS = ec2Label + "public_dns_name"
+ ec2LabelPublicIP = ec2Label + "public_ip"
+ ec2LabelRegion = ec2Label + "region"
+ ec2LabelSubnetID = ec2Label + "subnet_id"
+ ec2LabelTag = ec2Label + "tag_"
+ ec2LabelVPCID = ec2Label + "vpc_id"
+ ec2LabelSeparator = ","
)
// DefaultEC2SDConfig is the default EC2 SD configuration.
@@ -96,12 +99,19 @@ type EC2SDConfig struct {
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*EC2SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &ec2Metrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the EC2 Config.
func (*EC2SDConfig) Name() string { return "ec2" }
// NewDiscoverer returns a Discoverer for the EC2 Config.
func (c *EC2SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewEC2Discovery(c, opts.Logger), nil
+ return NewEC2Discovery(c, opts.Logger, opts.Metrics)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface for the EC2 Config.
@@ -129,7 +139,7 @@ func (c *EC2SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return errors.New("EC2 SD configuration filter values cannot be empty")
}
}
- return nil
+ return c.HTTPClientConfig.Validate()
}
// EC2Discovery periodically performs EC2-SD requests. It implements
@@ -147,7 +157,12 @@ type EC2Discovery struct {
}
// NewEC2Discovery returns a new EC2Discovery which periodically refreshes its targets.
-func NewEC2Discovery(conf *EC2SDConfig, logger log.Logger) *EC2Discovery {
+func NewEC2Discovery(conf *EC2SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*EC2Discovery, error) {
+ m, ok := metrics.(*ec2Metrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
@@ -156,12 +171,15 @@ func NewEC2Discovery(conf *EC2SDConfig, logger log.Logger) *EC2Discovery {
cfg: conf,
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "ec2",
- time.Duration(d.cfg.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "ec2",
+ Interval: time.Duration(d.cfg.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
- return d
+ return d, nil
}
func (d *EC2Discovery) ec2Client(context.Context) (*ec2.EC2, error) {
@@ -263,7 +281,7 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
if inst.PrivateDnsName != nil {
labels[ec2LabelPrivateDNS] = model.LabelValue(*inst.PrivateDnsName)
}
- addr := net.JoinHostPort(*inst.PrivateIpAddress, fmt.Sprintf("%d", d.cfg.Port))
+ addr := net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port))
labels[model.AddressLabel] = model.LabelValue(addr)
if inst.Platform != nil {
@@ -300,6 +318,7 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
var subnets []string
var ipv6addrs []string
+ var primaryipv6addrs []string
subnetsMap := make(map[string]struct{})
for _, eni := range inst.NetworkInterfaces {
if eni.SubnetId == nil {
@@ -313,6 +332,15 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
for _, ipv6addr := range eni.Ipv6Addresses {
ipv6addrs = append(ipv6addrs, *ipv6addr.Ipv6Address)
+ if *ipv6addr.IsPrimaryIpv6 {
+ // we might have to extend the slice with more than one element
+ // that could leave empty strings in the list which is intentional
+ // to keep the position/device index information
+ for int64(len(primaryipv6addrs)) <= *eni.Attachment.DeviceIndex {
+ primaryipv6addrs = append(primaryipv6addrs, "")
+ }
+ primaryipv6addrs[*eni.Attachment.DeviceIndex] = *ipv6addr.Ipv6Address
+ }
}
}
labels[ec2LabelSubnetID] = model.LabelValue(
@@ -325,6 +353,12 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
strings.Join(ipv6addrs, ec2LabelSeparator) +
ec2LabelSeparator)
}
+ if len(primaryipv6addrs) > 0 {
+ labels[ec2LabelPrimaryIPv6Addresses] = model.LabelValue(
+ ec2LabelSeparator +
+ strings.Join(primaryipv6addrs, ec2LabelSeparator) +
+ ec2LabelSeparator)
+ }
}
for _, t := range inst.Tags {
diff --git a/discovery/aws/lightsail.go b/discovery/aws/lightsail.go
index e671769ca3..0ad7f2d541 100644
--- a/discovery/aws/lightsail.go
+++ b/discovery/aws/lightsail.go
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"net"
+ "strconv"
"strings"
"time"
@@ -29,6 +30,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -79,12 +81,19 @@ type LightsailSDConfig struct {
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*LightsailSDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &lightsailMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Lightsail Config.
func (*LightsailSDConfig) Name() string { return "lightsail" }
// NewDiscoverer returns a Discoverer for the Lightsail Config.
func (c *LightsailSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewLightsailDiscovery(c, opts.Logger), nil
+ return NewLightsailDiscovery(c, opts.Logger, opts.Metrics)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface for the Lightsail Config.
@@ -109,7 +118,7 @@ func (c *LightsailSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
}
c.Region = region
}
- return nil
+ return c.HTTPClientConfig.Validate()
}
// LightsailDiscovery periodically performs Lightsail-SD requests. It implements
@@ -121,20 +130,29 @@ type LightsailDiscovery struct {
}
// NewLightsailDiscovery returns a new LightsailDiscovery which periodically refreshes its targets.
-func NewLightsailDiscovery(conf *LightsailSDConfig, logger log.Logger) *LightsailDiscovery {
+func NewLightsailDiscovery(conf *LightsailSDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*LightsailDiscovery, error) {
+ m, ok := metrics.(*lightsailMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
+
d := &LightsailDiscovery{
cfg: conf,
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "lightsail",
- time.Duration(d.cfg.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "lightsail",
+ Interval: time.Duration(d.cfg.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
- return d
+ return d, nil
}
func (d *LightsailDiscovery) lightsailClient() (*lightsail.Lightsail, error) {
@@ -212,7 +230,7 @@ func (d *LightsailDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group,
lightsailLabelRegion: model.LabelValue(d.cfg.Region),
}
- addr := net.JoinHostPort(*inst.PrivateIpAddress, fmt.Sprintf("%d", d.cfg.Port))
+ addr := net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port))
labels[model.AddressLabel] = model.LabelValue(addr)
if inst.PublicIpAddress != nil {
diff --git a/discovery/aws/metrics_ec2.go b/discovery/aws/metrics_ec2.go
new file mode 100644
index 0000000000..73c061c3d8
--- /dev/null
+++ b/discovery/aws/metrics_ec2.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package aws
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type ec2Metrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*ec2Metrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (m *ec2Metrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *ec2Metrics) Unregister() {}
diff --git a/discovery/aws/metrics_lightsail.go b/discovery/aws/metrics_lightsail.go
new file mode 100644
index 0000000000..69593e701d
--- /dev/null
+++ b/discovery/aws/metrics_lightsail.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package aws
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type lightsailMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*lightsailMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (m *lightsailMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *lightsailMetrics) Unregister() {}
diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go
index 098fbb4c5f..70d95b9f3a 100644
--- a/discovery/azure/azure.go
+++ b/discovery/azure/azure.go
@@ -17,21 +17,29 @@ import (
"context"
"errors"
"fmt"
+ "math/rand"
"net"
"net/http"
+ "strconv"
"strings"
"sync"
"time"
- "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute"
- "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-10-01/network"
- "github.com/Azure/go-autorest/autorest"
- "github.com/Azure/go-autorest/autorest/adal"
- "github.com/Azure/go-autorest/autorest/azure"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4"
+ cache "github.com/Code-Hex/go-generics-cache"
+ "github.com/Code-Hex/go-generics-cache/policy/lru"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
config_util "github.com/prometheus/common/config"
+
"github.com/prometheus/common/model"
"github.com/prometheus/common/version"
@@ -58,6 +66,7 @@ const (
azureLabelMachineSize = azureLabel + "machine_size"
authMethodOAuth = "OAuth"
+ authMethodSDK = "SDK"
authMethodManagedIdentity = "ManagedIdentity"
)
@@ -68,21 +77,34 @@ var (
DefaultSDConfig = SDConfig{
Port: 80,
RefreshInterval: model.Duration(5 * time.Minute),
- Environment: azure.PublicCloud.Name,
+ Environment: "AzurePublicCloud",
AuthenticationMethod: authMethodOAuth,
HTTPClientConfig: config_util.DefaultHTTPClientConfig,
}
-
- failuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Name: "prometheus_sd_azure_failures_total",
- Help: "Number of Azure service discovery refresh failures.",
- })
)
+var environments = map[string]cloud.Configuration{
+ "AZURECHINACLOUD": cloud.AzureChina,
+ "AZURECLOUD": cloud.AzurePublic,
+ "AZUREGERMANCLOUD": cloud.AzurePublic,
+ "AZUREPUBLICCLOUD": cloud.AzurePublic,
+ "AZUREUSGOVERNMENT": cloud.AzureGovernment,
+ "AZUREUSGOVERNMENTCLOUD": cloud.AzureGovernment,
+}
+
+// CloudConfigurationFromName returns cloud configuration based on the common name specified.
+func CloudConfigurationFromName(name string) (cloud.Configuration, error) {
+ name = strings.ToUpper(name)
+ env, ok := environments[name]
+ if !ok {
+ return env, fmt.Errorf("there is no cloud configuration matching the name %q", name)
+ }
+
+ return env, nil
+}
+
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(failuresCount)
}
// SDConfig is the configuration for Azure based service discovery.
@@ -100,12 +122,17 @@ type SDConfig struct {
HTTPClientConfig config_util.HTTPClientConfig `yaml:",inline"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "azure" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger), nil
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
func validateAuthParam(param, name string) error {
@@ -123,7 +150,6 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err != nil {
return err
}
-
if err = validateAuthParam(c.SubscriptionID, "subscription_id"); err != nil {
return err
}
@@ -140,118 +166,162 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}
- if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity {
- return fmt.Errorf("unknown authentication_type %q. Supported types are %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity)
+ if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity && c.AuthenticationMethod != authMethodSDK {
+ return fmt.Errorf("unknown authentication_type %q. Supported types are %q, %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity, authMethodSDK)
}
- return nil
+ return c.HTTPClientConfig.Validate()
}
type Discovery struct {
*refresh.Discovery
- logger log.Logger
- cfg *SDConfig
- port int
+ logger log.Logger
+ cfg *SDConfig
+ port int
+ cache *cache.Cache[string, *armnetwork.Interface]
+ metrics *azureMetrics
}
// NewDiscovery returns a new AzureDiscovery which periodically refreshes its targets.
-func NewDiscovery(cfg *SDConfig, logger log.Logger) *Discovery {
+func NewDiscovery(cfg *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*azureMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
+ l := cache.New(cache.AsLRU[string, *armnetwork.Interface](lru.WithCapacity(5000)))
d := &Discovery{
- cfg: cfg,
- port: cfg.Port,
- logger: logger,
+ cfg: cfg,
+ port: cfg.Port,
+ logger: logger,
+ cache: l,
+ metrics: m,
}
+
d.Discovery = refresh.NewDiscovery(
- logger,
- "azure",
- time.Duration(cfg.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "azure",
+ Interval: time.Duration(cfg.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
- return d
+
+ return d, nil
+}
+
+type client interface {
+ getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error)
+ getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error)
+ getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error)
+ getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error)
+ getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error)
}
// azureClient represents multiple Azure Resource Manager providers.
type azureClient struct {
- nic network.InterfacesClient
- vm compute.VirtualMachinesClient
- vmss compute.VirtualMachineScaleSetsClient
- vmssvm compute.VirtualMachineScaleSetVMsClient
+ nic *armnetwork.InterfacesClient
+ vm *armcompute.VirtualMachinesClient
+ vmss *armcompute.VirtualMachineScaleSetsClient
+ vmssvm *armcompute.VirtualMachineScaleSetVMsClient
+ logger log.Logger
}
+var _ client = &azureClient{}
+
// createAzureClient is a helper function for creating an Azure compute client to ARM.
-func createAzureClient(cfg SDConfig) (azureClient, error) {
- env, err := azure.EnvironmentFromName(cfg.Environment)
+func createAzureClient(cfg SDConfig, logger log.Logger) (client, error) {
+ cloudConfiguration, err := CloudConfigurationFromName(cfg.Environment)
if err != nil {
- return azureClient{}, err
+ return &azureClient{}, err
}
- activeDirectoryEndpoint := env.ActiveDirectoryEndpoint
- resourceManagerEndpoint := env.ResourceManagerEndpoint
-
var c azureClient
+ c.logger = logger
- var spt *adal.ServicePrincipalToken
-
- switch cfg.AuthenticationMethod {
- case authMethodManagedIdentity:
- spt, err = adal.NewServicePrincipalTokenFromManagedIdentity(resourceManagerEndpoint, &adal.ManagedIdentityOptions{ClientID: cfg.ClientID})
- if err != nil {
- return azureClient{}, err
- }
- case authMethodOAuth:
- oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, cfg.TenantID)
- if err != nil {
- return azureClient{}, err
- }
+ telemetry := policy.TelemetryOptions{
+ ApplicationID: userAgent,
+ }
- spt, err = adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, string(cfg.ClientSecret), resourceManagerEndpoint)
- if err != nil {
- return azureClient{}, err
- }
+ credential, err := newCredential(cfg, policy.ClientOptions{
+ Cloud: cloudConfiguration,
+ Telemetry: telemetry,
+ })
+ if err != nil {
+ return &azureClient{}, err
}
client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, "azure_sd")
if err != nil {
- return azureClient{}, err
+ return &azureClient{}, err
+ }
+ options := &arm.ClientOptions{
+ ClientOptions: policy.ClientOptions{
+ Transport: client,
+ Cloud: cloudConfiguration,
+ Telemetry: telemetry,
+ },
}
- sender := autorest.DecorateSender(client)
- preparer := autorest.WithUserAgent(userAgent)
-
- bearerAuthorizer := autorest.NewBearerAuthorizer(spt)
- c.vm = compute.NewVirtualMachinesClientWithBaseURI(resourceManagerEndpoint, cfg.SubscriptionID)
- c.vm.Authorizer = bearerAuthorizer
- c.vm.Sender = sender
- c.vm.RequestInspector = preparer
+ c.vm, err = armcompute.NewVirtualMachinesClient(cfg.SubscriptionID, credential, options)
+ if err != nil {
+ return &azureClient{}, err
+ }
- c.nic = network.NewInterfacesClientWithBaseURI(resourceManagerEndpoint, cfg.SubscriptionID)
- c.nic.Authorizer = bearerAuthorizer
- c.nic.Sender = sender
- c.nic.RequestInspector = preparer
+ c.nic, err = armnetwork.NewInterfacesClient(cfg.SubscriptionID, credential, options)
+ if err != nil {
+ return &azureClient{}, err
+ }
- c.vmss = compute.NewVirtualMachineScaleSetsClientWithBaseURI(resourceManagerEndpoint, cfg.SubscriptionID)
- c.vmss.Authorizer = bearerAuthorizer
- c.vmss.Sender = sender
- c.vmss.RequestInspector = preparer
+ c.vmss, err = armcompute.NewVirtualMachineScaleSetsClient(cfg.SubscriptionID, credential, options)
+ if err != nil {
+ return &azureClient{}, err
+ }
- c.vmssvm = compute.NewVirtualMachineScaleSetVMsClientWithBaseURI(resourceManagerEndpoint, cfg.SubscriptionID)
- c.vmssvm.Authorizer = bearerAuthorizer
- c.vmssvm.Sender = sender
- c.vmssvm.RequestInspector = preparer
+ c.vmssvm, err = armcompute.NewVirtualMachineScaleSetVMsClient(cfg.SubscriptionID, credential, options)
+ if err != nil {
+ return &azureClient{}, err
+ }
- return c, nil
+ return &c, nil
}
-// azureResource represents a resource identifier in Azure.
-type azureResource struct {
- Name string
- ResourceGroup string
+func newCredential(cfg SDConfig, policyClientOptions policy.ClientOptions) (azcore.TokenCredential, error) {
+ var credential azcore.TokenCredential
+ switch cfg.AuthenticationMethod {
+ case authMethodManagedIdentity:
+ options := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: policyClientOptions, ID: azidentity.ClientID(cfg.ClientID)}
+ managedIdentityCredential, err := azidentity.NewManagedIdentityCredential(options)
+ if err != nil {
+ return nil, err
+ }
+ credential = azcore.TokenCredential(managedIdentityCredential)
+ case authMethodOAuth:
+ options := &azidentity.ClientSecretCredentialOptions{ClientOptions: policyClientOptions}
+ secretCredential, err := azidentity.NewClientSecretCredential(cfg.TenantID, cfg.ClientID, string(cfg.ClientSecret), options)
+ if err != nil {
+ return nil, err
+ }
+ credential = azcore.TokenCredential(secretCredential)
+ case authMethodSDK:
+ options := &azidentity.DefaultAzureCredentialOptions{ClientOptions: policyClientOptions}
+ if len(cfg.TenantID) != 0 {
+ options.TenantID = cfg.TenantID
+ }
+ sdkCredential, err := azidentity.NewDefaultAzureCredential(options)
+ if err != nil {
+ return nil, err
+ }
+ credential = azcore.TokenCredential(sdkCredential)
+ }
+ return credential, nil
}
-// virtualMachine represents an Azure virtual machine (which can also be created by a VMSS)
+// virtualMachine represents an Azure virtual machine (which can also be created by a VMSS).
type virtualMachine struct {
ID string
Name string
@@ -260,42 +330,38 @@ type virtualMachine struct {
Location string
OsType string
ScaleSet string
+ InstanceID string
Tags map[string]*string
NetworkInterfaces []string
Size string
}
// Create a new azureResource object from an ID string.
-func newAzureResourceFromID(id string, logger log.Logger) (azureResource, error) {
- // Resource IDs have the following format.
- // /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/PROVIDER/TYPE/NAME
- // or if embedded resource then
- // /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/PROVIDER/TYPE/NAME/TYPE/NAME
- s := strings.Split(id, "/")
- if len(s) != 9 && len(s) != 11 {
- err := fmt.Errorf("invalid ID '%s'. Refusing to create azureResource", id)
+func newAzureResourceFromID(id string, logger log.Logger) (*arm.ResourceID, error) {
+ if logger == nil {
+ logger = log.NewNopLogger()
+ }
+ resourceID, err := arm.ParseResourceID(id)
+ if err != nil {
+ err := fmt.Errorf("invalid ID '%s': %w", id, err)
level.Error(logger).Log("err", err)
- return azureResource{}, err
+ return &arm.ResourceID{}, err
}
-
- return azureResource{
- Name: strings.ToLower(s[8]),
- ResourceGroup: strings.ToLower(s[4]),
- }, nil
+ return resourceID, nil
}
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
defer level.Debug(d.logger).Log("msg", "Azure discovery completed")
- client, err := createAzureClient(*d.cfg)
+ client, err := createAzureClient(*d.cfg, d.logger)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("could not create Azure client: %w", err)
}
machines, err := client.getVMs(ctx, d.cfg.ResourceGroup)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("could not get virtual machines: %w", err)
}
@@ -304,14 +370,14 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
// Load the vms managed by scale sets.
scaleSets, err := client.getScaleSets(ctx, d.cfg.ResourceGroup)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("could not get virtual machine scale sets: %w", err)
}
for _, scaleSet := range scaleSets {
scaleSetVms, err := client.getScaleSetVMs(ctx, scaleSet)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("could not get virtual machine scale set vms: %w", err)
}
machines = append(machines, scaleSetVms...)
@@ -330,81 +396,8 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
for _, vm := range machines {
go func(vm virtualMachine) {
defer wg.Done()
- r, err := newAzureResourceFromID(vm.ID, d.logger)
- if err != nil {
- ch <- target{labelSet: nil, err: err}
- return
- }
-
- labels := model.LabelSet{
- azureLabelSubscriptionID: model.LabelValue(d.cfg.SubscriptionID),
- azureLabelTenantID: model.LabelValue(d.cfg.TenantID),
- azureLabelMachineID: model.LabelValue(vm.ID),
- azureLabelMachineName: model.LabelValue(vm.Name),
- azureLabelMachineComputerName: model.LabelValue(vm.ComputerName),
- azureLabelMachineOSType: model.LabelValue(vm.OsType),
- azureLabelMachineLocation: model.LabelValue(vm.Location),
- azureLabelMachineResourceGroup: model.LabelValue(r.ResourceGroup),
- azureLabelMachineSize: model.LabelValue(vm.Size),
- }
-
- if vm.ScaleSet != "" {
- labels[azureLabelMachineScaleSet] = model.LabelValue(vm.ScaleSet)
- }
-
- for k, v := range vm.Tags {
- name := strutil.SanitizeLabelName(k)
- labels[azureLabelMachineTag+model.LabelName(name)] = model.LabelValue(*v)
- }
-
- // Get the IP address information via separate call to the network provider.
- for _, nicID := range vm.NetworkInterfaces {
- networkInterface, err := client.getNetworkInterfaceByID(ctx, nicID)
- if err != nil {
- if errors.Is(err, errorNotFound) {
- level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err)
- } else {
- ch <- target{labelSet: nil, err: err}
- }
- // Get out of this routine because we cannot continue without a network interface.
- return
- }
-
- if networkInterface.InterfacePropertiesFormat == nil {
- continue
- }
-
- // Unfortunately Azure does not return information on whether a VM is deallocated.
- // This information is available via another API call however the Go SDK does not
- // yet support this. On deallocated machines, this value happens to be nil so it
- // is a cheap and easy way to determine if a machine is allocated or not.
- if networkInterface.Primary == nil {
- level.Debug(d.logger).Log("msg", "Skipping deallocated virtual machine", "machine", vm.Name)
- return
- }
-
- if *networkInterface.Primary {
- for _, ip := range *networkInterface.IPConfigurations {
- // IPAddress is a field defined in PublicIPAddressPropertiesFormat,
- // therefore we need to validate that both are not nil.
- if ip.PublicIPAddress != nil && ip.PublicIPAddress.PublicIPAddressPropertiesFormat != nil && ip.PublicIPAddress.IPAddress != nil {
- labels[azureLabelMachinePublicIP] = model.LabelValue(*ip.PublicIPAddress.IPAddress)
- }
- if ip.PrivateIPAddress != nil {
- labels[azureLabelMachinePrivateIP] = model.LabelValue(*ip.PrivateIPAddress)
- address := net.JoinHostPort(*ip.PrivateIPAddress, fmt.Sprintf("%d", d.port))
- labels[model.AddressLabel] = model.LabelValue(address)
- ch <- target{labelSet: labels, err: nil}
- return
- }
- // If we made it here, we don't have a private IP which should be impossible.
- // Return an empty target and error to ensure an all or nothing situation.
- err = fmt.Errorf("unable to find a private IP for VM %s", vm.Name)
- ch <- target{labelSet: nil, err: err}
- return
- }
- }
- }
+ labelSet, err := d.vmToLabelSet(ctx, client, vm)
+ ch <- target{labelSet: labelSet, err: err}
}(vm)
}
@@ -414,7 +407,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
var tg targetgroup.Group
for tgt := range ch {
if tgt.err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("unable to complete Azure service discovery: %w", tgt.err)
}
if tgt.labelSet != nil {
@@ -425,95 +418,175 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
return []*targetgroup.Group{&tg}, nil
}
-func (client *azureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) {
- var vms []virtualMachine
- var result compute.VirtualMachineListResultPage
- var err error
- if len(resourceGroup) == 0 {
- result, err = client.vm.ListAll(ctx)
- } else {
- result, err = client.vm.List(ctx, resourceGroup)
- }
+func (d *Discovery) vmToLabelSet(ctx context.Context, client client, vm virtualMachine) (model.LabelSet, error) {
+ r, err := newAzureResourceFromID(vm.ID, d.logger)
if err != nil {
- return nil, fmt.Errorf("could not list virtual machines: %w", err)
+ return nil, err
}
- for result.NotDone() {
- for _, vm := range result.Values() {
- vms = append(vms, mapFromVM(vm))
+
+ labels := model.LabelSet{
+ azureLabelSubscriptionID: model.LabelValue(d.cfg.SubscriptionID),
+ azureLabelTenantID: model.LabelValue(d.cfg.TenantID),
+ azureLabelMachineID: model.LabelValue(vm.ID),
+ azureLabelMachineName: model.LabelValue(vm.Name),
+ azureLabelMachineComputerName: model.LabelValue(vm.ComputerName),
+ azureLabelMachineOSType: model.LabelValue(vm.OsType),
+ azureLabelMachineLocation: model.LabelValue(vm.Location),
+ azureLabelMachineResourceGroup: model.LabelValue(r.ResourceGroupName),
+ azureLabelMachineSize: model.LabelValue(vm.Size),
+ }
+
+ if vm.ScaleSet != "" {
+ labels[azureLabelMachineScaleSet] = model.LabelValue(vm.ScaleSet)
+ }
+
+ for k, v := range vm.Tags {
+ name := strutil.SanitizeLabelName(k)
+ labels[azureLabelMachineTag+model.LabelName(name)] = model.LabelValue(*v)
+ }
+
+ // Get the IP address information via separate call to the network provider.
+ for _, nicID := range vm.NetworkInterfaces {
+ var networkInterface *armnetwork.Interface
+ if v, ok := d.getFromCache(nicID); ok {
+ networkInterface = v
+ d.metrics.cacheHitCount.Add(1)
+ } else {
+ if vm.ScaleSet == "" {
+ networkInterface, err = client.getVMNetworkInterfaceByID(ctx, nicID)
+ } else {
+ networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID)
+ }
+ if err != nil {
+ if errors.Is(err, errorNotFound) {
+ level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err)
+ } else {
+ return nil, err
+ }
+ // Get out of this routine because we cannot continue without a network interface.
+ return nil, nil
+ }
+
+ // Continue processing with the network interface
+ d.addToCache(nicID, networkInterface)
}
- err = result.NextWithContext(ctx)
- if err != nil {
- return nil, fmt.Errorf("could not list virtual machines: %w", err)
+
+ if networkInterface.Properties == nil {
+ continue
}
- }
- return vms, nil
-}
+ // Unfortunately Azure does not return information on whether a VM is deallocated.
+ // This information is available via another API call however the Go SDK does not
+ // yet support this. On deallocated machines, this value happens to be nil so it
+ // is a cheap and easy way to determine if a machine is allocated or not.
+ if networkInterface.Properties.Primary == nil {
+ level.Debug(d.logger).Log("msg", "Skipping deallocated virtual machine", "machine", vm.Name)
+ return nil, nil
+ }
-type VmssListResultPage interface {
- NextWithContext(ctx context.Context) (err error)
- NotDone() bool
- Values() []compute.VirtualMachineScaleSet
+ if *networkInterface.Properties.Primary {
+ for _, ip := range networkInterface.Properties.IPConfigurations {
+ // IPAddress is a field defined in PublicIPAddressPropertiesFormat,
+ // therefore we need to validate that both are not nil.
+ if ip.Properties != nil && ip.Properties.PublicIPAddress != nil && ip.Properties.PublicIPAddress.Properties != nil && ip.Properties.PublicIPAddress.Properties.IPAddress != nil {
+ labels[azureLabelMachinePublicIP] = model.LabelValue(*ip.Properties.PublicIPAddress.Properties.IPAddress)
+ }
+ if ip.Properties != nil && ip.Properties.PrivateIPAddress != nil {
+ labels[azureLabelMachinePrivateIP] = model.LabelValue(*ip.Properties.PrivateIPAddress)
+ address := net.JoinHostPort(*ip.Properties.PrivateIPAddress, strconv.Itoa(d.port))
+ labels[model.AddressLabel] = model.LabelValue(address)
+ return labels, nil
+ }
+ // If we made it here, we don't have a private IP which should be impossible.
+ // Return an empty target and error to ensure an all or nothing situation.
+ return nil, fmt.Errorf("unable to find a private IP for VM %s", vm.Name)
+ }
+ }
+ }
+ // TODO: Should we say something at this point?
+ return nil, nil
}
-func (client *azureClient) getScaleSets(ctx context.Context, resourceGroup string) ([]compute.VirtualMachineScaleSet, error) {
- var scaleSets []compute.VirtualMachineScaleSet
- var result VmssListResultPage
- var err error
+func (client *azureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) {
+ var vms []virtualMachine
if len(resourceGroup) == 0 {
- var rtn compute.VirtualMachineScaleSetListWithLinkResultPage
- rtn, err = client.vmss.ListAll(ctx)
- if err != nil {
- return nil, fmt.Errorf("could not list virtual machine scale sets: %w", err)
+ pager := client.vm.NewListAllPager(nil)
+ for pager.More() {
+ nextResult, err := pager.NextPage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("could not list virtual machines: %w", err)
+ }
+ for _, vm := range nextResult.Value {
+ vms = append(vms, mapFromVM(*vm))
+ }
}
- result = &rtn
} else {
- var rtn compute.VirtualMachineScaleSetListResultPage
- rtn, err = client.vmss.List(ctx, resourceGroup)
- if err != nil {
- return nil, fmt.Errorf("could not list virtual machine scale sets: %w", err)
+ pager := client.vm.NewListPager(resourceGroup, nil)
+ for pager.More() {
+ nextResult, err := pager.NextPage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("could not list virtual machines: %w", err)
+ }
+ for _, vm := range nextResult.Value {
+ vms = append(vms, mapFromVM(*vm))
+ }
}
- result = &rtn
}
+ return vms, nil
+}
- for result.NotDone() {
- scaleSets = append(scaleSets, result.Values()...)
- err = result.NextWithContext(ctx)
- if err != nil {
- return nil, fmt.Errorf("could not list virtual machine scale sets: %w", err)
+func (client *azureClient) getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error) {
+ var scaleSets []armcompute.VirtualMachineScaleSet
+ if len(resourceGroup) == 0 {
+ pager := client.vmss.NewListAllPager(nil)
+ for pager.More() {
+ nextResult, err := pager.NextPage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("could not list virtual machine scale sets: %w", err)
+ }
+ for _, vmss := range nextResult.Value {
+ scaleSets = append(scaleSets, *vmss)
+ }
+ }
+ } else {
+ pager := client.vmss.NewListPager(resourceGroup, nil)
+ for pager.More() {
+ nextResult, err := pager.NextPage(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("could not list virtual machine scale sets: %w", err)
+ }
+ for _, vmss := range nextResult.Value {
+ scaleSets = append(scaleSets, *vmss)
+ }
}
}
-
return scaleSets, nil
}
-func (client *azureClient) getScaleSetVMs(ctx context.Context, scaleSet compute.VirtualMachineScaleSet) ([]virtualMachine, error) {
+func (client *azureClient) getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error) {
var vms []virtualMachine
// TODO do we really need to fetch the resourcegroup this way?
- r, err := newAzureResourceFromID(*scaleSet.ID, nil)
+ r, err := newAzureResourceFromID(*scaleSet.ID, client.logger)
if err != nil {
return nil, fmt.Errorf("could not parse scale set ID: %w", err)
}
- result, err := client.vmssvm.List(ctx, r.ResourceGroup, *(scaleSet.Name), "", "", "")
- if err != nil {
- return nil, fmt.Errorf("could not list virtual machine scale set vms: %w", err)
- }
- for result.NotDone() {
- for _, vm := range result.Values() {
- vms = append(vms, mapFromVMScaleSetVM(vm, *scaleSet.Name))
- }
- err = result.NextWithContext(ctx)
+ pager := client.vmssvm.NewListPager(r.ResourceGroupName, *(scaleSet.Name), nil)
+ for pager.More() {
+ nextResult, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("could not list virtual machine scale set vms: %w", err)
}
+ for _, vmssvm := range nextResult.Value {
+ vms = append(vms, mapFromVMScaleSetVM(*vmssvm, *scaleSet.Name))
+ }
}
return vms, nil
}
-func mapFromVM(vm compute.VirtualMachine) virtualMachine {
- osType := string(vm.StorageProfile.OsDisk.OsType)
+func mapFromVM(vm armcompute.VirtualMachine) virtualMachine {
+ var osType string
tags := map[string]*string{}
networkInterfaces := []string{}
var computerName string
@@ -523,18 +596,23 @@ func mapFromVM(vm compute.VirtualMachine) virtualMachine {
tags = vm.Tags
}
- if vm.NetworkProfile != nil {
- for _, vmNIC := range *(vm.NetworkProfile.NetworkInterfaces) {
- networkInterfaces = append(networkInterfaces, *vmNIC.ID)
+ if vm.Properties != nil {
+ if vm.Properties.StorageProfile != nil &&
+ vm.Properties.StorageProfile.OSDisk != nil &&
+ vm.Properties.StorageProfile.OSDisk.OSType != nil {
+ osType = string(*vm.Properties.StorageProfile.OSDisk.OSType)
}
- }
- if vm.VirtualMachineProperties != nil {
- if vm.VirtualMachineProperties.OsProfile != nil && vm.VirtualMachineProperties.OsProfile.ComputerName != nil {
- computerName = *(vm.VirtualMachineProperties.OsProfile.ComputerName)
+ if vm.Properties.NetworkProfile != nil {
+ for _, vmNIC := range vm.Properties.NetworkProfile.NetworkInterfaces {
+ networkInterfaces = append(networkInterfaces, *vmNIC.ID)
+ }
+ }
+ if vm.Properties.OSProfile != nil && vm.Properties.OSProfile.ComputerName != nil {
+ computerName = *(vm.Properties.OSProfile.ComputerName)
}
- if vm.VirtualMachineProperties.HardwareProfile != nil {
- size = string(vm.VirtualMachineProperties.HardwareProfile.VMSize)
+ if vm.Properties.HardwareProfile != nil {
+ size = string(*vm.Properties.HardwareProfile.VMSize)
}
}
@@ -552,8 +630,8 @@ func mapFromVM(vm compute.VirtualMachine) virtualMachine {
}
}
-func mapFromVMScaleSetVM(vm compute.VirtualMachineScaleSetVM, scaleSetName string) virtualMachine {
- osType := string(vm.StorageProfile.OsDisk.OsType)
+func mapFromVMScaleSetVM(vm armcompute.VirtualMachineScaleSetVM, scaleSetName string) virtualMachine {
+ var osType string
tags := map[string]*string{}
networkInterfaces := []string{}
var computerName string
@@ -563,18 +641,23 @@ func mapFromVMScaleSetVM(vm compute.VirtualMachineScaleSetVM, scaleSetName strin
tags = vm.Tags
}
- if vm.NetworkProfile != nil {
- for _, vmNIC := range *(vm.NetworkProfile.NetworkInterfaces) {
- networkInterfaces = append(networkInterfaces, *vmNIC.ID)
+ if vm.Properties != nil {
+ if vm.Properties.StorageProfile != nil &&
+ vm.Properties.StorageProfile.OSDisk != nil &&
+ vm.Properties.StorageProfile.OSDisk.OSType != nil {
+ osType = string(*vm.Properties.StorageProfile.OSDisk.OSType)
}
- }
- if vm.VirtualMachineScaleSetVMProperties != nil {
- if vm.VirtualMachineScaleSetVMProperties.OsProfile != nil && vm.VirtualMachineScaleSetVMProperties.OsProfile.ComputerName != nil {
- computerName = *(vm.VirtualMachineScaleSetVMProperties.OsProfile.ComputerName)
+ if vm.Properties.NetworkProfile != nil {
+ for _, vmNIC := range vm.Properties.NetworkProfile.NetworkInterfaces {
+ networkInterfaces = append(networkInterfaces, *vmNIC.ID)
+ }
}
- if vm.VirtualMachineScaleSetVMProperties.HardwareProfile != nil {
- size = string(vm.VirtualMachineScaleSetVMProperties.HardwareProfile.VMSize)
+ if vm.Properties.OSProfile != nil && vm.Properties.OSProfile.ComputerName != nil {
+ computerName = *(vm.Properties.OSProfile.ComputerName)
+ }
+ if vm.Properties.HardwareProfile != nil {
+ size = string(*vm.Properties.HardwareProfile.VMSize)
}
}
@@ -586,6 +669,7 @@ func mapFromVMScaleSetVM(vm compute.VirtualMachineScaleSetVM, scaleSetName strin
Location: *(vm.Location),
OsType: osType,
ScaleSet: scaleSetName,
+ InstanceID: *(vm.InstanceID),
Tags: tags,
NetworkInterfaces: networkInterfaces,
Size: size,
@@ -594,38 +678,58 @@ func mapFromVMScaleSetVM(vm compute.VirtualMachineScaleSetVM, scaleSetName strin
var errorNotFound = errors.New("network interface does not exist")
-// getNetworkInterfaceByID gets the network interface.
+// getVMNetworkInterfaceByID gets the network interface.
// If a 404 is returned from the Azure API, `errorNotFound` is returned.
-// On all other errors, an autorest.DetailedError is returned.
-func (client *azureClient) getNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*network.Interface, error) {
- result := network.Interface{}
- queryParameters := map[string]interface{}{
- "api-version": "2018-10-01",
- }
-
- preparer := autorest.CreatePreparer(
- autorest.AsGet(),
- autorest.WithBaseURL(client.nic.BaseURI),
- autorest.WithPath(networkInterfaceID),
- autorest.WithQueryParameters(queryParameters),
- autorest.WithUserAgent(userAgent))
- req, err := preparer.Prepare((&http.Request{}).WithContext(ctx))
+func (client *azureClient) getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error) {
+ r, err := newAzureResourceFromID(networkInterfaceID, client.logger)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse network interface ID: %w", err)
+ }
+
+ resp, err := client.nic.Get(ctx, r.ResourceGroupName, r.Name, &armnetwork.InterfacesClientGetOptions{Expand: to.Ptr("IPConfigurations/PublicIPAddress")})
if err != nil {
- return nil, autorest.NewErrorWithError(err, "network.InterfacesClient", "Get", nil, "Failure preparing request")
+ var responseError *azcore.ResponseError
+ if errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound {
+ return nil, errorNotFound
+ }
+ return nil, fmt.Errorf("failed to retrieve Interface %v with error: %w", networkInterfaceID, err)
}
- resp, err := client.nic.GetSender(req)
+ return &resp.Interface, nil
+}
+
+// getVMScaleSetVMNetworkInterfaceByID gets the network interface.
+// If a 404 is returned from the Azure API, `errorNotFound` is returned.
+func (client *azureClient) getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error) {
+ r, err := newAzureResourceFromID(networkInterfaceID, client.logger)
if err != nil {
- return nil, autorest.NewErrorWithError(err, "network.InterfacesClient", "Get", resp, "Failure sending request")
+ return nil, fmt.Errorf("could not parse network interface ID: %w", err)
}
- result, err = client.nic.GetResponder(resp)
+ resp, err := client.nic.GetVirtualMachineScaleSetNetworkInterface(ctx, r.ResourceGroupName, scaleSetName, instanceID, r.Name, &armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceOptions{Expand: to.Ptr("IPConfigurations/PublicIPAddress")})
if err != nil {
- if resp.StatusCode == http.StatusNotFound {
+ var responseError *azcore.ResponseError
+ if errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound {
return nil, errorNotFound
}
- return nil, autorest.NewErrorWithError(err, "network.InterfacesClient", "Get", resp, "Failure responding to request")
+ return nil, fmt.Errorf("failed to retrieve Interface %v with error: %w", networkInterfaceID, err)
}
- return &result, nil
+ return &resp.Interface, nil
+}
+
+// addToCache will add the network interface information for the specified nicID.
+func (d *Discovery) addToCache(nicID string, netInt *armnetwork.Interface) {
+ random := rand.Int63n(int64(time.Duration(d.cfg.RefreshInterval * 3).Seconds()))
+ rs := time.Duration(random) * time.Second
+ exptime := time.Duration(d.cfg.RefreshInterval*10) + rs
+ d.cache.Set(nicID, netInt, cache.WithExpiration(exptime))
+ level.Debug(d.logger).Log("msg", "Adding nic", "nic", nicID, "time", exptime.Seconds())
+}
+
+// getFromCache will get the network Interface for the specified nicID
+// If the cache is disabled nothing will happen.
+func (d *Discovery) getFromCache(nicID string) (*armnetwork.Interface, bool) {
+ net, found := d.cache.Get(nicID)
+ return net, found
}
diff --git a/discovery/azure/azure_test.go b/discovery/azure/azure_test.go
index 179b97ba61..32dab66c8c 100644
--- a/discovery/azure/azure_test.go
+++ b/discovery/azure/azure_test.go
@@ -14,49 +14,60 @@
package azure
import (
+ "context"
+ "fmt"
"testing"
- "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4"
+ cache "github.com/Code-Hex/go-generics-cache"
+ "github.com/Code-Hex/go-generics-cache/policy/lru"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
- goleak.VerifyTestMain(m)
+ goleak.VerifyTestMain(m,
+ goleak.IgnoreTopFunction("github.com/Code-Hex/go-generics-cache.(*janitor).run.func1"),
+ )
}
func TestMapFromVMWithEmptyTags(t *testing.T) {
id := "test"
name := "name"
size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ osType := armcompute.OperatingSystemTypesLinux
vmType := "type"
location := "westeurope"
computerName := "computer_name"
- networkProfile := compute.NetworkProfile{
- NetworkInterfaces: &[]compute.NetworkInterfaceReference{},
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{},
}
- properties := &compute.VirtualMachineProperties{
- OsProfile: &compute.OSProfile{
+ properties := &armcompute.VirtualMachineProperties{
+ OSProfile: &armcompute.OSProfile{
ComputerName: &computerName,
},
- StorageProfile: &compute.StorageProfile{
- OsDisk: &compute.OSDisk{
- OsType: "Linux",
+ StorageProfile: &armcompute.StorageProfile{
+ OSDisk: &armcompute.OSDisk{
+ OSType: &osType,
},
},
NetworkProfile: &networkProfile,
- HardwareProfile: &compute.HardwareProfile{
- VMSize: compute.VirtualMachineSizeTypes(size),
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
},
}
- testVM := compute.VirtualMachine{
- ID: &id,
- Name: &name,
- Type: &vmType,
- Location: &location,
- Tags: nil,
- VirtualMachineProperties: properties,
+ testVM := armcompute.VirtualMachine{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ Location: &location,
+ Tags: nil,
+ Properties: properties,
}
expectedVM := virtualMachine{
@@ -76,41 +87,177 @@ func TestMapFromVMWithEmptyTags(t *testing.T) {
require.Equal(t, expectedVM, actualVM)
}
+func TestVMToLabelSet(t *testing.T) {
+ id := "/subscriptions/00000000-0000-0000-0000-000000000000/test"
+ name := "name"
+ size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ osType := armcompute.OperatingSystemTypesLinux
+ vmType := "type"
+ location := "westeurope"
+ computerName := "computer_name"
+ networkID := "/subscriptions/00000000-0000-0000-0000-000000000000/network1"
+ ipAddress := "10.20.30.40"
+ primary := true
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
+ {
+ ID: &networkID,
+ Properties: &armcompute.NetworkInterfaceReferenceProperties{Primary: &primary},
+ },
+ },
+ }
+ properties := &armcompute.VirtualMachineProperties{
+ OSProfile: &armcompute.OSProfile{
+ ComputerName: &computerName,
+ },
+ StorageProfile: &armcompute.StorageProfile{
+ OSDisk: &armcompute.OSDisk{
+ OSType: &osType,
+ },
+ },
+ NetworkProfile: &networkProfile,
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
+ },
+ }
+
+ testVM := armcompute.VirtualMachine{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ Location: &location,
+ Tags: nil,
+ Properties: properties,
+ }
+
+ expectedVM := virtualMachine{
+ ID: id,
+ Name: name,
+ ComputerName: computerName,
+ Type: vmType,
+ Location: location,
+ OsType: "Linux",
+ Tags: map[string]*string{},
+ NetworkInterfaces: []string{networkID},
+ Size: size,
+ }
+
+ actualVM := mapFromVM(testVM)
+
+ require.Equal(t, expectedVM, actualVM)
+
+ cfg := DefaultSDConfig
+ d := &Discovery{
+ cfg: &cfg,
+ logger: log.NewNopLogger(),
+ cache: cache.New(cache.AsLRU[string, *armnetwork.Interface](lru.WithCapacity(5))),
+ }
+ network := armnetwork.Interface{
+ Name: &networkID,
+ Properties: &armnetwork.InterfacePropertiesFormat{
+ Primary: &primary,
+ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
+ {Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
+ PrivateIPAddress: &ipAddress,
+ }},
+ },
+ },
+ }
+ client := &mockAzureClient{
+ networkInterface: &network,
+ }
+ labelSet, err := d.vmToLabelSet(context.Background(), client, actualVM)
+ require.NoError(t, err)
+ require.Len(t, labelSet, 11)
+}
+
+func TestMapFromVMWithEmptyOSType(t *testing.T) {
+ id := "test"
+ name := "name"
+ size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ vmType := "type"
+ location := "westeurope"
+ computerName := "computer_name"
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{},
+ }
+ properties := &armcompute.VirtualMachineProperties{
+ OSProfile: &armcompute.OSProfile{
+ ComputerName: &computerName,
+ },
+ StorageProfile: &armcompute.StorageProfile{
+ OSDisk: &armcompute.OSDisk{},
+ },
+ NetworkProfile: &networkProfile,
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
+ },
+ }
+
+ testVM := armcompute.VirtualMachine{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ Location: &location,
+ Tags: nil,
+ Properties: properties,
+ }
+
+ expectedVM := virtualMachine{
+ ID: id,
+ Name: name,
+ ComputerName: computerName,
+ Type: vmType,
+ Location: location,
+ Tags: map[string]*string{},
+ NetworkInterfaces: []string{},
+ Size: size,
+ }
+
+ actualVM := mapFromVM(testVM)
+
+ require.Equal(t, expectedVM, actualVM)
+}
+
func TestMapFromVMWithTags(t *testing.T) {
id := "test"
name := "name"
size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ osType := armcompute.OperatingSystemTypesLinux
vmType := "type"
location := "westeurope"
computerName := "computer_name"
tags := map[string]*string{
"prometheus": new(string),
}
- networkProfile := compute.NetworkProfile{
- NetworkInterfaces: &[]compute.NetworkInterfaceReference{},
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{},
}
- properties := &compute.VirtualMachineProperties{
- OsProfile: &compute.OSProfile{
+ properties := &armcompute.VirtualMachineProperties{
+ OSProfile: &armcompute.OSProfile{
ComputerName: &computerName,
},
- StorageProfile: &compute.StorageProfile{
- OsDisk: &compute.OSDisk{
- OsType: "Linux",
+ StorageProfile: &armcompute.StorageProfile{
+ OSDisk: &armcompute.OSDisk{
+ OSType: &osType,
},
},
NetworkProfile: &networkProfile,
- HardwareProfile: &compute.HardwareProfile{
- VMSize: compute.VirtualMachineSizeTypes(size),
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
},
}
- testVM := compute.VirtualMachine{
- ID: &id,
- Name: &name,
- Type: &vmType,
- Location: &location,
- Tags: tags,
- VirtualMachineProperties: properties,
+ testVM := armcompute.VirtualMachine{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ Location: &location,
+ Tags: tags,
+ Properties: properties,
}
expectedVM := virtualMachine{
@@ -134,34 +281,38 @@ func TestMapFromVMScaleSetVMWithEmptyTags(t *testing.T) {
id := "test"
name := "name"
size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ osType := armcompute.OperatingSystemTypesLinux
vmType := "type"
+ instanceID := "123"
location := "westeurope"
computerName := "computer_name"
- networkProfile := compute.NetworkProfile{
- NetworkInterfaces: &[]compute.NetworkInterfaceReference{},
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{},
}
- properties := &compute.VirtualMachineScaleSetVMProperties{
- OsProfile: &compute.OSProfile{
+ properties := &armcompute.VirtualMachineScaleSetVMProperties{
+ OSProfile: &armcompute.OSProfile{
ComputerName: &computerName,
},
- StorageProfile: &compute.StorageProfile{
- OsDisk: &compute.OSDisk{
- OsType: "Linux",
+ StorageProfile: &armcompute.StorageProfile{
+ OSDisk: &armcompute.OSDisk{
+ OSType: &osType,
},
},
NetworkProfile: &networkProfile,
- HardwareProfile: &compute.HardwareProfile{
- VMSize: compute.VirtualMachineSizeTypes(size),
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
},
}
- testVM := compute.VirtualMachineScaleSetVM{
- ID: &id,
- Name: &name,
- Type: &vmType,
- Location: &location,
- Tags: nil,
- VirtualMachineScaleSetVMProperties: properties,
+ testVM := armcompute.VirtualMachineScaleSetVM{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ InstanceID: &instanceID,
+ Location: &location,
+ Tags: nil,
+ Properties: properties,
}
scaleSet := "testSet"
@@ -175,6 +326,59 @@ func TestMapFromVMScaleSetVMWithEmptyTags(t *testing.T) {
Tags: map[string]*string{},
NetworkInterfaces: []string{},
ScaleSet: scaleSet,
+ InstanceID: instanceID,
+ Size: size,
+ }
+
+ actualVM := mapFromVMScaleSetVM(testVM, scaleSet)
+
+ require.Equal(t, expectedVM, actualVM)
+}
+
+func TestMapFromVMScaleSetVMWithEmptyOSType(t *testing.T) {
+ id := "test"
+ name := "name"
+ size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ vmType := "type"
+ instanceID := "123"
+ location := "westeurope"
+ computerName := "computer_name"
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{},
+ }
+ properties := &armcompute.VirtualMachineScaleSetVMProperties{
+ OSProfile: &armcompute.OSProfile{
+ ComputerName: &computerName,
+ },
+ StorageProfile: &armcompute.StorageProfile{},
+ NetworkProfile: &networkProfile,
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
+ },
+ }
+
+ testVM := armcompute.VirtualMachineScaleSetVM{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ InstanceID: &instanceID,
+ Location: &location,
+ Tags: nil,
+ Properties: properties,
+ }
+
+ scaleSet := "testSet"
+ expectedVM := virtualMachine{
+ ID: id,
+ Name: name,
+ ComputerName: computerName,
+ Type: vmType,
+ Location: location,
+ Tags: map[string]*string{},
+ NetworkInterfaces: []string{},
+ ScaleSet: scaleSet,
+ InstanceID: instanceID,
Size: size,
}
@@ -187,37 +391,41 @@ func TestMapFromVMScaleSetVMWithTags(t *testing.T) {
id := "test"
name := "name"
size := "size"
+ vmSize := armcompute.VirtualMachineSizeTypes(size)
+ osType := armcompute.OperatingSystemTypesLinux
vmType := "type"
+ instanceID := "123"
location := "westeurope"
computerName := "computer_name"
tags := map[string]*string{
"prometheus": new(string),
}
- networkProfile := compute.NetworkProfile{
- NetworkInterfaces: &[]compute.NetworkInterfaceReference{},
+ networkProfile := armcompute.NetworkProfile{
+ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{},
}
- properties := &compute.VirtualMachineScaleSetVMProperties{
- OsProfile: &compute.OSProfile{
+ properties := &armcompute.VirtualMachineScaleSetVMProperties{
+ OSProfile: &armcompute.OSProfile{
ComputerName: &computerName,
},
- StorageProfile: &compute.StorageProfile{
- OsDisk: &compute.OSDisk{
- OsType: "Linux",
+ StorageProfile: &armcompute.StorageProfile{
+ OSDisk: &armcompute.OSDisk{
+ OSType: &osType,
},
},
NetworkProfile: &networkProfile,
- HardwareProfile: &compute.HardwareProfile{
- VMSize: compute.VirtualMachineSizeTypes(size),
+ HardwareProfile: &armcompute.HardwareProfile{
+ VMSize: &vmSize,
},
}
- testVM := compute.VirtualMachineScaleSetVM{
- ID: &id,
- Name: &name,
- Type: &vmType,
- Location: &location,
- Tags: tags,
- VirtualMachineScaleSetVMProperties: properties,
+ testVM := armcompute.VirtualMachineScaleSetVM{
+ ID: &id,
+ Name: &name,
+ Type: &vmType,
+ InstanceID: &instanceID,
+ Location: &location,
+ Tags: tags,
+ Properties: properties,
}
scaleSet := "testSet"
@@ -231,6 +439,7 @@ func TestMapFromVMScaleSetVMWithTags(t *testing.T) {
Tags: tags,
NetworkInterfaces: []string{},
ScaleSet: scaleSet,
+ InstanceID: instanceID,
Size: size,
}
@@ -242,18 +451,58 @@ func TestMapFromVMScaleSetVMWithTags(t *testing.T) {
func TestNewAzureResourceFromID(t *testing.T) {
for _, tc := range []struct {
id string
- expected azureResource
+ expected *arm.ResourceID
}{
{
- id: "/a/b/c/group/d/e/f/name",
- expected: azureResource{"name", "group"},
+ id: "/subscriptions/SUBSCRIPTION_ID/resourceGroups/group/providers/PROVIDER/TYPE/name",
+ expected: &arm.ResourceID{
+ Name: "name",
+ ResourceGroupName: "group",
+ },
},
{
- id: "/a/b/c/group/d/e/f/name/g/h",
- expected: azureResource{"name", "group"},
+ id: "/subscriptions/SUBSCRIPTION_ID/resourceGroups/group/providers/PROVIDER/TYPE/name/TYPE/h",
+ expected: &arm.ResourceID{
+ Name: "h",
+ ResourceGroupName: "group",
+ },
},
} {
- actual, _ := newAzureResourceFromID(tc.id, nil)
- require.Equal(t, tc.expected, actual)
+ actual, err := newAzureResourceFromID(tc.id, nil)
+ require.NoError(t, err)
+ require.Equal(t, tc.expected.Name, actual.Name)
+ require.Equal(t, tc.expected.ResourceGroupName, actual.ResourceGroupName)
+ }
+}
+
+type mockAzureClient struct {
+ networkInterface *armnetwork.Interface
+}
+
+var _ client = &mockAzureClient{}
+
+func (*mockAzureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) {
+ return nil, nil
+}
+
+func (*mockAzureClient) getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error) {
+ return nil, nil
+}
+
+func (*mockAzureClient) getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error) {
+ return nil, nil
+}
+
+func (m *mockAzureClient) getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error) {
+ if networkInterfaceID == "" {
+ return nil, fmt.Errorf("parameter networkInterfaceID cannot be empty")
+ }
+ return m.networkInterface, nil
+}
+
+func (m *mockAzureClient) getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error) {
+ if scaleSetName == "" {
+ return nil, fmt.Errorf("parameter virtualMachineScaleSetName cannot be empty")
}
+ return m.networkInterface, nil
}
diff --git a/discovery/azure/metrics.go b/discovery/azure/metrics.go
new file mode 100644
index 0000000000..3e3dbdbfbb
--- /dev/null
+++ b/discovery/azure/metrics.go
@@ -0,0 +1,64 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package azure
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*azureMetrics)(nil)
+
+type azureMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+
+ failuresCount prometheus.Counter
+ cacheHitCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &azureMetrics{
+ refreshMetrics: rmi,
+ failuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_azure_failures_total",
+ Help: "Number of Azure service discovery refresh failures.",
+ }),
+ cacheHitCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_azure_cache_hit_total",
+ Help: "Number of cache hit during refresh.",
+ }),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.failuresCount,
+ m.cacheHitCount,
+ })
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *azureMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *azureMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/consul/consul.go b/discovery/consul/consul.go
index 99ea396b99..bdc1fc8dce 100644
--- a/discovery/consul/consul.go
+++ b/discovery/consul/consul.go
@@ -50,7 +50,7 @@ const (
tagsLabel = model.MetaLabelPrefix + "consul_tags"
// serviceLabel is the name of the label containing the service name.
serviceLabel = model.MetaLabelPrefix + "consul_service"
- // healthLabel is the name of the label containing the health of the service instance
+ // healthLabel is the name of the label containing the health of the service instance.
healthLabel = model.MetaLabelPrefix + "consul_health"
// serviceAddressLabel is the name of the label containing the (optional) service address.
serviceAddressLabel = model.MetaLabelPrefix + "consul_service_address"
@@ -71,41 +71,18 @@ const (
namespace = "prometheus"
)
-var (
- rpcFailuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Namespace: namespace,
- Name: "sd_consul_rpc_failures_total",
- Help: "The number of Consul RPC call failures.",
- })
- rpcDuration = prometheus.NewSummaryVec(
- prometheus.SummaryOpts{
- Namespace: namespace,
- Name: "sd_consul_rpc_duration_seconds",
- Help: "The duration of a Consul RPC call in seconds.",
- Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
- },
- []string{"endpoint", "call"},
- )
-
- // Initialize metric vectors.
- servicesRPCDuration = rpcDuration.WithLabelValues("catalog", "services")
- serviceRPCDuration = rpcDuration.WithLabelValues("catalog", "service")
-
- // DefaultSDConfig is the default Consul SD configuration.
- DefaultSDConfig = SDConfig{
- TagSeparator: ",",
- Scheme: "http",
- Server: "localhost:8500",
- AllowStale: true,
- RefreshInterval: model.Duration(30 * time.Second),
- HTTPClientConfig: config.DefaultHTTPClientConfig,
- }
-)
+// DefaultSDConfig is the default Consul SD configuration.
+var DefaultSDConfig = SDConfig{
+ TagSeparator: ",",
+ Scheme: "http",
+ Server: "localhost:8500",
+ AllowStale: true,
+ RefreshInterval: model.Duration(30 * time.Second),
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+}
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(rpcFailuresCount, rpcDuration)
}
// SDConfig is the configuration for Consul service discovery.
@@ -142,12 +119,17 @@ type SDConfig struct {
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "consul" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -196,10 +178,16 @@ type Discovery struct {
refreshInterval time.Duration
finalizer func()
logger log.Logger
+ metrics *consulMetrics
}
// NewDiscovery returns a new Discovery for the given config.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*consulMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
@@ -237,7 +225,9 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
clientPartition: conf.Partition,
finalizer: wrapper.CloseIdleConnections,
logger: logger,
+ metrics: m,
}
+
return cd, nil
}
@@ -293,7 +283,7 @@ func (d *Discovery) getDatacenter() error {
info, err := d.client.Agent().Self()
if err != nil {
level.Error(d.logger).Log("msg", "Error retrieving datacenter name", "err", err)
- rpcFailuresCount.Inc()
+ d.metrics.rpcFailuresCount.Inc()
return err
}
@@ -382,7 +372,7 @@ func (d *Discovery) watchServices(ctx context.Context, ch chan<- []*targetgroup.
t0 := time.Now()
srvs, meta, err := catalog.Services(opts.WithContext(ctx))
elapsed := time.Since(t0)
- servicesRPCDuration.Observe(elapsed.Seconds())
+ d.metrics.servicesRPCDuration.Observe(elapsed.Seconds())
// Check the context before in order to exit early.
select {
@@ -393,7 +383,7 @@ func (d *Discovery) watchServices(ctx context.Context, ch chan<- []*targetgroup.
if err != nil {
level.Error(d.logger).Log("msg", "Error refreshing service list", "err", err)
- rpcFailuresCount.Inc()
+ d.metrics.rpcFailuresCount.Inc()
time.Sleep(retryInterval)
return
}
@@ -449,13 +439,15 @@ func (d *Discovery) watchServices(ctx context.Context, ch chan<- []*targetgroup.
// consulService contains data belonging to the same service.
type consulService struct {
- name string
- tags []string
- labels model.LabelSet
- discovery *Discovery
- client *consul.Client
- tagSeparator string
- logger log.Logger
+ name string
+ tags []string
+ labels model.LabelSet
+ discovery *Discovery
+ client *consul.Client
+ tagSeparator string
+ logger log.Logger
+ rpcFailuresCount prometheus.Counter
+ serviceRPCDuration prometheus.Observer
}
// Start watching a service.
@@ -469,8 +461,10 @@ func (d *Discovery) watchService(ctx context.Context, ch chan<- []*targetgroup.G
serviceLabel: model.LabelValue(name),
datacenterLabel: model.LabelValue(d.clientDatacenter),
},
- tagSeparator: d.tagSeparator,
- logger: d.logger,
+ tagSeparator: d.tagSeparator,
+ logger: d.logger,
+ rpcFailuresCount: d.metrics.rpcFailuresCount,
+ serviceRPCDuration: d.metrics.serviceRPCDuration,
}
go func() {
@@ -508,7 +502,7 @@ func (srv *consulService) watch(ctx context.Context, ch chan<- []*targetgroup.Gr
t0 := time.Now()
serviceNodes, meta, err := health.ServiceMultipleTags(srv.name, srv.tags, false, opts.WithContext(ctx))
elapsed := time.Since(t0)
- serviceRPCDuration.Observe(elapsed.Seconds())
+ srv.serviceRPCDuration.Observe(elapsed.Seconds())
// Check the context before in order to exit early.
select {
@@ -520,7 +514,7 @@ func (srv *consulService) watch(ctx context.Context, ch chan<- []*targetgroup.Gr
if err != nil {
level.Error(srv.logger).Log("msg", "Error refreshing service", "service", srv.name, "tags", strings.Join(srv.tags, ","), "err", err)
- rpcFailuresCount.Inc()
+ srv.rpcFailuresCount.Inc()
time.Sleep(retryInterval)
return
}
@@ -545,9 +539,9 @@ func (srv *consulService) watch(ctx context.Context, ch chan<- []*targetgroup.Gr
// since the service may be registered remotely through a different node.
var addr string
if serviceNode.Service.Address != "" {
- addr = net.JoinHostPort(serviceNode.Service.Address, fmt.Sprintf("%d", serviceNode.Service.Port))
+ addr = net.JoinHostPort(serviceNode.Service.Address, strconv.Itoa(serviceNode.Service.Port))
} else {
- addr = net.JoinHostPort(serviceNode.Node.Address, fmt.Sprintf("%d", serviceNode.Service.Port))
+ addr = net.JoinHostPort(serviceNode.Node.Address, strconv.Itoa(serviceNode.Service.Port))
}
labels := model.LabelSet{
diff --git a/discovery/consul/consul_test.go b/discovery/consul/consul_test.go
index c929601638..e3bc7938f5 100644
--- a/discovery/consul/consul_test.go
+++ b/discovery/consul/consul_test.go
@@ -22,12 +22,14 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"gopkg.in/yaml.v2"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -35,20 +37,30 @@ func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
+// TODO: Add ability to unregister metrics?
+func NewTestMetrics(t *testing.T, conf discovery.Config, reg prometheus.Registerer) discovery.DiscovererMetrics {
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ require.NoError(t, refreshMetrics.Register())
+
+ metrics := conf.NewDiscovererMetrics(prometheus.NewRegistry(), refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ return metrics
+}
+
func TestConfiguredService(t *testing.T) {
conf := &SDConfig{
Services: []string{"configuredServiceName"},
}
- consulDiscovery, err := NewDiscovery(conf, nil)
- if err != nil {
- t.Errorf("Unexpected error when initializing discovery %v", err)
- }
- if !consulDiscovery.shouldWatch("configuredServiceName", []string{""}) {
- t.Errorf("Expected service %s to be watched", "configuredServiceName")
- }
- if consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{""}) {
- t.Errorf("Expected service %s to not be watched", "nonConfiguredServiceName")
- }
+
+ metrics := NewTestMetrics(t, conf, prometheus.NewRegistry())
+
+ consulDiscovery, err := NewDiscovery(conf, nil, metrics)
+ require.NoError(t, err, "when initializing discovery")
+ require.True(t, consulDiscovery.shouldWatch("configuredServiceName", []string{""}),
+ "Expected service %s to be watched", "configuredServiceName")
+ require.False(t, consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{""}),
+ "Expected service %s to not be watched", "nonConfiguredServiceName")
}
func TestConfiguredServiceWithTag(t *testing.T) {
@@ -56,22 +68,22 @@ func TestConfiguredServiceWithTag(t *testing.T) {
Services: []string{"configuredServiceName"},
ServiceTags: []string{"http"},
}
- consulDiscovery, err := NewDiscovery(conf, nil)
- if err != nil {
- t.Errorf("Unexpected error when initializing discovery %v", err)
- }
- if consulDiscovery.shouldWatch("configuredServiceName", []string{""}) {
- t.Errorf("Expected service %s to not be watched without tag", "configuredServiceName")
- }
- if !consulDiscovery.shouldWatch("configuredServiceName", []string{"http"}) {
- t.Errorf("Expected service %s to be watched with tag %s", "configuredServiceName", "http")
- }
- if consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{""}) {
- t.Errorf("Expected service %s to not be watched without tag", "nonConfiguredServiceName")
- }
- if consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{"http"}) {
- t.Errorf("Expected service %s to not be watched with tag %s", "nonConfiguredServiceName", "http")
- }
+
+ metrics := NewTestMetrics(t, conf, prometheus.NewRegistry())
+
+ consulDiscovery, err := NewDiscovery(conf, nil, metrics)
+ require.NoError(t, err, "when initializing discovery")
+ require.False(t, consulDiscovery.shouldWatch("configuredServiceName", []string{""}),
+ "Expected service %s to not be watched without tag", "configuredServiceName")
+
+ require.True(t, consulDiscovery.shouldWatch("configuredServiceName", []string{"http"}),
+ "Expected service %s to be watched with tag %s", "configuredServiceName", "http")
+
+ require.False(t, consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{""}),
+ "Expected service %s to not be watched without tag", "nonConfiguredServiceName")
+
+ require.False(t, consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{"http"}),
+ "Expected service %s to not be watched with tag %s", "nonConfiguredServiceName", "http")
}
func TestConfiguredServiceWithTags(t *testing.T) {
@@ -151,27 +163,24 @@ func TestConfiguredServiceWithTags(t *testing.T) {
}
for _, tc := range cases {
- consulDiscovery, err := NewDiscovery(tc.conf, nil)
- if err != nil {
- t.Errorf("Unexpected error when initializing discovery %v", err)
- }
- ret := consulDiscovery.shouldWatch(tc.serviceName, tc.serviceTags)
- if ret != tc.shouldWatch {
- t.Errorf("Expected should watch? %t, got %t. Watched service and tags: %s %+v, input was %s %+v", tc.shouldWatch, ret, tc.conf.Services, tc.conf.ServiceTags, tc.serviceName, tc.serviceTags)
- }
+ metrics := NewTestMetrics(t, tc.conf, prometheus.NewRegistry())
+ consulDiscovery, err := NewDiscovery(tc.conf, nil, metrics)
+ require.NoError(t, err, "when initializing discovery")
+ ret := consulDiscovery.shouldWatch(tc.serviceName, tc.serviceTags)
+ require.Equal(t, tc.shouldWatch, ret, "Watched service and tags: %s %+v, input was %s %+v",
+ tc.conf.Services, tc.conf.ServiceTags, tc.serviceName, tc.serviceTags)
}
}
func TestNonConfiguredService(t *testing.T) {
conf := &SDConfig{}
- consulDiscovery, err := NewDiscovery(conf, nil)
- if err != nil {
- t.Errorf("Unexpected error when initializing discovery %v", err)
- }
- if !consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{""}) {
- t.Errorf("Expected service %s to be watched", "nonConfiguredServiceName")
- }
+
+ metrics := NewTestMetrics(t, conf, prometheus.NewRegistry())
+
+ consulDiscovery, err := NewDiscovery(conf, nil, metrics)
+ require.NoError(t, err, "when initializing discovery")
+ require.True(t, consulDiscovery.shouldWatch("nonConfiguredServiceName", []string{""}), "Expected service %s to be watched", "nonConfiguredServiceName")
}
const (
@@ -262,19 +271,22 @@ func newServer(t *testing.T) (*httptest.Server, *SDConfig) {
func newDiscovery(t *testing.T, config *SDConfig) *Discovery {
logger := log.NewNopLogger()
- d, err := NewDiscovery(config, logger)
+
+ metrics := NewTestMetrics(t, config, prometheus.NewRegistry())
+
+ d, err := NewDiscovery(config, logger, metrics)
require.NoError(t, err)
return d
}
func checkOneTarget(t *testing.T, tg []*targetgroup.Group) {
- require.Equal(t, 1, len(tg))
+ require.Len(t, tg, 1)
target := tg[0]
require.Equal(t, "test-dc", string(target.Labels["__meta_consul_dc"]))
require.Equal(t, target.Source, string(target.Labels["__meta_consul_service"]))
if target.Source == "test" {
// test service should have one node.
- require.Greater(t, len(target.Targets), 0, "Test service should have one node")
+ require.NotEmpty(t, target.Targets, "Test service should have one node")
}
}
@@ -313,7 +325,7 @@ func TestNoTargets(t *testing.T) {
}()
targets := (<-ch)[0].Targets
- require.Equal(t, 0, len(targets))
+ require.Empty(t, targets)
cancel()
<-ch
}
@@ -476,15 +488,12 @@ oauth2:
var config SDConfig
err := config.UnmarshalYAML(unmarshal([]byte(test.config)))
if err != nil {
- require.Equalf(t, err.Error(), test.errMessage, "Expected error '%s', got '%v'", test.errMessage, err)
- return
- }
- if test.errMessage != "" {
- t.Errorf("Expected error %s, got none", test.errMessage)
+ require.EqualError(t, err, test.errMessage)
return
}
+ require.Empty(t, test.errMessage, "Expected error.")
- require.Equal(t, config, test.expected)
+ require.Equal(t, test.expected, config)
})
}
}
diff --git a/discovery/consul/metrics.go b/discovery/consul/metrics.go
new file mode 100644
index 0000000000..8266e7cc60
--- /dev/null
+++ b/discovery/consul/metrics.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package consul
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*consulMetrics)(nil)
+
+type consulMetrics struct {
+ rpcFailuresCount prometheus.Counter
+ rpcDuration *prometheus.SummaryVec
+
+ servicesRPCDuration prometheus.Observer
+ serviceRPCDuration prometheus.Observer
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &consulMetrics{
+ rpcFailuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Namespace: namespace,
+ Name: "sd_consul_rpc_failures_total",
+ Help: "The number of Consul RPC call failures.",
+ }),
+ rpcDuration: prometheus.NewSummaryVec(
+ prometheus.SummaryOpts{
+ Namespace: namespace,
+ Name: "sd_consul_rpc_duration_seconds",
+ Help: "The duration of a Consul RPC call in seconds.",
+ Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
+ },
+ []string{"endpoint", "call"},
+ ),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.rpcFailuresCount,
+ m.rpcDuration,
+ })
+
+ // Initialize metric vectors.
+ m.servicesRPCDuration = m.rpcDuration.WithLabelValues("catalog", "services")
+ m.serviceRPCDuration = m.rpcDuration.WithLabelValues("catalog", "service")
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *consulMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *consulMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/digitalocean/digitalocean.go b/discovery/digitalocean/digitalocean.go
index e207388b3d..ecee60cb1f 100644
--- a/discovery/digitalocean/digitalocean.go
+++ b/discovery/digitalocean/digitalocean.go
@@ -24,6 +24,7 @@ import (
"github.com/digitalocean/godo"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/version"
@@ -62,6 +63,13 @@ func init() {
discovery.RegisterConfig(&SDConfig{})
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &digitaloceanMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// SDConfig is the configuration for DigitalOcean based service discovery.
type SDConfig struct {
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
@@ -75,7 +83,7 @@ func (*SDConfig) Name() string { return "digitalocean" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -103,7 +111,12 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*digitaloceanMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
d := &Discovery{
port: conf.Port,
}
@@ -125,10 +138,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "digitalocean",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "digitalocean",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -161,7 +177,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
}
labels := model.LabelSet{
- doLabelID: model.LabelValue(fmt.Sprintf("%d", droplet.ID)),
+ doLabelID: model.LabelValue(strconv.Itoa(droplet.ID)),
doLabelName: model.LabelValue(droplet.Name),
doLabelImage: model.LabelValue(droplet.Image.Slug),
doLabelImageName: model.LabelValue(droplet.Image.Name),
diff --git a/discovery/digitalocean/digitalocean_test.go b/discovery/digitalocean/digitalocean_test.go
index a5da4b26e3..841b5ef977 100644
--- a/discovery/digitalocean/digitalocean_test.go
+++ b/discovery/digitalocean/digitalocean_test.go
@@ -20,8 +20,11 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery"
)
type DigitalOceanSDTestSuite struct {
@@ -46,7 +49,15 @@ func TestDigitalOceanSDRefresh(t *testing.T) {
cfg := DefaultSDConfig
cfg.HTTPClientConfig.BearerToken = tokenID
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
endpoint, err := url.Parse(sdmock.Mock.Endpoint())
require.NoError(t, err)
@@ -56,12 +67,12 @@ func TestDigitalOceanSDRefresh(t *testing.T) {
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 4, len(tg.Targets))
+ require.Len(t, tg.Targets, 4)
for i, lbls := range []model.LabelSet{
{
diff --git a/discovery/digitalocean/metrics.go b/discovery/digitalocean/metrics.go
new file mode 100644
index 0000000000..91cfd9bf30
--- /dev/null
+++ b/discovery/digitalocean/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package digitalocean
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*digitaloceanMetrics)(nil)
+
+type digitaloceanMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *digitaloceanMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *digitaloceanMetrics) Unregister() {}
diff --git a/discovery/digitalocean/mock_test.go b/discovery/digitalocean/mock_test.go
index 2f19b5e1a1..62d963c3b3 100644
--- a/discovery/digitalocean/mock_test.go
+++ b/discovery/digitalocean/mock_test.go
@@ -21,7 +21,7 @@ import (
"testing"
)
-// SDMock is the interface for the DigitalOcean mock
+// SDMock is the interface for the DigitalOcean mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
@@ -35,18 +35,18 @@ func NewSDMock(t *testing.T) *SDMock {
}
}
-// Endpoint returns the URI to the mock server
+// Endpoint returns the URI to the mock server.
func (m *SDMock) Endpoint() string {
return m.Server.URL + "/"
}
-// Setup creates the mock server
+// Setup creates the mock server.
func (m *SDMock) Setup() {
m.Mux = http.NewServeMux()
m.Server = httptest.NewServer(m.Mux)
}
-// ShutdownServer creates the mock server
+// ShutdownServer creates the mock server.
func (m *SDMock) ShutdownServer() {
m.Server.Close()
}
diff --git a/discovery/discoverer_metrics_noop.go b/discovery/discoverer_metrics_noop.go
new file mode 100644
index 0000000000..4321204b6c
--- /dev/null
+++ b/discovery/discoverer_metrics_noop.go
@@ -0,0 +1,28 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package discovery
+
+// NoopDiscovererMetrics creates a dummy metrics struct, because this SD doesn't have any metrics.
+type NoopDiscovererMetrics struct{}
+
+var _ DiscovererMetrics = (*NoopDiscovererMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (*NoopDiscovererMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (*NoopDiscovererMetrics) Unregister() {
+}
diff --git a/discovery/discovery.go b/discovery/discovery.go
index 9dc010a09a..9a83df409b 100644
--- a/discovery/discovery.go
+++ b/discovery/discovery.go
@@ -18,6 +18,7 @@ import (
"reflect"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/prometheus/discovery/targetgroup"
@@ -38,15 +39,48 @@ type Discoverer interface {
Run(ctx context.Context, up chan<- []*targetgroup.Group)
}
+// DiscovererMetrics are internal metrics of service discovery mechanisms.
+type DiscovererMetrics interface {
+ Register() error
+ Unregister()
+}
+
// DiscovererOptions provides options for a Discoverer.
type DiscovererOptions struct {
Logger log.Logger
+ Metrics DiscovererMetrics
+
// Extra HTTP client options to expose to Discoverers. This field may be
// ignored; Discoverer implementations must opt-in to reading it.
HTTPClientOptions []config.HTTPClientOption
}
+// RefreshMetrics are used by the "refresh" package.
+// We define them here in the "discovery" package in order to avoid a cyclic dependency between
+// "discovery" and "refresh".
+type RefreshMetrics struct {
+ Failures prometheus.Counter
+ Duration prometheus.Observer
+}
+
+// RefreshMetricsInstantiator instantiates the metrics used by the "refresh" package.
+type RefreshMetricsInstantiator interface {
+ Instantiate(mech string) *RefreshMetrics
+}
+
+// RefreshMetricsManager is an interface for registering, unregistering, and
+// instantiating metrics for the "refresh" package. Refresh metrics are
+// registered and unregistered outside of the service discovery mechanism. This
+// is so that the same metrics can be reused across different service discovery
+// mechanisms. To manage refresh metrics inside the SD mechanism, we'd need to
+// use const labels which are specific to that SD. However, doing so would also
+// expose too many unused metrics on the Prometheus /metrics endpoint.
+type RefreshMetricsManager interface {
+ DiscovererMetrics
+ RefreshMetricsInstantiator
+}
+
// A Config provides the configuration and constructor for a Discoverer.
type Config interface {
// Name returns the name of the discovery mechanism.
@@ -55,6 +89,9 @@ type Config interface {
// NewDiscoverer returns a Discoverer for the Config
// with the given DiscovererOptions.
NewDiscoverer(DiscovererOptions) (Discoverer, error)
+
+ // NewDiscovererMetrics returns the metrics used by the service discovery.
+ NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics
}
// Configs is a slice of Config values that uses custom YAML marshaling and unmarshaling
@@ -72,7 +109,7 @@ func (c *Configs) SetDirectory(dir string) {
// UnmarshalYAML implements yaml.Unmarshaler.
func (c *Configs) UnmarshalYAML(unmarshal func(interface{}) error) error {
- cfgTyp := getConfigType(configsType)
+ cfgTyp := reflect.StructOf(configFields)
cfgPtr := reflect.New(cfgTyp)
cfgVal := cfgPtr.Elem()
@@ -87,7 +124,7 @@ func (c *Configs) UnmarshalYAML(unmarshal func(interface{}) error) error {
// MarshalYAML implements yaml.Marshaler.
func (c Configs) MarshalYAML() (interface{}, error) {
- cfgTyp := getConfigType(configsType)
+ cfgTyp := reflect.StructOf(configFields)
cfgPtr := reflect.New(cfgTyp)
cfgVal := cfgPtr.Elem()
@@ -109,6 +146,12 @@ func (c StaticConfig) NewDiscoverer(DiscovererOptions) (Discoverer, error) {
return staticDiscoverer(c), nil
}
+// NewDiscovererMetrics returns NoopDiscovererMetrics because no metrics are
+// needed for this service discovery mechanism.
+func (c StaticConfig) NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics {
+ return &NoopDiscovererMetrics{}
+}
+
type staticDiscoverer []*targetgroup.Group
func (c staticDiscoverer) Run(ctx context.Context, up chan<- []*targetgroup.Group) {
diff --git a/discovery/discovery_test.go b/discovery/discovery_test.go
new file mode 100644
index 0000000000..af327195f2
--- /dev/null
+++ b/discovery/discovery_test.go
@@ -0,0 +1,36 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package discovery
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gopkg.in/yaml.v2"
+)
+
+func TestConfigsCustomUnMarshalMarshal(t *testing.T) {
+ input := `static_configs:
+- targets:
+ - foo:1234
+ - bar:4321
+`
+ cfg := &Configs{}
+ err := yaml.UnmarshalStrict([]byte(input), cfg)
+ require.NoError(t, err)
+
+ output, err := yaml.Marshal(cfg)
+ require.NoError(t, err)
+ require.Equal(t, input, string(output))
+}
diff --git a/discovery/dns/dns.go b/discovery/dns/dns.go
index 96e07254f0..314c3d38cd 100644
--- a/discovery/dns/dns.go
+++ b/discovery/dns/dns.go
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"net"
+ "strconv"
"strings"
"sync"
"time"
@@ -42,35 +43,21 @@ const (
dnsSrvRecordPortLabel = dnsSrvRecordPrefix + "port"
dnsMxRecordPrefix = model.MetaLabelPrefix + "dns_mx_record_"
dnsMxRecordTargetLabel = dnsMxRecordPrefix + "target"
+ dnsNsRecordPrefix = model.MetaLabelPrefix + "dns_ns_record_"
+ dnsNsRecordTargetLabel = dnsNsRecordPrefix + "target"
// Constants for instrumentation.
namespace = "prometheus"
)
-var (
- dnsSDLookupsCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Namespace: namespace,
- Name: "sd_dns_lookups_total",
- Help: "The number of DNS-SD lookups.",
- })
- dnsSDLookupFailuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Namespace: namespace,
- Name: "sd_dns_lookup_failures_total",
- Help: "The number of DNS-SD lookup failures.",
- })
-
- // DefaultSDConfig is the default DNS SD configuration.
- DefaultSDConfig = SDConfig{
- RefreshInterval: model.Duration(30 * time.Second),
- Type: "SRV",
- }
-)
+// DefaultSDConfig is the default DNS SD configuration.
+var DefaultSDConfig = SDConfig{
+ RefreshInterval: model.Duration(30 * time.Second),
+ Type: "SRV",
+}
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(dnsSDLookupFailuresCount, dnsSDLookupsCount)
}
// SDConfig is the configuration for DNS based service discovery.
@@ -81,12 +68,17 @@ type SDConfig struct {
Port int `yaml:"port"` // Ignored for SRV records
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "dns" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(*c, opts.Logger), nil
+ return NewDiscovery(*c, opts.Logger, opts.Metrics)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -102,7 +94,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
switch strings.ToUpper(c.Type) {
case "SRV":
- case "A", "AAAA", "MX":
+ case "A", "AAAA", "MX", "NS":
if c.Port == 0 {
return errors.New("a port is required in DNS-SD configs for all record types except SRV")
}
@@ -116,16 +108,22 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
// the Discoverer interface.
type Discovery struct {
*refresh.Discovery
- names []string
- port int
- qtype uint16
- logger log.Logger
+ names []string
+ port int
+ qtype uint16
+ logger log.Logger
+ metrics *dnsMetrics
lookupFn func(name string, qtype uint16, logger log.Logger) (*dns.Msg, error)
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf SDConfig, logger log.Logger) *Discovery {
+func NewDiscovery(conf SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*dnsMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
@@ -140,6 +138,8 @@ func NewDiscovery(conf SDConfig, logger log.Logger) *Discovery {
qtype = dns.TypeSRV
case "MX":
qtype = dns.TypeMX
+ case "NS":
+ qtype = dns.TypeNS
}
d := &Discovery{
names: conf.Names,
@@ -147,14 +147,20 @@ func NewDiscovery(conf SDConfig, logger log.Logger) *Discovery {
port: conf.Port,
logger: logger,
lookupFn: lookupWithSearchPath,
+ metrics: m,
}
+
d.Discovery = refresh.NewDiscovery(
- logger,
- "dns",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "dns",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
- return d
+
+ return d, nil
}
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
@@ -187,24 +193,24 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
func (d *Discovery) refreshOne(ctx context.Context, name string, ch chan<- *targetgroup.Group) error {
response, err := d.lookupFn(name, d.qtype, d.logger)
- dnsSDLookupsCount.Inc()
+ d.metrics.dnsSDLookupsCount.Inc()
if err != nil {
- dnsSDLookupFailuresCount.Inc()
+ d.metrics.dnsSDLookupFailuresCount.Inc()
return err
}
tg := &targetgroup.Group{}
hostPort := func(a string, p int) model.LabelValue {
- return model.LabelValue(net.JoinHostPort(a, fmt.Sprintf("%d", p)))
+ return model.LabelValue(net.JoinHostPort(a, strconv.Itoa(p)))
}
for _, record := range response.Answer {
- var target, dnsSrvRecordTarget, dnsSrvRecordPort, dnsMxRecordTarget model.LabelValue
+ var target, dnsSrvRecordTarget, dnsSrvRecordPort, dnsMxRecordTarget, dnsNsRecordTarget model.LabelValue
switch addr := record.(type) {
case *dns.SRV:
dnsSrvRecordTarget = model.LabelValue(addr.Target)
- dnsSrvRecordPort = model.LabelValue(fmt.Sprintf("%d", addr.Port))
+ dnsSrvRecordPort = model.LabelValue(strconv.Itoa(int(addr.Port)))
// Remove the final dot from rooted DNS names to make them look more usual.
addr.Target = strings.TrimRight(addr.Target, ".")
@@ -217,6 +223,13 @@ func (d *Discovery) refreshOne(ctx context.Context, name string, ch chan<- *targ
addr.Mx = strings.TrimRight(addr.Mx, ".")
target = hostPort(addr.Mx, d.port)
+ case *dns.NS:
+ dnsNsRecordTarget = model.LabelValue(addr.Ns)
+
+ // Remove the final dot from rooted DNS names to make them look more usual.
+ addr.Ns = strings.TrimRight(addr.Ns, ".")
+
+ target = hostPort(addr.Ns, d.port)
case *dns.A:
target = hostPort(addr.A.String(), d.port)
case *dns.AAAA:
@@ -234,6 +247,7 @@ func (d *Discovery) refreshOne(ctx context.Context, name string, ch chan<- *targ
dnsSrvRecordTargetLabel: dnsSrvRecordTarget,
dnsSrvRecordPortLabel: dnsSrvRecordPort,
dnsMxRecordTargetLabel: dnsMxRecordTarget,
+ dnsNsRecordTargetLabel: dnsNsRecordTarget,
})
}
diff --git a/discovery/dns/dns_test.go b/discovery/dns/dns_test.go
index 50b2860496..33a976827d 100644
--- a/discovery/dns/dns_test.go
+++ b/discovery/dns/dns_test.go
@@ -22,11 +22,13 @@ import (
"github.com/go-kit/log"
"github.com/miekg/dns"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"gopkg.in/yaml.v2"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -81,6 +83,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "",
+ "__meta_dns_ns_record_target": "",
},
},
},
@@ -112,6 +115,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "",
+ "__meta_dns_ns_record_target": "",
},
},
},
@@ -143,6 +147,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "db1.example.com.",
"__meta_dns_srv_record_port": "3306",
"__meta_dns_mx_record_target": "",
+ "__meta_dns_ns_record_target": "",
},
{
"__address__": "db2.example.com:3306",
@@ -150,6 +155,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "db2.example.com.",
"__meta_dns_srv_record_port": "3306",
"__meta_dns_mx_record_target": "",
+ "__meta_dns_ns_record_target": "",
},
},
},
@@ -180,6 +186,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "db1.example.com.",
"__meta_dns_srv_record_port": "3306",
"__meta_dns_mx_record_target": "",
+ "__meta_dns_ns_record_target": "",
},
},
},
@@ -227,6 +234,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "smtp1.example.com.",
+ "__meta_dns_ns_record_target": "",
},
{
"__address__": "smtp2.example.com:25",
@@ -234,6 +242,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "smtp2.example.com.",
+ "__meta_dns_ns_record_target": "",
},
},
},
@@ -245,12 +254,21 @@ func TestDNS(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
- sd := NewDiscovery(tc.config, nil)
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := tc.config.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ sd, err := NewDiscovery(tc.config, nil, metrics)
+ require.NoError(t, err)
sd.lookupFn = tc.lookup
tgs, err := sd.refresh(context.Background())
require.NoError(t, err)
require.Equal(t, tc.expected, tgs)
+
+ metrics.Unregister()
})
}
}
diff --git a/discovery/dns/metrics.go b/discovery/dns/metrics.go
new file mode 100644
index 0000000000..27c96b53e0
--- /dev/null
+++ b/discovery/dns/metrics.go
@@ -0,0 +1,66 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dns
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*dnsMetrics)(nil)
+
+type dnsMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+
+ dnsSDLookupsCount prometheus.Counter
+ dnsSDLookupFailuresCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &dnsMetrics{
+ refreshMetrics: rmi,
+ dnsSDLookupsCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Namespace: namespace,
+ Name: "sd_dns_lookups_total",
+ Help: "The number of DNS-SD lookups.",
+ }),
+ dnsSDLookupFailuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Namespace: namespace,
+ Name: "sd_dns_lookup_failures_total",
+ Help: "The number of DNS-SD lookup failures.",
+ }),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.dnsSDLookupsCount,
+ m.dnsSDLookupFailuresCount,
+ })
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *dnsMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *dnsMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/eureka/client.go b/discovery/eureka/client.go
index a833415a53..52e8ce7b48 100644
--- a/discovery/eureka/client.go
+++ b/discovery/eureka/client.go
@@ -81,7 +81,7 @@ const appListPath string = "/apps"
func fetchApps(ctx context.Context, server string, client *http.Client) (*Applications, error) {
url := fmt.Sprintf("%s%s", server, appListPath)
- request, err := http.NewRequest("GET", url, nil)
+ request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
diff --git a/discovery/eureka/client_test.go b/discovery/eureka/client_test.go
index f1451c3a9e..83f6fd5ff1 100644
--- a/discovery/eureka/client_test.go
+++ b/discovery/eureka/client_test.go
@@ -184,17 +184,17 @@ func TestFetchApps(t *testing.T) {
apps, err := fetchApps(context.TODO(), ts.URL, &http.Client{})
require.NoError(t, err)
- require.Equal(t, len(apps.Applications), 2)
- require.Equal(t, apps.Applications[0].Name, "CONFIG-SERVICE")
- require.Equal(t, apps.Applications[1].Name, "META-SERVICE")
+ require.Len(t, apps.Applications, 2)
+ require.Equal(t, "CONFIG-SERVICE", apps.Applications[0].Name)
+ require.Equal(t, "META-SERVICE", apps.Applications[1].Name)
- require.Equal(t, len(apps.Applications[1].Instances), 2)
- require.Equal(t, apps.Applications[1].Instances[0].InstanceID, "meta-service002.test.com:meta-service:8080")
- require.Equal(t, apps.Applications[1].Instances[0].Metadata.Items[0].XMLName.Local, "project")
- require.Equal(t, apps.Applications[1].Instances[0].Metadata.Items[0].Content, "meta-service")
- require.Equal(t, apps.Applications[1].Instances[0].Metadata.Items[1].XMLName.Local, "management.port")
- require.Equal(t, apps.Applications[1].Instances[0].Metadata.Items[1].Content, "8090")
- require.Equal(t, apps.Applications[1].Instances[1].InstanceID, "meta-service001.test.com:meta-service:8080")
+ require.Len(t, apps.Applications[1].Instances, 2)
+ require.Equal(t, "meta-service002.test.com:meta-service:8080", apps.Applications[1].Instances[0].InstanceID)
+ require.Equal(t, "project", apps.Applications[1].Instances[0].Metadata.Items[0].XMLName.Local)
+ require.Equal(t, "meta-service", apps.Applications[1].Instances[0].Metadata.Items[0].Content)
+ require.Equal(t, "management.port", apps.Applications[1].Instances[0].Metadata.Items[1].XMLName.Local)
+ require.Equal(t, "8090", apps.Applications[1].Instances[0].Metadata.Items[1].Content)
+ require.Equal(t, "meta-service001.test.com:meta-service:8080", apps.Applications[1].Instances[1].InstanceID)
}
func Test500ErrorHttpResponse(t *testing.T) {
diff --git a/discovery/eureka/eureka.go b/discovery/eureka/eureka.go
index 5d9d8d552d..779c081aee 100644
--- a/discovery/eureka/eureka.go
+++ b/discovery/eureka/eureka.go
@@ -16,6 +16,7 @@ package eureka
import (
"context"
"errors"
+ "fmt"
"net"
"net/http"
"net/url"
@@ -23,6 +24,7 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -75,12 +77,19 @@ type SDConfig struct {
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &eurekaMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "eureka" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -117,7 +126,12 @@ type Discovery struct {
}
// NewDiscovery creates a new Eureka discovery for the given role.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*eurekaMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "eureka_sd")
if err != nil {
return nil, err
@@ -128,10 +142,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
server: conf.Server,
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "eureka",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "eureka",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -211,7 +228,6 @@ func targetsForApp(app *Application) []model.LabelSet {
}
targets = append(targets, target)
-
}
return targets
}
diff --git a/discovery/eureka/eureka_test.go b/discovery/eureka/eureka_test.go
index 0641aa7bf2..b499410bfc 100644
--- a/discovery/eureka/eureka_test.go
+++ b/discovery/eureka/eureka_test.go
@@ -20,9 +20,11 @@ import (
"net/http/httptest"
"testing"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -35,7 +37,17 @@ func testUpdateServices(respHandler http.HandlerFunc) ([]*targetgroup.Group, err
Server: ts.URL,
}
- md, err := NewDiscovery(&conf, nil)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := conf.NewDiscovererMetrics(reg, refreshMetrics)
+ err := metrics.Register()
+ if err != nil {
+ return nil, err
+ }
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ md, err := NewDiscovery(&conf, nil, metrics)
if err != nil {
return nil, err
}
@@ -55,7 +67,7 @@ func TestEurekaSDHandleError(t *testing.T) {
tgs, err := testUpdateServices(respHandler)
require.EqualError(t, err, errTesting)
- require.Equal(t, len(tgs), 0)
+ require.Empty(t, tgs)
}
func TestEurekaSDEmptyList(t *testing.T) {
@@ -72,7 +84,7 @@ func TestEurekaSDEmptyList(t *testing.T) {
)
tgs, err := testUpdateServices(respHandler)
require.NoError(t, err)
- require.Equal(t, len(tgs), 1)
+ require.Len(t, tgs, 1)
}
func TestEurekaSDSendGroup(t *testing.T) {
@@ -232,11 +244,11 @@ func TestEurekaSDSendGroup(t *testing.T) {
tgs, err := testUpdateServices(respHandler)
require.NoError(t, err)
- require.Equal(t, len(tgs), 1)
+ require.Len(t, tgs, 1)
tg := tgs[0]
- require.Equal(t, tg.Source, "eureka")
- require.Equal(t, len(tg.Targets), 4)
+ require.Equal(t, "eureka", tg.Source)
+ require.Len(t, tg.Targets, 4)
tgt := tg.Targets[0]
require.Equal(t, tgt[model.AddressLabel], model.LabelValue("config-service001.test.com:8080"))
diff --git a/discovery/eureka/metrics.go b/discovery/eureka/metrics.go
new file mode 100644
index 0000000000..6d6463ccd3
--- /dev/null
+++ b/discovery/eureka/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package eureka
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*eurekaMetrics)(nil)
+
+type eurekaMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *eurekaMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *eurekaMetrics) Unregister() {}
diff --git a/discovery/file/file.go b/discovery/file/file.go
index 60b63350f5..e7e9d0870f 100644
--- a/discovery/file/file.go
+++ b/discovery/file/file.go
@@ -39,24 +39,6 @@ import (
)
var (
- fileSDReadErrorsCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Name: "prometheus_sd_file_read_errors_total",
- Help: "The number of File-SD read errors.",
- })
- fileSDScanDuration = prometheus.NewSummary(
- prometheus.SummaryOpts{
- Name: "prometheus_sd_file_scan_duration_seconds",
- Help: "The duration of the File-SD scan in seconds.",
- Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
- })
- fileSDTimeStamp = NewTimestampCollector()
- fileWatcherErrorsCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Name: "prometheus_sd_file_watcher_errors_total",
- Help: "The number of File-SD errors caused by filesystem watch failures.",
- })
-
patFileSDName = regexp.MustCompile(`^[^*]*(\*[^/]*)?\.(json|yml|yaml|JSON|YML|YAML)$`)
// DefaultSDConfig is the default file SD configuration.
@@ -67,7 +49,6 @@ var (
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(fileSDReadErrorsCount, fileSDScanDuration, fileSDTimeStamp, fileWatcherErrorsCount)
}
// SDConfig is the configuration for file based discovery.
@@ -76,12 +57,17 @@ type SDConfig struct {
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "file" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger), nil
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -113,6 +99,9 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
const fileSDFilepathLabel = model.MetaLabelPrefix + "filepath"
// TimestampCollector is a Custom Collector for Timestamps of the files.
+// TODO(ptodev): Now that each file SD has its own TimestampCollector
+// inside discovery/file/metrics.go, we can refactor this collector
+// (or get rid of it) as each TimestampCollector instance will only use one discoverer.
type TimestampCollector struct {
Description *prometheus.Desc
discoverers map[*Discovery]struct{}
@@ -187,10 +176,17 @@ type Discovery struct {
// This is used to detect deleted target groups.
lastRefresh map[string]int
logger log.Logger
+
+ metrics *fileMetrics
}
// NewDiscovery returns a new file discovery for the given paths.
-func NewDiscovery(conf *SDConfig, logger log.Logger) *Discovery {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ fm, ok := metrics.(*fileMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
@@ -200,9 +196,12 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) *Discovery {
interval: time.Duration(conf.RefreshInterval),
timestamps: make(map[string]float64),
logger: logger,
+ metrics: fm,
}
- fileSDTimeStamp.addDiscoverer(disc)
- return disc
+
+ fm.init(disc)
+
+ return disc, nil
}
// listFiles returns a list of all files that match the configured patterns.
@@ -242,7 +241,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
level.Error(d.logger).Log("msg", "Error adding file watcher", "err", err)
- fileWatcherErrorsCount.Inc()
+ d.metrics.fileWatcherErrorsCount.Inc()
return
}
d.watcher = watcher
@@ -306,7 +305,7 @@ func (d *Discovery) stop() {
done := make(chan struct{})
defer close(done)
- fileSDTimeStamp.removeDiscoverer(d)
+ d.metrics.fileSDTimeStamp.removeDiscoverer(d)
// Closing the watcher will deadlock unless all events and errors are drained.
go func() {
@@ -332,13 +331,13 @@ func (d *Discovery) stop() {
func (d *Discovery) refresh(ctx context.Context, ch chan<- []*targetgroup.Group) {
t0 := time.Now()
defer func() {
- fileSDScanDuration.Observe(time.Since(t0).Seconds())
+ d.metrics.fileSDScanDuration.Observe(time.Since(t0).Seconds())
}()
ref := map[string]int{}
for _, p := range d.listFiles() {
tgroups, err := d.readFile(p)
if err != nil {
- fileSDReadErrorsCount.Inc()
+ d.metrics.fileSDReadErrorsCount.Inc()
level.Error(d.logger).Log("msg", "Error reading file", "path", p, "err", err)
// Prevent deletion down below.
diff --git a/discovery/file/file_test.go b/discovery/file/file_test.go
index 76e1cebed9..179ac5cd1c 100644
--- a/discovery/file/file_test.go
+++ b/discovery/file/file_test.go
@@ -24,10 +24,12 @@ import (
"testing"
"time"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -143,15 +145,28 @@ func (t *testRunner) run(files ...string) {
ctx, cancel := context.WithCancel(context.Background())
t.cancelSD = cancel
go func() {
- NewDiscovery(
- &SDConfig{
- Files: files,
- // Setting a high refresh interval to make sure that the tests only
- // rely on file watches.
- RefreshInterval: model.Duration(1 * time.Hour),
- },
+ conf := &SDConfig{
+ Files: files,
+ // Setting a high refresh interval to make sure that the tests only
+ // rely on file watches.
+ RefreshInterval: model.Duration(1 * time.Hour),
+ }
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := conf.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ d, err := NewDiscovery(
+ conf,
nil,
- ).Run(ctx, t.ch)
+ metrics,
+ )
+ require.NoError(t, err)
+
+ d.Run(ctx, t.ch)
+
+ metrics.Unregister()
}()
}
@@ -188,11 +203,11 @@ func (t *testRunner) targets() []*targetgroup.Group {
func (t *testRunner) requireUpdate(ref time.Time, expected []*targetgroup.Group) {
t.Helper()
+ timeout := time.After(defaultWait)
for {
select {
- case <-time.After(defaultWait):
+ case <-timeout:
t.Fatalf("Expected update but got none")
- return
case <-time.After(defaultWait / 10):
if ref.Equal(t.lastReceive()) {
// No update received.
@@ -342,9 +357,7 @@ func TestInvalidFile(t *testing.T) {
// Verify that we've received nothing.
time.Sleep(defaultWait)
- if runner.lastReceive().After(now) {
- t.Fatalf("unexpected targets received: %v", runner.targets())
- }
+ require.False(t, runner.lastReceive().After(now), "unexpected targets received: %v", runner.targets())
})
}
}
diff --git a/discovery/file/metrics.go b/discovery/file/metrics.go
new file mode 100644
index 0000000000..c01501e4ef
--- /dev/null
+++ b/discovery/file/metrics.go
@@ -0,0 +1,76 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package file
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*fileMetrics)(nil)
+
+type fileMetrics struct {
+ fileSDReadErrorsCount prometheus.Counter
+ fileSDScanDuration prometheus.Summary
+ fileWatcherErrorsCount prometheus.Counter
+ fileSDTimeStamp *TimestampCollector
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ fm := &fileMetrics{
+ fileSDReadErrorsCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_file_read_errors_total",
+ Help: "The number of File-SD read errors.",
+ }),
+ fileSDScanDuration: prometheus.NewSummary(
+ prometheus.SummaryOpts{
+ Name: "prometheus_sd_file_scan_duration_seconds",
+ Help: "The duration of the File-SD scan in seconds.",
+ Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
+ }),
+ fileWatcherErrorsCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_file_watcher_errors_total",
+ Help: "The number of File-SD errors caused by filesystem watch failures.",
+ }),
+ fileSDTimeStamp: NewTimestampCollector(),
+ }
+
+ fm.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ fm.fileSDReadErrorsCount,
+ fm.fileSDScanDuration,
+ fm.fileWatcherErrorsCount,
+ fm.fileSDTimeStamp,
+ })
+
+ return fm
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (fm *fileMetrics) Register() error {
+ return fm.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (fm *fileMetrics) Unregister() {
+ fm.metricRegisterer.UnregisterMetrics()
+}
+
+func (fm *fileMetrics) init(disc *Discovery) {
+ fm.fileSDTimeStamp.addDiscoverer(disc)
+}
diff --git a/discovery/gce/gce.go b/discovery/gce/gce.go
index fa05fbbf38..15f32dd247 100644
--- a/discovery/gce/gce.go
+++ b/discovery/gce/gce.go
@@ -23,6 +23,7 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"golang.org/x/oauth2/google"
"google.golang.org/api/compute/v1"
@@ -81,12 +82,19 @@ type SDConfig struct {
TagSeparator string `yaml:"tag_separator,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &gceMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "gce" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(*c, opts.Logger)
+ return NewDiscovery(*c, opts.Logger, opts.Metrics)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -121,7 +129,12 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*gceMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
d := &Discovery{
project: conf.Project,
zone: conf.Zone,
@@ -141,10 +154,13 @@ func NewDiscovery(conf SDConfig, logger log.Logger) (*Discovery, error) {
d.isvc = compute.NewInstancesService(d.svc)
d.Discovery = refresh.NewDiscovery(
- logger,
- "gce",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "gce",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
diff --git a/discovery/gce/metrics.go b/discovery/gce/metrics.go
new file mode 100644
index 0000000000..f53ea5a8c6
--- /dev/null
+++ b/discovery/gce/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gce
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*gceMetrics)(nil)
+
+type gceMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *gceMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *gceMetrics) Unregister() {}
diff --git a/discovery/hetzner/hcloud.go b/discovery/hetzner/hcloud.go
index 6d0599dfa2..df56f94c5f 100644
--- a/discovery/hetzner/hcloud.go
+++ b/discovery/hetzner/hcloud.go
@@ -15,7 +15,6 @@ package hetzner
import (
"context"
- "fmt"
"net"
"net/http"
"strconv"
@@ -92,7 +91,7 @@ func (d *hcloudDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
for i, server := range servers {
labels := model.LabelSet{
hetznerLabelRole: model.LabelValue(HetznerRoleHcloud),
- hetznerLabelServerID: model.LabelValue(fmt.Sprintf("%d", server.ID)),
+ hetznerLabelServerID: model.LabelValue(strconv.FormatInt(server.ID, 10)),
hetznerLabelServerName: model.LabelValue(server.Name),
hetznerLabelDatacenter: model.LabelValue(server.Datacenter.Name),
hetznerLabelPublicIPv4: model.LabelValue(server.PublicNet.IPv4.IP.String()),
@@ -102,10 +101,10 @@ func (d *hcloudDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
hetznerLabelHcloudDatacenterLocation: model.LabelValue(server.Datacenter.Location.Name),
hetznerLabelHcloudDatacenterLocationNetworkZone: model.LabelValue(server.Datacenter.Location.NetworkZone),
hetznerLabelHcloudType: model.LabelValue(server.ServerType.Name),
- hetznerLabelHcloudCPUCores: model.LabelValue(fmt.Sprintf("%d", server.ServerType.Cores)),
+ hetznerLabelHcloudCPUCores: model.LabelValue(strconv.Itoa(server.ServerType.Cores)),
hetznerLabelHcloudCPUType: model.LabelValue(server.ServerType.CPUType),
- hetznerLabelHcloudMemoryGB: model.LabelValue(fmt.Sprintf("%d", int(server.ServerType.Memory))),
- hetznerLabelHcloudDiskGB: model.LabelValue(fmt.Sprintf("%d", server.ServerType.Disk)),
+ hetznerLabelHcloudMemoryGB: model.LabelValue(strconv.Itoa(int(server.ServerType.Memory))),
+ hetznerLabelHcloudDiskGB: model.LabelValue(strconv.Itoa(server.ServerType.Disk)),
model.AddressLabel: model.LabelValue(net.JoinHostPort(server.PublicNet.IPv4.IP.String(), strconv.FormatUint(uint64(d.port), 10))),
}
diff --git a/discovery/hetzner/hcloud_test.go b/discovery/hetzner/hcloud_test.go
index a4f19cfddd..10b799037a 100644
--- a/discovery/hetzner/hcloud_test.go
+++ b/discovery/hetzner/hcloud_test.go
@@ -48,12 +48,12 @@ func TestHCloudSDRefresh(t *testing.T) {
targetGroups, err := d.refresh(context.Background())
require.NoError(t, err)
- require.Equal(t, 1, len(targetGroups))
+ require.Len(t, targetGroups, 1)
targetGroup := targetGroups[0]
require.NotNil(t, targetGroup, "targetGroup should not be nil")
require.NotNil(t, targetGroup.Targets, "targetGroup.targets should not be nil")
- require.Equal(t, 3, len(targetGroup.Targets))
+ require.Len(t, targetGroup.Targets, 3)
for i, labelSet := range []model.LabelSet{
{
diff --git a/discovery/hetzner/hetzner.go b/discovery/hetzner/hetzner.go
index c3f7ec39c3..69c823d382 100644
--- a/discovery/hetzner/hetzner.go
+++ b/discovery/hetzner/hetzner.go
@@ -21,6 +21,7 @@ import (
"github.com/go-kit/log"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -62,12 +63,19 @@ type SDConfig struct {
robotEndpoint string // For tests only.
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &hetznerMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "hetzner" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
type refresher interface {
@@ -127,17 +135,25 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
+ m, ok := metrics.(*hetznerMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
r, err := newRefresher(conf, logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
- logger,
- "hetzner",
- time.Duration(conf.RefreshInterval),
- r.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "hetzner",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: r.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
), nil
}
diff --git a/discovery/hetzner/metrics.go b/discovery/hetzner/metrics.go
new file mode 100644
index 0000000000..27361ee755
--- /dev/null
+++ b/discovery/hetzner/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hetzner
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*hetznerMetrics)(nil)
+
+type hetznerMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *hetznerMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *hetznerMetrics) Unregister() {}
diff --git a/discovery/hetzner/mock_test.go b/discovery/hetzner/mock_test.go
index ecf3132742..d192a4eae9 100644
--- a/discovery/hetzner/mock_test.go
+++ b/discovery/hetzner/mock_test.go
@@ -20,7 +20,7 @@ import (
"testing"
)
-// SDMock is the interface for the Hetzner Cloud mock
+// SDMock is the interface for the Hetzner Cloud mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
@@ -34,19 +34,19 @@ func NewSDMock(t *testing.T) *SDMock {
}
}
-// Endpoint returns the URI to the mock server
+// Endpoint returns the URI to the mock server.
func (m *SDMock) Endpoint() string {
return m.Server.URL + "/"
}
-// Setup creates the mock server
+// Setup creates the mock server.
func (m *SDMock) Setup() {
m.Mux = http.NewServeMux()
m.Server = httptest.NewServer(m.Mux)
m.t.Cleanup(m.Server.Close)
}
-// ShutdownServer creates the mock server
+// ShutdownServer creates the mock server.
func (m *SDMock) ShutdownServer() {
m.Server.Close()
}
diff --git a/discovery/hetzner/robot.go b/discovery/hetzner/robot.go
index 1d8aa9302f..516470b05a 100644
--- a/discovery/hetzner/robot.go
+++ b/discovery/hetzner/robot.go
@@ -70,7 +70,7 @@ func newRobotDiscovery(conf *SDConfig, _ log.Logger) (*robotDiscovery, error) {
}
func (d *robotDiscovery) refresh(context.Context) ([]*targetgroup.Group, error) {
- req, err := http.NewRequest("GET", d.endpoint+"/server", nil)
+ req, err := http.NewRequest(http.MethodGet, d.endpoint+"/server", nil)
if err != nil {
return nil, err
}
@@ -112,7 +112,7 @@ func (d *robotDiscovery) refresh(context.Context) ([]*targetgroup.Group, error)
hetznerLabelPublicIPv4: model.LabelValue(server.Server.ServerIP),
hetznerLabelServerStatus: model.LabelValue(server.Server.Status),
hetznerLabelRobotProduct: model.LabelValue(server.Server.Product),
- hetznerLabelRobotCancelled: model.LabelValue(fmt.Sprintf("%t", server.Server.Canceled)),
+ hetznerLabelRobotCancelled: model.LabelValue(strconv.FormatBool(server.Server.Canceled)),
model.AddressLabel: model.LabelValue(net.JoinHostPort(server.Server.ServerIP, strconv.FormatUint(uint64(d.port), 10))),
}
@@ -122,7 +122,6 @@ func (d *robotDiscovery) refresh(context.Context) ([]*targetgroup.Group, error)
labels[hetznerLabelPublicIPv6Network] = model.LabelValue(fmt.Sprintf("%s/%s", subnet.IP, subnet.Mask))
break
}
-
}
targets[i] = labels
}
diff --git a/discovery/hetzner/robot_test.go b/discovery/hetzner/robot_test.go
index f78a0bbda1..abee5fea90 100644
--- a/discovery/hetzner/robot_test.go
+++ b/discovery/hetzner/robot_test.go
@@ -47,12 +47,12 @@ func TestRobotSDRefresh(t *testing.T) {
targetGroups, err := d.refresh(context.Background())
require.NoError(t, err)
- require.Equal(t, 1, len(targetGroups))
+ require.Len(t, targetGroups, 1)
targetGroup := targetGroups[0]
require.NotNil(t, targetGroup, "targetGroup should not be nil")
require.NotNil(t, targetGroup.Targets, "targetGroup.targets should not be nil")
- require.Equal(t, 2, len(targetGroup.Targets))
+ require.Len(t, targetGroup.Targets, 2)
for i, labelSet := range []model.LabelSet{
{
@@ -98,5 +98,5 @@ func TestRobotSDRefreshHandleError(t *testing.T) {
require.Error(t, err)
require.Equal(t, "non 2xx status '401' response during hetzner service discovery with role robot", err.Error())
- require.Equal(t, 0, len(targetGroups))
+ require.Empty(t, targetGroups)
}
diff --git a/discovery/http/http.go b/discovery/http/http.go
index ec958c6148..ff76fd7627 100644
--- a/discovery/http/http.go
+++ b/discovery/http/http.go
@@ -45,17 +45,10 @@ var (
}
userAgent = fmt.Sprintf("Prometheus/%s", version.Version)
matchContentType = regexp.MustCompile(`^(?i:application\/json(;\s*charset=("utf-8"|utf-8))?)$`)
-
- failuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Name: "prometheus_sd_http_failures_total",
- Help: "Number of HTTP service discovery refresh failures.",
- })
)
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(failuresCount)
}
// SDConfig is the configuration for HTTP based discovery.
@@ -65,12 +58,17 @@ type SDConfig struct {
URL string `yaml:"url"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "http" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger, opts.HTTPClientOptions)
+ return NewDiscovery(c, opts.Logger, opts.HTTPClientOptions, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -99,7 +97,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
if parsedURL.Host == "" {
return fmt.Errorf("host is missing in URL")
}
- return nil
+ return c.HTTPClientConfig.Validate()
}
const httpSDURLLabel = model.MetaLabelPrefix + "url"
@@ -112,10 +110,16 @@ type Discovery struct {
client *http.Client
refreshInterval time.Duration
tgLastLength int
+ metrics *httpMetrics
}
// NewDiscovery returns a new HTTP discovery for the given config.
-func NewDiscovery(conf *SDConfig, logger log.Logger, clientOpts []config.HTTPClientOption) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, clientOpts []config.HTTPClientOption, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*httpMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
@@ -130,19 +134,23 @@ func NewDiscovery(conf *SDConfig, logger log.Logger, clientOpts []config.HTTPCli
url: conf.URL,
client: client,
refreshInterval: time.Duration(conf.RefreshInterval), // Stored to be sent as headers.
+ metrics: m,
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "http",
- time.Duration(conf.RefreshInterval),
- d.Refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "http",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.Refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
func (d *Discovery) Refresh(ctx context.Context) ([]*targetgroup.Group, error) {
- req, err := http.NewRequest("GET", d.url, nil)
+ req, err := http.NewRequest(http.MethodGet, d.url, nil)
if err != nil {
return nil, err
}
@@ -152,7 +160,7 @@ func (d *Discovery) Refresh(ctx context.Context) ([]*targetgroup.Group, error) {
resp, err := d.client.Do(req.WithContext(ctx))
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, err
}
defer func() {
@@ -161,31 +169,31 @@ func (d *Discovery) Refresh(ctx context.Context) ([]*targetgroup.Group, error) {
}()
if resp.StatusCode != http.StatusOK {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("server returned HTTP status %s", resp.Status)
}
if !matchContentType.MatchString(strings.TrimSpace(resp.Header.Get("Content-Type"))) {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("unsupported content type %q", resp.Header.Get("Content-Type"))
}
b, err := io.ReadAll(resp.Body)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, err
}
var targetGroups []*targetgroup.Group
if err := json.Unmarshal(b, &targetGroups); err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, err
}
for i, tg := range targetGroups {
if tg == nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
err = errors.New("nil target group item found")
return nil, err
}
diff --git a/discovery/http/http_test.go b/discovery/http/http_test.go
index a284e7f361..0cafe035dc 100644
--- a/discovery/http/http_test.go
+++ b/discovery/http/http_test.go
@@ -28,6 +28,7 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -41,7 +42,14 @@ func TestHTTPValidRefresh(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ defer refreshMetrics.Unregister()
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil, metrics)
require.NoError(t, err)
ctx := context.Background()
@@ -62,8 +70,8 @@ func TestHTTPValidRefresh(t *testing.T) {
Source: urlSource(ts.URL+"/http_sd.good.json", 0),
},
}
- require.Equal(t, tgs, expectedTargets)
- require.Equal(t, 0.0, getFailureCount())
+ require.Equal(t, expectedTargets, tgs)
+ require.Equal(t, 0.0, getFailureCount(d.metrics.failuresCount))
}
func TestHTTPInvalidCode(t *testing.T) {
@@ -79,13 +87,20 @@ func TestHTTPInvalidCode(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ defer refreshMetrics.Unregister()
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil, metrics)
require.NoError(t, err)
ctx := context.Background()
_, err = d.Refresh(ctx)
require.EqualError(t, err, "server returned HTTP status 400 Bad Request")
- require.Equal(t, 1.0, getFailureCount())
+ require.Equal(t, 1.0, getFailureCount(d.metrics.failuresCount))
}
func TestHTTPInvalidFormat(t *testing.T) {
@@ -101,18 +116,23 @@ func TestHTTPInvalidFormat(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ defer refreshMetrics.Unregister()
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil, metrics)
require.NoError(t, err)
ctx := context.Background()
_, err = d.Refresh(ctx)
require.EqualError(t, err, `unsupported content type "text/plain; charset=utf-8"`)
- require.Equal(t, 1.0, getFailureCount())
+ require.Equal(t, 1.0, getFailureCount(d.metrics.failuresCount))
}
-var lastFailureCount float64
-
-func getFailureCount() float64 {
+func getFailureCount(failuresCount prometheus.Counter) float64 {
failureChan := make(chan prometheus.Metric)
go func() {
@@ -129,10 +149,7 @@ func getFailureCount() float64 {
metric.Write(&counter)
}
- // account for failures in prior tests
- count := *counter.Counter.Value - lastFailureCount
- lastFailureCount = *counter.Counter.Value
- return count
+ return *counter.Counter.Value
}
func TestContentTypeRegex(t *testing.T) {
@@ -417,7 +434,15 @@ func TestSourceDisappeared(t *testing.T) {
URL: ts.URL,
RefreshInterval: model.Duration(1 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil)
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ defer refreshMetrics.Unregister()
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), nil, metrics)
require.NoError(t, err)
for _, test := range cases {
ctx := context.Background()
diff --git a/discovery/http/metrics.go b/discovery/http/metrics.go
new file mode 100644
index 0000000000..b1f8b84433
--- /dev/null
+++ b/discovery/http/metrics.go
@@ -0,0 +1,57 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package http
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*httpMetrics)(nil)
+
+type httpMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+
+ failuresCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &httpMetrics{
+ refreshMetrics: rmi,
+ failuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_http_failures_total",
+ Help: "Number of HTTP service discovery refresh failures.",
+ }),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.failuresCount,
+ })
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *httpMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *httpMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/ionos/ionos.go b/discovery/ionos/ionos.go
index a13a000585..c8b4f7f8e5 100644
--- a/discovery/ionos/ionos.go
+++ b/discovery/ionos/ionos.go
@@ -14,10 +14,12 @@
package ionos
import (
+ "errors"
+ "fmt"
"time"
"github.com/go-kit/log"
- "github.com/pkg/errors"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -41,7 +43,12 @@ func init() {
type Discovery struct{}
// NewDiscovery returns a new refresh.Discovery for IONOS Cloud.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
+ m, ok := metrics.(*ionosMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if conf.ionosEndpoint == "" {
conf.ionosEndpoint = "https://api.ionos.com"
}
@@ -52,10 +59,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error)
}
return refresh.NewDiscovery(
- logger,
- "ionos",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "ionos",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
), nil
}
@@ -79,6 +89,13 @@ type SDConfig struct {
ionosEndpoint string // For tests only.
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &ionosMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the IONOS Cloud service discovery.
func (c SDConfig) Name() string {
return "ionos"
@@ -86,7 +103,7 @@ func (c SDConfig) Name() string {
// NewDiscoverer returns a new discovery.Discoverer for IONOS Cloud.
func (c SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(&c, options.Logger)
+ return NewDiscovery(&c, options.Logger, options.Metrics)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
diff --git a/discovery/ionos/metrics.go b/discovery/ionos/metrics.go
new file mode 100644
index 0000000000..88e465acf3
--- /dev/null
+++ b/discovery/ionos/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ionos
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*ionosMetrics)(nil)
+
+type ionosMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *ionosMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *ionosMetrics) Unregister() {}
diff --git a/discovery/ionos/server_test.go b/discovery/ionos/server_test.go
index 92f2a96f9d..30f358e325 100644
--- a/discovery/ionos/server_test.go
+++ b/discovery/ionos/server_test.go
@@ -48,12 +48,12 @@ func TestIONOSServerRefresh(t *testing.T) {
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 2, len(tg.Targets))
+ require.Len(t, tg.Targets, 2)
for i, lbls := range []model.LabelSet{
{
diff --git a/discovery/kubernetes/endpoints.go b/discovery/kubernetes/endpoints.go
index 7200d52dde..542bc95edc 100644
--- a/discovery/kubernetes/endpoints.go
+++ b/discovery/kubernetes/endpoints.go
@@ -11,7 +11,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-// nolint:revive // Many legitimately empty blocks in this file.
package kubernetes
import (
@@ -23,6 +22,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
apiv1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
@@ -31,12 +31,6 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-var (
- epAddCount = eventCount.WithLabelValues("endpoints", "add")
- epUpdateCount = eventCount.WithLabelValues("endpoints", "update")
- epDeleteCount = eventCount.WithLabelValues("endpoints", "delete")
-)
-
// Endpoints discovers new endpoint targets.
type Endpoints struct {
logger log.Logger
@@ -55,10 +49,21 @@ type Endpoints struct {
}
// NewEndpoints returns a new endpoints discovery.
-func NewEndpoints(l log.Logger, eps cache.SharedIndexInformer, svc, pod, node cache.SharedInformer) *Endpoints {
+func NewEndpoints(l log.Logger, eps cache.SharedIndexInformer, svc, pod, node cache.SharedInformer, eventCount *prometheus.CounterVec) *Endpoints {
if l == nil {
l = log.NewNopLogger()
}
+
+ epAddCount := eventCount.WithLabelValues(RoleEndpoint.String(), MetricLabelRoleAdd)
+ epUpdateCount := eventCount.WithLabelValues(RoleEndpoint.String(), MetricLabelRoleUpdate)
+ epDeleteCount := eventCount.WithLabelValues(RoleEndpoint.String(), MetricLabelRoleDelete)
+
+ svcAddCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleAdd)
+ svcUpdateCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleUpdate)
+ svcDeleteCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleDelete)
+
+ podUpdateCount := eventCount.WithLabelValues(RolePod.String(), MetricLabelRoleUpdate)
+
e := &Endpoints{
logger: l,
endpointsInf: eps,
@@ -69,7 +74,7 @@ func NewEndpoints(l log.Logger, eps cache.SharedIndexInformer, svc, pod, node ca
podStore: pod.GetStore(),
nodeInf: node,
withNodeMetadata: node != nil,
- queue: workqueue.NewNamed("endpoints"),
+ queue: workqueue.NewNamed(RoleEndpoint.String()),
}
_, err := e.endpointsInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -128,6 +133,29 @@ func NewEndpoints(l log.Logger, eps cache.SharedIndexInformer, svc, pod, node ca
if err != nil {
level.Error(l).Log("msg", "Error adding services event handler.", "err", err)
}
+ _, err = e.podInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ UpdateFunc: func(old, cur interface{}) {
+ podUpdateCount.Inc()
+ oldPod, ok := old.(*apiv1.Pod)
+ if !ok {
+ return
+ }
+
+ curPod, ok := cur.(*apiv1.Pod)
+ if !ok {
+ return
+ }
+
+ // the Pod's phase may change without triggering an update on the Endpoints/Service.
+ // https://github.com/prometheus/prometheus/issues/11305.
+ if curPod.Status.Phase != oldPod.Status.Phase {
+ e.enqueuePod(namespacedName(curPod.Namespace, curPod.Name))
+ }
+ },
+ })
+ if err != nil {
+ level.Error(l).Log("msg", "Error adding pods event handler.", "err", err)
+ }
if e.withNodeMetadata {
_, err = e.nodeInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(o interface{}) {
@@ -163,6 +191,18 @@ func (e *Endpoints) enqueueNode(nodeName string) {
}
}
+func (e *Endpoints) enqueuePod(podNamespacedName string) {
+ endpoints, err := e.endpointsInf.GetIndexer().ByIndex(podIndex, podNamespacedName)
+ if err != nil {
+ level.Error(e.logger).Log("msg", "Error getting endpoints for pod", "pod", podNamespacedName, "err", err)
+ return
+ }
+
+ for _, endpoint := range endpoints {
+ e.enqueue(endpoint)
+ }
+}
+
func (e *Endpoints) enqueue(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
@@ -309,7 +349,7 @@ func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
tg.Targets = append(tg.Targets, target)
return
}
- s := pod.Namespace + "/" + pod.Name
+ s := namespacedName(pod.Namespace, pod.Name)
sp, ok := seenPods[s]
if !ok {
@@ -321,16 +361,19 @@ func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
target = target.Merge(podLabels(pod))
// Attach potential container port labels matching the endpoint port.
- for _, c := range pod.Spec.Containers {
+ containers := append(pod.Spec.Containers, pod.Spec.InitContainers...)
+ for i, c := range containers {
for _, cport := range c.Ports {
if port.Port == cport.ContainerPort {
ports := strconv.FormatUint(uint64(port.Port), 10)
+ isInit := i >= len(pod.Spec.Containers)
target[podContainerNameLabel] = lv(c.Name)
target[podContainerImageLabel] = lv(c.Image)
target[podContainerPortNameLabel] = lv(cport.Name)
target[podContainerPortNumberLabel] = lv(ports)
target[podContainerPortProtocolLabel] = lv(string(port.Protocol))
+ target[podContainerIsInit] = lv(strconv.FormatBool(isInit))
break
}
}
@@ -366,7 +409,13 @@ func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
// For all seen pods, check all container ports. If they were not covered
// by one of the service endpoints, generate targets for them.
for _, pe := range seenPods {
- for _, c := range pe.pod.Spec.Containers {
+ // PodIP can be empty when a pod is starting or has been evicted.
+ if len(pe.pod.Status.PodIP) == 0 {
+ continue
+ }
+
+ containers := append(pe.pod.Spec.Containers, pe.pod.Spec.InitContainers...)
+ for i, c := range containers {
for _, cport := range c.Ports {
hasSeenPort := func() bool {
for _, eport := range pe.servicePorts {
@@ -380,21 +429,20 @@ func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
continue
}
- // PodIP can be empty when a pod is starting or has been evicted.
- if len(pe.pod.Status.PodIP) != 0 {
- a := net.JoinHostPort(pe.pod.Status.PodIP, strconv.FormatUint(uint64(cport.ContainerPort), 10))
- ports := strconv.FormatUint(uint64(cport.ContainerPort), 10)
-
- target := model.LabelSet{
- model.AddressLabel: lv(a),
- podContainerNameLabel: lv(c.Name),
- podContainerImageLabel: lv(c.Image),
- podContainerPortNameLabel: lv(cport.Name),
- podContainerPortNumberLabel: lv(ports),
- podContainerPortProtocolLabel: lv(string(cport.Protocol)),
- }
- tg.Targets = append(tg.Targets, target.Merge(podLabels(pe.pod)))
+ a := net.JoinHostPort(pe.pod.Status.PodIP, strconv.FormatUint(uint64(cport.ContainerPort), 10))
+ ports := strconv.FormatUint(uint64(cport.ContainerPort), 10)
+
+ isInit := i >= len(pe.pod.Spec.Containers)
+ target := model.LabelSet{
+ model.AddressLabel: lv(a),
+ podContainerNameLabel: lv(c.Name),
+ podContainerImageLabel: lv(c.Image),
+ podContainerPortNameLabel: lv(cport.Name),
+ podContainerPortNumberLabel: lv(ports),
+ podContainerPortProtocolLabel: lv(string(cport.Protocol)),
+ podContainerIsInit: lv(strconv.FormatBool(isInit)),
}
+ tg.Targets = append(tg.Targets, target.Merge(podLabels(pe.pod)))
}
}
}
diff --git a/discovery/kubernetes/endpoints_test.go b/discovery/kubernetes/endpoints_test.go
index cf7fd9aee0..4af6889602 100644
--- a/discovery/kubernetes/endpoints_test.go
+++ b/discovery/kubernetes/endpoints_test.go
@@ -244,6 +244,7 @@ func TestEndpointsDiscoveryAdd(t *testing.T) {
"__meta_kubernetes_pod_container_port_number": "9000",
"__meta_kubernetes_pod_container_port_protocol": "TCP",
"__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
},
{
"__address__": "1.2.3.4:9001",
@@ -259,6 +260,7 @@ func TestEndpointsDiscoveryAdd(t *testing.T) {
"__meta_kubernetes_pod_container_port_number": "9001",
"__meta_kubernetes_pod_container_port_protocol": "TCP",
"__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
},
},
Labels: model.LabelSet{
@@ -821,6 +823,7 @@ func TestEndpointsDiscoveryNamespaces(t *testing.T) {
"__meta_kubernetes_pod_container_port_number": "9000",
"__meta_kubernetes_pod_container_port_protocol": "TCP",
"__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
},
},
Labels: model.LabelSet{
@@ -969,3 +972,288 @@ func TestEndpointsDiscoveryEmptyPodStatus(t *testing.T) {
expectedRes: map[string]*targetgroup.Group{},
}.Run(t)
}
+
+// TestEndpointsDiscoveryUpdatePod makes sure that Endpoints discovery detects underlying Pods changes.
+// See https://github.com/prometheus/prometheus/issues/11305 for more details.
+func TestEndpointsDiscoveryUpdatePod(t *testing.T) {
+ pod := &v1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "testpod",
+ Namespace: "default",
+ UID: types.UID("deadbeef"),
+ },
+ Spec: v1.PodSpec{
+ NodeName: "testnode",
+ Containers: []v1.Container{
+ {
+ Name: "c1",
+ Image: "c1:latest",
+ Ports: []v1.ContainerPort{
+ {
+ Name: "mainport",
+ ContainerPort: 9000,
+ Protocol: v1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ },
+ Status: v1.PodStatus{
+ // Pod is in Pending phase when discovered for first time.
+ Phase: "Pending",
+ Conditions: []v1.PodCondition{
+ {
+ Type: v1.PodReady,
+ Status: v1.ConditionFalse,
+ },
+ },
+ HostIP: "2.3.4.5",
+ PodIP: "4.3.2.1",
+ },
+ }
+ objs := []runtime.Object{
+ &v1.Endpoints{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "testendpoints",
+ Namespace: "default",
+ },
+ Subsets: []v1.EndpointSubset{
+ {
+ Addresses: []v1.EndpointAddress{
+ {
+ IP: "4.3.2.1",
+ // The Pending Pod may be included because the Endpoints was created manually.
+ // Or because the corresponding service has ".spec.publishNotReadyAddresses: true".
+ TargetRef: &v1.ObjectReference{
+ Kind: "Pod",
+ Name: "testpod",
+ Namespace: "default",
+ },
+ },
+ },
+ Ports: []v1.EndpointPort{
+ {
+ Name: "mainport",
+ Port: 9000,
+ Protocol: v1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ },
+ pod,
+ }
+ n, c := makeDiscovery(RoleEndpoint, NamespaceDiscovery{}, objs...)
+
+ k8sDiscoveryTest{
+ discovery: n,
+ afterStart: func() {
+ // the Pod becomes Ready.
+ pod.Status.Phase = "Running"
+ pod.Status.Conditions = []v1.PodCondition{
+ {
+ Type: v1.PodReady,
+ Status: v1.ConditionTrue,
+ },
+ }
+ c.CoreV1().Pods(pod.Namespace).Update(context.Background(), pod, metav1.UpdateOptions{})
+ },
+ expectedMaxItems: 2,
+ expectedRes: map[string]*targetgroup.Group{
+ "endpoints/default/testendpoints": {
+ Targets: []model.LabelSet{
+ {
+ "__address__": "4.3.2.1:9000",
+ "__meta_kubernetes_endpoint_port_name": "mainport",
+ "__meta_kubernetes_endpoint_port_protocol": "TCP",
+ "__meta_kubernetes_endpoint_ready": "true",
+ "__meta_kubernetes_endpoint_address_target_kind": "Pod",
+ "__meta_kubernetes_endpoint_address_target_name": "testpod",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_ready": "true",
+ "__meta_kubernetes_pod_phase": "Running",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_container_name": "c1",
+ "__meta_kubernetes_pod_container_image": "c1:latest",
+ "__meta_kubernetes_pod_container_port_name": "mainport",
+ "__meta_kubernetes_pod_container_port_number": "9000",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
+ },
+ },
+ Labels: model.LabelSet{
+ "__meta_kubernetes_namespace": "default",
+ "__meta_kubernetes_endpoints_name": "testendpoints",
+ },
+ Source: "endpoints/default/testendpoints",
+ },
+ },
+ }.Run(t)
+}
+
+func TestEndpointsDiscoverySidecarContainer(t *testing.T) {
+ objs := []runtime.Object{
+ &v1.Endpoints{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "testsidecar",
+ Namespace: "default",
+ },
+ Subsets: []v1.EndpointSubset{
+ {
+ Addresses: []v1.EndpointAddress{
+ {
+ IP: "4.3.2.1",
+ TargetRef: &v1.ObjectReference{
+ Kind: "Pod",
+ Name: "testpod",
+ Namespace: "default",
+ },
+ },
+ },
+ Ports: []v1.EndpointPort{
+ {
+ Name: "testport",
+ Port: 9000,
+ Protocol: v1.ProtocolTCP,
+ },
+ {
+ Name: "initport",
+ Port: 9111,
+ Protocol: v1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ },
+ &v1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "testpod",
+ Namespace: "default",
+ UID: types.UID("deadbeef"),
+ },
+ Spec: v1.PodSpec{
+ NodeName: "testnode",
+ InitContainers: []v1.Container{
+ {
+ Name: "ic1",
+ Image: "ic1:latest",
+ Ports: []v1.ContainerPort{
+ {
+ Name: "initport",
+ ContainerPort: 1111,
+ Protocol: v1.ProtocolTCP,
+ },
+ },
+ },
+ {
+ Name: "ic2",
+ Image: "ic2:latest",
+ Ports: []v1.ContainerPort{
+ {
+ Name: "initport",
+ ContainerPort: 9111,
+ Protocol: v1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ Containers: []v1.Container{
+ {
+ Name: "c1",
+ Image: "c1:latest",
+ Ports: []v1.ContainerPort{
+ {
+ Name: "mainport",
+ ContainerPort: 9000,
+ Protocol: v1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ },
+ Status: v1.PodStatus{
+ HostIP: "2.3.4.5",
+ PodIP: "4.3.2.1",
+ },
+ },
+ }
+
+ n, _ := makeDiscovery(RoleEndpoint, NamespaceDiscovery{}, objs...)
+
+ k8sDiscoveryTest{
+ discovery: n,
+ expectedMaxItems: 1,
+ expectedRes: map[string]*targetgroup.Group{
+ "endpoints/default/testsidecar": {
+ Targets: []model.LabelSet{
+ {
+ "__address__": "4.3.2.1:9000",
+ "__meta_kubernetes_endpoint_address_target_kind": "Pod",
+ "__meta_kubernetes_endpoint_address_target_name": "testpod",
+ "__meta_kubernetes_endpoint_port_name": "testport",
+ "__meta_kubernetes_endpoint_port_protocol": "TCP",
+ "__meta_kubernetes_endpoint_ready": "true",
+ "__meta_kubernetes_pod_container_image": "c1:latest",
+ "__meta_kubernetes_pod_container_name": "c1",
+ "__meta_kubernetes_pod_container_port_name": "mainport",
+ "__meta_kubernetes_pod_container_port_number": "9000",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_phase": "",
+ "__meta_kubernetes_pod_ready": "unknown",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
+ },
+ {
+ "__address__": "4.3.2.1:9111",
+ "__meta_kubernetes_endpoint_address_target_kind": "Pod",
+ "__meta_kubernetes_endpoint_address_target_name": "testpod",
+ "__meta_kubernetes_endpoint_port_name": "initport",
+ "__meta_kubernetes_endpoint_port_protocol": "TCP",
+ "__meta_kubernetes_endpoint_ready": "true",
+ "__meta_kubernetes_pod_container_image": "ic2:latest",
+ "__meta_kubernetes_pod_container_name": "ic2",
+ "__meta_kubernetes_pod_container_port_name": "initport",
+ "__meta_kubernetes_pod_container_port_number": "9111",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_phase": "",
+ "__meta_kubernetes_pod_ready": "unknown",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "true",
+ },
+ {
+ "__address__": "4.3.2.1:1111",
+ "__meta_kubernetes_pod_container_image": "ic1:latest",
+ "__meta_kubernetes_pod_container_name": "ic1",
+ "__meta_kubernetes_pod_container_port_name": "initport",
+ "__meta_kubernetes_pod_container_port_number": "1111",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_phase": "",
+ "__meta_kubernetes_pod_ready": "unknown",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "true",
+ },
+ },
+ Labels: model.LabelSet{
+ "__meta_kubernetes_endpoints_name": "testsidecar",
+ "__meta_kubernetes_namespace": "default",
+ },
+ Source: "endpoints/default/testsidecar",
+ },
+ },
+ }.Run(t)
+}
diff --git a/discovery/kubernetes/endpointslice.go b/discovery/kubernetes/endpointslice.go
index e241c758bc..1368303104 100644
--- a/discovery/kubernetes/endpointslice.go
+++ b/discovery/kubernetes/endpointslice.go
@@ -15,16 +15,17 @@ package kubernetes
import (
"context"
+ "errors"
"fmt"
"net"
"strconv"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/api/discovery/v1"
- "k8s.io/api/discovery/v1beta1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
@@ -32,12 +33,6 @@ import (
"github.com/prometheus/prometheus/util/strutil"
)
-var (
- epslAddCount = eventCount.WithLabelValues("endpointslice", "add")
- epslUpdateCount = eventCount.WithLabelValues("endpointslice", "update")
- epslDeleteCount = eventCount.WithLabelValues("endpointslice", "delete")
-)
-
// EndpointSlice discovers new endpoint targets.
type EndpointSlice struct {
logger log.Logger
@@ -56,10 +51,19 @@ type EndpointSlice struct {
}
// NewEndpointSlice returns a new endpointslice discovery.
-func NewEndpointSlice(l log.Logger, eps cache.SharedIndexInformer, svc, pod, node cache.SharedInformer) *EndpointSlice {
+func NewEndpointSlice(l log.Logger, eps cache.SharedIndexInformer, svc, pod, node cache.SharedInformer, eventCount *prometheus.CounterVec) *EndpointSlice {
if l == nil {
l = log.NewNopLogger()
}
+
+ epslAddCount := eventCount.WithLabelValues(RoleEndpointSlice.String(), MetricLabelRoleAdd)
+ epslUpdateCount := eventCount.WithLabelValues(RoleEndpointSlice.String(), MetricLabelRoleUpdate)
+ epslDeleteCount := eventCount.WithLabelValues(RoleEndpointSlice.String(), MetricLabelRoleDelete)
+
+ svcAddCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleAdd)
+ svcUpdateCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleUpdate)
+ svcDeleteCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleDelete)
+
e := &EndpointSlice{
logger: l,
endpointSliceInf: eps,
@@ -70,7 +74,7 @@ func NewEndpointSlice(l log.Logger, eps cache.SharedIndexInformer, svc, pod, nod
podStore: pod.GetStore(),
nodeInf: node,
withNodeMetadata: node != nil,
- queue: workqueue.NewNamed("endpointSlice"),
+ queue: workqueue.NewNamed(RoleEndpointSlice.String()),
}
_, err := e.endpointSliceInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -98,9 +102,9 @@ func NewEndpointSlice(l log.Logger, eps cache.SharedIndexInformer, svc, pod, nod
return
}
- // TODO(brancz): use cache.Indexer to index endpoints by
- // disv1beta1.LabelServiceName so this operation doesn't have to
- // iterate over all endpoint objects.
+ // TODO(brancz): use cache.Indexer to index endpointslices by
+ // LabelServiceName so this operation doesn't have to iterate over all
+ // endpoint objects.
for _, obj := range e.endpointSliceStore.List() {
esa, err := e.getEndpointSliceAdaptor(obj)
if err != nil {
@@ -183,14 +187,14 @@ func (e *EndpointSlice) Run(ctx context.Context, ch chan<- []*targetgroup.Group)
cacheSyncs = append(cacheSyncs, e.nodeInf.HasSynced)
}
if !cache.WaitForCacheSync(ctx.Done(), cacheSyncs...) {
- if ctx.Err() != context.Canceled {
+ if !errors.Is(ctx.Err(), context.Canceled) {
level.Error(e.logger).Log("msg", "endpointslice informer unable to sync cache")
}
return
}
go func() {
- for e.process(ctx, ch) { // nolint:revive
+ for e.process(ctx, ch) {
}
}()
@@ -236,8 +240,6 @@ func (e *EndpointSlice) getEndpointSliceAdaptor(o interface{}) (endpointSliceAda
switch endpointSlice := o.(type) {
case *v1.EndpointSlice:
return newEndpointSliceAdaptorFromV1(endpointSlice), nil
- case *v1beta1.EndpointSlice:
- return newEndpointSliceAdaptorFromV1beta1(endpointSlice), nil
default:
return nil, fmt.Errorf("received unexpected object: %v", o)
}
@@ -260,7 +262,9 @@ const (
endpointSliceEndpointConditionsReadyLabel = metaLabelPrefix + "endpointslice_endpoint_conditions_ready"
endpointSliceEndpointConditionsServingLabel = metaLabelPrefix + "endpointslice_endpoint_conditions_serving"
endpointSliceEndpointConditionsTerminatingLabel = metaLabelPrefix + "endpointslice_endpoint_conditions_terminating"
+ endpointSliceEndpointZoneLabel = metaLabelPrefix + "endpointslice_endpoint_zone"
endpointSliceEndpointHostnameLabel = metaLabelPrefix + "endpointslice_endpoint_hostname"
+ endpointSliceEndpointNodenameLabel = metaLabelPrefix + "endpointslice_endpoint_node_name"
endpointSliceAddressTargetKindLabel = metaLabelPrefix + "endpointslice_address_target_kind"
endpointSliceAddressTargetNameLabel = metaLabelPrefix + "endpointslice_address_target_name"
endpointSliceEndpointTopologyLabelPrefix = metaLabelPrefix + "endpointslice_endpoint_topology_"
@@ -333,6 +337,14 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
target[model.LabelName(endpointSliceAddressTargetNameLabel)] = lv(ep.targetRef().Name)
}
+ if ep.nodename() != nil {
+ target[endpointSliceEndpointNodenameLabel] = lv(*ep.nodename())
+ }
+
+ if ep.zone() != nil {
+ target[model.LabelName(endpointSliceEndpointZoneLabel)] = lv(*ep.zone())
+ }
+
for k, v := range ep.topology() {
ln := strutil.SanitizeLabelName(k)
target[model.LabelName(endpointSliceEndpointTopologyLabelPrefix+ln)] = lv(v)
@@ -353,7 +365,7 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
tg.Targets = append(tg.Targets, target)
return
}
- s := pod.Namespace + "/" + pod.Name
+ s := namespacedName(pod.Namespace, pod.Name)
sp, ok := seenPods[s]
if !ok {
@@ -365,19 +377,23 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
target = target.Merge(podLabels(pod))
// Attach potential container port labels matching the endpoint port.
- for _, c := range pod.Spec.Containers {
+ containers := append(pod.Spec.Containers, pod.Spec.InitContainers...)
+ for i, c := range containers {
for _, cport := range c.Ports {
if port.port() == nil {
continue
}
+
if *port.port() == cport.ContainerPort {
ports := strconv.FormatUint(uint64(*port.port()), 10)
+ isInit := i >= len(pod.Spec.Containers)
target[podContainerNameLabel] = lv(c.Name)
target[podContainerImageLabel] = lv(c.Image)
target[podContainerPortNameLabel] = lv(cport.Name)
target[podContainerPortNumberLabel] = lv(ports)
target[podContainerPortProtocolLabel] = lv(string(cport.Protocol))
+ target[podContainerIsInit] = lv(strconv.FormatBool(isInit))
break
}
}
@@ -400,7 +416,13 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
// For all seen pods, check all container ports. If they were not covered
// by one of the service endpoints, generate targets for them.
for _, pe := range seenPods {
- for _, c := range pe.pod.Spec.Containers {
+ // PodIP can be empty when a pod is starting or has been evicted.
+ if len(pe.pod.Status.PodIP) == 0 {
+ continue
+ }
+
+ containers := append(pe.pod.Spec.Containers, pe.pod.Spec.InitContainers...)
+ for i, c := range containers {
for _, cport := range c.Ports {
hasSeenPort := func() bool {
for _, eport := range pe.servicePorts {
@@ -417,21 +439,20 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
continue
}
- // PodIP can be empty when a pod is starting or has been evicted.
- if len(pe.pod.Status.PodIP) != 0 {
- a := net.JoinHostPort(pe.pod.Status.PodIP, strconv.FormatUint(uint64(cport.ContainerPort), 10))
- ports := strconv.FormatUint(uint64(cport.ContainerPort), 10)
-
- target := model.LabelSet{
- model.AddressLabel: lv(a),
- podContainerNameLabel: lv(c.Name),
- podContainerImageLabel: lv(c.Image),
- podContainerPortNameLabel: lv(cport.Name),
- podContainerPortNumberLabel: lv(ports),
- podContainerPortProtocolLabel: lv(string(cport.Protocol)),
- }
- tg.Targets = append(tg.Targets, target.Merge(podLabels(pe.pod)))
+ a := net.JoinHostPort(pe.pod.Status.PodIP, strconv.FormatUint(uint64(cport.ContainerPort), 10))
+ ports := strconv.FormatUint(uint64(cport.ContainerPort), 10)
+
+ isInit := i >= len(pe.pod.Spec.Containers)
+ target := model.LabelSet{
+ model.AddressLabel: lv(a),
+ podContainerNameLabel: lv(c.Name),
+ podContainerImageLabel: lv(c.Image),
+ podContainerPortNameLabel: lv(cport.Name),
+ podContainerPortNumberLabel: lv(ports),
+ podContainerPortProtocolLabel: lv(string(cport.Protocol)),
+ podContainerIsInit: lv(strconv.FormatBool(isInit)),
}
+ tg.Targets = append(tg.Targets, target.Merge(podLabels(pe.pod)))
}
}
}
diff --git a/discovery/kubernetes/endpointslice_adaptor.go b/discovery/kubernetes/endpointslice_adaptor.go
index 46fa708c10..81243e2ce0 100644
--- a/discovery/kubernetes/endpointslice_adaptor.go
+++ b/discovery/kubernetes/endpointslice_adaptor.go
@@ -16,11 +16,10 @@ package kubernetes
import (
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/discovery/v1"
- "k8s.io/api/discovery/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
-// endpointSliceAdaptor is an adaptor for the different EndpointSlice versions
+// endpointSliceAdaptor is an adaptor for the different EndpointSlice versions.
type endpointSliceAdaptor interface {
get() interface{}
getObjectMeta() metav1.ObjectMeta
@@ -44,6 +43,7 @@ type endpointSliceEndpointAdaptor interface {
addresses() []string
hostname() *string
nodename() *string
+ zone() *string
conditions() endpointSliceEndpointConditionsAdaptor
targetRef() *corev1.ObjectReference
topology() map[string]string
@@ -55,7 +55,7 @@ type endpointSliceEndpointConditionsAdaptor interface {
terminating() *bool
}
-// Adaptor for k8s.io/api/discovery/v1
+// Adaptor for k8s.io/api/discovery/v1.
type endpointSliceAdaptorV1 struct {
endpointSlice *v1.EndpointSlice
}
@@ -108,59 +108,6 @@ func (e *endpointSliceAdaptorV1) labelServiceName() string {
return v1.LabelServiceName
}
-// Adaptor for k8s.io/api/discovery/v1beta1
-type endpointSliceAdaptorV1Beta1 struct {
- endpointSlice *v1beta1.EndpointSlice
-}
-
-func newEndpointSliceAdaptorFromV1beta1(endpointSlice *v1beta1.EndpointSlice) endpointSliceAdaptor {
- return &endpointSliceAdaptorV1Beta1{endpointSlice: endpointSlice}
-}
-
-func (e *endpointSliceAdaptorV1Beta1) get() interface{} {
- return e.endpointSlice
-}
-
-func (e *endpointSliceAdaptorV1Beta1) getObjectMeta() metav1.ObjectMeta {
- return e.endpointSlice.ObjectMeta
-}
-
-func (e *endpointSliceAdaptorV1Beta1) name() string {
- return e.endpointSlice.Name
-}
-
-func (e *endpointSliceAdaptorV1Beta1) namespace() string {
- return e.endpointSlice.Namespace
-}
-
-func (e *endpointSliceAdaptorV1Beta1) addressType() string {
- return string(e.endpointSlice.AddressType)
-}
-
-func (e *endpointSliceAdaptorV1Beta1) endpoints() []endpointSliceEndpointAdaptor {
- eps := make([]endpointSliceEndpointAdaptor, 0, len(e.endpointSlice.Endpoints))
- for i := 0; i < len(e.endpointSlice.Endpoints); i++ {
- eps = append(eps, newEndpointSliceEndpointAdaptorFromV1beta1(e.endpointSlice.Endpoints[i]))
- }
- return eps
-}
-
-func (e *endpointSliceAdaptorV1Beta1) ports() []endpointSlicePortAdaptor {
- ports := make([]endpointSlicePortAdaptor, 0, len(e.endpointSlice.Ports))
- for i := 0; i < len(e.endpointSlice.Ports); i++ {
- ports = append(ports, newEndpointSlicePortAdaptorFromV1beta1(e.endpointSlice.Ports[i]))
- }
- return ports
-}
-
-func (e *endpointSliceAdaptorV1Beta1) labels() map[string]string {
- return e.endpointSlice.Labels
-}
-
-func (e *endpointSliceAdaptorV1Beta1) labelServiceName() string {
- return v1beta1.LabelServiceName
-}
-
type endpointSliceEndpointAdaptorV1 struct {
endpoint v1.Endpoint
}
@@ -181,6 +128,10 @@ func (e *endpointSliceEndpointAdaptorV1) nodename() *string {
return e.endpoint.NodeName
}
+func (e *endpointSliceEndpointAdaptorV1) zone() *string {
+ return e.endpoint.Zone
+}
+
func (e *endpointSliceEndpointAdaptorV1) conditions() endpointSliceEndpointConditionsAdaptor {
return newEndpointSliceEndpointConditionsAdaptorFromV1(e.endpoint.Conditions)
}
@@ -213,58 +164,6 @@ func (e *endpointSliceEndpointConditionsAdaptorV1) terminating() *bool {
return e.endpointConditions.Terminating
}
-type endpointSliceEndpointAdaptorV1beta1 struct {
- endpoint v1beta1.Endpoint
-}
-
-func newEndpointSliceEndpointAdaptorFromV1beta1(endpoint v1beta1.Endpoint) endpointSliceEndpointAdaptor {
- return &endpointSliceEndpointAdaptorV1beta1{endpoint: endpoint}
-}
-
-func (e *endpointSliceEndpointAdaptorV1beta1) addresses() []string {
- return e.endpoint.Addresses
-}
-
-func (e *endpointSliceEndpointAdaptorV1beta1) hostname() *string {
- return e.endpoint.Hostname
-}
-
-func (e *endpointSliceEndpointAdaptorV1beta1) nodename() *string {
- return e.endpoint.NodeName
-}
-
-func (e *endpointSliceEndpointAdaptorV1beta1) conditions() endpointSliceEndpointConditionsAdaptor {
- return newEndpointSliceEndpointConditionsAdaptorFromV1beta1(e.endpoint.Conditions)
-}
-
-func (e *endpointSliceEndpointAdaptorV1beta1) targetRef() *corev1.ObjectReference {
- return e.endpoint.TargetRef
-}
-
-func (e *endpointSliceEndpointAdaptorV1beta1) topology() map[string]string {
- return e.endpoint.Topology
-}
-
-type endpointSliceEndpointConditionsAdaptorV1beta1 struct {
- endpointConditions v1beta1.EndpointConditions
-}
-
-func newEndpointSliceEndpointConditionsAdaptorFromV1beta1(endpointConditions v1beta1.EndpointConditions) endpointSliceEndpointConditionsAdaptor {
- return &endpointSliceEndpointConditionsAdaptorV1beta1{endpointConditions: endpointConditions}
-}
-
-func (e *endpointSliceEndpointConditionsAdaptorV1beta1) ready() *bool {
- return e.endpointConditions.Ready
-}
-
-func (e *endpointSliceEndpointConditionsAdaptorV1beta1) serving() *bool {
- return e.endpointConditions.Serving
-}
-
-func (e *endpointSliceEndpointConditionsAdaptorV1beta1) terminating() *bool {
- return e.endpointConditions.Terminating
-}
-
type endpointSlicePortAdaptorV1 struct {
endpointPort v1.EndpointPort
}
@@ -289,28 +188,3 @@ func (e *endpointSlicePortAdaptorV1) protocol() *string {
func (e *endpointSlicePortAdaptorV1) appProtocol() *string {
return e.endpointPort.AppProtocol
}
-
-type endpointSlicePortAdaptorV1beta1 struct {
- endpointPort v1beta1.EndpointPort
-}
-
-func newEndpointSlicePortAdaptorFromV1beta1(port v1beta1.EndpointPort) endpointSlicePortAdaptor {
- return &endpointSlicePortAdaptorV1beta1{endpointPort: port}
-}
-
-func (e *endpointSlicePortAdaptorV1beta1) name() *string {
- return e.endpointPort.Name
-}
-
-func (e *endpointSlicePortAdaptorV1beta1) port() *int32 {
- return e.endpointPort.Port
-}
-
-func (e *endpointSlicePortAdaptorV1beta1) protocol() *string {
- val := string(*e.endpointPort.Protocol)
- return &val
-}
-
-func (e *endpointSlicePortAdaptorV1beta1) appProtocol() *string {
- return e.endpointPort.AppProtocol
-}
diff --git a/discovery/kubernetes/endpointslice_adaptor_test.go b/discovery/kubernetes/endpointslice_adaptor_test.go
index e564910936..de33c64b66 100644
--- a/discovery/kubernetes/endpointslice_adaptor_test.go
+++ b/discovery/kubernetes/endpointslice_adaptor_test.go
@@ -18,7 +18,6 @@ import (
"github.com/stretchr/testify/require"
v1 "k8s.io/api/discovery/v1"
- "k8s.io/api/discovery/v1beta1"
)
func Test_EndpointSliceAdaptor_v1(t *testing.T) {
@@ -29,7 +28,7 @@ func Test_EndpointSliceAdaptor_v1(t *testing.T) {
require.Equal(t, endpointSlice.ObjectMeta.Namespace, adaptor.namespace())
require.Equal(t, endpointSlice.AddressType, v1.AddressType(adaptor.addressType()))
require.Equal(t, endpointSlice.Labels, adaptor.labels())
- require.Equal(t, endpointSlice.Labels[v1.LabelServiceName], "testendpoints")
+ require.Equal(t, "testendpoints", endpointSlice.Labels[v1.LabelServiceName])
for i, endpointAdaptor := range adaptor.endpoints() {
require.Equal(t, endpointSlice.Endpoints[i].Addresses, endpointAdaptor.addresses())
@@ -48,31 +47,3 @@ func Test_EndpointSliceAdaptor_v1(t *testing.T) {
require.Equal(t, endpointSlice.Ports[i].AppProtocol, portAdaptor.appProtocol())
}
}
-
-func Test_EndpointSliceAdaptor_v1beta1(t *testing.T) {
- endpointSlice := makeEndpointSliceV1beta1()
- adaptor := newEndpointSliceAdaptorFromV1beta1(endpointSlice)
-
- require.Equal(t, endpointSlice.ObjectMeta.Name, adaptor.name())
- require.Equal(t, endpointSlice.ObjectMeta.Namespace, adaptor.namespace())
- require.Equal(t, endpointSlice.AddressType, v1beta1.AddressType(adaptor.addressType()))
- require.Equal(t, endpointSlice.Labels, adaptor.labels())
- require.Equal(t, endpointSlice.Labels[v1beta1.LabelServiceName], "testendpoints")
-
- for i, endpointAdaptor := range adaptor.endpoints() {
- require.Equal(t, endpointSlice.Endpoints[i].Addresses, endpointAdaptor.addresses())
- require.Equal(t, endpointSlice.Endpoints[i].Hostname, endpointAdaptor.hostname())
- require.Equal(t, endpointSlice.Endpoints[i].Conditions.Ready, endpointAdaptor.conditions().ready())
- require.Equal(t, endpointSlice.Endpoints[i].Conditions.Serving, endpointAdaptor.conditions().serving())
- require.Equal(t, endpointSlice.Endpoints[i].Conditions.Terminating, endpointAdaptor.conditions().terminating())
- require.Equal(t, endpointSlice.Endpoints[i].TargetRef, endpointAdaptor.targetRef())
- require.Equal(t, endpointSlice.Endpoints[i].Topology, endpointAdaptor.topology())
- }
-
- for i, portAdaptor := range adaptor.ports() {
- require.Equal(t, endpointSlice.Ports[i].Name, portAdaptor.name())
- require.Equal(t, endpointSlice.Ports[i].Port, portAdaptor.port())
- require.EqualValues(t, endpointSlice.Ports[i].Protocol, portAdaptor.protocol())
- require.Equal(t, endpointSlice.Ports[i].AppProtocol, portAdaptor.appProtocol())
- }
-}
diff --git a/discovery/kubernetes/endpointslice_test.go b/discovery/kubernetes/endpointslice_test.go
index f23c9e6557..cc92c7ddaa 100644
--- a/discovery/kubernetes/endpointslice_test.go
+++ b/discovery/kubernetes/endpointslice_test.go
@@ -18,9 +18,9 @@ import (
"testing"
"github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/discovery/v1"
- "k8s.io/api/discovery/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
@@ -79,6 +79,7 @@ func makeEndpointSliceV1() *v1.EndpointSlice {
DeprecatedTopology: map[string]string{
"topology": "value",
},
+ Zone: strptr("us-east-1a"),
}, {
Addresses: []string{"2.3.4.5"},
Conditions: v1.EndpointConditions{
@@ -86,6 +87,7 @@ func makeEndpointSliceV1() *v1.EndpointSlice {
Serving: boolptr(true),
Terminating: boolptr(false),
},
+ Zone: strptr("us-east-1b"),
}, {
Addresses: []string{"3.4.5.6"},
Conditions: v1.EndpointConditions{
@@ -93,6 +95,7 @@ func makeEndpointSliceV1() *v1.EndpointSlice {
Serving: boolptr(true),
Terminating: boolptr(true),
},
+ Zone: strptr("us-east-1c"),
}, {
Addresses: []string{"4.5.6.7"},
Conditions: v1.EndpointConditions{
@@ -104,67 +107,14 @@ func makeEndpointSliceV1() *v1.EndpointSlice {
Kind: "Node",
Name: "barbaz",
},
- },
- },
- }
-}
-
-func makeEndpointSliceV1beta1() *v1beta1.EndpointSlice {
- return &v1beta1.EndpointSlice{
- ObjectMeta: metav1.ObjectMeta{
- Name: "testendpoints",
- Namespace: "default",
- Labels: map[string]string{
- v1beta1.LabelServiceName: "testendpoints",
- },
- Annotations: map[string]string{
- "test.annotation": "test",
- },
- },
- AddressType: v1beta1.AddressTypeIPv4,
- Ports: []v1beta1.EndpointPort{
- {
- Name: strptr("testport"),
- Port: int32ptr(9000),
- Protocol: protocolptr(corev1.ProtocolTCP),
- },
- },
- Endpoints: []v1beta1.Endpoint{
- {
- Addresses: []string{"1.2.3.4"},
- Hostname: strptr("testendpoint1"),
- }, {
- Addresses: []string{"2.3.4.5"},
- Conditions: v1beta1.EndpointConditions{
- Ready: boolptr(true),
- Serving: boolptr(true),
- Terminating: boolptr(false),
- },
- }, {
- Addresses: []string{"3.4.5.6"},
- Conditions: v1beta1.EndpointConditions{
- Ready: boolptr(false),
- Serving: boolptr(true),
- Terminating: boolptr(true),
- },
- }, {
- Addresses: []string{"4.5.6.7"},
- Conditions: v1beta1.EndpointConditions{
- Ready: boolptr(true),
- Serving: boolptr(true),
- Terminating: boolptr(false),
- },
- TargetRef: &corev1.ObjectReference{
- Kind: "Node",
- Name: "barbaz",
- },
+ Zone: strptr("us-east-1a"),
},
},
}
}
func TestEndpointSliceDiscoveryBeforeRun(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.25.0")
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}})
k8sDiscoveryTest{
discovery: n,
@@ -184,8 +134,10 @@ func TestEndpointSliceDiscoveryBeforeRun(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -196,6 +148,7 @@ func TestEndpointSliceDiscoveryBeforeRun(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -206,6 +159,7 @@ func TestEndpointSliceDiscoveryBeforeRun(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -218,6 +172,7 @@ func TestEndpointSliceDiscoveryBeforeRun(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -239,71 +194,6 @@ func TestEndpointSliceDiscoveryBeforeRun(t *testing.T) {
}.Run(t)
}
-func TestEndpointSliceDiscoveryBeforeRunV1beta1(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "1.20.0")
-
- k8sDiscoveryTest{
- discovery: n,
- beforeRun: func() {
- obj := makeEndpointSliceV1beta1()
- c.DiscoveryV1beta1().EndpointSlices(obj.Namespace).Create(context.Background(), obj, metav1.CreateOptions{})
- },
- expectedMaxItems: 1,
- expectedRes: map[string]*targetgroup.Group{
- "endpointslice/default/testendpoints": {
- Targets: []model.LabelSet{
- {
- "__address__": "1.2.3.4:9000",
- "__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "2.3.4.5:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "3.4.5.6:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "4.5.6.7:9000",
- "__meta_kubernetes_endpointslice_address_target_kind": "Node",
- "__meta_kubernetes_endpointslice_address_target_name": "barbaz",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- },
- Labels: model.LabelSet{
- "__meta_kubernetes_endpointslice_address_type": "IPv4",
- "__meta_kubernetes_namespace": "default",
- "__meta_kubernetes_endpointslice_name": "testendpoints",
- "__meta_kubernetes_endpointslice_label_kubernetes_io_service_name": "testendpoints",
- "__meta_kubernetes_endpointslice_labelpresent_kubernetes_io_service_name": "true",
- "__meta_kubernetes_endpointslice_annotation_test_annotation": "test",
- "__meta_kubernetes_endpointslice_annotationpresent_test_annotation": "true",
- },
- Source: "endpointslice/default/testendpoints",
- },
- },
- }.Run(t)
-}
-
func TestEndpointSliceDiscoveryAdd(t *testing.T) {
obj := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -343,25 +233,25 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
PodIP: "1.2.3.4",
},
}
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.20.0", obj)
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, obj)
k8sDiscoveryTest{
discovery: n,
afterStart: func() {
- obj := &v1beta1.EndpointSlice{
+ obj := &v1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "testendpoints",
Namespace: "default",
},
- AddressType: v1beta1.AddressTypeIPv4,
- Ports: []v1beta1.EndpointPort{
+ AddressType: v1.AddressTypeIPv4,
+ Ports: []v1.EndpointPort{
{
Name: strptr("testport"),
Port: int32ptr(9000),
Protocol: protocolptr(corev1.ProtocolTCP),
},
},
- Endpoints: []v1beta1.Endpoint{
+ Endpoints: []v1.Endpoint{
{
Addresses: []string{"4.3.2.1"},
TargetRef: &corev1.ObjectReference{
@@ -369,13 +259,13 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
Name: "testpod",
Namespace: "default",
},
- Conditions: v1beta1.EndpointConditions{
+ Conditions: v1.EndpointConditions{
Ready: boolptr(false),
},
},
},
}
- c.DiscoveryV1beta1().EndpointSlices(obj.Namespace).Create(context.Background(), obj, metav1.CreateOptions{})
+ c.DiscoveryV1().EndpointSlices(obj.Namespace).Create(context.Background(), obj, metav1.CreateOptions{})
},
expectedMaxItems: 1,
expectedRes: map[string]*targetgroup.Group{
@@ -401,6 +291,7 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
"__meta_kubernetes_pod_phase": "",
"__meta_kubernetes_pod_ready": "unknown",
"__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
},
{
"__address__": "1.2.3.4:9001",
@@ -416,6 +307,7 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
"__meta_kubernetes_pod_phase": "",
"__meta_kubernetes_pod_ready": "unknown",
"__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
},
},
Labels: model.LabelSet{
@@ -430,113 +322,34 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
}
func TestEndpointSliceDiscoveryDelete(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.21.0", makeEndpointSliceV1())
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, makeEndpointSliceV1())
k8sDiscoveryTest{
discovery: n,
afterStart: func() {
obj := makeEndpointSliceV1()
- c.DiscoveryV1beta1().EndpointSlices(obj.Namespace).Delete(context.Background(), obj.Name, metav1.DeleteOptions{})
+ c.DiscoveryV1().EndpointSlices(obj.Namespace).Delete(context.Background(), obj.Name, metav1.DeleteOptions{})
},
expectedMaxItems: 2,
expectedRes: map[string]*targetgroup.Group{
"endpointslice/default/testendpoints": {
Source: "endpointslice/default/testendpoints",
- Targets: []model.LabelSet{
- {
- "__address__": "1.2.3.4:9000",
- "__meta_kubernetes_endpointslice_address_target_kind": "",
- "__meta_kubernetes_endpointslice_address_target_name": "",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
- "__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
- "__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "2.3.4.5:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "3.4.5.6:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "4.5.6.7:9000",
- "__meta_kubernetes_endpointslice_address_target_kind": "Node",
- "__meta_kubernetes_endpointslice_address_target_name": "barbaz",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- },
- Labels: map[model.LabelName]model.LabelValue{
- "__meta_kubernetes_endpointslice_address_type": "IPv4",
- "__meta_kubernetes_endpointslice_name": "testendpoints",
- "__meta_kubernetes_endpointslice_label_kubernetes_io_service_name": "testendpoints",
- "__meta_kubernetes_endpointslice_labelpresent_kubernetes_io_service_name": "true",
- "__meta_kubernetes_endpointslice_annotation_test_annotation": "test",
- "__meta_kubernetes_endpointslice_annotationpresent_test_annotation": "true",
- "__meta_kubernetes_namespace": "default",
- },
},
},
}.Run(t)
}
func TestEndpointSliceDiscoveryUpdate(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.21.0", makeEndpointSliceV1())
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, makeEndpointSliceV1())
k8sDiscoveryTest{
discovery: n,
afterStart: func() {
- obj := &v1beta1.EndpointSlice{
- ObjectMeta: metav1.ObjectMeta{
- Name: "testendpoints",
- Namespace: "default",
- },
- AddressType: v1beta1.AddressTypeIPv4,
- Ports: []v1beta1.EndpointPort{
- {
- Name: strptr("testport"),
- Port: int32ptr(9000),
- Protocol: protocolptr(corev1.ProtocolTCP),
- },
- },
- Endpoints: []v1beta1.Endpoint{
- {
- Addresses: []string{"1.2.3.4"},
- Hostname: strptr("testendpoint1"),
- }, {
- Addresses: []string{"2.3.4.5"},
- Conditions: v1beta1.EndpointConditions{
- Ready: boolptr(true),
- },
- },
- },
- }
- c.DiscoveryV1beta1().EndpointSlices(obj.Namespace).Update(context.Background(), obj, metav1.UpdateOptions{})
+ obj := makeEndpointSliceV1()
+ obj.ObjectMeta.Labels = nil
+ obj.ObjectMeta.Annotations = nil
+ obj.Endpoints = obj.Endpoints[0:2]
+ c.DiscoveryV1().EndpointSlices(obj.Namespace).Update(context.Background(), obj, metav1.UpdateOptions{})
},
expectedMaxItems: 2,
expectedRes: map[string]*targetgroup.Group{
@@ -551,8 +364,10 @@ func TestEndpointSliceDiscoveryUpdate(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -563,28 +378,7 @@ func TestEndpointSliceDiscoveryUpdate(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "3.4.5.6:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "4.5.6.7:9000",
- "__meta_kubernetes_endpointslice_address_target_kind": "Node",
- "__meta_kubernetes_endpointslice_address_target_name": "barbaz",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -592,13 +386,9 @@ func TestEndpointSliceDiscoveryUpdate(t *testing.T) {
},
},
Labels: model.LabelSet{
- "__meta_kubernetes_endpointslice_address_type": "IPv4",
- "__meta_kubernetes_endpointslice_name": "testendpoints",
- "__meta_kubernetes_endpointslice_label_kubernetes_io_service_name": "testendpoints",
- "__meta_kubernetes_endpointslice_labelpresent_kubernetes_io_service_name": "true",
- "__meta_kubernetes_endpointslice_annotation_test_annotation": "test",
- "__meta_kubernetes_endpointslice_annotationpresent_test_annotation": "true",
- "__meta_kubernetes_namespace": "default",
+ "__meta_kubernetes_endpointslice_address_type": "IPv4",
+ "__meta_kubernetes_endpointslice_name": "testendpoints",
+ "__meta_kubernetes_namespace": "default",
},
},
},
@@ -606,80 +396,18 @@ func TestEndpointSliceDiscoveryUpdate(t *testing.T) {
}
func TestEndpointSliceDiscoveryEmptyEndpoints(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.21.0", makeEndpointSliceV1())
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, makeEndpointSliceV1())
k8sDiscoveryTest{
discovery: n,
afterStart: func() {
- obj := &v1beta1.EndpointSlice{
- ObjectMeta: metav1.ObjectMeta{
- Name: "testendpoints",
- Namespace: "default",
- },
- AddressType: v1beta1.AddressTypeIPv4,
- Ports: []v1beta1.EndpointPort{
- {
- Name: strptr("testport"),
- Port: int32ptr(9000),
- Protocol: protocolptr(corev1.ProtocolTCP),
- },
- },
- Endpoints: []v1beta1.Endpoint{},
- }
- c.DiscoveryV1beta1().EndpointSlices(obj.Namespace).Update(context.Background(), obj, metav1.UpdateOptions{})
+ obj := makeEndpointSliceV1()
+ obj.Endpoints = []v1.Endpoint{}
+ c.DiscoveryV1().EndpointSlices(obj.Namespace).Update(context.Background(), obj, metav1.UpdateOptions{})
},
expectedMaxItems: 2,
expectedRes: map[string]*targetgroup.Group{
"endpointslice/default/testendpoints": {
- Targets: []model.LabelSet{
- {
- "__address__": "1.2.3.4:9000",
- "__meta_kubernetes_endpointslice_address_target_kind": "",
- "__meta_kubernetes_endpointslice_address_target_name": "",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
- "__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
- "__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "2.3.4.5:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "3.4.5.6:9000",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- {
- "__address__": "4.5.6.7:9000",
- "__meta_kubernetes_endpointslice_address_target_kind": "Node",
- "__meta_kubernetes_endpointslice_address_target_name": "barbaz",
- "__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
- "__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
- "__meta_kubernetes_endpointslice_port": "9000",
- "__meta_kubernetes_endpointslice_port_app_protocol": "http",
- "__meta_kubernetes_endpointslice_port_name": "testport",
- "__meta_kubernetes_endpointslice_port_protocol": "TCP",
- },
- },
Labels: model.LabelSet{
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_name": "testendpoints",
@@ -696,7 +424,7 @@ func TestEndpointSliceDiscoveryEmptyEndpoints(t *testing.T) {
}
func TestEndpointSliceDiscoveryWithService(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.21.0", makeEndpointSliceV1())
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, makeEndpointSliceV1())
k8sDiscoveryTest{
discovery: n,
@@ -724,8 +452,10 @@ func TestEndpointSliceDiscoveryWithService(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -736,6 +466,7 @@ func TestEndpointSliceDiscoveryWithService(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -746,6 +477,7 @@ func TestEndpointSliceDiscoveryWithService(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -758,6 +490,7 @@ func TestEndpointSliceDiscoveryWithService(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -783,7 +516,7 @@ func TestEndpointSliceDiscoveryWithService(t *testing.T) {
}
func TestEndpointSliceDiscoveryWithServiceUpdate(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, "v1.21.0", makeEndpointSliceV1())
+ n, c := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{Names: []string{"default"}}, makeEndpointSliceV1())
k8sDiscoveryTest{
discovery: n,
@@ -824,8 +557,10 @@ func TestEndpointSliceDiscoveryWithServiceUpdate(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -836,6 +571,7 @@ func TestEndpointSliceDiscoveryWithServiceUpdate(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_name": "testport",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
@@ -846,6 +582,7 @@ func TestEndpointSliceDiscoveryWithServiceUpdate(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_name": "testport",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
@@ -858,6 +595,7 @@ func TestEndpointSliceDiscoveryWithServiceUpdate(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -914,8 +652,10 @@ func TestEndpointsSlicesDiscoveryWithNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -929,6 +669,7 @@ func TestEndpointsSlicesDiscoveryWithNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -939,6 +680,7 @@ func TestEndpointsSlicesDiscoveryWithNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -951,6 +693,7 @@ func TestEndpointsSlicesDiscoveryWithNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1014,8 +757,10 @@ func TestEndpointsSlicesDiscoveryWithUpdatedNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1029,6 +774,7 @@ func TestEndpointsSlicesDiscoveryWithUpdatedNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1039,6 +785,7 @@ func TestEndpointsSlicesDiscoveryWithUpdatedNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1051,6 +798,7 @@ func TestEndpointsSlicesDiscoveryWithUpdatedNodeMetadata(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1160,8 +908,10 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1172,6 +922,7 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_name": "testport",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
@@ -1182,6 +933,7 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_name": "testport",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
@@ -1194,6 +946,7 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1235,6 +988,7 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
"__meta_kubernetes_pod_phase": "",
"__meta_kubernetes_pod_ready": "unknown",
"__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
},
},
Labels: model.LabelSet{
@@ -1308,8 +1062,10 @@ func TestEndpointSliceDiscoveryOwnNamespace(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
"__meta_kubernetes_endpointslice_endpoint_hostname": "testendpoint1",
+ "__meta_kubernetes_endpointslice_endpoint_node_name": "foobar",
"__meta_kubernetes_endpointslice_endpoint_topology_present_topology": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_topology": "value",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1320,6 +1076,7 @@ func TestEndpointSliceDiscoveryOwnNamespace(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1b",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_name": "testport",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
@@ -1330,6 +1087,7 @@ func TestEndpointSliceDiscoveryOwnNamespace(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "false",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "true",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1c",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_name": "testport",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
@@ -1342,6 +1100,7 @@ func TestEndpointSliceDiscoveryOwnNamespace(t *testing.T) {
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_serving": "true",
"__meta_kubernetes_endpointslice_endpoint_conditions_terminating": "false",
+ "__meta_kubernetes_endpointslice_endpoint_zone": "us-east-1a",
"__meta_kubernetes_endpointslice_port": "9000",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "testport",
@@ -1405,3 +1164,203 @@ func TestEndpointSliceDiscoveryEmptyPodStatus(t *testing.T) {
expectedRes: map[string]*targetgroup.Group{},
}.Run(t)
}
+
+// TestEndpointSliceInfIndexersCount makes sure that RoleEndpointSlice discovery
+// sets up indexing for the main Kube informer only when needed.
+// See: https://github.com/prometheus/prometheus/pull/13554#discussion_r1490965817
+func TestEndpointSliceInfIndexersCount(t *testing.T) {
+ tests := []struct {
+ name string
+ withNodeMetadata bool
+ }{
+ {"with node metadata", true},
+ {"without node metadata", false},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var (
+ n *Discovery
+ mainInfIndexersCount int
+ )
+ if tc.withNodeMetadata {
+ mainInfIndexersCount = 1
+ n, _ = makeDiscoveryWithMetadata(RoleEndpointSlice, NamespaceDiscovery{}, AttachMetadataConfig{Node: true})
+ } else {
+ n, _ = makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{})
+ }
+
+ k8sDiscoveryTest{
+ discovery: n,
+ afterStart: func() {
+ n.RLock()
+ defer n.RUnlock()
+ require.Len(t, n.discoverers, 1)
+ require.Len(t, n.discoverers[0].(*EndpointSlice).endpointSliceInf.GetIndexer().GetIndexers(), mainInfIndexersCount)
+ },
+ }.Run(t)
+ })
+ }
+}
+
+func TestEndpointSliceDiscoverySidecarContainer(t *testing.T) {
+ objs := []runtime.Object{
+ &v1.EndpointSlice{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "testsidecar",
+ Namespace: "default",
+ },
+ AddressType: v1.AddressTypeIPv4,
+ Ports: []v1.EndpointPort{
+ {
+ Name: strptr("testport"),
+ Port: int32ptr(9000),
+ Protocol: protocolptr(corev1.ProtocolTCP),
+ },
+ {
+ Name: strptr("initport"),
+ Port: int32ptr(9111),
+ Protocol: protocolptr(corev1.ProtocolTCP),
+ },
+ },
+ Endpoints: []v1.Endpoint{
+ {
+ Addresses: []string{"4.3.2.1"},
+ TargetRef: &corev1.ObjectReference{
+ Kind: "Pod",
+ Name: "testpod",
+ Namespace: "default",
+ },
+ },
+ },
+ },
+ &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "testpod",
+ Namespace: "default",
+ UID: types.UID("deadbeef"),
+ },
+ Spec: corev1.PodSpec{
+ NodeName: "testnode",
+ InitContainers: []corev1.Container{
+ {
+ Name: "ic1",
+ Image: "ic1:latest",
+ Ports: []corev1.ContainerPort{
+ {
+ Name: "initport",
+ ContainerPort: 1111,
+ Protocol: corev1.ProtocolTCP,
+ },
+ },
+ },
+ {
+ Name: "ic2",
+ Image: "ic2:latest",
+ Ports: []corev1.ContainerPort{
+ {
+ Name: "initport",
+ ContainerPort: 9111,
+ Protocol: corev1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: "c1",
+ Image: "c1:latest",
+ Ports: []corev1.ContainerPort{
+ {
+ Name: "mainport",
+ ContainerPort: 9000,
+ Protocol: corev1.ProtocolTCP,
+ },
+ },
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ HostIP: "2.3.4.5",
+ PodIP: "4.3.2.1",
+ },
+ },
+ }
+
+ n, _ := makeDiscovery(RoleEndpointSlice, NamespaceDiscovery{}, objs...)
+
+ k8sDiscoveryTest{
+ discovery: n,
+ expectedMaxItems: 1,
+ expectedRes: map[string]*targetgroup.Group{
+ "endpointslice/default/testsidecar": {
+ Targets: []model.LabelSet{
+ {
+ "__address__": "4.3.2.1:9000",
+ "__meta_kubernetes_endpointslice_address_target_kind": "Pod",
+ "__meta_kubernetes_endpointslice_address_target_name": "testpod",
+ "__meta_kubernetes_endpointslice_port": "9000",
+ "__meta_kubernetes_endpointslice_port_name": "testport",
+ "__meta_kubernetes_endpointslice_port_protocol": "TCP",
+ "__meta_kubernetes_pod_container_image": "c1:latest",
+ "__meta_kubernetes_pod_container_name": "c1",
+ "__meta_kubernetes_pod_container_port_name": "mainport",
+ "__meta_kubernetes_pod_container_port_number": "9000",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_phase": "",
+ "__meta_kubernetes_pod_ready": "unknown",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "false",
+ },
+ {
+ "__address__": "4.3.2.1:9111",
+ "__meta_kubernetes_endpointslice_address_target_kind": "Pod",
+ "__meta_kubernetes_endpointslice_address_target_name": "testpod",
+ "__meta_kubernetes_endpointslice_port": "9111",
+ "__meta_kubernetes_endpointslice_port_name": "initport",
+ "__meta_kubernetes_endpointslice_port_protocol": "TCP",
+ "__meta_kubernetes_pod_container_image": "ic2:latest",
+ "__meta_kubernetes_pod_container_name": "ic2",
+ "__meta_kubernetes_pod_container_port_name": "initport",
+ "__meta_kubernetes_pod_container_port_number": "9111",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_phase": "",
+ "__meta_kubernetes_pod_ready": "unknown",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "true",
+ },
+ {
+ "__address__": "4.3.2.1:1111",
+ "__meta_kubernetes_pod_container_image": "ic1:latest",
+ "__meta_kubernetes_pod_container_name": "ic1",
+ "__meta_kubernetes_pod_container_port_name": "initport",
+ "__meta_kubernetes_pod_container_port_number": "1111",
+ "__meta_kubernetes_pod_container_port_protocol": "TCP",
+ "__meta_kubernetes_pod_host_ip": "2.3.4.5",
+ "__meta_kubernetes_pod_ip": "4.3.2.1",
+ "__meta_kubernetes_pod_name": "testpod",
+ "__meta_kubernetes_pod_node_name": "testnode",
+ "__meta_kubernetes_pod_phase": "",
+ "__meta_kubernetes_pod_ready": "unknown",
+ "__meta_kubernetes_pod_uid": "deadbeef",
+ "__meta_kubernetes_pod_container_init": "true",
+ },
+ },
+ Labels: model.LabelSet{
+ "__meta_kubernetes_endpointslice_address_type": "IPv4",
+ "__meta_kubernetes_endpointslice_name": "testsidecar",
+ "__meta_kubernetes_namespace": "default",
+ },
+ Source: "endpointslice/default/testsidecar",
+ },
+ },
+ }.Run(t)
+}
diff --git a/discovery/kubernetes/ingress.go b/discovery/kubernetes/ingress.go
index 697b6f5198..4d91e7a460 100644
--- a/discovery/kubernetes/ingress.go
+++ b/discovery/kubernetes/ingress.go
@@ -21,21 +21,15 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
v1 "k8s.io/api/networking/v1"
- "k8s.io/api/networking/v1beta1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-var (
- ingressAddCount = eventCount.WithLabelValues("ingress", "add")
- ingressUpdateCount = eventCount.WithLabelValues("ingress", "update")
- ingressDeleteCount = eventCount.WithLabelValues("ingress", "delete")
-)
-
// Ingress implements discovery of Kubernetes ingress.
type Ingress struct {
logger log.Logger
@@ -45,8 +39,18 @@ type Ingress struct {
}
// NewIngress returns a new ingress discovery.
-func NewIngress(l log.Logger, inf cache.SharedInformer) *Ingress {
- s := &Ingress{logger: l, informer: inf, store: inf.GetStore(), queue: workqueue.NewNamed("ingress")}
+func NewIngress(l log.Logger, inf cache.SharedInformer, eventCount *prometheus.CounterVec) *Ingress {
+ ingressAddCount := eventCount.WithLabelValues(RoleIngress.String(), MetricLabelRoleAdd)
+ ingressUpdateCount := eventCount.WithLabelValues(RoleIngress.String(), MetricLabelRoleUpdate)
+ ingressDeleteCount := eventCount.WithLabelValues(RoleIngress.String(), MetricLabelRoleDelete)
+
+ s := &Ingress{
+ logger: l,
+ informer: inf,
+ store: inf.GetStore(),
+ queue: workqueue.NewNamed(RoleIngress.String()),
+ }
+
_, err := s.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(o interface{}) {
ingressAddCount.Inc()
@@ -88,7 +92,7 @@ func (i *Ingress) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
}
go func() {
- for i.process(ctx, ch) { // nolint:revive
+ for i.process(ctx, ch) {
}
}()
@@ -122,8 +126,6 @@ func (i *Ingress) process(ctx context.Context, ch chan<- []*targetgroup.Group) b
switch ingress := o.(type) {
case *v1.Ingress:
ia = newIngressAdaptorFromV1(ingress)
- case *v1beta1.Ingress:
- ia = newIngressAdaptorFromV1beta1(ingress)
default:
level.Error(i.logger).Log("msg", "converting to Ingress object failed", "err",
fmt.Errorf("received unexpected object: %v", o))
diff --git a/discovery/kubernetes/ingress_adaptor.go b/discovery/kubernetes/ingress_adaptor.go
index 7be8538b53..84281196b4 100644
--- a/discovery/kubernetes/ingress_adaptor.go
+++ b/discovery/kubernetes/ingress_adaptor.go
@@ -15,11 +15,10 @@ package kubernetes
import (
v1 "k8s.io/api/networking/v1"
- "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
-// ingressAdaptor is an adaptor for the different Ingress versions
+// ingressAdaptor is an adaptor for the different Ingress versions.
type ingressAdaptor interface {
getObjectMeta() metav1.ObjectMeta
name() string
@@ -36,7 +35,7 @@ type ingressRuleAdaptor interface {
host() string
}
-// Adaptor for networking.k8s.io/v1
+// Adaptor for networking.k8s.io/v1.
type ingressAdaptorV1 struct {
ingress *v1.Ingress
}
@@ -89,56 +88,3 @@ func (i *ingressRuleAdaptorV1) paths() []string {
}
func (i *ingressRuleAdaptorV1) host() string { return i.rule.Host }
-
-// Adaptor for networking.k8s.io/v1beta1
-type ingressAdaptorV1Beta1 struct {
- ingress *v1beta1.Ingress
-}
-
-func newIngressAdaptorFromV1beta1(ingress *v1beta1.Ingress) ingressAdaptor {
- return &ingressAdaptorV1Beta1{ingress: ingress}
-}
-func (i *ingressAdaptorV1Beta1) getObjectMeta() metav1.ObjectMeta { return i.ingress.ObjectMeta }
-func (i *ingressAdaptorV1Beta1) name() string { return i.ingress.Name }
-func (i *ingressAdaptorV1Beta1) namespace() string { return i.ingress.Namespace }
-func (i *ingressAdaptorV1Beta1) labels() map[string]string { return i.ingress.Labels }
-func (i *ingressAdaptorV1Beta1) annotations() map[string]string { return i.ingress.Annotations }
-func (i *ingressAdaptorV1Beta1) ingressClassName() *string { return i.ingress.Spec.IngressClassName }
-
-func (i *ingressAdaptorV1Beta1) tlsHosts() []string {
- var hosts []string
- for _, tls := range i.ingress.Spec.TLS {
- hosts = append(hosts, tls.Hosts...)
- }
- return hosts
-}
-
-func (i *ingressAdaptorV1Beta1) rules() []ingressRuleAdaptor {
- var rules []ingressRuleAdaptor
- for _, rule := range i.ingress.Spec.Rules {
- rules = append(rules, newIngressRuleAdaptorFromV1Beta1(rule))
- }
- return rules
-}
-
-type ingressRuleAdaptorV1Beta1 struct {
- rule v1beta1.IngressRule
-}
-
-func newIngressRuleAdaptorFromV1Beta1(rule v1beta1.IngressRule) ingressRuleAdaptor {
- return &ingressRuleAdaptorV1Beta1{rule: rule}
-}
-
-func (i *ingressRuleAdaptorV1Beta1) paths() []string {
- rv := i.rule.IngressRuleValue
- if rv.HTTP == nil {
- return nil
- }
- paths := make([]string, len(rv.HTTP.Paths))
- for n, p := range rv.HTTP.Paths {
- paths[n] = p.Path
- }
- return paths
-}
-
-func (i *ingressRuleAdaptorV1Beta1) host() string { return i.rule.Host }
diff --git a/discovery/kubernetes/ingress_test.go b/discovery/kubernetes/ingress_test.go
index 8e6654c2cc..9bddfb1e14 100644
--- a/discovery/kubernetes/ingress_test.go
+++ b/discovery/kubernetes/ingress_test.go
@@ -20,7 +20,6 @@ import (
"github.com/prometheus/common/model"
v1 "k8s.io/api/networking/v1"
- "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/prometheus/prometheus/discovery/targetgroup"
@@ -89,60 +88,6 @@ func makeIngress(tls TLSMode) *v1.Ingress {
return ret
}
-func makeIngressV1beta1(tls TLSMode) *v1beta1.Ingress {
- ret := &v1beta1.Ingress{
- ObjectMeta: metav1.ObjectMeta{
- Name: "testingress",
- Namespace: "default",
- Labels: map[string]string{"test/label": "testvalue"},
- Annotations: map[string]string{"test/annotation": "testannotationvalue"},
- },
- Spec: v1beta1.IngressSpec{
- IngressClassName: classString("testclass"),
- TLS: nil,
- Rules: []v1beta1.IngressRule{
- {
- Host: "example.com",
- IngressRuleValue: v1beta1.IngressRuleValue{
- HTTP: &v1beta1.HTTPIngressRuleValue{
- Paths: []v1beta1.HTTPIngressPath{
- {Path: "/"},
- {Path: "/foo"},
- },
- },
- },
- },
- {
- // No backend config, ignored
- Host: "nobackend.example.com",
- IngressRuleValue: v1beta1.IngressRuleValue{
- HTTP: &v1beta1.HTTPIngressRuleValue{},
- },
- },
- {
- Host: "test.example.com",
- IngressRuleValue: v1beta1.IngressRuleValue{
- HTTP: &v1beta1.HTTPIngressRuleValue{
- Paths: []v1beta1.HTTPIngressPath{{}},
- },
- },
- },
- },
- },
- }
-
- switch tls {
- case TLSYes:
- ret.Spec.TLS = []v1beta1.IngressTLS{{Hosts: []string{"example.com", "test.example.com"}}}
- case TLSMixed:
- ret.Spec.TLS = []v1beta1.IngressTLS{{Hosts: []string{"example.com"}}}
- case TLSWildcard:
- ret.Spec.TLS = []v1beta1.IngressTLS{{Hosts: []string{"*.example.com"}}}
- }
-
- return ret
-}
-
func classString(v string) *string {
return &v
}
@@ -212,20 +157,6 @@ func TestIngressDiscoveryAdd(t *testing.T) {
}.Run(t)
}
-func TestIngressDiscoveryAddV1beta1(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleIngress, NamespaceDiscovery{Names: []string{"default"}}, "v1.18.0")
-
- k8sDiscoveryTest{
- discovery: n,
- afterStart: func() {
- obj := makeIngressV1beta1(TLSNo)
- c.NetworkingV1beta1().Ingresses("default").Create(context.Background(), obj, metav1.CreateOptions{})
- },
- expectedMaxItems: 1,
- expectedRes: expectedTargetGroups("default", TLSNo),
- }.Run(t)
-}
-
func TestIngressDiscoveryAddTLS(t *testing.T) {
n, c := makeDiscovery(RoleIngress, NamespaceDiscovery{Names: []string{"default"}})
@@ -240,20 +171,6 @@ func TestIngressDiscoveryAddTLS(t *testing.T) {
}.Run(t)
}
-func TestIngressDiscoveryAddTLSV1beta1(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleIngress, NamespaceDiscovery{Names: []string{"default"}}, "v1.18.0")
-
- k8sDiscoveryTest{
- discovery: n,
- afterStart: func() {
- obj := makeIngressV1beta1(TLSYes)
- c.NetworkingV1beta1().Ingresses("default").Create(context.Background(), obj, metav1.CreateOptions{})
- },
- expectedMaxItems: 1,
- expectedRes: expectedTargetGroups("default", TLSYes),
- }.Run(t)
-}
-
func TestIngressDiscoveryAddMixed(t *testing.T) {
n, c := makeDiscovery(RoleIngress, NamespaceDiscovery{Names: []string{"default"}})
@@ -268,20 +185,6 @@ func TestIngressDiscoveryAddMixed(t *testing.T) {
}.Run(t)
}
-func TestIngressDiscoveryAddMixedV1beta1(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleIngress, NamespaceDiscovery{Names: []string{"default"}}, "v1.18.0")
-
- k8sDiscoveryTest{
- discovery: n,
- afterStart: func() {
- obj := makeIngressV1beta1(TLSMixed)
- c.NetworkingV1beta1().Ingresses("default").Create(context.Background(), obj, metav1.CreateOptions{})
- },
- expectedMaxItems: 1,
- expectedRes: expectedTargetGroups("default", TLSMixed),
- }.Run(t)
-}
-
func TestIngressDiscoveryNamespaces(t *testing.T) {
n, c := makeDiscovery(RoleIngress, NamespaceDiscovery{Names: []string{"ns1", "ns2"}})
@@ -303,27 +206,6 @@ func TestIngressDiscoveryNamespaces(t *testing.T) {
}.Run(t)
}
-func TestIngressDiscoveryNamespacesV1beta1(t *testing.T) {
- n, c := makeDiscoveryWithVersion(RoleIngress, NamespaceDiscovery{Names: []string{"ns1", "ns2"}}, "v1.18.0")
-
- expected := expectedTargetGroups("ns1", TLSNo)
- for k, v := range expectedTargetGroups("ns2", TLSNo) {
- expected[k] = v
- }
- k8sDiscoveryTest{
- discovery: n,
- afterStart: func() {
- for _, ns := range []string{"ns1", "ns2"} {
- obj := makeIngressV1beta1(TLSNo)
- obj.Namespace = ns
- c.NetworkingV1beta1().Ingresses(obj.Namespace).Create(context.Background(), obj, metav1.CreateOptions{})
- }
- },
- expectedMaxItems: 2,
- expectedRes: expected,
- }.Run(t)
-}
-
func TestIngressDiscoveryOwnNamespace(t *testing.T) {
n, c := makeDiscovery(RoleIngress, NamespaceDiscovery{IncludeOwnNamespace: true})
diff --git a/discovery/kubernetes/kubernetes.go b/discovery/kubernetes/kubernetes.go
index ca5ee49e28..93ac65d8dc 100644
--- a/discovery/kubernetes/kubernetes.go
+++ b/discovery/kubernetes/kubernetes.go
@@ -25,8 +25,6 @@ import (
"github.com/prometheus/prometheus/util/strutil"
- disv1beta1 "k8s.io/api/discovery/v1beta1"
-
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
@@ -36,12 +34,10 @@ import (
apiv1 "k8s.io/api/core/v1"
disv1 "k8s.io/api/discovery/v1"
networkv1 "k8s.io/api/networking/v1"
- "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
- utilversion "k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@@ -58,25 +54,15 @@ import (
const (
// metaLabelPrefix is the meta prefix used for all meta labels.
// in this discovery.
- metaLabelPrefix = model.MetaLabelPrefix + "kubernetes_"
- namespaceLabel = metaLabelPrefix + "namespace"
- metricsNamespace = "prometheus_sd_kubernetes"
- presentValue = model.LabelValue("true")
+ metaLabelPrefix = model.MetaLabelPrefix + "kubernetes_"
+ namespaceLabel = metaLabelPrefix + "namespace"
+ presentValue = model.LabelValue("true")
)
var (
- // Http header
+ // Http header.
userAgent = fmt.Sprintf("Prometheus/%s", version.Version)
- // Custom events metric
- eventCount = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Namespace: metricsNamespace,
- Name: "events_total",
- Help: "The number of Kubernetes events handled.",
- },
- []string{"role", "event"},
- )
- // DefaultSDConfig is the default Kubernetes SD configuration
+ // DefaultSDConfig is the default Kubernetes SD configuration.
DefaultSDConfig = SDConfig{
HTTPClientConfig: config.DefaultHTTPClientConfig,
}
@@ -84,15 +70,6 @@ var (
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(eventCount)
- // Initialize metric vectors.
- for _, role := range []string{"endpointslice", "endpoints", "node", "pod", "service", "ingress"} {
- for _, evt := range []string{"add", "delete", "update"} {
- eventCount.WithLabelValues(role, evt)
- }
- }
- (&clientGoRequestMetricAdapter{}).Register(prometheus.DefaultRegisterer)
- (&clientGoWorkqueueMetricsProvider{}).Register(prometheus.DefaultRegisterer)
}
// Role is role of the service in Kubernetes.
@@ -121,6 +98,16 @@ func (c *Role) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}
+func (c Role) String() string {
+ return string(c)
+}
+
+const (
+ MetricLabelRoleAdd = "add"
+ MetricLabelRoleDelete = "delete"
+ MetricLabelRoleUpdate = "update"
+)
+
// SDConfig is the configuration for Kubernetes service discovery.
type SDConfig struct {
APIServer config.URL `yaml:"api_server,omitempty"`
@@ -132,12 +119,17 @@ type SDConfig struct {
AttachMetadata AttachMetadataConfig `yaml:"attach_metadata,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "kubernetes" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return New(opts.Logger, c)
+ return New(opts.Logger, opts.Metrics, c)
}
// SetDirectory joins any relative file paths with dir.
@@ -274,6 +266,7 @@ type Discovery struct {
selectors roleSelector
ownNamespace string
attachMetadata AttachMetadataConfig
+ metrics *kubernetesMetrics
}
func (d *Discovery) getNamespaces() []string {
@@ -292,7 +285,12 @@ func (d *Discovery) getNamespaces() []string {
}
// New creates a new Kubernetes discovery for the given role.
-func New(l log.Logger, conf *SDConfig) (*Discovery, error) {
+func New(l log.Logger, metrics discovery.DiscovererMetrics, conf *SDConfig) (*Discovery, error) {
+ m, ok := metrics.(*kubernetesMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if l == nil {
l = log.NewNopLogger()
}
@@ -309,7 +307,7 @@ func New(l log.Logger, conf *SDConfig) (*Discovery, error) {
}
case conf.APIServer.URL == nil:
// Use the Kubernetes provided pod service account
- // as described in https://kubernetes.io/docs/admin/service-accounts-admin/
+ // as described in https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/#using-official-client-libraries
kcfg, err = rest.InClusterConfig()
if err != nil {
return nil, err
@@ -346,7 +344,7 @@ func New(l log.Logger, conf *SDConfig) (*Discovery, error) {
return nil, err
}
- return &Discovery{
+ d := &Discovery{
client: c,
logger: l,
role: conf.Role,
@@ -355,7 +353,10 @@ func New(l log.Logger, conf *SDConfig) (*Discovery, error) {
selectors: mapSelector(conf.Selectors),
ownNamespace: ownNamespace,
attachMetadata: conf.AttachMetadata,
- }, nil
+ metrics: m,
+ }
+
+ return d, nil
}
func mapSelector(rawSelector []SelectorConfig) roleSelector {
@@ -391,59 +392,27 @@ const resyncDisabled = 0
// Run implements the discoverer interface.
func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
d.Lock()
+
namespaces := d.getNamespaces()
switch d.role {
case RoleEndpointSlice:
- // Check "networking.k8s.io/v1" availability with retries.
- // If "v1" is not available, use "networking.k8s.io/v1beta1" for backward compatibility
- var v1Supported bool
- if retryOnError(ctx, 10*time.Second,
- func() (err error) {
- v1Supported, err = checkDiscoveryV1Supported(d.client)
- if err != nil {
- level.Error(d.logger).Log("msg", "Failed to check networking.k8s.io/v1 availability", "err", err)
- }
- return err
- },
- ) {
- d.Unlock()
- return
- }
-
for _, namespace := range namespaces {
var informer cache.SharedIndexInformer
- if v1Supported {
- e := d.client.DiscoveryV1().EndpointSlices(namespace)
- elw := &cache.ListWatch{
- ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
- options.FieldSelector = d.selectors.endpointslice.field
- options.LabelSelector = d.selectors.endpointslice.label
- return e.List(ctx, options)
- },
- WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
- options.FieldSelector = d.selectors.endpointslice.field
- options.LabelSelector = d.selectors.endpointslice.label
- return e.Watch(ctx, options)
- },
- }
- informer = d.newEndpointSlicesByNodeInformer(elw, &disv1.EndpointSlice{})
- } else {
- e := d.client.DiscoveryV1beta1().EndpointSlices(namespace)
- elw := &cache.ListWatch{
- ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
- options.FieldSelector = d.selectors.endpointslice.field
- options.LabelSelector = d.selectors.endpointslice.label
- return e.List(ctx, options)
- },
- WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
- options.FieldSelector = d.selectors.endpointslice.field
- options.LabelSelector = d.selectors.endpointslice.label
- return e.Watch(ctx, options)
- },
- }
- informer = d.newEndpointSlicesByNodeInformer(elw, &disv1beta1.EndpointSlice{})
+ e := d.client.DiscoveryV1().EndpointSlices(namespace)
+ elw := &cache.ListWatch{
+ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+ options.FieldSelector = d.selectors.endpointslice.field
+ options.LabelSelector = d.selectors.endpointslice.label
+ return e.List(ctx, options)
+ },
+ WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+ options.FieldSelector = d.selectors.endpointslice.field
+ options.LabelSelector = d.selectors.endpointslice.label
+ return e.Watch(ctx, options)
+ },
}
+ informer = d.newEndpointSlicesByNodeInformer(elw, &disv1.EndpointSlice{})
s := d.client.CoreV1().Services(namespace)
slw := &cache.ListWatch{
@@ -479,9 +448,10 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
eps := NewEndpointSlice(
log.With(d.logger, "role", "endpointslice"),
informer,
- cache.NewSharedInformer(slw, &apiv1.Service{}, resyncDisabled),
- cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled),
+ d.mustNewSharedInformer(slw, &apiv1.Service{}, resyncDisabled),
+ d.mustNewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled),
nodeInf,
+ d.metrics.eventCount,
)
d.discoverers = append(d.discoverers, eps)
go eps.endpointSliceInf.Run(ctx.Done())
@@ -538,9 +508,10 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
eps := NewEndpoints(
log.With(d.logger, "role", "endpoint"),
d.newEndpointsByNodeInformer(elw),
- cache.NewSharedInformer(slw, &apiv1.Service{}, resyncDisabled),
- cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled),
+ d.mustNewSharedInformer(slw, &apiv1.Service{}, resyncDisabled),
+ d.mustNewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled),
nodeInf,
+ d.metrics.eventCount,
)
d.discoverers = append(d.discoverers, eps)
go eps.endpointsInf.Run(ctx.Done())
@@ -572,6 +543,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
log.With(d.logger, "role", "pod"),
d.newPodsByNodeInformer(plw),
nodeInformer,
+ d.metrics.eventCount,
)
d.discoverers = append(d.discoverers, pod)
go pod.podInf.Run(ctx.Done())
@@ -593,71 +565,40 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
}
svc := NewService(
log.With(d.logger, "role", "service"),
- cache.NewSharedInformer(slw, &apiv1.Service{}, resyncDisabled),
+ d.mustNewSharedInformer(slw, &apiv1.Service{}, resyncDisabled),
+ d.metrics.eventCount,
)
d.discoverers = append(d.discoverers, svc)
go svc.informer.Run(ctx.Done())
}
case RoleIngress:
- // Check "networking.k8s.io/v1" availability with retries.
- // If "v1" is not available, use "networking.k8s.io/v1beta1" for backward compatibility
- var v1Supported bool
- if retryOnError(ctx, 10*time.Second,
- func() (err error) {
- v1Supported, err = checkNetworkingV1Supported(d.client)
- if err != nil {
- level.Error(d.logger).Log("msg", "Failed to check networking.k8s.io/v1 availability", "err", err)
- }
- return err
- },
- ) {
- d.Unlock()
- return
- }
-
for _, namespace := range namespaces {
var informer cache.SharedInformer
- if v1Supported {
- i := d.client.NetworkingV1().Ingresses(namespace)
- ilw := &cache.ListWatch{
- ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
- options.FieldSelector = d.selectors.ingress.field
- options.LabelSelector = d.selectors.ingress.label
- return i.List(ctx, options)
- },
- WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
- options.FieldSelector = d.selectors.ingress.field
- options.LabelSelector = d.selectors.ingress.label
- return i.Watch(ctx, options)
- },
- }
- informer = cache.NewSharedInformer(ilw, &networkv1.Ingress{}, resyncDisabled)
- } else {
- i := d.client.NetworkingV1beta1().Ingresses(namespace)
- ilw := &cache.ListWatch{
- ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
- options.FieldSelector = d.selectors.ingress.field
- options.LabelSelector = d.selectors.ingress.label
- return i.List(ctx, options)
- },
- WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
- options.FieldSelector = d.selectors.ingress.field
- options.LabelSelector = d.selectors.ingress.label
- return i.Watch(ctx, options)
- },
- }
- informer = cache.NewSharedInformer(ilw, &v1beta1.Ingress{}, resyncDisabled)
+ i := d.client.NetworkingV1().Ingresses(namespace)
+ ilw := &cache.ListWatch{
+ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+ options.FieldSelector = d.selectors.ingress.field
+ options.LabelSelector = d.selectors.ingress.label
+ return i.List(ctx, options)
+ },
+ WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+ options.FieldSelector = d.selectors.ingress.field
+ options.LabelSelector = d.selectors.ingress.label
+ return i.Watch(ctx, options)
+ },
}
+ informer = d.mustNewSharedInformer(ilw, &networkv1.Ingress{}, resyncDisabled)
ingress := NewIngress(
log.With(d.logger, "role", "ingress"),
informer,
+ d.metrics.eventCount,
)
d.discoverers = append(d.discoverers, ingress)
go ingress.informer.Run(ctx.Done())
}
case RoleNode:
nodeInformer := d.newNodeInformer(ctx)
- node := NewNode(log.With(d.logger, "role", "node"), nodeInformer)
+ node := NewNode(log.With(d.logger, "role", "node"), nodeInformer, d.metrics.eventCount)
d.discoverers = append(d.discoverers, node)
go node.informer.Run(ctx.Done())
default:
@@ -709,20 +650,6 @@ func retryOnError(ctx context.Context, interval time.Duration, f func() error) (
}
}
-func checkNetworkingV1Supported(client kubernetes.Interface) (bool, error) {
- k8sVer, err := client.Discovery().ServerVersion()
- if err != nil {
- return false, err
- }
- semVer, err := utilversion.ParseSemantic(k8sVer.String())
- if err != nil {
- return false, err
- }
- // networking.k8s.io/v1 is available since Kubernetes v1.19
- // https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.19.md
- return semVer.Major() >= 1 && semVer.Minor() >= 19, nil
-}
-
func (d *Discovery) newNodeInformer(ctx context.Context) cache.SharedInformer {
nlw := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
@@ -736,7 +663,7 @@ func (d *Discovery) newNodeInformer(ctx context.Context) cache.SharedInformer {
return d.client.CoreV1().Nodes().Watch(ctx, options)
},
}
- return cache.NewSharedInformer(nlw, &apiv1.Node{}, resyncDisabled)
+ return d.mustNewSharedInformer(nlw, &apiv1.Node{}, resyncDisabled)
}
func (d *Discovery) newPodsByNodeInformer(plw *cache.ListWatch) cache.SharedIndexInformer {
@@ -751,13 +678,28 @@ func (d *Discovery) newPodsByNodeInformer(plw *cache.ListWatch) cache.SharedInde
}
}
- return cache.NewSharedIndexInformer(plw, &apiv1.Pod{}, resyncDisabled, indexers)
+ return d.mustNewSharedIndexInformer(plw, &apiv1.Pod{}, resyncDisabled, indexers)
}
func (d *Discovery) newEndpointsByNodeInformer(plw *cache.ListWatch) cache.SharedIndexInformer {
indexers := make(map[string]cache.IndexFunc)
+ indexers[podIndex] = func(obj interface{}) ([]string, error) {
+ e, ok := obj.(*apiv1.Endpoints)
+ if !ok {
+ return nil, fmt.Errorf("object is not endpoints")
+ }
+ var pods []string
+ for _, target := range e.Subsets {
+ for _, addr := range target.Addresses {
+ if addr.TargetRef != nil && addr.TargetRef.Kind == "Pod" {
+ pods = append(pods, namespacedName(addr.TargetRef.Namespace, addr.TargetRef.Name))
+ }
+ }
+ }
+ return pods, nil
+ }
if !d.attachMetadata.Node {
- return cache.NewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers)
+ return d.mustNewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers)
}
indexers[nodeIndex] = func(obj interface{}) ([]string, error) {
@@ -783,13 +725,13 @@ func (d *Discovery) newEndpointsByNodeInformer(plw *cache.ListWatch) cache.Share
return nodes, nil
}
- return cache.NewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers)
+ return d.mustNewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers)
}
func (d *Discovery) newEndpointSlicesByNodeInformer(plw *cache.ListWatch, object runtime.Object) cache.SharedIndexInformer {
indexers := make(map[string]cache.IndexFunc)
if !d.attachMetadata.Node {
- cache.NewSharedIndexInformer(plw, &disv1.EndpointSlice{}, resyncDisabled, indexers)
+ return d.mustNewSharedIndexInformer(plw, object, resyncDisabled, indexers)
}
indexers[nodeIndex] = func(obj interface{}) ([]string, error) {
@@ -808,19 +750,6 @@ func (d *Discovery) newEndpointSlicesByNodeInformer(plw *cache.ListWatch, object
}
}
}
- case *disv1beta1.EndpointSlice:
- for _, target := range e.Endpoints {
- if target.TargetRef != nil {
- switch target.TargetRef.Kind {
- case "Pod":
- if target.NodeName != nil {
- nodes = append(nodes, *target.NodeName)
- }
- case "Node":
- nodes = append(nodes, target.TargetRef.Name)
- }
- }
- }
default:
return nil, fmt.Errorf("object is not an endpointslice")
}
@@ -828,22 +757,32 @@ func (d *Discovery) newEndpointSlicesByNodeInformer(plw *cache.ListWatch, object
return nodes, nil
}
- return cache.NewSharedIndexInformer(plw, object, resyncDisabled, indexers)
+ return d.mustNewSharedIndexInformer(plw, object, resyncDisabled, indexers)
}
-func checkDiscoveryV1Supported(client kubernetes.Interface) (bool, error) {
- k8sVer, err := client.Discovery().ServerVersion()
- if err != nil {
- return false, err
+func (d *Discovery) informerWatchErrorHandler(r *cache.Reflector, err error) {
+ d.metrics.failuresCount.Inc()
+ cache.DefaultWatchErrorHandler(r, err)
+}
+
+func (d *Discovery) mustNewSharedInformer(lw cache.ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration) cache.SharedInformer {
+ informer := cache.NewSharedInformer(lw, exampleObject, defaultEventHandlerResyncPeriod)
+ // Invoking SetWatchErrorHandler should fail only if the informer has been started beforehand.
+ // Such a scenario would suggest an incorrect use of the API, thus the panic.
+ if err := informer.SetWatchErrorHandler(d.informerWatchErrorHandler); err != nil {
+ panic(err)
}
- semVer, err := utilversion.ParseSemantic(k8sVer.String())
- if err != nil {
- return false, err
+ return informer
+}
+
+func (d *Discovery) mustNewSharedIndexInformer(lw cache.ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+ informer := cache.NewSharedIndexInformer(lw, exampleObject, defaultEventHandlerResyncPeriod, indexers)
+ // Invoking SetWatchErrorHandler should fail only if the informer has been started beforehand.
+ // Such a scenario would suggest an incorrect use of the API, thus the panic.
+ if err := informer.SetWatchErrorHandler(d.informerWatchErrorHandler); err != nil {
+ panic(err)
}
- // The discovery.k8s.io/v1beta1 API version of EndpointSlice will no longer be served in v1.25.
- // discovery.k8s.io/v1 is available since Kubernetes v1.21
- // https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25
- return semVer.Major() >= 1 && semVer.Minor() >= 21, nil
+ return informer
}
func addObjectMetaLabels(labelSet model.LabelSet, objectMeta metav1.ObjectMeta, role Role) {
@@ -861,3 +800,7 @@ func addObjectMetaLabels(labelSet model.LabelSet, objectMeta metav1.ObjectMeta,
labelSet[model.LabelName(metaLabelPrefix+string(role)+"_annotationpresent_"+ln)] = presentValue
}
}
+
+func namespacedName(namespace, name string) string {
+ return namespace + "/" + name
+}
diff --git a/discovery/kubernetes/kubernetes_test.go b/discovery/kubernetes/kubernetes_test.go
index d0ed4c6ca1..a026366502 100644
--- a/discovery/kubernetes/kubernetes_test.go
+++ b/discovery/kubernetes/kubernetes_test.go
@@ -21,14 +21,20 @@ import (
"time"
"github.com/go-kit/log"
+ prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/require"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/version"
+ "k8s.io/apimachinery/pkg/watch"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
+ kubetesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
+ "github.com/prometheus/client_golang/prometheus"
+
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/util/testutil"
@@ -40,7 +46,7 @@ func TestMain(m *testing.M) {
// makeDiscovery creates a kubernetes.Discovery instance for testing.
func makeDiscovery(role Role, nsDiscovery NamespaceDiscovery, objects ...runtime.Object) (*Discovery, kubernetes.Interface) {
- return makeDiscoveryWithVersion(role, nsDiscovery, "v1.22.0", objects...)
+ return makeDiscoveryWithVersion(role, nsDiscovery, "v1.25.0", objects...)
}
// makeDiscoveryWithVersion creates a kubernetes.Discovery instance with the specified kubernetes version for testing.
@@ -49,13 +55,30 @@ func makeDiscoveryWithVersion(role Role, nsDiscovery NamespaceDiscovery, k8sVer
fakeDiscovery, _ := clientset.Discovery().(*fakediscovery.FakeDiscovery)
fakeDiscovery.FakedServerVersion = &version.Info{GitVersion: k8sVer}
- return &Discovery{
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := newDiscovererMetrics(reg, refreshMetrics)
+ err := metrics.Register()
+ if err != nil {
+ panic(err)
+ }
+ // TODO(ptodev): Unregister the metrics at the end of the test.
+
+ kubeMetrics, ok := metrics.(*kubernetesMetrics)
+ if !ok {
+ panic("invalid discovery metrics type")
+ }
+
+ d := &Discovery{
client: clientset,
logger: log.NewNopLogger(),
role: role,
namespaceDiscovery: &nsDiscovery,
ownNamespace: "own-ns",
- }, clientset
+ metrics: kubeMetrics,
+ }
+
+ return d, clientset
}
// makeDiscoveryWithMetadata creates a kubernetes.Discovery instance with the specified metadata config.
@@ -109,17 +132,11 @@ func (d k8sDiscoveryTest) Run(t *testing.T) {
}
resChan := make(chan map[string]*targetgroup.Group)
- go readResultWithTimeout(t, ch, d.expectedMaxItems, time.Second, resChan)
+ go readResultWithTimeout(t, ctx, ch, d.expectedMaxItems, time.Second, resChan)
dd, ok := d.discovery.(hasSynced)
- if !ok {
- t.Errorf("discoverer does not implement hasSynced interface")
- return
- }
- if !cache.WaitForCacheSync(ctx.Done(), dd.hasSynced) {
- t.Errorf("discoverer failed to sync: %v", dd)
- return
- }
+ require.True(t, ok, "discoverer does not implement hasSynced interface")
+ require.True(t, cache.WaitForCacheSync(ctx.Done(), dd.hasSynced), "discoverer failed to sync: %v", dd)
if d.afterStart != nil {
d.afterStart()
@@ -128,13 +145,18 @@ func (d k8sDiscoveryTest) Run(t *testing.T) {
if d.expectedRes != nil {
res := <-resChan
requireTargetGroups(t, d.expectedRes, res)
+ } else {
+ // Stop readResultWithTimeout and wait for it.
+ cancel()
+ <-resChan
}
}
// readResultWithTimeout reads all targetgroups from channel with timeout.
// It merges targetgroups by source and sends the result to result channel.
-func readResultWithTimeout(t *testing.T, ch <-chan []*targetgroup.Group, max int, timeout time.Duration, resChan chan<- map[string]*targetgroup.Group) {
+func readResultWithTimeout(t *testing.T, ctx context.Context, ch <-chan []*targetgroup.Group, maxGroups int, stopAfter time.Duration, resChan chan<- map[string]*targetgroup.Group) {
res := make(map[string]*targetgroup.Group)
+ timeout := time.After(stopAfter)
Loop:
for {
select {
@@ -145,15 +167,18 @@ Loop:
}
res[tg.Source] = tg
}
- if len(res) == max {
+ if len(res) == maxGroups {
// Reached max target groups we may get, break fast.
break Loop
}
- case <-time.After(timeout):
+ case <-timeout:
// Because we use queue, an object that is created then
// deleted or updated may be processed only once.
// So possibly we may skip events, timed out here.
- t.Logf("timed out, got %d (max: %d) items, some events are skipped", len(res), max)
+ t.Logf("timed out, got %d (max: %d) items, some events are skipped", len(res), maxGroups)
+ break Loop
+ case <-ctx.Done():
+ t.Logf("stopped, got %d (max: %d) items", len(res), maxGroups)
break Loop
}
}
@@ -260,36 +285,38 @@ func TestRetryOnError(t *testing.T) {
}
}
-func TestCheckNetworkingV1Supported(t *testing.T) {
+func TestFailuresCountMetric(t *testing.T) {
tests := []struct {
- version string
- wantSupported bool
- wantErr bool
+ role Role
+ minFailedWatches int
}{
- {version: "v1.18.0", wantSupported: false, wantErr: false},
- {version: "v1.18.1", wantSupported: false, wantErr: false},
- // networking v1 is supported since Kubernetes v1.19
- {version: "v1.19.0", wantSupported: true, wantErr: false},
- {version: "v1.20.0-beta.2", wantSupported: true, wantErr: false},
- // error patterns
- {version: "", wantSupported: false, wantErr: true},
- {version: "<>", wantSupported: false, wantErr: true},
+ {RoleNode, 1},
+ {RolePod, 1},
+ {RoleService, 1},
+ {RoleEndpoint, 3},
+ {RoleEndpointSlice, 3},
+ {RoleIngress, 1},
}
for _, tc := range tests {
tc := tc
- t.Run(tc.version, func(t *testing.T) {
- clientset := fake.NewSimpleClientset()
- fakeDiscovery, _ := clientset.Discovery().(*fakediscovery.FakeDiscovery)
- fakeDiscovery.FakedServerVersion = &version.Info{GitVersion: tc.version}
- supported, err := checkNetworkingV1Supported(clientset)
-
- if tc.wantErr {
- require.Error(t, err)
- } else {
- require.NoError(t, err)
- }
- require.Equal(t, tc.wantSupported, supported)
+ t.Run(string(tc.role), func(t *testing.T) {
+ t.Parallel()
+
+ n, c := makeDiscovery(tc.role, NamespaceDiscovery{})
+ // The counter is initialized and no failures at the beginning.
+ require.Equal(t, float64(0), prom_testutil.ToFloat64(n.metrics.failuresCount))
+
+ // Simulate an error on watch requests.
+ c.Discovery().(*fakediscovery.FakeDiscovery).PrependWatchReactor("*", func(action kubetesting.Action) (bool, watch.Interface, error) {
+ return true, nil, apierrors.NewUnauthorized("unauthorized")
+ })
+
+ // Start the discovery.
+ k8sDiscoveryTest{discovery: n}.Run(t)
+
+ // At least the errors of the initial watches should be caught (watches are retried on errors).
+ require.GreaterOrEqual(t, prom_testutil.ToFloat64(n.metrics.failuresCount), float64(tc.minFailedWatches))
})
}
}
diff --git a/discovery/kubernetes/metrics.go b/discovery/kubernetes/metrics.go
new file mode 100644
index 0000000000..fe419bc782
--- /dev/null
+++ b/discovery/kubernetes/metrics.go
@@ -0,0 +1,86 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package kubernetes
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*kubernetesMetrics)(nil)
+
+type kubernetesMetrics struct {
+ eventCount *prometheus.CounterVec
+ failuresCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &kubernetesMetrics{
+ eventCount: prometheus.NewCounterVec(
+ prometheus.CounterOpts{
+ Namespace: discovery.KubernetesMetricsNamespace,
+ Name: "events_total",
+ Help: "The number of Kubernetes events handled.",
+ },
+ []string{"role", "event"},
+ ),
+ failuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Namespace: discovery.KubernetesMetricsNamespace,
+ Name: "failures_total",
+ Help: "The number of failed WATCH/LIST requests.",
+ },
+ ),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.eventCount,
+ m.failuresCount,
+ })
+
+ // Initialize metric vectors.
+ for _, role := range []string{
+ RoleEndpointSlice.String(),
+ RoleEndpoint.String(),
+ RoleNode.String(),
+ RolePod.String(),
+ RoleService.String(),
+ RoleIngress.String(),
+ } {
+ for _, evt := range []string{
+ MetricLabelRoleAdd,
+ MetricLabelRoleDelete,
+ MetricLabelRoleUpdate,
+ } {
+ m.eventCount.WithLabelValues(role, evt)
+ }
+ }
+
+ m.failuresCount.Add(0)
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *kubernetesMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *kubernetesMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/kubernetes/node.go b/discovery/kubernetes/node.go
index 6a20e7b1f2..74d87e22c4 100644
--- a/discovery/kubernetes/node.go
+++ b/discovery/kubernetes/node.go
@@ -22,6 +22,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
apiv1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
@@ -35,12 +36,6 @@ const (
NodeLegacyHostIP = "LegacyHostIP"
)
-var (
- nodeAddCount = eventCount.WithLabelValues("node", "add")
- nodeUpdateCount = eventCount.WithLabelValues("node", "update")
- nodeDeleteCount = eventCount.WithLabelValues("node", "delete")
-)
-
// Node discovers Kubernetes nodes.
type Node struct {
logger log.Logger
@@ -50,11 +45,22 @@ type Node struct {
}
// NewNode returns a new node discovery.
-func NewNode(l log.Logger, inf cache.SharedInformer) *Node {
+func NewNode(l log.Logger, inf cache.SharedInformer, eventCount *prometheus.CounterVec) *Node {
if l == nil {
l = log.NewNopLogger()
}
- n := &Node{logger: l, informer: inf, store: inf.GetStore(), queue: workqueue.NewNamed("node")}
+
+ nodeAddCount := eventCount.WithLabelValues(RoleNode.String(), MetricLabelRoleAdd)
+ nodeUpdateCount := eventCount.WithLabelValues(RoleNode.String(), MetricLabelRoleUpdate)
+ nodeDeleteCount := eventCount.WithLabelValues(RoleNode.String(), MetricLabelRoleDelete)
+
+ n := &Node{
+ logger: l,
+ informer: inf,
+ store: inf.GetStore(),
+ queue: workqueue.NewNamed(RoleNode.String()),
+ }
+
_, err := n.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(o interface{}) {
nodeAddCount.Inc()
@@ -96,7 +102,7 @@ func (n *Node) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
}
go func() {
- for n.process(ctx, ch) { // nolint:revive
+ for n.process(ctx, ch) {
}
}()
@@ -202,7 +208,7 @@ func (n *Node) buildNode(node *apiv1.Node) *targetgroup.Group {
// 5. NodeLegacyHostIP
// 6. NodeHostName
//
-// Derived from k8s.io/kubernetes/pkg/util/node/node.go
+// Derived from k8s.io/kubernetes/pkg/util/node/node.go.
func nodeAddress(node *apiv1.Node) (string, map[apiv1.NodeAddressType][]string, error) {
m := map[apiv1.NodeAddressType][]string{}
for _, a := range node.Status.Addresses {
diff --git a/discovery/kubernetes/pod.go b/discovery/kubernetes/pod.go
index 74f74c1f75..02990e415f 100644
--- a/discovery/kubernetes/pod.go
+++ b/discovery/kubernetes/pod.go
@@ -23,6 +23,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,12 +33,9 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-const nodeIndex = "node"
-
-var (
- podAddCount = eventCount.WithLabelValues("pod", "add")
- podUpdateCount = eventCount.WithLabelValues("pod", "update")
- podDeleteCount = eventCount.WithLabelValues("pod", "delete")
+const (
+ nodeIndex = "node"
+ podIndex = "pod"
)
// Pod discovers new pod targets.
@@ -51,18 +49,22 @@ type Pod struct {
}
// NewPod creates a new pod discovery.
-func NewPod(l log.Logger, pods cache.SharedIndexInformer, nodes cache.SharedInformer) *Pod {
+func NewPod(l log.Logger, pods cache.SharedIndexInformer, nodes cache.SharedInformer, eventCount *prometheus.CounterVec) *Pod {
if l == nil {
l = log.NewNopLogger()
}
+ podAddCount := eventCount.WithLabelValues(RolePod.String(), MetricLabelRoleAdd)
+ podDeleteCount := eventCount.WithLabelValues(RolePod.String(), MetricLabelRoleDelete)
+ podUpdateCount := eventCount.WithLabelValues(RolePod.String(), MetricLabelRoleUpdate)
+
p := &Pod{
podInf: pods,
nodeInf: nodes,
withNodeMetadata: nodes != nil,
store: pods.GetStore(),
logger: l,
- queue: workqueue.NewNamed("pod"),
+ queue: workqueue.NewNamed(RolePod.String()),
}
_, err := p.podInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(o interface{}) {
@@ -131,7 +133,7 @@ func (p *Pod) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
}
go func() {
- for p.process(ctx, ch) { // nolint:revive
+ for p.process(ctx, ch) {
}
}()
@@ -327,7 +329,7 @@ func podSource(pod *apiv1.Pod) string {
}
func podSourceFromNamespaceAndName(namespace, name string) string {
- return "pod/" + namespace + "/" + name
+ return "pod/" + namespacedName(namespace, name)
}
func podReady(pod *apiv1.Pod) model.LabelValue {
diff --git a/discovery/kubernetes/service.go b/discovery/kubernetes/service.go
index 7addf0054e..51204a5a1a 100644
--- a/discovery/kubernetes/service.go
+++ b/discovery/kubernetes/service.go
@@ -22,6 +22,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
apiv1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
@@ -30,12 +31,6 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-var (
- svcAddCount = eventCount.WithLabelValues("service", "add")
- svcUpdateCount = eventCount.WithLabelValues("service", "update")
- svcDeleteCount = eventCount.WithLabelValues("service", "delete")
-)
-
// Service implements discovery of Kubernetes services.
type Service struct {
logger log.Logger
@@ -45,11 +40,22 @@ type Service struct {
}
// NewService returns a new service discovery.
-func NewService(l log.Logger, inf cache.SharedInformer) *Service {
+func NewService(l log.Logger, inf cache.SharedInformer, eventCount *prometheus.CounterVec) *Service {
if l == nil {
l = log.NewNopLogger()
}
- s := &Service{logger: l, informer: inf, store: inf.GetStore(), queue: workqueue.NewNamed("service")}
+
+ svcAddCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleAdd)
+ svcUpdateCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleUpdate)
+ svcDeleteCount := eventCount.WithLabelValues(RoleService.String(), MetricLabelRoleDelete)
+
+ s := &Service{
+ logger: l,
+ informer: inf,
+ store: inf.GetStore(),
+ queue: workqueue.NewNamed(RoleService.String()),
+ }
+
_, err := s.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(o interface{}) {
svcAddCount.Inc()
@@ -91,7 +97,7 @@ func (s *Service) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
}
go func() {
- for s.process(ctx, ch) { // nolint:revive
+ for s.process(ctx, ch) {
}
}()
diff --git a/discovery/legacymanager/manager.go b/discovery/legacymanager/manager.go
deleted file mode 100644
index 87823f4010..0000000000
--- a/discovery/legacymanager/manager.go
+++ /dev/null
@@ -1,357 +0,0 @@
-// Copyright 2016 The Prometheus Authors
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package legacymanager
-
-import (
- "context"
- "fmt"
- "reflect"
- "sync"
- "time"
-
- "github.com/go-kit/log"
- "github.com/go-kit/log/level"
- "github.com/prometheus/client_golang/prometheus"
-
- "github.com/prometheus/prometheus/discovery"
- "github.com/prometheus/prometheus/discovery/targetgroup"
-)
-
-var (
- failedConfigs = prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "prometheus_sd_failed_configs",
- Help: "Current number of service discovery configurations that failed to load.",
- },
- []string{"name"},
- )
- discoveredTargets = prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "prometheus_sd_discovered_targets",
- Help: "Current number of discovered targets.",
- },
- []string{"name", "config"},
- )
- receivedUpdates = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_received_updates_total",
- Help: "Total number of update events received from the SD providers.",
- },
- []string{"name"},
- )
- delayedUpdates = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_updates_delayed_total",
- Help: "Total number of update events that couldn't be sent immediately.",
- },
- []string{"name"},
- )
- sentUpdates = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_updates_total",
- Help: "Total number of update events sent to the SD consumers.",
- },
- []string{"name"},
- )
-)
-
-func RegisterMetrics() {
- prometheus.MustRegister(failedConfigs, discoveredTargets, receivedUpdates, delayedUpdates, sentUpdates)
-}
-
-type poolKey struct {
- setName string
- provider string
-}
-
-// provider holds a Discoverer instance, its configuration and its subscribers.
-type provider struct {
- name string
- d discovery.Discoverer
- subs []string
- config interface{}
-}
-
-// NewManager is the Discovery Manager constructor.
-func NewManager(ctx context.Context, logger log.Logger, options ...func(*Manager)) *Manager {
- if logger == nil {
- logger = log.NewNopLogger()
- }
- mgr := &Manager{
- logger: logger,
- syncCh: make(chan map[string][]*targetgroup.Group),
- targets: make(map[poolKey]map[string]*targetgroup.Group),
- discoverCancel: []context.CancelFunc{},
- ctx: ctx,
- updatert: 5 * time.Second,
- triggerSend: make(chan struct{}, 1),
- }
- for _, option := range options {
- option(mgr)
- }
- return mgr
-}
-
-// Name sets the name of the manager.
-func Name(n string) func(*Manager) {
- return func(m *Manager) {
- m.mtx.Lock()
- defer m.mtx.Unlock()
- m.name = n
- }
-}
-
-// Manager maintains a set of discovery providers and sends each update to a map channel.
-// Targets are grouped by the target set name.
-type Manager struct {
- logger log.Logger
- name string
- mtx sync.RWMutex
- ctx context.Context
- discoverCancel []context.CancelFunc
-
- // Some Discoverers(eg. k8s) send only the updates for a given target group
- // so we use map[tg.Source]*targetgroup.Group to know which group to update.
- targets map[poolKey]map[string]*targetgroup.Group
- // providers keeps track of SD providers.
- providers []*provider
- // The sync channel sends the updates as a map where the key is the job value from the scrape config.
- syncCh chan map[string][]*targetgroup.Group
-
- // How long to wait before sending updates to the channel. The variable
- // should only be modified in unit tests.
- updatert time.Duration
-
- // The triggerSend channel signals to the manager that new updates have been received from providers.
- triggerSend chan struct{}
-}
-
-// Run starts the background processing
-func (m *Manager) Run() error {
- go m.sender()
- for range m.ctx.Done() {
- m.cancelDiscoverers()
- return m.ctx.Err()
- }
- return nil
-}
-
-// SyncCh returns a read only channel used by all the clients to receive target updates.
-func (m *Manager) SyncCh() <-chan map[string][]*targetgroup.Group {
- return m.syncCh
-}
-
-// ApplyConfig removes all running discovery providers and starts new ones using the provided config.
-func (m *Manager) ApplyConfig(cfg map[string]discovery.Configs) error {
- m.mtx.Lock()
- defer m.mtx.Unlock()
-
- for pk := range m.targets {
- if _, ok := cfg[pk.setName]; !ok {
- discoveredTargets.DeleteLabelValues(m.name, pk.setName)
- }
- }
- m.cancelDiscoverers()
- m.targets = make(map[poolKey]map[string]*targetgroup.Group)
- m.providers = nil
- m.discoverCancel = nil
-
- failedCount := 0
- for name, scfg := range cfg {
- failedCount += m.registerProviders(scfg, name)
- discoveredTargets.WithLabelValues(m.name, name).Set(0)
- }
- failedConfigs.WithLabelValues(m.name).Set(float64(failedCount))
-
- for _, prov := range m.providers {
- m.startProvider(m.ctx, prov)
- }
-
- return nil
-}
-
-// StartCustomProvider is used for sdtool. Only use this if you know what you're doing.
-func (m *Manager) StartCustomProvider(ctx context.Context, name string, worker discovery.Discoverer) {
- p := &provider{
- name: name,
- d: worker,
- subs: []string{name},
- }
- m.providers = append(m.providers, p)
- m.startProvider(ctx, p)
-}
-
-func (m *Manager) startProvider(ctx context.Context, p *provider) {
- level.Debug(m.logger).Log("msg", "Starting provider", "provider", p.name, "subs", fmt.Sprintf("%v", p.subs))
- ctx, cancel := context.WithCancel(ctx)
- updates := make(chan []*targetgroup.Group)
-
- m.discoverCancel = append(m.discoverCancel, cancel)
-
- go p.d.Run(ctx, updates)
- go m.updater(ctx, p, updates)
-}
-
-func (m *Manager) updater(ctx context.Context, p *provider, updates chan []*targetgroup.Group) {
- for {
- select {
- case <-ctx.Done():
- return
- case tgs, ok := <-updates:
- receivedUpdates.WithLabelValues(m.name).Inc()
- if !ok {
- level.Debug(m.logger).Log("msg", "Discoverer channel closed", "provider", p.name)
- return
- }
-
- for _, s := range p.subs {
- m.updateGroup(poolKey{setName: s, provider: p.name}, tgs)
- }
-
- select {
- case m.triggerSend <- struct{}{}:
- default:
- }
- }
- }
-}
-
-func (m *Manager) sender() {
- ticker := time.NewTicker(m.updatert)
- defer ticker.Stop()
-
- for {
- select {
- case <-m.ctx.Done():
- return
- case <-ticker.C: // Some discoverers send updates too often so we throttle these with the ticker.
- select {
- case <-m.triggerSend:
- sentUpdates.WithLabelValues(m.name).Inc()
- select {
- case m.syncCh <- m.allGroups():
- default:
- delayedUpdates.WithLabelValues(m.name).Inc()
- level.Debug(m.logger).Log("msg", "Discovery receiver's channel was full so will retry the next cycle")
- select {
- case m.triggerSend <- struct{}{}:
- default:
- }
- }
- default:
- }
- }
- }
-}
-
-func (m *Manager) cancelDiscoverers() {
- for _, c := range m.discoverCancel {
- c()
- }
-}
-
-func (m *Manager) updateGroup(poolKey poolKey, tgs []*targetgroup.Group) {
- m.mtx.Lock()
- defer m.mtx.Unlock()
-
- if _, ok := m.targets[poolKey]; !ok {
- m.targets[poolKey] = make(map[string]*targetgroup.Group)
- }
- for _, tg := range tgs {
- if tg != nil { // Some Discoverers send nil target group so need to check for it to avoid panics.
- m.targets[poolKey][tg.Source] = tg
- }
- }
-}
-
-func (m *Manager) allGroups() map[string][]*targetgroup.Group {
- m.mtx.RLock()
- defer m.mtx.RUnlock()
-
- tSets := map[string][]*targetgroup.Group{}
- n := map[string]int{}
- for pkey, tsets := range m.targets {
- for _, tg := range tsets {
- // Even if the target group 'tg' is empty we still need to send it to the 'Scrape manager'
- // to signal that it needs to stop all scrape loops for this target set.
- tSets[pkey.setName] = append(tSets[pkey.setName], tg)
- n[pkey.setName] += len(tg.Targets)
- }
- }
- for setName, v := range n {
- discoveredTargets.WithLabelValues(m.name, setName).Set(float64(v))
- }
- return tSets
-}
-
-// registerProviders returns a number of failed SD config.
-func (m *Manager) registerProviders(cfgs discovery.Configs, setName string) int {
- var (
- failed int
- added bool
- )
- add := func(cfg discovery.Config) {
- for _, p := range m.providers {
- if reflect.DeepEqual(cfg, p.config) {
- p.subs = append(p.subs, setName)
- added = true
- return
- }
- }
- typ := cfg.Name()
- d, err := cfg.NewDiscoverer(discovery.DiscovererOptions{
- Logger: log.With(m.logger, "discovery", typ, "config", setName),
- })
- if err != nil {
- level.Error(m.logger).Log("msg", "Cannot create service discovery", "err", err, "type", typ, "config", setName)
- failed++
- return
- }
- m.providers = append(m.providers, &provider{
- name: fmt.Sprintf("%s/%d", typ, len(m.providers)),
- d: d,
- config: cfg,
- subs: []string{setName},
- })
- added = true
- }
- for _, cfg := range cfgs {
- add(cfg)
- }
- if !added {
- // Add an empty target group to force the refresh of the corresponding
- // scrape pool and to notify the receiver that this target set has no
- // current targets.
- // It can happen because the combined set of SD configurations is empty
- // or because we fail to instantiate all the SD configurations.
- add(discovery.StaticConfig{{}})
- }
- return failed
-}
-
-// StaticProvider holds a list of target groups that never change.
-type StaticProvider struct {
- TargetGroups []*targetgroup.Group
-}
-
-// Run implements the Worker interface.
-func (sd *StaticProvider) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
- // We still have to consider that the consumer exits right away in which case
- // the context will be canceled.
- select {
- case ch <- sd.TargetGroups:
- case <-ctx.Done():
- }
- close(ch)
-}
diff --git a/discovery/legacymanager/manager_test.go b/discovery/legacymanager/manager_test.go
deleted file mode 100644
index 13b84e6e36..0000000000
--- a/discovery/legacymanager/manager_test.go
+++ /dev/null
@@ -1,1136 +0,0 @@
-// Copyright 2016 The Prometheus Authors
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package legacymanager
-
-import (
- "context"
- "fmt"
- "sort"
- "strconv"
- "testing"
- "time"
-
- "github.com/go-kit/log"
- client_testutil "github.com/prometheus/client_golang/prometheus/testutil"
- "github.com/prometheus/common/model"
- "github.com/stretchr/testify/require"
-
- "github.com/prometheus/prometheus/discovery"
- "github.com/prometheus/prometheus/discovery/targetgroup"
- "github.com/prometheus/prometheus/util/testutil"
-)
-
-func TestMain(m *testing.M) {
- testutil.TolerantVerifyLeak(m)
-}
-
-// TestTargetUpdatesOrder checks that the target updates are received in the expected order.
-func TestTargetUpdatesOrder(t *testing.T) {
- // The order by which the updates are send is determined by the interval passed to the mock discovery adapter
- // Final targets array is ordered alphabetically by the name of the discoverer.
- // For example discoverer "A" with targets "t2,t3" and discoverer "B" with targets "t1,t2" will result in "t2,t3,t1,t2" after the merge.
- testCases := []struct {
- title string
- updates map[string][]update
- expectedTargets [][]*targetgroup.Group
- }{
- {
- title: "Single TP no updates",
- updates: map[string][]update{
- "tp1": {},
- },
- expectedTargets: nil,
- },
- {
- title: "Multiple TPs no updates",
- updates: map[string][]update{
- "tp1": {},
- "tp2": {},
- "tp3": {},
- },
- expectedTargets: nil,
- },
- {
- title: "Single TP empty initials",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{},
- interval: 5 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {},
- },
- },
- {
- title: "Multiple TPs empty initials",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{},
- interval: 5 * time.Millisecond,
- },
- },
- "tp2": {
- {
- targetGroups: []targetgroup.Group{},
- interval: 200 * time.Millisecond,
- },
- },
- "tp3": {
- {
- targetGroups: []targetgroup.Group{},
- interval: 100 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {},
- {},
- {},
- },
- },
- {
- title: "Single TP initials only",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- },
- {
- title: "Multiple TPs initials only",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- },
- "tp2": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- },
- interval: 10 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- }, {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- },
- },
- },
- {
- title: "Single TP initials followed by empty updates",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- interval: 0,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{},
- },
- },
- interval: 10 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{},
- },
- },
- },
- },
- {
- title: "Single TP initials and new groups",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- interval: 0,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- {
- Source: "tp1_group3",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- interval: 10 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- {
- Source: "tp1_group3",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- },
- },
- {
- title: "Multiple TPs initials and new groups",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- interval: 10 * time.Millisecond,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group3",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group4",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- },
- interval: 500 * time.Millisecond,
- },
- },
- "tp2": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "5"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "6"}},
- },
- },
- interval: 100 * time.Millisecond,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp2_group3",
- Targets: []model.LabelSet{{"__instance__": "7"}},
- },
- {
- Source: "tp2_group4",
- Targets: []model.LabelSet{{"__instance__": "8"}},
- },
- },
- interval: 10 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "5"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "6"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "5"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "6"}},
- },
- {
- Source: "tp2_group3",
- Targets: []model.LabelSet{{"__instance__": "7"}},
- },
- {
- Source: "tp2_group4",
- Targets: []model.LabelSet{{"__instance__": "8"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- {
- Source: "tp1_group3",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group4",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "5"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "6"}},
- },
- {
- Source: "tp2_group3",
- Targets: []model.LabelSet{{"__instance__": "7"}},
- },
- {
- Source: "tp2_group4",
- Targets: []model.LabelSet{{"__instance__": "8"}},
- },
- },
- },
- },
- {
- title: "One TP initials arrive after other TP updates.",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- interval: 10 * time.Millisecond,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- },
- interval: 150 * time.Millisecond,
- },
- },
- "tp2": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "5"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "6"}},
- },
- },
- interval: 200 * time.Millisecond,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "7"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "8"}},
- },
- },
- interval: 100 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "5"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "6"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- {
- Source: "tp2_group1",
- Targets: []model.LabelSet{{"__instance__": "7"}},
- },
- {
- Source: "tp2_group2",
- Targets: []model.LabelSet{{"__instance__": "8"}},
- },
- },
- },
- },
-
- {
- title: "Single TP empty update in between",
- updates: map[string][]update{
- "tp1": {
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- interval: 30 * time.Millisecond,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{},
- },
- },
- interval: 10 * time.Millisecond,
- },
- {
- targetGroups: []targetgroup.Group{
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- },
- interval: 300 * time.Millisecond,
- },
- },
- },
- expectedTargets: [][]*targetgroup.Group{
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{},
- },
- },
- {
- {
- Source: "tp1_group1",
- Targets: []model.LabelSet{{"__instance__": "3"}},
- },
- {
- Source: "tp1_group2",
- Targets: []model.LabelSet{{"__instance__": "4"}},
- },
- },
- },
- },
- }
-
- for i, tc := range testCases {
- tc := tc
- t.Run(tc.title, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- discoveryManager := NewManager(ctx, log.NewNopLogger())
- discoveryManager.updatert = 100 * time.Millisecond
-
- var totalUpdatesCount int
- for _, up := range tc.updates {
- if len(up) > 0 {
- totalUpdatesCount += len(up)
- }
- }
- provUpdates := make(chan []*targetgroup.Group, totalUpdatesCount)
-
- for _, up := range tc.updates {
- go newMockDiscoveryProvider(up...).Run(ctx, provUpdates)
- }
-
- for x := 0; x < totalUpdatesCount; x++ {
- select {
- case <-ctx.Done():
- t.Fatalf("%d: no update arrived within the timeout limit", x)
- case tgs := <-provUpdates:
- discoveryManager.updateGroup(poolKey{setName: strconv.Itoa(i), provider: tc.title}, tgs)
- for _, got := range discoveryManager.allGroups() {
- assertEqualGroups(t, got, tc.expectedTargets[x])
- }
- }
- }
- })
- }
-}
-
-func assertEqualGroups(t *testing.T, got, expected []*targetgroup.Group) {
- t.Helper()
-
- // Need to sort by the groups's source as the received order is not guaranteed.
- sort.Sort(byGroupSource(got))
- sort.Sort(byGroupSource(expected))
-
- require.Equal(t, expected, got)
-}
-
-func staticConfig(addrs ...string) discovery.StaticConfig {
- var cfg discovery.StaticConfig
- for i, addr := range addrs {
- cfg = append(cfg, &targetgroup.Group{
- Source: fmt.Sprint(i),
- Targets: []model.LabelSet{
- {model.AddressLabel: model.LabelValue(addr)},
- },
- })
- }
- return cfg
-}
-
-func verifyPresence(t *testing.T, tSets map[poolKey]map[string]*targetgroup.Group, poolKey poolKey, label string, present bool) {
- t.Helper()
- if _, ok := tSets[poolKey]; !ok {
- t.Fatalf("'%s' should be present in Pool keys: %v", poolKey, tSets)
- return
- }
-
- match := false
- var mergedTargets string
- for _, targetGroup := range tSets[poolKey] {
- for _, l := range targetGroup.Targets {
- mergedTargets = mergedTargets + " " + l.String()
- if l.String() == label {
- match = true
- }
- }
- }
- if match != present {
- msg := ""
- if !present {
- msg = "not"
- }
- t.Fatalf("%q should %s be present in Targets labels: %q", label, msg, mergedTargets)
- }
-}
-
-func TestTargetSetRecreatesTargetGroupsEveryRun(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
- discoveryManager.updatert = 100 * time.Millisecond
- go discoveryManager.Run()
-
- c := map[string]discovery.Configs{
- "prometheus": {
- staticConfig("foo:9090", "bar:9090"),
- },
- }
- discoveryManager.ApplyConfig(c)
-
- <-discoveryManager.SyncCh()
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"foo:9090\"}", true)
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"bar:9090\"}", true)
-
- c["prometheus"] = discovery.Configs{
- staticConfig("foo:9090"),
- }
- discoveryManager.ApplyConfig(c)
-
- <-discoveryManager.SyncCh()
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"foo:9090\"}", true)
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"bar:9090\"}", false)
-}
-
-func TestDiscovererConfigs(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
- discoveryManager.updatert = 100 * time.Millisecond
- go discoveryManager.Run()
-
- c := map[string]discovery.Configs{
- "prometheus": {
- staticConfig("foo:9090", "bar:9090"),
- staticConfig("baz:9090"),
- },
- }
- discoveryManager.ApplyConfig(c)
-
- <-discoveryManager.SyncCh()
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"foo:9090\"}", true)
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"bar:9090\"}", true)
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/1"}, "{__address__=\"baz:9090\"}", true)
-}
-
-// TestTargetSetRecreatesEmptyStaticConfigs ensures that reloading a config file after
-// removing all targets from the static_configs sends an update with empty targetGroups.
-// This is required to signal the receiver that this target set has no current targets.
-func TestTargetSetRecreatesEmptyStaticConfigs(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
- discoveryManager.updatert = 100 * time.Millisecond
- go discoveryManager.Run()
-
- c := map[string]discovery.Configs{
- "prometheus": {
- staticConfig("foo:9090"),
- },
- }
- discoveryManager.ApplyConfig(c)
-
- <-discoveryManager.SyncCh()
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"foo:9090\"}", true)
-
- c["prometheus"] = discovery.Configs{
- discovery.StaticConfig{{}},
- }
- discoveryManager.ApplyConfig(c)
-
- <-discoveryManager.SyncCh()
-
- pkey := poolKey{setName: "prometheus", provider: "static/0"}
- targetGroups, ok := discoveryManager.targets[pkey]
- if !ok {
- t.Fatalf("'%v' should be present in target groups", pkey)
- }
- group, ok := targetGroups[""]
- if !ok {
- t.Fatalf("missing '' key in target groups %v", targetGroups)
- }
-
- if len(group.Targets) != 0 {
- t.Fatalf("Invalid number of targets: expected 0, got %d", len(group.Targets))
- }
-}
-
-func TestIdenticalConfigurationsAreCoalesced(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- discoveryManager := NewManager(ctx, nil)
- discoveryManager.updatert = 100 * time.Millisecond
- go discoveryManager.Run()
-
- c := map[string]discovery.Configs{
- "prometheus": {
- staticConfig("foo:9090"),
- },
- "prometheus2": {
- staticConfig("foo:9090"),
- },
- }
- discoveryManager.ApplyConfig(c)
-
- <-discoveryManager.SyncCh()
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus", provider: "static/0"}, "{__address__=\"foo:9090\"}", true)
- verifyPresence(t, discoveryManager.targets, poolKey{setName: "prometheus2", provider: "static/0"}, "{__address__=\"foo:9090\"}", true)
- if len(discoveryManager.providers) != 1 {
- t.Fatalf("Invalid number of providers: expected 1, got %d", len(discoveryManager.providers))
- }
-}
-
-func TestApplyConfigDoesNotModifyStaticTargets(t *testing.T) {
- originalConfig := discovery.Configs{
- staticConfig("foo:9090", "bar:9090", "baz:9090"),
- }
- processedConfig := discovery.Configs{
- staticConfig("foo:9090", "bar:9090", "baz:9090"),
- }
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
- discoveryManager.updatert = 100 * time.Millisecond
- go discoveryManager.Run()
-
- cfgs := map[string]discovery.Configs{
- "prometheus": processedConfig,
- }
- discoveryManager.ApplyConfig(cfgs)
- <-discoveryManager.SyncCh()
-
- for _, cfg := range cfgs {
- require.Equal(t, originalConfig, cfg)
- }
-}
-
-type errorConfig struct{ err error }
-
-func (e errorConfig) Name() string { return "error" }
-func (e errorConfig) NewDiscoverer(discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return nil, e.err
-}
-
-func TestGaugeFailedConfigs(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
- discoveryManager.updatert = 100 * time.Millisecond
- go discoveryManager.Run()
-
- c := map[string]discovery.Configs{
- "prometheus": {
- errorConfig{fmt.Errorf("tests error 0")},
- errorConfig{fmt.Errorf("tests error 1")},
- errorConfig{fmt.Errorf("tests error 2")},
- },
- }
- discoveryManager.ApplyConfig(c)
- <-discoveryManager.SyncCh()
-
- failedCount := client_testutil.ToFloat64(failedConfigs)
- if failedCount != 3 {
- t.Fatalf("Expected to have 3 failed configs, got: %v", failedCount)
- }
-
- c["prometheus"] = discovery.Configs{
- staticConfig("foo:9090"),
- }
- discoveryManager.ApplyConfig(c)
- <-discoveryManager.SyncCh()
-
- failedCount = client_testutil.ToFloat64(failedConfigs)
- if failedCount != 0 {
- t.Fatalf("Expected to get no failed config, got: %v", failedCount)
- }
-}
-
-func TestCoordinationWithReceiver(t *testing.T) {
- updateDelay := 100 * time.Millisecond
-
- type expect struct {
- delay time.Duration
- tgs map[string][]*targetgroup.Group
- }
-
- testCases := []struct {
- title string
- providers map[string]discovery.Discoverer
- expected []expect
- }{
- {
- title: "Receiver should get all updates even when one provider closes its channel",
- providers: map[string]discovery.Discoverer{
- "once1": &onceProvider{
- tgs: []*targetgroup.Group{
- {
- Source: "tg1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- },
- "mock1": newMockDiscoveryProvider(
- update{
- interval: 2 * updateDelay,
- targetGroups: []targetgroup.Group{
- {
- Source: "tg2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- ),
- },
- expected: []expect{
- {
- tgs: map[string][]*targetgroup.Group{
- "once1": {
- {
- Source: "tg1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- },
- },
- {
- tgs: map[string][]*targetgroup.Group{
- "once1": {
- {
- Source: "tg1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- "mock1": {
- {
- Source: "tg2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- },
- },
- },
- {
- title: "Receiver should get all updates even when the channel is blocked",
- providers: map[string]discovery.Discoverer{
- "mock1": newMockDiscoveryProvider(
- update{
- targetGroups: []targetgroup.Group{
- {
- Source: "tg1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- },
- update{
- interval: 4 * updateDelay,
- targetGroups: []targetgroup.Group{
- {
- Source: "tg2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- ),
- },
- expected: []expect{
- {
- delay: 2 * updateDelay,
- tgs: map[string][]*targetgroup.Group{
- "mock1": {
- {
- Source: "tg1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- },
- },
- },
- {
- delay: 4 * updateDelay,
- tgs: map[string][]*targetgroup.Group{
- "mock1": {
- {
- Source: "tg1",
- Targets: []model.LabelSet{{"__instance__": "1"}},
- },
- {
- Source: "tg2",
- Targets: []model.LabelSet{{"__instance__": "2"}},
- },
- },
- },
- },
- },
- },
- }
-
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.title, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- mgr := NewManager(ctx, nil)
- mgr.updatert = updateDelay
- go mgr.Run()
-
- for name, p := range tc.providers {
- mgr.StartCustomProvider(ctx, name, p)
- }
-
- for i, expected := range tc.expected {
- time.Sleep(expected.delay)
- select {
- case <-ctx.Done():
- t.Fatalf("step %d: no update received in the expected timeframe", i)
- case tgs, ok := <-mgr.SyncCh():
- if !ok {
- t.Fatalf("step %d: discovery manager channel is closed", i)
- }
- if len(tgs) != len(expected.tgs) {
- t.Fatalf("step %d: target groups mismatch, got: %d, expected: %d\ngot: %#v\nexpected: %#v",
- i, len(tgs), len(expected.tgs), tgs, expected.tgs)
- }
- for k := range expected.tgs {
- if _, ok := tgs[k]; !ok {
- t.Fatalf("step %d: target group not found: %s\ngot: %#v", i, k, tgs)
- }
- assertEqualGroups(t, tgs[k], expected.tgs[k])
- }
- }
- }
- })
- }
-}
-
-type update struct {
- targetGroups []targetgroup.Group
- interval time.Duration
-}
-
-type mockdiscoveryProvider struct {
- updates []update
-}
-
-func newMockDiscoveryProvider(updates ...update) mockdiscoveryProvider {
- tp := mockdiscoveryProvider{
- updates: updates,
- }
- return tp
-}
-
-func (tp mockdiscoveryProvider) Run(ctx context.Context, upCh chan<- []*targetgroup.Group) {
- for _, u := range tp.updates {
- if u.interval > 0 {
- select {
- case <-ctx.Done():
- return
- case <-time.After(u.interval):
- }
- }
- tgs := make([]*targetgroup.Group, len(u.targetGroups))
- for i := range u.targetGroups {
- tgs[i] = &u.targetGroups[i]
- }
- upCh <- tgs
- }
- <-ctx.Done()
-}
-
-// byGroupSource implements sort.Interface so we can sort by the Source field.
-type byGroupSource []*targetgroup.Group
-
-func (a byGroupSource) Len() int { return len(a) }
-func (a byGroupSource) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a byGroupSource) Less(i, j int) bool { return a[i].Source < a[j].Source }
-
-// onceProvider sends updates once (if any) and closes the update channel.
-type onceProvider struct {
- tgs []*targetgroup.Group
-}
-
-func (o onceProvider) Run(_ context.Context, ch chan<- []*targetgroup.Group) {
- if len(o.tgs) > 0 {
- ch <- o.tgs
- }
- close(ch)
-}
diff --git a/discovery/legacymanager/registry.go b/discovery/legacymanager/registry.go
deleted file mode 100644
index 955705394d..0000000000
--- a/discovery/legacymanager/registry.go
+++ /dev/null
@@ -1,261 +0,0 @@
-// Copyright 2020 The Prometheus Authors
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package legacymanager
-
-import (
- "errors"
- "fmt"
- "reflect"
- "sort"
- "strconv"
- "strings"
- "sync"
-
- "gopkg.in/yaml.v2"
-
- "github.com/prometheus/prometheus/discovery"
- "github.com/prometheus/prometheus/discovery/targetgroup"
-)
-
-const (
- configFieldPrefix = "AUTO_DISCOVERY_"
- staticConfigsKey = "static_configs"
- staticConfigsFieldName = configFieldPrefix + staticConfigsKey
-)
-
-var (
- configNames = make(map[string]discovery.Config)
- configFieldNames = make(map[reflect.Type]string)
- configFields []reflect.StructField
-
- configTypesMu sync.Mutex
- configTypes = make(map[reflect.Type]reflect.Type)
-
- emptyStructType = reflect.TypeOf(struct{}{})
- configsType = reflect.TypeOf(discovery.Configs{})
-)
-
-// RegisterConfig registers the given Config type for YAML marshaling and unmarshaling.
-func RegisterConfig(config discovery.Config) {
- registerConfig(config.Name()+"_sd_configs", reflect.TypeOf(config), config)
-}
-
-func init() {
- // N.B.: static_configs is the only Config type implemented by default.
- // All other types are registered at init by their implementing packages.
- elemTyp := reflect.TypeOf(&targetgroup.Group{})
- registerConfig(staticConfigsKey, elemTyp, discovery.StaticConfig{})
-}
-
-func registerConfig(yamlKey string, elemType reflect.Type, config discovery.Config) {
- name := config.Name()
- if _, ok := configNames[name]; ok {
- panic(fmt.Sprintf("discovery: Config named %q is already registered", name))
- }
- configNames[name] = config
-
- fieldName := configFieldPrefix + yamlKey // Field must be exported.
- configFieldNames[elemType] = fieldName
-
- // Insert fields in sorted order.
- i := sort.Search(len(configFields), func(k int) bool {
- return fieldName < configFields[k].Name
- })
- configFields = append(configFields, reflect.StructField{}) // Add empty field at end.
- copy(configFields[i+1:], configFields[i:]) // Shift fields to the right.
- configFields[i] = reflect.StructField{ // Write new field in place.
- Name: fieldName,
- Type: reflect.SliceOf(elemType),
- Tag: reflect.StructTag(`yaml:"` + yamlKey + `,omitempty"`),
- }
-}
-
-func getConfigType(out reflect.Type) reflect.Type {
- configTypesMu.Lock()
- defer configTypesMu.Unlock()
- if typ, ok := configTypes[out]; ok {
- return typ
- }
- // Initial exported fields map one-to-one.
- var fields []reflect.StructField
- for i, n := 0, out.NumField(); i < n; i++ {
- switch field := out.Field(i); {
- case field.PkgPath == "" && field.Type != configsType:
- fields = append(fields, field)
- default:
- fields = append(fields, reflect.StructField{
- Name: "_" + field.Name, // Field must be unexported.
- PkgPath: out.PkgPath(),
- Type: emptyStructType,
- })
- }
- }
- // Append extra config fields on the end.
- fields = append(fields, configFields...)
- typ := reflect.StructOf(fields)
- configTypes[out] = typ
- return typ
-}
-
-// UnmarshalYAMLWithInlineConfigs helps implement yaml.Unmarshal for structs
-// that have a Configs field that should be inlined.
-func UnmarshalYAMLWithInlineConfigs(out interface{}, unmarshal func(interface{}) error) error {
- outVal := reflect.ValueOf(out)
- if outVal.Kind() != reflect.Ptr {
- return fmt.Errorf("discovery: can only unmarshal into a struct pointer: %T", out)
- }
- outVal = outVal.Elem()
- if outVal.Kind() != reflect.Struct {
- return fmt.Errorf("discovery: can only unmarshal into a struct pointer: %T", out)
- }
- outTyp := outVal.Type()
-
- cfgTyp := getConfigType(outTyp)
- cfgPtr := reflect.New(cfgTyp)
- cfgVal := cfgPtr.Elem()
-
- // Copy shared fields (defaults) to dynamic value.
- var configs *discovery.Configs
- for i, n := 0, outVal.NumField(); i < n; i++ {
- if outTyp.Field(i).Type == configsType {
- configs = outVal.Field(i).Addr().Interface().(*discovery.Configs)
- continue
- }
- if cfgTyp.Field(i).PkgPath != "" {
- continue // Field is unexported: ignore.
- }
- cfgVal.Field(i).Set(outVal.Field(i))
- }
- if configs == nil {
- return fmt.Errorf("discovery: Configs field not found in type: %T", out)
- }
-
- // Unmarshal into dynamic value.
- if err := unmarshal(cfgPtr.Interface()); err != nil {
- return replaceYAMLTypeError(err, cfgTyp, outTyp)
- }
-
- // Copy shared fields from dynamic value.
- for i, n := 0, outVal.NumField(); i < n; i++ {
- if cfgTyp.Field(i).PkgPath != "" {
- continue // Field is unexported: ignore.
- }
- outVal.Field(i).Set(cfgVal.Field(i))
- }
-
- var err error
- *configs, err = readConfigs(cfgVal, outVal.NumField())
- return err
-}
-
-func readConfigs(structVal reflect.Value, startField int) (discovery.Configs, error) {
- var (
- configs discovery.Configs
- targets []*targetgroup.Group
- )
- for i, n := startField, structVal.NumField(); i < n; i++ {
- field := structVal.Field(i)
- if field.Kind() != reflect.Slice {
- panic("discovery: internal error: field is not a slice")
- }
- for k := 0; k < field.Len(); k++ {
- val := field.Index(k)
- if val.IsZero() || (val.Kind() == reflect.Ptr && val.Elem().IsZero()) {
- key := configFieldNames[field.Type().Elem()]
- key = strings.TrimPrefix(key, configFieldPrefix)
- return nil, fmt.Errorf("empty or null section in %s", key)
- }
- switch c := val.Interface().(type) {
- case *targetgroup.Group:
- // Add index to the static config target groups for unique identification
- // within scrape pool.
- c.Source = strconv.Itoa(len(targets))
- // Coalesce multiple static configs into a single static config.
- targets = append(targets, c)
- case discovery.Config:
- configs = append(configs, c)
- default:
- panic("discovery: internal error: slice element is not a Config")
- }
- }
- }
- if len(targets) > 0 {
- configs = append(configs, discovery.StaticConfig(targets))
- }
- return configs, nil
-}
-
-// MarshalYAMLWithInlineConfigs helps implement yaml.Marshal for structs
-// that have a Configs field that should be inlined.
-func MarshalYAMLWithInlineConfigs(in interface{}) (interface{}, error) {
- inVal := reflect.ValueOf(in)
- for inVal.Kind() == reflect.Ptr {
- inVal = inVal.Elem()
- }
- inTyp := inVal.Type()
-
- cfgTyp := getConfigType(inTyp)
- cfgPtr := reflect.New(cfgTyp)
- cfgVal := cfgPtr.Elem()
-
- // Copy shared fields to dynamic value.
- var configs *discovery.Configs
- for i, n := 0, inTyp.NumField(); i < n; i++ {
- if inTyp.Field(i).Type == configsType {
- configs = inVal.Field(i).Addr().Interface().(*discovery.Configs)
- }
- if cfgTyp.Field(i).PkgPath != "" {
- continue // Field is unexported: ignore.
- }
- cfgVal.Field(i).Set(inVal.Field(i))
- }
- if configs == nil {
- return nil, fmt.Errorf("discovery: Configs field not found in type: %T", in)
- }
-
- if err := writeConfigs(cfgVal, *configs); err != nil {
- return nil, err
- }
-
- return cfgPtr.Interface(), nil
-}
-
-func writeConfigs(structVal reflect.Value, configs discovery.Configs) error {
- targets := structVal.FieldByName(staticConfigsFieldName).Addr().Interface().(*[]*targetgroup.Group)
- for _, c := range configs {
- if sc, ok := c.(discovery.StaticConfig); ok {
- *targets = append(*targets, sc...)
- continue
- }
- fieldName, ok := configFieldNames[reflect.TypeOf(c)]
- if !ok {
- return fmt.Errorf("discovery: cannot marshal unregistered Config type: %T", c)
- }
- field := structVal.FieldByName(fieldName)
- field.Set(reflect.Append(field, reflect.ValueOf(c)))
- }
- return nil
-}
-
-func replaceYAMLTypeError(err error, oldTyp, newTyp reflect.Type) error {
- var e *yaml.TypeError
- if errors.As(err, &e) {
- oldStr := oldTyp.String()
- newStr := newTyp.String()
- for i, s := range e.Errors {
- e.Errors[i] = strings.ReplaceAll(s, oldStr, newStr)
- }
- }
- return err
-}
diff --git a/discovery/linode/linode.go b/discovery/linode/linode.go
index 63213c87b2..634a6b1d4b 100644
--- a/discovery/linode/linode.go
+++ b/discovery/linode/linode.go
@@ -51,6 +51,7 @@ const (
linodeLabelStatus = linodeLabel + "status"
linodeLabelTags = linodeLabel + "tags"
linodeLabelGroup = linodeLabel + "group"
+ linodeLabelGPUs = linodeLabel + "gpus"
linodeLabelHypervisor = linodeLabel + "hypervisor"
linodeLabelBackups = linodeLabel + "backups"
linodeLabelSpecsDiskBytes = linodeLabel + "specs_disk_bytes"
@@ -58,32 +59,28 @@ const (
linodeLabelSpecsVCPUs = linodeLabel + "specs_vcpus"
linodeLabelSpecsTransferBytes = linodeLabel + "specs_transfer_bytes"
linodeLabelExtraIPs = linodeLabel + "extra_ips"
+ linodeLabelIPv6Ranges = linodeLabel + "ipv6_ranges"
// This is our events filter; when polling for changes, we care only about
// events since our last refresh.
- // Docs: https://www.linode.com/docs/api/account/#events-list
+ // Docs: https://www.linode.com/docs/api/account/#events-list.
filterTemplate = `{"created": {"+gte": "%s"}}`
+
+ // Optional region filtering.
+ regionFilterTemplate = `{"region": "%s"}`
)
// DefaultSDConfig is the default Linode SD configuration.
-var (
- DefaultSDConfig = SDConfig{
- TagSeparator: ",",
- Port: 80,
- RefreshInterval: model.Duration(60 * time.Second),
- HTTPClientConfig: config.DefaultHTTPClientConfig,
- }
-
- failuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Name: "prometheus_sd_linode_failures_total",
- Help: "Number of Linode service discovery refresh failures.",
- })
-)
+var DefaultSDConfig = SDConfig{
+ TagSeparator: ",",
+ Port: 80,
+ Region: "",
+ RefreshInterval: model.Duration(60 * time.Second),
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+}
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(failuresCount)
}
// SDConfig is the configuration for Linode based service discovery.
@@ -93,6 +90,12 @@ type SDConfig struct {
RefreshInterval model.Duration `yaml:"refresh_interval"`
Port int `yaml:"port"`
TagSeparator string `yaml:"tag_separator,omitempty"`
+ Region string `yaml:"region,omitempty"`
+}
+
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
}
// Name returns the name of the Config.
@@ -100,7 +103,7 @@ func (*SDConfig) Name() string { return "linode" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -125,21 +128,30 @@ type Discovery struct {
*refresh.Discovery
client *linodego.Client
port int
+ region string
tagSeparator string
lastRefreshTimestamp time.Time
pollCount int
lastResults []*targetgroup.Group
eventPollingEnabled bool
+ metrics *linodeMetrics
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*linodeMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
d := &Discovery{
port: conf.Port,
+ region: conf.Region,
tagSeparator: conf.TagSeparator,
pollCount: 0,
lastRefreshTimestamp: time.Now().UTC(),
eventPollingEnabled: true,
+ metrics: m,
}
rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "linode_sd")
@@ -157,10 +169,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
d.client = &client
d.Discovery = refresh.NewDiscovery(
- logger,
- "linode",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "linode",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -171,12 +186,12 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
if d.lastResults != nil && d.eventPollingEnabled {
// Check to see if there have been any events. If so, refresh our data.
- opts := linodego.ListOptions{
+ eventsOpts := linodego.ListOptions{
PageOptions: &linodego.PageOptions{Page: 1},
PageSize: 25,
Filter: fmt.Sprintf(filterTemplate, d.lastRefreshTimestamp.Format("2006-01-02T15:04:05")),
}
- events, err := d.client.ListEvents(ctx, &opts)
+ events, err := d.client.ListEvents(ctx, &eventsOpts)
if err != nil {
var e *linodego.Error
if errors.As(err, &e) && e.Code == http.StatusUnauthorized {
@@ -217,18 +232,42 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro
tg := &targetgroup.Group{
Source: "Linode",
}
+ // We need 3 of these because Linodego writes into the structure during pagination
+ listInstancesOpts := linodego.ListOptions{
+ PageSize: 500,
+ }
+ listIPAddressesOpts := linodego.ListOptions{
+ PageSize: 500,
+ }
+ listIPv6RangesOpts := linodego.ListOptions{
+ PageSize: 500,
+ }
+
+ // If region filter provided, use it to constrain results.
+ if d.region != "" {
+ listInstancesOpts.Filter = fmt.Sprintf(regionFilterTemplate, d.region)
+ listIPAddressesOpts.Filter = fmt.Sprintf(regionFilterTemplate, d.region)
+ listIPv6RangesOpts.Filter = fmt.Sprintf(regionFilterTemplate, d.region)
+ }
// Gather all linode instances.
- instances, err := d.client.ListInstances(ctx, &linodego.ListOptions{PageSize: 500})
+ instances, err := d.client.ListInstances(ctx, &listInstancesOpts)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, err
}
// Gather detailed IP address info for all IPs on all linode instances.
- detailedIPs, err := d.client.ListIPAddresses(ctx, &linodego.ListOptions{PageSize: 500})
+ detailedIPs, err := d.client.ListIPAddresses(ctx, &listIPAddressesOpts)
+ if err != nil {
+ d.metrics.failuresCount.Inc()
+ return nil, err
+ }
+
+ // Gather detailed IPv6 Range info for all linode instances.
+ ipv6RangeList, err := d.client.ListIPv6Ranges(ctx, &listIPv6RangesOpts)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, err
}
@@ -241,7 +280,7 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro
privateIPv4, publicIPv4, publicIPv6 string
privateIPv4RDNS, publicIPv4RDNS, publicIPv6RDNS string
backupsStatus string
- extraIPs []string
+ extraIPs, ipv6Ranges []string
)
for _, ip := range instance.IPv4 {
@@ -269,17 +308,23 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro
}
if instance.IPv6 != "" {
+ slaac := strings.Split(instance.IPv6, "/")[0]
for _, detailedIP := range detailedIPs {
- if detailedIP.Address != strings.Split(instance.IPv6, "/")[0] {
+ if detailedIP.Address != slaac {
continue
}
-
publicIPv6 = detailedIP.Address
if detailedIP.RDNS != "" && detailedIP.RDNS != "null" {
publicIPv6RDNS = detailedIP.RDNS
}
}
+ for _, ipv6Range := range ipv6RangeList {
+ if ipv6Range.RouteTarget != slaac {
+ continue
+ }
+ ipv6Ranges = append(ipv6Ranges, fmt.Sprintf("%s/%d", ipv6Range.Range, ipv6Range.Prefix))
+ }
}
if instance.Backups.Enabled {
@@ -289,7 +334,7 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro
}
labels := model.LabelSet{
- linodeLabelID: model.LabelValue(fmt.Sprintf("%d", instance.ID)),
+ linodeLabelID: model.LabelValue(strconv.Itoa(instance.ID)),
linodeLabelName: model.LabelValue(instance.Label),
linodeLabelImage: model.LabelValue(instance.Image),
linodeLabelPrivateIPv4: model.LabelValue(privateIPv4),
@@ -302,12 +347,13 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro
linodeLabelType: model.LabelValue(instance.Type),
linodeLabelStatus: model.LabelValue(instance.Status),
linodeLabelGroup: model.LabelValue(instance.Group),
+ linodeLabelGPUs: model.LabelValue(strconv.Itoa(instance.Specs.GPUs)),
linodeLabelHypervisor: model.LabelValue(instance.Hypervisor),
linodeLabelBackups: model.LabelValue(backupsStatus),
- linodeLabelSpecsDiskBytes: model.LabelValue(fmt.Sprintf("%d", int64(instance.Specs.Disk)<<20)),
- linodeLabelSpecsMemoryBytes: model.LabelValue(fmt.Sprintf("%d", int64(instance.Specs.Memory)<<20)),
- linodeLabelSpecsVCPUs: model.LabelValue(fmt.Sprintf("%d", instance.Specs.VCPUs)),
- linodeLabelSpecsTransferBytes: model.LabelValue(fmt.Sprintf("%d", int64(instance.Specs.Transfer)<<20)),
+ linodeLabelSpecsDiskBytes: model.LabelValue(strconv.FormatInt(int64(instance.Specs.Disk)<<20, 10)),
+ linodeLabelSpecsMemoryBytes: model.LabelValue(strconv.FormatInt(int64(instance.Specs.Memory)<<20, 10)),
+ linodeLabelSpecsVCPUs: model.LabelValue(strconv.Itoa(instance.Specs.VCPUs)),
+ linodeLabelSpecsTransferBytes: model.LabelValue(strconv.FormatInt(int64(instance.Specs.Transfer)<<20, 10)),
}
addr := net.JoinHostPort(publicIPv4, strconv.FormatUint(uint64(d.port), 10))
@@ -322,12 +368,20 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro
if len(extraIPs) > 0 {
// This instance has more than one of at least one type of IP address (public, private,
- // IPv4, IPv6, etc. We provide those extra IPs found here just like we do for instance
+ // IPv4,etc. We provide those extra IPs found here just like we do for instance
// tags, we surround a separated list with the tagSeparator config.
ips := d.tagSeparator + strings.Join(extraIPs, d.tagSeparator) + d.tagSeparator
labels[linodeLabelExtraIPs] = model.LabelValue(ips)
}
+ if len(ipv6Ranges) > 0 {
+ // This instance has more than one IPv6 Ranges routed to it we provide these
+ // Ranges found here just like we do for instance tags, we surround a separated
+ // list with the tagSeparator config.
+ ips := d.tagSeparator + strings.Join(ipv6Ranges, d.tagSeparator) + d.tagSeparator
+ labels[linodeLabelIPv6Ranges] = model.LabelValue(ips)
+ }
+
tg.Targets = append(tg.Targets, labels)
}
return []*targetgroup.Group{tg}, nil
diff --git a/discovery/linode/linode_test.go b/discovery/linode/linode_test.go
index 67eb8198e8..3c10650653 100644
--- a/discovery/linode/linode_test.go
+++ b/discovery/linode/linode_test.go
@@ -20,152 +20,244 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
-)
-type LinodeSDTestSuite struct {
- Mock *SDMock
-}
+ "github.com/prometheus/prometheus/discovery"
+)
-func (s *LinodeSDTestSuite) TearDownSuite() {
- s.Mock.ShutdownServer()
-}
+func TestLinodeSDRefresh(t *testing.T) {
+ sdmock := NewSDMock(t)
+ sdmock.Setup()
-func (s *LinodeSDTestSuite) SetupTest(t *testing.T) {
- s.Mock = NewSDMock(t)
- s.Mock.Setup()
+ tests := map[string]struct {
+ region string
+ targetCount int
+ want []model.LabelSet
+ }{
+ "no_region": {region: "", targetCount: 4, want: []model.LabelSet{
+ {
+ "__address__": model.LabelValue("45.33.82.151:80"),
+ "__meta_linode_instance_id": model.LabelValue("26838044"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-1"),
+ "__meta_linode_image": model.LabelValue("linode/arch"),
+ "__meta_linode_private_ipv4": model.LabelValue("192.168.170.51"),
+ "__meta_linode_public_ipv4": model.LabelValue("45.33.82.151"),
+ "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:1382"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li1028-151.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("us-east"),
+ "__meta_linode_type": model.LabelValue("g6-standard-2"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"),
+ "__meta_linode_specs_vcpus": model.LabelValue("2"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"),
+ "__meta_linode_extra_ips": model.LabelValue(",96.126.108.16,192.168.201.25,"),
+ },
+ {
+ "__address__": model.LabelValue("139.162.196.43:80"),
+ "__meta_linode_instance_id": model.LabelValue("26848419"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-2"),
+ "__meta_linode_image": model.LabelValue("linode/debian10"),
+ "__meta_linode_private_ipv4": model.LabelValue(""),
+ "__meta_linode_public_ipv4": model.LabelValue("139.162.196.43"),
+ "__meta_linode_public_ipv6": model.LabelValue("2a01:7e00::f03c:92ff:fe1a:9976"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li1359-43.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("eu-west"),
+ "__meta_linode_type": model.LabelValue("g6-standard-2"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"),
+ "__meta_linode_specs_vcpus": model.LabelValue("2"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"),
+ },
+ {
+ "__address__": model.LabelValue("192.53.120.25:80"),
+ "__meta_linode_instance_id": model.LabelValue("26837938"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-3"),
+ "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"),
+ "__meta_linode_private_ipv4": model.LabelValue(""),
+ "__meta_linode_public_ipv4": model.LabelValue("192.53.120.25"),
+ "__meta_linode_public_ipv6": model.LabelValue("2600:3c04::f03c:92ff:fe1a:fb68"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li2216-25.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("ca-central"),
+ "__meta_linode_type": model.LabelValue("g6-standard-1"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("53687091200"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("2147483648"),
+ "__meta_linode_specs_vcpus": model.LabelValue("1"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("2097152000"),
+ "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c04:e001:456::/64,"),
+ },
+ {
+ "__address__": model.LabelValue("66.228.47.103:80"),
+ "__meta_linode_instance_id": model.LabelValue("26837992"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-4"),
+ "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"),
+ "__meta_linode_private_ipv4": model.LabelValue("192.168.148.94"),
+ "__meta_linode_public_ipv4": model.LabelValue("66.228.47.103"),
+ "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:fb4c"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li328-103.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("us-east"),
+ "__meta_linode_type": model.LabelValue("g6-nanode-1"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("26843545600"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("1073741824"),
+ "__meta_linode_specs_vcpus": model.LabelValue("1"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("1048576000"),
+ "__meta_linode_extra_ips": model.LabelValue(",172.104.18.104,"),
+ "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c03:e000:123::/64,"),
+ },
+ }},
+ "us-east": {region: "us-east", targetCount: 2, want: []model.LabelSet{
+ {
+ "__address__": model.LabelValue("45.33.82.151:80"),
+ "__meta_linode_instance_id": model.LabelValue("26838044"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-1"),
+ "__meta_linode_image": model.LabelValue("linode/arch"),
+ "__meta_linode_private_ipv4": model.LabelValue("192.168.170.51"),
+ "__meta_linode_public_ipv4": model.LabelValue("45.33.82.151"),
+ "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:1382"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li1028-151.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("us-east"),
+ "__meta_linode_type": model.LabelValue("g6-standard-2"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"),
+ "__meta_linode_specs_vcpus": model.LabelValue("2"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"),
+ "__meta_linode_extra_ips": model.LabelValue(",96.126.108.16,192.168.201.25,"),
+ },
+ {
+ "__address__": model.LabelValue("66.228.47.103:80"),
+ "__meta_linode_instance_id": model.LabelValue("26837992"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-4"),
+ "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"),
+ "__meta_linode_private_ipv4": model.LabelValue("192.168.148.94"),
+ "__meta_linode_public_ipv4": model.LabelValue("66.228.47.103"),
+ "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:fb4c"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li328-103.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("us-east"),
+ "__meta_linode_type": model.LabelValue("g6-nanode-1"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("26843545600"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("1073741824"),
+ "__meta_linode_specs_vcpus": model.LabelValue("1"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("1048576000"),
+ "__meta_linode_extra_ips": model.LabelValue(",172.104.18.104,"),
+ "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c03:e000:123::/64,"),
+ },
+ }},
+ "us-central": {region: "ca-central", targetCount: 1, want: []model.LabelSet{
+ {
+ "__address__": model.LabelValue("192.53.120.25:80"),
+ "__meta_linode_instance_id": model.LabelValue("26837938"),
+ "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-3"),
+ "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"),
+ "__meta_linode_private_ipv4": model.LabelValue(""),
+ "__meta_linode_public_ipv4": model.LabelValue("192.53.120.25"),
+ "__meta_linode_public_ipv6": model.LabelValue("2600:3c04::f03c:92ff:fe1a:fb68"),
+ "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
+ "__meta_linode_public_ipv4_rdns": model.LabelValue("li2216-25.members.linode.com"),
+ "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
+ "__meta_linode_region": model.LabelValue("ca-central"),
+ "__meta_linode_type": model.LabelValue("g6-standard-1"),
+ "__meta_linode_status": model.LabelValue("running"),
+ "__meta_linode_tags": model.LabelValue(",monitoring,"),
+ "__meta_linode_group": model.LabelValue(""),
+ "__meta_linode_gpus": model.LabelValue("0"),
+ "__meta_linode_hypervisor": model.LabelValue("kvm"),
+ "__meta_linode_backups": model.LabelValue("disabled"),
+ "__meta_linode_specs_disk_bytes": model.LabelValue("53687091200"),
+ "__meta_linode_specs_memory_bytes": model.LabelValue("2147483648"),
+ "__meta_linode_specs_vcpus": model.LabelValue("1"),
+ "__meta_linode_specs_transfer_bytes": model.LabelValue("2097152000"),
+ "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c04:e001:456::/64,"),
+ },
+ }},
+ }
- s.Mock.HandleLinodeInstancesList()
- s.Mock.HandleLinodeNeworkingIPs()
- s.Mock.HandleLinodeAccountEvents()
-}
+ for _, tc := range tests {
+ cfg := DefaultSDConfig
+ if tc.region != "" {
+ cfg.Region = tc.region
+ }
+ cfg.HTTPClientConfig.Authorization = &config.Authorization{
+ Credentials: tokenID,
+ Type: "Bearer",
+ }
-func TestLinodeSDRefresh(t *testing.T) {
- sdmock := &LinodeSDTestSuite{}
- sdmock.SetupTest(t)
- t.Cleanup(sdmock.TearDownSuite)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
- cfg := DefaultSDConfig
- cfg.HTTPClientConfig.Authorization = &config.Authorization{
- Credentials: tokenID,
- Type: "Bearer",
- }
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
- require.NoError(t, err)
- endpoint, err := url.Parse(sdmock.Mock.Endpoint())
- require.NoError(t, err)
- d.client.SetBaseURL(endpoint.String())
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
+ require.NoError(t, err)
+ endpoint, err := url.Parse(sdmock.Endpoint())
+ require.NoError(t, err)
+ d.client.SetBaseURL(endpoint.String())
- tgs, err := d.refresh(context.Background())
- require.NoError(t, err)
+ tgs, err := d.refresh(context.Background())
+ require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
- tg := tgs[0]
- require.NotNil(t, tg)
- require.NotNil(t, tg.Targets)
- require.Equal(t, 4, len(tg.Targets))
+ tg := tgs[0]
+ require.NotNil(t, tg)
+ require.NotNil(t, tg.Targets)
+ require.Len(t, tg.Targets, tc.targetCount)
- for i, lbls := range []model.LabelSet{
- {
- "__address__": model.LabelValue("45.33.82.151:80"),
- "__meta_linode_instance_id": model.LabelValue("26838044"),
- "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-1"),
- "__meta_linode_image": model.LabelValue("linode/arch"),
- "__meta_linode_private_ipv4": model.LabelValue("192.168.170.51"),
- "__meta_linode_public_ipv4": model.LabelValue("45.33.82.151"),
- "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:1382"),
- "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
- "__meta_linode_public_ipv4_rdns": model.LabelValue("li1028-151.members.linode.com"),
- "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
- "__meta_linode_region": model.LabelValue("us-east"),
- "__meta_linode_type": model.LabelValue("g6-standard-2"),
- "__meta_linode_status": model.LabelValue("running"),
- "__meta_linode_tags": model.LabelValue(",monitoring,"),
- "__meta_linode_group": model.LabelValue(""),
- "__meta_linode_hypervisor": model.LabelValue("kvm"),
- "__meta_linode_backups": model.LabelValue("disabled"),
- "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"),
- "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"),
- "__meta_linode_specs_vcpus": model.LabelValue("2"),
- "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"),
- "__meta_linode_extra_ips": model.LabelValue(",96.126.108.16,192.168.201.25,"),
- },
- {
- "__address__": model.LabelValue("139.162.196.43:80"),
- "__meta_linode_instance_id": model.LabelValue("26848419"),
- "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-2"),
- "__meta_linode_image": model.LabelValue("linode/debian10"),
- "__meta_linode_private_ipv4": model.LabelValue(""),
- "__meta_linode_public_ipv4": model.LabelValue("139.162.196.43"),
- "__meta_linode_public_ipv6": model.LabelValue("2a01:7e00::f03c:92ff:fe1a:9976"),
- "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
- "__meta_linode_public_ipv4_rdns": model.LabelValue("li1359-43.members.linode.com"),
- "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
- "__meta_linode_region": model.LabelValue("eu-west"),
- "__meta_linode_type": model.LabelValue("g6-standard-2"),
- "__meta_linode_status": model.LabelValue("running"),
- "__meta_linode_tags": model.LabelValue(",monitoring,"),
- "__meta_linode_group": model.LabelValue(""),
- "__meta_linode_hypervisor": model.LabelValue("kvm"),
- "__meta_linode_backups": model.LabelValue("disabled"),
- "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"),
- "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"),
- "__meta_linode_specs_vcpus": model.LabelValue("2"),
- "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"),
- },
- {
- "__address__": model.LabelValue("192.53.120.25:80"),
- "__meta_linode_instance_id": model.LabelValue("26837938"),
- "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-3"),
- "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"),
- "__meta_linode_private_ipv4": model.LabelValue(""),
- "__meta_linode_public_ipv4": model.LabelValue("192.53.120.25"),
- "__meta_linode_public_ipv6": model.LabelValue("2600:3c04::f03c:92ff:fe1a:fb68"),
- "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
- "__meta_linode_public_ipv4_rdns": model.LabelValue("li2216-25.members.linode.com"),
- "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
- "__meta_linode_region": model.LabelValue("ca-central"),
- "__meta_linode_type": model.LabelValue("g6-standard-1"),
- "__meta_linode_status": model.LabelValue("running"),
- "__meta_linode_tags": model.LabelValue(",monitoring,"),
- "__meta_linode_group": model.LabelValue(""),
- "__meta_linode_hypervisor": model.LabelValue("kvm"),
- "__meta_linode_backups": model.LabelValue("disabled"),
- "__meta_linode_specs_disk_bytes": model.LabelValue("53687091200"),
- "__meta_linode_specs_memory_bytes": model.LabelValue("2147483648"),
- "__meta_linode_specs_vcpus": model.LabelValue("1"),
- "__meta_linode_specs_transfer_bytes": model.LabelValue("2097152000"),
- },
- {
- "__address__": model.LabelValue("66.228.47.103:80"),
- "__meta_linode_instance_id": model.LabelValue("26837992"),
- "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-4"),
- "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"),
- "__meta_linode_private_ipv4": model.LabelValue("192.168.148.94"),
- "__meta_linode_public_ipv4": model.LabelValue("66.228.47.103"),
- "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:fb4c"),
- "__meta_linode_private_ipv4_rdns": model.LabelValue(""),
- "__meta_linode_public_ipv4_rdns": model.LabelValue("li328-103.members.linode.com"),
- "__meta_linode_public_ipv6_rdns": model.LabelValue(""),
- "__meta_linode_region": model.LabelValue("us-east"),
- "__meta_linode_type": model.LabelValue("g6-nanode-1"),
- "__meta_linode_status": model.LabelValue("running"),
- "__meta_linode_tags": model.LabelValue(",monitoring,"),
- "__meta_linode_group": model.LabelValue(""),
- "__meta_linode_hypervisor": model.LabelValue("kvm"),
- "__meta_linode_backups": model.LabelValue("disabled"),
- "__meta_linode_specs_disk_bytes": model.LabelValue("26843545600"),
- "__meta_linode_specs_memory_bytes": model.LabelValue("1073741824"),
- "__meta_linode_specs_vcpus": model.LabelValue("1"),
- "__meta_linode_specs_transfer_bytes": model.LabelValue("1048576000"),
- "__meta_linode_extra_ips": model.LabelValue(",172.104.18.104,"),
- },
- } {
- t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
- require.Equal(t, lbls, tg.Targets[i])
- })
+ for i, lbls := range tc.want {
+ t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
+ require.Equal(t, lbls, tg.Targets[i])
+ })
+ }
}
}
diff --git a/discovery/linode/metrics.go b/discovery/linode/metrics.go
new file mode 100644
index 0000000000..8f81389226
--- /dev/null
+++ b/discovery/linode/metrics.go
@@ -0,0 +1,57 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package linode
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*linodeMetrics)(nil)
+
+type linodeMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+
+ failuresCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &linodeMetrics{
+ refreshMetrics: rmi,
+ failuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_linode_failures_total",
+ Help: "Number of Linode service discovery refresh failures.",
+ }),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.failuresCount,
+ })
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *linodeMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *linodeMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/linode/mock_test.go b/discovery/linode/mock_test.go
index ade726ed71..50f0572ecd 100644
--- a/discovery/linode/mock_test.go
+++ b/discovery/linode/mock_test.go
@@ -14,13 +14,18 @@
package linode
import (
+ "encoding/json"
"fmt"
"net/http"
"net/http/httptest"
+ "os"
+ "path/filepath"
"testing"
)
-// SDMock is the interface for the Linode mock
+const tokenID = "7b2c56dd51edd90952c1b94c472b94b176f20c5c777e376849edd8ad1c6c03bb"
+
+// SDMock is the interface for the Linode mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
@@ -34,421 +39,43 @@ func NewSDMock(t *testing.T) *SDMock {
}
}
-// Endpoint returns the URI to the mock server
+// Endpoint returns the URI to the mock server.
func (m *SDMock) Endpoint() string {
return m.Server.URL + "/"
}
-// Setup creates the mock server
+// Setup creates the mock server.
func (m *SDMock) Setup() {
m.Mux = http.NewServeMux()
m.Server = httptest.NewServer(m.Mux)
+ m.t.Cleanup(m.Server.Close)
+ m.SetupHandlers()
}
-// ShutdownServer creates the mock server
-func (m *SDMock) ShutdownServer() {
- m.Server.Close()
-}
-
-const tokenID = "7b2c56dd51edd90952c1b94c472b94b176f20c5c777e376849edd8ad1c6c03bb"
-
-// HandleLinodeInstancesList mocks linode instances list.
-func (m *SDMock) HandleLinodeInstancesList() {
- m.Mux.HandleFunc("/v4/linode/instances", func(w http.ResponseWriter, r *http.Request) {
- if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) {
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
-
- w.Header().Set("content-type", "application/json; charset=utf-8")
- w.WriteHeader(http.StatusOK)
-
- fmt.Fprint(w, `
-{
- "data": [
- {
- "id": 26838044,
- "label": "prometheus-linode-sd-exporter-1",
- "group": "",
- "status": "running",
- "created": "2021-05-12T04:23:44",
- "updated": "2021-05-12T04:23:44",
- "type": "g6-standard-2",
- "ipv4": [
- "45.33.82.151",
- "96.126.108.16",
- "192.168.170.51",
- "192.168.201.25"
- ],
- "ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128",
- "image": "linode/arch",
- "region": "us-east",
- "specs": {
- "disk": 81920,
- "memory": 4096,
- "vcpus": 2,
- "gpus": 0,
- "transfer": 4000
- },
- "alerts": {
- "cpu": 180,
- "network_in": 10,
- "network_out": 10,
- "transfer_quota": 80,
- "io": 10000
- },
- "backups": {
- "enabled": false,
- "schedule": {
- "day": null,
- "window": null
- },
- "last_successful": null
- },
- "hypervisor": "kvm",
- "watchdog_enabled": true,
- "tags": [
- "monitoring"
- ]
- },
- {
- "id": 26848419,
- "label": "prometheus-linode-sd-exporter-2",
- "group": "",
- "status": "running",
- "created": "2021-05-12T12:41:49",
- "updated": "2021-05-12T12:41:49",
- "type": "g6-standard-2",
- "ipv4": [
- "139.162.196.43"
- ],
- "ipv6": "2a01:7e00::f03c:92ff:fe1a:9976/128",
- "image": "linode/debian10",
- "region": "eu-west",
- "specs": {
- "disk": 81920,
- "memory": 4096,
- "vcpus": 2,
- "gpus": 0,
- "transfer": 4000
- },
- "alerts": {
- "cpu": 180,
- "network_in": 10,
- "network_out": 10,
- "transfer_quota": 80,
- "io": 10000
- },
- "backups": {
- "enabled": false,
- "schedule": {
- "day": null,
- "window": null
- },
- "last_successful": null
- },
- "hypervisor": "kvm",
- "watchdog_enabled": true,
- "tags": [
- "monitoring"
- ]
- },
- {
- "id": 26837938,
- "label": "prometheus-linode-sd-exporter-3",
- "group": "",
- "status": "running",
- "created": "2021-05-12T04:20:11",
- "updated": "2021-05-12T04:20:11",
- "type": "g6-standard-1",
- "ipv4": [
- "192.53.120.25"
- ],
- "ipv6": "2600:3c04::f03c:92ff:fe1a:fb68/128",
- "image": "linode/ubuntu20.04",
- "region": "ca-central",
- "specs": {
- "disk": 51200,
- "memory": 2048,
- "vcpus": 1,
- "gpus": 0,
- "transfer": 2000
- },
- "alerts": {
- "cpu": 90,
- "network_in": 10,
- "network_out": 10,
- "transfer_quota": 80,
- "io": 10000
- },
- "backups": {
- "enabled": false,
- "schedule": {
- "day": null,
- "window": null
- },
- "last_successful": null
- },
- "hypervisor": "kvm",
- "watchdog_enabled": true,
- "tags": [
- "monitoring"
- ]
- },
- {
- "id": 26837992,
- "label": "prometheus-linode-sd-exporter-4",
- "group": "",
- "status": "running",
- "created": "2021-05-12T04:22:06",
- "updated": "2021-05-12T04:22:06",
- "type": "g6-nanode-1",
- "ipv4": [
- "66.228.47.103",
- "172.104.18.104",
- "192.168.148.94"
- ],
- "ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c/128",
- "image": "linode/ubuntu20.04",
- "region": "us-east",
- "specs": {
- "disk": 25600,
- "memory": 1024,
- "vcpus": 1,
- "gpus": 0,
- "transfer": 1000
- },
- "alerts": {
- "cpu": 90,
- "network_in": 10,
- "network_out": 10,
- "transfer_quota": 80,
- "io": 10000
- },
- "backups": {
- "enabled": false,
- "schedule": {
- "day": null,
- "window": null
- },
- "last_successful": null
- },
- "hypervisor": "kvm",
- "watchdog_enabled": true,
- "tags": [
- "monitoring"
- ]
- }
- ],
- "page": 1,
- "pages": 1,
- "results": 4
-}`,
- )
- })
-}
-
-// HandleLinodeNeworkingIPs mocks linode networking ips endpoint.
-func (m *SDMock) HandleLinodeNeworkingIPs() {
- m.Mux.HandleFunc("/v4/networking/ips", func(w http.ResponseWriter, r *http.Request) {
- if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) {
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
-
- w.Header().Set("content-type", "application/json; charset=utf-8")
- w.WriteHeader(http.StatusOK)
-
- fmt.Fprint(w, `
-{
- "page": 1,
- "pages": 1,
- "results": 13,
- "data": [
- {
- "address": "192.53.120.25",
- "gateway": "192.53.120.1",
- "subnet_mask": "255.255.255.0",
- "prefix": 24,
- "type": "ipv4",
- "public": true,
- "rdns": "li2216-25.members.linode.com",
- "linode_id": 26837938,
- "region": "ca-central"
- },
- {
- "address": "66.228.47.103",
- "gateway": "66.228.47.1",
- "subnet_mask": "255.255.255.0",
- "prefix": 24,
- "type": "ipv4",
- "public": true,
- "rdns": "li328-103.members.linode.com",
- "linode_id": 26837992,
- "region": "us-east"
- },
- {
- "address": "172.104.18.104",
- "gateway": "172.104.18.1",
- "subnet_mask": "255.255.255.0",
- "prefix": 24,
- "type": "ipv4",
- "public": true,
- "rdns": "li1832-104.members.linode.com",
- "linode_id": 26837992,
- "region": "us-east"
- },
- {
- "address": "192.168.148.94",
- "gateway": null,
- "subnet_mask": "255.255.128.0",
- "prefix": 17,
- "type": "ipv4",
- "public": false,
- "rdns": null,
- "linode_id": 26837992,
- "region": "us-east"
- },
- {
- "address": "192.168.170.51",
- "gateway": null,
- "subnet_mask": "255.255.128.0",
- "prefix": 17,
- "type": "ipv4",
- "public": false,
- "rdns": null,
- "linode_id": 26838044,
- "region": "us-east"
- },
- {
- "address": "96.126.108.16",
- "gateway": "96.126.108.1",
- "subnet_mask": "255.255.255.0",
- "prefix": 24,
- "type": "ipv4",
- "public": true,
- "rdns": "li365-16.members.linode.com",
- "linode_id": 26838044,
- "region": "us-east"
- },
- {
- "address": "45.33.82.151",
- "gateway": "45.33.82.1",
- "subnet_mask": "255.255.255.0",
- "prefix": 24,
- "type": "ipv4",
- "public": true,
- "rdns": "li1028-151.members.linode.com",
- "linode_id": 26838044,
- "region": "us-east"
- },
- {
- "address": "192.168.201.25",
- "gateway": null,
- "subnet_mask": "255.255.128.0",
- "prefix": 17,
- "type": "ipv4",
- "public": false,
- "rdns": null,
- "linode_id": 26838044,
- "region": "us-east"
- },
- {
- "address": "139.162.196.43",
- "gateway": "139.162.196.1",
- "subnet_mask": "255.255.255.0",
- "prefix": 24,
- "type": "ipv4",
- "public": true,
- "rdns": "li1359-43.members.linode.com",
- "linode_id": 26848419,
- "region": "eu-west"
- },
- {
- "address": "2600:3c04::f03c:92ff:fe1a:fb68",
- "gateway": "fe80::1",
- "subnet_mask": "ffff:ffff:ffff:ffff::",
- "prefix": 64,
- "type": "ipv6",
- "rdns": null,
- "linode_id": 26837938,
- "region": "ca-central",
- "public": true
- },
- {
- "address": "2600:3c03::f03c:92ff:fe1a:fb4c",
- "gateway": "fe80::1",
- "subnet_mask": "ffff:ffff:ffff:ffff::",
- "prefix": 64,
- "type": "ipv6",
- "rdns": null,
- "linode_id": 26837992,
- "region": "us-east",
- "public": true
- },
- {
- "address": "2600:3c03::f03c:92ff:fe1a:1382",
- "gateway": "fe80::1",
- "subnet_mask": "ffff:ffff:ffff:ffff::",
- "prefix": 64,
- "type": "ipv6",
- "rdns": null,
- "linode_id": 26838044,
- "region": "us-east",
- "public": true
- },
- {
- "address": "2a01:7e00::f03c:92ff:fe1a:9976",
- "gateway": "fe80::1",
- "subnet_mask": "ffff:ffff:ffff:ffff::",
- "prefix": 64,
- "type": "ipv6",
- "rdns": null,
- "linode_id": 26848419,
- "region": "eu-west",
- "public": true
- }
- ]
-}`,
- )
- })
-}
-
-// HandleLinodeAccountEvents mocks linode the account/events endpoint.
-func (m *SDMock) HandleLinodeAccountEvents() {
- m.Mux.HandleFunc("/v4/account/events", func(w http.ResponseWriter, r *http.Request) {
- if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) {
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
-
- if r.Header.Get("X-Filter") == "" {
- // This should never happen; if the client sends an events request without
- // a filter, cause it to fail. The error below is not a real response from
- // the API, but should aid in debugging failed tests.
- w.WriteHeader(http.StatusBadRequest)
- fmt.Fprint(w, `
-{
- "errors": [
- {
- "reason": "Request missing expected X-Filter headers"
- }
- ]
-}`,
- )
- return
- }
-
- w.Header().Set("content-type", "application/json; charset=utf-8")
- w.WriteHeader(http.StatusOK)
-
- fmt.Fprint(w, `
-{
- "data": [],
- "results": 0,
- "pages": 1,
- "page": 1
-}`,
- )
- })
+// SetupHandlers for endpoints of interest.
+func (m *SDMock) SetupHandlers() {
+ for _, handler := range []string{"/v4/account/events", "/v4/linode/instances", "/v4/networking/ips", "/v4/networking/ipv6/ranges"} {
+ m.Mux.HandleFunc(handler, func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ xFilter := struct {
+ Region string `json:"region"`
+ }{}
+ json.Unmarshal([]byte(r.Header.Get("X-Filter")), &xFilter)
+
+ directory := "testdata/no_region_filter"
+ if xFilter.Region != "" { // Validate region filter matches test criteria.
+ directory = "testdata/" + xFilter.Region
+ }
+ if response, err := os.ReadFile(filepath.Join(directory, r.URL.Path+".json")); err == nil {
+ w.Header().Add("content-type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write(response)
+ return
+ }
+ w.WriteHeader(http.StatusInternalServerError)
+ })
+ }
}
diff --git a/discovery/linode/testdata/ca-central/v4/account/events.json b/discovery/linode/testdata/ca-central/v4/account/events.json
new file mode 100644
index 0000000000..ca302e4fd0
--- /dev/null
+++ b/discovery/linode/testdata/ca-central/v4/account/events.json
@@ -0,0 +1,6 @@
+{
+ "data": [],
+ "results": 0,
+ "pages": 1,
+ "page": 1
+}
diff --git a/discovery/linode/testdata/ca-central/v4/linode/instances.json b/discovery/linode/testdata/ca-central/v4/linode/instances.json
new file mode 100644
index 0000000000..dfc1172477
--- /dev/null
+++ b/discovery/linode/testdata/ca-central/v4/linode/instances.json
@@ -0,0 +1,49 @@
+{
+ "data": [
+ {
+ "id": 26837938,
+ "label": "prometheus-linode-sd-exporter-3",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T04:20:11",
+ "updated": "2021-05-12T04:20:11",
+ "type": "g6-standard-1",
+ "ipv4": [
+ "192.53.120.25"
+ ],
+ "ipv6": "2600:3c04::f03c:92ff:fe1a:fb68/128",
+ "image": "linode/ubuntu20.04",
+ "region": "ca-central",
+ "specs": {
+ "disk": 51200,
+ "memory": 2048,
+ "vcpus": 1,
+ "gpus": 0,
+ "transfer": 2000
+ },
+ "alerts": {
+ "cpu": 90,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ }
+ ],
+ "page": 1,
+ "pages": 1,
+ "results": 1
+}
diff --git a/discovery/linode/testdata/ca-central/v4/networking/ips.json b/discovery/linode/testdata/ca-central/v4/networking/ips.json
new file mode 100644
index 0000000000..23d974a886
--- /dev/null
+++ b/discovery/linode/testdata/ca-central/v4/networking/ips.json
@@ -0,0 +1,29 @@
+{
+ "page": 1,
+ "pages": 1,
+ "results": 2,
+ "data": [
+ {
+ "address": "192.53.120.25",
+ "gateway": "192.53.120.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li2216-25.members.linode.com",
+ "linode_id": 26837938,
+ "region": "ca-central"
+ },
+ {
+ "address": "2600:3c04::f03c:92ff:fe1a:fb68",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26837938,
+ "region": "ca-central",
+ "public": true
+ }
+ ]
+}
diff --git a/discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json b/discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json
new file mode 100644
index 0000000000..442615cbbc
--- /dev/null
+++ b/discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json
@@ -0,0 +1,13 @@
+{
+ "data": [
+ {
+ "range": "2600:3c04:e001:456::",
+ "prefix": 64,
+ "region": "ca-central",
+ "route_target": "2600:3c04::f03c:92ff:fe1a:fb68"
+ }
+ ],
+ "page": 1,
+ "pages": 1,
+ "results": 1
+}
diff --git a/discovery/linode/testdata/no_region_filter/v4/account/events.json b/discovery/linode/testdata/no_region_filter/v4/account/events.json
new file mode 100644
index 0000000000..ca302e4fd0
--- /dev/null
+++ b/discovery/linode/testdata/no_region_filter/v4/account/events.json
@@ -0,0 +1,6 @@
+{
+ "data": [],
+ "results": 0,
+ "pages": 1,
+ "page": 1
+}
diff --git a/discovery/linode/testdata/no_region_filter/v4/linode/instances.json b/discovery/linode/testdata/no_region_filter/v4/linode/instances.json
new file mode 100644
index 0000000000..25d5271d9c
--- /dev/null
+++ b/discovery/linode/testdata/no_region_filter/v4/linode/instances.json
@@ -0,0 +1,180 @@
+{
+ "data": [
+ {
+ "id": 26838044,
+ "label": "prometheus-linode-sd-exporter-1",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T04:23:44",
+ "updated": "2021-05-12T04:23:44",
+ "type": "g6-standard-2",
+ "ipv4": [
+ "45.33.82.151",
+ "96.126.108.16",
+ "192.168.170.51",
+ "192.168.201.25"
+ ],
+ "ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128",
+ "image": "linode/arch",
+ "region": "us-east",
+ "specs": {
+ "disk": 81920,
+ "memory": 4096,
+ "vcpus": 2,
+ "gpus": 0,
+ "transfer": 4000
+ },
+ "alerts": {
+ "cpu": 180,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ },
+ {
+ "id": 26848419,
+ "label": "prometheus-linode-sd-exporter-2",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T12:41:49",
+ "updated": "2021-05-12T12:41:49",
+ "type": "g6-standard-2",
+ "ipv4": [
+ "139.162.196.43"
+ ],
+ "ipv6": "2a01:7e00::f03c:92ff:fe1a:9976/128",
+ "image": "linode/debian10",
+ "region": "eu-west",
+ "specs": {
+ "disk": 81920,
+ "memory": 4096,
+ "vcpus": 2,
+ "gpus": 0,
+ "transfer": 4000
+ },
+ "alerts": {
+ "cpu": 180,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ },
+ {
+ "id": 26837938,
+ "label": "prometheus-linode-sd-exporter-3",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T04:20:11",
+ "updated": "2021-05-12T04:20:11",
+ "type": "g6-standard-1",
+ "ipv4": [
+ "192.53.120.25"
+ ],
+ "ipv6": "2600:3c04::f03c:92ff:fe1a:fb68/128",
+ "image": "linode/ubuntu20.04",
+ "region": "ca-central",
+ "specs": {
+ "disk": 51200,
+ "memory": 2048,
+ "vcpus": 1,
+ "gpus": 0,
+ "transfer": 2000
+ },
+ "alerts": {
+ "cpu": 90,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ },
+ {
+ "id": 26837992,
+ "label": "prometheus-linode-sd-exporter-4",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T04:22:06",
+ "updated": "2021-05-12T04:22:06",
+ "type": "g6-nanode-1",
+ "ipv4": [
+ "66.228.47.103",
+ "172.104.18.104",
+ "192.168.148.94"
+ ],
+ "ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c/128",
+ "image": "linode/ubuntu20.04",
+ "region": "us-east",
+ "specs": {
+ "disk": 25600,
+ "memory": 1024,
+ "vcpus": 1,
+ "gpus": 0,
+ "transfer": 1000
+ },
+ "alerts": {
+ "cpu": 90,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ }
+ ],
+ "page": 1,
+ "pages": 1,
+ "results": 4
+}
diff --git a/discovery/linode/testdata/no_region_filter/v4/networking/ips.json b/discovery/linode/testdata/no_region_filter/v4/networking/ips.json
new file mode 100644
index 0000000000..5173036f1c
--- /dev/null
+++ b/discovery/linode/testdata/no_region_filter/v4/networking/ips.json
@@ -0,0 +1,150 @@
+{
+ "page": 1,
+ "pages": 1,
+ "results": 13,
+ "data": [
+ {
+ "address": "192.53.120.25",
+ "gateway": "192.53.120.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li2216-25.members.linode.com",
+ "linode_id": 26837938,
+ "region": "ca-central"
+ },
+ {
+ "address": "66.228.47.103",
+ "gateway": "66.228.47.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li328-103.members.linode.com",
+ "linode_id": 26837992,
+ "region": "us-east"
+ },
+ {
+ "address": "172.104.18.104",
+ "gateway": "172.104.18.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li1832-104.members.linode.com",
+ "linode_id": 26837992,
+ "region": "us-east"
+ },
+ {
+ "address": "192.168.148.94",
+ "gateway": null,
+ "subnet_mask": "255.255.128.0",
+ "prefix": 17,
+ "type": "ipv4",
+ "public": false,
+ "rdns": null,
+ "linode_id": 26837992,
+ "region": "us-east"
+ },
+ {
+ "address": "192.168.170.51",
+ "gateway": null,
+ "subnet_mask": "255.255.128.0",
+ "prefix": 17,
+ "type": "ipv4",
+ "public": false,
+ "rdns": null,
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "96.126.108.16",
+ "gateway": "96.126.108.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li365-16.members.linode.com",
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "45.33.82.151",
+ "gateway": "45.33.82.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li1028-151.members.linode.com",
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "192.168.201.25",
+ "gateway": null,
+ "subnet_mask": "255.255.128.0",
+ "prefix": 17,
+ "type": "ipv4",
+ "public": false,
+ "rdns": null,
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "139.162.196.43",
+ "gateway": "139.162.196.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li1359-43.members.linode.com",
+ "linode_id": 26848419,
+ "region": "eu-west"
+ },
+ {
+ "address": "2600:3c04::f03c:92ff:fe1a:fb68",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26837938,
+ "region": "ca-central",
+ "public": true
+ },
+ {
+ "address": "2600:3c03::f03c:92ff:fe1a:fb4c",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26837992,
+ "region": "us-east",
+ "public": true
+ },
+ {
+ "address": "2600:3c03::f03c:92ff:fe1a:1382",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26838044,
+ "region": "us-east",
+ "public": true
+ },
+ {
+ "address": "2a01:7e00::f03c:92ff:fe1a:9976",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26848419,
+ "region": "eu-west",
+ "public": true
+ }
+ ]
+}
diff --git a/discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json b/discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json
new file mode 100644
index 0000000000..511a4d9a8c
--- /dev/null
+++ b/discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json
@@ -0,0 +1,19 @@
+{
+ "data": [
+ {
+ "range": "2600:3c03:e000:123::",
+ "prefix": 64,
+ "region": "us-east",
+ "route_target": "2600:3c03::f03c:92ff:fe1a:fb4c"
+ },
+ {
+ "range": "2600:3c04:e001:456::",
+ "prefix": 64,
+ "region": "ca-central",
+ "route_target": "2600:3c04::f03c:92ff:fe1a:fb68"
+ }
+ ],
+ "page": 1,
+ "pages": 1,
+ "results": 2
+}
diff --git a/discovery/linode/testdata/us-east/v4/account/events.json b/discovery/linode/testdata/us-east/v4/account/events.json
new file mode 100644
index 0000000000..ca302e4fd0
--- /dev/null
+++ b/discovery/linode/testdata/us-east/v4/account/events.json
@@ -0,0 +1,6 @@
+{
+ "data": [],
+ "results": 0,
+ "pages": 1,
+ "page": 1
+}
diff --git a/discovery/linode/testdata/us-east/v4/linode/instances.json b/discovery/linode/testdata/us-east/v4/linode/instances.json
new file mode 100644
index 0000000000..5e9a8f5abe
--- /dev/null
+++ b/discovery/linode/testdata/us-east/v4/linode/instances.json
@@ -0,0 +1,97 @@
+{
+ "data": [
+ {
+ "id": 26838044,
+ "label": "prometheus-linode-sd-exporter-1",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T04:23:44",
+ "updated": "2021-05-12T04:23:44",
+ "type": "g6-standard-2",
+ "ipv4": [
+ "45.33.82.151",
+ "96.126.108.16",
+ "192.168.170.51",
+ "192.168.201.25"
+ ],
+ "ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128",
+ "image": "linode/arch",
+ "region": "us-east",
+ "specs": {
+ "disk": 81920,
+ "memory": 4096,
+ "vcpus": 2,
+ "gpus": 0,
+ "transfer": 4000
+ },
+ "alerts": {
+ "cpu": 180,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ },
+ {
+ "id": 26837992,
+ "label": "prometheus-linode-sd-exporter-4",
+ "group": "",
+ "status": "running",
+ "created": "2021-05-12T04:22:06",
+ "updated": "2021-05-12T04:22:06",
+ "type": "g6-nanode-1",
+ "ipv4": [
+ "66.228.47.103",
+ "172.104.18.104",
+ "192.168.148.94"
+ ],
+ "ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c/128",
+ "image": "linode/ubuntu20.04",
+ "region": "us-east",
+ "specs": {
+ "disk": 25600,
+ "memory": 1024,
+ "vcpus": 1,
+ "gpus": 0,
+ "transfer": 1000
+ },
+ "alerts": {
+ "cpu": 90,
+ "network_in": 10,
+ "network_out": 10,
+ "transfer_quota": 80,
+ "io": 10000
+ },
+ "backups": {
+ "enabled": false,
+ "schedule": {
+ "day": null,
+ "window": null
+ },
+ "last_successful": null
+ },
+ "hypervisor": "kvm",
+ "watchdog_enabled": true,
+ "tags": [
+ "monitoring"
+ ]
+ }
+ ],
+ "page": 1,
+ "pages": 1,
+ "results": 2
+}
+
diff --git a/discovery/linode/testdata/us-east/v4/networking/ips.json b/discovery/linode/testdata/us-east/v4/networking/ips.json
new file mode 100644
index 0000000000..388cf59659
--- /dev/null
+++ b/discovery/linode/testdata/us-east/v4/networking/ips.json
@@ -0,0 +1,106 @@
+{
+ "page": 1,
+ "pages": 1,
+ "results": 9,
+ "data": [
+ {
+ "address": "66.228.47.103",
+ "gateway": "66.228.47.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li328-103.members.linode.com",
+ "linode_id": 26837992,
+ "region": "us-east"
+ },
+ {
+ "address": "172.104.18.104",
+ "gateway": "172.104.18.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li1832-104.members.linode.com",
+ "linode_id": 26837992,
+ "region": "us-east"
+ },
+ {
+ "address": "192.168.148.94",
+ "gateway": null,
+ "subnet_mask": "255.255.128.0",
+ "prefix": 17,
+ "type": "ipv4",
+ "public": false,
+ "rdns": null,
+ "linode_id": 26837992,
+ "region": "us-east"
+ },
+ {
+ "address": "192.168.170.51",
+ "gateway": null,
+ "subnet_mask": "255.255.128.0",
+ "prefix": 17,
+ "type": "ipv4",
+ "public": false,
+ "rdns": null,
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "96.126.108.16",
+ "gateway": "96.126.108.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li365-16.members.linode.com",
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "45.33.82.151",
+ "gateway": "45.33.82.1",
+ "subnet_mask": "255.255.255.0",
+ "prefix": 24,
+ "type": "ipv4",
+ "public": true,
+ "rdns": "li1028-151.members.linode.com",
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "192.168.201.25",
+ "gateway": null,
+ "subnet_mask": "255.255.128.0",
+ "prefix": 17,
+ "type": "ipv4",
+ "public": false,
+ "rdns": null,
+ "linode_id": 26838044,
+ "region": "us-east"
+ },
+ {
+ "address": "2600:3c03::f03c:92ff:fe1a:fb4c",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26837992,
+ "region": "us-east",
+ "public": true
+ },
+ {
+ "address": "2600:3c03::f03c:92ff:fe1a:1382",
+ "gateway": "fe80::1",
+ "subnet_mask": "ffff:ffff:ffff:ffff::",
+ "prefix": 64,
+ "type": "ipv6",
+ "rdns": null,
+ "linode_id": 26838044,
+ "region": "us-east",
+ "public": true
+ }
+ ]
+}
diff --git a/discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json b/discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json
new file mode 100644
index 0000000000..34b2ae1cdd
--- /dev/null
+++ b/discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json
@@ -0,0 +1,13 @@
+{
+ "data": [
+ {
+ "range": "2600:3c03:e000:123::",
+ "prefix": 64,
+ "region": "us-east",
+ "route_target": "2600:3c03::f03c:92ff:fe1a:fb4c"
+ }
+ ],
+ "page": 1,
+ "pages": 1,
+ "results": 1
+}
diff --git a/discovery/manager.go b/discovery/manager.go
index 8b304a0faf..542655d4cc 100644
--- a/discovery/manager.go
+++ b/discovery/manager.go
@@ -28,48 +28,6 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-var (
- failedConfigs = prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "prometheus_sd_failed_configs",
- Help: "Current number of service discovery configurations that failed to load.",
- },
- []string{"name"},
- )
- discoveredTargets = prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "prometheus_sd_discovered_targets",
- Help: "Current number of discovered targets.",
- },
- []string{"name", "config"},
- )
- receivedUpdates = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_received_updates_total",
- Help: "Total number of update events received from the SD providers.",
- },
- []string{"name"},
- )
- delayedUpdates = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_updates_delayed_total",
- Help: "Total number of update events that couldn't be sent immediately.",
- },
- []string{"name"},
- )
- sentUpdates = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_updates_total",
- Help: "Total number of update events sent to the SD consumers.",
- },
- []string{"name"},
- )
-)
-
-func RegisterMetrics() {
- prometheus.MustRegister(failedConfigs, discoveredTargets, receivedUpdates, delayedUpdates, sentUpdates)
-}
-
type poolKey struct {
setName string
provider string
@@ -92,7 +50,7 @@ type Provider struct {
newSubs map[string]struct{}
}
-// Discoverer return the Discoverer of the provider
+// Discoverer return the Discoverer of the provider.
func (p *Provider) Discoverer() Discoverer {
return p.d
}
@@ -106,8 +64,24 @@ func (p *Provider) Config() interface{} {
return p.config
}
+// CreateAndRegisterSDMetrics registers the metrics needed for SD mechanisms.
+// Does not register the metrics for the Discovery Manager.
+// TODO(ptodev): Add ability to unregister the metrics?
+func CreateAndRegisterSDMetrics(reg prometheus.Registerer) (map[string]DiscovererMetrics, error) {
+ // Some SD mechanisms use the "refresh" package, which has its own metrics.
+ refreshSdMetrics := NewRefreshMetrics(reg)
+
+ // Register the metrics specific for each SD mechanism, and the ones for the refresh package.
+ sdMetrics, err := RegisterSDMetrics(reg, refreshSdMetrics)
+ if err != nil {
+ return nil, fmt.Errorf("failed to register service discovery metrics: %w", err)
+ }
+
+ return sdMetrics, nil
+}
+
// NewManager is the Discovery Manager constructor.
-func NewManager(ctx context.Context, logger log.Logger, options ...func(*Manager)) *Manager {
+func NewManager(ctx context.Context, logger log.Logger, registerer prometheus.Registerer, sdMetrics map[string]DiscovererMetrics, options ...func(*Manager)) *Manager {
if logger == nil {
logger = log.NewNopLogger()
}
@@ -118,10 +92,22 @@ func NewManager(ctx context.Context, logger log.Logger, options ...func(*Manager
ctx: ctx,
updatert: 5 * time.Second,
triggerSend: make(chan struct{}, 1),
+ registerer: registerer,
+ sdMetrics: sdMetrics,
}
for _, option := range options {
option(mgr)
}
+
+ // Register the metrics.
+ // We have to do this after setting all options, so that the name of the Manager is set.
+ if metrics, err := NewManagerMetrics(registerer, mgr.name); err == nil {
+ mgr.metrics = metrics
+ } else {
+ level.Error(logger).Log("msg", "Failed to create discovery manager metrics", "manager", mgr.name, "err", err)
+ return nil
+ }
+
return mgr
}
@@ -134,6 +120,26 @@ func Name(n string) func(*Manager) {
}
}
+// Updatert sets the updatert of the manager.
+// Used to speed up tests.
+func Updatert(u time.Duration) func(*Manager) {
+ return func(m *Manager) {
+ m.mtx.Lock()
+ defer m.mtx.Unlock()
+ m.updatert = u
+ }
+}
+
+// SkipInitialWait sets the name of the manager. This is used in serverless flavours of OTel's prometheusreceiver
+// which is sensitive to startup latencies.
+func SkipInitialWait() func(*Manager) {
+ return func(m *Manager) {
+ m.mtx.Lock()
+ defer m.mtx.Unlock()
+ m.skipStartupWait = true
+ }
+}
+
// HTTPClientOptions sets the list of HTTP client options to expose to
// Discoverers. It is up to Discoverers to choose to use the options provided.
func HTTPClientOptions(opts ...config.HTTPClientOption) func(*Manager) {
@@ -165,11 +171,22 @@ type Manager struct {
// should only be modified in unit tests.
updatert time.Duration
+ // skipStartupWait allows the discovery manager to skip the initial wait before sending updates
+ // to the channel. This is used in serverless flavours of OTel's prometheusreceiver
+ // which is sensitive to startup latencies.
+ skipStartupWait bool
+
// The triggerSend channel signals to the Manager that new updates have been received from providers.
triggerSend chan struct{}
// lastProvider counts providers registered during Manager's lifetime.
lastProvider uint
+
+ // A registerer for all service discovery metrics.
+ registerer prometheus.Registerer
+
+ metrics *Metrics
+ sdMetrics map[string]DiscovererMetrics
}
// Providers returns the currently configured SD providers.
@@ -177,14 +194,19 @@ func (m *Manager) Providers() []*Provider {
return m.providers
}
+// UnregisterMetrics unregisters manager metrics. It does not unregister
+// service discovery or refresh metrics, whose lifecycle is managed independent
+// of the discovery Manager.
+func (m *Manager) UnregisterMetrics() {
+ m.metrics.Unregister(m.registerer)
+}
+
// Run starts the background processing.
func (m *Manager) Run() error {
go m.sender()
- for range m.ctx.Done() {
- m.cancelDiscoverers()
- return m.ctx.Err()
- }
- return nil
+ <-m.ctx.Done()
+ m.cancelDiscoverers()
+ return m.ctx.Err()
}
// SyncCh returns a read only channel used by all the clients to receive target updates.
@@ -202,12 +224,18 @@ func (m *Manager) ApplyConfig(cfg map[string]Configs) error {
for name, scfg := range cfg {
failedCount += m.registerProviders(scfg, name)
}
- failedConfigs.WithLabelValues(m.name).Set(float64(failedCount))
+ m.metrics.FailedConfigs.Set(float64(failedCount))
var (
- wg sync.WaitGroup
- // keep shows if we keep any providers after reload.
- keep bool
+ // The updater goroutine is responsible for gathering updates to the
+ // tsets and notifying the manager but in some cases, the reload will
+ // update the tsets as well (for example: providers change during a
+ // reload and old targets need to be cleared).
+ // shouldNotify tracks whether this call to ApplyConfig updates the
+ // targets and is used to determine whether a notification to the
+ // manager needs to be triggered.
+ shouldNotify bool
+ wg sync.WaitGroup
newProviders []*Provider
)
for _, prov := range m.providers {
@@ -221,24 +249,29 @@ func (m *Manager) ApplyConfig(cfg map[string]Configs) error {
continue
}
newProviders = append(newProviders, prov)
- // refTargets keeps reference targets used to populate new subs' targets
+ // refTargets keeps reference targets used to populate new subs' targets as they should be the same.
var refTargets map[string]*targetgroup.Group
prov.mu.Lock()
m.targetsMtx.Lock()
for s := range prov.subs {
- keep = true
+ // Since we have existing subs, and have new subs as well - we are
+ // going to be making changes to the targets map (deleting obsolete
+ // subs' targets and setting new subs' targets). Therefore,
+ // regardless of the discoverer we should notify the downstream
+ // managers so they can update the full target state.
+ shouldNotify = true
refTargets = m.targets[poolKey{s, prov.name}]
// Remove obsolete subs' targets.
if _, ok := prov.newSubs[s]; !ok {
delete(m.targets, poolKey{s, prov.name})
- discoveredTargets.DeleteLabelValues(m.name, s)
+ m.metrics.DiscoveredTargets.DeleteLabelValues(m.name, s)
}
}
// Set metrics and targets for new subs.
for s := range prov.newSubs {
if _, ok := prov.subs[s]; !ok {
- discoveredTargets.WithLabelValues(m.name, s).Set(0)
+ m.metrics.DiscoveredTargets.WithLabelValues(s).Set(0)
}
if l := len(refTargets); l > 0 {
m.targets[poolKey{s, prov.name}] = make(map[string]*targetgroup.Group, l)
@@ -256,11 +289,18 @@ func (m *Manager) ApplyConfig(cfg map[string]Configs) error {
m.startProvider(m.ctx, prov)
}
}
+
+ // This also helps making the downstream managers drop stale targets as soon as possible.
+ // See https://github.com/prometheus/prometheus/pull/13147 for details.
+ if !reflect.DeepEqual(m.providers, newProviders) {
+ shouldNotify = true
+ }
+
// Currently downstream managers expect full target state upon config reload, so we must oblige.
// While startProvider does pull the trigger, it may take some time to do so, therefore
// we pull the trigger as soon as possible so that downstream managers can populate their state.
// See https://github.com/prometheus/prometheus/pull/8639 for details.
- if keep {
+ if shouldNotify {
select {
case m.triggerSend <- struct{}{}:
default:
@@ -281,7 +321,9 @@ func (m *Manager) StartCustomProvider(ctx context.Context, name string, worker D
name: {},
},
}
+ m.mtx.Lock()
m.providers = append(m.providers, p)
+ m.mtx.Unlock()
m.startProvider(ctx, p)
}
@@ -318,7 +360,7 @@ func (m *Manager) updater(ctx context.Context, p *Provider, updates chan []*targ
case <-ctx.Done():
return
case tgs, ok := <-updates:
- receivedUpdates.WithLabelValues(m.name).Inc()
+ m.metrics.ReceivedUpdates.Inc()
if !ok {
level.Debug(m.logger).Log("msg", "Discoverer channel closed", "provider", p.name)
// Wait for provider cancellation to ensure targets are cleaned up when expected.
@@ -344,6 +386,33 @@ func (m *Manager) sender() {
ticker := time.NewTicker(m.updatert)
defer ticker.Stop()
+ // Send the targets downstream as soon as you see them if skipStartupWait is
+ // set. If discovery receiver's channel is too busy, fall back to the
+ // regular loop.
+ if m.skipStartupWait {
+ select {
+ case <-m.triggerSend:
+ m.metrics.SentUpdates.Inc()
+ select {
+ case m.syncCh <- m.allGroups():
+ case <-ticker.C:
+ m.metrics.DelayedUpdates.Inc()
+ level.Debug(m.logger).Log("msg", "Discovery receiver's channel was full so will retry the next cycle")
+ select {
+ case m.triggerSend <- struct{}{}:
+ default:
+ }
+ case <-m.ctx.Done():
+ return
+ }
+ case <-m.ctx.Done():
+ return
+ }
+
+ // We restart the ticker to ensure that no two updates are less than updatert apart.
+ ticker.Reset(m.updatert)
+ }
+
for {
select {
case <-m.ctx.Done():
@@ -351,11 +420,11 @@ func (m *Manager) sender() {
case <-ticker.C: // Some discoverers send updates too often, so we throttle these with the ticker.
select {
case <-m.triggerSend:
- sentUpdates.WithLabelValues(m.name).Inc()
+ m.metrics.SentUpdates.Inc()
select {
case m.syncCh <- m.allGroups():
default:
- delayedUpdates.WithLabelValues(m.name).Inc()
+ m.metrics.DelayedUpdates.Inc()
level.Debug(m.logger).Log("msg", "Discovery receiver's channel was full so will retry the next cycle")
select {
case m.triggerSend <- struct{}{}:
@@ -386,8 +455,16 @@ func (m *Manager) updateGroup(poolKey poolKey, tgs []*targetgroup.Group) {
m.targets[poolKey] = make(map[string]*targetgroup.Group)
}
for _, tg := range tgs {
- if tg != nil { // Some Discoverers send nil target group so need to check for it to avoid panics.
+ // Some Discoverers send nil target group so need to check for it to avoid panics.
+ if tg == nil {
+ continue
+ }
+ if len(tg.Targets) > 0 {
m.targets[poolKey][tg.Source] = tg
+ } else {
+ // The target group is empty, drop the corresponding entry to avoid leaks.
+ // In case the group yielded targets before, allGroups() will take care of making consumers drop them.
+ delete(m.targets[poolKey], tg.Source)
}
}
}
@@ -396,19 +473,33 @@ func (m *Manager) allGroups() map[string][]*targetgroup.Group {
tSets := map[string][]*targetgroup.Group{}
n := map[string]int{}
+ m.mtx.RLock()
m.targetsMtx.Lock()
- defer m.targetsMtx.Unlock()
- for pkey, tsets := range m.targets {
- for _, tg := range tsets {
- // Even if the target group 'tg' is empty we still need to send it to the 'Scrape manager'
- // to signal that it needs to stop all scrape loops for this target set.
- tSets[pkey.setName] = append(tSets[pkey.setName], tg)
- n[pkey.setName] += len(tg.Targets)
+ for _, p := range m.providers {
+ p.mu.RLock()
+ for s := range p.subs {
+ // Send empty lists for subs without any targets to make sure old stale targets are dropped by consumers.
+ // See: https://github.com/prometheus/prometheus/issues/12858 for details.
+ if _, ok := tSets[s]; !ok {
+ tSets[s] = []*targetgroup.Group{}
+ n[s] = 0
+ }
+ if tsets, ok := m.targets[poolKey{s, p.name}]; ok {
+ for _, tg := range tsets {
+ tSets[s] = append(tSets[s], tg)
+ n[s] += len(tg.Targets)
+ }
+ }
}
+ p.mu.RUnlock()
}
+ m.targetsMtx.Unlock()
+ m.mtx.RUnlock()
+
for setName, v := range n {
- discoveredTargets.WithLabelValues(m.name, setName).Set(float64(v))
+ m.metrics.DiscoveredTargets.WithLabelValues(setName).Set(float64(v))
}
+
return tSets
}
@@ -430,6 +521,7 @@ func (m *Manager) registerProviders(cfgs Configs, setName string) int {
d, err := cfg.NewDiscoverer(DiscovererOptions{
Logger: log.With(m.logger, "discovery", typ, "config", setName),
HTTPClientOptions: m.httpOpts,
+ Metrics: m.sdMetrics[typ],
})
if err != nil {
level.Error(m.logger).Log("msg", "Cannot create service discovery", "err", err, "type", typ, "config", setName)
diff --git a/discovery/manager_test.go b/discovery/manager_test.go
index 5371608112..58349bb635 100644
--- a/discovery/manager_test.go
+++ b/discovery/manager_test.go
@@ -23,6 +23,7 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
client_testutil "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
@@ -35,6 +36,13 @@ func TestMain(m *testing.M) {
testutil.TolerantVerifyLeak(m)
}
+func NewTestMetrics(t *testing.T, reg prometheus.Registerer) (RefreshMetricsManager, map[string]DiscovererMetrics) {
+ refreshMetrics := NewRefreshMetrics(reg)
+ sdMetrics, err := RegisterSDMetrics(reg, refreshMetrics)
+ require.NoError(t, err)
+ return refreshMetrics, sdMetrics
+}
+
// TestTargetUpdatesOrder checks that the target updates are received in the expected order.
func TestTargetUpdatesOrder(t *testing.T) {
// The order by which the updates are send is determined by the interval passed to the mock discovery adapter
@@ -664,7 +672,11 @@ func TestTargetUpdatesOrder(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
var totalUpdatesCount int
@@ -682,7 +694,7 @@ func TestTargetUpdatesOrder(t *testing.T) {
for x := 0; x < totalUpdatesCount; x++ {
select {
case <-ctx.Done():
- t.Fatalf("%d: no update arrived within the timeout limit", x)
+ require.FailNow(t, "%d: no update arrived within the timeout limit", x)
case tgs := <-provUpdates:
discoveryManager.updateGroup(poolKey{setName: strconv.Itoa(i), provider: tc.title}, tgs)
for _, got := range discoveryManager.allGroups() {
@@ -708,7 +720,7 @@ func staticConfig(addrs ...string) StaticConfig {
var cfg StaticConfig
for i, addr := range addrs {
cfg = append(cfg, &targetgroup.Group{
- Source: fmt.Sprint(i),
+ Source: strconv.Itoa(i),
Targets: []model.LabelSet{
{model.AddressLabel: model.LabelValue(addr)},
},
@@ -721,7 +733,6 @@ func verifySyncedPresence(t *testing.T, tGroups map[string][]*targetgroup.Group,
t.Helper()
if _, ok := tGroups[key]; !ok {
t.Fatalf("'%s' should be present in Group map keys: %v", key, tGroups)
- return
}
match := false
var mergedTargets string
@@ -744,10 +755,8 @@ func verifySyncedPresence(t *testing.T, tGroups map[string][]*targetgroup.Group,
func verifyPresence(t *testing.T, tSets map[poolKey]map[string]*targetgroup.Group, poolKey poolKey, label string, present bool) {
t.Helper()
- if _, ok := tSets[poolKey]; !ok {
- t.Fatalf("'%s' should be present in Pool keys: %v", poolKey, tSets)
- return
- }
+ _, ok := tSets[poolKey]
+ require.True(t, ok, "'%s' should be present in Pool keys: %v", poolKey, tSets)
match := false
var mergedTargets string
@@ -764,7 +773,7 @@ func verifyPresence(t *testing.T, tSets map[poolKey]map[string]*targetgroup.Grou
if !present {
msg = "not"
}
- t.Fatalf("%q should %s be present in Targets labels: %q", label, msg, mergedTargets)
+ require.FailNow(t, "%q should %s be present in Targets labels: %q", label, msg, mergedTargets)
}
}
@@ -775,10 +784,45 @@ func pk(provider, setName string, n int) poolKey {
}
}
+func TestTargetSetTargetGroupsPresentOnStartup(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ defer cancel()
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics, SkipInitialWait())
+ require.NotNil(t, discoveryManager)
+
+ // Set the updatert to a super long time so we can verify that the skip worked correctly.
+ discoveryManager.updatert = 100 * time.Hour
+ go discoveryManager.Run()
+
+ c := map[string]Configs{
+ "prometheus": {
+ staticConfig("foo:9090"),
+ },
+ }
+ discoveryManager.ApplyConfig(c)
+
+ syncedTargets := <-discoveryManager.SyncCh()
+ require.Len(t, syncedTargets, 1)
+ verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
+ require.Len(t, syncedTargets["prometheus"], 1)
+ p := pk("static", "prometheus", 0)
+ verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
+ require.Len(t, discoveryManager.targets, 1)
+}
+
func TestTargetSetTargetGroupsPresentOnConfigReload(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -790,27 +834,32 @@ func TestTargetSetTargetGroupsPresentOnConfigReload(t *testing.T) {
discoveryManager.ApplyConfig(c)
syncedTargets := <-discoveryManager.SyncCh()
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
p := pk("static", "prometheus", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(discoveryManager.targets))
+ require.Len(t, discoveryManager.targets, 1)
discoveryManager.ApplyConfig(c)
syncedTargets = <-discoveryManager.SyncCh()
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(discoveryManager.targets))
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, discoveryManager.targets, 1)
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
}
func TestTargetSetTargetGroupsPresentOnConfigRename(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -822,12 +871,12 @@ func TestTargetSetTargetGroupsPresentOnConfigRename(t *testing.T) {
discoveryManager.ApplyConfig(c)
syncedTargets := <-discoveryManager.SyncCh()
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
p := pk("static", "prometheus", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(discoveryManager.targets))
+ require.Len(t, discoveryManager.targets, 1)
c["prometheus2"] = c["prometheus"]
delete(c, "prometheus")
@@ -836,16 +885,21 @@ func TestTargetSetTargetGroupsPresentOnConfigRename(t *testing.T) {
syncedTargets = <-discoveryManager.SyncCh()
p = pk("static", "prometheus2", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(discoveryManager.targets))
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, discoveryManager.targets, 1)
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus2", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus2"]))
+ require.Len(t, syncedTargets["prometheus2"], 1)
}
func TestTargetSetTargetGroupsPresentOnConfigDuplicateAndDeleteOriginal(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -860,30 +914,35 @@ func TestTargetSetTargetGroupsPresentOnConfigDuplicateAndDeleteOriginal(t *testi
c["prometheus2"] = c["prometheus"]
discoveryManager.ApplyConfig(c)
syncedTargets := <-discoveryManager.SyncCh()
- require.Equal(t, 2, len(syncedTargets))
+ require.Len(t, syncedTargets, 2)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
verifySyncedPresence(t, syncedTargets, "prometheus2", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus2"]))
+ require.Len(t, syncedTargets["prometheus2"], 1)
p := pk("static", "prometheus", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 2, len(discoveryManager.targets))
+ require.Len(t, discoveryManager.targets, 2)
delete(c, "prometheus")
discoveryManager.ApplyConfig(c)
syncedTargets = <-discoveryManager.SyncCh()
p = pk("static", "prometheus2", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(discoveryManager.targets))
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, discoveryManager.targets, 1)
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus2", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus2"]))
+ require.Len(t, syncedTargets["prometheus2"], 1)
}
func TestTargetSetTargetGroupsPresentOnConfigChange(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -895,9 +954,9 @@ func TestTargetSetTargetGroupsPresentOnConfigChange(t *testing.T) {
discoveryManager.ApplyConfig(c)
syncedTargets := <-discoveryManager.SyncCh()
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
var mu sync.Mutex
c["prometheus2"] = Configs{
@@ -910,41 +969,48 @@ func TestTargetSetTargetGroupsPresentOnConfigChange(t *testing.T) {
discoveryManager.ApplyConfig(c)
// Original targets should be present as soon as possible.
+ // An empty list should be sent for prometheus2 to drop any stale targets
syncedTargets = <-discoveryManager.SyncCh()
mu.Unlock()
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, syncedTargets, 2)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
+ require.Empty(t, syncedTargets["prometheus2"])
// prometheus2 configs should be ready on second sync.
syncedTargets = <-discoveryManager.SyncCh()
- require.Equal(t, 2, len(syncedTargets))
+ require.Len(t, syncedTargets, 2)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
verifySyncedPresence(t, syncedTargets, "prometheus2", "{__address__=\"bar:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus2"]))
+ require.Len(t, syncedTargets["prometheus2"], 1)
p := pk("static", "prometheus", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
p = pk("lockstatic", "prometheus2", 1)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"bar:9090\"}", true)
- require.Equal(t, 2, len(discoveryManager.targets))
+ require.Len(t, discoveryManager.targets, 2)
// Delete part of config and ensure only original targets exist.
delete(c, "prometheus2")
discoveryManager.ApplyConfig(c)
syncedTargets = <-discoveryManager.SyncCh()
- require.Equal(t, 1, len(discoveryManager.targets))
+ require.Len(t, discoveryManager.targets, 1)
verifyPresence(t, discoveryManager.targets, pk("static", "prometheus", 0), "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
}
func TestTargetSetRecreatesTargetGroupsOnConfigChange(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -959,31 +1025,36 @@ func TestTargetSetRecreatesTargetGroupsOnConfigChange(t *testing.T) {
p := pk("static", "prometheus", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"bar:9090\"}", true)
- require.Equal(t, 1, len(discoveryManager.targets))
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, discoveryManager.targets, 1)
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"bar:9090\"}", true)
- require.Equal(t, 2, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 2)
c["prometheus"] = Configs{
staticConfig("foo:9090"),
}
discoveryManager.ApplyConfig(c)
syncedTargets = <-discoveryManager.SyncCh()
- require.Equal(t, 1, len(discoveryManager.targets))
+ require.Len(t, discoveryManager.targets, 1)
p = pk("static", "prometheus", 1)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"bar:9090\"}", false)
- require.Equal(t, 1, len(discoveryManager.targets))
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, discoveryManager.targets, 1)
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
}
func TestDiscovererConfigs(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -1001,21 +1072,26 @@ func TestDiscovererConfigs(t *testing.T) {
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"bar:9090\"}", true)
p = pk("static", "prometheus", 1)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"baz:9090\"}", true)
- require.Equal(t, 2, len(discoveryManager.targets))
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, discoveryManager.targets, 2)
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"bar:9090\"}", true)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"baz:9090\"}", true)
- require.Equal(t, 3, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 3)
}
// TestTargetSetRecreatesEmptyStaticConfigs ensures that reloading a config file after
-// removing all targets from the static_configs sends an update with empty targetGroups.
-// This is required to signal the receiver that this target set has no current targets.
+// removing all targets from the static_configs cleans the corresponding targetGroups entries to avoid leaks and sends an empty update.
+// The update is required to signal the consumers that the previous targets should be dropped.
func TestTargetSetRecreatesEmptyStaticConfigs(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -1029,9 +1105,9 @@ func TestTargetSetRecreatesEmptyStaticConfigs(t *testing.T) {
syncedTargets := <-discoveryManager.SyncCh()
p := pk("static", "prometheus", 0)
verifyPresence(t, discoveryManager.targets, p, "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets))
+ require.Len(t, syncedTargets, 1)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
c["prometheus"] = Configs{
StaticConfig{{}},
@@ -1039,30 +1115,25 @@ func TestTargetSetRecreatesEmptyStaticConfigs(t *testing.T) {
discoveryManager.ApplyConfig(c)
syncedTargets = <-discoveryManager.SyncCh()
+ require.Len(t, discoveryManager.targets, 1)
p = pk("static", "prometheus", 1)
targetGroups, ok := discoveryManager.targets[p]
- if !ok {
- t.Fatalf("'%v' should be present in target groups", p)
- }
- group, ok := targetGroups[""]
- if !ok {
- t.Fatalf("missing '' key in target groups %v", targetGroups)
- }
-
- if len(group.Targets) != 0 {
- t.Fatalf("Invalid number of targets: expected 0, got %d", len(group.Targets))
- }
- require.Equal(t, 1, len(syncedTargets))
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
- if lbls := syncedTargets["prometheus"][0].Labels; lbls != nil {
- t.Fatalf("Unexpected Group: expected nil Labels, got %v", lbls)
- }
+ require.True(t, ok, "'%v' should be present in targets", p)
+ // Otherwise the targetGroups will leak, see https://github.com/prometheus/prometheus/issues/12436.
+ require.Empty(t, targetGroups, 0, "'%v' should no longer have any associated target groups", p)
+ require.Len(t, syncedTargets, 1, "an update with no targetGroups should still be sent.")
+ require.Empty(t, syncedTargets["prometheus"], 0)
}
func TestIdenticalConfigurationsAreCoalesced(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, nil)
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, nil, reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -1079,14 +1150,12 @@ func TestIdenticalConfigurationsAreCoalesced(t *testing.T) {
syncedTargets := <-discoveryManager.SyncCh()
verifyPresence(t, discoveryManager.targets, pk("static", "prometheus", 0), "{__address__=\"foo:9090\"}", true)
verifyPresence(t, discoveryManager.targets, pk("static", "prometheus2", 0), "{__address__=\"foo:9090\"}", true)
- if len(discoveryManager.providers) != 1 {
- t.Fatalf("Invalid number of providers: expected 1, got %d", len(discoveryManager.providers))
- }
- require.Equal(t, 2, len(syncedTargets))
+ require.Len(t, discoveryManager.providers, 1, "Invalid number of providers.")
+ require.Len(t, syncedTargets, 2)
verifySyncedPresence(t, syncedTargets, "prometheus", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus"]))
+ require.Len(t, syncedTargets["prometheus"], 1)
verifySyncedPresence(t, syncedTargets, "prometheus2", "{__address__=\"foo:9090\"}", true)
- require.Equal(t, 1, len(syncedTargets["prometheus2"]))
+ require.Len(t, syncedTargets["prometheus2"], 1)
}
func TestApplyConfigDoesNotModifyStaticTargets(t *testing.T) {
@@ -1098,7 +1167,12 @@ func TestApplyConfigDoesNotModifyStaticTargets(t *testing.T) {
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -1118,11 +1192,21 @@ type errorConfig struct{ err error }
func (e errorConfig) Name() string { return "error" }
func (e errorConfig) NewDiscoverer(DiscovererOptions) (Discoverer, error) { return nil, e.err }
+// NewDiscovererMetrics implements discovery.Config.
+func (errorConfig) NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics {
+ return &NoopDiscovererMetrics{}
+}
+
type lockStaticConfig struct {
mu *sync.Mutex
config StaticConfig
}
+// NewDiscovererMetrics implements discovery.Config.
+func (lockStaticConfig) NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics {
+ return &NoopDiscovererMetrics{}
+}
+
func (s lockStaticConfig) Name() string { return "lockstatic" }
func (s lockStaticConfig) NewDiscoverer(DiscovererOptions) (Discoverer, error) {
return (lockStaticDiscoverer)(s), nil
@@ -1144,7 +1228,12 @@ func (s lockStaticDiscoverer) Run(ctx context.Context, up chan<- []*targetgroup.
func TestGaugeFailedConfigs(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -1158,10 +1247,8 @@ func TestGaugeFailedConfigs(t *testing.T) {
discoveryManager.ApplyConfig(c)
<-discoveryManager.SyncCh()
- failedCount := client_testutil.ToFloat64(failedConfigs)
- if failedCount != 3 {
- t.Fatalf("Expected to have 3 failed configs, got: %v", failedCount)
- }
+ failedCount := client_testutil.ToFloat64(discoveryManager.metrics.FailedConfigs)
+ require.Equal(t, 3.0, failedCount, "Expected to have 3 failed configs.")
c["prometheus"] = Configs{
staticConfig("foo:9090"),
@@ -1169,10 +1256,8 @@ func TestGaugeFailedConfigs(t *testing.T) {
discoveryManager.ApplyConfig(c)
<-discoveryManager.SyncCh()
- failedCount = client_testutil.ToFloat64(failedConfigs)
- if failedCount != 0 {
- t.Fatalf("Expected to get no failed config, got: %v", failedCount)
- }
+ failedCount = client_testutil.ToFloat64(discoveryManager.metrics.FailedConfigs)
+ require.Equal(t, 0.0, failedCount, "Expected to get no failed config.")
}
func TestCoordinationWithReceiver(t *testing.T) {
@@ -1220,6 +1305,7 @@ func TestCoordinationWithReceiver(t *testing.T) {
Targets: []model.LabelSet{{"__instance__": "1"}},
},
},
+ "mock1": {},
},
},
{
@@ -1300,7 +1386,11 @@ func TestCoordinationWithReceiver(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
- mgr := NewManager(ctx, nil)
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ mgr := NewManager(ctx, nil, reg, sdMetrics)
+ require.NotNil(t, mgr)
mgr.updatert = updateDelay
go mgr.Run()
@@ -1312,19 +1402,14 @@ func TestCoordinationWithReceiver(t *testing.T) {
time.Sleep(expected.delay)
select {
case <-ctx.Done():
- t.Fatalf("step %d: no update received in the expected timeframe", i)
+ require.FailNow(t, "step %d: no update received in the expected timeframe", i)
case tgs, ok := <-mgr.SyncCh():
- if !ok {
- t.Fatalf("step %d: discovery manager channel is closed", i)
- }
- if len(tgs) != len(expected.tgs) {
- t.Fatalf("step %d: target groups mismatch, got: %d, expected: %d\ngot: %#v\nexpected: %#v",
- i, len(tgs), len(expected.tgs), tgs, expected.tgs)
- }
+ require.True(t, ok, "step %d: discovery manager channel is closed", i)
+ require.Equal(t, len(expected.tgs), len(tgs), "step %d: targets mismatch", i)
+
for k := range expected.tgs {
- if _, ok := tgs[k]; !ok {
- t.Fatalf("step %d: target group not found: %s\ngot: %#v", i, k, tgs)
- }
+ _, ok := tgs[k]
+ require.True(t, ok, "step %d: target group not found: %s", i, k)
assertEqualGroups(t, tgs[k], expected.tgs[k])
}
}
@@ -1392,10 +1477,15 @@ func (o onceProvider) Run(_ context.Context, ch chan<- []*targetgroup.Group) {
// TestTargetSetTargetGroupsUpdateDuringApplyConfig is used to detect races when
// ApplyConfig happens at the same time as targets update.
-func TestTargetSetTargetGroupsUpdateDuringApplyConfig(*testing.T) {
+func TestTargetSetTargetGroupsUpdateDuringApplyConfig(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- discoveryManager := NewManager(ctx, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ _, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ require.NotNil(t, discoveryManager)
discoveryManager.updatert = 100 * time.Millisecond
go discoveryManager.Run()
@@ -1456,6 +1546,11 @@ func newTestDiscoverer() *testDiscoverer {
}
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*testDiscoverer) NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics {
+ return &NoopDiscovererMetrics{}
+}
+
// Name implements Config.
func (t *testDiscoverer) Name() string {
return "test"
@@ -1477,3 +1572,24 @@ func (t *testDiscoverer) update(tgs []*targetgroup.Group) {
<-t.ready
t.up <- tgs
}
+
+func TestUnregisterMetrics(t *testing.T) {
+ reg := prometheus.NewRegistry()
+ // Check that all metrics can be unregistered, allowing a second manager to be created.
+ for i := 0; i < 2; i++ {
+ ctx, cancel := context.WithCancel(context.Background())
+
+ refreshMetrics, sdMetrics := NewTestMetrics(t, reg)
+
+ discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics)
+ // discoveryManager will be nil if there was an error configuring metrics.
+ require.NotNil(t, discoveryManager)
+ // Unregister all metrics.
+ discoveryManager.UnregisterMetrics()
+ for _, sdMetric := range sdMetrics {
+ sdMetric.Unregister()
+ }
+ refreshMetrics.Unregister()
+ cancel()
+ }
+}
diff --git a/discovery/marathon/marathon.go b/discovery/marathon/marathon.go
index cfd3e2c083..38b47accff 100644
--- a/discovery/marathon/marathon.go
+++ b/discovery/marathon/marathon.go
@@ -28,6 +28,7 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -48,7 +49,7 @@ const (
// imageLabel is the label that is used for the docker image running the service.
imageLabel model.LabelName = metaLabelPrefix + "image"
// portIndexLabel is the integer port index when multiple ports are defined;
- // e.g. PORT1 would have a value of '1'
+ // e.g. PORT1 would have a value of '1'.
portIndexLabel model.LabelName = metaLabelPrefix + "port_index"
// taskLabel contains the mesos task name of the app instance.
taskLabel model.LabelName = metaLabelPrefix + "task"
@@ -78,12 +79,19 @@ type SDConfig struct {
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &marathonMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "marathon" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(*c, opts.Logger)
+ return NewDiscovery(*c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -106,14 +114,16 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
if len(c.AuthToken) > 0 && len(c.AuthTokenFile) > 0 {
return errors.New("marathon_sd: at most one of auth_token & auth_token_file must be configured")
}
- if c.HTTPClientConfig.BasicAuth != nil && (len(c.AuthToken) > 0 || len(c.AuthTokenFile) > 0) {
- return errors.New("marathon_sd: at most one of basic_auth, auth_token & auth_token_file must be configured")
- }
- if (len(c.HTTPClientConfig.BearerToken) > 0 || len(c.HTTPClientConfig.BearerTokenFile) > 0) && (len(c.AuthToken) > 0 || len(c.AuthTokenFile) > 0) {
- return errors.New("marathon_sd: at most one of bearer_token, bearer_token_file, auth_token & auth_token_file must be configured")
- }
- if c.HTTPClientConfig.Authorization != nil && (len(c.AuthToken) > 0 || len(c.AuthTokenFile) > 0) {
- return errors.New("marathon_sd: at most one of auth_token, auth_token_file & authorization must be configured")
+
+ if len(c.AuthToken) > 0 || len(c.AuthTokenFile) > 0 {
+ switch {
+ case c.HTTPClientConfig.BasicAuth != nil:
+ return errors.New("marathon_sd: at most one of basic_auth, auth_token & auth_token_file must be configured")
+ case len(c.HTTPClientConfig.BearerToken) > 0 || len(c.HTTPClientConfig.BearerTokenFile) > 0:
+ return errors.New("marathon_sd: at most one of bearer_token, bearer_token_file, auth_token & auth_token_file must be configured")
+ case c.HTTPClientConfig.Authorization != nil:
+ return errors.New("marathon_sd: at most one of auth_token, auth_token_file & authorization must be configured")
+ }
}
return c.HTTPClientConfig.Validate()
}
@@ -130,7 +140,12 @@ type Discovery struct {
}
// NewDiscovery returns a new Marathon Discovery.
-func NewDiscovery(conf SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*marathonMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "marathon_sd")
if err != nil {
return nil, err
@@ -152,10 +167,13 @@ func NewDiscovery(conf SDConfig, logger log.Logger) (*Discovery, error) {
appsClient: fetchApps,
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "marathon",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "marathon",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -321,7 +339,7 @@ type appListClient func(ctx context.Context, client *http.Client, url string) (*
// fetchApps requests a list of applications from a marathon server.
func fetchApps(ctx context.Context, client *http.Client, url string) (*appList, error) {
- request, err := http.NewRequest("GET", url, nil)
+ request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
@@ -435,7 +453,6 @@ func targetsForApp(app *app) []model.LabelSet {
// Gather info about the app's 'tasks'. Each instance (container) is considered a task
// and can be reachable at one or more host:port endpoints.
for _, t := range app.Tasks {
-
// There are no labels to gather if only Ports is defined. (eg. with host networking)
// Ports can only be gathered from the Task (not from the app) and are guaranteed
// to be the same across all tasks. If we haven't gathered any ports by now,
@@ -446,7 +463,6 @@ func targetsForApp(app *app) []model.LabelSet {
// Iterate over the ports we gathered using one of the methods above.
for i, port := range ports {
-
// A zero port here means that either the portMapping has a zero port defined,
// or there is a portDefinition with requirePorts set to false. This means the port
// is auto-generated by Mesos and needs to be looked up in the task.
@@ -489,7 +505,7 @@ func targetEndpoint(task *task, port uint32, containerNet bool) string {
host = task.Host
}
- return net.JoinHostPort(host, fmt.Sprintf("%d", port))
+ return net.JoinHostPort(host, strconv.Itoa(int(port)))
}
// Get a list of ports and a list of labels from a PortMapping.
@@ -498,7 +514,6 @@ func extractPortMapping(portMappings []portMapping, containerNet bool) ([]uint32
labels := make([]map[string]string, len(portMappings))
for i := 0; i < len(portMappings); i++ {
-
labels[i] = portMappings[i].Labels
if containerNet {
diff --git a/discovery/marathon/marathon_test.go b/discovery/marathon/marathon_test.go
index 258e3c8ddf..659899f163 100644
--- a/discovery/marathon/marathon_test.go
+++ b/discovery/marathon/marathon_test.go
@@ -21,8 +21,11 @@ import (
"net/http/httptest"
"testing"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -36,7 +39,19 @@ func testConfig() SDConfig {
}
func testUpdateServices(client appListClient) ([]*targetgroup.Group, error) {
- md, err := NewDiscovery(testConfig(), nil)
+ cfg := testConfig()
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ err := metrics.Register()
+ if err != nil {
+ return nil, err
+ }
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ md, err := NewDiscovery(cfg, nil, metrics)
if err != nil {
return nil, err
}
@@ -54,23 +69,15 @@ func TestMarathonSDHandleError(t *testing.T) {
}
)
tgs, err := testUpdateServices(client)
- if !errors.Is(err, errTesting) {
- t.Fatalf("Expected error: %s", err)
- }
- if len(tgs) != 0 {
- t.Fatalf("Got group: %s", tgs)
- }
+ require.ErrorIs(t, err, errTesting)
+ require.Empty(t, tgs, "Expected no target groups.")
}
func TestMarathonSDEmptyList(t *testing.T) {
client := func(_ context.Context, _ *http.Client, _ string) (*appList, error) { return &appList{}, nil }
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) > 0 {
- t.Fatalf("Got group: %v", tgs)
- }
+ require.NoError(t, err)
+ require.Empty(t, tgs, "Expected no target groups.")
}
func marathonTestAppList(labels map[string]string, runningTasks int) *appList {
@@ -104,66 +111,49 @@ func TestMarathonSDSendGroup(t *testing.T) {
return marathonTestAppList(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 1, "Expected 1 target.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 1 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:31000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:31000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the first port.")
}
func TestMarathonSDRemoveApp(t *testing.T) {
- md, err := NewDiscovery(testConfig(), nil)
- if err != nil {
- t.Fatalf("%s", err)
- }
+ cfg := testConfig()
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ md, err := NewDiscovery(cfg, nil, metrics)
+ require.NoError(t, err)
md.appsClient = func(_ context.Context, _ *http.Client, _ string) (*appList, error) {
return marathonTestAppList(marathonValidLabel, 1), nil
}
tgs, err := md.refresh(context.Background())
- if err != nil {
- t.Fatalf("Got error on first update: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 targetgroup, got", len(tgs))
- }
+ require.NoError(t, err, "Got error on first update.")
+ require.Len(t, tgs, 1, "Expected 1 targetgroup.")
tg1 := tgs[0]
md.appsClient = func(_ context.Context, _ *http.Client, _ string) (*appList, error) {
return marathonTestAppList(marathonValidLabel, 0), nil
}
tgs, err = md.refresh(context.Background())
- if err != nil {
- t.Fatalf("Got error on second update: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 targetgroup, got", len(tgs))
- }
+ require.NoError(t, err, "Got error on second update.")
+ require.Len(t, tgs, 1, "Expected 1 targetgroup.")
+
tg2 := tgs[0]
- if tg2.Source != tg1.Source {
- if len(tg2.Targets) > 0 {
- t.Errorf("Got a non-empty target set: %s", tg2.Targets)
- }
- t.Fatalf("Source is different: %s != %s", tg1.Source, tg2.Source)
- }
+ require.NotEmpty(t, tg2.Targets, "Got a non-empty target set.")
+ require.Equal(t, tg1.Source, tg2.Source, "Source is different.")
}
func marathonTestAppListWithMultiplePorts(labels map[string]string, runningTasks int) *appList {
@@ -198,34 +188,22 @@ func TestMarathonSDSendGroupWithMultiplePort(t *testing.T) {
return marathonTestAppListWithMultiplePorts(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:31000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:31000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]),
+ "Wrong portMappings label from the first port: %s", tgt[model.AddressLabel])
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "mesos-slave1:32000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:32000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]),
+ "Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
}
func marathonTestZeroTaskPortAppList(labels map[string]string, runningTasks int) *appList {
@@ -255,20 +233,12 @@ func TestMarathonZeroTaskPorts(t *testing.T) {
return marathonTestZeroTaskPortAppList(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
- tg := tgs[0]
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
- if tg.Source != "test-service-zero-ports" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 0 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
+ tg := tgs[0]
+ require.Equal(t, "test-service-zero-ports", tg.Source, "Wrong target group name.")
+ require.Empty(t, tg.Targets, "Wrong number of targets.")
}
func Test500ErrorHttpResponseWithValidJSONBody(t *testing.T) {
@@ -283,9 +253,7 @@ func Test500ErrorHttpResponseWithValidJSONBody(t *testing.T) {
defer ts.Close()
// Execute test case and validate behavior.
_, err := testUpdateServices(nil)
- if err == nil {
- t.Fatalf("Expected error for 5xx HTTP response from marathon server, got nil")
- }
+ require.Error(t, err, "Expected error for 5xx HTTP response from marathon server.")
}
func marathonTestAppListWithPortDefinitions(labels map[string]string, runningTasks int) *appList {
@@ -323,40 +291,24 @@ func TestMarathonSDSendGroupWithPortDefinitions(t *testing.T) {
return marathonTestAppListWithPortDefinitions(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:1234" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portDefinitions label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:1234", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]),
+ "Wrong portMappings label from the first port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]),
+ "Wrong portDefinitions label from the first port.")
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "mesos-slave1:5678" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong portDefinitions label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:5678", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Empty(t, tgt[model.LabelName(portMappingLabelPrefix+"prometheus")], "Wrong portMappings label from the second port.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the second port.")
}
func marathonTestAppListWithPortDefinitionsRequirePorts(labels map[string]string, runningTasks int) *appList {
@@ -393,40 +345,22 @@ func TestMarathonSDSendGroupWithPortDefinitionsRequirePorts(t *testing.T) {
return marathonTestAppListWithPortDefinitionsRequirePorts(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:31000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portDefinitions label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:31000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the first port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the first port.")
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "mesos-slave1:32000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong portDefinitions label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:32000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the second port.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the second port.")
}
func marathonTestAppListWithPorts(labels map[string]string, runningTasks int) *appList {
@@ -458,40 +392,22 @@ func TestMarathonSDSendGroupWithPorts(t *testing.T) {
return marathonTestAppListWithPorts(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:31000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portDefinitions label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:31000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the first port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the first port.")
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "mesos-slave1:32000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portDefinitions label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:32000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the second port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the second port.")
}
func marathonTestAppListWithContainerPortMappings(labels map[string]string, runningTasks int) *appList {
@@ -532,40 +448,22 @@ func TestMarathonSDSendGroupWithContainerPortMappings(t *testing.T) {
return marathonTestAppListWithContainerPortMappings(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:12345" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portDefinitions label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:12345", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the first port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the first port.")
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "mesos-slave1:32000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portDefinitions label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:32000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the second port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the second port.")
}
func marathonTestAppListWithDockerContainerPortMappings(labels map[string]string, runningTasks int) *appList {
@@ -606,40 +504,22 @@ func TestMarathonSDSendGroupWithDockerContainerPortMappings(t *testing.T) {
return marathonTestAppListWithDockerContainerPortMappings(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "mesos-slave1:31000" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portDefinitions label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:31000", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the first port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the first port.")
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "mesos-slave1:12345" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portDefinitions label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "mesos-slave1:12345", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the second port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the second port.")
}
func marathonTestAppListWithContainerNetworkAndPortMappings(labels map[string]string, runningTasks int) *appList {
@@ -684,38 +564,20 @@ func TestMarathonSDSendGroupWithContainerNetworkAndPortMapping(t *testing.T) {
return marathonTestAppListWithContainerNetworkAndPortMappings(marathonValidLabel, 1), nil
}
tgs, err := testUpdateServices(client)
- if err != nil {
- t.Fatalf("Got error: %s", err)
- }
- if len(tgs) != 1 {
- t.Fatal("Expected 1 target group, got", len(tgs))
- }
+ require.NoError(t, err)
+ require.Len(t, tgs, 1, "Expected 1 target group.")
+
tg := tgs[0]
+ require.Equal(t, "test-service", tg.Source, "Wrong target group name.")
+ require.Len(t, tg.Targets, 2, "Wrong number of targets.")
- if tg.Source != "test-service" {
- t.Fatalf("Wrong target group name: %s", tg.Source)
- }
- if len(tg.Targets) != 2 {
- t.Fatalf("Wrong number of targets: %v", tg.Targets)
- }
tgt := tg.Targets[0]
- if tgt[model.AddressLabel] != "1.2.3.4:8080" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "yes" {
- t.Fatalf("Wrong first portMappings label from the first port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong first portDefinitions label from the first port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "1.2.3.4:8080", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "yes", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the first port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the first port.")
+
tgt = tg.Targets[1]
- if tgt[model.AddressLabel] != "1.2.3.4:1234" {
- t.Fatalf("Wrong target address: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portMappingLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portMappings label from the second port: %s", tgt[model.AddressLabel])
- }
- if tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")] != "" {
- t.Fatalf("Wrong portDefinitions label from the second port: %s", tgt[model.AddressLabel])
- }
+ require.Equal(t, "1.2.3.4:1234", string(tgt[model.AddressLabel]), "Wrong target address.")
+ require.Equal(t, "", string(tgt[model.LabelName(portMappingLabelPrefix+"prometheus")]), "Wrong portMappings label from the second port.")
+ require.Equal(t, "", string(tgt[model.LabelName(portDefinitionLabelPrefix+"prometheus")]), "Wrong portDefinitions label from the second port.")
}
diff --git a/discovery/marathon/metrics.go b/discovery/marathon/metrics.go
new file mode 100644
index 0000000000..790c167471
--- /dev/null
+++ b/discovery/marathon/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marathon
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*marathonMetrics)(nil)
+
+type marathonMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *marathonMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *marathonMetrics) Unregister() {}
diff --git a/discovery/metrics.go b/discovery/metrics.go
new file mode 100644
index 0000000000..356be1ddcb
--- /dev/null
+++ b/discovery/metrics.go
@@ -0,0 +1,100 @@
+// Copyright 2016 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package discovery
+
+import (
+ "fmt"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// Metrics to be used with a discovery manager.
+type Metrics struct {
+ FailedConfigs prometheus.Gauge
+ DiscoveredTargets *prometheus.GaugeVec
+ ReceivedUpdates prometheus.Counter
+ DelayedUpdates prometheus.Counter
+ SentUpdates prometheus.Counter
+}
+
+func NewManagerMetrics(registerer prometheus.Registerer, sdManagerName string) (*Metrics, error) {
+ m := &Metrics{}
+
+ m.FailedConfigs = prometheus.NewGauge(
+ prometheus.GaugeOpts{
+ Name: "prometheus_sd_failed_configs",
+ Help: "Current number of service discovery configurations that failed to load.",
+ ConstLabels: prometheus.Labels{"name": sdManagerName},
+ },
+ )
+
+ m.DiscoveredTargets = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Name: "prometheus_sd_discovered_targets",
+ Help: "Current number of discovered targets.",
+ ConstLabels: prometheus.Labels{"name": sdManagerName},
+ },
+ []string{"config"},
+ )
+
+ m.ReceivedUpdates = prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_received_updates_total",
+ Help: "Total number of update events received from the SD providers.",
+ ConstLabels: prometheus.Labels{"name": sdManagerName},
+ },
+ )
+
+ m.DelayedUpdates = prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_updates_delayed_total",
+ Help: "Total number of update events that couldn't be sent immediately.",
+ ConstLabels: prometheus.Labels{"name": sdManagerName},
+ },
+ )
+
+ m.SentUpdates = prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_updates_total",
+ Help: "Total number of update events sent to the SD consumers.",
+ ConstLabels: prometheus.Labels{"name": sdManagerName},
+ },
+ )
+
+ metrics := []prometheus.Collector{
+ m.FailedConfigs,
+ m.DiscoveredTargets,
+ m.ReceivedUpdates,
+ m.DelayedUpdates,
+ m.SentUpdates,
+ }
+
+ for _, collector := range metrics {
+ err := registerer.Register(collector)
+ if err != nil {
+ return nil, fmt.Errorf("failed to register discovery manager metrics: %w", err)
+ }
+ }
+
+ return m, nil
+}
+
+// Unregister unregisters all metrics.
+func (m *Metrics) Unregister(registerer prometheus.Registerer) {
+ registerer.Unregister(m.FailedConfigs)
+ registerer.Unregister(m.DiscoveredTargets)
+ registerer.Unregister(m.ReceivedUpdates)
+ registerer.Unregister(m.DelayedUpdates)
+ registerer.Unregister(m.SentUpdates)
+}
diff --git a/discovery/kubernetes/client_metrics.go b/discovery/metrics_k8s_client.go
similarity index 79%
rename from discovery/kubernetes/client_metrics.go
rename to discovery/metrics_k8s_client.go
index b316f7d885..c13ce53317 100644
--- a/discovery/kubernetes/client_metrics.go
+++ b/discovery/metrics_k8s_client.go
@@ -11,10 +11,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package kubernetes
+package discovery
import (
"context"
+ "fmt"
"net/url"
"time"
@@ -23,13 +24,27 @@ import (
"k8s.io/client-go/util/workqueue"
)
-const workqueueMetricsNamespace = metricsNamespace + "_workqueue"
+// This file registers metrics used by the Kubernetes Go client (k8s.io/client-go).
+// Unfortunately, k8s.io/client-go metrics are global.
+// If we instantiate multiple k8s SD instances, their k8s/client-go metrics will overlap.
+// To prevent us from displaying misleading metrics, we register k8s.io/client-go metrics
+// outside of the Kubernetes SD.
+
+const (
+ KubernetesMetricsNamespace = "prometheus_sd_kubernetes"
+ workqueueMetricsNamespace = KubernetesMetricsNamespace + "_workqueue"
+)
+
+var (
+ clientGoRequestMetrics = &clientGoRequestMetricAdapter{}
+ clientGoWorkloadMetrics = &clientGoWorkqueueMetricsProvider{}
+)
var (
// Metrics for client-go's HTTP requests.
clientGoRequestResultMetricVec = prometheus.NewCounterVec(
prometheus.CounterOpts{
- Namespace: metricsNamespace,
+ Namespace: KubernetesMetricsNamespace,
Name: "http_request_total",
Help: "Total number of HTTP requests to the Kubernetes API by status code.",
},
@@ -37,7 +52,7 @@ var (
)
clientGoRequestLatencyMetricVec = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
- Namespace: metricsNamespace,
+ Namespace: KubernetesMetricsNamespace,
Name: "http_request_duration_seconds",
Help: "Summary of latencies for HTTP requests to the Kubernetes API by endpoint.",
Objectives: map[float64]float64{},
@@ -45,7 +60,7 @@ var (
[]string{"endpoint"},
)
- // Definition of metrics for client-go workflow metrics provider
+ // Definition of metrics for client-go workflow metrics provider.
clientGoWorkqueueDepthMetricVec = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: workqueueMetricsNamespace,
@@ -106,20 +121,44 @@ func (noopMetric) Dec() {}
func (noopMetric) Observe(float64) {}
func (noopMetric) Set(float64) {}
-// Definition of client-go metrics adapters for HTTP requests observation
+// Definition of client-go metrics adapters for HTTP requests observation.
type clientGoRequestMetricAdapter struct{}
-func (f *clientGoRequestMetricAdapter) Register(registerer prometheus.Registerer) {
+// Returns all of the Prometheus metrics derived from k8s.io/client-go.
+// This may be used tu register and unregister the metrics.
+func clientGoMetrics() []prometheus.Collector {
+ return []prometheus.Collector{
+ clientGoRequestResultMetricVec,
+ clientGoRequestLatencyMetricVec,
+ clientGoWorkqueueDepthMetricVec,
+ clientGoWorkqueueAddsMetricVec,
+ clientGoWorkqueueLatencyMetricVec,
+ clientGoWorkqueueUnfinishedWorkSecondsMetricVec,
+ clientGoWorkqueueLongestRunningProcessorMetricVec,
+ clientGoWorkqueueWorkDurationMetricVec,
+ }
+}
+
+func RegisterK8sClientMetricsWithPrometheus(registerer prometheus.Registerer) error {
+ clientGoRequestMetrics.RegisterWithK8sGoClient()
+ clientGoWorkloadMetrics.RegisterWithK8sGoClient()
+
+ for _, collector := range clientGoMetrics() {
+ err := registerer.Register(collector)
+ if err != nil {
+ return fmt.Errorf("failed to register Kubernetes Go Client metrics: %w", err)
+ }
+ }
+ return nil
+}
+
+func (f *clientGoRequestMetricAdapter) RegisterWithK8sGoClient() {
metrics.Register(
metrics.RegisterOpts{
RequestLatency: f,
RequestResult: f,
},
)
- registerer.MustRegister(
- clientGoRequestResultMetricVec,
- clientGoRequestLatencyMetricVec,
- )
}
func (clientGoRequestMetricAdapter) Increment(_ context.Context, code, _, _ string) {
@@ -130,19 +169,11 @@ func (clientGoRequestMetricAdapter) Observe(_ context.Context, _ string, u url.U
clientGoRequestLatencyMetricVec.WithLabelValues(u.EscapedPath()).Observe(latency.Seconds())
}
-// Definition of client-go workqueue metrics provider definition
+// Definition of client-go workqueue metrics provider definition.
type clientGoWorkqueueMetricsProvider struct{}
-func (f *clientGoWorkqueueMetricsProvider) Register(registerer prometheus.Registerer) {
+func (f *clientGoWorkqueueMetricsProvider) RegisterWithK8sGoClient() {
workqueue.SetProvider(f)
- registerer.MustRegister(
- clientGoWorkqueueDepthMetricVec,
- clientGoWorkqueueAddsMetricVec,
- clientGoWorkqueueLatencyMetricVec,
- clientGoWorkqueueWorkDurationMetricVec,
- clientGoWorkqueueUnfinishedWorkSecondsMetricVec,
- clientGoWorkqueueLongestRunningProcessorMetricVec,
- )
}
func (f *clientGoWorkqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric {
diff --git a/discovery/metrics_refresh.go b/discovery/metrics_refresh.go
new file mode 100644
index 0000000000..ef49e591a3
--- /dev/null
+++ b/discovery/metrics_refresh.go
@@ -0,0 +1,75 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package discovery
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// RefreshMetricsVecs are metric vectors for the "refresh" package.
+// We define them here in the "discovery" package in order to avoid a cyclic dependency between
+// "discovery" and "refresh".
+type RefreshMetricsVecs struct {
+ failuresVec *prometheus.CounterVec
+ durationVec *prometheus.SummaryVec
+
+ metricRegisterer MetricRegisterer
+}
+
+var _ RefreshMetricsManager = (*RefreshMetricsVecs)(nil)
+
+func NewRefreshMetrics(reg prometheus.Registerer) RefreshMetricsManager {
+ m := &RefreshMetricsVecs{
+ failuresVec: prometheus.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_refresh_failures_total",
+ Help: "Number of refresh failures for the given SD mechanism.",
+ },
+ []string{"mechanism"}),
+ durationVec: prometheus.NewSummaryVec(
+ prometheus.SummaryOpts{
+ Name: "prometheus_sd_refresh_duration_seconds",
+ Help: "The duration of a refresh in seconds for the given SD mechanism.",
+ Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
+ },
+ []string{"mechanism"}),
+ }
+
+ // The reason we register metric vectors instead of metrics is so that
+ // the metrics are not visible until they are recorded.
+ m.metricRegisterer = NewMetricRegisterer(reg, []prometheus.Collector{
+ m.failuresVec,
+ m.durationVec,
+ })
+
+ return m
+}
+
+// Instantiate returns metrics out of metric vectors.
+func (m *RefreshMetricsVecs) Instantiate(mech string) *RefreshMetrics {
+ return &RefreshMetrics{
+ Failures: m.failuresVec.WithLabelValues(mech),
+ Duration: m.durationVec.WithLabelValues(mech),
+ }
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *RefreshMetricsVecs) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *RefreshMetricsVecs) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/moby/docker.go b/discovery/moby/docker.go
index 162833ece4..68f6fe3ccc 100644
--- a/discovery/moby/docker.go
+++ b/discovery/moby/docker.go
@@ -19,13 +19,17 @@ import (
"net"
"net/http"
"net/url"
+ "sort"
"strconv"
"time"
"github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
+ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -57,6 +61,7 @@ var DefaultDockerSDConfig = DockerSDConfig{
Filters: []Filter{},
HostNetworkingHost: "localhost",
HTTPClientConfig: config.DefaultHTTPClientConfig,
+ MatchFirstNetwork: true,
}
func init() {
@@ -72,7 +77,15 @@ type DockerSDConfig struct {
Filters []Filter `yaml:"filters"`
HostNetworkingHost string `yaml:"host_networking_host"`
- RefreshInterval model.Duration `yaml:"refresh_interval"`
+ RefreshInterval model.Duration `yaml:"refresh_interval"`
+ MatchFirstNetwork bool `yaml:"match_first_network"`
+}
+
+// NewDiscovererMetrics implements discovery.Config.
+func (*DockerSDConfig) NewDiscovererMetrics(_ prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &dockerMetrics{
+ refreshMetrics: rmi,
+ }
}
// Name returns the name of the Config.
@@ -80,7 +93,7 @@ func (*DockerSDConfig) Name() string { return "docker" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *DockerSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDockerDiscovery(c, opts.Logger)
+ return NewDockerDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -111,15 +124,20 @@ type DockerDiscovery struct {
port int
hostNetworkingHost string
filters filters.Args
+ matchFirstNetwork bool
}
// NewDockerDiscovery returns a new DockerDiscovery which periodically refreshes its targets.
-func NewDockerDiscovery(conf *DockerSDConfig, logger log.Logger) (*DockerDiscovery, error) {
- var err error
+func NewDockerDiscovery(conf *DockerSDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*DockerDiscovery, error) {
+ m, ok := metrics.(*dockerMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
d := &DockerDiscovery{
port: conf.Port,
hostNetworkingHost: conf.HostNetworkingHost,
+ matchFirstNetwork: conf.MatchFirstNetwork,
}
hostURL, err := url.Parse(conf.Host)
@@ -165,10 +183,13 @@ func NewDockerDiscovery(conf *DockerSDConfig, logger log.Logger) (*DockerDiscove
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "docker",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "docker",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -178,7 +199,7 @@ func (d *DockerDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
Source: "Docker",
}
- containers, err := d.client.ContainerList(ctx, types.ContainerListOptions{Filters: d.filters})
+ containers, err := d.client.ContainerList(ctx, container.ListOptions{Filters: d.filters})
if err != nil {
return nil, fmt.Errorf("error while listing containers: %w", err)
}
@@ -188,6 +209,11 @@ func (d *DockerDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
return nil, fmt.Errorf("error while computing network labels: %w", err)
}
+ allContainers := make(map[string]types.Container)
+ for _, c := range containers {
+ allContainers[c.ID] = c
+ }
+
for _, c := range containers {
if len(c.Names) == 0 {
continue
@@ -204,7 +230,48 @@ func (d *DockerDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
commonLabels[dockerLabelContainerLabelPrefix+ln] = v
}
- for _, n := range c.NetworkSettings.Networks {
+ networks := c.NetworkSettings.Networks
+ containerNetworkMode := container.NetworkMode(c.HostConfig.NetworkMode)
+ if len(networks) == 0 {
+ // Try to lookup shared networks
+ for {
+ if containerNetworkMode.IsContainer() {
+ tmpContainer, exists := allContainers[containerNetworkMode.ConnectedContainer()]
+ if !exists {
+ break
+ }
+ networks = tmpContainer.NetworkSettings.Networks
+ containerNetworkMode = container.NetworkMode(tmpContainer.HostConfig.NetworkMode)
+ if len(networks) > 0 {
+ break
+ }
+ } else {
+ break
+ }
+ }
+ }
+
+ if d.matchFirstNetwork && len(networks) > 1 {
+ // Sort networks by name and take first non-nil network.
+ keys := make([]string, 0, len(networks))
+ for k, n := range networks {
+ if n != nil {
+ keys = append(keys, k)
+ }
+ }
+ if len(keys) > 0 {
+ sort.Strings(keys)
+ firstNetworkMode := keys[0]
+ firstNetwork := networks[firstNetworkMode]
+ networks = map[string]*network.EndpointSettings{firstNetworkMode: firstNetwork}
+ }
+ }
+
+ for _, n := range networks {
+ if n == nil {
+ continue
+ }
+
var added bool
for _, p := range c.Ports {
diff --git a/discovery/moby/docker_test.go b/discovery/moby/docker_test.go
index bb84b1571a..398393a15a 100644
--- a/discovery/moby/docker_test.go
+++ b/discovery/moby/docker_test.go
@@ -16,12 +16,16 @@ package moby
import (
"context"
"fmt"
+ "sort"
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
+
+ "github.com/prometheus/prometheus/discovery"
)
func TestDockerSDRefresh(t *testing.T) {
@@ -37,21 +41,206 @@ host: %s
var cfg DockerSDConfig
require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg))
- d, err := NewDockerDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDockerDiscovery(&cfg, log.NewNopLogger(), metrics)
+ require.NoError(t, err)
+
+ ctx := context.Background()
+ tgs, err := d.refresh(ctx)
+ require.NoError(t, err)
+
+ require.Len(t, tgs, 1)
+
+ tg := tgs[0]
+ require.NotNil(t, tg)
+ require.NotNil(t, tg.Targets)
+ require.Len(t, tg.Targets, 8)
+
+ expected := []model.LabelSet{
+ {
+ "__address__": "172.19.0.2:9100",
+ "__meta_docker_container_id": "c301b928faceb1a18fe379f6bc178727ef920bb30b0f9b8592b32b36255a0eca",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "node",
+ "__meta_docker_container_label_com_docker_compose_version": "1.25.0",
+ "__meta_docker_container_label_maintainer": "The Prometheus Authors ",
+ "__meta_docker_container_label_prometheus_job": "node",
+ "__meta_docker_container_name": "/dockersd_node_1",
+ "__meta_docker_container_network_mode": "dockersd_default",
+ "__meta_docker_network_id": "7189986ab399e144e52a71b7451b4e04e2158c044b4cd2f3ae26fc3a285d3798",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.19.0.2",
+ "__meta_docker_network_label_com_docker_compose_network": "default",
+ "__meta_docker_network_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_network_label_com_docker_compose_version": "1.25.0",
+ "__meta_docker_network_name": "dockersd_default",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "9100",
+ },
+ {
+ "__address__": "172.19.0.3:80",
+ "__meta_docker_container_id": "c301b928faceb1a18fe379f6bc178727ef920bb30b0f9b8592b32b36255a0eca",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "noport",
+ "__meta_docker_container_label_com_docker_compose_version": "1.25.0",
+ "__meta_docker_container_label_maintainer": "The Prometheus Authors ",
+ "__meta_docker_container_label_prometheus_job": "noport",
+ "__meta_docker_container_name": "/dockersd_noport_1",
+ "__meta_docker_container_network_mode": "dockersd_default",
+ "__meta_docker_network_id": "7189986ab399e144e52a71b7451b4e04e2158c044b4cd2f3ae26fc3a285d3798",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.19.0.3",
+ "__meta_docker_network_label_com_docker_compose_network": "default",
+ "__meta_docker_network_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_network_label_com_docker_compose_version": "1.25.0",
+ "__meta_docker_network_name": "dockersd_default",
+ "__meta_docker_network_scope": "local",
+ },
+ {
+ "__address__": "localhost",
+ "__meta_docker_container_id": "54ed6cc5c0988260436cb0e739b7b6c9cad6c439a93b4c4fdbe9753e1c94b189",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "host_networking",
+ "__meta_docker_container_label_com_docker_compose_version": "1.25.0",
+ "__meta_docker_container_name": "/dockersd_host_networking_1",
+ "__meta_docker_container_network_mode": "host",
+ "__meta_docker_network_ip": "",
+ },
+ {
+ "__address__": "172.20.0.2:3306",
+ "__meta_docker_container_id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_mysql",
+ "__meta_docker_container_network_mode": "dockersd_private",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.2",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "3306",
+ },
+ {
+ "__address__": "172.20.0.2:33060",
+ "__meta_docker_container_id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_mysql",
+ "__meta_docker_container_network_mode": "dockersd_private",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.2",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "33060",
+ },
+ {
+ "__address__": "172.20.0.2:9104",
+ "__meta_docker_container_id": "59bf76e8816af98856b90dd619c91027145ca501043b1c51756d03b085882e06",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysqlexporter",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_label_maintainer": "The Prometheus Authors ",
+ "__meta_docker_container_name": "/dockersd_mysql_exporter",
+ "__meta_docker_container_network_mode": "container:f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.2",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "9104",
+ },
+ {
+ "__address__": "172.20.0.3:3306",
+ "__meta_docker_container_id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_multi_networks",
+ "__meta_docker_container_network_mode": "dockersd_private_none",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.3",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "3306",
+ },
+ {
+ "__address__": "172.20.0.3:33060",
+ "__meta_docker_container_id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_multi_networks",
+ "__meta_docker_container_network_mode": "dockersd_private_none",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.3",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "33060",
+ },
+ }
+ sortFunc(expected)
+ sortFunc(tg.Targets)
+
+ for i, lbls := range expected {
+ t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
+ require.Equal(t, lbls, tg.Targets[i])
+ })
+ }
+}
+
+func TestDockerSDRefreshMatchAllNetworks(t *testing.T) {
+ sdmock := NewSDMock(t, "dockerprom")
+ sdmock.Setup()
+
+ e := sdmock.Endpoint()
+ url := e[:len(e)-1]
+ cfgString := fmt.Sprintf(`
+---
+host: %s
+`, url)
+ var cfg DockerSDConfig
+ require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg))
+
+ cfg.MatchFirstNetwork = false
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+ d, err := NewDockerDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 3, len(tg.Targets))
+ require.Len(t, tg.Targets, 13)
- for i, lbls := range []model.LabelSet{
+ expected := []model.LabelSet{
{
"__address__": "172.19.0.2:9100",
"__meta_docker_container_id": "c301b928faceb1a18fe379f6bc178727ef920bb30b0f9b8592b32b36255a0eca",
@@ -103,9 +292,182 @@ host: %s
"__meta_docker_container_network_mode": "host",
"__meta_docker_network_ip": "",
},
- } {
+ {
+ "__address__": "172.20.0.2:3306",
+ "__meta_docker_container_id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_mysql",
+ "__meta_docker_container_network_mode": "dockersd_private",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.2",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "3306",
+ },
+ {
+ "__address__": "172.20.0.2:33060",
+ "__meta_docker_container_id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_mysql",
+ "__meta_docker_container_network_mode": "dockersd_private",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.2",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "33060",
+ },
+ {
+ "__address__": "172.21.0.2:3306",
+ "__meta_docker_container_id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_mysql",
+ "__meta_docker_container_network_mode": "dockersd_private",
+ "__meta_docker_network_id": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.21.0.2",
+ "__meta_docker_network_name": "dockersd_private1",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "3306",
+ },
+ {
+ "__address__": "172.21.0.2:33060",
+ "__meta_docker_container_id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_mysql",
+ "__meta_docker_container_network_mode": "dockersd_private",
+ "__meta_docker_network_id": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.21.0.2",
+ "__meta_docker_network_name": "dockersd_private1",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "33060",
+ },
+ {
+ "__address__": "172.21.0.2:9104",
+ "__meta_docker_container_id": "59bf76e8816af98856b90dd619c91027145ca501043b1c51756d03b085882e06",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysqlexporter",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_label_maintainer": "The Prometheus Authors ",
+ "__meta_docker_container_name": "/dockersd_mysql_exporter",
+ "__meta_docker_container_network_mode": "container:f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_network_id": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.21.0.2",
+ "__meta_docker_network_name": "dockersd_private1",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "9104",
+ },
+ {
+ "__address__": "172.20.0.2:9104",
+ "__meta_docker_container_id": "59bf76e8816af98856b90dd619c91027145ca501043b1c51756d03b085882e06",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysqlexporter",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_label_maintainer": "The Prometheus Authors ",
+ "__meta_docker_container_name": "/dockersd_mysql_exporter",
+ "__meta_docker_container_network_mode": "container:f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.2",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "9104",
+ },
+ {
+ "__address__": "172.20.0.3:3306",
+ "__meta_docker_container_id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_multi_networks",
+ "__meta_docker_container_network_mode": "dockersd_private_none",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.3",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "3306",
+ },
+ {
+ "__address__": "172.20.0.3:33060",
+ "__meta_docker_container_id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_multi_networks",
+ "__meta_docker_container_network_mode": "dockersd_private_none",
+ "__meta_docker_network_id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.20.0.3",
+ "__meta_docker_network_name": "dockersd_private",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "33060",
+ },
+ {
+ "__address__": "172.21.0.3:3306",
+ "__meta_docker_container_id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_multi_networks",
+ "__meta_docker_container_network_mode": "dockersd_private_none",
+ "__meta_docker_network_id": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.21.0.3",
+ "__meta_docker_network_name": "dockersd_private1",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "3306",
+ },
+ {
+ "__address__": "172.21.0.3:33060",
+ "__meta_docker_container_id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "__meta_docker_container_label_com_docker_compose_project": "dockersd",
+ "__meta_docker_container_label_com_docker_compose_service": "mysql",
+ "__meta_docker_container_label_com_docker_compose_version": "2.2.2",
+ "__meta_docker_container_name": "/dockersd_multi_networks",
+ "__meta_docker_container_network_mode": "dockersd_private_none",
+ "__meta_docker_network_id": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "__meta_docker_network_ingress": "false",
+ "__meta_docker_network_internal": "false",
+ "__meta_docker_network_ip": "172.21.0.3",
+ "__meta_docker_network_name": "dockersd_private1",
+ "__meta_docker_network_scope": "local",
+ "__meta_docker_port_private": "33060",
+ },
+ }
+
+ sortFunc(expected)
+ sortFunc(tg.Targets)
+
+ for i, lbls := range expected {
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
require.Equal(t, lbls, tg.Targets[i])
})
}
}
+
+func sortFunc(labelSets []model.LabelSet) {
+ sort.Slice(labelSets, func(i, j int) bool {
+ return labelSets[i]["__address__"] < labelSets[j]["__address__"]
+ })
+}
diff --git a/discovery/moby/dockerswarm.go b/discovery/moby/dockerswarm.go
index 371f9d5ed1..b0147467d2 100644
--- a/discovery/moby/dockerswarm.go
+++ b/discovery/moby/dockerswarm.go
@@ -23,6 +23,7 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/version"
@@ -69,12 +70,19 @@ type Filter struct {
Values []string `yaml:"values"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*DockerSwarmSDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &dockerswarmMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*DockerSwarmSDConfig) Name() string { return "dockerswarm" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *DockerSwarmSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -117,8 +125,11 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *DockerSwarmSDConfig, logger log.Logger) (*Discovery, error) {
- var err error
+func NewDiscovery(conf *DockerSwarmSDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*dockerswarmMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
d := &Discovery{
port: conf.Port,
@@ -168,10 +179,13 @@ func NewDiscovery(conf *DockerSwarmSDConfig, logger log.Logger) (*Discovery, err
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "dockerswarm",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "dockerswarm",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
diff --git a/discovery/moby/metrics_docker.go b/discovery/moby/metrics_docker.go
new file mode 100644
index 0000000000..457d520a0c
--- /dev/null
+++ b/discovery/moby/metrics_docker.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package moby
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*dockerMetrics)(nil)
+
+type dockerMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *dockerMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *dockerMetrics) Unregister() {}
diff --git a/discovery/moby/metrics_dockerswarm.go b/discovery/moby/metrics_dockerswarm.go
new file mode 100644
index 0000000000..227ba6c2b0
--- /dev/null
+++ b/discovery/moby/metrics_dockerswarm.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package moby
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*dockerswarmMetrics)(nil)
+
+type dockerswarmMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *dockerswarmMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *dockerswarmMetrics) Unregister() {}
diff --git a/discovery/moby/mock_test.go b/discovery/moby/mock_test.go
index f6511ed26f..7ef5cb07c3 100644
--- a/discovery/moby/mock_test.go
+++ b/discovery/moby/mock_test.go
@@ -29,7 +29,7 @@ import (
"github.com/prometheus/prometheus/util/strutil"
)
-// SDMock is the interface for the DigitalOcean mock
+// SDMock is the interface for the DigitalOcean mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
@@ -47,12 +47,12 @@ func NewSDMock(t *testing.T, directory string) *SDMock {
}
}
-// Endpoint returns the URI to the mock server
+// Endpoint returns the URI to the mock server.
func (m *SDMock) Endpoint() string {
return m.Server.URL + "/"
}
-// Setup creates the mock server
+// Setup creates the mock server.
func (m *SDMock) Setup() {
m.Mux = http.NewServeMux()
m.Server = httptest.NewServer(m.Mux)
@@ -98,7 +98,7 @@ func (m *SDMock) SetupHandlers() {
if len(query) == 2 {
h := sha1.New()
h.Write([]byte(query[1]))
- // Avoing long filenames for Windows.
+ // Avoiding long filenames for Windows.
f += "__" + base64.URLEncoding.EncodeToString(h.Sum(nil))[:10]
}
}
diff --git a/discovery/moby/network.go b/discovery/moby/network.go
index 0e0d0041de..ea1ca66bc7 100644
--- a/discovery/moby/network.go
+++ b/discovery/moby/network.go
@@ -15,9 +15,9 @@ package moby
import (
"context"
- "fmt"
+ "strconv"
- "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/prometheus/prometheus/util/strutil"
@@ -34,7 +34,7 @@ const (
)
func getNetworksLabels(ctx context.Context, client *client.Client, labelPrefix string) (map[string]map[string]string, error) {
- networks, err := client.NetworkList(ctx, types.NetworkListOptions{})
+ networks, err := client.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
@@ -44,8 +44,8 @@ func getNetworksLabels(ctx context.Context, client *client.Client, labelPrefix s
labelPrefix + labelNetworkID: network.ID,
labelPrefix + labelNetworkName: network.Name,
labelPrefix + labelNetworkScope: network.Scope,
- labelPrefix + labelNetworkInternal: fmt.Sprintf("%t", network.Internal),
- labelPrefix + labelNetworkIngress: fmt.Sprintf("%t", network.Ingress),
+ labelPrefix + labelNetworkInternal: strconv.FormatBool(network.Internal),
+ labelPrefix + labelNetworkIngress: strconv.FormatBool(network.Ingress),
}
for k, v := range network.Labels {
ln := strutil.SanitizeLabelName(k)
diff --git a/discovery/moby/nodes.go b/discovery/moby/nodes.go
index 85092f9071..b5be844eda 100644
--- a/discovery/moby/nodes.go
+++ b/discovery/moby/nodes.go
@@ -66,7 +66,7 @@ func (d *Discovery) refreshNodes(ctx context.Context) ([]*targetgroup.Group, err
swarmLabelNodeAddress: model.LabelValue(n.Status.Addr),
}
if n.ManagerStatus != nil {
- labels[swarmLabelNodeManagerLeader] = model.LabelValue(fmt.Sprintf("%t", n.ManagerStatus.Leader))
+ labels[swarmLabelNodeManagerLeader] = model.LabelValue(strconv.FormatBool(n.ManagerStatus.Leader))
labels[swarmLabelNodeManagerReachability] = model.LabelValue(n.ManagerStatus.Reachability)
labels[swarmLabelNodeManagerAddr] = model.LabelValue(n.ManagerStatus.Addr)
}
@@ -80,7 +80,6 @@ func (d *Discovery) refreshNodes(ctx context.Context) ([]*targetgroup.Group, err
labels[model.AddressLabel] = model.LabelValue(addr)
tg.Targets = append(tg.Targets, labels)
-
}
return []*targetgroup.Group{tg}, nil
}
diff --git a/discovery/moby/nodes_test.go b/discovery/moby/nodes_test.go
index 1a53321378..4ad1088d1a 100644
--- a/discovery/moby/nodes_test.go
+++ b/discovery/moby/nodes_test.go
@@ -19,9 +19,12 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
+
+ "github.com/prometheus/prometheus/discovery"
)
func TestDockerSwarmNodesSDRefresh(t *testing.T) {
@@ -38,19 +41,26 @@ host: %s
var cfg DockerSwarmSDConfig
require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg))
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 5, len(tg.Targets))
+ require.Len(t, tg.Targets, 5)
for i, lbls := range []model.LabelSet{
{
diff --git a/discovery/moby/services.go b/discovery/moby/services.go
index 1d472b5c00..c61b499259 100644
--- a/discovery/moby/services.go
+++ b/discovery/moby/services.go
@@ -116,7 +116,7 @@ func (d *Discovery) refreshServices(ctx context.Context) ([]*targetgroup.Group,
labels[model.LabelName(k)] = model.LabelValue(v)
}
- addr := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", d.port))
+ addr := net.JoinHostPort(ip.String(), strconv.Itoa(d.port))
labels[model.AddressLabel] = model.LabelValue(addr)
tg.Targets = append(tg.Targets, labels)
diff --git a/discovery/moby/services_test.go b/discovery/moby/services_test.go
index 1bc9832c7a..47ca69e33a 100644
--- a/discovery/moby/services_test.go
+++ b/discovery/moby/services_test.go
@@ -19,9 +19,12 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
+
+ "github.com/prometheus/prometheus/discovery"
)
func TestDockerSwarmSDServicesRefresh(t *testing.T) {
@@ -38,19 +41,26 @@ host: %s
var cfg DockerSwarmSDConfig
require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg))
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 15, len(tg.Targets))
+ require.Len(t, tg.Targets, 15)
for i, lbls := range []model.LabelSet{
{
@@ -332,19 +342,26 @@ filters:
var cfg DockerSwarmSDConfig
require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg))
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg, "tg should not be nil")
require.NotNil(t, tg.Targets, "tg.targets should not be nil")
- require.Equal(t, 4, len(tg.Targets))
+ require.Len(t, tg.Targets, 4)
for i, lbls := range []model.LabelSet{
{
diff --git a/discovery/moby/tasks.go b/discovery/moby/tasks.go
index 2505a7b07a..38b9d33de2 100644
--- a/discovery/moby/tasks.go
+++ b/discovery/moby/tasks.go
@@ -150,7 +150,7 @@ func (d *Discovery) refreshTasks(ctx context.Context) ([]*targetgroup.Group, err
labels[model.LabelName(k)] = model.LabelValue(v)
}
- addr := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", d.port))
+ addr := net.JoinHostPort(ip.String(), strconv.Itoa(d.port))
labels[model.AddressLabel] = model.LabelValue(addr)
tg.Targets = append(tg.Targets, labels)
diff --git a/discovery/moby/tasks_test.go b/discovery/moby/tasks_test.go
index 2cc9322f61..ef71bc02f5 100644
--- a/discovery/moby/tasks_test.go
+++ b/discovery/moby/tasks_test.go
@@ -19,9 +19,12 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
+
+ "github.com/prometheus/prometheus/discovery"
)
func TestDockerSwarmTasksSDRefresh(t *testing.T) {
@@ -38,19 +41,26 @@ host: %s
var cfg DockerSwarmSDConfig
require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg))
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 27, len(tg.Targets))
+ require.Len(t, tg.Targets, 27)
for i, lbls := range []model.LabelSet{
{
diff --git a/discovery/moby/testdata/dockerprom/containers/json.json b/discovery/moby/testdata/dockerprom/containers/json.json
index 37f575d22c..33406bf9a4 100644
--- a/discovery/moby/testdata/dockerprom/containers/json.json
+++ b/discovery/moby/testdata/dockerprom/containers/json.json
@@ -128,5 +128,174 @@
}
},
"Mounts": []
+ },
+ {
+ "Id": "f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8",
+ "Names": [
+ "/dockersd_mysql"
+ ],
+ "Image": "mysql:5.7.29",
+ "ImageID": "sha256:5d9483f9a7b21c87e0f5b9776c3e06567603c28c0062013eda127c968175f5e8",
+ "Command": "mysqld",
+ "Created": 1616273136,
+ "Ports": [
+ {
+ "PrivatePort": 3306,
+ "Type": "tcp"
+ },
+ {
+ "PrivatePort": 33060,
+ "Type": "tcp"
+ }
+ ],
+ "Labels": {
+ "com.docker.compose.project": "dockersd",
+ "com.docker.compose.service": "mysql",
+ "com.docker.compose.version": "2.2.2"
+ },
+ "State": "running",
+ "Status": "Up 40 seconds",
+ "HostConfig": {
+ "NetworkMode": "dockersd_private"
+ },
+ "NetworkSettings": {
+ "Networks": {
+ "dockersd_private": {
+ "IPAMConfig": null,
+ "Links": null,
+ "Aliases": null,
+ "NetworkID": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "EndpointID": "80f8a61b37701a9991bb98c75ddd23fd9b7c16b5575ca81343f6b44ff4a2a9d9",
+ "Gateway": "172.20.0.1",
+ "IPAddress": "172.20.0.2",
+ "IPPrefixLen": 16,
+ "IPv6Gateway": "",
+ "GlobalIPv6Address": "",
+ "GlobalIPv6PrefixLen": 0,
+ "MacAddress": "02:42:ac:14:00:0a",
+ "DriverOpts": null
+ },
+ "dockersd_private1": {
+ "IPAMConfig": {},
+ "Links": null,
+ "Aliases": [
+ "mysql",
+ "mysql",
+ "f9ade4b83199"
+ ],
+ "NetworkID": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "EndpointID": "f80921d10e78c99a5907705aae75befea40c3d3e9f820e66ab392f7274be16b8",
+ "Gateway": "172.21.0.1",
+ "IPAddress": "172.21.0.2",
+ "IPPrefixLen": 24,
+ "IPv6Gateway": "",
+ "GlobalIPv6Address": "",
+ "GlobalIPv6PrefixLen": 0,
+ "MacAddress": "02:42:ac:15:00:02",
+ "DriverOpts": null
+ }
+ }
+ },
+ "Mounts": []
+ },
+ {
+ "Id": "59bf76e8816af98856b90dd619c91027145ca501043b1c51756d03b085882e06",
+ "Names": [
+ "/dockersd_mysql_exporter"
+ ],
+ "Image": "prom/mysqld-exporter:latest",
+ "ImageID": "sha256:121b8a7cd0525dd89aaec58ad7d34c3bb3714740e5a67daf6510ccf71ab219a9",
+ "Command": "/bin/mysqld_exporter",
+ "Created": 1616273136,
+ "Ports": [
+ {
+ "PrivatePort": 9104,
+ "Type": "tcp"
+ }
+ ],
+ "Labels": {
+ "com.docker.compose.project": "dockersd",
+ "com.docker.compose.service": "mysqlexporter",
+ "com.docker.compose.version": "2.2.2",
+ "maintainer": "The Prometheus Authors "
+ },
+ "State": "running",
+ "Status": "Up 40 seconds",
+ "HostConfig": {
+ "NetworkMode": "container:f9ade4b83199d6f83020b7c0bfd1e8281b19dbf9e6cef2cf89bc45c8f8d20fe8"
+ },
+ "NetworkSettings": {
+ "Networks": {}
+ },
+ "Mounts": []
+ },
+ {
+ "Id": "f84b2a0cfaa58d9e70b0657e2b3c6f44f0e973de4163a871299b4acf127b224f",
+ "Names": [
+ "/dockersd_multi_networks"
+ ],
+ "Image": "mysql:5.7.29",
+ "ImageID": "sha256:16ae2f4625ba63a250462bedeece422e741de9f0caf3b1d89fd5b257aca80cd1",
+ "Command": "mysqld",
+ "Created": 1616273136,
+ "Ports": [
+ {
+ "PrivatePort": 3306,
+ "Type": "tcp"
+ },
+ {
+ "PrivatePort": 33060,
+ "Type": "tcp"
+ }
+ ],
+ "Labels": {
+ "com.docker.compose.project": "dockersd",
+ "com.docker.compose.service": "mysql",
+ "com.docker.compose.version": "2.2.2"
+ },
+ "State": "running",
+ "Status": "Up 40 seconds",
+ "HostConfig": {
+ "NetworkMode": "dockersd_private_none"
+ },
+ "NetworkSettings": {
+ "Networks": {
+ "dockersd_private": {
+ "IPAMConfig": null,
+ "Links": null,
+ "Aliases": null,
+ "NetworkID": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "EndpointID": "972d6807997369605ace863af58de6cb90c787a5bf2ffc4105662d393ae539b7",
+ "Gateway": "172.20.0.1",
+ "IPAddress": "172.20.0.3",
+ "IPPrefixLen": 16,
+ "IPv6Gateway": "",
+ "GlobalIPv6Address": "",
+ "GlobalIPv6PrefixLen": 0,
+ "MacAddress": "02:42:ac:14:00:02",
+ "DriverOpts": null
+ },
+ "dockersd_private1": {
+ "IPAMConfig": {},
+ "Links": null,
+ "Aliases": [
+ "mysql",
+ "mysql",
+ "f9ade4b83199"
+ ],
+ "NetworkID": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "EndpointID": "91a98405344ee1cb7d977cafabe634837876651544b32da20a5e0155868e6f5f",
+ "Gateway": "172.21.0.1",
+ "IPAddress": "172.21.0.3",
+ "IPPrefixLen": 24,
+ "IPv6Gateway": "",
+ "GlobalIPv6Address": "",
+ "GlobalIPv6PrefixLen": 0,
+ "MacAddress": "02:42:ac:15:00:02",
+ "DriverOpts": null
+ }
+ }
+ },
+ "Mounts": []
}
]
diff --git a/discovery/moby/testdata/dockerprom/networks.json b/discovery/moby/testdata/dockerprom/networks.json
index 35facd3bb9..75d4442df8 100644
--- a/discovery/moby/testdata/dockerprom/networks.json
+++ b/discovery/moby/testdata/dockerprom/networks.json
@@ -111,5 +111,59 @@
"Containers": {},
"Options": {},
"Labels": {}
+ },
+ {
+ "Name": "dockersd_private",
+ "Id": "e804771e55254a360fdb70dfdd78d3610fdde231b14ef2f837a00ac1eeb9e601",
+ "Created": "2022-03-25T09:21:17.718370976+08:00",
+ "Scope": "local",
+ "Driver": "bridge",
+ "EnableIPv6": false,
+ "IPAM": {
+ "Driver": "default",
+ "Options": null,
+ "Config": [
+ {
+ "Subnet": "172.20.0.1/16"
+ }
+ ]
+ },
+ "Internal": false,
+ "Attachable": false,
+ "Ingress": false,
+ "ConfigFrom": {
+ "Network": ""
+ },
+ "ConfigOnly": false,
+ "Containers": {},
+ "Options": {},
+ "Labels": {}
+ },
+ {
+ "Name": "dockersd_private1",
+ "Id": "bfcf66a6b64f7d518f009e34290dc3f3c66a08164257ad1afc3bd31d75f656e8",
+ "Created": "2022-03-25T09:21:17.718370976+08:00",
+ "Scope": "local",
+ "Driver": "bridge",
+ "EnableIPv6": false,
+ "IPAM": {
+ "Driver": "default",
+ "Options": null,
+ "Config": [
+ {
+ "Subnet": "172.21.0.1/16"
+ }
+ ]
+ },
+ "Internal": false,
+ "Attachable": false,
+ "Ingress": false,
+ "ConfigFrom": {
+ "Network": ""
+ },
+ "ConfigOnly": false,
+ "Containers": {},
+ "Options": {},
+ "Labels": {}
}
]
diff --git a/discovery/moby/testdata/swarmprom/services.json b/discovery/moby/testdata/swarmprom/services.json
index 72caa7a7f8..8f6c0793dd 100644
--- a/discovery/moby/testdata/swarmprom/services.json
+++ b/discovery/moby/testdata/swarmprom/services.json
@@ -224,7 +224,7 @@
"Args": [
"--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.path=/prometheus",
- "--storage.tsdb.retention=24h"
+ "--storage.tsdb.retention.time=24h"
],
"Privileges": {
"CredentialSpec": null,
diff --git a/discovery/moby/testdata/swarmprom/tasks.json b/discovery/moby/testdata/swarmprom/tasks.json
index 33d81f25ce..af5ff9fe28 100644
--- a/discovery/moby/testdata/swarmprom/tasks.json
+++ b/discovery/moby/testdata/swarmprom/tasks.json
@@ -973,7 +973,7 @@
"Args": [
"--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.path=/prometheus",
- "--storage.tsdb.retention=24h"
+ "--storage.tsdb.retention.time=24h"
],
"Privileges": {
"CredentialSpec": null,
diff --git a/discovery/nomad/metrics.go b/discovery/nomad/metrics.go
new file mode 100644
index 0000000000..9707153d91
--- /dev/null
+++ b/discovery/nomad/metrics.go
@@ -0,0 +1,57 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package nomad
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*nomadMetrics)(nil)
+
+type nomadMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+
+ failuresCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &nomadMetrics{
+ refreshMetrics: rmi,
+ failuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "prometheus_sd_nomad_failures_total",
+ Help: "Number of nomad service discovery refresh failures.",
+ }),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.failuresCount,
+ })
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *nomadMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *nomadMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/nomad/nomad.go b/discovery/nomad/nomad.go
index 7013f0737c..d9c48120ae 100644
--- a/discovery/nomad/nomad.go
+++ b/discovery/nomad/nomad.go
@@ -49,27 +49,18 @@ const (
)
// DefaultSDConfig is the default nomad SD configuration.
-var (
- DefaultSDConfig = SDConfig{
- AllowStale: true,
- HTTPClientConfig: config.DefaultHTTPClientConfig,
- Namespace: "default",
- RefreshInterval: model.Duration(60 * time.Second),
- Region: "global",
- Server: "http://localhost:4646",
- TagSeparator: ",",
- }
-
- failuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Name: "prometheus_sd_nomad_failures_total",
- Help: "Number of nomad service discovery refresh failures.",
- })
-)
+var DefaultSDConfig = SDConfig{
+ AllowStale: true,
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+ Namespace: "default",
+ RefreshInterval: model.Duration(60 * time.Second),
+ Region: "global",
+ Server: "http://localhost:4646",
+ TagSeparator: ",",
+}
func init() {
discovery.RegisterConfig(&SDConfig{})
- prometheus.MustRegister(failuresCount)
}
// SDConfig is the configuration for nomad based service discovery.
@@ -83,12 +74,17 @@ type SDConfig struct {
TagSeparator string `yaml:"tag_separator,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "nomad" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -121,10 +117,16 @@ type Discovery struct {
region string
server string
tagSeparator string
+ metrics *nomadMetrics
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*nomadMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
d := &Discovery{
allowStale: conf.AllowStale,
namespace: conf.Namespace,
@@ -132,6 +134,7 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
region: conf.Region,
server: conf.Server,
tagSeparator: conf.TagSeparator,
+ metrics: m,
}
HTTPClient, err := config.NewClientFromConfig(conf.HTTPClientConfig, "nomad_sd")
@@ -153,10 +156,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
d.client = client
d.Discovery = refresh.NewDiscovery(
- logger,
- "nomad",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "nomad",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -167,7 +173,7 @@ func (d *Discovery) refresh(context.Context) ([]*targetgroup.Group, error) {
}
stubs, _, err := d.client.Services().List(opts)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, err
}
@@ -179,7 +185,7 @@ func (d *Discovery) refresh(context.Context) ([]*targetgroup.Group, error) {
for _, service := range stub.Services {
instances, _, err := d.client.Services().Get(service.ServiceName, opts)
if err != nil {
- failuresCount.Inc()
+ d.metrics.failuresCount.Inc()
return nil, fmt.Errorf("failed to fetch services: %w", err)
}
diff --git a/discovery/nomad/nomad_test.go b/discovery/nomad/nomad_test.go
index 40d412d74f..357d4a8e9b 100644
--- a/discovery/nomad/nomad_test.go
+++ b/discovery/nomad/nomad_test.go
@@ -22,15 +22,18 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery"
)
type NomadSDTestSuite struct {
Mock *SDMock
}
-// SDMock is the interface for the nomad mock
+// SDMock is the interface for the nomad mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
@@ -127,8 +130,16 @@ func TestConfiguredService(t *testing.T) {
conf := &SDConfig{
Server: "http://localhost:4646",
}
- _, err := NewDiscovery(conf, nil)
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := conf.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ _, err := NewDiscovery(conf, nil, metrics)
require.NoError(t, err)
+
+ metrics.Unregister()
}
func TestNomadSDRefresh(t *testing.T) {
@@ -141,18 +152,26 @@ func TestNomadSDRefresh(t *testing.T) {
cfg := DefaultSDConfig
cfg.Server = endpoint.String()
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
tgs, err := d.refresh(context.Background())
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 1, len(tg.Targets))
+ require.Len(t, tg.Targets, 1)
lbls := model.LabelSet{
"__address__": model.LabelValue("127.0.0.1:30456"),
diff --git a/discovery/openstack/hypervisor.go b/discovery/openstack/hypervisor.go
index 16964cfb62..8964da9294 100644
--- a/discovery/openstack/hypervisor.go
+++ b/discovery/openstack/hypervisor.go
@@ -17,6 +17,7 @@ import (
"context"
"fmt"
"net"
+ "strconv"
"github.com/go-kit/log"
"github.com/gophercloud/gophercloud"
@@ -72,7 +73,7 @@ func (h *HypervisorDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group
}
tg := &targetgroup.Group{
- Source: fmt.Sprintf("OS_" + h.region),
+ Source: "OS_" + h.region,
}
// OpenStack API reference
// https://developer.openstack.org/api-ref/compute/#list-hypervisors-details
@@ -84,7 +85,7 @@ func (h *HypervisorDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group
}
for _, hypervisor := range hypervisorList {
labels := model.LabelSet{}
- addr := net.JoinHostPort(hypervisor.HostIP, fmt.Sprintf("%d", h.port))
+ addr := net.JoinHostPort(hypervisor.HostIP, strconv.Itoa(h.port))
labels[model.AddressLabel] = model.LabelValue(addr)
labels[openstackLabelHypervisorID] = model.LabelValue(hypervisor.ID)
labels[openstackLabelHypervisorHostName] = model.LabelValue(hypervisor.HypervisorHostname)
diff --git a/discovery/openstack/hypervisor_test.go b/discovery/openstack/hypervisor_test.go
index 396d5283dc..45684b4a2e 100644
--- a/discovery/openstack/hypervisor_test.go
+++ b/discovery/openstack/hypervisor_test.go
@@ -53,12 +53,12 @@ func TestOpenstackSDHypervisorRefresh(t *testing.T) {
hypervisor, _ := mock.openstackAuthSuccess()
ctx := context.Background()
tgs, err := hypervisor.refresh(ctx)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NoError(t, err)
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 2, len(tg.Targets))
+ require.Len(t, tg.Targets, 2)
for l, v := range map[string]string{
"__address__": "172.16.70.14:0",
diff --git a/discovery/openstack/instance.go b/discovery/openstack/instance.go
index b2fe1e7870..78c669e6f7 100644
--- a/discovery/openstack/instance.go
+++ b/discovery/openstack/instance.go
@@ -17,6 +17,7 @@ import (
"context"
"fmt"
"net"
+ "strconv"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
@@ -120,7 +121,7 @@ func (i *InstanceDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group,
}
pager := servers.List(client, opts)
tg := &targetgroup.Group{
- Source: fmt.Sprintf("OS_" + i.region),
+ Source: "OS_" + i.region,
}
err = pager.EachPage(func(page pagination.Page) (bool, error) {
if ctx.Err() != nil {
@@ -145,16 +146,22 @@ func (i *InstanceDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group,
openstackLabelUserID: model.LabelValue(s.UserID),
}
- flavorId, ok := s.Flavor["id"].(string)
- if !ok {
- level.Warn(i.logger).Log("msg", "Invalid type for flavor id, expected string")
- continue
+ flavorName, nameOk := s.Flavor["original_name"].(string)
+ // "original_name" is only available for microversion >= 2.47. It was added in favor of "id".
+ if !nameOk {
+ flavorID, idOk := s.Flavor["id"].(string)
+ if !idOk {
+ level.Warn(i.logger).Log("msg", "Invalid type for both flavor original_name and flavor id, expected string")
+ continue
+ }
+ labels[openstackLabelInstanceFlavor] = model.LabelValue(flavorID)
+ } else {
+ labels[openstackLabelInstanceFlavor] = model.LabelValue(flavorName)
}
- labels[openstackLabelInstanceFlavor] = model.LabelValue(flavorId)
- imageId, ok := s.Image["id"].(string)
+ imageID, ok := s.Image["id"].(string)
if ok {
- labels[openstackLabelInstanceImage] = model.LabelValue(imageId)
+ labels[openstackLabelInstanceImage] = model.LabelValue(imageID)
}
for k, v := range s.Metadata {
@@ -194,7 +201,7 @@ func (i *InstanceDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group,
if val, ok := floatingIPList[floatingIPKey{id: s.ID, fixed: addr}]; ok {
lbls[openstackLabelPublicIP] = model.LabelValue(val)
}
- addr = net.JoinHostPort(addr, fmt.Sprintf("%d", i.port))
+ addr = net.JoinHostPort(addr, strconv.Itoa(i.port))
lbls[model.AddressLabel] = model.LabelValue(addr)
tg.Targets = append(tg.Targets, lbls)
diff --git a/discovery/openstack/instance_test.go b/discovery/openstack/instance_test.go
index d2da5d9681..2b5ac1b89e 100644
--- a/discovery/openstack/instance_test.go
+++ b/discovery/openstack/instance_test.go
@@ -61,12 +61,12 @@ func TestOpenstackSDInstanceRefresh(t *testing.T) {
tgs, err := instance.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 4, len(tg.Targets))
+ require.Len(t, tg.Targets, 4)
for i, lbls := range []model.LabelSet{
{
@@ -84,7 +84,7 @@ func TestOpenstackSDInstanceRefresh(t *testing.T) {
},
{
"__address__": model.LabelValue("10.0.0.31:0"),
- "__meta_openstack_instance_flavor": model.LabelValue("1"),
+ "__meta_openstack_instance_flavor": model.LabelValue("m1.medium"),
"__meta_openstack_instance_id": model.LabelValue("9e5476bd-a4ec-4653-93d6-72c93aa682ba"),
"__meta_openstack_instance_image": model.LabelValue("f90f6034-2570-4974-8351-6b49732ef2eb"),
"__meta_openstack_instance_status": model.LabelValue("ACTIVE"),
@@ -96,7 +96,7 @@ func TestOpenstackSDInstanceRefresh(t *testing.T) {
},
{
"__address__": model.LabelValue("10.0.0.33:0"),
- "__meta_openstack_instance_flavor": model.LabelValue("4"),
+ "__meta_openstack_instance_flavor": model.LabelValue("m1.small"),
"__meta_openstack_instance_id": model.LabelValue("9e5476bd-a4ec-4653-93d6-72c93aa682bb"),
"__meta_openstack_instance_status": model.LabelValue("ACTIVE"),
"__meta_openstack_instance_name": model.LabelValue("merp"),
@@ -108,7 +108,7 @@ func TestOpenstackSDInstanceRefresh(t *testing.T) {
},
{
"__address__": model.LabelValue("10.0.0.34:0"),
- "__meta_openstack_instance_flavor": model.LabelValue("4"),
+ "__meta_openstack_instance_flavor": model.LabelValue("m1.small"),
"__meta_openstack_instance_id": model.LabelValue("9e5476bd-a4ec-4653-93d6-72c93aa682bb"),
"__meta_openstack_instance_status": model.LabelValue("ACTIVE"),
"__meta_openstack_instance_name": model.LabelValue("merp"),
diff --git a/discovery/openstack/metrics.go b/discovery/openstack/metrics.go
new file mode 100644
index 0000000000..a64c1a6732
--- /dev/null
+++ b/discovery/openstack/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package openstack
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+type openstackMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+var _ discovery.DiscovererMetrics = (*openstackMetrics)(nil)
+
+// Register implements discovery.DiscovererMetrics.
+func (m *openstackMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *openstackMetrics) Unregister() {}
diff --git a/discovery/openstack/mock_test.go b/discovery/openstack/mock_test.go
index e64c336482..4518f41166 100644
--- a/discovery/openstack/mock_test.go
+++ b/discovery/openstack/mock_test.go
@@ -18,9 +18,11 @@ import (
"net/http"
"net/http/httptest"
"testing"
+
+ "github.com/stretchr/testify/require"
)
-// SDMock is the interface for the OpenStack mock
+// SDMock is the interface for the OpenStack mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
@@ -34,12 +36,12 @@ func NewSDMock(t *testing.T) *SDMock {
}
}
-// Endpoint returns the URI to the mock server
+// Endpoint returns the URI to the mock server.
func (m *SDMock) Endpoint() string {
return m.Server.URL + "/"
}
-// Setup creates the mock server
+// Setup creates the mock server.
func (m *SDMock) Setup() {
m.Mux = http.NewServeMux()
m.Server = httptest.NewServer(m.Mux)
@@ -49,18 +51,16 @@ func (m *SDMock) Setup() {
const tokenID = "cbc36478b0bd8e67e89469c7749d4127"
func testMethod(t *testing.T, r *http.Request, expected string) {
- if expected != r.Method {
- t.Errorf("Request method = %v, expected %v", r.Method, expected)
- }
+ require.Equal(t, expected, r.Method, "Unexpected request method.")
}
func testHeader(t *testing.T, r *http.Request, header, expected string) {
- if actual := r.Header.Get(header); expected != actual {
- t.Errorf("Header %s = %s, expected %s", header, actual, expected)
- }
+ t.Helper()
+ actual := r.Header.Get(header)
+ require.Equal(t, expected, actual, "Unexpected value for request header %s.", header)
}
-// HandleVersionsSuccessfully mocks version call
+// HandleVersionsSuccessfully mocks version call.
func (m *SDMock) HandleVersionsSuccessfully() {
m.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
@@ -88,7 +88,7 @@ func (m *SDMock) HandleVersionsSuccessfully() {
})
}
-// HandleAuthSuccessfully mocks auth call
+// HandleAuthSuccessfully mocks auth call.
func (m *SDMock) HandleAuthSuccessfully() {
m.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Subject-Token", tokenID)
@@ -236,10 +236,10 @@ const hypervisorListBody = `
]
}`
-// HandleHypervisorListSuccessfully mocks os-hypervisors detail call
+// HandleHypervisorListSuccessfully mocks os-hypervisors detail call.
func (m *SDMock) HandleHypervisorListSuccessfully() {
m.Mux.HandleFunc("/os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) {
- testMethod(m.t, r, "GET")
+ testMethod(m.t, r, http.MethodGet)
testHeader(m.t, r, "X-Auth-Token", tokenID)
w.Header().Add("Content-Type", "application/json")
@@ -427,13 +427,17 @@ const serverListBody = `
"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
"flavor": {
- "id": "1",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark"
- }
- ]
+ "vcpus": 2,
+ "ram": 4096,
+ "disk": 0,
+ "ephemeral": 0,
+ "swap": 0,
+ "original_name": "m1.medium",
+ "extra_specs": {
+ "aggregate_instance_extra_specs:general": "true",
+ "hw:mem_page_size": "large",
+ "hw:vif_multiqueue_enabled": "true"
+ }
},
"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
"security_groups": [
@@ -498,13 +502,17 @@ const serverListBody = `
"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
"flavor": {
- "id": "4",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark"
- }
- ]
+ "vcpus": 2,
+ "ram": 4096,
+ "disk": 0,
+ "ephemeral": 0,
+ "swap": 0,
+ "original_name": "m1.small",
+ "extra_specs": {
+ "aggregate_instance_extra_specs:general": "true",
+ "hw:mem_page_size": "large",
+ "hw:vif_multiqueue_enabled": "true"
+ }
},
"id": "9e5476bd-a4ec-4653-93d6-72c93aa682bb",
"security_groups": [
@@ -533,10 +541,10 @@ const serverListBody = `
}
`
-// HandleServerListSuccessfully mocks server detail call
+// HandleServerListSuccessfully mocks server detail call.
func (m *SDMock) HandleServerListSuccessfully() {
m.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
- testMethod(m.t, r, "GET")
+ testMethod(m.t, r, http.MethodGet)
testHeader(m.t, r, "X-Auth-Token", tokenID)
w.Header().Add("Content-Type", "application/json")
@@ -572,10 +580,10 @@ const listOutput = `
}
`
-// HandleFloatingIPListSuccessfully mocks floating ips call
+// HandleFloatingIPListSuccessfully mocks floating ips call.
func (m *SDMock) HandleFloatingIPListSuccessfully() {
m.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) {
- testMethod(m.t, r, "GET")
+ testMethod(m.t, r, http.MethodGet)
testHeader(m.t, r, "X-Auth-Token", tokenID)
w.Header().Add("Content-Type", "application/json")
diff --git a/discovery/openstack/openstack.go b/discovery/openstack/openstack.go
index 92c83a4cf4..c98f78788d 100644
--- a/discovery/openstack/openstack.go
+++ b/discovery/openstack/openstack.go
@@ -24,6 +24,7 @@ import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/mwitkow/go-conntrack"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -65,12 +66,19 @@ type SDConfig struct {
Availability string `yaml:"availability,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &openstackMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "openstack" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -134,16 +142,24 @@ type refresher interface {
}
// NewDiscovery returns a new OpenStack Discoverer which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, l log.Logger) (*refresh.Discovery, error) {
+func NewDiscovery(conf *SDConfig, l log.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
+ m, ok := metrics.(*openstackMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
r, err := newRefresher(conf, l)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
- l,
- "openstack",
- time.Duration(conf.RefreshInterval),
- r.refresh,
+ refresh.Options{
+ Logger: l,
+ Mech: "openstack",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: r.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
), nil
}
diff --git a/discovery/ovhcloud/dedicated_server.go b/discovery/ovhcloud/dedicated_server.go
index bb5dadcd7b..a70857a08b 100644
--- a/discovery/ovhcloud/dedicated_server.go
+++ b/discovery/ovhcloud/dedicated_server.go
@@ -144,12 +144,12 @@ func (d *dedicatedServerDiscovery) refresh(context.Context) ([]*targetgroup.Grou
model.InstanceLabel: model.LabelValue(server.Name),
dedicatedServerLabelPrefix + "state": model.LabelValue(server.State),
dedicatedServerLabelPrefix + "commercial_range": model.LabelValue(server.CommercialRange),
- dedicatedServerLabelPrefix + "link_speed": model.LabelValue(fmt.Sprintf("%d", server.LinkSpeed)),
+ dedicatedServerLabelPrefix + "link_speed": model.LabelValue(strconv.Itoa(server.LinkSpeed)),
dedicatedServerLabelPrefix + "rack": model.LabelValue(server.Rack),
dedicatedServerLabelPrefix + "no_intervention": model.LabelValue(strconv.FormatBool(server.NoIntervention)),
dedicatedServerLabelPrefix + "os": model.LabelValue(server.Os),
dedicatedServerLabelPrefix + "support_level": model.LabelValue(server.SupportLevel),
- dedicatedServerLabelPrefix + "server_id": model.LabelValue(fmt.Sprintf("%d", server.ServerID)),
+ dedicatedServerLabelPrefix + "server_id": model.LabelValue(strconv.FormatInt(server.ServerID, 10)),
dedicatedServerLabelPrefix + "reverse": model.LabelValue(server.Reverse),
dedicatedServerLabelPrefix + "datacenter": model.LabelValue(server.Datacenter),
dedicatedServerLabelPrefix + "name": model.LabelValue(server.Name),
diff --git a/discovery/ovhcloud/dedicated_server_test.go b/discovery/ovhcloud/dedicated_server_test.go
index e8ffa4a283..52311bcc87 100644
--- a/discovery/ovhcloud/dedicated_server_test.go
+++ b/discovery/ovhcloud/dedicated_server_test.go
@@ -47,11 +47,11 @@ consumer_key: %s`, mock.URL, ovhcloudApplicationKeyTest, ovhcloudApplicationSecr
targetGroups, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(targetGroups))
+ require.Len(t, targetGroups, 1)
targetGroup := targetGroups[0]
require.NotNil(t, targetGroup)
require.NotNil(t, targetGroup.Targets)
- require.Equal(t, 1, len(targetGroup.Targets))
+ require.Len(t, targetGroup.Targets, 1)
for i, lbls := range []model.LabelSet{
{
diff --git a/discovery/ovhcloud/metrics.go b/discovery/ovhcloud/metrics.go
new file mode 100644
index 0000000000..1c51709469
--- /dev/null
+++ b/discovery/ovhcloud/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ovhcloud
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*ovhcloudMetrics)(nil)
+
+type ovhcloudMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *ovhcloudMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *ovhcloudMetrics) Unregister() {}
diff --git a/discovery/ovhcloud/ovhcloud.go b/discovery/ovhcloud/ovhcloud.go
index 535ade4df5..988b4482f2 100644
--- a/discovery/ovhcloud/ovhcloud.go
+++ b/discovery/ovhcloud/ovhcloud.go
@@ -22,6 +22,7 @@ import (
"github.com/go-kit/log"
"github.com/ovh/go-ovh/ovh"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -52,6 +53,13 @@ type SDConfig struct {
Service string `yaml:"service"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &ovhcloudMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name implements the Discoverer interface.
func (c SDConfig) Name() string {
return "ovhcloud"
@@ -93,7 +101,7 @@ func createClient(config *SDConfig) (*ovh.Client, error) {
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, options.Logger)
+ return NewDiscovery(c, options.Logger, options.Metrics)
}
func init() {
@@ -140,16 +148,24 @@ func newRefresher(conf *SDConfig, logger log.Logger) (refresher, error) {
}
// NewDiscovery returns a new OVHcloud Discoverer which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
+ m, ok := metrics.(*ovhcloudMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
r, err := newRefresher(conf, logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
- logger,
- "ovhcloud",
- time.Duration(conf.RefreshInterval),
- r.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "ovhcloud",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: r.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
), nil
}
diff --git a/discovery/ovhcloud/ovhcloud_test.go b/discovery/ovhcloud/ovhcloud_test.go
index efcd95bb0d..9c95bf90e6 100644
--- a/discovery/ovhcloud/ovhcloud_test.go
+++ b/discovery/ovhcloud/ovhcloud_test.go
@@ -18,6 +18,7 @@ import (
"fmt"
"testing"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
@@ -65,7 +66,7 @@ endpoint: %s
_, err := createClient(&conf)
- require.ErrorContains(t, err, "missing application key")
+ require.ErrorContains(t, err, "missing authentication information")
}
func TestParseIPs(t *testing.T) {
@@ -121,8 +122,17 @@ func TestParseIPs(t *testing.T) {
func TestDiscoverer(t *testing.T) {
conf, _ := getMockConf("vps")
logger := testutil.NewLogger(t)
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := conf.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
_, err := conf.NewDiscoverer(discovery.DiscovererOptions{
- Logger: logger,
+ Logger: logger,
+ Metrics: metrics,
})
require.NoError(t, err)
diff --git a/discovery/ovhcloud/vps.go b/discovery/ovhcloud/vps.go
index e2d1dee364..58ceeabd87 100644
--- a/discovery/ovhcloud/vps.go
+++ b/discovery/ovhcloud/vps.go
@@ -19,6 +19,7 @@ import (
"net/netip"
"net/url"
"path"
+ "strconv"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
@@ -161,21 +162,21 @@ func (d *vpsDiscovery) refresh(context.Context) ([]*targetgroup.Group, error) {
model.InstanceLabel: model.LabelValue(server.Name),
vpsLabelPrefix + "offer": model.LabelValue(server.Model.Offer),
vpsLabelPrefix + "datacenter": model.LabelValue(fmt.Sprintf("%+v", server.Model.Datacenter)),
- vpsLabelPrefix + "model_vcore": model.LabelValue(fmt.Sprintf("%d", server.Model.Vcore)),
- vpsLabelPrefix + "maximum_additional_ip": model.LabelValue(fmt.Sprintf("%d", server.Model.MaximumAdditionalIP)),
+ vpsLabelPrefix + "model_vcore": model.LabelValue(strconv.Itoa(server.Model.Vcore)),
+ vpsLabelPrefix + "maximum_additional_ip": model.LabelValue(strconv.Itoa(server.Model.MaximumAdditionalIP)),
vpsLabelPrefix + "version": model.LabelValue(server.Model.Version),
vpsLabelPrefix + "model_name": model.LabelValue(server.Model.Name),
- vpsLabelPrefix + "disk": model.LabelValue(fmt.Sprintf("%d", server.Model.Disk)),
- vpsLabelPrefix + "memory": model.LabelValue(fmt.Sprintf("%d", server.Model.Memory)),
+ vpsLabelPrefix + "disk": model.LabelValue(strconv.Itoa(server.Model.Disk)),
+ vpsLabelPrefix + "memory": model.LabelValue(strconv.Itoa(server.Model.Memory)),
vpsLabelPrefix + "zone": model.LabelValue(server.Zone),
vpsLabelPrefix + "display_name": model.LabelValue(server.DisplayName),
vpsLabelPrefix + "cluster": model.LabelValue(server.Cluster),
vpsLabelPrefix + "state": model.LabelValue(server.State),
vpsLabelPrefix + "name": model.LabelValue(server.Name),
vpsLabelPrefix + "netboot_mode": model.LabelValue(server.NetbootMode),
- vpsLabelPrefix + "memory_limit": model.LabelValue(fmt.Sprintf("%d", server.MemoryLimit)),
+ vpsLabelPrefix + "memory_limit": model.LabelValue(strconv.Itoa(server.MemoryLimit)),
vpsLabelPrefix + "offer_type": model.LabelValue(server.OfferType),
- vpsLabelPrefix + "vcore": model.LabelValue(fmt.Sprintf("%d", server.Vcore)),
+ vpsLabelPrefix + "vcore": model.LabelValue(strconv.Itoa(server.Vcore)),
vpsLabelPrefix + "ipv4": model.LabelValue(ipv4),
vpsLabelPrefix + "ipv6": model.LabelValue(ipv6),
}
diff --git a/discovery/ovhcloud/vps_test.go b/discovery/ovhcloud/vps_test.go
index b1177f215e..2d2d6dcd21 100644
--- a/discovery/ovhcloud/vps_test.go
+++ b/discovery/ovhcloud/vps_test.go
@@ -49,11 +49,11 @@ consumer_key: %s`, mock.URL, ovhcloudApplicationKeyTest, ovhcloudApplicationSecr
targetGroups, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(targetGroups))
+ require.Len(t, targetGroups, 1)
targetGroup := targetGroups[0]
require.NotNil(t, targetGroup)
require.NotNil(t, targetGroup.Targets)
- require.Equal(t, 1, len(targetGroup.Targets))
+ require.Len(t, targetGroup.Targets, 1)
for i, lbls := range []model.LabelSet{
{
"__address__": "192.0.2.1",
diff --git a/discovery/puppetdb/metrics.go b/discovery/puppetdb/metrics.go
new file mode 100644
index 0000000000..2f5faa57ce
--- /dev/null
+++ b/discovery/puppetdb/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package puppetdb
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*puppetdbMetrics)(nil)
+
+type puppetdbMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *puppetdbMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *puppetdbMetrics) Unregister() {}
diff --git a/discovery/puppetdb/puppetdb.go b/discovery/puppetdb/puppetdb.go
index f22a2e22b5..8f89acbf93 100644
--- a/discovery/puppetdb/puppetdb.go
+++ b/discovery/puppetdb/puppetdb.go
@@ -29,6 +29,7 @@ import (
"github.com/go-kit/log"
"github.com/grafana/regexp"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/version"
@@ -78,12 +79,19 @@ type SDConfig struct {
Port int `yaml:"port"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &puppetdbMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "puppetdb" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -115,7 +123,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
if c.Query == "" {
return fmt.Errorf("query missing")
}
- return nil
+ return c.HTTPClientConfig.Validate()
}
// Discovery provides service discovery functionality based
@@ -130,7 +138,12 @@ type Discovery struct {
}
// NewDiscovery returns a new PuppetDB discovery for the given config.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*puppetdbMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
if logger == nil {
logger = log.NewNopLogger()
}
@@ -156,10 +169,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "http",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "puppetdb",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -173,7 +189,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
return nil, err
}
- req, err := http.NewRequest("POST", d.url, bytes.NewBuffer(bodyBytes))
+ req, err := http.NewRequest(http.MethodPost, d.url, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, err
}
@@ -221,7 +237,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
pdbLabelResource: model.LabelValue(resource.Resource),
pdbLabelType: model.LabelValue(resource.Type),
pdbLabelTitle: model.LabelValue(resource.Title),
- pdbLabelExported: model.LabelValue(fmt.Sprintf("%t", resource.Exported)),
+ pdbLabelExported: model.LabelValue(strconv.FormatBool(resource.Exported)),
pdbLabelFile: model.LabelValue(resource.File),
pdbLabelEnvironment: model.LabelValue(resource.Environment),
}
diff --git a/discovery/puppetdb/puppetdb_test.go b/discovery/puppetdb/puppetdb_test.go
index 5514787d46..bf9c7b215e 100644
--- a/discovery/puppetdb/puppetdb_test.go
+++ b/discovery/puppetdb/puppetdb_test.go
@@ -23,10 +23,12 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -62,9 +64,17 @@ func TestPuppetSlashInURL(t *testing.T) {
Port: 80,
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
require.Equal(t, apiURL, d.url)
+
+ metrics.Unregister()
}
}
@@ -79,7 +89,12 @@ func TestPuppetDBRefresh(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
@@ -105,7 +120,9 @@ func TestPuppetDBRefresh(t *testing.T) {
Source: ts.URL + "/pdb/query/v4?query=vhosts",
},
}
- require.Equal(t, tgs, expectedTargets)
+ require.Equal(t, expectedTargets, tgs)
+
+ metrics.Unregister()
}
func TestPuppetDBRefreshWithParameters(t *testing.T) {
@@ -120,7 +137,12 @@ func TestPuppetDBRefreshWithParameters(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
@@ -156,7 +178,9 @@ func TestPuppetDBRefreshWithParameters(t *testing.T) {
Source: ts.URL + "/pdb/query/v4?query=vhosts",
},
}
- require.Equal(t, tgs, expectedTargets)
+ require.Equal(t, expectedTargets, tgs)
+
+ metrics.Unregister()
}
func TestPuppetDBInvalidCode(t *testing.T) {
@@ -172,12 +196,19 @@ func TestPuppetDBInvalidCode(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
_, err = d.refresh(ctx)
require.EqualError(t, err, "server returned HTTP status 400 Bad Request")
+
+ metrics.Unregister()
}
func TestPuppetDBInvalidFormat(t *testing.T) {
@@ -193,10 +224,17 @@ func TestPuppetDBInvalidFormat(t *testing.T) {
RefreshInterval: model.Duration(30 * time.Second),
}
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
ctx := context.Background()
_, err = d.refresh(ctx)
require.EqualError(t, err, "unsupported content type text/plain; charset=utf-8")
+
+ metrics.Unregister()
}
diff --git a/discovery/refresh/refresh.go b/discovery/refresh/refresh.go
index 919567a53b..f037a90cff 100644
--- a/discovery/refresh/refresh.go
+++ b/discovery/refresh/refresh.go
@@ -20,31 +20,17 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
- "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-var (
- failuresCount = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "prometheus_sd_refresh_failures_total",
- Help: "Number of refresh failures for the given SD mechanism.",
- },
- []string{"mechanism"},
- )
- duration = prometheus.NewSummaryVec(
- prometheus.SummaryOpts{
- Name: "prometheus_sd_refresh_duration_seconds",
- Help: "The duration of a refresh in seconds for the given SD mechanism.",
- Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
- },
- []string{"mechanism"},
- )
-)
-
-func init() {
- prometheus.MustRegister(duration, failuresCount)
+type Options struct {
+ Logger log.Logger
+ Mech string
+ Interval time.Duration
+ RefreshF func(ctx context.Context) ([]*targetgroup.Group, error)
+ MetricsInstantiator discovery.RefreshMetricsInstantiator
}
// Discovery implements the Discoverer interface.
@@ -52,23 +38,28 @@ type Discovery struct {
logger log.Logger
interval time.Duration
refreshf func(ctx context.Context) ([]*targetgroup.Group, error)
-
- failures prometheus.Counter
- duration prometheus.Observer
+ metrics *discovery.RefreshMetrics
}
// NewDiscovery returns a Discoverer function that calls a refresh() function at every interval.
-func NewDiscovery(l log.Logger, mech string, interval time.Duration, refreshf func(ctx context.Context) ([]*targetgroup.Group, error)) *Discovery {
- if l == nil {
- l = log.NewNopLogger()
+func NewDiscovery(opts Options) *Discovery {
+ m := opts.MetricsInstantiator.Instantiate(opts.Mech)
+
+ var logger log.Logger
+ if opts.Logger == nil {
+ logger = log.NewNopLogger()
+ } else {
+ logger = opts.Logger
}
- return &Discovery{
- logger: l,
- interval: interval,
- refreshf: refreshf,
- failures: failuresCount.WithLabelValues(mech),
- duration: duration.WithLabelValues(mech),
+
+ d := Discovery{
+ logger: logger,
+ interval: opts.Interval,
+ refreshf: opts.RefreshF,
+ metrics: m,
}
+
+ return &d
}
// Run implements the Discoverer interface.
@@ -115,12 +106,12 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
now := time.Now()
defer func() {
- d.duration.Observe(time.Since(now).Seconds())
+ d.metrics.Duration.Observe(time.Since(now).Seconds())
}()
tgs, err := d.refreshf(ctx)
if err != nil {
- d.failures.Inc()
+ d.metrics.Failures.Inc()
}
return tgs, err
}
diff --git a/discovery/refresh/refresh_test.go b/discovery/refresh/refresh_test.go
index 6decef19fc..b70a326355 100644
--- a/discovery/refresh/refresh_test.go
+++ b/discovery/refresh/refresh_test.go
@@ -19,10 +19,12 @@ import (
"testing"
"time"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -65,7 +67,20 @@ func TestRefresh(t *testing.T) {
return nil, fmt.Errorf("some error")
}
interval := time.Millisecond
- d := NewDiscovery(nil, "test", interval, refresh)
+
+ metrics := discovery.NewRefreshMetrics(prometheus.NewRegistry())
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+
+ d := NewDiscovery(
+ Options{
+ Logger: nil,
+ Mech: "test",
+ Interval: interval,
+ RefreshF: refresh,
+ MetricsInstantiator: metrics,
+ },
+ )
ch := make(chan []*targetgroup.Group)
ctx, cancel := context.WithCancel(context.Background())
@@ -82,7 +97,7 @@ func TestRefresh(t *testing.T) {
defer tick.Stop()
select {
case <-ch:
- t.Fatal("Unexpected target group")
+ require.FailNow(t, "Unexpected target group")
case <-tick.C:
}
}
diff --git a/discovery/registry.go b/discovery/registry.go
index 13168a07a7..1f491d4ca9 100644
--- a/discovery/registry.go
+++ b/discovery/registry.go
@@ -24,6 +24,8 @@ import (
"gopkg.in/yaml.v2"
+ "github.com/prometheus/client_golang/prometheus"
+
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -258,3 +260,24 @@ func replaceYAMLTypeError(err error, oldTyp, newTyp reflect.Type) error {
}
return err
}
+
+// RegisterSDMetrics registers the metrics used by service discovery mechanisms.
+// RegisterSDMetrics should be called only once during the lifetime of the Prometheus process.
+// There is no need for the Prometheus process to unregister the metrics.
+func RegisterSDMetrics(registerer prometheus.Registerer, rmm RefreshMetricsManager) (map[string]DiscovererMetrics, error) {
+ err := rmm.Register()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create service discovery refresh metrics")
+ }
+
+ metrics := make(map[string]DiscovererMetrics)
+ for _, conf := range configNames {
+ currentSdMetrics := conf.NewDiscovererMetrics(registerer, rmm)
+ err = currentSdMetrics.Register()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create service discovery metrics")
+ }
+ metrics[conf.Name()] = currentSdMetrics
+ }
+ return metrics, nil
+}
diff --git a/discovery/scaleway/instance.go b/discovery/scaleway/instance.go
index 67311216d0..2542c63253 100644
--- a/discovery/scaleway/instance.go
+++ b/discovery/scaleway/instance.go
@@ -174,23 +174,27 @@ func (d *instanceDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group,
labels[instanceTagsLabel] = model.LabelValue(tags)
}
- if server.IPv6 != nil {
- labels[instancePublicIPv6Label] = model.LabelValue(server.IPv6.Address.String())
+ addr := ""
+ if server.IPv6 != nil { //nolint:staticcheck
+ labels[instancePublicIPv6Label] = model.LabelValue(server.IPv6.Address.String()) //nolint:staticcheck
+ addr = server.IPv6.Address.String() //nolint:staticcheck
}
- if server.PublicIP != nil {
- labels[instancePublicIPv4Label] = model.LabelValue(server.PublicIP.Address.String())
+ if server.PublicIP != nil { //nolint:staticcheck
+ labels[instancePublicIPv4Label] = model.LabelValue(server.PublicIP.Address.String()) //nolint:staticcheck
+ addr = server.PublicIP.Address.String() //nolint:staticcheck
}
if server.PrivateIP != nil {
labels[instancePrivateIPv4Label] = model.LabelValue(*server.PrivateIP)
+ addr = *server.PrivateIP
+ }
- addr := net.JoinHostPort(*server.PrivateIP, strconv.FormatUint(uint64(d.port), 10))
+ if addr != "" {
+ addr := net.JoinHostPort(addr, strconv.FormatUint(uint64(d.port), 10))
labels[model.AddressLabel] = model.LabelValue(addr)
-
targets = append(targets, labels)
}
-
}
return []*targetgroup.Group{{Source: "scaleway", Targets: targets}}, nil
diff --git a/discovery/scaleway/instance_test.go b/discovery/scaleway/instance_test.go
index e7a32dd924..ae70a9ed25 100644
--- a/discovery/scaleway/instance_test.go
+++ b/discovery/scaleway/instance_test.go
@@ -55,12 +55,12 @@ api_url: %s
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 2, len(tg.Targets))
+ require.Len(t, tg.Targets, 3)
for i, lbls := range []model.LabelSet{
{
@@ -110,6 +110,28 @@ api_url: %s
"__meta_scaleway_instance_type": "DEV1-S",
"__meta_scaleway_instance_zone": "fr-par-1",
},
+ {
+ "__address__": "51.158.183.115:80",
+ "__meta_scaleway_instance_boot_type": "local",
+ "__meta_scaleway_instance_hostname": "routed-dualstack",
+ "__meta_scaleway_instance_id": "4904366a-7e26-4b65-b97b-6392c761247a",
+ "__meta_scaleway_instance_image_arch": "x86_64",
+ "__meta_scaleway_instance_image_id": "3e0a5b84-1d69-4993-8fa4-0d7df52d5160",
+ "__meta_scaleway_instance_image_name": "Ubuntu 22.04 Jammy Jellyfish",
+ "__meta_scaleway_instance_location_cluster_id": "19",
+ "__meta_scaleway_instance_location_hypervisor_id": "1201",
+ "__meta_scaleway_instance_location_node_id": "24",
+ "__meta_scaleway_instance_name": "routed-dualstack",
+ "__meta_scaleway_instance_organization_id": "20b3d507-96ac-454c-a795-bc731b46b12f",
+ "__meta_scaleway_instance_project_id": "20b3d507-96ac-454c-a795-bc731b46b12f",
+ "__meta_scaleway_instance_public_ipv4": "51.158.183.115",
+ "__meta_scaleway_instance_region": "nl-ams",
+ "__meta_scaleway_instance_security_group_id": "984414da-9fc2-49c0-a925-fed6266fe092",
+ "__meta_scaleway_instance_security_group_name": "Default security group",
+ "__meta_scaleway_instance_status": "running",
+ "__meta_scaleway_instance_type": "DEV1-S",
+ "__meta_scaleway_instance_zone": "nl-ams-1",
+ },
} {
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
require.Equal(t, lbls, tg.Targets[i])
@@ -161,5 +183,5 @@ api_url: %s
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
}
diff --git a/discovery/scaleway/metrics.go b/discovery/scaleway/metrics.go
new file mode 100644
index 0000000000..a8277d0fb3
--- /dev/null
+++ b/discovery/scaleway/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package scaleway
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*scalewayMetrics)(nil)
+
+type scalewayMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *scalewayMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *scalewayMetrics) Unregister() {}
diff --git a/discovery/scaleway/scaleway.go b/discovery/scaleway/scaleway.go
index 90091b3172..f8e1a83f5e 100644
--- a/discovery/scaleway/scaleway.go
+++ b/discovery/scaleway/scaleway.go
@@ -23,6 +23,7 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/scaleway/scaleway-sdk-go/scw"
@@ -103,6 +104,13 @@ type SDConfig struct {
Role role `yaml:"role"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &scalewayMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
func (c SDConfig) Name() string {
return "scaleway"
}
@@ -160,7 +168,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
func (c SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(&c, options.Logger)
+ return NewDiscovery(&c, options.Logger, options.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -177,17 +185,25 @@ func init() {
// the Discoverer interface.
type Discovery struct{}
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
+ m, ok := metrics.(*scalewayMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
r, err := newRefresher(conf)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
- logger,
- "scaleway",
- time.Duration(conf.RefreshInterval),
- r.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "scaleway",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: r.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
), nil
}
diff --git a/discovery/scaleway/testdata/instance.json b/discovery/scaleway/testdata/instance.json
index f8d35b215c..b433f7598e 100644
--- a/discovery/scaleway/testdata/instance.json
+++ b/discovery/scaleway/testdata/instance.json
@@ -216,6 +216,146 @@
"placement_group": null,
"private_nics": [],
"zone": "fr-par-1"
+ },
+ {
+ "id": "4904366a-7e26-4b65-b97b-6392c761247a",
+ "name": "routed-dualstack",
+ "arch": "x86_64",
+ "commercial_type": "DEV1-S",
+ "boot_type": "local",
+ "organization": "20b3d507-96ac-454c-a795-bc731b46b12f",
+ "project": "20b3d507-96ac-454c-a795-bc731b46b12f",
+ "hostname": "routed-dualstack",
+ "image": {
+ "id": "3e0a5b84-1d69-4993-8fa4-0d7df52d5160",
+ "name": "Ubuntu 22.04 Jammy Jellyfish",
+ "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db",
+ "project": "51b656e3-4865-41e8-adbc-0c45bdd780db",
+ "root_volume": {
+ "id": "13d945b9-5e78-4f9d-8ac4-c4bc2fa7c31a",
+ "name": "Ubuntu 22.04 Jammy Jellyfish",
+ "volume_type": "unified",
+ "size": 10000000000
+ },
+ "extra_volumes": {},
+ "public": true,
+ "arch": "x86_64",
+ "creation_date": "2024-02-22T15:52:56.037007+00:00",
+ "modification_date": "2024-02-22T15:52:56.037007+00:00",
+ "default_bootscript": null,
+ "from_server": null,
+ "state": "available",
+ "tags": [],
+ "zone": "nl-ams-1"
+ },
+ "volumes": {
+ "0": {
+ "boot": false,
+ "id": "fe85c817-e67e-4e24-8f13-bde3e9f42620",
+ "name": "Ubuntu 22.04 Jammy Jellyfish",
+ "volume_type": "l_ssd",
+ "export_uri": null,
+ "organization": "20b3d507-96ac-454c-a795-bc731b46b12f",
+ "project": "20b3d507-96ac-454c-a795-bc731b46b12f",
+ "server": {
+ "id": "4904366a-7e26-4b65-b97b-6392c761247a",
+ "name": "routed-dualstack"
+ },
+ "size": 20000000000,
+ "state": "available",
+ "creation_date": "2024-04-19T14:50:14.019739+00:00",
+ "modification_date": "2024-04-19T14:50:14.019739+00:00",
+ "tags": [],
+ "zone": "nl-ams-1"
+ }
+ },
+ "tags": [],
+ "state": "running",
+ "protected": false,
+ "state_detail": "booted",
+ "public_ip": {
+ "id": "53f8f861-7a11-4b16-a4bc-fb8f4b4a11d0",
+ "address": "51.158.183.115",
+ "dynamic": false,
+ "gateway": "62.210.0.1",
+ "netmask": "32",
+ "family": "inet",
+ "provisioning_mode": "dhcp",
+ "tags": [],
+ "state": "attached",
+ "ipam_id": "ec3499ff-a664-49b7-818a-9fe4b95aee5e"
+ },
+ "public_ips": [
+ {
+ "id": "53f8f861-7a11-4b16-a4bc-fb8f4b4a11d0",
+ "address": "51.158.183.115",
+ "dynamic": false,
+ "gateway": "62.210.0.1",
+ "netmask": "32",
+ "family": "inet",
+ "provisioning_mode": "dhcp",
+ "tags": [],
+ "state": "attached",
+ "ipam_id": "ec3499ff-a664-49b7-818a-9fe4b95aee5e"
+ },
+ {
+ "id": "f52a8c81-0875-4aee-b96e-eccfc6bec367",
+ "address": "2001:bc8:1640:1568:dc00:ff:fe21:91b",
+ "dynamic": false,
+ "gateway": "fe80::dc00:ff:fe21:91c",
+ "netmask": "64",
+ "family": "inet6",
+ "provisioning_mode": "slaac",
+ "tags": [],
+ "state": "attached",
+ "ipam_id": "40d1e6ea-e932-42f9-8acb-55398bec7ad6"
+ }
+ ],
+ "mac_address": "de:00:00:21:09:1b",
+ "routed_ip_enabled": true,
+ "ipv6": null,
+ "extra_networks": [],
+ "dynamic_ip_required": false,
+ "enable_ipv6": false,
+ "private_ip": null,
+ "creation_date": "2024-04-19T14:50:14.019739+00:00",
+ "modification_date": "2024-04-19T14:52:21.181670+00:00",
+ "bootscript": {
+ "id": "5a520dda-96d6-4ed2-acd1-1d526b6859fe",
+ "public": true,
+ "title": "x86_64 mainline 4.4.182 rev1",
+ "architecture": "x86_64",
+ "organization": "11111111-1111-4111-8111-111111111111",
+ "project": "11111111-1111-4111-8111-111111111111",
+ "kernel": "http://10.196.2.9/kernel/x86_64-mainline-lts-4.4-4.4.182-rev1/vmlinuz-4.4.182",
+ "dtb": "",
+ "initrd": "http://10.196.2.9/initrd/initrd-Linux-x86_64-v3.14.6.gz",
+ "bootcmdargs": "LINUX_COMMON scaleway boot=local nbd.max_part=16",
+ "default": true,
+ "zone": "nl-ams-1"
+ },
+ "security_group": {
+ "id": "984414da-9fc2-49c0-a925-fed6266fe092",
+ "name": "Default security group"
+ },
+ "location": {
+ "zone_id": "ams1",
+ "platform_id": "23",
+ "cluster_id": "19",
+ "hypervisor_id": "1201",
+ "node_id": "24"
+ },
+ "maintenances": [],
+ "allowed_actions": [
+ "poweroff",
+ "terminate",
+ "reboot",
+ "stop_in_place",
+ "backup"
+ ],
+ "placement_group": null,
+ "private_nics": [],
+ "zone": "nl-ams-1"
}
]
-}
+}
\ No newline at end of file
diff --git a/discovery/triton/metrics.go b/discovery/triton/metrics.go
new file mode 100644
index 0000000000..3e34a840af
--- /dev/null
+++ b/discovery/triton/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package triton
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*tritonMetrics)(nil)
+
+type tritonMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *tritonMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *tritonMetrics) Unregister() {}
diff --git a/discovery/triton/triton.go b/discovery/triton/triton.go
index c83f3b34ab..675149f2a3 100644
--- a/discovery/triton/triton.go
+++ b/discovery/triton/triton.go
@@ -26,6 +26,7 @@ import (
"github.com/go-kit/log"
"github.com/mwitkow/go-conntrack"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -69,12 +70,19 @@ type SDConfig struct {
Version int `yaml:"version"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &tritonMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "triton" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return New(opts.Logger, c)
+ return New(opts.Logger, c, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -138,7 +146,12 @@ type Discovery struct {
}
// New returns a new Discovery which periodically refreshes its targets.
-func New(logger log.Logger, conf *SDConfig) (*Discovery, error) {
+func New(logger log.Logger, conf *SDConfig, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*tritonMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
tls, err := config.NewTLSConfig(&conf.TLSConfig)
if err != nil {
return nil, err
@@ -159,10 +172,13 @@ func New(logger log.Logger, conf *SDConfig) (*Discovery, error) {
sdConfig: conf,
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "triton",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "triton",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -195,7 +211,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
endpoint = fmt.Sprintf("%s?groups=%s", endpoint, groups)
}
- req, err := http.NewRequest("GET", endpoint, nil)
+ req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
diff --git a/discovery/triton/triton_test.go b/discovery/triton/triton_test.go
index ca38965322..e37693e6bf 100644
--- a/discovery/triton/triton_test.go
+++ b/discovery/triton/triton_test.go
@@ -24,9 +24,12 @@ import (
"strings"
"testing"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery"
)
var (
@@ -78,12 +81,26 @@ var (
}
)
-func newTritonDiscovery(c SDConfig) (*Discovery, error) {
- return New(nil, &c)
+func newTritonDiscovery(c SDConfig) (*Discovery, discovery.DiscovererMetrics, error) {
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ // TODO(ptodev): Add the ability to unregister refresh metrics.
+ metrics := c.NewDiscovererMetrics(reg, refreshMetrics)
+ err := metrics.Register()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ d, err := New(nil, &c, metrics)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return d, metrics, nil
}
func TestTritonSDNew(t *testing.T) {
- td, err := newTritonDiscovery(conf)
+ td, m, err := newTritonDiscovery(conf)
require.NoError(t, err)
require.NotNil(t, td)
require.NotNil(t, td.client)
@@ -93,16 +110,17 @@ func TestTritonSDNew(t *testing.T) {
require.Equal(t, conf.DNSSuffix, td.sdConfig.DNSSuffix)
require.Equal(t, conf.Endpoint, td.sdConfig.Endpoint)
require.Equal(t, conf.Port, td.sdConfig.Port)
+ m.Unregister()
}
func TestTritonSDNewBadConfig(t *testing.T) {
- td, err := newTritonDiscovery(badconf)
+ td, _, err := newTritonDiscovery(badconf)
require.Error(t, err)
require.Nil(t, td)
}
func TestTritonSDNewGroupsConfig(t *testing.T) {
- td, err := newTritonDiscovery(groupsconf)
+ td, m, err := newTritonDiscovery(groupsconf)
require.NoError(t, err)
require.NotNil(t, td)
require.NotNil(t, td.client)
@@ -113,10 +131,11 @@ func TestTritonSDNewGroupsConfig(t *testing.T) {
require.Equal(t, groupsconf.Endpoint, td.sdConfig.Endpoint)
require.Equal(t, groupsconf.Groups, td.sdConfig.Groups)
require.Equal(t, groupsconf.Port, td.sdConfig.Port)
+ m.Unregister()
}
func TestTritonSDNewCNConfig(t *testing.T) {
- td, err := newTritonDiscovery(cnconf)
+ td, m, err := newTritonDiscovery(cnconf)
require.NoError(t, err)
require.NotNil(t, td)
require.NotNil(t, td.client)
@@ -127,6 +146,7 @@ func TestTritonSDNewCNConfig(t *testing.T) {
require.Equal(t, cnconf.DNSSuffix, td.sdConfig.DNSSuffix)
require.Equal(t, cnconf.Endpoint, td.sdConfig.Endpoint)
require.Equal(t, cnconf.Port, td.sdConfig.Port)
+ m.Unregister()
}
func TestTritonSDRefreshNoTargets(t *testing.T) {
@@ -155,25 +175,27 @@ func TestTritonSDRefreshMultipleTargets(t *testing.T) {
tgts := testTritonSDRefresh(t, conf, dstr)
require.NotNil(t, tgts)
- require.Equal(t, 2, len(tgts))
+ require.Len(t, tgts, 2)
}
func TestTritonSDRefreshNoServer(t *testing.T) {
- td, _ := newTritonDiscovery(conf)
+ td, m, _ := newTritonDiscovery(conf)
_, err := td.refresh(context.Background())
require.Error(t, err)
- require.Equal(t, strings.Contains(err.Error(), "an error occurred when requesting targets from the discovery endpoint"), true)
+ require.True(t, strings.Contains(err.Error(), "an error occurred when requesting targets from the discovery endpoint"))
+ m.Unregister()
}
func TestTritonSDRefreshCancelled(t *testing.T) {
- td, _ := newTritonDiscovery(conf)
+ td, m, _ := newTritonDiscovery(conf)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := td.refresh(ctx)
require.Error(t, err)
- require.Equal(t, strings.Contains(err.Error(), context.Canceled.Error()), true)
+ require.True(t, strings.Contains(err.Error(), context.Canceled.Error()))
+ m.Unregister()
}
func TestTritonSDRefreshCNsUUIDOnly(t *testing.T) {
@@ -188,7 +210,7 @@ func TestTritonSDRefreshCNsUUIDOnly(t *testing.T) {
tgts := testTritonSDRefresh(t, cnconf, dstr)
require.NotNil(t, tgts)
- require.Equal(t, 2, len(tgts))
+ require.Len(t, tgts, 2)
}
func TestTritonSDRefreshCNsWithHostname(t *testing.T) {
@@ -205,13 +227,13 @@ func TestTritonSDRefreshCNsWithHostname(t *testing.T) {
tgts := testTritonSDRefresh(t, cnconf, dstr)
require.NotNil(t, tgts)
- require.Equal(t, 2, len(tgts))
+ require.Len(t, tgts, 2)
}
func testTritonSDRefresh(t *testing.T, c SDConfig, dstr string) []model.LabelSet {
var (
- td, _ = newTritonDiscovery(c)
- s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ td, m, _ = newTritonDiscovery(c)
+ s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, dstr)
}))
)
@@ -235,9 +257,11 @@ func testTritonSDRefresh(t *testing.T, c SDConfig, dstr string) []model.LabelSet
tgs, err := td.refresh(context.Background())
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
+ m.Unregister()
+
return tg.Targets
}
diff --git a/discovery/util.go b/discovery/util.go
new file mode 100644
index 0000000000..4e2a088518
--- /dev/null
+++ b/discovery/util.go
@@ -0,0 +1,72 @@
+// Copyright 2020 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package discovery
+
+import (
+ "fmt"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// MetricRegisterer is used by implementations of discovery.Discoverer that need
+// to manage the lifetime of their metrics.
+type MetricRegisterer interface {
+ RegisterMetrics() error
+ UnregisterMetrics()
+}
+
+// metricRegistererImpl is an implementation of MetricRegisterer.
+type metricRegistererImpl struct {
+ reg prometheus.Registerer
+ metrics []prometheus.Collector
+}
+
+var _ MetricRegisterer = &metricRegistererImpl{}
+
+// NewMetricRegisterer creates an instance of a MetricRegisterer.
+// Typically called inside the implementation of the NewDiscoverer() method.
+func NewMetricRegisterer(reg prometheus.Registerer, metrics []prometheus.Collector) MetricRegisterer {
+ return &metricRegistererImpl{
+ reg: reg,
+ metrics: metrics,
+ }
+}
+
+// RegisterMetrics registers the metrics with a Prometheus registerer.
+// If any metric fails to register, it will unregister all metrics that
+// were registered so far, and return an error.
+// Typically called at the start of the SD's Run() method.
+func (rh *metricRegistererImpl) RegisterMetrics() error {
+ for _, collector := range rh.metrics {
+ err := rh.reg.Register(collector)
+ if err != nil {
+ // Unregister all metrics that were registered so far.
+ // This is so that if RegisterMetrics() gets called again,
+ // there will not be an error due to a duplicate registration.
+ rh.UnregisterMetrics()
+
+ return fmt.Errorf("failed to register metric: %w", err)
+ }
+ }
+ return nil
+}
+
+// UnregisterMetrics unregisters the metrics from the same Prometheus
+// registerer which was used to register them.
+// Typically called at the end of the SD's Run() method by a defer statement.
+func (rh *metricRegistererImpl) UnregisterMetrics() {
+ for _, collector := range rh.metrics {
+ rh.reg.Unregister(collector)
+ }
+}
diff --git a/discovery/uyuni/metrics.go b/discovery/uyuni/metrics.go
new file mode 100644
index 0000000000..6793b59206
--- /dev/null
+++ b/discovery/uyuni/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package uyuni
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*uyuniMetrics)(nil)
+
+type uyuniMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *uyuniMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *uyuniMetrics) Unregister() {}
diff --git a/discovery/uyuni/uyuni.go b/discovery/uyuni/uyuni.go
index e37acbf98a..2ab3396951 100644
--- a/discovery/uyuni/uyuni.go
+++ b/discovery/uyuni/uyuni.go
@@ -20,11 +20,13 @@ import (
"net/http"
"net/url"
"path"
+ "strconv"
"strings"
"time"
"github.com/go-kit/log"
"github.com/kolo/xmlrpc"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -39,10 +41,10 @@ const (
uyuniMetaLabelPrefix = model.MetaLabelPrefix + "uyuni_"
uyuniLabelMinionHostname = uyuniMetaLabelPrefix + "minion_hostname"
uyuniLabelPrimaryFQDN = uyuniMetaLabelPrefix + "primary_fqdn"
- uyuniLablelSystemID = uyuniMetaLabelPrefix + "system_id"
- uyuniLablelGroups = uyuniMetaLabelPrefix + "groups"
- uyuniLablelEndpointName = uyuniMetaLabelPrefix + "endpoint_name"
- uyuniLablelExporter = uyuniMetaLabelPrefix + "exporter"
+ uyuniLabelSystemID = uyuniMetaLabelPrefix + "system_id"
+ uyuniLabelGroups = uyuniMetaLabelPrefix + "groups"
+ uyuniLabelEndpointName = uyuniMetaLabelPrefix + "endpoint_name"
+ uyuniLabelExporter = uyuniMetaLabelPrefix + "exporter"
uyuniLabelProxyModule = uyuniMetaLabelPrefix + "proxy_module"
uyuniLabelMetricsPath = uyuniMetaLabelPrefix + "metrics_path"
uyuniLabelScheme = uyuniMetaLabelPrefix + "scheme"
@@ -110,12 +112,19 @@ type Discovery struct {
logger log.Logger
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &uyuniMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "uyuni" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -146,7 +155,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
if c.Password == "" {
return errors.New("Uyuni SD configuration requires a password")
}
- return nil
+ return c.HTTPClientConfig.Validate()
}
func login(rpcclient *xmlrpc.Client, user, pass string, duration int) (string, error) {
@@ -203,7 +212,12 @@ func getEndpointInfoForSystems(
}
// NewDiscovery returns a uyuni discovery for the given configuration.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*uyuniMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
apiURL, err := url.Parse(conf.Server)
if err != nil {
return nil, err
@@ -227,10 +241,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "uyuni",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "uyuni",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
@@ -253,10 +270,10 @@ func (d *Discovery) getEndpointLabels(
model.AddressLabel: model.LabelValue(addr),
uyuniLabelMinionHostname: model.LabelValue(networkInfo.Hostname),
uyuniLabelPrimaryFQDN: model.LabelValue(networkInfo.PrimaryFQDN),
- uyuniLablelSystemID: model.LabelValue(fmt.Sprintf("%d", endpoint.SystemID)),
- uyuniLablelGroups: model.LabelValue(strings.Join(managedGroupNames, d.separator)),
- uyuniLablelEndpointName: model.LabelValue(endpoint.EndpointName),
- uyuniLablelExporter: model.LabelValue(endpoint.ExporterName),
+ uyuniLabelSystemID: model.LabelValue(strconv.Itoa(endpoint.SystemID)),
+ uyuniLabelGroups: model.LabelValue(strings.Join(managedGroupNames, d.separator)),
+ uyuniLabelEndpointName: model.LabelValue(endpoint.EndpointName),
+ uyuniLabelExporter: model.LabelValue(endpoint.ExporterName),
uyuniLabelProxyModule: model.LabelValue(endpoint.Module),
uyuniLabelMetricsPath: model.LabelValue(endpoint.Path),
uyuniLabelScheme: model.LabelValue(scheme),
diff --git a/discovery/uyuni/uyuni_test.go b/discovery/uyuni/uyuni_test.go
index d045cde6d7..09be23e2b4 100644
--- a/discovery/uyuni/uyuni_test.go
+++ b/discovery/uyuni/uyuni_test.go
@@ -23,6 +23,9 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -35,7 +38,17 @@ func testUpdateServices(respHandler http.HandlerFunc) ([]*targetgroup.Group, err
Server: ts.URL,
}
- md, err := NewDiscovery(&conf, nil)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := conf.NewDiscovererMetrics(reg, refreshMetrics)
+ err := metrics.Register()
+ if err != nil {
+ return nil, err
+ }
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ md, err := NewDiscovery(&conf, nil, metrics)
if err != nil {
return nil, err
}
@@ -55,7 +68,7 @@ func TestUyuniSDHandleError(t *testing.T) {
tgs, err := testUpdateServices(respHandler)
require.EqualError(t, err, errTesting)
- require.Equal(t, len(tgs), 0)
+ require.Empty(t, tgs)
}
func TestUyuniSDLogin(t *testing.T) {
@@ -87,7 +100,7 @@ func TestUyuniSDLogin(t *testing.T) {
tgs, err := testUpdateServices(respHandler)
require.EqualError(t, err, errTesting)
- require.Equal(t, len(tgs), 0)
+ require.Empty(t, tgs)
}
func TestUyuniSDSkipLogin(t *testing.T) {
@@ -108,7 +121,14 @@ func TestUyuniSDSkipLogin(t *testing.T) {
Server: ts.URL,
}
- md, err := NewDiscovery(&conf, nil)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := conf.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ md, err := NewDiscovery(&conf, nil, metrics)
if err != nil {
t.Error(err)
}
@@ -119,5 +139,5 @@ func TestUyuniSDSkipLogin(t *testing.T) {
tgs, err := md.refresh(context.Background())
require.EqualError(t, err, errTesting)
- require.Equal(t, len(tgs), 0)
+ require.Empty(t, tgs)
}
diff --git a/discovery/vultr/metrics.go b/discovery/vultr/metrics.go
new file mode 100644
index 0000000000..193436aad2
--- /dev/null
+++ b/discovery/vultr/metrics.go
@@ -0,0 +1,32 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vultr
+
+import (
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*vultrMetrics)(nil)
+
+type vultrMetrics struct {
+ refreshMetrics discovery.RefreshMetricsInstantiator
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *vultrMetrics) Register() error {
+ return nil
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *vultrMetrics) Unregister() {}
diff --git a/discovery/vultr/mock_test.go b/discovery/vultr/mock_test.go
index 417866fcef..bfc24d06fb 100644
--- a/discovery/vultr/mock_test.go
+++ b/discovery/vultr/mock_test.go
@@ -20,7 +20,7 @@ import (
"testing"
)
-// SDMock is the interface for the Vultr mock
+// SDMock is the interface for the Vultr mock.
type SDMock struct {
t *testing.T
Server *httptest.Server
diff --git a/discovery/vultr/vultr.go b/discovery/vultr/vultr.go
index 42881d3c19..aaa9c64e47 100644
--- a/discovery/vultr/vultr.go
+++ b/discovery/vultr/vultr.go
@@ -23,6 +23,7 @@ import (
"time"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/version"
@@ -73,12 +74,19 @@ type SDConfig struct {
Port int `yaml:"port"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &vultrMetrics{
+ refreshMetrics: rmi,
+ }
+}
+
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "vultr" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
- return NewDiscovery(c, opts.Logger)
+ return NewDiscovery(c, opts.Logger, opts.Metrics)
}
// SetDirectory joins any relative file paths with dir.
@@ -106,7 +114,12 @@ type Discovery struct {
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
-func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
+func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (*Discovery, error) {
+ m, ok := metrics.(*vultrMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
d := &Discovery{
port: conf.Port,
}
@@ -128,10 +141,13 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
}
d.Discovery = refresh.NewDiscovery(
- logger,
- "vultr",
- time.Duration(conf.RefreshInterval),
- d.refresh,
+ refresh.Options{
+ Logger: logger,
+ Mech: "vultr",
+ Interval: time.Duration(conf.RefreshInterval),
+ RefreshF: d.refresh,
+ MetricsInstantiator: m.refreshMetrics,
+ },
)
return d, nil
}
diff --git a/discovery/vultr/vultr_test.go b/discovery/vultr/vultr_test.go
index b729541531..2f12a35529 100644
--- a/discovery/vultr/vultr_test.go
+++ b/discovery/vultr/vultr_test.go
@@ -20,8 +20,11 @@ import (
"testing"
"github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
+
+ "github.com/prometheus/prometheus/discovery"
)
type VultrSDTestSuite struct {
@@ -46,7 +49,15 @@ func TestVultrSDRefresh(t *testing.T) {
cfg := DefaultSDConfig
cfg.HTTPClientConfig.BearerToken = APIKey
- d, err := NewDiscovery(&cfg, log.NewNopLogger())
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ defer metrics.Unregister()
+ defer refreshMetrics.Unregister()
+
+ d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics)
require.NoError(t, err)
endpoint, err := url.Parse(sdMock.Mock.Endpoint())
require.NoError(t, err)
@@ -56,12 +67,12 @@ func TestVultrSDRefresh(t *testing.T) {
tgs, err := d.refresh(ctx)
require.NoError(t, err)
- require.Equal(t, 1, len(tgs))
+ require.Len(t, tgs, 1)
tg := tgs[0]
require.NotNil(t, tg)
require.NotNil(t, tg.Targets)
- require.Equal(t, 3, len(tg.Targets))
+ require.Len(t, tg.Targets, 3)
for i, k := range []model.LabelSet{
{
diff --git a/discovery/xds/client.go b/discovery/xds/client.go
index 9844c6d7ed..027ceb2715 100644
--- a/discovery/xds/client.go
+++ b/discovery/xds/client.go
@@ -179,7 +179,7 @@ func (rc *HTTPResourceClient) Fetch(ctx context.Context) (*v3.DiscoveryResponse,
return nil, err
}
- request, err := http.NewRequest("POST", rc.endpoint, bytes.NewBuffer(reqBody))
+ request, err := http.NewRequest(http.MethodPost, rc.endpoint, bytes.NewBuffer(reqBody))
if err != nil {
return nil, err
}
diff --git a/discovery/xds/client_test.go b/discovery/xds/client_test.go
index ff5217359c..b699995fb7 100644
--- a/discovery/xds/client_test.go
+++ b/discovery/xds/client_test.go
@@ -53,14 +53,14 @@ func TestMakeXDSResourceHttpEndpointEmptyServerURLScheme(t *testing.T) {
require.Empty(t, endpointURL)
require.Error(t, err)
- require.Equal(t, err.Error(), "invalid xDS server URL")
+ require.Equal(t, "invalid xDS server URL", err.Error())
}
func TestMakeXDSResourceHttpEndpointEmptyServerURLHost(t *testing.T) {
endpointURL, err := makeXDSResourceHTTPEndpointURL(ProtocolV3, urlMustParse("grpc://127.0.0.1"), "monitoring")
require.Empty(t, endpointURL)
- require.NotNil(t, err)
+ require.Error(t, err)
require.Contains(t, err.Error(), "must be either 'http' or 'https'")
}
@@ -68,7 +68,7 @@ func TestMakeXDSResourceHttpEndpoint(t *testing.T) {
endpointURL, err := makeXDSResourceHTTPEndpointURL(ProtocolV3, urlMustParse("http://127.0.0.1:5000"), "monitoring")
require.NoError(t, err)
- require.Equal(t, endpointURL.String(), "http://127.0.0.1:5000/v3/discovery:monitoring")
+ require.Equal(t, "http://127.0.0.1:5000/v3/discovery:monitoring", endpointURL.String())
}
func TestCreateNewHTTPResourceClient(t *testing.T) {
@@ -89,8 +89,8 @@ func TestCreateNewHTTPResourceClient(t *testing.T) {
require.NoError(t, err)
- require.Equal(t, client.endpoint, "http://127.0.0.1:5000/v3/discovery:monitoring?param1=v1")
- require.Equal(t, client.client.Timeout, 1*time.Minute)
+ require.Equal(t, "http://127.0.0.1:5000/v3/discovery:monitoring?param1=v1", client.endpoint)
+ require.Equal(t, 1*time.Minute, client.client.Timeout)
}
func createTestHTTPResourceClient(t *testing.T, conf *HTTPResourceClientConfig, protocolVersion ProtocolVersion, responder discoveryResponder) (*HTTPResourceClient, func()) {
@@ -138,7 +138,7 @@ func TestHTTPResourceClientFetchFullResponse(t *testing.T) {
require.NotNil(t, res)
require.Equal(t, client.ResourceTypeURL(), res.TypeUrl)
- require.Len(t, res.Resources, 0)
+ require.Empty(t, res.Resources)
require.Equal(t, "abc", client.latestNonce, "Nonce not cached")
require.Equal(t, "1", client.latestVersion, "Version not cached")
diff --git a/discovery/xds/kuma.go b/discovery/xds/kuma.go
index bc88ba5540..d1d540aaf4 100644
--- a/discovery/xds/kuma.go
+++ b/discovery/xds/kuma.go
@@ -30,35 +30,12 @@ import (
"github.com/prometheus/prometheus/util/strutil"
)
-var (
- // DefaultKumaSDConfig is the default Kuma MADS SD configuration.
- DefaultKumaSDConfig = KumaSDConfig{
- HTTPClientConfig: config.DefaultHTTPClientConfig,
- RefreshInterval: model.Duration(15 * time.Second),
- FetchTimeout: model.Duration(2 * time.Minute),
- }
-
- kumaFetchFailuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Namespace: namespace,
- Name: "sd_kuma_fetch_failures_total",
- Help: "The number of Kuma MADS fetch call failures.",
- })
- kumaFetchSkipUpdateCount = prometheus.NewCounter(
- prometheus.CounterOpts{
- Namespace: namespace,
- Name: "sd_kuma_fetch_skipped_updates_total",
- Help: "The number of Kuma MADS fetch calls that result in no updates to the targets.",
- })
- kumaFetchDuration = prometheus.NewSummary(
- prometheus.SummaryOpts{
- Namespace: namespace,
- Name: "sd_kuma_fetch_duration_seconds",
- Help: "The duration of a Kuma MADS fetch call.",
- Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
- },
- )
-)
+// DefaultKumaSDConfig is the default Kuma MADS SD configuration.
+var DefaultKumaSDConfig = KumaSDConfig{
+ HTTPClientConfig: config.DefaultHTTPClientConfig,
+ RefreshInterval: model.Duration(15 * time.Second),
+ FetchTimeout: model.Duration(2 * time.Minute),
+}
const (
// kumaMetaLabelPrefix is the meta prefix used for all kuma meta labels.
@@ -81,6 +58,11 @@ const (
type KumaSDConfig = SDConfig
+// NewDiscovererMetrics implements discovery.Config.
+func (*KumaSDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return newDiscovererMetrics(reg, rmi)
+}
+
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *KumaSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultKumaSDConfig
@@ -120,7 +102,7 @@ func (c *KumaSDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discover
logger = log.NewNopLogger()
}
- return NewKumaHTTPDiscovery(c, logger)
+ return NewKumaHTTPDiscovery(c, logger, opts.Metrics)
}
func convertKumaV1MonitoringAssignment(assignment *MonitoringAssignment) []model.LabelSet {
@@ -176,12 +158,21 @@ func kumaMadsV1ResourceParser(resources []*anypb.Any, typeURL string) ([]model.L
return targets, nil
}
-func NewKumaHTTPDiscovery(conf *KumaSDConfig, logger log.Logger) (discovery.Discoverer, error) {
+func NewKumaHTTPDiscovery(conf *KumaSDConfig, logger log.Logger, metrics discovery.DiscovererMetrics) (discovery.Discoverer, error) {
+ m, ok := metrics.(*xdsMetrics)
+ if !ok {
+ return nil, fmt.Errorf("invalid discovery metrics type")
+ }
+
// Default to "prometheus" if hostname is unavailable.
- clientID, err := osutil.GetFQDN()
- if err != nil {
- level.Debug(logger).Log("msg", "error getting FQDN", "err", err)
- clientID = "prometheus"
+ clientID := conf.ClientID
+ if clientID == "" {
+ var err error
+ clientID, err = osutil.GetFQDN()
+ if err != nil {
+ level.Debug(logger).Log("msg", "error getting FQDN", "err", err)
+ clientID = "prometheus"
+ }
}
clientConfig := &HTTPResourceClientConfig{
@@ -203,14 +194,12 @@ func NewKumaHTTPDiscovery(conf *KumaSDConfig, logger log.Logger) (discovery.Disc
}
d := &fetchDiscovery{
- client: client,
- logger: logger,
- refreshInterval: time.Duration(conf.RefreshInterval),
- source: "kuma",
- parseResources: kumaMadsV1ResourceParser,
- fetchFailuresCount: kumaFetchFailuresCount,
- fetchSkipUpdateCount: kumaFetchSkipUpdateCount,
- fetchDuration: kumaFetchDuration,
+ client: client,
+ logger: logger,
+ refreshInterval: time.Duration(conf.RefreshInterval),
+ source: "kuma",
+ parseResources: kumaMadsV1ResourceParser,
+ metrics: m,
}
return d, nil
diff --git a/discovery/xds/kuma_test.go b/discovery/xds/kuma_test.go
index 1db0a0831d..cfb9cbac50 100644
--- a/discovery/xds/kuma_test.go
+++ b/discovery/xds/kuma_test.go
@@ -21,12 +21,14 @@ import (
"time"
v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"gopkg.in/yaml.v2"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
@@ -107,7 +109,16 @@ func getKumaMadsV1DiscoveryResponse(resources ...*MonitoringAssignment) (*v3.Dis
}
func newKumaTestHTTPDiscovery(c KumaSDConfig) (*fetchDiscovery, error) {
- kd, err := NewKumaHTTPDiscovery(&c, nopLogger)
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ // TODO(ptodev): Add the ability to unregister refresh metrics.
+ metrics := c.NewDiscovererMetrics(reg, refreshMetrics)
+ err := metrics.Register()
+ if err != nil {
+ return nil, err
+ }
+
+ kd, err := NewKumaHTTPDiscovery(&c, nopLogger, metrics)
if err != nil {
return nil, err
}
@@ -129,7 +140,7 @@ func TestKumaMadsV1ResourceParserInvalidTypeURL(t *testing.T) {
func TestKumaMadsV1ResourceParserEmptySlice(t *testing.T) {
resources := make([]*anypb.Any, 0)
groups, err := kumaMadsV1ResourceParser(resources, KumaMadsV1ResourceTypeURL)
- require.Len(t, groups, 0)
+ require.Empty(t, groups)
require.NoError(t, err)
}
@@ -204,8 +215,10 @@ func TestNewKumaHTTPDiscovery(t *testing.T) {
require.True(t, ok)
require.Equal(t, kumaConf.Server, resClient.Server())
require.Equal(t, KumaMadsV1ResourceTypeURL, resClient.ResourceTypeURL())
- require.NotEmpty(t, resClient.ID())
+ require.Equal(t, kumaConf.ClientID, resClient.ID())
require.Equal(t, KumaMadsV1ResourceType, resClient.config.ResourceType)
+
+ kd.metrics.Unregister()
}
func TestKumaHTTPDiscoveryRefresh(t *testing.T) {
@@ -300,4 +313,6 @@ tls_config:
case <-ch:
require.Fail(t, "no update expected")
}
+
+ kd.metrics.Unregister()
}
diff --git a/discovery/xds/metrics.go b/discovery/xds/metrics.go
new file mode 100644
index 0000000000..597d516566
--- /dev/null
+++ b/discovery/xds/metrics.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xds
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/prometheus/prometheus/discovery"
+)
+
+var _ discovery.DiscovererMetrics = (*xdsMetrics)(nil)
+
+type xdsMetrics struct {
+ fetchDuration prometheus.Summary
+ fetchSkipUpdateCount prometheus.Counter
+ fetchFailuresCount prometheus.Counter
+
+ metricRegisterer discovery.MetricRegisterer
+}
+
+func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ m := &xdsMetrics{
+ fetchFailuresCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Namespace: namespace,
+ Name: "sd_kuma_fetch_failures_total",
+ Help: "The number of Kuma MADS fetch call failures.",
+ }),
+ fetchSkipUpdateCount: prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Namespace: namespace,
+ Name: "sd_kuma_fetch_skipped_updates_total",
+ Help: "The number of Kuma MADS fetch calls that result in no updates to the targets.",
+ }),
+ fetchDuration: prometheus.NewSummary(
+ prometheus.SummaryOpts{
+ Namespace: namespace,
+ Name: "sd_kuma_fetch_duration_seconds",
+ Help: "The duration of a Kuma MADS fetch call.",
+ Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
+ },
+ ),
+ }
+
+ m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{
+ m.fetchFailuresCount,
+ m.fetchSkipUpdateCount,
+ m.fetchDuration,
+ })
+
+ return m
+}
+
+// Register implements discovery.DiscovererMetrics.
+func (m *xdsMetrics) Register() error {
+ return m.metricRegisterer.RegisterMetrics()
+}
+
+// Unregister implements discovery.DiscovererMetrics.
+func (m *xdsMetrics) Unregister() {
+ m.metricRegisterer.UnregisterMetrics()
+}
diff --git a/discovery/xds/xds.go b/discovery/xds/xds.go
index 48bdbab02b..8191d6be1a 100644
--- a/discovery/xds/xds.go
+++ b/discovery/xds/xds.go
@@ -20,7 +20,6 @@ import (
v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
- "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"google.golang.org/protobuf/encoding/protojson"
@@ -55,6 +54,7 @@ type SDConfig struct {
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
FetchTimeout model.Duration `yaml:"fetch_timeout,omitempty"`
Server string `yaml:"server,omitempty"`
+ ClientID string `yaml:"client_id,omitempty"`
}
// mustRegisterMessage registers the provided message type in the typeRegistry, and panics
@@ -69,9 +69,6 @@ func init() {
// Register top-level SD Configs.
discovery.RegisterConfig(&KumaSDConfig{})
- // Register metrics.
- prometheus.MustRegister(kumaFetchDuration, kumaFetchSkipUpdateCount, kumaFetchFailuresCount)
-
// Register protobuf types that need to be marshalled/ unmarshalled.
mustRegisterMessage(protoTypes, (&v3.DiscoveryRequest{}).ProtoReflect().Type())
mustRegisterMessage(protoTypes, (&v3.DiscoveryResponse{}).ProtoReflect().Type())
@@ -109,9 +106,7 @@ type fetchDiscovery struct {
parseResources resourceParser
logger log.Logger
- fetchDuration prometheus.Observer
- fetchSkipUpdateCount prometheus.Counter
- fetchFailuresCount prometheus.Counter
+ metrics *xdsMetrics
}
func (d *fetchDiscovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
@@ -135,7 +130,7 @@ func (d *fetchDiscovery) poll(ctx context.Context, ch chan<- []*targetgroup.Grou
t0 := time.Now()
response, err := d.client.Fetch(ctx)
elapsed := time.Since(t0)
- d.fetchDuration.Observe(elapsed.Seconds())
+ d.metrics.fetchDuration.Observe(elapsed.Seconds())
// Check the context before in order to exit early.
select {
@@ -146,20 +141,20 @@ func (d *fetchDiscovery) poll(ctx context.Context, ch chan<- []*targetgroup.Grou
if err != nil {
level.Error(d.logger).Log("msg", "error parsing resources", "err", err)
- d.fetchFailuresCount.Inc()
+ d.metrics.fetchFailuresCount.Inc()
return
}
if response == nil {
// No update needed.
- d.fetchSkipUpdateCount.Inc()
+ d.metrics.fetchSkipUpdateCount.Inc()
return
}
parsedTargets, err := d.parseResources(response.Resources, response.TypeUrl)
if err != nil {
level.Error(d.logger).Log("msg", "error parsing resources", "err", err)
- d.fetchFailuresCount.Inc()
+ d.metrics.fetchFailuresCount.Inc()
return
}
diff --git a/discovery/xds/xds_test.go b/discovery/xds/xds_test.go
index 974a47342f..7cce021c5f 100644
--- a/discovery/xds/xds_test.go
+++ b/discovery/xds/xds_test.go
@@ -29,23 +29,15 @@ import (
"go.uber.org/goleak"
"google.golang.org/protobuf/types/known/anypb"
+ "github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
-var (
- sdConf = SDConfig{
- Server: "http://127.0.0.1",
- RefreshInterval: model.Duration(10 * time.Second),
- }
-
- testFetchFailuresCount = prometheus.NewCounter(
- prometheus.CounterOpts{})
- testFetchSkipUpdateCount = prometheus.NewCounter(
- prometheus.CounterOpts{})
- testFetchDuration = prometheus.NewSummary(
- prometheus.SummaryOpts{},
- )
-)
+var sdConf = SDConfig{
+ Server: "http://127.0.0.1",
+ RefreshInterval: model.Duration(10 * time.Second),
+ ClientID: "test-id",
+}
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
@@ -132,12 +124,22 @@ func TestPollingRefreshSkipUpdate(t *testing.T) {
return nil, nil
},
}
+
+ cfg := &SDConfig{}
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ xdsMetrics, ok := metrics.(*xdsMetrics)
+ if !ok {
+ require.Fail(t, "invalid discovery metrics type")
+ }
+
pd := &fetchDiscovery{
- client: rc,
- logger: nopLogger,
- fetchDuration: testFetchDuration,
- fetchFailuresCount: testFetchFailuresCount,
- fetchSkipUpdateCount: testFetchSkipUpdateCount,
+ client: rc,
+ logger: nopLogger,
+ metrics: xdsMetrics,
}
ctx, cancel := context.WithCancel(context.Background())
@@ -154,6 +156,9 @@ func TestPollingRefreshSkipUpdate(t *testing.T) {
case <-ch:
require.Fail(t, "no update expected")
}
+
+ metrics.Unregister()
+ refreshMetrics.Unregister()
}
func TestPollingRefreshAttachesGroupMetadata(t *testing.T) {
@@ -166,13 +171,18 @@ func TestPollingRefreshAttachesGroupMetadata(t *testing.T) {
return &v3.DiscoveryResponse{}, nil
},
}
+
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := newDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+ xdsMetrics, ok := metrics.(*xdsMetrics)
+ require.True(t, ok)
+
pd := &fetchDiscovery{
- source: source,
- client: rc,
- logger: nopLogger,
- fetchDuration: testFetchDuration,
- fetchFailuresCount: testFetchFailuresCount,
- fetchSkipUpdateCount: testFetchSkipUpdateCount,
+ source: source,
+ client: rc,
+ logger: nopLogger,
parseResources: constantResourceParser([]model.LabelSet{
{
"__meta_custom_xds_label": "a-value",
@@ -185,6 +195,7 @@ func TestPollingRefreshAttachesGroupMetadata(t *testing.T) {
"instance": "prometheus-02",
},
}, nil),
+ metrics: xdsMetrics,
}
ch := make(chan []*targetgroup.Group, 1)
pd.poll(context.Background(), ch)
@@ -201,6 +212,9 @@ func TestPollingRefreshAttachesGroupMetadata(t *testing.T) {
target2 := group.Targets[1]
require.Contains(t, target2, model.LabelName("__meta_custom_xds_label"))
require.Equal(t, model.LabelValue("a-value"), target2["__meta_custom_xds_label"])
+
+ metrics.Unregister()
+ refreshMetrics.Unregister()
}
func TestPollingDisappearingTargets(t *testing.T) {
@@ -242,14 +256,23 @@ func TestPollingDisappearingTargets(t *testing.T) {
}, nil
}
+ cfg := &SDConfig{}
+ reg := prometheus.NewRegistry()
+ refreshMetrics := discovery.NewRefreshMetrics(reg)
+ metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics)
+ require.NoError(t, metrics.Register())
+
+ xdsMetrics, ok := metrics.(*xdsMetrics)
+ if !ok {
+ require.Fail(t, "invalid discovery metrics type")
+ }
+
pd := &fetchDiscovery{
- source: source,
- client: rc,
- logger: nopLogger,
- fetchDuration: testFetchDuration,
- fetchFailuresCount: testFetchFailuresCount,
- fetchSkipUpdateCount: testFetchSkipUpdateCount,
- parseResources: parser,
+ source: source,
+ client: rc,
+ logger: nopLogger,
+ parseResources: parser,
+ metrics: xdsMetrics,
}
ch := make(chan []*targetgroup.Group, 1)
@@ -270,4 +293,7 @@ func TestPollingDisappearingTargets(t *testing.T) {
require.Equal(t, source, groups[0].Source)
require.Len(t, groups[0].Targets, 1)
+
+ metrics.Unregister()
+ refreshMetrics.Unregister()
}
diff --git a/discovery/zookeeper/zookeeper.go b/discovery/zookeeper/zookeeper.go
index cadff5fd2e..92904dd71c 100644
--- a/discovery/zookeeper/zookeeper.go
+++ b/discovery/zookeeper/zookeeper.go
@@ -25,6 +25,7 @@ import (
"github.com/go-kit/log"
"github.com/go-zookeeper/zk"
+ "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/discovery"
@@ -56,6 +57,11 @@ type ServersetSDConfig struct {
Timeout model.Duration `yaml:"timeout,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*ServersetSDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &discovery.NoopDiscovererMetrics{}
+}
+
// Name returns the name of the Config.
func (*ServersetSDConfig) Name() string { return "serverset" }
@@ -93,6 +99,11 @@ type NerveSDConfig struct {
Timeout model.Duration `yaml:"timeout,omitempty"`
}
+// NewDiscovererMetrics implements discovery.Config.
+func (*NerveSDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
+ return &discovery.NoopDiscovererMetrics{}
+}
+
// Name returns the name of the Config.
func (*NerveSDConfig) Name() string { return "nerve" }
@@ -193,7 +204,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
}
for _, pathUpdate := range d.pathUpdates {
// Drain event channel in case the treecache leaks goroutines otherwise.
- for range pathUpdate { // nolint:revive
+ for range pathUpdate {
}
}
d.conn.Close()
@@ -269,18 +280,17 @@ func parseServersetMember(data []byte, path string) (model.LabelSet, error) {
labels := model.LabelSet{}
labels[serversetPathLabel] = model.LabelValue(path)
labels[model.AddressLabel] = model.LabelValue(
- net.JoinHostPort(member.ServiceEndpoint.Host, fmt.Sprintf("%d", member.ServiceEndpoint.Port)))
+ net.JoinHostPort(member.ServiceEndpoint.Host, strconv.Itoa(member.ServiceEndpoint.Port)))
labels[serversetEndpointLabelPrefix+"_host"] = model.LabelValue(member.ServiceEndpoint.Host)
- labels[serversetEndpointLabelPrefix+"_port"] = model.LabelValue(fmt.Sprintf("%d", member.ServiceEndpoint.Port))
+ labels[serversetEndpointLabelPrefix+"_port"] = model.LabelValue(strconv.Itoa(member.ServiceEndpoint.Port))
for name, endpoint := range member.AdditionalEndpoints {
cleanName := model.LabelName(strutil.SanitizeLabelName(name))
labels[serversetEndpointLabelPrefix+"_host_"+cleanName] = model.LabelValue(
endpoint.Host)
labels[serversetEndpointLabelPrefix+"_port_"+cleanName] = model.LabelValue(
- fmt.Sprintf("%d", endpoint.Port))
-
+ strconv.Itoa(endpoint.Port))
}
labels[serversetStatusLabel] = model.LabelValue(member.Status)
@@ -311,10 +321,10 @@ func parseNerveMember(data []byte, path string) (model.LabelSet, error) {
labels := model.LabelSet{}
labels[nervePathLabel] = model.LabelValue(path)
labels[model.AddressLabel] = model.LabelValue(
- net.JoinHostPort(member.Host, fmt.Sprintf("%d", member.Port)))
+ net.JoinHostPort(member.Host, strconv.Itoa(member.Port)))
labels[nerveEndpointLabelPrefix+"_host"] = model.LabelValue(member.Host)
- labels[nerveEndpointLabelPrefix+"_port"] = model.LabelValue(fmt.Sprintf("%d", member.Port))
+ labels[nerveEndpointLabelPrefix+"_port"] = model.LabelValue(strconv.Itoa(member.Port))
labels[nerveEndpointLabelPrefix+"_name"] = model.LabelValue(member.Name)
return labels, nil
diff --git a/discovery/zookeeper/zookeeper_test.go b/discovery/zookeeper/zookeeper_test.go
index d0e67b50ac..c2b41ce7a3 100644
--- a/discovery/zookeeper/zookeeper_test.go
+++ b/discovery/zookeeper/zookeeper_test.go
@@ -18,6 +18,7 @@ import (
"time"
"github.com/prometheus/common/model"
+ "github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
@@ -31,7 +32,5 @@ func TestNewDiscoveryError(t *testing.T) {
time.Second, []string{"/"},
nil,
func(data []byte, path string) (model.LabelSet, error) { return nil, nil })
- if err == nil {
- t.Fatalf("expected error, got nil")
- }
+ require.Error(t, err)
}
diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md
index 78ec205f24..a179a2f9f1 100644
--- a/docs/command-line/prometheus.md
+++ b/docs/command-line/prometheus.md
@@ -15,23 +15,27 @@ The Prometheus monitoring server
| -h, --help | Show context-sensitive help (also try --help-long and --help-man). | |
| --version | Show application version. | |
| --config.file | Prometheus configuration file path. | `prometheus.yml` |
-| --web.listen-address | Address to listen on for UI, API, and telemetry. | `0.0.0.0:9090` |
+| --config.auto-reload-interval | Specifies the interval for checking and automatically reloading the Prometheus configuration file upon detecting changes. | `30s` |
+| --web.listen-address ... | Address to listen on for UI, API, and telemetry. Can be repeated. | `0.0.0.0:9090` |
+| --auto-gomemlimit.ratio | The ratio of reserved GOMEMLIMIT memory to the detected maximum container or system memory | `0.9` |
| --web.config.file | [EXPERIMENTAL] Path to configuration file that can enable TLS or authentication. | |
| --web.read-timeout | Maximum duration before timing out read of the request, and closing idle connections. | `5m` |
-| --web.max-connections | Maximum number of simultaneous connections. | `512` |
+| --web.max-connections | Maximum number of simultaneous connections across all listeners. | `512` |
+| --web.max-notifications-subscribers | Limits the maximum number of subscribers that can concurrently receive live notifications. If the limit is reached, new subscription requests will be denied until existing connections close. | `16` |
| --web.external-url | The URL under which Prometheus is externally reachable (for example, if Prometheus is served via a reverse proxy). Used for generating relative and absolute links back to Prometheus itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Prometheus. If omitted, relevant URL components will be derived automatically. | |
| --web.route-prefix | Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url. | |
| --web.user-assets | Path to static asset directory, available at /user. | |
| --web.enable-lifecycle | Enable shutdown and reload via HTTP request. | `false` |
| --web.enable-admin-api | Enable API endpoints for admin control actions. | `false` |
| --web.enable-remote-write-receiver | Enable API endpoint accepting remote write requests. | `false` |
+| --web.remote-write-receiver.accepted-protobuf-messages | List of the remote write protobuf messages to accept when receiving the remote writes. Supported values: prometheus.WriteRequest, io.prometheus.write.v2.Request | `prometheus.WriteRequest` |
+| --web.enable-otlp-receiver | Enable API endpoint accepting OTLP write requests. | `false` |
| --web.console.templates | Path to the console template directory, available at /consoles. | `consoles` |
| --web.console.libraries | Path to the console library directory. | `console_libraries` |
| --web.page-title | Document title of Prometheus instance. | `Prometheus Time Series Collection and Processing Server` |
-| --web.cors.origin | Regex for CORS origin. It is fully anchored. Example: 'https?://(domain1|domain2)\.com' | `.*` |
+| --web.cors.origin | Regex for CORS origin. It is fully anchored. Example: 'https?://(domain1\|domain2)\.com' | `.*` |
| --storage.tsdb.path | Base path for metrics storage. Use with server mode only. | `data/` |
-| --storage.tsdb.retention | [DEPRECATED] How long to retain samples in storage. This flag has been deprecated, use "storage.tsdb.retention.time" instead. Use with server mode only. | |
-| --storage.tsdb.retention.time | How long to retain samples in storage. When this flag is set it overrides "storage.tsdb.retention". If neither this flag nor "storage.tsdb.retention" nor "storage.tsdb.retention.size" is set, the retention time defaults to 15d. Units Supported: y, w, d, h, m, s, ms. Use with server mode only. | |
+| --storage.tsdb.retention.time | How long to retain samples in storage. If neither this flag nor "storage.tsdb.retention.size" is set, the retention time defaults to 15d. Units Supported: y, w, d, h, m, s, ms. Use with server mode only. | |
| --storage.tsdb.retention.size | Maximum number of bytes that can be stored for blocks. A unit is required, supported units: B, KB, MB, GB, TB, PB, EB. Ex: "512MB". Based on powers-of-2, so 1KB is 1024B. Use with server mode only. | |
| --storage.tsdb.no-lockfile | Do not create lockfile in data directory. Use with server mode only. | `false` |
| --storage.tsdb.head-chunks-write-queue-size | Size of the queue through which head chunks are written to the disk to be m-mapped, 0 disables the queue completely. Experimental. Use with server mode only. | `0` |
@@ -47,12 +51,15 @@ The Prometheus monitoring server
| --rules.alert.for-outage-tolerance | Max time to tolerate prometheus outage for restoring "for" state of alert. Use with server mode only. | `1h` |
| --rules.alert.for-grace-period | Minimum duration between alert and restored "for" state. This is maintained only for alerts with configured "for" time greater than grace period. Use with server mode only. | `10m` |
| --rules.alert.resend-delay | Minimum amount of time to wait before resending an alert to Alertmanager. Use with server mode only. | `1m` |
+| --rules.max-concurrent-evals | Global concurrency limit for independent rules that can run concurrently. When set, "query.max-concurrency" may need to be adjusted accordingly. Use with server mode only. | `4` |
| --alertmanager.notification-queue-capacity | The capacity of the queue for pending Alertmanager notifications. Use with server mode only. | `10000` |
+| --alertmanager.drain-notification-queue-on-shutdown | Send any outstanding Alertmanager notifications when shutting down. If false, any outstanding Alertmanager notifications will be dropped when shutting down. Use with server mode only. | `true` |
| --query.lookback-delta | The maximum lookback duration for retrieving metrics during expression evaluations and federation. Use with server mode only. | `5m` |
| --query.timeout | Maximum time a query may take before being aborted. Use with server mode only. | `2m` |
| --query.max-concurrency | Maximum number of queries executed concurrently. Use with server mode only. | `20` |
| --query.max-samples | Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return. Use with server mode only. | `50000000` |
-| --enable-feature | Comma separated feature names to enable. Valid options: agent, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | |
+| --enable-feature ... | Comma separated feature names to enable. Valid options: auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, native-histograms, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | |
+| --agent | Run Prometheus in 'Agent mode'. | |
| --log.level | Only log messages with the given severity or above. One of: [debug, info, warn, error] | `info` |
| --log.format | Output format of log messages. One of: [logfmt, json] | `logfmt` |
diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md
index 546b200e27..996a996555 100644
--- a/docs/command-line/promtool.md
+++ b/docs/command-line/promtool.md
@@ -15,7 +15,7 @@ Tooling for the Prometheus monitoring system.
| -h, --help | Show context-sensitive help (also try --help-long and --help-man). |
| --version | Show application version. |
| --experimental | Enable experimental commands. |
-| --enable-feature | Comma separated feature names to enable (only PromQL related and no-default-scrape-port). See https://prometheus.io/docs/prometheus/latest/feature_flags/ for the options and more details. |
+| --enable-feature ... | Comma separated feature names to enable. Currently unused. |
@@ -281,7 +281,7 @@ Run series query.
| Flag | Description |
| --- | --- |
-| --match | Series selector. Can be specified multiple times. |
+| --match ... | Series selector. Can be specified multiple times. |
| --start | Start time (RFC3339 or Unix timestamp). |
| --end | End time (RFC3339 or Unix timestamp). |
@@ -309,7 +309,7 @@ Run labels query.
| --- | --- |
| --start | Start time (RFC3339 or Unix timestamp). |
| --end | End time (RFC3339 or Unix timestamp). |
-| --match | Series selector. Can be specified multiple times. |
+| --match ... | Series selector. Can be specified multiple times. |
@@ -324,6 +324,25 @@ Run labels query.
+##### `promtool query analyze`
+
+Run queries against your Prometheus to analyze the usage pattern of certain metrics.
+
+
+
+###### Flags
+
+| Flag | Description | Default |
+| --- | --- | --- |
+| --server | Prometheus server to query. | |
+| --type | Type of metric: histogram. | |
+| --duration | Time frame to analyze. | `1h` |
+| --time | Query time (RFC3339 or Unix timestamp), defaults to now. | |
+| --match ... | Series selector. Can be specified multiple times. | |
+
+
+
+
### `promtool debug`
Fetch debug information.
@@ -423,12 +442,31 @@ Unit testing.
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| --junit | File path to store JUnit XML test results. |
+
+
+
+
##### `promtool test rules`
Unit tests for rules.
+###### Flags
+
+| Flag | Description | Default |
+| --- | --- | --- |
+| --run ... | If set, will only run test groups whose names match the regular expression. Can be specified multiple times. | |
+| --diff | [Experimental] Print colored differential output between expected & received output. | `false` |
+
+
+
+
###### Arguments
| Argument | Description | Required |
@@ -488,6 +526,7 @@ Analyze churn, label pair cardinality and compaction efficiency.
| --- | --- | --- |
| --limit | How many items to show in each list. | `20` |
| --extended | Run extended analysis. | |
+| --match | Series selector to analyze. Only 1 set of matchers is supported now. | |
@@ -536,9 +575,37 @@ Dump samples from a TSDB.
| Flag | Description | Default |
| --- | --- | --- |
+| --sandbox-dir-root | Root directory where a sandbox directory will be created, this sandbox is used in case WAL replay generates chunks (default is the database path). The sandbox is cleaned up at the end. | |
| --min-time | Minimum timestamp to dump. | `-9223372036854775808` |
| --max-time | Maximum timestamp to dump. | `9223372036854775807` |
-| --match | Series selector. | `{__name__=~'(?s:.*)'}` |
+| --match ... | Series selector. Can be specified multiple times. | `{__name__=~'(?s:.*)'}` |
+
+
+
+
+###### Arguments
+
+| Argument | Description | Default |
+| --- | --- | --- |
+| db path | Database path (default is data/). | `data/` |
+
+
+
+
+##### `promtool tsdb dump-openmetrics`
+
+[Experimental] Dump samples from a TSDB into OpenMetrics text format, excluding native histograms and staleness markers, which are not representable in OpenMetrics.
+
+
+
+###### Flags
+
+| Flag | Description | Default |
+| --- | --- | --- |
+| --sandbox-dir-root | Root directory where a sandbox directory will be created, this sandbox is used in case WAL replay generates chunks (default is the database path). The sandbox is cleaned up at the end. | |
+| --min-time | Minimum timestamp to dump. | `-9223372036854775808` |
+| --max-time | Maximum timestamp to dump. | `9223372036854775807` |
+| --match ... | Series selector. Can be specified multiple times. | `{__name__=~'(?s:.*)'}` |
@@ -574,6 +641,15 @@ Import samples from OpenMetrics input and produce TSDB blocks. Please refer to t
+###### Flags
+
+| Flag | Description |
+| --- | --- |
+| --label | Label to attach to metrics. Can be specified multiple times. Example --label=label_name=label_value |
+
+
+
+
###### Arguments
| Argument | Description | Default | Required |
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 6691902579..096809397a 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -37,7 +37,7 @@ Generic placeholders are defined as follows:
* ``: a floating-point number
* ``: a valid string consisting of a hostname or IP followed by an optional port number
* ``: an integer value
-* ``: a string matching the regular expression `[a-zA-Z_][a-zA-Z0-9_]*`
+* ``: a string matching the regular expression `[a-zA-Z_][a-zA-Z0-9_]*`. Any other unsupported character in the source label should be converted to an underscore. For example, the label `app.kubernetes.io/name` should be written as `app_kubernetes_io_name`.
* ``: a string of unicode characters
* ``: a valid URL path
* ``: a string that can take the values `http` or `https`
@@ -61,9 +61,23 @@ global:
# How long until a scrape request times out.
[ scrape_timeout: | default = 10s ]
+ # The protocols to negotiate during a scrape with the client.
+ # Supported values (case sensitive): PrometheusProto, OpenMetricsText0.0.1,
+ # OpenMetricsText1.0.0, PrometheusText0.0.4.
+ # The default value changes to [ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]
+ # when native_histogram feature flag is set.
+ [ scrape_protocols: [, ...] | default = [ OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ] ]
+
# How frequently to evaluate rules.
[ evaluation_interval: | default = 1m ]
+ # Offset the rule evaluation timestamp of this particular group by the
+ # specified duration into the past to ensure the underlying metrics have
+ # been received. Metric availability delays are more likely to occur when
+ # Prometheus is running as a remote write target, but can also occur when
+ # there's anomalies with scraping.
+ [ rule_query_offset: | default = 0s ]
+
# The labels to add to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
@@ -73,39 +87,60 @@ global:
# Reloading the configuration will reopen the file.
[ query_log_file: ]
+ # File to which scrape failures are logged.
+ # Reloading the configuration will reopen the file.
+ [ scrape_failure_log_file: ]
+
# An uncompressed response body larger than this many bytes will cause the
# scrape to fail. 0 means no limit. Example: 100MB.
# This is an experimental feature, this behaviour could
# change or be removed in the future.
[ body_size_limit: | default = 0 ]
- # Per-scrape limit on number of scraped samples that will be accepted.
+ # Per-scrape limit on the number of scraped samples that will be accepted.
# If more than this number of samples are present after metric relabeling
# the entire scrape will be treated as failed. 0 means no limit.
[ sample_limit: | default = 0 ]
- # Per-scrape limit on number of labels that will be accepted for a sample. If
- # more than this number of labels are present post metric-relabeling, the
- # entire scrape will be treated as failed. 0 means no limit.
+ # Limit on the number of labels that will be accepted per sample. If more
+ # than this number of labels are present on any sample post metric-relabeling,
+ # the entire scrape will be treated as failed. 0 means no limit.
[ label_limit: | default = 0 ]
- # Per-scrape limit on length of labels name that will be accepted for a sample.
- # If a label name is longer than this number post metric-relabeling, the entire
- # scrape will be treated as failed. 0 means no limit.
+ # Limit on the length (in bytes) of each individual label name. If any label
+ # name in a scrape is longer than this number post metric-relabeling, the
+ # entire scrape will be treated as failed. Note that label names are UTF-8
+ # encoded, and characters can take up to 4 bytes. 0 means no limit.
[ label_name_length_limit: | default = 0 ]
- # Per-scrape limit on length of labels value that will be accepted for a sample.
- # If a label value is longer than this number post metric-relabeling, the
- # entire scrape will be treated as failed. 0 means no limit.
+ # Limit on the length (in bytes) of each individual label value. If any label
+ # value in a scrape is longer than this number post metric-relabeling, the
+ # entire scrape will be treated as failed. Note that label values are UTF-8
+ # encoded, and characters can take up to 4 bytes. 0 means no limit.
[ label_value_length_limit: | default = 0 ]
- # Per-scrape config limit on number of unique targets that will be
+ # Limit per scrape config on number of unique targets that will be
# accepted. If more than this number of targets are present after target
# relabeling, Prometheus will mark the targets as failed without scraping them.
# 0 means no limit. This is an experimental feature, this behaviour could
# change in the future.
[ target_limit: | default = 0 ]
+ # Limit per scrape config on the number of targets dropped by relabeling
+ # that will be kept in memory. 0 means no limit.
+ [ keep_dropped_targets: | default = 0 ]
+
+ # Specifies the validation scheme for metric and label names. Either blank or
+ # "utf8" for for full UTF-8 support, or "legacy" for letters, numbers, colons,
+ # and underscores.
+ [ metric_name_validation_scheme | default "utf8" ]
+
+runtime:
+ # Configure the Go garbage collector GOGC parameter
+ # See: https://tip.golang.org/doc/gc-guide#GOGC
+ # Lowering this number increases CPU usage.
+ [ gogc: | default = 75 ]
+
# Rule files specifies a list of globs. Rules and alerts are read from
# all matching files.
rule_files:
@@ -131,6 +166,10 @@ alerting:
remote_write:
[ - ... ]
+# Settings related to the OTLP receiver feature.
+otlp:
+ [ promote_resource_attributes: [, ...] | default = [ ] ]
+
# Settings related to the remote read feature.
remote_read:
[ - ... ]
@@ -167,6 +206,11 @@ job_name:
# Per-scrape timeout when scraping this job.
[ scrape_timeout: | default = ]
+# The protocols to negotiate during a scrape with the client.
+# Supported values (case sensitive): PrometheusProto, OpenMetricsText0.0.1,
+# OpenMetricsText1.0.0, PrometheusText0.0.4.
+[ scrape_protocols: [, ...] | default = ]
+
# Whether to scrape a classic histogram that is also exposed as a native
# histogram (has no effect without --enable-feature=native-histograms).
[ scrape_classic_histograms: | default = false ]
@@ -206,6 +250,14 @@ job_name:
# by the target will be ignored.
[ honor_timestamps: | default = true ]
+# track_timestamps_staleness controls whether Prometheus tracks staleness of
+# the metrics that have an explicit timestamps present in scraped data.
+#
+# If track_timestamps_staleness is set to "true", a staleness marker will be
+# inserted in the TSDB when a metric is no longer present or the target
+# is down.
+[ track_timestamps_staleness: | default = false ]
+
# Configures the protocol scheme used for requests.
[ scheme: | default = http ]
@@ -213,11 +265,17 @@ job_name:
params:
[ : [, ...] ]
+# If enable_compression is set to "false", Prometheus will request uncompressed
+# response from the scraped target.
+[ enable_compression: | default = true ]
+
# Sets the `Authorization` header on every scrape request with the
# configured username and password.
+# username and username_file are mutually exclusive.
# password and password_file are mutually exclusive.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -260,6 +318,21 @@ tls_config:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
+# File to which scrape failures are logged.
+# Reloading the configuration will reopen the file.
+[ scrape_failure_log_file: ]
# List of Azure service discovery configurations.
azure_sd_configs:
@@ -388,37 +461,90 @@ metric_relabel_configs:
# change or be removed in the future.
[ body_size_limit: | default = 0 ]
-# Per-scrape limit on number of scraped samples that will be accepted.
+# Per-scrape limit on the number of scraped samples that will be accepted.
# If more than this number of samples are present after metric relabeling
# the entire scrape will be treated as failed. 0 means no limit.
[ sample_limit: | default = 0 ]
-# Per-scrape limit on number of labels that will be accepted for a sample. If
-# more than this number of labels are present post metric-relabeling, the
-# entire scrape will be treated as failed. 0 means no limit.
+# Limit on the number of labels that will be accepted per sample. If more
+# than this number of labels are present on any sample post metric-relabeling,
+# the entire scrape will be treated as failed. 0 means no limit.
[ label_limit: | default = 0 ]
-# Per-scrape limit on length of labels name that will be accepted for a sample.
-# If a label name is longer than this number post metric-relabeling, the entire
-# scrape will be treated as failed. 0 means no limit.
+# Limit on the length (in bytes) of each individual label name. If any label
+# name in a scrape is longer than this number post metric-relabeling, the
+# entire scrape will be treated as failed. Note that label names are UTF-8
+# encoded, and characters can take up to 4 bytes. 0 means no limit.
[ label_name_length_limit: | default = 0 ]
-# Per-scrape limit on length of labels value that will be accepted for a sample.
-# If a label value is longer than this number post metric-relabeling, the
-# entire scrape will be treated as failed. 0 means no limit.
+# Limit on the length (in bytes) of each individual label value. If any label
+# value in a scrape is longer than this number post metric-relabeling, the
+# entire scrape will be treated as failed. Note that label values are UTF-8
+# encoded, and characters can take up to 4 bytes. 0 means no limit.
[ label_value_length_limit: | default = 0 ]
-# Per-scrape config limit on number of unique targets that will be
+# Limit per scrape config on number of unique targets that will be
# accepted. If more than this number of targets are present after target
# relabeling, Prometheus will mark the targets as failed without scraping them.
# 0 means no limit. This is an experimental feature, this behaviour could
# change in the future.
[ target_limit: | default = 0 ]
+# Limit per scrape config on the number of targets dropped by relabeling
+# that will be kept in memory. 0 means no limit.
+[ keep_dropped_targets: | default = 0 ]
+
+# Specifies the validation scheme for metric and label names. Either blank or
+# "utf8" for full UTF-8 support, or "legacy" for letters, numbers, colons, and
+# underscores.
+[ metric_name_validation_scheme | default "utf8" ]
+
# Limit on total number of positive and negative buckets allowed in a single
-# native histogram. If this is exceeded, the entire scrape will be treated as
-# failed. 0 means no limit.
+# native histogram. The resolution of a histogram with more buckets will be
+# reduced until the number of buckets is within the limit. If the limit cannot
+# be reached, the scrape will fail.
+# 0 means no limit.
[ native_histogram_bucket_limit: | default = 0 ]
+
+# Lower limit for the growth factor of one bucket to the next in each native
+# histogram. The resolution of a histogram with a lower growth factor will be
+# reduced as much as possible until it is within the limit.
+# To set an upper limit for the schema (equivalent to "scale" in OTel's
+# exponential histograms), use the following factor limits:
+#
+# +----------------------------+----------------------------+
+# | growth factor | resulting schema AKA scale |
+# +----------------------------+----------------------------+
+# | 65536 | -4 |
+# +----------------------------+----------------------------+
+# | 256 | -3 |
+# +----------------------------+----------------------------+
+# | 16 | -2 |
+# +----------------------------+----------------------------+
+# | 4 | -1 |
+# +----------------------------+----------------------------+
+# | 2 | 0 |
+# +----------------------------+----------------------------+
+# | 1.4 | 1 |
+# +----------------------------+----------------------------+
+# | 1.1 | 2 |
+# +----------------------------+----------------------------+
+# | 1.09 | 3 |
+# +----------------------------+----------------------------+
+# | 1.04 | 4 |
+# +----------------------------+----------------------------+
+# | 1.02 | 5 |
+# +----------------------------+----------------------------+
+# | 1.01 | 6 |
+# +----------------------------+----------------------------+
+# | 1.005 | 7 |
+# +----------------------------+----------------------------+
+# | 1.002 | 8 |
+# +----------------------------+----------------------------+
+#
+# 0 results in the smallest supported factor (which is currently ~1.0027 or
+# schema 8, but might change in the future).
+[ native_histogram_min_bucket_factor: | default = 0 ]
```
Where `` must be unique across all scrape configurations.
@@ -499,6 +625,18 @@ tls_config:
# Specifies headers to send to proxies during CONNECT requests.
[ proxy_connect_header:
[ : [, ...] ] ]
+
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
```
### ``
@@ -528,8 +666,10 @@ See below for the configuration options for Azure discovery:
# The Azure environment.
[ environment: | default = AzurePublicCloud ]
-# The authentication method, either OAuth or ManagedIdentity.
+# The authentication method, either OAuth, ManagedIdentity or SDK.
# See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
+# SDK authentication method uses environment variables by default.
+# See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication
[ authentication_method: | default = OAuth]
# The subscription ID. Always required.
subscription_id:
@@ -553,11 +693,13 @@ subscription_id:
# Authentication information used to authenticate to the Azure API.
# Note that `basic_auth`, `authorization` and `oauth2` options are
# mutually exclusive.
+# `username` and `username_file` are mutually exclusive.
# `password` and `password_file` are mutually exclusive.
# Optional HTTP basic authentication information, currently not support by Azure.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -588,6 +730,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -666,11 +820,13 @@ tags:
# Authentication information used to authenticate to the consul server.
# Note that `basic_auth`, `authorization` and `oauth2` options are
# mutually exclusive.
+# `username` and `username_file` are mutually exclusive.
# `password` and `password_file` are mutually exclusive.
# Optional HTTP basic authentication information.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -701,6 +857,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -752,11 +920,13 @@ The following meta labels are available on targets during [relabeling](#relabel_
# Authentication information used to authenticate to the API server.
# Note that `basic_auth` and `authorization` options are
# mutually exclusive.
+# username and username_file are mutually exclusive.
# password and password_file are mutually exclusive.
# Optional HTTP basic authentication information, not currently supported by DigitalOcean.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -788,6 +958,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -816,12 +998,12 @@ Available meta labels:
* `__meta_docker_container_id`: the id of the container
* `__meta_docker_container_name`: the name of the container
* `__meta_docker_container_network_mode`: the network mode of the container
-* `__meta_docker_container_label_`: each label of the container
+* `__meta_docker_container_label_`: each label of the container, with any unsupported characters converted to an underscore
* `__meta_docker_network_id`: the ID of the network
* `__meta_docker_network_name`: the name of the network
* `__meta_docker_network_ingress`: whether the network is ingress
* `__meta_docker_network_internal`: whether the network is internal
-* `__meta_docker_network_label_`: each label of the network
+* `__meta_docker_network_label_`: each label of the network, with any unsupported characters converted to an underscore
* `__meta_docker_network_scope`: the scope of the network
* `__meta_docker_network_ip`: the IP of the container in this network
* `__meta_docker_port_private`: the port on the container
@@ -846,6 +1028,18 @@ host:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# TLS configuration.
tls_config:
[ ]
@@ -857,6 +1051,11 @@ tls_config:
# The host to use if the container is in host networking mode.
[ host_networking_host: | default = "localhost" ]
+# Sort all non-nil networks in ascending order based on network name and
+# get the first network if the container has multiple networks defined,
+# thus avoiding collecting duplicate targets.
+[ match_first_network: | default = true ]
+
# Optional filters to limit the discovery process to a subset of available
# resources.
# The available filters are listed in the upstream documentation:
@@ -871,11 +1070,13 @@ tls_config:
# Authentication information used to authenticate to the Docker daemon.
# Note that `basic_auth` and `authorization` options are
# mutually exclusive.
+# username and username_file are mutually exclusive.
# password and password_file are mutually exclusive.
# Optional HTTP basic authentication information.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -932,7 +1133,7 @@ Available meta labels:
* `__meta_dockerswarm_service_mode`: the mode of the service
* `__meta_dockerswarm_service_endpoint_port_name`: the name of the endpoint port, if available
* `__meta_dockerswarm_service_endpoint_port_publish_mode`: the publish mode of the endpoint port
-* `__meta_dockerswarm_service_label_`: each label of the service
+* `__meta_dockerswarm_service_label_`: each label of the service, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_service_task_container_hostname`: the container hostname of the target, if available
* `__meta_dockerswarm_service_task_container_image`: the container image of the target
* `__meta_dockerswarm_service_updating_status`: the status of the service, if available
@@ -940,7 +1141,7 @@ Available meta labels:
* `__meta_dockerswarm_network_name`: the name of the network
* `__meta_dockerswarm_network_ingress`: whether the network is ingress
* `__meta_dockerswarm_network_internal`: whether the network is internal
-* `__meta_dockerswarm_network_label_`: each label of the network
+* `__meta_dockerswarm_network_label_`: each label of the network, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_network_scope`: the scope of the network
#### `tasks`
@@ -952,7 +1153,7 @@ created using the `port` parameter defined in the SD configuration.
Available meta labels:
-* `__meta_dockerswarm_container_label_`: each label of the container
+* `__meta_dockerswarm_container_label_`: each label of the container, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_task_id`: the id of the task
* `__meta_dockerswarm_task_container_id`: the container id of the task
* `__meta_dockerswarm_task_desired_state`: the desired state of the task
@@ -962,19 +1163,19 @@ Available meta labels:
* `__meta_dockerswarm_service_id`: the id of the service
* `__meta_dockerswarm_service_name`: the name of the service
* `__meta_dockerswarm_service_mode`: the mode of the service
-* `__meta_dockerswarm_service_label_`: each label of the service
+* `__meta_dockerswarm_service_label_`: each label of the service, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_network_id`: the ID of the network
* `__meta_dockerswarm_network_name`: the name of the network
* `__meta_dockerswarm_network_ingress`: whether the network is ingress
* `__meta_dockerswarm_network_internal`: whether the network is internal
-* `__meta_dockerswarm_network_label_`: each label of the network
-* `__meta_dockerswarm_network_label`: each label of the network
+* `__meta_dockerswarm_network_label_`: each label of the network, with any unsupported characters converted to an underscore
+* `__meta_dockerswarm_network_label`: each label of the network, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_network_scope`: the scope of the network
* `__meta_dockerswarm_node_id`: the ID of the node
* `__meta_dockerswarm_node_hostname`: the hostname of the node
* `__meta_dockerswarm_node_address`: the address of the node
* `__meta_dockerswarm_node_availability`: the availability of the node
-* `__meta_dockerswarm_node_label_`: each label of the node
+* `__meta_dockerswarm_node_label_`: each label of the node, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_node_platform_architecture`: the architecture of the node
* `__meta_dockerswarm_node_platform_os`: the operating system of the node
* `__meta_dockerswarm_node_role`: the role of the node
@@ -994,7 +1195,7 @@ Available meta labels:
* `__meta_dockerswarm_node_engine_version`: the version of the node engine
* `__meta_dockerswarm_node_hostname`: the hostname of the node
* `__meta_dockerswarm_node_id`: the ID of the node
-* `__meta_dockerswarm_node_label_`: each label of the node
+* `__meta_dockerswarm_node_label_`: each label of the node, with any unsupported characters converted to an underscore
* `__meta_dockerswarm_node_manager_address`: the address of the manager component of the node
* `__meta_dockerswarm_node_manager_leader`: the leadership status of the manager component of the node (true or false)
* `__meta_dockerswarm_node_manager_reachability`: the reachability of the manager component of the node
@@ -1021,6 +1222,18 @@ host:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# TLS configuration.
tls_config:
[ ]
@@ -1048,11 +1261,13 @@ role:
# Authentication information used to authenticate to the Docker daemon.
# Note that `basic_auth` and `authorization` options are
# mutually exclusive.
+# username and username_file are mutually exclusive.
# password and password_file are mutually exclusive.
# Optional HTTP basic authentication information.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -1094,7 +1309,7 @@ A DNS-based service discovery configuration allows specifying a set of DNS
domain names which are periodically queried to discover a list of targets. The
DNS servers to be contacted are read from `/etc/resolv.conf`.
-This service discovery method only supports basic DNS A, AAAA, MX and SRV
+This service discovery method only supports basic DNS A, AAAA, MX, NS and SRV
record queries, but not the advanced DNS-SD approach specified in
[RFC6763](https://tools.ietf.org/html/rfc6763).
@@ -1104,13 +1319,14 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_dns_srv_record_target`: the target field of the SRV record
* `__meta_dns_srv_record_port`: the port field of the SRV record
* `__meta_dns_mx_record_target`: the target field of the MX record
+* `__meta_dns_ns_record_target`: the target field of the NS record
```yaml
# A list of DNS domain names to be queried.
names:
[ - ]
-# The type of DNS query to perform. One of SRV, A, AAAA or MX.
+# The type of DNS query to perform. One of SRV, A, AAAA, MX or NS.
[ type: | default = 'SRV' ]
# The port number used if the query type is not SRV.
@@ -1144,6 +1360,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_ec2_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present
* `__meta_ec2_owner_id`: the ID of the AWS account that owns the EC2 instance
* `__meta_ec2_platform`: the Operating System platform, set to 'windows' on Windows servers, absent otherwise
+* `__meta_ec2_primary_ipv6_addresses`: comma separated list of the Primary IPv6 addresses of the instance, if present. The list is ordered based on the position of each corresponding network interface in the attachment order.
* `__meta_ec2_primary_subnet_id`: the subnet ID of the primary network interface, if available
* `__meta_ec2_private_dns_name`: the private DNS name of the instance, if available
* `__meta_ec2_private_ip`: the private IP address of the instance, if present
@@ -1193,11 +1410,13 @@ filters:
# Authentication information used to authenticate to the EC2 API.
# Note that `basic_auth`, `authorization` and `oauth2` options are
# mutually exclusive.
+# `username` and `username_file` are mutually exclusive.
# `password` and `password_file` are mutually exclusive.
# Optional HTTP basic authentication information, currently not supported by AWS.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -1209,7 +1428,7 @@ authorization:
# `credentials_file`.
[ credentials: ]
# Sets the credentials to the credentials read from the configured file.
- # It is mutuall exclusive with `credentials`.
+ # It is mutually exclusive with `credentials`.
[ credentials_file: ]
# Optional OAuth 2.0 configuration, currently not supported by AWS.
@@ -1228,6 +1447,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -1274,7 +1505,7 @@ interface.
The following meta labels are available on targets during [relabeling](#relabel_config):
* `__meta_openstack_address_pool`: the pool of the private IP.
-* `__meta_openstack_instance_flavor`: the flavor of the OpenStack instance.
+* `__meta_openstack_instance_flavor`: the flavor name of the OpenStack instance, or the flavor ID if the flavor name isn't available.
* `__meta_openstack_instance_id`: the OpenStack instance ID.
* `__meta_openstack_instance_image`: the ID of the image the OpenStack instance is using.
* `__meta_openstack_instance_name`: the OpenStack instance name.
@@ -1282,7 +1513,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_openstack_private_ip`: the private IP of the OpenStack instance.
* `__meta_openstack_project_id`: the project (tenant) owning this instance.
* `__meta_openstack_public_ip`: the public IP of the OpenStack instance.
-* `__meta_openstack_tag_`: each tag value of the instance.
+* `__meta_openstack_tag_`: each metadata item of the instance, with any unsupported characters converted to an underscore.
* `__meta_openstack_user_id`: the user account owning the tenant.
See below for the configuration options for OpenStack discovery:
@@ -1392,6 +1623,7 @@ For OVHcloud's [public cloud instances](https://www.ovhcloud.com/en/public-cloud
* `__meta_ovhcloud_dedicated_server_ipv6`: the IPv6 of the server
* `__meta_ovhcloud_dedicated_server_link_speed`: the link speed of the server
* `__meta_ovhcloud_dedicated_server_name`: the name of the server
+* `__meta_ovhcloud_dedicated_server_no_intervention`: whether datacenter intervention is disabled for the server
* `__meta_ovhcloud_dedicated_server_os`: the operating system of the server
* `__meta_ovhcloud_dedicated_server_rack`: the rack of the server
* `__meta_ovhcloud_dedicated_server_reverse`: the reverse DNS name of the server
@@ -1473,6 +1705,7 @@ tls_config:
# Optional HTTP basic authentication information.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -1504,6 +1737,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -1522,7 +1767,16 @@ and serves as an interface to plug in custom service discovery mechanisms.
It reads a set of files containing a list of zero or more
``s. Changes to all defined files are detected via disk watches
-and applied immediately. Files may be provided in YAML or JSON format. Only
+and applied immediately.
+
+While those individual files are watched for changes,
+the parent directory is also watched implicitly. This is to handle [atomic
+renaming](https://github.com/fsnotify/fsnotify/blob/c1467c02fba575afdb5f4201072ab8403bbf00f4/README.md?plain=1#L128) efficiently and to detect new files that match the configured globs.
+This may cause issues if the parent directory contains a large number of other files,
+as each of these files will be watched too, even though the events related
+to them are not relevant.
+
+Files may be provided in YAML or JSON format. Only
changes resulting in well-formed target groups are applied.
Files must contain a list of static configs, using these formats:
@@ -1583,7 +1837,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_gce_instance_id`: the numeric id of the instance
* `__meta_gce_instance_name`: the name of the instance
-* `__meta_gce_label_`: each GCE label of the instance
+* `__meta_gce_label_`: each GCE label of the instance, with any unsupported characters converted to an underscore
* `__meta_gce_machine_type`: full or partial URL of the machine type of the instance
* `__meta_gce_metadata_`: each metadata item of the instance
* `__meta_gce_network`: the network URL of the instance
@@ -1667,8 +1921,8 @@ The labels below are only available for targets with `role` set to `hcloud`:
* `__meta_hetzner_hcloud_memory_size_gb`: the amount of memory of the server (in GB)
* `__meta_hetzner_hcloud_disk_size_gb`: the disk size of the server (in GB)
* `__meta_hetzner_hcloud_private_ipv4_`: the private ipv4 address of the server within a given network
-* `__meta_hetzner_hcloud_label_`: each label of the server
-* `__meta_hetzner_hcloud_labelpresent_`: `true` for each label of the server
+* `__meta_hetzner_hcloud_label_`: each label of the server, with any unsupported characters converted to an underscore
+* `__meta_hetzner_hcloud_labelpresent_`: `true` for each label of the server, with any unsupported characters converted to an underscore
The labels below are only available for targets with `role` set to `robot`:
@@ -1683,12 +1937,14 @@ role:
# Authentication information used to authenticate to the API server.
# Note that `basic_auth` and `authorization` options are
# mutually exclusive.
+# username and username_file are mutually exclusive.
# password and password_file are mutually exclusive.
# Optional HTTP basic authentication information, required when role is robot
# Role hcloud does not support basic auth.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -1721,6 +1977,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -1780,11 +2048,13 @@ url:
# Authentication information used to authenticate to the API server.
# Note that `basic_auth`, `authorization` and `oauth2` options are
# mutually exclusive.
+# `username` and `username_file` are mutually exclusive.
# `password` and `password_file` are mutually exclusive.
# Optional HTTP basic authentication information.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -1815,6 +2085,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -1860,12 +2142,14 @@ datacenter_id:
# Authentication information used to authenticate to the API server.
# Note that `basic_auth` and `authorization` options are
# mutually exclusive.
+# username and username_file are mutually exclusive.
# password and password_file are mutually exclusive.
# Optional HTTP basic authentication information, required when using IONOS
# Cloud username and password as authentication method.
basic_auth:
[ username: ]
+ [ username_file: ]
[ password: ]
[ password_file: ]
@@ -1898,6 +2182,18 @@ oauth2:
[ proxy_connect_header:
[ : [, ...] ] ]
+# Custom HTTP headers to be sent along with each request.
+# Headers that are set by Prometheus itself can't be overwritten.
+http_headers:
+ # Header name.
+ [ :
+ # Header values.
+ [ values: [, ...] ]
+ # Headers values. Hidden in configuration page.
+ [ secrets: [, ...] ]
+ # Files to read header values from.
+ [ files: [, ...] ] ]
+
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: | default = true ]
@@ -1935,8 +2231,8 @@ Available meta labels:
* `__meta_kubernetes_node_name`: The name of the node object.
* `__meta_kubernetes_node_provider_id`: The cloud provider's name for the node object.
-* `__meta_kubernetes_node_label_`: Each label from the node object.
-* `__meta_kubernetes_node_labelpresent_`: `true` for each label from the node object.
+* `__meta_kubernetes_node_label_`: Each label from the node object, with any unsupported characters converted to an underscore.
+* `__meta_kubernetes_node_labelpresent_`: `true` for each label from the node object, with any unsupported characters converted to an underscore.
* `__meta_kubernetes_node_annotation_`: Each annotation from the node object.
* `__meta_kubernetes_node_annotationpresent_