internal/datadog/hostmetadata/internal/ec2/ec2.go (144 lines of code) (raw):
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package ec2 contains the AWS EC2 hostname provider
package ec2 // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/datadog/hostmetadata/internal/ec2"
import (
"context"
"fmt"
"io"
"strings"
"sync"
"github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes/source"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"go.uber.org/zap"
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/datadog/hostmetadata/provider"
ec2provider "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/aws/ec2"
)
var defaultPrefixes = [3]string{"ip-", "domu", "ec2amaz-"}
type HostInfo struct {
InstanceID string
EC2Hostname string
EC2Tags []string
}
// isDefaultHostname checks if a hostname is an EC2 default
func isDefaultHostname(hostname string) bool {
for _, val := range defaultPrefixes {
if strings.HasPrefix(hostname, val) {
return true
}
}
return false
}
// GetHostInfo gets the hostname info from EC2 metadata
func GetHostInfo(ctx context.Context, logger *zap.Logger) (hostInfo *HostInfo) {
hostInfo = &HostInfo{}
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
logger.Warn("Failed to build AWS config", zap.Error(err))
return
}
client := imds.NewFromConfig(cfg)
// Check if metadata service is available by trying to retrieve instance ID
_, err = client.GetMetadata(ctx, &imds.GetMetadataInput{
Path: "instance-id",
})
if err != nil {
logger.Debug("EC2 Metadata service is not available", zap.Error(err))
return
}
idDoc, err := client.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
if err == nil {
hostInfo.InstanceID = idDoc.InstanceID
} else {
logger.Warn("Failed to get EC2 instance id document", zap.Error(err))
}
metadataOutput, err := client.GetMetadata(ctx, &imds.GetMetadataInput{Path: "hostname"})
if err != nil {
logger.Warn("Failed to retrieve EC2 hostname", zap.Error(err))
} else {
defer metadataOutput.Content.Close()
hostnameBytes, readErr := io.ReadAll(metadataOutput.Content)
if readErr != nil {
logger.Warn("Failed to read EC2 hostname content", zap.Error(readErr))
} else {
hostInfo.EC2Hostname = string(hostnameBytes)
}
}
return
}
func (hi *HostInfo) GetHostname(_ *zap.Logger) string {
if isDefaultHostname(hi.EC2Hostname) {
return hi.InstanceID
}
return hi.EC2Hostname
}
var (
_ source.Provider = (*Provider)(nil)
_ provider.ClusterNameProvider = (*Provider)(nil)
)
type Provider struct {
once sync.Once
hostInfo HostInfo
detector ec2provider.Provider
logger *zap.Logger
}
func NewProvider(logger *zap.Logger) (*Provider, error) {
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
return nil, err
}
return &Provider{
logger: logger,
detector: ec2provider.NewProvider(cfg),
}, nil
}
func (p *Provider) fillHostInfo(ctx context.Context) {
p.once.Do(func() { p.hostInfo = *GetHostInfo(ctx, p.logger) })
}
func (p *Provider) Source(ctx context.Context) (source.Source, error) {
p.fillHostInfo(ctx)
if p.hostInfo.InstanceID == "" {
return source.Source{}, fmt.Errorf("instance ID is unavailable")
}
return source.Source{Kind: source.HostnameKind, Identifier: p.hostInfo.InstanceID}, nil
}
// instanceTags gets the EC2 tags for the current instance.
func (p *Provider) instanceTags(ctx context.Context) (*ec2.DescribeTagsOutput, error) {
// Get EC2 metadata to find the region and instance ID
meta, err := p.detector.Get(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get metadata: %w", err)
}
// Get the EC2 tags for the instance id.
// Similar to:
// - https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/39dbc1ac8/processor/resourcedetectionprocessor/internal/aws/ec2/ec2.go#L118-L151
// - https://github.com/DataDog/datadog-agent/blob/1b4afdd6a03e8fabcc169b924931b2bb8935dab9/pkg/util/ec2/ec2_tags.go#L104-L134
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(meta.Region),
)
if err != nil {
return nil, fmt.Errorf("failed to load AWS config: %w", err)
}
client := ec2.NewFromConfig(cfg)
return client.DescribeTags(ctx, &ec2.DescribeTagsInput{
Filters: []types.Filter{{
Name: aws.String("resource-id"),
Values: []string{meta.InstanceID},
}},
})
}
// clusterNameFromTags gets the AWS EC2 Cluster name from the tags on an EC2 instance.
func clusterNameFromTags(ec2Tags *ec2.DescribeTagsOutput) (string, error) {
// Similar to:
// - https://github.com/DataDog/datadog-agent/blob/1b4afdd6a03/pkg/util/ec2/ec2.go#L256-L271
const clusterNameTagPrefix = "kubernetes.io/cluster/"
for _, tag := range ec2Tags.Tags {
if strings.HasPrefix(*tag.Key, clusterNameTagPrefix) {
if len(*tag.Key) == len(clusterNameTagPrefix) {
return "", fmt.Errorf("missing cluster name in %q tag", *tag.Key)
}
return strings.Split(*tag.Key, "/")[2], nil
}
}
return "", fmt.Errorf("no tag found with prefix %q", clusterNameTagPrefix)
}
// ClusterName gets the cluster name from an AWS EC2 machine.
func (p *Provider) ClusterName(ctx context.Context) (string, error) {
ec2Tags, err := p.instanceTags(ctx)
if err != nil {
return "", fmt.Errorf("failed to get EC2 instance tags: %w", err)
}
return clusterNameFromTags(ec2Tags)
}
func (p *Provider) HostInfo() *HostInfo {
p.fillHostInfo(context.Background())
return &p.hostInfo
}