Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal API changes are not present.
Main (unreleased)
-----------------

- Support specifying DNS discovery mode prefixes in `--cluster.join-addresses` flag. (@x1unix)

### Features

- Add `otelcol.connector.count` component to count the number of spans, metrics, and logs passing through it. (@hhertout)
Expand Down
18 changes: 18 additions & 0 deletions docs/sources/reference/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ Since Windows doesn't use the interface names `eth0` or `en0`, Windows users mus
The comma-separated list of addresses provided in `--cluster.join-addresses` can either be IP addresses or DNS names to lookup (supports SRV and A/AAAA records).
In both cases, the port number can be specified with a `:<port>` suffix. If ports aren't provided, default of the port used for the HTTP listener is used.
If you don't provide the port number explicitly, you must ensure that all instances use the same port for the HTTP listener.
Optionally, you may specify a DNS query type as a prefix for each address. See [join addresses format](#join-address-format) for more information.

The `--cluster.enable-tls` flag can be set to enable TLS for peer-to-peer communications.
Additional arguments are required to configure the TLS client, including the CA certificate, the TLS certificate, the key, and the server name.
Expand Down Expand Up @@ -171,6 +172,23 @@ When `--cluster.name` is provided, nodes only join peers who share the same clus
By default, the cluster name is empty, and any node that doesn't set the flag can join.
Attempting to join a cluster with a wrong `--cluster.name` results in a "failed to join memberlist" error.

### Join Address Format

The `--cluster.join-addresses` flag supports DNS names with discovery mode prefix.
You select a discovery mode by adding one of the following supported prefixes to the address:

* **`dns+`**\
The domain name after the prefix is looked up as an A/AAAA query.\
For example: `dns+alloy.local:11211`.
* **`dnssrv+`**\
The domain name after the prefix is looked up as a SRV query, and then each SRV record is resolved as an A/AAAA record.\
For example: `dnssrv+_alloy._tcp.alloy.namespace.svc.cluster.local`.
* **`dnssrvnoa+`**\
The domain name after the prefix is looked up as a SRV query, with no A/AAAA lookup made after that.\
For example: `dnssrvnoa+_alloy-memberlist._tcp.service.consul`

If no prefix is provided, Alloy will attempt to resolve the name using both A/AAAA and DNSSRV queries.

### Clustering states

Clustered {{< param "PRODUCT_NAME" >}}s are in one of three states:
Expand Down
55 changes: 55 additions & 0 deletions internal/service/cluster/discovery/join_peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net"
"strconv"
"strings"

"github.com/go-kit/log"
"github.com/samber/lo"
Expand All @@ -29,7 +30,9 @@ func newWithJoinPeers(opts Options) DiscoverFn {
defer span.End()

// Use these resolvers in order to resolve the provided addresses into a form that can be used by clustering.
// NOTE: dnsSDURLResolver should be above other DNS resolvers.
resolvers := []addressResolver{
dnsSDURLResolver(opts, ctx),
ipResolver(opts.Logger),
dnsAResolver(opts, ctx),
dnsSRVResolver(opts, ctx),
Expand Down Expand Up @@ -128,6 +131,7 @@ func dnsSRVResolver(opts Options, ctx context.Context) addressResolver {
if srvLookup == nil {
srvLookup = net.LookupSRV
}

return dnsResolver(opts, ctx, "SRV", func(addr string) ([]string, error) {
_, addresses, err := srvLookup("", "", addr)
result := make([]string, 0, len(addresses))
Expand All @@ -138,6 +142,57 @@ func dnsSRVResolver(opts Options, ctx context.Context) addressResolver {
})
}

const (
dnsSchemeDNS = "dns+"
dnsSchemeDNSSRV = "dnssrv+"
dnsSchemeDNSSRVNOA = "dnssrvnoa+"
)

// dnsSDURLResolver handles DNS-SD URLs which explicitly states what DNS query should be used for host resolve.
Comment thread
x1unix marked this conversation as resolved.
//
// Example: `dnssrv+_memcached._tcp.memcached.namespace.svc.cluster.local`
//
// Resolver rejects any non-URL values.
func dnsSDURLResolver(opts Options, ctx context.Context) addressResolver {
srvLookup := opts.lookupSRVFn
if srvLookup == nil {
srvLookup = net.LookupSRV
}

return func(addr string) ([]string, error) {
var (
nextAddr string
nextResolver addressResolver
)

switch {
case strings.HasPrefix(addr, dnsSchemeDNS):
nextAddr = addr[len(dnsSchemeDNS):]
nextResolver = dnsAResolver(opts, ctx)
case strings.HasPrefix(addr, dnsSchemeDNSSRV):
nextAddr = addr[len(dnsSchemeDNSSRV):]
nextResolver = dnsSRVResolver(opts, ctx)
case strings.HasPrefix(addr, dnsSchemeDNSSRVNOA):
nextAddr = addr[len(dnsSchemeDNSSRVNOA):]
nextResolver = dnsResolver(opts, ctx, "SRVNOA", func(addr string) ([]string, error) {
// NOTE: the only difference between SRVNOA and SRV, as SRV request should do N+1 query for A/AAAA.
_, addresses, err := srvLookup("", "", addr)
result := make([]string, 0, len(addresses))
for _, a := range addresses {
result = append(result, a.Target)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything in spec about randomizing the order here? for clustering purposes, it may be useful to have the addresses in random order :)

Copy link
Copy Markdown
Member Author

@x1unix x1unix Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thampiotr IIRC spec doesn't mention this, but some DNS servers return items in a random order - this was the case with Consul, and Alloy worked perfectly fine with that during my test run.

Do you know if Loki and Tempo do any explicit randomization?

}
return result, err
})

default:
// skip and pass control to a next resolver.
return nil, nil
}

return nextResolver(nextAddr)
}
}

func dnsResolver(opts Options, ctx context.Context, recordType string, dnsLookupFn func(string) ([]string, error)) addressResolver {
return func(addr string) ([]string, error) {
_, span := opts.Tracer.Tracer("").Start(
Expand Down
33 changes: 33 additions & 0 deletions internal/service/cluster/discovery/peer_discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,39 @@ func TestPeerDiscovery(t *testing.T) {
},
expected: []string{"10.10.10.11:8888"},
},
{
name: "dnssrvnoa records are parsed",
args: Options{
JoinPeers: []string{"dnssrvnoa+_alloy-memberlist._tcp.service.consul", "dns+host2:7777"},
DefaultPort: 8888,
Logger: logger,
Tracer: tracer,
lookupIPFn: func(name string) ([]net.IP, error) {
if name == "host2" {
return []net.IP{
net.ParseIP("192.168.1.10"),
}, nil
}

return nil, fmt.Errorf("unexpected name %q", name)
},
lookupSRVFn: func(service, proto, name string) (string, []*net.SRV, error) {
if name == "_alloy-memberlist._tcp.service.consul" {
return "", []*net.SRV{
{Target: "10.10.10.10"},
{Target: "10.10.10.11"},
}, nil
}

return "", nil, fmt.Errorf("unexpected name %q", name)
},
},
expected: []string{
"10.10.10.10:8888",
"10.10.10.11:8888",
"192.168.1.10:7777",
},
},
{
name: "go discovery factory error",
args: Options{
Expand Down
Loading