Skip to content

Commit

Permalink
internal/hostname: Add tracer side hostname detection (DataDog#1712)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajgajg1134 authored Feb 21, 2023
1 parent 0ef937c commit 3ccf65d
Show file tree
Hide file tree
Showing 23 changed files with 1,382 additions and 7 deletions.
4 changes: 2 additions & 2 deletions checkcopyright.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
func main() {
var missing bool
// copyrightRegexp matches years or year ranges like "2016", "2016-2019",
// "2016,2018-2020" in the copyright header.
copyrightRegexp := regexp.MustCompile(`// Copyright 20[0-9]{2}[0-9,\-]* Datadog, Inc.`)
// "2016,2018-2020", "2016-present" in the copyright header.
copyrightRegexp := regexp.MustCompile(`// Copyright 20[0-9]{2}[0-9,\-(present)]* Datadog, Inc.`)
generatedRegexp := regexp.MustCompile(`Code generated by.+DO NOT EDIT`)

if err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
Expand Down
6 changes: 6 additions & 0 deletions ddtrace/tracer/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ type config struct {

// enabled reports whether tracing is enabled.
enabled bool

// disableHostnameDetection specifies whether the tracer should disable hostname detection.
disableHostnameDetection bool
}

// HasFeature reports whether feature f is enabled.
Expand Down Expand Up @@ -227,6 +230,9 @@ func newConfig(opts ...StartOption) *config {
}
}
if c.agentURL.Scheme == "unix" {
// If we're connecting over UDS we can just rely on the agent to provide the hostname
log.Debug("connecting to agent over unix, do not set hostname on any traces")
c.disableHostnameDetection = true
c.httpClient = udsClient(c.agentURL.Path)
c.agentURL = &url.URL{
Scheme: "http",
Expand Down
15 changes: 10 additions & 5 deletions ddtrace/tracer/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,11 +631,13 @@ func (s *span) Format(f fmt.State, c rune) {
}

const (
keySamplingPriority = "_sampling_priority_v1"
keySamplingPriorityRate = "_dd.agent_psr"
keyDecisionMaker = "_dd.p.dm"
keyServiceHash = "_dd.dm.service_hash"
keyOrigin = "_dd.origin"
keySamplingPriority = "_sampling_priority_v1"
keySamplingPriorityRate = "_dd.agent_psr"
keyDecisionMaker = "_dd.p.dm"
keyServiceHash = "_dd.dm.service_hash"
keyOrigin = "_dd.origin"
// keyHostname can be used to override the agent's hostname detection when using `WithHostname`. Not to be confused with keyTracerHostname
// which is set via auto-detection.
keyHostname = "_dd.hostname"
keyRulesSamplerAppliedRate = "_dd.rule_psr"
keyRulesSamplerLimiterRate = "_dd.limit_psr"
Expand All @@ -654,6 +656,9 @@ const (
keySingleSpanSamplingMPS = "_dd.span_sampling.max_per_second"
// keyPropagatedUserID holds the propagated user identifier, if user id propagation is enabled.
keyPropagatedUserID = "_dd.p.usr.id"

//keyTracerHostname holds the tracer detected hostname, only present when not connected over UDS to agent.
keyTracerHostname = "_dd.tracer_hostname"
)

// The following set of tags is used for user monitoring and set through calls to span.SetUser().
Expand Down
3 changes: 3 additions & 0 deletions ddtrace/tracer/spancontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,9 @@ func (t *trace) finishedOne(s *span) {
if !ok {
return
}
if hn := tr.hostname(); hn != "" {
s.setMeta(keyTracerHostname, hn)
}
// we have a tracer that can receive completed traces.
atomic.AddUint32(&tr.spansFinished, uint32(len(t.spans)))
tr.pushTrace(&finishedTrace{
Expand Down
9 changes: 9 additions & 0 deletions ddtrace/tracer/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/hostname"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/remoteconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/traceprof"
Expand Down Expand Up @@ -138,6 +139,7 @@ func Start(opts ...StartOption) {
cfg.HTTP = t.config.httpClient
cfg.ServiceName = t.config.serviceName
appsec.Start(appsec.WithRCConfig(cfg))
hostname.Get() // Prime the hostname cache
}

// Stop stops the started tracer. Subsequent calls are valid but become no-op.
Expand Down Expand Up @@ -623,3 +625,10 @@ func startExecutionTracerTask(ctx gocontext.Context, span *span) (gocontext.Cont
rt.Log(ctx, "span id", strconv.FormatUint(span.SpanID, 10))
return ctx, task.End
}

func (t *tracer) hostname() string {
if !t.config.disableHostnameDetection {
return hostname.Get()
}
return ""
}
63 changes: 63 additions & 0 deletions internal/hostname/azure/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package azure

import (
"context"
"encoding/json"
"fmt"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/internal/hostname/cachedfetch"
"gopkg.in/DataDog/dd-trace-go.v1/internal/hostname/httputils"
"gopkg.in/DataDog/dd-trace-go.v1/internal/hostname/validate"
)

// declare these as vars not const to ease testing
var (
metadataURL = "http://169.254.169.254"
timeout = 300 * time.Millisecond

// CloudProviderName contains the inventory name of for Azure
CloudProviderName = "Azure"
)

func getResponse(ctx context.Context, url string) (string, error) {
return httputils.Get(ctx, url, map[string]string{"Metadata": "true"}, timeout)
}

// GetHostname returns hostname based on Azure instance metadata.
func GetHostname(ctx context.Context) (string, error) {
metadataJSON, err := instanceMetaFetcher.Fetch(ctx)
if err != nil {
return "", err
}

var metadata struct {
VMID string
}
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
return "", fmt.Errorf("failed to parse Azure instance metadata: %s", err)
}

if err := validate.ValidHostname(metadata.VMID); err != nil {
return "", err
}

return metadata.VMID, nil
}

var instanceMetaFetcher = cachedfetch.Fetcher{
Name: "Azure Instance Metadata",
Attempt: func(ctx context.Context) (string, error) {
metadataJSON, err := getResponse(ctx,
metadataURL+"/metadata/instance/compute?api-version=2017-08-01")
if err != nil {
return "", fmt.Errorf("failed to get Azure instance metadata: %s", err)
}
return metadataJSON, nil
},
}
70 changes: 70 additions & 0 deletions internal/hostname/azure/azure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package azure

import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetHostname(t *testing.T) {
ctx := context.Background()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{
"name": "vm-name",
"resourceGroupName": "my-resource-group",
"subscriptionId": "2370ac56-5683-45f8-a2d4-d1054292facb",
"vmId": "b33fa46-6aff-4dfa-be0a-9e922ca3ac6d"
}`)
}))
defer ts.Close()
metadataURL = ts.URL

cases := []struct {
value string
err bool
}{
{"b33fa46-6aff-4dfa-be0a-9e922ca3ac6d", false},
}

for _, tt := range cases {
hostname, err := GetHostname(ctx)
assert.Equal(t, tt.value, hostname)
assert.Equal(t, tt.err, err != nil)
}
}

func TestGetHostnameWithInvalidMetadata(t *testing.T) {
ctx := context.Background()

for _, response := range []string{"", "!"} {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, fmt.Sprintf(`{
"name": "%s",
"resourceGroupName": "%s",
"subscriptionId": "%s",
"vmId": "%s"
}`, response, response, response, response))
}))
metadataURL = ts.URL

t.Run(fmt.Sprintf("with response '%s'", response), func(t *testing.T) {
hostname, err := GetHostname(ctx)
assert.Empty(t, hostname)
assert.NotNil(t, err)
})

ts.Close()
}
}
86 changes: 86 additions & 0 deletions internal/hostname/cachedfetch/fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

// This file is pulled from datadog-agent/pkg/util/cachedfetch changing the logger and using strings only

// Package cachedfetch provides a read-through cache for fetched values.
package cachedfetch

import (
"context"
"sync"

"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

// Fetcher supports fetching a value, such as from a cloud service API. An
// attempt is made to fetch the value on each call to Fetch, but if that
// attempt fails then a cached value from the last successful attempt is
// returned, if such a value exists. This helps the agent to "ride out"
// temporary failures in cloud APIs while still fetching fresh data when those
// APIs are functioning properly. Cached values do not expire.
//
// Callers should instantiate one fetcher per piece of data required.
type Fetcher struct {
// function that attempts to fetch the value
Attempt func(context.Context) (string, error)

// the name of the thing being fetched, used in the default log message. At
// least one of Name and LogFailure must be non-nil.
Name string

// function to log a fetch failure, given the error and the last successful
// value. This function is not called if there is no last successful value.
// If left at its zero state, a default log message will be generated, using
// Name.
LogFailure func(error, interface{})

// previous successfully fetched value
lastValue interface{}

// mutex to protect access to lastValue
sync.Mutex
}

// Fetch attempts to fetch the value, returning the result or the last successful
// value, or an error if no attempt has ever been successful. No special handling
// is included for the Context: both context.Cancelled and context.DeadlineExceeded
// are handled like any other error by returning the cached value.
//
// This can be called from multiple goroutines, in which case it will call Attempt
// concurrently.
func (f *Fetcher) Fetch(ctx context.Context) (string, error) {
value, err := f.Attempt(ctx)
if err == nil {
f.Lock()
f.lastValue = value
f.Unlock()
return value, nil
}

f.Lock()
lastValue := f.lastValue
f.Unlock()

if lastValue == nil {
// attempt was never successful
return value, err
}

if f.LogFailure == nil {
log.Debug("Unable to get %s; returning cached value instead", f.Name)
} else {
f.LogFailure(err, lastValue)
}

return lastValue.(string), nil
}

// Reset resets the cached value (used for testing)
func (f *Fetcher) Reset() {
f.Lock()
f.lastValue = nil
f.Unlock()
}
Loading

0 comments on commit 3ccf65d

Please sign in to comment.