Skip to content

Commit

Permalink
Add component discovery.dockerswarm to flow (#5181)
Browse files Browse the repository at this point in the history
* Add component discovery.dockerswarm to flow

* minor cleanup

* Update docs/sources/flow/reference/components/discovery.dockerswarm.md

Co-authored-by: Robert Fratto <[email protected]>

* improve doc

---------

Co-authored-by: Robert Fratto <[email protected]>
  • Loading branch information
wildum and rfratto authored Sep 18, 2023
1 parent 8bb22c7 commit c5ef0f6
Show file tree
Hide file tree
Showing 14 changed files with 657 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Main (unreleased)
- `discovery.serverset` discovers Serversets stored in Zookeeper. (@thampiotr)
- `discovery.scaleway` discovers scrape targets from Scaleway virtual
instances and bare-metal machines. (@rfratto)
- `discovery.dockerswarm` discovers scrape targets from Docker Swarm. (@wildum)
- `otelcol.processor.probabilistic_sampler` samples logs and traces based on configuration options. (@mar4uk)
- `remote.kubernetes.configmap` loads a configmap's data for use in other components (@captncraig)
- `remote.kubernetes.secret` loads a secret's data for use in other components (@captncraig)
Expand Down
1 change: 1 addition & 0 deletions component/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
_ "github.com/grafana/agent/component/discovery/digitalocean" // Import discovery.digitalocean
_ "github.com/grafana/agent/component/discovery/dns" // Import discovery.dns
_ "github.com/grafana/agent/component/discovery/docker" // Import discovery.docker
_ "github.com/grafana/agent/component/discovery/dockerswarm" // Import discovery.dockerswarm
_ "github.com/grafana/agent/component/discovery/eureka" // Import discovery.eureka
_ "github.com/grafana/agent/component/discovery/file" // Import discovery.file
_ "github.com/grafana/agent/component/discovery/gce" // Import discovery.gce
Expand Down
104 changes: 104 additions & 0 deletions component/discovery/dockerswarm/dockerswarm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package dockerswarm

import (
"fmt"
"net/url"
"time"

"github.com/grafana/agent/component"
"github.com/grafana/agent/component/common/config"
"github.com/grafana/agent/component/discovery"
"github.com/prometheus/common/model"
prom_discovery "github.com/prometheus/prometheus/discovery/moby"
)

func init() {
component.Register(component.Registration{
Name: "discovery.dockerswarm",
Args: Arguments{},
Exports: discovery.Exports{},

Build: func(opts component.Options, args component.Arguments) (component.Component, error) {
return New(opts, args.(Arguments))
},
})
}

type Arguments struct {
Host string `river:"host,attr"`
Role string `river:"role,attr"`
Port int `river:"port,attr,optional"`
Filters []Filter `river:"filter,block,optional"`
RefreshInterval time.Duration `river:"refresh_interval,attr,optional"`
HTTPClientConfig config.HTTPClientConfig `river:",squash"`
}

type Filter struct {
Name string `river:"name,attr"`
Values []string `river:"values,attr"`
}

var DefaultArguments = Arguments{
RefreshInterval: 60 * time.Second,
Port: 80,
HTTPClientConfig: config.DefaultHTTPClientConfig,
}

// SetToDefault implements river.Defaulter.
func (a *Arguments) SetToDefault() {
*a = DefaultArguments
}

// Validate implements river.Validator.
func (a *Arguments) Validate() error {
if _, err := url.Parse(a.Host); err != nil {
return err
}
if a.RefreshInterval <= 0 {
return fmt.Errorf("refresh_interval must be greater than 0")
}
switch a.Role {
case "services", "nodes", "tasks":
default:
return fmt.Errorf("invalid role %s, expected tasks, services, or nodes", a.Role)
}
return a.HTTPClientConfig.Validate()
}

// Convert converts Arguments into the SDConfig type.
func (a *Arguments) Convert() *prom_discovery.DockerSwarmSDConfig {
return &prom_discovery.DockerSwarmSDConfig{
Host: a.Host,
Role: a.Role,
Port: a.Port,
Filters: convertFilters(a.Filters),
RefreshInterval: model.Duration(a.RefreshInterval),
HTTPClientConfig: *a.HTTPClientConfig.Convert(),
}
}

func convertFilters(filters []Filter) []prom_discovery.Filter {
promFilters := make([]prom_discovery.Filter, len(filters))
for i, filter := range filters {
promFilters[i] = filter.convert()
}
return promFilters
}

func (f *Filter) convert() prom_discovery.Filter {
values := make([]string, len(f.Values))
copy(values, f.Values)

return prom_discovery.Filter{
Name: f.Name,
Values: values,
}
}

// New returns a new instance of discovery.dockerswarm component.
func New(opts component.Options, args Arguments) (*discovery.Component, error) {
return discovery.New(opts, args, func(args component.Arguments) (discovery.Discoverer, error) {
newArgs := args.(Arguments)
return prom_discovery.NewDiscovery(newArgs.Convert(), opts.Logger)
})
}
107 changes: 107 additions & 0 deletions component/discovery/dockerswarm/dockerswarm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package dockerswarm

import (
"testing"
"time"

"github.com/grafana/agent/component/common/config"
"github.com/grafana/river"
"github.com/grafana/river/rivertypes"
promConfig "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gotest.tools/assert"
)

func TestRiverUnmarshal(t *testing.T) {
riverCfg := `
host = "unix:///var/run/docker.sock"
role = "nodes"
port = 81
filter {
name = "n1"
values = ["v11", "v12"]
}
filter {
name = "n2"
values = ["v21"]
}
refresh_interval = "12s"
basic_auth {
username = "username"
password = "pass"
}
`

var args Arguments
err := river.Unmarshal([]byte(riverCfg), &args)
require.NoError(t, err)
require.ElementsMatch(t, []Filter{{"n1", []string{"v11", "v12"}}, {"n2", []string{"v21"}}}, args.Filters)
assert.Equal(t, "unix:///var/run/docker.sock", args.Host)
assert.Equal(t, "nodes", args.Role)
assert.Equal(t, 81, args.Port)
assert.Equal(t, 12*time.Second, args.RefreshInterval)
assert.Equal(t, "username", args.HTTPClientConfig.BasicAuth.Username)
assert.Equal(t, rivertypes.Secret("pass"), args.HTTPClientConfig.BasicAuth.Password)
}

func TestConvert(t *testing.T) {
riverArgs := Arguments{
Host: "host",
Role: "nodes",
Port: 81,
Filters: []Filter{{"n1", []string{"v11", "v12"}}, {"n2", []string{"v21"}}},
RefreshInterval: time.Minute,
HTTPClientConfig: config.HTTPClientConfig{
BasicAuth: &config.BasicAuth{
Username: "username",
Password: "pass",
},
},
}

promArgs := riverArgs.Convert()
assert.Equal(t, 2, len(promArgs.Filters))
assert.Equal(t, "n1", promArgs.Filters[0].Name)
require.ElementsMatch(t, []string{"v11", "v12"}, promArgs.Filters[0].Values)
assert.Equal(t, "n2", promArgs.Filters[1].Name)
require.ElementsMatch(t, []string{"v21"}, promArgs.Filters[1].Values)
assert.Equal(t, "host", promArgs.Host)
assert.Equal(t, "nodes", promArgs.Role)
assert.Equal(t, 81, promArgs.Port)
assert.Equal(t, model.Duration(time.Minute), promArgs.RefreshInterval)
assert.Equal(t, "username", promArgs.HTTPClientConfig.BasicAuth.Username)
assert.Equal(t, promConfig.Secret("pass"), promArgs.HTTPClientConfig.BasicAuth.Password)
}

func TestValidateRole(t *testing.T) {
riverArgs := Arguments{
Host: "host",
Role: "nodes",
RefreshInterval: time.Second,
}
err := riverArgs.Validate()
require.NoError(t, err)

riverArgs.Role = "services"
err = riverArgs.Validate()
require.NoError(t, err)

riverArgs.Role = "tasks"
err = riverArgs.Validate()
require.NoError(t, err)

riverArgs.Role = "wrong"
err = riverArgs.Validate()
assert.Error(t, err, "invalid role wrong, expected tasks, services, or nodes")
}

func TestValidateUrl(t *testing.T) {
riverArgs := Arguments{
Host: "::",
Role: "nodes",
RefreshInterval: time.Second,
}
err := riverArgs.Validate()
assert.Error(t, err, "parse \"::\": missing protocol scheme")
}
56 changes: 56 additions & 0 deletions converter/internal/prometheusconvert/dockerswarm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package prometheusconvert

import (
"time"

"github.com/grafana/agent/component/discovery"
"github.com/grafana/agent/component/discovery/dockerswarm"
"github.com/grafana/agent/converter/diag"
"github.com/grafana/agent/converter/internal/common"
prom_moby "github.com/prometheus/prometheus/discovery/moby"
)

func appendDiscoveryDockerswarm(pb *prometheusBlocks, label string, sdConfig *prom_moby.DockerSwarmSDConfig) discovery.Exports {
discoveryDockerswarmArgs := toDiscoveryDockerswarm(sdConfig)
name := []string{"discovery", "dockerswarm"}
block := common.NewBlockWithOverride(name, label, discoveryDockerswarmArgs)
pb.discoveryBlocks = append(pb.discoveryBlocks, newPrometheusBlock(block, name, label, "", ""))
return NewDiscoveryExports("discovery.dockerswarm." + label + ".targets")
}

func validateDiscoveryDockerswarm(sdConfig *prom_moby.DockerSwarmSDConfig) diag.Diagnostics {
return ValidateHttpClientConfig(&sdConfig.HTTPClientConfig)
}

func toDiscoveryDockerswarm(sdConfig *prom_moby.DockerSwarmSDConfig) *dockerswarm.Arguments {
if sdConfig == nil {
return nil
}

return &dockerswarm.Arguments{
Host: sdConfig.Host,
Role: sdConfig.Role,
Port: sdConfig.Port,
Filters: convertFilters(sdConfig.Filters),
RefreshInterval: time.Duration(sdConfig.RefreshInterval),
HTTPClientConfig: *ToHttpClientConfig(&sdConfig.HTTPClientConfig),
}
}

func convertFilters(mobyFilters []prom_moby.Filter) []dockerswarm.Filter {
riverFilters := make([]dockerswarm.Filter, len(mobyFilters))
for i, mobyFilter := range mobyFilters {
riverFilters[i] = convertFilter(&mobyFilter)
}
return riverFilters
}

func convertFilter(mobyFilter *prom_moby.Filter) dockerswarm.Filter {
values := make([]string, len(mobyFilter.Values))
copy(values, mobyFilter.Values)

return dockerswarm.Filter{
Name: mobyFilter.Name,
Values: values,
}
}
4 changes: 4 additions & 0 deletions converter/internal/prometheusconvert/prometheusconvert.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
prom_linode "github.com/prometheus/prometheus/discovery/linode"
prom_marathon "github.com/prometheus/prometheus/discovery/marathon"
prom_docker "github.com/prometheus/prometheus/discovery/moby"
prom_moby "github.com/prometheus/prometheus/discovery/moby"
prom_scaleway "github.com/prometheus/prometheus/discovery/scaleway"
prom_triton "github.com/prometheus/prometheus/discovery/triton"
prom_xds "github.com/prometheus/prometheus/discovery/xds"
Expand Down Expand Up @@ -188,6 +189,9 @@ func AppendServiceDiscoveryConfigs(pb *prometheusBlocks, serviceDiscoveryConfig
case *prom_nerve.NerveSDConfig:
labelCounts["nerve"]++
exports = appendDiscoveryNerve(pb, common.LabelWithIndex(labelCounts["nerve"]-1, label), sdc)
case *prom_moby.DockerSwarmSDConfig:
labelCounts["dockerswarm"]++
exports = appendDiscoveryDockerswarm(pb, common.LabelWithIndex(labelCounts["dockerswarm"]-1, label), sdc)
}

targets = append(targets, exports.Targets...)
Expand Down
54 changes: 54 additions & 0 deletions converter/internal/prometheusconvert/testdata/dockerswarm.river
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
discovery.dockerswarm "prometheus1" {
host = "http://localhost:8081"
role = "nodes"
port = 81

filter {
name = "n1"
values = ["v1", "v2"]
}

filter {
name = "n2"
values = ["v3"]
}
refresh_interval = "5m0s"

basic_auth {
username = "username"
password = "password"
}
}

discovery.dockerswarm "prometheus2" {
host = "http://localhost:8080"
role = "services"
}

prometheus.scrape "prometheus1" {
targets = concat(
discovery.dockerswarm.prometheus1.targets,
[{
__address__ = "localhost:9090",
}],
)
forward_to = [prometheus.remote_write.default.receiver]
job_name = "prometheus1"
}

prometheus.scrape "prometheus2" {
targets = discovery.dockerswarm.prometheus2.targets
forward_to = [prometheus.remote_write.default.receiver]
job_name = "prometheus2"
}

prometheus.remote_write "default" {
endpoint {
name = "remote1"
url = "http://remote-write-url1"

queue_config { }

metadata_config { }
}
}
29 changes: 29 additions & 0 deletions converter/internal/prometheusconvert/testdata/dockerswarm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
scrape_configs:
- job_name: "prometheus1"
static_configs:
- targets: ["localhost:9090"]
dockerswarm_sd_configs:
- host : "http://localhost:8081"
role: "nodes"
port: 81
filters:
- name: "n1"
values:
- "v1"
- "v2"
- name: "n2"
values:
- "v3"
refresh_interval: 5m
basic_auth:
username: "username"
password: "password"

- job_name: "prometheus2"
dockerswarm_sd_configs:
- host : "http://localhost:8080"
role: "services"

remote_write:
- name: "remote1"
url: "http://remote-write-url1"
Loading

0 comments on commit c5ef0f6

Please sign in to comment.