diff --git a/CHANGELOG.md b/CHANGELOG.md index 7689f059a1bd8..84ffd04b70260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - `transformprocessor`: Add new `limit` function to allow limiting the number of items in a map, such as the number of attributes in `attributes` or `resource.attributes` (#9552) - `processor/attributes`: Support attributes set by server authenticator (#9420) - `datadogexporter`: Experimental support for Exponential Histograms with delta aggregation temporality (#8350) +- `resourcedetectionprocessor`: Add "cname" and "lookup" hostname sources ### 🧰 Bug fixes 🧰 diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index 785b9b283bffc..6523285684cb2 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -26,6 +26,8 @@ processors: ### System metadata +Note: use the Docker detector (see below) if running the Collector as a Docker container. + Queries the host machine to retrieve the following resource attributes: * host.name @@ -47,8 +49,30 @@ processors: * all valid options for `hostname_sources`: * "dns" * "os" + * "cname" + * "lookup" -Note: use the Docker detector (see below) if running the Collector as a Docker container. +#### Hostname Sources + +##### dns + +The "dns" hostname source uses multiple sources to get the fully qualified domain name. First, it looks up the +host name in the local machine's `hosts` file. If that fails, it looks up the CNAME. Lastly, if that fails, +it does a reverse DNS query. Note: this hostname source may produce unreliable results on Windows. To produce +a FQDN, Windows hosts might have better results using the "lookup" hostname source, which is mentioned below. + +##### os + +The "os" hostname source provides the hostname provided by the local machine's kernel. + +##### cname + +The "cname" hostname source provides the canonical name, as provided by net.LookupCNAME in the Go standard library. +Note: this hostname source may produce unreliable results on Windows. + +##### lookup + +The "lookup" hostname source does a reverse DNS lookup of the current host's IP address. ### Docker metadata diff --git a/processor/resourcedetectionprocessor/internal/system/metadata.go b/processor/resourcedetectionprocessor/internal/system/metadata.go index da1bd55e74bda..073594ea432ac 100644 --- a/processor/resourcedetectionprocessor/internal/system/metadata.go +++ b/processor/resourcedetectionprocessor/internal/system/metadata.go @@ -15,15 +15,38 @@ package system // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" import ( + "fmt" + "net" "os" "runtime" + "strings" "github.com/Showmax/go-fqdn" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" ) -type systemMetadata interface { +// nameInfoProvider abstracts domain name resolution so it can be swapped for +// testing +type nameInfoProvider struct { + osHostname func() (string, error) + lookupCNAME func(string) (string, error) + lookupHost func(string) ([]string, error) + lookupAddr func(string) ([]string, error) +} + +// newNameInfoProvider creates a name info provider for production use, using +// DNS to resolve domain names +func newNameInfoProvider() nameInfoProvider { + return nameInfoProvider{ + osHostname: os.Hostname, + lookupCNAME: net.LookupCNAME, + lookupHost: net.LookupHost, + lookupAddr: net.LookupAddr, + } +} + +type metadataProvider interface { // Hostname returns the OS hostname Hostname() (string, error) @@ -32,18 +55,71 @@ type systemMetadata interface { // OSType returns the host operating system OSType() (string, error) + + // LookupCNAME returns the canonical name for the current host + LookupCNAME() (string, error) + + // ReverseLookupHost does a reverse DNS query on the current host's IP address + ReverseLookupHost() (string, error) +} + +type systemMetadataProvider struct { + nameInfoProvider nameInfoProvider } -type systemMetadataImpl struct{} +func newSystemMetadataProvider() metadataProvider { + return systemMetadataProvider{nameInfoProvider: newNameInfoProvider()} +} -func (*systemMetadataImpl) OSType() (string, error) { +func (systemMetadataProvider) OSType() (string, error) { return internal.GOOSToOSType(runtime.GOOS), nil } -func (*systemMetadataImpl) FQDN() (string, error) { +func (systemMetadataProvider) FQDN() (string, error) { return fqdn.FqdnHostname() } -func (*systemMetadataImpl) Hostname() (string, error) { - return os.Hostname() +func (p systemMetadataProvider) Hostname() (string, error) { + return p.nameInfoProvider.osHostname() +} + +func (p systemMetadataProvider) LookupCNAME() (string, error) { + hostname, err := p.Hostname() + if err != nil { + return "", fmt.Errorf("LookupCNAME failed to get hostname: %w", err) + } + cname, err := p.nameInfoProvider.lookupCNAME(hostname) + if err != nil { + return "", fmt.Errorf("LookupCNAME failed to get CNAME: %w", err) + } + return strings.TrimRight(cname, "."), nil +} + +func (p systemMetadataProvider) ReverseLookupHost() (string, error) { + hostname, err := p.Hostname() + if err != nil { + return "", fmt.Errorf("ReverseLookupHost failed to get hostname: %w", err) + } + return p.hostnameToDomainName(hostname) +} + +func (p systemMetadataProvider) hostnameToDomainName(hostname string) (string, error) { + ipAddresses, err := p.nameInfoProvider.lookupHost(hostname) + if err != nil { + return "", fmt.Errorf("hostnameToDomainName failed to convert hostname to IP addresses: %w", err) + } + return p.reverseLookup(ipAddresses) +} + +func (p systemMetadataProvider) reverseLookup(ipAddresses []string) (string, error) { + var err error + for _, ip := range ipAddresses { + var names []string + names, err = p.nameInfoProvider.lookupAddr(ip) + if err != nil { + continue + } + return strings.TrimRight(names[0], "."), nil + } + return "", fmt.Errorf("reverseLookup failed to convert IP addresses to name: %w", err) } diff --git a/processor/resourcedetectionprocessor/internal/system/metadata_test.go b/processor/resourcedetectionprocessor/internal/system/metadata_test.go new file mode 100644 index 0000000000000..3dba1fb2854a6 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/system/metadata_test.go @@ -0,0 +1,102 @@ +// Copyright The OpenTelemetry 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 system + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLookupCNAME_Linux(t *testing.T) { + p := fakeLinuxSystemMetadataProvider() + cname, err := p.LookupCNAME() + require.NoError(t, err) + assert.Equal(t, "my-linux-vm.abcdefghijklmnopqrstuvwxyz.xx.internal.foo.net", cname) +} + +func TestLookupCNAME_Windows(t *testing.T) { + p := fakeWindowsSystemMetadataProvider() + cname, err := p.LookupCNAME() + require.NoError(t, err) + assert.Equal(t, "my-windows-vm.abcdefghijklmnopqrstuvwxyz.xx.internal.foo.net", cname) +} + +func TestReverseLookupHost_Linux(t *testing.T) { + p := fakeLinuxSystemMetadataProvider() + fqdn, err := p.ReverseLookupHost() + require.NoError(t, err) + assert.Equal(t, "my-linux-vm.internal.foo.net", fqdn) +} + +func TestReverseLookupHost_Windows(t *testing.T) { + p := fakeWindowsSystemMetadataProvider() + fqdn, err := p.ReverseLookupHost() + require.NoError(t, err) + assert.Equal(t, "my-windows-vm.abcdefghijklmnopqrstuvwxyz.xx.internal.foo.net", fqdn) +} + +func fakeLinuxSystemMetadataProvider() *systemMetadataProvider { + return &systemMetadataProvider{ + nameInfoProvider: fakeLinuxNameInfoProvider(), + } +} + +func fakeWindowsSystemMetadataProvider() *systemMetadataProvider { + return &systemMetadataProvider{ + nameInfoProvider: fakeWindowsNameInfoProvider(), + } +} + +func fakeLinuxNameInfoProvider() nameInfoProvider { + return nameInfoProvider{ + osHostname: func() (string, error) { + return "my-linux-vm", nil + }, + lookupCNAME: func(s string) (string, error) { + return "my-linux-vm.abcdefghijklmnopqrstuvwxyz.xx.internal.foo.net.", nil + }, + lookupHost: func(s string) ([]string, error) { + return []string{"172.24.0.4"}, nil + }, + lookupAddr: func(s string) ([]string, error) { + return []string{"my-linux-vm.internal.foo.net."}, nil + }, + } +} + +func fakeWindowsNameInfoProvider() nameInfoProvider { + fqdn := "my-windows-vm.abcdefghijklmnopqrstuvwxyz.xx.internal.foo.net." + return nameInfoProvider{ + osHostname: func() (string, error) { + return "my-windows-vm", nil + }, + lookupCNAME: func(s string) (string, error) { + return fqdn, nil + }, + lookupHost: func(s string) ([]string, error) { + return []string{"ffff::0000:1111:2222:3333%Ethernet", "1.2.3.4"}, nil + }, + lookupAddr: func(s string) ([]string, error) { + if strings.HasSuffix(s, "%Ethernet") { + return nil, fmt.Errorf("lookup %s: unrecognized address", s) + } + return []string{fqdn}, nil + }, + } +} diff --git a/processor/resourcedetectionprocessor/internal/system/system.go b/processor/resourcedetectionprocessor/internal/system/system.go index 1d5eb9c446ace..892d6e81fb1eb 100644 --- a/processor/resourcedetectionprocessor/internal/system/system.go +++ b/processor/resourcedetectionprocessor/internal/system/system.go @@ -33,15 +33,17 @@ const ( ) var hostnameSourcesMap = map[string]func(*Detector) (string, error){ - "dns": getFQDN, - "os": getHostname, + "os": getHostname, + "dns": getFQDN, + "cname": lookupCNAME, + "lookup": reverseLookupHost, } var _ internal.Detector = (*Detector)(nil) // Detector is a system metadata detector type Detector struct { - provider systemMetadata + provider metadataProvider logger *zap.Logger hostnameSources []string } @@ -52,7 +54,8 @@ func NewDetector(p component.ProcessorCreateSettings, dcfg internal.DetectorConf if len(cfg.HostnameSources) == 0 { cfg.HostnameSources = []string{"dns", "os"} } - return &Detector{provider: &systemMetadataImpl{}, logger: p.Logger, hostnameSources: cfg.HostnameSources}, nil + + return &Detector{provider: newSystemMetadataProvider(), logger: p.Logger, hostnameSources: cfg.HostnameSources}, nil } // Detect detects system metadata and returns a resource with the available ones @@ -78,7 +81,7 @@ func (d *Detector) Detect(_ context.Context) (resource pcommon.Resource, schemaU d.logger.Debug(err.Error()) } - return res, "", errors.New("all hostname sources are failed to get hostname") + return res, "", errors.New("all hostname sources failed to get hostname") } // getHostname returns OS hostname @@ -94,7 +97,23 @@ func getHostname(d *Detector) (string, error) { func getFQDN(d *Detector) (string, error) { hostname, err := d.provider.FQDN() if err != nil { - return "", fmt.Errorf("failed getting FQDN: %w", err) + return "", fmt.Errorf("getFQDN failed getting FQDN: %w", err) + } + return hostname, nil +} + +func lookupCNAME(d *Detector) (string, error) { + cname, err := d.provider.LookupCNAME() + if err != nil { + return "", fmt.Errorf("lookupCNAME failed to get CNAME: %w", err) + } + return cname, nil +} + +func reverseLookupHost(d *Detector) (string, error) { + hostname, err := d.provider.ReverseLookupHost() + if err != nil { + return "", fmt.Errorf("reverseLookupHost failed to lookup host: %w", err) } return hostname, nil } diff --git a/processor/resourcedetectionprocessor/internal/system/system_test.go b/processor/resourcedetectionprocessor/internal/system/system_test.go index 93642b026b443..075266f95ad2c 100644 --- a/processor/resourcedetectionprocessor/internal/system/system_test.go +++ b/processor/resourcedetectionprocessor/internal/system/system_test.go @@ -48,6 +48,16 @@ func (m *mockMetadata) OSType() (string, error) { return args.String(0), args.Error(1) } +func (m *mockMetadata) LookupCNAME() (string, error) { + args := m.MethodCalled("LookupCNAME") + return args.String(0), args.Error(1) +} + +func (m *mockMetadata) ReverseLookupHost() (string, error) { + args := m.MethodCalled("ReverseLookupHost") + return args.String(0), args.Error(1) +} + func TestNewDetector(t *testing.T) { tests := []struct { name string