Skip to content

Commit 2832b92

Browse files
authored
feat: dns lookup source enhancements (#48869)
#### Description 1. Added A and AAAA lookups to DNS lookup source 2. Added configurable option to return multiple results if a single dns resolution has multiple values. #### Link to tracking issue Planned enhancement which was mentioned in TODO list of the processor. #### Testing 1. Unit tests added to test A and AAAA lookups. 2. Manual testing done, Ran the collector with new config to ensure all lookups work fine, [Test config](https://github.com/user-attachments/files/28584515/dns-test-collector.yaml) **Test data sent**: ``` curl -s http://localhost:4318/v1/logs -X POST -H "Content-Type: application/json" -d '{ "resourceLogs": [{ "resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "dns-lookup-test"}}]}, "scopeLogs": [{ "logRecords": [ { "body": {"stringValue": "[PTR] resolve 8.8.8.8 -> hostname"}, "attributes": [ {"key": "client.ip", "value": {"stringValue": "8.8.8.8"}}, {"key": "target.hostname", "value": {"stringValue": "dns.google"}} ] } ] }] }] }' ``` **Collector debug log trace after enrichment:** Shows all different supported lookups working on a single log line. Took client.ip from resource attribute and does a PTR lookup and target.hostname was used to do A and AAAA lookups. ``` LogRecord #0 ObservedTimestamp: 1970-01-01 00:00:00 +0000 UTC Timestamp: 1970-01-01 00:00:00 +0000 UTC SeverityText: SeverityNumber: Unspecified(0) Body: Str([PTR] resolve 8.8.8.8 -> hostname) Attributes: -> client.ip: Str(8.8.8.8) -> target.hostname: Str(dns.google) -> client.hostname: Str(dns.google) // PTR lookup -> target.ipv4: Str(8.8.8.8) // A record lookup -> target.ipv6: Str(2001:4860:4860::8844) // AAAA record Lookup Trace ID: Span ID: Flags: 0 {"resource": {"service.instance.id": "3b8271db-6f06-484f-9737-7bf53bb3b53a", "service.name": "otelcol-custom", "service.version": "0.153.0-dev"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"} ``` #### Documentation Added relevant readme changes of dns source. <!--Authorship attestation. See AGENTS.md for details. AI agents must not check this box on behalf of the user; the human author must check it themselves before the PR is ready for review.--> #### Authorship - [x] I, a human, wrote this pull request description myself.
1 parent 7c643cb commit 2832b92

4 files changed

Lines changed: 220 additions & 21 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7+
component: processor/lookup
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add A and AAAA lookup support for dns source of lookup processor
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [48869]
14+
15+
# If your change doesn't affect end users or the exported elements of any package,
16+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
17+
# Optional: The change log or logs in which this entry should be included.
18+
# e.g. '[user]' or '[user, api]'
19+
# Include 'user' if the change is relevant to end users.
20+
# Include 'api' if there is a change to a library API.
21+
# Default: '[user]'
22+
change_logs: [user]

processor/lookupprocessor/internal/source/dns/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# dns Source
22

3-
Performs reverse DNS lookups (PTR records) to resolve IP addresses to hostnames. Caching is enabled by default to minimize DNS queries.
3+
Performs DNS lookups supporting forward (A/AAAA) and reverse (PTR) record types. Caching is enabled by default to minimize DNS queries.
44

55
## Configuration
66

77
| Field | Description | Default |
88
| ----- | ----------- | ------- |
9-
| `record_type` | DNS record type (currently only `PTR` is supported) | `PTR` |
9+
| `record_type` | DNS record type: `PTR` (IP → hostname), `A` (hostname → IPv4), `AAAA` (hostname → IPv6) | `PTR` |
1010
| `timeout` | Maximum time to wait for DNS query (must be `> 0`) | `1s` |
1111
| `server` | DNS server to use (e.g., `8.8.8.8:53`). Empty uses system resolver | - |
12+
| `multiple_results` | Return all results as comma-separated string instead of just the first | `false` |
1213
| `cache.enabled` | Enable caching | `true` |
1314
| `cache.size` | Maximum cache entries (LRU eviction) | `10000` |
1415
| `cache.ttl` | Time-to-live for successful lookups | `5m` |
@@ -50,7 +51,5 @@ Cache hit is ~5,000x faster than a network lookup, with zero allocations.
5051

5152
## TODO
5253

53-
- [ ] Support A record lookups (hostname to IPv4)
54-
- [ ] Support AAAA record lookups (hostname to IPv6)
5554
- [ ] Support TXT, CNAME, MX record lookups
5655
- [ ] Support multiple DNS servers

processor/lookupprocessor/internal/source/dns/dns.go

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,20 @@ const sourceType = "dns"
2020
type RecordType string
2121

2222
const (
23-
// Only PTR for now.
23+
// RecordTypePTR performs reverse DNS lookup (IP address to hostname).
2424
RecordTypePTR RecordType = "PTR"
25+
// RecordTypeA performs forward IPv4 DNS lookup (hostname to IPv4 address).
26+
RecordTypeA RecordType = "A"
27+
// RecordTypeAAAA performs forward IPv6 DNS lookup (hostname to IPv6 address).
28+
RecordTypeAAAA RecordType = "AAAA"
2529
)
2630

2731
type Config struct {
28-
// RecordType specifies the DNS record type to look up.
32+
// RecordType specifies the DNS record type to look up: "PTR", "A", or "AAAA".
33+
// - PTR: reverse lookup, resolves IP address to hostname(s)
34+
// - A: forward IPv4 lookup, resolves hostname to IPv4 address(es)
35+
// - AAAA: forward IPv6 lookup, resolves hostname to IPv6 address(es)
36+
// Default: "PTR"
2937
RecordType RecordType `mapstructure:"record_type"`
3038

3139
// Timeout is the maximum time to wait for a DNS query.
@@ -36,6 +44,11 @@ type Config struct {
3644
// If empty, uses the system resolver.
3745
Server string `mapstructure:"server"`
3846

47+
// MultipleResults specifies whether to return all DNS results or just the first.
48+
// If true, multiple results are returned as a comma-separated string.
49+
// Default: false
50+
MultipleResults bool `mapstructure:"multiple_results"`
51+
3952
// Cache configures caching for DNS lookups.
4053
// Enabled by default.
4154
// Disabling is not recommended due to potential performance impact.
@@ -44,10 +57,10 @@ type Config struct {
4457

4558
func (c *Config) Validate() error {
4659
switch c.RecordType {
47-
case "", RecordTypePTR:
60+
case "", RecordTypePTR, RecordTypeA, RecordTypeAAAA:
4861
// Valid
4962
default:
50-
return fmt.Errorf("invalid record_type %q, only PTR is currently supported", c.RecordType)
63+
return fmt.Errorf("invalid record_type %q, must be one of: PTR, A, AAAA", c.RecordType)
5164
}
5265

5366
if c.Timeout <= 0 {
@@ -110,9 +123,10 @@ func createSource(
110123
}
111124

112125
s := &dnsSource{
113-
recordType: recordType,
114-
timeout: timeout,
115-
resolver: resolver,
126+
recordType: recordType,
127+
timeout: timeout,
128+
resolver: resolver,
129+
multipleResults: dnsCfg.MultipleResults,
116130
}
117131

118132
// Create the lookup function, optionally wrapped with cache
@@ -131,16 +145,57 @@ func createSource(
131145
}
132146

133147
type dnsSource struct {
134-
recordType RecordType
135-
timeout time.Duration
136-
resolver *net.Resolver
148+
recordType RecordType
149+
timeout time.Duration
150+
resolver *net.Resolver
151+
multipleResults bool
137152
}
138153

139154
func (s *dnsSource) lookup(ctx context.Context, key string) (any, bool, error) {
140155
ctx, cancel := context.WithTimeout(ctx, s.timeout)
141156
defer cancel()
142-
// Currently only PTR is supported
143-
return s.lookupPTR(ctx, key)
157+
switch s.recordType {
158+
case RecordTypeA, RecordTypeAAAA:
159+
return s.lookupForward(ctx, key)
160+
default:
161+
return s.lookupPTR(ctx, key)
162+
}
163+
}
164+
165+
// lookupForward performs forward DNS lookup (hostname -> IP addresses).
166+
func (s *dnsSource) lookupForward(ctx context.Context, hostname string) (any, bool, error) {
167+
addrs, err := s.resolver.LookupIPAddr(ctx, hostname)
168+
if err != nil {
169+
var dnsErr *net.DNSError
170+
if errors.As(err, &dnsErr) && (dnsErr.IsNotFound || dnsErr.IsTemporary) {
171+
return nil, false, nil
172+
}
173+
return nil, false, err
174+
}
175+
176+
if len(addrs) == 0 {
177+
return nil, false, nil
178+
}
179+
180+
var ips []string
181+
for _, addr := range addrs {
182+
if s.recordType == RecordTypeA && addr.IP.To4() != nil {
183+
// IPv4 address for A record
184+
ips = append(ips, addr.IP.String())
185+
} else if s.recordType == RecordTypeAAAA && addr.IP.To4() == nil && addr.IP.To16() != nil {
186+
// IPv6 address for AAAA record
187+
ips = append(ips, addr.IP.String())
188+
}
189+
}
190+
191+
if len(ips) == 0 {
192+
return nil, false, nil
193+
}
194+
195+
if s.multipleResults {
196+
return strings.Join(ips, ","), true, nil
197+
}
198+
return ips[0], true, nil
144199
}
145200

146201
// lookupPTR performs reverse DNS lookup (IP -> hostname).
@@ -159,6 +214,13 @@ func (s *dnsSource) lookupPTR(ctx context.Context, ip string) (any, bool, error)
159214
return nil, false, nil
160215
}
161216

162-
// Return the first hostname, trimming trailing dot
163-
return strings.TrimSuffix(names[0], "."), true, nil
217+
// Trim trailing dots from PTR records
218+
for i := range names {
219+
names[i] = strings.TrimSuffix(names[i], ".")
220+
}
221+
222+
if s.multipleResults {
223+
return strings.Join(names, ","), true, nil
224+
}
225+
return names[0], true, nil
164226
}

processor/lookupprocessor/internal/source/dns/dns_test.go

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package dns
66
import (
77
"net"
88
"runtime"
9+
"strings"
910
"testing"
1011
"time"
1112

@@ -41,16 +42,26 @@ func TestConfigValidate(t *testing.T) {
4142
wantErr: false,
4243
},
4344
{
44-
name: "unsupported record type",
45+
name: "A record type",
4546
config: &Config{
46-
RecordType: "A",
47+
RecordType: RecordTypeA,
48+
Timeout: 1 * time.Second,
4749
},
48-
wantErr: true,
50+
wantErr: false,
51+
},
52+
{
53+
name: "AAAA record type",
54+
config: &Config{
55+
RecordType: RecordTypeAAAA,
56+
Timeout: 1 * time.Second,
57+
},
58+
wantErr: false,
4959
},
5060
{
5161
name: "invalid record type",
5262
config: &Config{
5363
RecordType: "INVALID",
64+
Timeout: 1 * time.Second,
5465
},
5566
wantErr: true,
5667
},
@@ -143,6 +154,111 @@ func TestDefaultConfig(t *testing.T) {
143154
// Integration tests - these actually perform DNS lookups
144155
// They're skipped in CI but useful for local development
145156

157+
func TestLookupA_Integration(t *testing.T) {
158+
if testing.Short() {
159+
t.Skip("skipping integration test in short mode")
160+
}
161+
162+
factory := NewFactory()
163+
cfg := &Config{
164+
RecordType: RecordTypeA,
165+
Timeout: 5 * time.Second,
166+
Cache: lookupsource.CacheConfig{
167+
Enabled: false,
168+
},
169+
}
170+
171+
source, err := factory.CreateSource(t.Context(), lookupsource.CreateSettings{}, cfg)
172+
require.NoError(t, err)
173+
174+
val, found, err := source.Lookup(t.Context(), "dns.google")
175+
require.NoError(t, err)
176+
177+
if found {
178+
ip, ok := val.(string)
179+
assert.True(t, ok)
180+
// A record should be an IPv4 address (contains dots, no colons)
181+
assert.Contains(t, ip, ".")
182+
assert.NotContains(t, ip, ":")
183+
t.Logf("A lookup for dns.google: %s", ip)
184+
} else {
185+
t.Log("A lookup for dns.google not found (may be network issue)")
186+
}
187+
}
188+
189+
func TestLookupAAAA_Integration(t *testing.T) {
190+
if testing.Short() {
191+
t.Skip("skipping integration test in short mode")
192+
}
193+
194+
factory := NewFactory()
195+
cfg := &Config{
196+
RecordType: RecordTypeAAAA,
197+
Timeout: 5 * time.Second,
198+
Cache: lookupsource.CacheConfig{
199+
Enabled: false,
200+
},
201+
}
202+
203+
source, err := factory.CreateSource(t.Context(), lookupsource.CreateSettings{}, cfg)
204+
require.NoError(t, err)
205+
206+
val, found, err := source.Lookup(t.Context(), "dns.google")
207+
require.NoError(t, err)
208+
209+
if found {
210+
ip, ok := val.(string)
211+
assert.True(t, ok)
212+
// AAAA record should be an IPv6 address (contains colons)
213+
assert.Contains(t, ip, ":")
214+
t.Logf("AAAA lookup for dns.google: %s", ip)
215+
} else {
216+
t.Log("AAAA lookup for dns.google not found (may be network issue or no IPv6)")
217+
}
218+
}
219+
220+
func TestLookupA_MultipleResults_Integration(t *testing.T) {
221+
if testing.Short() {
222+
t.Skip("skipping integration test in short mode")
223+
}
224+
225+
factory := NewFactory()
226+
cfg := &Config{
227+
RecordType: RecordTypeA,
228+
Timeout: 5 * time.Second,
229+
MultipleResults: true,
230+
Cache: lookupsource.CacheConfig{
231+
Enabled: false,
232+
},
233+
}
234+
235+
source, err := factory.CreateSource(t.Context(), lookupsource.CreateSettings{}, cfg)
236+
require.NoError(t, err)
237+
238+
val, found, err := source.Lookup(t.Context(), "dns.google")
239+
require.NoError(t, err)
240+
241+
if !found {
242+
t.Skip("A lookup for dns.google not found (may be network issue)")
243+
}
244+
245+
result, ok := val.(string)
246+
require.True(t, ok)
247+
248+
// Multiple results must be comma-separated
249+
ips := strings.Split(result, ",")
250+
assert.Greater(t, len(ips), 1, "expected multiple IPs but got single result %q — MultipleResults may not be working", result)
251+
252+
// Each entry must be a valid IPv4 address (no colons)
253+
for _, ip := range ips {
254+
ip = strings.TrimSpace(ip)
255+
assert.NotEmpty(t, ip)
256+
assert.Contains(t, ip, ".", "expected IPv4 address, got %q", ip)
257+
assert.NotContains(t, ip, ":", "A record should not return IPv6 addresses, got %q", ip)
258+
}
259+
t.Logf("A multiple ipv4 results for dns.google (%d IPs): %s", len(ips), result)
260+
}
261+
146262
func TestLookupPTR_Integration(t *testing.T) {
147263
if testing.Short() {
148264
t.Skip("skipping integration test in short mode")

0 commit comments

Comments
 (0)