Skip to content

Commit 999222b

Browse files
mx-psialbertvaka
andauthored
Push host metadata on startup (#65)
* Push host metadata on startup * Have the same behavior as the Datadog Agent with tags Only env should be added to host tags if set. * Apply suggestions from code review Co-authored-by: Albert Vaca Cintora <[email protected]> * Run on separate goroutine and add exponential backoff * Set User-Agent for zorkian's client and for the host metadata * Disable Fast Fallback Co-authored-by: Albert Vaca Cintora <[email protected]>
1 parent 23c1a9d commit 999222b

File tree

6 files changed

+174
-38
lines changed

6 files changed

+174
-38
lines changed

exporter/datadogexporter/config.go

+4-16
Original file line numberDiff line numberDiff line change
@@ -98,23 +98,11 @@ type TagsConfig struct {
9898
}
9999

100100
// GetTags gets the default tags extracted from the configuration
101-
func (t *TagsConfig) GetTags(addHost bool) []string {
102-
tags := make([]string, 0, 4)
101+
func (t *TagsConfig) GetTags() []string {
102+
tags := make([]string, 0, len(t.Tags)+1)
103103

104-
vars := map[string]string{
105-
"env": t.Env,
106-
"service": t.Service,
107-
"version": t.Version,
108-
}
109-
110-
if addHost {
111-
vars["host"] = t.Hostname
112-
}
113-
114-
for name, val := range vars {
115-
if val != "" {
116-
tags = append(tags, fmt.Sprintf("%s:%s", name, val))
117-
}
104+
if t.Env != "none" {
105+
tags = append(tags, fmt.Sprintf("env:%s", t.Env))
118106
}
119107

120108
tags = append(tags, t.Tags...)

exporter/datadogexporter/config_test.go

+12-9
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,27 @@ func TestLoadConfig(t *testing.T) {
8484

8585
func TestTags(t *testing.T) {
8686
tc := TagsConfig{
87-
Hostname: "customhost",
88-
Env: "customenv",
89-
Service: "customservice",
90-
Version: "customversion",
91-
Tags: []string{"key1:val1", "key2:val2"},
87+
// environment should be picked up if it is not 'none'
88+
Env: "customenv",
89+
90+
// these should be ignored;
91+
// they are used only on trace translation
92+
Service: "customservice",
93+
Version: "customversion",
94+
Tags: []string{"key1:val1", "key2:val2"},
9295
}
9396

9497
assert.ElementsMatch(t,
9598
[]string{
96-
"host:customhost",
9799
"env:customenv",
98-
"service:customservice",
99-
"version:customversion",
100100
"key1:val1",
101101
"key2:val2",
102102
},
103-
tc.GetTags(true), // get host
103+
tc.GetTags(),
104104
)
105+
106+
tc.Env = "none"
107+
assert.ElementsMatch(t, tc.GetTags(), tc.Tags)
105108
}
106109

107110
// TestOverrideMetricsURL tests that the metrics URL is overridden

exporter/datadogexporter/factory.go

+30
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ package datadogexporter
1515

1616
import (
1717
"context"
18+
"time"
1819

1920
"go.opentelemetry.io/collector/component"
2021
"go.opentelemetry.io/collector/config/configmodels"
2122
"go.opentelemetry.io/collector/config/confignet"
2223
"go.opentelemetry.io/collector/exporter/exporterhelper"
24+
"go.uber.org/zap"
2325
)
2426

2527
const (
@@ -28,6 +30,9 @@ const (
2830

2931
// DefaultSite is the default site of the Datadog intake to send data to
3032
DefaultSite = "datadoghq.com"
33+
34+
// maxRetries is the maximum number of retries for pushing host metadata
35+
maxRetries = 5
3136
)
3237

3338
// NewFactory creates a Datadog exporter factory
@@ -86,6 +91,31 @@ func createMetricsExporter(
8691
return nil, err
8792
}
8893

94+
go func() {
95+
// Send host metadata
96+
var sent bool
97+
wait := 1 * time.Second
98+
metadata := getHostMetadata(cfg)
99+
for i := 0; i < maxRetries; i++ {
100+
err := exp.pushHostMetadata(metadata)
101+
if err != nil {
102+
params.Logger.Warn("Sending host metadata failed", zap.Error(err))
103+
} else {
104+
sent = true
105+
params.Logger.Info("Sent host metadata", zap.Int("numRetries", i))
106+
break
107+
}
108+
109+
time.Sleep(wait)
110+
wait = 2 * wait
111+
}
112+
113+
if !sent {
114+
// log and continue without metadata
115+
params.Logger.Error("Could not send host metadata", zap.Int("numRetries", maxRetries))
116+
}
117+
}()
118+
89119
return exporterhelper.NewMetricsExporter(
90120
cfg,
91121
exp.PushMetricsData,

exporter/datadogexporter/host.go

+73-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@
1414

1515
package datadogexporter
1616

17-
import "os"
17+
import (
18+
"fmt"
19+
"os"
20+
)
21+
22+
const (
23+
opentelemetryFlavor = "opentelemetry-collector"
24+
opentelemetryVersion = "alpha"
25+
)
26+
27+
var (
28+
userAgent = fmt.Sprintf("%s/%s", opentelemetryFlavor, opentelemetryVersion)
29+
)
1830

1931
// GetHost gets the hostname according to configuration.
2032
// It gets the configuration hostname and if
@@ -30,3 +42,63 @@ func GetHost(cfg *Config) *string {
3042
}
3143
return &host
3244
}
45+
46+
// hostMetadata includes metadata about the host tags,
47+
// host aliases and identifies the host as an OpenTelemetry host
48+
type hostMetadata struct {
49+
// Meta includes metadata about the host.
50+
Meta *meta `json:"meta"`
51+
52+
// InternalHostname is the canonical hostname
53+
InternalHostname string `json:"internalHostname"`
54+
55+
// Version is the OpenTelemetry Collector version.
56+
// This is used for correctly identifying the Collector in the backend,
57+
// and for telemetry purposes.
58+
Version string `json:"otel_version"`
59+
60+
// Flavor is always set to "opentelemetry-collector".
61+
// It is used for telemetry purposes in the backend.
62+
Flavor string `json:"agent-flavor"`
63+
64+
// Tags includes the host tags
65+
Tags *hostTags `json:"host-tags"`
66+
}
67+
68+
// hostTags are the host tags.
69+
// Currently only system (configuration) tags are considered.
70+
type hostTags struct {
71+
// System are host tags set in the configuration
72+
System []string `json:"system,omitempty"`
73+
}
74+
75+
// meta includes metadata about the host aliases
76+
type meta struct {
77+
// InstanceID is the EC2 instance id the Collector is running on, if available
78+
InstanceID string `json:"instance-id,omitempty"`
79+
80+
// EC2Hostname is the hostname from the EC2 metadata API
81+
EC2Hostname string `json:"ec2-hostname,omitempty"`
82+
83+
// Hostname is the canonical hostname
84+
Hostname string `json:"hostname"`
85+
86+
// SocketHostname is the OS hostname
87+
SocketHostname string `json:"socket-hostname"`
88+
89+
// HostAliases are other available host names
90+
HostAliases []string `json:"host-aliases,omitempty"`
91+
}
92+
93+
func getHostMetadata(cfg *Config) hostMetadata {
94+
host := *GetHost(cfg)
95+
return hostMetadata{
96+
InternalHostname: host,
97+
Flavor: opentelemetryFlavor,
98+
Version: opentelemetryVersion,
99+
Tags: &hostTags{cfg.TagsConfig.GetTags()},
100+
Meta: &meta{
101+
Hostname: host,
102+
},
103+
}
104+
}

exporter/datadogexporter/metrics_exporter.go

+53-10
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
package datadogexporter
1616

1717
import (
18+
"bytes"
1819
"context"
20+
"encoding/json"
21+
"fmt"
22+
"net"
23+
"net/http"
24+
"time"
1925

2026
"go.opentelemetry.io/collector/consumer/pdata"
2127
"go.uber.org/zap"
@@ -26,23 +32,65 @@ type metricsExporter struct {
2632
logger *zap.Logger
2733
cfg *Config
2834
client *datadog.Client
29-
tags []string
35+
}
36+
37+
func newHTTPClient() *http.Client {
38+
return &http.Client{
39+
Timeout: 20 * time.Second,
40+
Transport: &http.Transport{
41+
Proxy: http.ProxyFromEnvironment,
42+
DialContext: (&net.Dialer{
43+
// Disable RFC 6555 Fast Fallback ("Happy Eyeballs")
44+
FallbackDelay: -1 * time.Nanosecond,
45+
}).DialContext,
46+
MaxIdleConns: 100,
47+
// Not supported by intake
48+
ForceAttemptHTTP2: false,
49+
},
50+
}
3051
}
3152

3253
func newMetricsExporter(logger *zap.Logger, cfg *Config) (*metricsExporter, error) {
3354
client := datadog.NewClient(cfg.API.Key, "")
55+
client.ExtraHeader["User-Agent"] = userAgent
3456
client.SetBaseUrl(cfg.Metrics.TCPAddr.Endpoint)
57+
client.HttpClient = newHTTPClient()
3558

36-
// Calculate tags at startup
37-
tags := cfg.TagsConfig.GetTags(false)
59+
return &metricsExporter{logger, cfg, client}, nil
60+
}
61+
62+
// pushHostMetadata sends a host metadata payload to the "/intake" endpoint
63+
func (exp *metricsExporter) pushHostMetadata(metadata hostMetadata) error {
64+
path := exp.cfg.Metrics.TCPAddr.Endpoint + "/intake"
65+
buf, _ := json.Marshal(metadata)
66+
req, _ := http.NewRequest(http.MethodPost, path, bytes.NewBuffer(buf))
67+
req.Header.Set("DD-API-KEY", exp.cfg.API.Key)
68+
req.Header.Set("Content-Type", "application/json")
69+
req.Header.Set("User-Agent", userAgent)
70+
client := newHTTPClient()
71+
resp, err := client.Do(req)
72+
73+
if err != nil {
74+
return err
75+
}
3876

39-
return &metricsExporter{logger, cfg, client, tags}, nil
77+
defer resp.Body.Close()
78+
79+
if resp.StatusCode >= 400 {
80+
return fmt.Errorf(
81+
"'%d - %s' error when sending metadata payload to %s",
82+
resp.StatusCode,
83+
resp.Status,
84+
path,
85+
)
86+
}
87+
88+
return nil
4089
}
4190

4291
func (exp *metricsExporter) processMetrics(metrics []datadog.Metric) {
4392
addNamespace := exp.cfg.Metrics.Namespace != ""
4493
overrideHostname := exp.cfg.Hostname != ""
45-
addTags := len(exp.tags) > 0
4694

4795
for i := range metrics {
4896
if addNamespace {
@@ -53,11 +101,6 @@ func (exp *metricsExporter) processMetrics(metrics []datadog.Metric) {
53101
if overrideHostname || metrics[i].GetHost() == "" {
54102
metrics[i].Host = GetHost(exp.cfg)
55103
}
56-
57-
if addTags {
58-
metrics[i].Tags = append(metrics[i].Tags, exp.tags...)
59-
}
60-
61104
}
62105
}
63106

exporter/datadogexporter/metrics_exporter_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ func TestProcessMetrics(t *testing.T) {
5858
0,
5959
[]string{"key2:val2"},
6060
),
61-
}
61+
}
6262

6363
exp.processMetrics(metrics)
6464

6565
assert.Equal(t, "test_host", *metrics[0].Host)
6666
assert.Equal(t, "test.metric_name", *metrics[0].Metric)
6767
assert.ElementsMatch(t,
68-
[]string{"key:val", "env:test_env", "key2:val2"},
68+
[]string{"key2:val2"},
6969
metrics[0].Tags,
7070
)
7171

0 commit comments

Comments
 (0)