Repository: VictoriaMetrics/VictoriaMetrics
Pull request #818 (2020-10-07 17:46:30)

Adds dockerswarm sd


Patch:
diff --git a/README.md b/README.md
index 3365cc6f2..b32a5ad7f 100644
--- a/README.md
+++ b/README.md
@@ -295,6 +295,7 @@ Currently the following [scrape_config](https://prometheus.io/docs/prometheus/la
 * [consul_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config)
 * [dns_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config)
 * [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config)
+* [dockerswarm_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dockerswarm_sd_config)
 
 In the future other `*_sd_config` types will be supported.
 
diff --git a/app/vmagent/README.md b/app/vmagent/README.md
index 4d8b03f06..710b44f51 100644
--- a/app/vmagent/README.md
+++ b/app/vmagent/README.md
@@ -151,6 +151,8 @@ The following scrape types in [scrape_config](https://prometheus.io/docs/prometh
 * `openstack_sd_configs` - for scraping OpenStack targets.
   See [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config) for details.
   [OpenStack identity API v3](https://docs.openstack.org/api-ref/identity/v3/) is supported only.
+* `dockerswarm_sd_configs` - for scraping dockerswarm targets.
+  See [dockerswarm_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dockerswarm_sd_config) for details.
 
 File feature requests at [our issue tracker](https://github.com/VictoriaMetrics/VictoriaMetrics/issues) if you need other service discovery mechanisms to be supported by `vmagent`.
 
diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go
index 8a402692a..9595e7833 100644
--- a/lib/promscrape/config.go
+++ b/lib/promscrape/config.go
@@ -18,6 +18,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/dns"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/dockerswarm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/ec2"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/gce"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kubernetes"
@@ -72,6 +73,7 @@ type ScrapeConfig struct {
 	KubernetesSDConfigs  []kubernetes.SDConfig       `yaml:"kubernetes_sd_configs"`
 	OpenStackSDConfigs   []openstack.SDConfig        `yaml:"openstack_sd_configs"`
 	ConsulSDConfigs      []consul.SDConfig           `yaml:"consul_sd_configs"`
+	DockerSwarmConfigs   []dockerswarm.SDConfig      `yaml:"dockerswarm_sd_configs"`
 	DNSSDConfigs         []dns.SDConfig              `yaml:"dns_sd_configs"`
 	EC2SDConfigs         []ec2.SDConfig              `yaml:"ec2_sd_configs"`
 	GCESDConfigs         []gce.SDConfig              `yaml:"gce_sd_configs"`
@@ -231,6 +233,34 @@ func (cfg *Config) getOpenStackSDScrapeWork(prev []ScrapeWork) []ScrapeWork {
 	return dst
 }
 
+// getDockerSwarmSDScrapeWork returns `dockerswarm_sd_configs` ScrapeWork from cfg.
+func (cfg *Config) getDockerSwarmSDScrapeWork(prev []ScrapeWork) []ScrapeWork {
+	swsPrevByJob := getSWSByJob(prev)
+	var dst []ScrapeWork
+	for i := range cfg.ScrapeConfigs {
+		sc := &cfg.ScrapeConfigs[i]
+		dstLen := len(dst)
+		ok := true
+		for j := range sc.DockerSwarmConfigs {
+			sdc := &sc.DockerSwarmConfigs[j]
+			var okLocal bool
+			dst, okLocal = appendDockerSwarmScrapeWork(dst, sdc, cfg.baseDir, sc.swc)
+			if ok {
+				ok = okLocal
+			}
+		}
+		if ok {
+			continue
+		}
+		swsPrev := swsPrevByJob[sc.swc.jobName]
+		if len(swsPrev) > 0 {
+			logger.Errorf("there were errors when discovering dockerswarm targets for job %q, so preserving the previous targets", sc.swc.jobName)
+			dst = append(dst[:dstLen], swsPrev...)
+		}
+	}
+	return dst
+}
+
 // getConsulSDScrapeWork returns `consul_sd_configs` ScrapeWork from cfg.
 func (cfg *Config) getConsulSDScrapeWork(prev []ScrapeWork) []ScrapeWork {
 	swsPrevByJob := getSWSByJob(prev)
@@ -483,6 +513,15 @@ func appendOpenstackScrapeWork(dst []ScrapeWork, sdc *openstack.SDConfig, baseDi
 	return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "openstack_sd_config"), true
 }
 
+func appendDockerSwarmScrapeWork(dst []ScrapeWork, sdc *dockerswarm.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]ScrapeWork, bool) {
+	targetLabels, err := dockerswarm.GetLabels(sdc, baseDir)
+	if err != nil {
+		logger.Errorf("error when discovering dockerswarm targets for `job_name` %q: %s; skipping it", swc.jobName, err)
+		return dst, false
+	}
+	return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "dockerswarm_sd_config"), true
+}
+
 func appendConsulScrapeWork(dst []ScrapeWork, sdc *consul.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]ScrapeWork, bool) {
 	targetLabels, err := consul.GetLabels(sdc, baseDir)
 	if err != nil {
diff --git a/lib/promscrape/discovery/dockerswarm/api.go b/lib/promscrape/discovery/dockerswarm/api.go
new file mode 100644
index 000000000..4e8b591d5
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/api.go
@@ -0,0 +1,39 @@
+package dockerswarm
+
+import (
+	"fmt"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+var configMap = discoveryutils.NewConfigMap()
+
+type apiConfig struct {
+	client *discoveryutils.Client
+	port   int
+}
+
+func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
+	v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc, baseDir) })
+	if err != nil {
+		return nil, err
+	}
+	return v.(*apiConfig), nil
+}
+
+func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
+	cfg := &apiConfig{
+		port: sdc.Port,
+	}
+	config, err := promauth.NewConfig(baseDir, sdc.BasicAuth, sdc.BearerToken, sdc.BearerTokenFile, sdc.TLSConfig)
+	if err != nil {
+		return nil, err
+	}
+	client, err := discoveryutils.NewClient(sdc.Host, config)
+	if err != nil {
+		return nil, fmt.Errorf("cannot create HTTP client for %q: %w", sdc.Host, err)
+	}
+	cfg.client = client
+	return cfg, nil
+}
diff --git a/lib/promscrape/discovery/dockerswarm/dockerswarm.go b/lib/promscrape/discovery/dockerswarm/dockerswarm.go
new file mode 100644
index 000000000..1b172415c
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/dockerswarm.go
@@ -0,0 +1,51 @@
+package dockerswarm
+
+import (
+	"fmt"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
+)
+
+// SDConfig represents docker swarm service discovery configuration
+//
+// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dockerswarm_sd_config
+type SDConfig struct {
+	Host            string                    `yaml:"host"`
+	Role            string                    `yaml:"role"`
+	Port            int                       `yaml:"port"`
+	TLSConfig       *promauth.TLSConfig       `yaml:"tls_config"`
+	BasicAuth       *promauth.BasicAuthConfig `yaml:"basic_auth"`
+	BearerToken     string                    `yaml:"bearer_token"`
+	BearerTokenFile string                    `yaml:"bearer_token_file"`
+}
+
+// joinLabels adds labels to destination from source with given key from destination matching given value.
+func joinLabels(source []map[string]string, destination map[string]string, key, value string) map[string]string {
+	for _, sourceLabels := range source {
+		if sourceLabels[key] == value {
+			for k, v := range sourceLabels {
+				destination[k] = v
+			}
+			return destination
+		}
+	}
+	return destination
+}
+
+// GetLabels returns gce labels according to sdc.
+func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) {
+	cfg, err := getAPIConfig(sdc, baseDir)
+	if err != nil {
+		return nil, fmt.Errorf("cannot get API config: %w", err)
+	}
+	switch sdc.Role {
+	case "tasks":
+		return getTasksLabels(cfg)
+	case "services":
+		return getServicesLabels(cfg)
+	case "nodes":
+		return getNodesLabels(cfg)
+	default:
+		return nil, fmt.Errorf("unexpected `role`: %q; must be one of `tasks`, `services` or `nodes`; skipping it", sdc.Role)
+	}
+}
diff --git a/lib/promscrape/discovery/dockerswarm/network.go b/lib/promscrape/discovery/dockerswarm/network.go
new file mode 100644
index 000000000..3200d9ee8
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/network.go
@@ -0,0 +1,61 @@
+package dockerswarm
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+// See https://docs.docker.com/engine/api/v1.40/#tag/Network
+type network struct {
+	ID       string
+	Name     string
+	Scope    string
+	Internal bool
+	Ingress  bool
+	Labels   map[string]string
+}
+
+func getNetworksLabels(cfg *apiConfig) ([]map[string]string, error) {
+	networks, err := getNetworks(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return addNetworkLabels(networks), nil
+}
+
+func getNetworks(cfg *apiConfig) ([]network, error) {
+	resp, err := cfg.client.GetAPIResponse("/networks")
+	if err != nil {
+		return nil, fmt.Errorf("cannot query dockerswarm api for networks: %w", err)
+	}
+	return parseNetworks(resp)
+}
+
+func parseNetworks(data []byte) ([]network, error) {
+	var networks []network
+	if err := json.Unmarshal(data, &networks); err != nil {
+		return nil, fmt.Errorf("cannot parse networks: %w", err)
+	}
+	return networks, nil
+}
+
+func addNetworkLabels(networks []network) []map[string]string {
+	var ms []map[string]string
+	for _, network := range networks {
+		m := map[string]string{
+			"__meta_dockerswarm_network_id":       network.ID,
+			"__meta_dockerswarm_network_name":     network.Name,
+			"__meta_dockerswarm_network_scope":    network.Scope,
+			"__meta_dockerswarm_network_internal": strconv.FormatBool(network.Internal),
+			"__meta_dockerswarm_network_ingress":  strconv.FormatBool(network.Ingress),
+		}
+		for k, v := range network.Labels {
+			m["__meta_dockerswarm_network_label_"+discoveryutils.SanitizeLabelName(k)] = v
+		}
+		ms = append(ms, m)
+	}
+	return ms
+}
diff --git a/lib/promscrape/discovery/dockerswarm/nodes.go b/lib/promscrape/discovery/dockerswarm/nodes.go
new file mode 100644
index 000000000..0474956a4
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/nodes.go
@@ -0,0 +1,90 @@
+package dockerswarm
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+// See https://docs.docker.com/engine/api/v1.40/#tag/Node
+type node struct {
+	ID   string
+	Spec struct {
+		Labels       map[string]string
+		Role         string
+		Availability string
+	}
+	Description struct {
+		Hostname string
+		Platform struct {
+			Architecture string
+			OS           string
+		}
+		Engine struct {
+			EngineVersion string
+		}
+	}
+	Status struct {
+		State   string
+		Message string
+		Addr    string
+	}
+	ManagerStatus *struct {
+		Leader       bool
+		Reachability string
+		Addr         string
+	}
+}
+
+func getNodesLabels(cfg *apiConfig) ([]map[string]string, error) {
+	nodes, err := getNodes(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return addNodeLabels(nodes, cfg.port), nil
+}
+
+func getNodes(cfg *apiConfig) ([]node, error) {
+	resp, err := cfg.client.GetAPIResponse("/nodes")
+	if err != nil {
+		return nil, fmt.Errorf("cannot query dockerswarm api for nodes: %w", err)
+	}
+	return parseNodes(resp)
+}
+
+func parseNodes(data []byte) ([]node, error) {
+	var nodes []node
+	if err := json.Unmarshal(data, &nodes); err != nil {
+		return nil, fmt.Errorf("cannot parse nodes: %w", err)
+	}
+	return nodes, nil
+}
+
+func addNodeLabels(nodes []node, port int) []map[string]string {
+	var ms []map[string]string
+	for _, node := range nodes {
+		m := map[string]string{
+			"__address__":                                   discoveryutils.JoinHostPort(node.Status.Addr, port),
+			"__meta_dockerswarm_node_id":                    node.ID,
+			"__meta_dockerswarm_node_address":               node.Status.Addr,
+			"__meta_dockerswarm_node_availability":          node.Spec.Availability,
+			"__meta_dockerswarm_node_engine_version":        node.Description.Engine.EngineVersion,
+			"__meta_dockerswarm_node_hostname":              node.Description.Hostname,
+			"__meta_dockerswarm_node_platform_architecture": node.Description.Platform.Architecture,
+			"__meta_dockerswarm_node_platform_os":           node.Description.Platform.OS,
+			"__meta_dockerswarm_node_role":                  node.Spec.Role,
+			"__meta_dockerswarm_node_status":                node.Status.State,
+		}
+		if node.ManagerStatus != nil {
+			m["__meta_dockerswarm_node_manager_address"] = node.ManagerStatus.Addr
+			m["__meta_dockerswarm_node_manager_manager_reachability"] = node.ManagerStatus.Reachability
+			m["__meta_dockerswarm_node_manager_leader"] = fmt.Sprintf("%t", node.ManagerStatus.Leader)
+		}
+		for k, v := range node.Spec.Labels {
+			m["__meta_dockerswarm_node_label_"+discoveryutils.SanitizeLabelName(k)] = v
+		}
+		ms = append(ms, m)
+	}
+	return ms
+}
diff --git a/lib/promscrape/discovery/dockerswarm/services.go b/lib/promscrape/discovery/dockerswarm/services.go
new file mode 100644
index 000000000..708b4d2ec
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/services.go
@@ -0,0 +1,139 @@
+package dockerswarm
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+// https://docs.docker.com/engine/api/v1.40/#tag/Service
+type service struct {
+	ID   string
+	Spec struct {
+		Labels       map[string]string
+		Name         string
+		TaskTemplate struct {
+			ContainerSpec struct {
+				Hostname string
+				Image    string
+			}
+		}
+		Mode struct {
+			Global     interface{}
+			Replicated interface{}
+		}
+	}
+	UpdateStatus *struct {
+		State string
+	}
+	Endpoint struct {
+		Ports      []portConfig
+		VirtualIPs []struct {
+			NetworkID string
+			Addr      string
+		}
+	}
+}
+
+type portConfig struct {
+	Protocol      string
+	Name          string
+	PublishMode   string
+	PublishedPort int
+}
+
+func getServicesLabels(cfg *apiConfig) ([]map[string]string, error) {
+	services, err := getServices(cfg)
+	if err != nil {
+		return nil, err
+	}
+	networksLabels, err := getNetworksLabels(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return addServicesLabels(services, networksLabels, cfg.port), nil
+}
+
+func getServices(cfg *apiConfig) ([]service, error) {
+	data, err := cfg.client.GetAPIResponse("/services")
+	if err != nil {
+		return nil, fmt.Errorf("cannot query dockerswarm api for services: %w", err)
+	}
+	return parseServicesResponse(data)
+}
+
+func parseServicesResponse(data []byte) ([]service, error) {
+	var services []service
+	if err := json.Unmarshal(data, &services); err != nil {
+		return nil, fmt.Errorf("cannot parse services: %w", err)
+	}
+	return services, nil
+}
+
+func getServiceMode(svc service) string {
+	if svc.Spec.Mode.Global != nil {
+		return "global"
+	}
+	if svc.Spec.Mode.Replicated != nil {
+		return "replicated"
+	}
+	return ""
+}
+
+func addServicesLabels(services []service, networksLabels []map[string]string, port int) []map[string]string {
+	var ms []map[string]string
+	for _, service := range services {
+		m := map[string]string{
+			"__meta_dockerswarm_service_id":                      service.ID,
+			"__meta_dockerswarm_service_name":                    service.Spec.Name,
+			"__meta_dockerswarm_service_task_container_hostname": service.Spec.TaskTemplate.ContainerSpec.Hostname,
+			"__meta_dockerswarm_service_task_container_image":    service.Spec.TaskTemplate.ContainerSpec.Image,
+			"__meta_dockerswarm_service_mode":                    getServiceMode(service),
+		}
+		if service.UpdateStatus != nil {
+			m["__meta_dockerswarm_service_updating_status"] = service.UpdateStatus.State
+		}
+		for k, v := range service.Spec.Labels {
+			m["__meta_dockerswarm_service_label_"+discoveryutils.SanitizeLabelName(k)] = v
+		}
+		for _, vip := range service.Endpoint.VirtualIPs {
+			var added bool
+			ip, _, err := net.ParseCIDR(vip.Addr)
+			if err != nil {
+				logger.Errorf("cannot parse: %q as cidr for service label add, err: %v", vip.Addr, err)
+				continue
+			}
+			for _, ep := range service.Endpoint.Ports {
+				if ep.Protocol != "tcp" {
+					continue
+				}
+				lbls := map[string]string{
+					"__meta_dockerswarm_service_endpoint_port_name":         ep.Name,
+					"__meta_dockerswarm_service_endpoint_port_publish_mode": ep.PublishMode,
+					"__address__": discoveryutils.JoinHostPort(ip.String(), ep.PublishedPort),
+				}
+				for k, v := range m {
+					lbls[k] = v
+				}
+				lbls = joinLabels(networksLabels, lbls, "__meta_dockerswarm_network_id", vip.NetworkID)
+				added = true
+				ms = append(ms, lbls)
+			}
+			if !added {
+				lbls := make(map[string]string, len(m))
+				for k, v := range m {
+					lbls[k] = v
+				}
+				lbls = joinLabels(networksLabels, lbls, "__meta_dockerswarm_network_id", vip.NetworkID)
+				lbls["__address__"] = discoveryutils.JoinHostPort(ip.String(), port)
+				ms = append(ms, lbls)
+			}
+		}
+
+	}
+	return ms
+}
diff --git a/lib/promscrape/discovery/dockerswarm/tasks.go b/lib/promscrape/discovery/dockerswarm/tasks.go
new file mode 100644
index 000000000..cd0821d90
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/tasks.go
@@ -0,0 +1,149 @@
+package dockerswarm
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+	"strconv"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+// See https://docs.docker.com/engine/api/v1.40/#tag/Task
+type task struct {
+	ID                  string
+	ServiceID           string
+	NodeID              string
+	Labels              map[string]string
+	DesiredState        string
+	NetworksAttachments []struct {
+		Addresses []string
+		Network   struct {
+			ID string
+		}
+	}
+	Status struct {
+		State           string
+		ContainerStatus *struct {
+			ContainerID string
+		}
+		PortStatus struct {
+			Ports []portConfig
+		}
+	}
+	Slot int
+}
+
+func getTasksLabels(cfg *apiConfig) ([]map[string]string, error) {
+	tasks, err := getTasks(cfg)
+	if err != nil {
+		return nil, err
+	}
+	services, err := getServices(cfg)
+	if err != nil {
+		return nil, err
+	}
+	networkLabels, err := getNetworksLabels(cfg)
+	if err != nil {
+		return nil, err
+	}
+	svcLabels := addServicesLabels(services, networkLabels, cfg.port)
+	nodeLabels, err := getNodesLabels(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return addTasksLabels(tasks, nodeLabels, svcLabels, networkLabels, services, cfg.port), nil
+}
+
+func getTasks(cfg *apiConfig) ([]task, error) {
+	resp, err := cfg.client.GetAPIResponse("/tasks")
+	if err != nil {
+		return nil, fmt.Errorf("cannot query dockerswarm api for tasks: %w", err)
+	}
+	return parseTasks(resp)
+}
+
+func parseTasks(data []byte) ([]task, error) {
+	var tasks []task
+	if err := json.Unmarshal(data, &tasks); err != nil {
+		return nil, fmt.Errorf("cannot parse tasks: %w", err)
+	}
+	return tasks, nil
+}
+
+func addTasksLabels(tasks []task, nodesLabels, servicesLabels, networksLabels []map[string]string, services []service, port int) []map[string]string {
+	var ms []map[string]string
+	for _, task := range tasks {
+		m := map[string]string{
+			"__meta_dockerswarm_task_id":            task.ID,
+			"__meta_dockerswarm_task_desired_state": task.DesiredState,
+			"__meta_dockerswarm_task_state":         task.Status.State,
+			"__meta_dockerswarm_task_slot":          strconv.Itoa(task.Slot),
+		}
+		if task.Status.ContainerStatus != nil {
+			m["__meta_dockerswarm_task_container_id"] = task.Status.ContainerStatus.ContainerID
+		}
+		for k, v := range task.Labels {
+			m["__meta_dockerswarm_task_label_"+discoveryutils.SanitizeLabelName(k)] = v
+		}
+		var svcPorts []portConfig
+		for i, v := range services {
+			if v.ID == task.ServiceID {
+				svcPorts = services[i].Endpoint.Ports
+				break
+			}
+		}
+		m = joinLabels(servicesLabels, m, "__meta_dockerswarm_service_id", task.ServiceID)
+		m = joinLabels(nodesLabels, m, "__meta_dockerswarm_node_id", task.NodeID)
+
+		for _, port := range task.Status.PortStatus.Ports {
+			if port.Protocol != "tcp" {
+				continue
+			}
+			lbls := make(map[string]string, len(m))
+			lbls["__meta_dockerswarm_task_port_publish_mode"] = port.PublishMode
+			lbls["__address__"] = discoveryutils.JoinHostPort(m["__meta_dockerswarm_node_address"], port.PublishedPort)
+			for k, v := range m {
+				lbls[k] = v
+			}
+			ms = append(ms, lbls)
+		}
+		for _, na := range task.NetworksAttachments {
+			for _, address := range na.Addresses {
+				ip, _, err := net.ParseCIDR(address)
+				if err != nil {
+					logger.Errorf("cannot parse task network attachments address: %s as net CIDR: %v", address, err)
+					continue
+				}
+				var added bool
+				for _, v := range svcPorts {
+					if v.Protocol != "tcp" {
+						continue
+					}
+					lbls := make(map[string]string, len(m))
+					for k, v := range m {
+						lbls[k] = v
+					}
+					lbls = joinLabels(networksLabels, lbls, "__meta_dockerswarm_network_id", na.Network.ID)
+					lbls["__address"] = discoveryutils.JoinHostPort(ip.String(), v.PublishedPort)
+					lbls["__meta_dockerswarm_task_port_publish_mode"] = v.PublishMode
+					ms = append(ms, lbls)
+					added = true
+				}
+
+				if !added {
+					lbls := make(map[string]string, len(m))
+					for k, v := range m {
+						lbls[k] = v
+					}
+					lbls = joinLabels(networksLabels, lbls, "__meta_dockerswarm_network_id", na.Network.ID)
+					lbls["__address__"] = discoveryutils.JoinHostPort(ip.String(), port)
+					ms = append(ms, lbls)
+				}
+			}
+		}
+	}
+	return ms
+}
diff --git a/lib/promscrape/discoveryutils/client.go b/lib/promscrape/discoveryutils/client.go
index f0bc4b592..014edced4 100644
--- a/lib/promscrape/discoveryutils/client.go
+++ b/lib/promscrape/discoveryutils/client.go
@@ -41,11 +41,23 @@ type Client struct {
 
 // NewClient returns new Client for the given apiServer and the given ac.
 func NewClient(apiServer string, ac *promauth.Config) (*Client, error) {
-	var u fasthttp.URI
+	var (
+		dialFunc fasthttp.DialFunc
+		tlsCfg   *tls.Config
+		u        fasthttp.URI
+	)
 	u.Update(apiServer)
+
+	// special case for unix socket connection
+	if string(u.Scheme()) == "unix" {
+		dialAddr := string(u.Path())
+		apiServer = "http://"
+		dialFunc = func(_ string) (net.Conn, error) {
+			return net.Dial("unix", dialAddr)
+		}
+	}
 	hostPort := string(u.Host())
 	isTLS := string(u.Scheme()) == "https"
-	var tlsCfg *tls.Config
 	if isTLS && ac != nil {
 		tlsCfg = ac.NewTLSConfig()
 	}
@@ -66,6 +78,7 @@ func NewClient(apiServer string, ac *promauth.Config) (*Client, error) {
 		WriteTimeout:        10 * time.Second,
 		MaxResponseBodySize: 300 * 1024 * 1024,
 		MaxConns:            2 * *maxConcurrency,
+		Dial:                dialFunc,
 	}
 	return &Client{
 		hc:        hc,
diff --git a/lib/promscrape/scraper.go b/lib/promscrape/scraper.go
index cab51cafd..bd611c13e 100644
--- a/lib/promscrape/scraper.go
+++ b/lib/promscrape/scraper.go
@@ -36,9 +36,11 @@ var (
 	gceSDCheckInterval = flag.Duration("promscrape.gceSDCheckInterval", time.Minute, "Interval for checking for changes in gce. "+
 		"This works only if `gce_sd_configs` is configured in '-promscrape.config' file. "+
 		"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config for details")
+	dockerswarmSDCheckInterval = flag.Duration("promscrape.dockerswarmSDCheckInterval", 30*time.Second, "Interval for checking for changes in dockerswarm. "+
+		"This works only if `dockerswarm_sd_configs` is configured in '-promscrape.config' file. "+
+		"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dockerswarm_sd_config for details")
 	promscrapeConfigFile = flag.String("promscrape.config", "", "Optional path to Prometheus config file with 'scrape_configs' section containing targets to scrape. "+
 		"See https://victoriametrics.github.io/#how-to-scrape-prometheus-exporters-such-as-node-exporter for details")
-
 	suppressDuplicateScrapeTargetErrors = flag.Bool("promscrape.suppressDuplicateScrapeTargetErrors", false, "Whether to suppress `duplicate scrape target` errors; "+
 		"see https://victoriametrics.github.io/vmagent.html#troubleshooting for details")
 )
@@ -96,6 +98,7 @@ func runScraper(configFile string, pushData func(wr *prompbmarshal.WriteRequest)
 	scs.add("dns_sd_configs", *dnsSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getDNSSDScrapeWork(swsPrev) })
 	scs.add("ec2_sd_configs", *ec2SDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getEC2SDScrapeWork(swsPrev) })
 	scs.add("gce_sd_configs", *gceSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getGCESDScrapeWork(swsPrev) })
+	scs.add("dockerswarm_sd_configs", *dockerswarmSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getDockerSwarmSDScrapeWork(swsPrev) })
 
 	sighupCh := procutil.NewSighupChan()
 


Test patch:
diff --git a/lib/promscrape/discovery/dockerswarm/network_test.go b/lib/promscrape/discovery/dockerswarm/network_test.go
new file mode 100644
index 000000000..7bcbce020
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/network_test.go
@@ -0,0 +1,166 @@
+package dockerswarm
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+func Test_addNetworkLabels(t *testing.T) {
+	type args struct {
+		networks []network
+	}
+	tests := []struct {
+		name string
+		args args
+		want [][]prompbmarshal.Label
+	}{
+		{
+			name: "ingress network",
+			args: args{
+				networks: []network{
+					{
+						ID:      "qs0hog6ldlei9ct11pr3c77v1",
+						Ingress: true,
+						Scope:   "swarm",
+						Name:    "ingress",
+						Labels: map[string]string{
+							"key1": "value1",
+						},
+					},
+				},
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__meta_dockerswarm_network_id":         "qs0hog6ldlei9ct11pr3c77v1",
+					"__meta_dockerswarm_network_ingress":    "true",
+					"__meta_dockerswarm_network_internal":   "false",
+					"__meta_dockerswarm_network_label_key1": "value1",
+					"__meta_dockerswarm_network_name":       "ingress",
+					"__meta_dockerswarm_network_scope":      "swarm",
+				})},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := addNetworkLabels(tt.args.networks)
+			var sortedLabelss [][]prompbmarshal.Label
+			for _, labels := range got {
+				sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
+			}
+			if !reflect.DeepEqual(sortedLabelss, tt.want) {
+				t.Errorf("addNetworkLabels() \ngot %v, \nwant %v", sortedLabelss, tt.want)
+			}
+		})
+	}
+}
+
+func Test_parseNetworks(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []network
+		wantErr bool
+	}{
+		{
+			name: "parse two networks",
+			args: args{
+				data: []byte(`[
+  {
+    "Name": "ingress",
+    "Id": "qs0hog6ldlei9ct11pr3c77v1",
+    "Created": "2020-10-06T08:39:58.957083331Z",
+    "Scope": "swarm",
+    "Driver": "overlay",
+    "EnableIPv6": false,
+    "IPAM": {
+      "Driver": "default",
+      "Options": null,
+      "Config": [
+        {
+          "Subnet": "10.0.0.0/24",
+          "Gateway": "10.0.0.1"
+        }
+      ]
+    },
+    "Internal": false,
+    "Attachable": false,
+    "Ingress": true,
+    "ConfigFrom": {
+      "Network": ""
+    },
+    "ConfigOnly": false,
+    "Containers": null,
+    "Options": {
+      "com.docker.network.driver.overlay.vxlanid_list": "4096"
+    },
+    "Labels": {
+      "key1": "value1"
+    }
+  },
+  {
+    "Name": "host",
+    "Id": "317f0384d7e5f5c26304a0b04599f9f54bc08def4d0535059ece89955e9c4b7b",
+    "Created": "2020-10-06T08:39:52.843373136Z",
+    "Scope": "local",
+    "Driver": "host",
+    "EnableIPv6": false,
+    "IPAM": {
+      "Driver": "default",
+      "Options": null,
+      "Config": []
+    },
+    "Internal": false,
+    "Attachable": false,
+    "Ingress": false,
+    "ConfigFrom": {
+      "Network": ""
+    },
+    "ConfigOnly": false,
+    "Containers": {},
+    "Options": {},
+    "Labels": {
+      "key": "value"
+    }
+  }
+]`),
+			},
+			want: []network{
+				{
+					ID:      "qs0hog6ldlei9ct11pr3c77v1",
+					Ingress: true,
+					Scope:   "swarm",
+					Name:    "ingress",
+					Labels: map[string]string{
+						"key1": "value1",
+					},
+				},
+				{
+					ID:    "317f0384d7e5f5c26304a0b04599f9f54bc08def4d0535059ece89955e9c4b7b",
+					Scope: "local",
+					Name:  "host",
+					Labels: map[string]string{
+						"key": "value",
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseNetworks(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseNetworks() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseNetworks() \ngot  %v, \nwant %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/lib/promscrape/discovery/dockerswarm/nodes_test.go b/lib/promscrape/discovery/dockerswarm/nodes_test.go
new file mode 100644
index 000000000..6d5c63629
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/nodes_test.go
@@ -0,0 +1,185 @@
+package dockerswarm
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+func Test_parseNodes(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []node
+		wantErr bool
+	}{
+		{
+			name: "parse ok",
+			args: args{
+				data: []byte(`[
+  {
+    "ID": "qauwmifceyvqs0sipvzu8oslu",
+    "Version": {
+      "Index": 16
+    },
+    "Spec": {
+      "Role": "manager",
+      "Availability": "active"
+    },
+    "Description": {
+      "Hostname": "ip-172-31-40-97",
+      "Platform": {
+        "Architecture": "x86_64",
+        "OS": "linux"
+      },
+      "Resources": {
+        "NanoCPUs": 1000000000,
+        "MemoryBytes": 1026158592
+      },
+      "Engine": {
+        "EngineVersion": "19.03.11"
+      }
+    },
+    "Status": {
+      "State": "ready",
+      "Addr": "172.31.40.97"
+    }
+  }
+]
+`),
+			},
+			want: []node{
+				{
+					ID: "qauwmifceyvqs0sipvzu8oslu",
+					Spec: struct {
+						Labels       map[string]string
+						Role         string
+						Availability string
+					}{Role: "manager", Availability: "active"},
+					Status: struct {
+						State   string
+						Message string
+						Addr    string
+					}{State: "ready", Addr: "172.31.40.97"},
+					Description: struct {
+						Hostname string
+						Platform struct {
+							Architecture string
+							OS           string
+						}
+						Engine struct{ EngineVersion string }
+					}{
+						Hostname: "ip-172-31-40-97",
+						Platform: struct {
+							Architecture string
+							OS           string
+						}{
+							Architecture: "x86_64",
+							OS:           "linux",
+						},
+						Engine: struct{ EngineVersion string }{
+							EngineVersion: "19.03.11",
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseNodes(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseNodes() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseNodes() \ngot  %v, \nwant %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_addNodeLabels(t *testing.T) {
+	type args struct {
+		nodes []node
+		port  int
+	}
+	tests := []struct {
+		name string
+		args args
+		want [][]prompbmarshal.Label
+	}{
+		{
+			name: "add labels to one node",
+			args: args{
+				nodes: []node{
+					{
+						ID: "qauwmifceyvqs0sipvzu8oslu",
+						Spec: struct {
+							Labels       map[string]string
+							Role         string
+							Availability string
+						}{Role: "manager", Availability: "active"},
+						Status: struct {
+							State   string
+							Message string
+							Addr    string
+						}{State: "ready", Addr: "172.31.40.97"},
+						Description: struct {
+							Hostname string
+							Platform struct {
+								Architecture string
+								OS           string
+							}
+							Engine struct{ EngineVersion string }
+						}{
+							Hostname: "ip-172-31-40-97",
+							Platform: struct {
+								Architecture string
+								OS           string
+							}{
+								Architecture: "x86_64",
+								OS:           "linux",
+							},
+							Engine: struct{ EngineVersion string }{
+								EngineVersion: "19.03.11",
+							},
+						},
+					},
+				},
+				port: 9100,
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address__":                                   "172.31.40.97:9100",
+					"__meta_dockerswarm_node_address":               "172.31.40.97",
+					"__meta_dockerswarm_node_availability":          "active",
+					"__meta_dockerswarm_node_engine_version":        "19.03.11",
+					"__meta_dockerswarm_node_hostname":              "ip-172-31-40-97",
+					"__meta_dockerswarm_node_id":                    "qauwmifceyvqs0sipvzu8oslu",
+					"__meta_dockerswarm_node_platform_architecture": "x86_64",
+					"__meta_dockerswarm_node_platform_os":           "linux",
+					"__meta_dockerswarm_node_role":                  "manager",
+					"__meta_dockerswarm_node_status":                "ready",
+				})},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := addNodeLabels(tt.args.nodes, tt.args.port)
+
+			var sortedLabelss [][]prompbmarshal.Label
+			for _, labels := range got {
+				sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
+			}
+			if !reflect.DeepEqual(sortedLabelss, tt.want) {
+				t.Errorf("addNodeLabels() \ngot  %v, \nwant %v", sortedLabelss, tt.want)
+			}
+		})
+	}
+}
diff --git a/lib/promscrape/discovery/dockerswarm/services_test.go b/lib/promscrape/discovery/dockerswarm/services_test.go
new file mode 100644
index 000000000..615b21af2
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/services_test.go
@@ -0,0 +1,293 @@
+package dockerswarm
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+func Test_parseServicesResponse(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []service
+		wantErr bool
+	}{
+		{
+			name: "parse ok",
+			args: args{
+				data: []byte(`[
+  {
+    "ID": "tgsci5gd31aai3jyudv98pqxf",
+    "Version": {
+      "Index": 25
+    },
+    "CreatedAt": "2020-10-06T11:17:31.948808444Z",
+    "UpdatedAt": "2020-10-06T11:17:31.950195138Z",
+    "Spec": {
+      "Name": "redis2",
+      "Labels": {},
+      "TaskTemplate": {
+        "ContainerSpec": {
+          "Image": "redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842",
+          "Init": false,
+          "DNSConfig": {},
+          "Isolation": "default"
+        },
+        "Resources": {
+          "Limits": {},
+          "Reservations": {}
+        }
+      },
+      "Mode": {
+        "Replicated": {}
+      },
+      "EndpointSpec": {
+        "Mode": "vip",
+        "Ports": [
+          {
+            "Protocol": "tcp",
+            "TargetPort": 6379,
+            "PublishedPort": 8081,
+            "PublishMode": "ingress"
+          }
+        ]
+      }
+    },
+    "Endpoint": {
+      "Spec": {
+        "Mode": "vip",
+        "Ports": [
+          {
+            "Protocol": "tcp",
+            "TargetPort": 6379,
+            "PublishedPort": 8081,
+            "PublishMode": "ingress"
+          }
+        ]
+      },
+      "Ports": [
+        {
+          "Protocol": "tcp",
+          "TargetPort": 6379,
+          "PublishedPort": 8081,
+          "PublishMode": "ingress"
+        }
+      ],
+      "VirtualIPs": [
+        {
+          "NetworkID": "qs0hog6ldlei9ct11pr3c77v1",
+          "Addr": "10.0.0.3/24"
+        }
+      ]
+    }
+  }
+]`),
+			},
+			want: []service{
+				{
+					ID: "tgsci5gd31aai3jyudv98pqxf",
+					Spec: struct {
+						Labels       map[string]string
+						Name         string
+						TaskTemplate struct {
+							ContainerSpec struct {
+								Hostname string
+								Image    string
+							}
+						}
+						Mode struct {
+							Global     interface{}
+							Replicated interface{}
+						}
+					}{
+						Labels: map[string]string{},
+						Name:   "redis2",
+						TaskTemplate: struct {
+							ContainerSpec struct {
+								Hostname string
+								Image    string
+							}
+						}{
+							ContainerSpec: struct {
+								Hostname string
+								Image    string
+							}{
+								Hostname: "",
+								Image:    "redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842",
+							},
+						},
+						Mode: struct {
+							Global     interface{}
+							Replicated interface{}
+						}{
+							Replicated: map[string]interface{}{},
+						},
+					},
+					Endpoint: struct {
+						Ports      []portConfig
+						VirtualIPs []struct {
+							NetworkID string
+							Addr      string
+						}
+					}{Ports: []portConfig{
+						{
+							Protocol:      "tcp",
+							PublishMode:   "ingress",
+							PublishedPort: 8081,
+						},
+					}, VirtualIPs: []struct {
+						NetworkID string
+						Addr      string
+					}{
+						{
+							NetworkID: "qs0hog6ldlei9ct11pr3c77v1",
+							Addr:      "10.0.0.3/24",
+						},
+					},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseServicesResponse(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseServicesResponse() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseServicesResponse() \ngot  %v, \nwant %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_addServicesLabels(t *testing.T) {
+	type args struct {
+		services       []service
+		networksLabels []map[string]string
+		port           int
+	}
+	tests := []struct {
+		name string
+		args args
+		want [][]prompbmarshal.Label
+	}{
+		{
+			name: "add 2 services with network labels join",
+			args: args{
+				port: 9100,
+				networksLabels: []map[string]string{
+					{
+						"__meta_dockerswarm_network_id":         "qs0hog6ldlei9ct11pr3c77v1",
+						"__meta_dockerswarm_network_ingress":    "true",
+						"__meta_dockerswarm_network_internal":   "false",
+						"__meta_dockerswarm_network_label_key1": "value1",
+						"__meta_dockerswarm_network_name":       "ingress",
+						"__meta_dockerswarm_network_scope":      "swarm",
+					},
+				},
+				services: []service{
+					{
+						ID: "tgsci5gd31aai3jyudv98pqxf",
+						Spec: struct {
+							Labels       map[string]string
+							Name         string
+							TaskTemplate struct {
+								ContainerSpec struct {
+									Hostname string
+									Image    string
+								}
+							}
+							Mode struct {
+								Global     interface{}
+								Replicated interface{}
+							}
+						}{
+							Labels: map[string]string{},
+							Name:   "redis2",
+							TaskTemplate: struct {
+								ContainerSpec struct {
+									Hostname string
+									Image    string
+								}
+							}{
+								ContainerSpec: struct {
+									Hostname string
+									Image    string
+								}{
+									Hostname: "node1",
+									Image:    "redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842",
+								},
+							},
+							Mode: struct {
+								Global     interface{}
+								Replicated interface{}
+							}{
+								Replicated: map[string]interface{}{},
+							},
+						},
+						Endpoint: struct {
+							Ports      []portConfig
+							VirtualIPs []struct {
+								NetworkID string
+								Addr      string
+							}
+						}{Ports: []portConfig{
+							{
+								Protocol:    "tcp",
+								Name:        "redis",
+								PublishMode: "ingress",
+							},
+						}, VirtualIPs: []struct {
+							NetworkID string
+							Addr      string
+						}{
+							{
+								NetworkID: "qs0hog6ldlei9ct11pr3c77v1",
+								Addr:      "10.0.0.3/24",
+							},
+						},
+						},
+					},
+				},
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address__":                                           "10.0.0.3:0",
+					"__meta_dockerswarm_network_id":                         "qs0hog6ldlei9ct11pr3c77v1",
+					"__meta_dockerswarm_network_ingress":                    "true",
+					"__meta_dockerswarm_network_internal":                   "false",
+					"__meta_dockerswarm_network_label_key1":                 "value1",
+					"__meta_dockerswarm_network_name":                       "ingress",
+					"__meta_dockerswarm_network_scope":                      "swarm",
+					"__meta_dockerswarm_service_endpoint_port_name":         "redis",
+					"__meta_dockerswarm_service_endpoint_port_publish_mode": "ingress",
+					"__meta_dockerswarm_service_id":                         "tgsci5gd31aai3jyudv98pqxf",
+					"__meta_dockerswarm_service_mode":                       "replicated",
+					"__meta_dockerswarm_service_name":                       "redis2",
+					"__meta_dockerswarm_service_task_container_hostname":    "node1",
+					"__meta_dockerswarm_service_task_container_image":       "redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842",
+				})},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := addServicesLabels(tt.args.services, tt.args.networksLabels, tt.args.port)
+			var sortedLabelss [][]prompbmarshal.Label
+			for _, labels := range got {
+				sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
+			}
+			if !reflect.DeepEqual(sortedLabelss, tt.want) {
+				t.Errorf("addServicesLabels() \ngot  %v, \nwant %v", sortedLabelss, tt.want)
+			}
+		})
+	}
+}
diff --git a/lib/promscrape/discovery/dockerswarm/tasks_test.go b/lib/promscrape/discovery/dockerswarm/tasks_test.go
new file mode 100644
index 000000000..0d32c4cfb
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/tasks_test.go
@@ -0,0 +1,352 @@
+package dockerswarm
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+func Test_parseTasks(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []task
+		wantErr bool
+	}{
+		{
+			name: "parse ok",
+			args: args{
+				data: []byte(`[
+  {
+    "ID": "t4rdm7j2y9yctbrksiwvsgpu5",
+    "Version": {
+      "Index": 23
+    },
+    "Labels": {},
+    "Spec": {
+      "ContainerSpec": {
+        "Image": "redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842",
+        "Init": false
+      },
+      "Resources": {
+        "Limits": {},
+        "Reservations": {}
+      },
+      "Placement": {
+        "Platforms": [
+          {
+            "Architecture": "amd64",
+            "OS": "linux"
+          }
+        ]
+      },
+      "ForceUpdate": 0
+    },
+    "ServiceID": "t91nf284wzle1ya09lqvyjgnq",
+    "Slot": 1,
+    "NodeID": "qauwmifceyvqs0sipvzu8oslu",
+    "Status": {
+      "State": "running",
+      "ContainerStatus": {
+        "ContainerID": "33034b69f6fa5f808098208752fd1fe4e0e1ca86311988cea6a73b998cdc62e8",
+        "ExitCode": 0
+      },
+      "PortStatus": {}
+    },
+    "DesiredState": "running"
+  }
+]
+`),
+			},
+			want: []task{
+				{
+					ID:           "t4rdm7j2y9yctbrksiwvsgpu5",
+					ServiceID:    "t91nf284wzle1ya09lqvyjgnq",
+					NodeID:       "qauwmifceyvqs0sipvzu8oslu",
+					Labels:       map[string]string{},
+					DesiredState: "running",
+					Slot:         1,
+					Status: struct {
+						State           string
+						ContainerStatus *struct{ ContainerID string }
+						PortStatus      struct{ Ports []portConfig }
+					}{
+						State: "running",
+						ContainerStatus: &struct{ ContainerID string }{
+							ContainerID: "33034b69f6fa5f808098208752fd1fe4e0e1ca86311988cea6a73b998cdc62e8",
+						},
+						PortStatus: struct{ Ports []portConfig }{}},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseTasks(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseTasks() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseTasks() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_addTasksLabels(t *testing.T) {
+	type args struct {
+		tasks          []task
+		nodesLabels    []map[string]string
+		servicesLabels []map[string]string
+		networksLabels []map[string]string
+		services       []service
+		port           int
+	}
+	tests := []struct {
+		name string
+		args args
+		want [][]prompbmarshal.Label
+	}{
+		{
+			name: "adds 1 task with nodes labels",
+			args: args{
+				port: 9100,
+				tasks: []task{
+					{
+						ID:           "t4rdm7j2y9yctbrksiwvsgpu5",
+						ServiceID:    "t91nf284wzle1ya09lqvyjgnq",
+						NodeID:       "qauwmifceyvqs0sipvzu8oslu",
+						Labels:       map[string]string{},
+						DesiredState: "running",
+						Slot:         1,
+						Status: struct {
+							State           string
+							ContainerStatus *struct{ ContainerID string }
+							PortStatus      struct{ Ports []portConfig }
+						}{
+							State: "running",
+							ContainerStatus: &struct{ ContainerID string }{
+								ContainerID: "33034b69f6fa5f808098208752fd1fe4e0e1ca86311988cea6a73b998cdc62e8",
+							},
+							PortStatus: struct{ Ports []portConfig }{
+								Ports: []portConfig{
+									{
+										PublishMode:   "ingress",
+										Name:          "redis",
+										Protocol:      "tcp",
+										PublishedPort: 6379,
+									},
+								},
+							}},
+					},
+				},
+				nodesLabels: []map[string]string{
+					{
+						"__address__":                                   "172.31.40.97:9100",
+						"__meta_dockerswarm_node_address":               "172.31.40.97",
+						"__meta_dockerswarm_node_availability":          "active",
+						"__meta_dockerswarm_node_engine_version":        "19.03.11",
+						"__meta_dockerswarm_node_hostname":              "ip-172-31-40-97",
+						"__meta_dockerswarm_node_id":                    "qauwmifceyvqs0sipvzu8oslu",
+						"__meta_dockerswarm_node_platform_architecture": "x86_64",
+						"__meta_dockerswarm_node_platform_os":           "linux",
+						"__meta_dockerswarm_node_role":                  "manager",
+						"__meta_dockerswarm_node_status":                "ready",
+					},
+				},
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address__":                                   "172.31.40.97:9100",
+					"__meta_dockerswarm_node_address":               "172.31.40.97",
+					"__meta_dockerswarm_node_availability":          "active",
+					"__meta_dockerswarm_node_engine_version":        "19.03.11",
+					"__meta_dockerswarm_node_hostname":              "ip-172-31-40-97",
+					"__meta_dockerswarm_node_id":                    "qauwmifceyvqs0sipvzu8oslu",
+					"__meta_dockerswarm_node_platform_architecture": "x86_64",
+					"__meta_dockerswarm_node_platform_os":           "linux",
+					"__meta_dockerswarm_node_role":                  "manager",
+					"__meta_dockerswarm_node_status":                "ready",
+					"__meta_dockerswarm_task_container_id":          "33034b69f6fa5f808098208752fd1fe4e0e1ca86311988cea6a73b998cdc62e8",
+					"__meta_dockerswarm_task_desired_state":         "running",
+					"__meta_dockerswarm_task_id":                    "t4rdm7j2y9yctbrksiwvsgpu5",
+					"__meta_dockerswarm_task_port_publish_mode":     "ingress",
+					"__meta_dockerswarm_task_slot":                  "1",
+					"__meta_dockerswarm_task_state":                 "running",
+				})},
+		},
+		{
+			name: "adds 1 task with nodes, network and services labels",
+			args: args{
+				port: 9100,
+				tasks: []task{
+					{
+						ID:           "t4rdm7j2y9yctbrksiwvsgpu5",
+						ServiceID:    "tgsci5gd31aai3jyudv98pqxf",
+						NodeID:       "qauwmifceyvqs0sipvzu8oslu",
+						Labels:       map[string]string{},
+						DesiredState: "running",
+						Slot:         1,
+						NetworksAttachments: []struct {
+							Addresses []string
+							Network   struct{ ID string }
+						}{
+							{
+								Network: struct {
+									ID string
+								}{
+									ID: "qs0hog6ldlei9ct11pr3c77v1",
+								},
+								Addresses: []string{"10.10.15.15/24"},
+							},
+						},
+						Status: struct {
+							State           string
+							ContainerStatus *struct{ ContainerID string }
+							PortStatus      struct{ Ports []portConfig }
+						}{
+							State: "running",
+							ContainerStatus: &struct{ ContainerID string }{
+								ContainerID: "33034b69f6fa5f808098208752fd1fe4e0e1ca86311988cea6a73b998cdc62e8",
+							},
+							PortStatus: struct{ Ports []portConfig }{}},
+					},
+				},
+				networksLabels: []map[string]string{
+					{
+						"__meta_dockerswarm_network_id":         "qs0hog6ldlei9ct11pr3c77v1",
+						"__meta_dockerswarm_network_ingress":    "true",
+						"__meta_dockerswarm_network_internal":   "false",
+						"__meta_dockerswarm_network_label_key1": "value1",
+						"__meta_dockerswarm_network_name":       "ingress",
+						"__meta_dockerswarm_network_scope":      "swarm",
+					},
+				},
+				nodesLabels: []map[string]string{
+					{
+						"__address__":                                   "172.31.40.97:9100",
+						"__meta_dockerswarm_node_address":               "172.31.40.97",
+						"__meta_dockerswarm_node_availability":          "active",
+						"__meta_dockerswarm_node_engine_version":        "19.03.11",
+						"__meta_dockerswarm_node_hostname":              "ip-172-31-40-97",
+						"__meta_dockerswarm_node_id":                    "qauwmifceyvqs0sipvzu8oslu",
+						"__meta_dockerswarm_node_platform_architecture": "x86_64",
+						"__meta_dockerswarm_node_platform_os":           "linux",
+						"__meta_dockerswarm_node_role":                  "manager",
+						"__meta_dockerswarm_node_status":                "ready",
+					},
+				},
+				services: []service{
+					{
+						ID: "tgsci5gd31aai3jyudv98pqxf",
+						Spec: struct {
+							Labels       map[string]string
+							Name         string
+							TaskTemplate struct {
+								ContainerSpec struct {
+									Hostname string
+									Image    string
+								}
+							}
+							Mode struct {
+								Global     interface{}
+								Replicated interface{}
+							}
+						}{
+							Labels: map[string]string{},
+							Name:   "redis2",
+							TaskTemplate: struct {
+								ContainerSpec struct {
+									Hostname string
+									Image    string
+								}
+							}{
+								ContainerSpec: struct {
+									Hostname string
+									Image    string
+								}{
+									Hostname: "node1",
+									Image:    "redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842",
+								},
+							},
+							Mode: struct {
+								Global     interface{}
+								Replicated interface{}
+							}{
+								Replicated: map[string]interface{}{},
+							},
+						},
+						Endpoint: struct {
+							Ports      []portConfig
+							VirtualIPs []struct {
+								NetworkID string
+								Addr      string
+							}
+						}{Ports: []portConfig{
+							{
+								Protocol:    "tcp",
+								Name:        "redis",
+								PublishMode: "ingress",
+							},
+						}, VirtualIPs: []struct {
+							NetworkID string
+							Addr      string
+						}{
+							{
+								NetworkID: "qs0hog6ldlei9ct11pr3c77v1",
+								Addr:      "10.0.0.3/24",
+							},
+						},
+						},
+					},
+				},
+				servicesLabels: []map[string]string{},
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address":                                     "10.10.15.15:0",
+					"__address__":                                   "172.31.40.97:9100",
+					"__meta_dockerswarm_network_id":                 "qs0hog6ldlei9ct11pr3c77v1",
+					"__meta_dockerswarm_network_ingress":            "true",
+					"__meta_dockerswarm_network_internal":           "false",
+					"__meta_dockerswarm_network_label_key1":         "value1",
+					"__meta_dockerswarm_network_name":               "ingress",
+					"__meta_dockerswarm_network_scope":              "swarm",
+					"__meta_dockerswarm_node_address":               "172.31.40.97",
+					"__meta_dockerswarm_node_availability":          "active",
+					"__meta_dockerswarm_node_engine_version":        "19.03.11",
+					"__meta_dockerswarm_node_hostname":              "ip-172-31-40-97",
+					"__meta_dockerswarm_node_id":                    "qauwmifceyvqs0sipvzu8oslu",
+					"__meta_dockerswarm_node_platform_architecture": "x86_64",
+					"__meta_dockerswarm_node_platform_os":           "linux",
+					"__meta_dockerswarm_node_role":                  "manager",
+					"__meta_dockerswarm_node_status":                "ready",
+					"__meta_dockerswarm_task_container_id":          "33034b69f6fa5f808098208752fd1fe4e0e1ca86311988cea6a73b998cdc62e8",
+					"__meta_dockerswarm_task_desired_state":         "running",
+					"__meta_dockerswarm_task_id":                    "t4rdm7j2y9yctbrksiwvsgpu5",
+					"__meta_dockerswarm_task_port_publish_mode":     "ingress",
+					"__meta_dockerswarm_task_slot":                  "1",
+					"__meta_dockerswarm_task_state":                 "running",
+				}),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := addTasksLabels(tt.args.tasks, tt.args.nodesLabels, tt.args.servicesLabels, tt.args.networksLabels, tt.args.services, tt.args.port)
+			var sortedLabelss [][]prompbmarshal.Label
+			for _, labels := range got {
+				sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
+			}
+			if !reflect.DeepEqual(sortedLabelss, tt.want) {
+				t.Errorf("addTasksLabels() \ngot  %v, \nwant %v", sortedLabelss, tt.want)
+			}
+		})
+	}
+}