metrics_exporter_test.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. // Copyright The OpenTelemetry Authors
  2. // SPDX-License-Identifier: Apache-2.0
  3. package datadogexporter
  4. import (
  5. "bytes"
  6. "compress/gzip"
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "sync"
  12. "testing"
  13. "time"
  14. "github.com/DataDog/agent-payload/v5/gogen"
  15. pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/trace"
  16. "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
  17. "github.com/DataDog/opentelemetry-mapping-go/pkg/inframetadata"
  18. "github.com/DataDog/opentelemetry-mapping-go/pkg/inframetadata/payload"
  19. "github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes/source"
  20. "github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/metrics"
  21. "github.com/stretchr/testify/assert"
  22. "github.com/stretchr/testify/require"
  23. "go.opentelemetry.io/collector/config/confignet"
  24. "go.opentelemetry.io/collector/exporter/exportertest"
  25. "go.opentelemetry.io/collector/pdata/pcommon"
  26. "go.opentelemetry.io/collector/pdata/pmetric"
  27. conventions "go.opentelemetry.io/collector/semconv/v1.6.1"
  28. "go.uber.org/zap"
  29. "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/testutil"
  30. )
  31. func TestNewExporter(t *testing.T) {
  32. if !isMetricExportV2Enabled() {
  33. require.NoError(t, enableNativeMetricExport())
  34. defer require.NoError(t, enableZorkianMetricExport())
  35. }
  36. server := testutil.DatadogServerMock()
  37. defer server.Close()
  38. cfg := &Config{
  39. API: APIConfig{
  40. Key: "ddog_32_characters_long_api_key1",
  41. },
  42. Metrics: MetricsConfig{
  43. TCPAddr: confignet.TCPAddr{
  44. Endpoint: server.URL,
  45. },
  46. DeltaTTL: 3600,
  47. HistConfig: HistogramConfig{
  48. Mode: HistogramModeDistributions,
  49. SendAggregations: false,
  50. },
  51. SumConfig: SumConfig{
  52. CumulativeMonotonicMode: CumulativeMonotonicSumModeToDelta,
  53. },
  54. },
  55. }
  56. params := exportertest.NewNopCreateSettings()
  57. f := NewFactory()
  58. // The client should have been created correctly
  59. exp, err := f.CreateMetricsExporter(context.Background(), params, cfg)
  60. require.NoError(t, err)
  61. assert.NotNil(t, exp)
  62. testMetrics := pmetric.NewMetrics()
  63. testutil.TestMetrics.CopyTo(testMetrics)
  64. err = exp.ConsumeMetrics(context.Background(), testMetrics)
  65. require.NoError(t, err)
  66. assert.Equal(t, len(server.MetadataChan), 0)
  67. cfg.HostMetadata.Enabled = true
  68. cfg.HostMetadata.HostnameSource = HostnameSourceFirstResource
  69. testMetrics = pmetric.NewMetrics()
  70. testutil.TestMetrics.CopyTo(testMetrics)
  71. err = exp.ConsumeMetrics(context.Background(), testMetrics)
  72. require.NoError(t, err)
  73. body := <-server.MetadataChan
  74. var recvMetadata payload.HostMetadata
  75. err = json.Unmarshal(body, &recvMetadata)
  76. require.NoError(t, err)
  77. assert.Equal(t, recvMetadata.InternalHostname, "custom-hostname")
  78. }
  79. func Test_metricsExporter_PushMetricsData(t *testing.T) {
  80. if !isMetricExportV2Enabled() {
  81. require.NoError(t, enableNativeMetricExport())
  82. t.Cleanup(func() { require.NoError(t, enableZorkianMetricExport()) })
  83. }
  84. attrs := map[string]string{
  85. conventions.AttributeDeploymentEnvironment: "dev",
  86. "custom_attribute": "custom_value",
  87. }
  88. tests := []struct {
  89. metrics pmetric.Metrics
  90. source source.Source
  91. hostTags []string
  92. histogramMode HistogramMode
  93. expectedSeries map[string]any
  94. expectedSketchPayload *gogen.SketchPayload
  95. expectedErr error
  96. expectedStats []*pb.ClientStatsPayload
  97. }{
  98. {
  99. metrics: createTestMetrics(attrs),
  100. source: source.Source{
  101. Kind: source.HostnameKind,
  102. Identifier: "test-host",
  103. },
  104. histogramMode: HistogramModeNoBuckets,
  105. hostTags: []string{"key1:value1", "key2:value2"},
  106. expectedErr: errors.New("no buckets mode and no send count sum are incompatible"),
  107. },
  108. {
  109. metrics: createTestMetrics(attrs),
  110. source: source.Source{
  111. Kind: source.HostnameKind,
  112. Identifier: "test-host",
  113. },
  114. histogramMode: HistogramModeCounters,
  115. hostTags: []string{"key1:value1", "key2:value2"},
  116. expectedSeries: map[string]any{
  117. "series": []any{
  118. map[string]any{
  119. "metric": "int.gauge",
  120. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(222)}},
  121. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  122. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  123. "tags": []any{"env:dev"},
  124. },
  125. map[string]any{
  126. "metric": "otel.system.filesystem.utilization",
  127. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(333)}},
  128. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  129. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  130. "tags": []any{"env:dev"},
  131. },
  132. map[string]any{
  133. "metric": "double.histogram.bucket",
  134. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(2)}},
  135. "type": float64(datadogV2.METRICINTAKETYPE_COUNT),
  136. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  137. "tags": []any{"lower_bound:-inf", "upper_bound:0", "env:dev"},
  138. },
  139. map[string]any{
  140. "metric": "double.histogram.bucket",
  141. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(18)}},
  142. "type": float64(datadogV2.METRICINTAKETYPE_COUNT),
  143. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  144. "tags": []any{"lower_bound:0", "upper_bound:inf", "env:dev"},
  145. },
  146. map[string]any{
  147. "metric": "system.disk.in_use",
  148. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(333)}},
  149. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  150. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  151. "tags": []any{"env:dev"},
  152. },
  153. map[string]any{
  154. "metric": "otel.datadog_exporter.metrics.running",
  155. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(1)}},
  156. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  157. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  158. "tags": []any{"version:latest", "command:otelcol"},
  159. },
  160. },
  161. },
  162. },
  163. {
  164. metrics: createTestMetrics(attrs),
  165. source: source.Source{
  166. Kind: source.HostnameKind,
  167. Identifier: "test-host",
  168. },
  169. histogramMode: HistogramModeDistributions,
  170. hostTags: []string{"key1:value1", "key2:value2"},
  171. expectedSeries: map[string]any{
  172. "series": []any{
  173. map[string]any{
  174. "metric": "int.gauge",
  175. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(222)}},
  176. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  177. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  178. "tags": []any{"env:dev"},
  179. },
  180. map[string]any{
  181. "metric": "otel.system.filesystem.utilization",
  182. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(333)}},
  183. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  184. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  185. "tags": []any{"env:dev"},
  186. },
  187. map[string]any{
  188. "metric": "system.disk.in_use",
  189. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(333)}},
  190. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  191. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  192. "tags": []any{"env:dev"},
  193. },
  194. map[string]any{
  195. "metric": "otel.datadog_exporter.metrics.running",
  196. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(1)}},
  197. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  198. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  199. "tags": []any{"version:latest", "command:otelcol"},
  200. },
  201. },
  202. },
  203. expectedSketchPayload: &gogen.SketchPayload{
  204. Sketches: []gogen.SketchPayload_Sketch{
  205. {
  206. Metric: "double.histogram",
  207. Host: "test-host",
  208. Tags: []string{"env:dev"},
  209. Dogsketches: []gogen.SketchPayload_Sketch_Dogsketch{
  210. {
  211. Cnt: 20,
  212. Avg: 0.3,
  213. Sum: 6,
  214. K: []int32{0},
  215. N: []uint32{20},
  216. },
  217. },
  218. },
  219. },
  220. },
  221. },
  222. {
  223. metrics: createTestMetrics(attrs),
  224. source: source.Source{
  225. Kind: source.AWSECSFargateKind,
  226. Identifier: "task_arn",
  227. },
  228. histogramMode: HistogramModeCounters,
  229. hostTags: []string{"key1:value1", "key2:value2"},
  230. expectedSeries: map[string]any{
  231. "series": []any{
  232. map[string]any{
  233. "metric": "int.gauge",
  234. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(222)}},
  235. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  236. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  237. "tags": []any{"env:dev", "key1:value1", "key2:value2"},
  238. },
  239. map[string]any{
  240. "metric": "otel.system.filesystem.utilization",
  241. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(333)}},
  242. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  243. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  244. "tags": []any{"env:dev", "key1:value1", "key2:value2"},
  245. },
  246. map[string]any{
  247. "metric": "double.histogram.bucket",
  248. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(2)}},
  249. "type": float64(datadogV2.METRICINTAKETYPE_COUNT),
  250. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  251. "tags": []any{"lower_bound:-inf", "upper_bound:0", "env:dev", "key1:value1", "key2:value2"},
  252. },
  253. map[string]any{
  254. "metric": "double.histogram.bucket",
  255. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(18)}},
  256. "type": float64(datadogV2.METRICINTAKETYPE_COUNT),
  257. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  258. "tags": []any{"lower_bound:0", "upper_bound:inf", "env:dev", "key1:value1", "key2:value2"},
  259. },
  260. map[string]any{
  261. "metric": "system.disk.in_use",
  262. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(333)}},
  263. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  264. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  265. "tags": []any{"env:dev", "key1:value1", "key2:value2"},
  266. },
  267. map[string]any{
  268. "metric": "otel.datadog_exporter.metrics.running",
  269. "points": []any{map[string]any{"timestamp": float64(0), "value": float64(1)}},
  270. "type": float64(datadogV2.METRICINTAKETYPE_GAUGE),
  271. "resources": []any{map[string]any{"name": "test-host", "type": "host"}},
  272. "tags": []any{"version:latest", "command:otelcol", "key1:value1", "key2:value2"},
  273. },
  274. },
  275. },
  276. },
  277. }
  278. for _, tt := range tests {
  279. t.Run(fmt.Sprintf("kind=%s,histgramMode=%s", tt.source.Kind, tt.histogramMode), func(t *testing.T) {
  280. seriesRecorder := &testutil.HTTPRequestRecorder{Pattern: testutil.MetricV2Endpoint}
  281. sketchRecorder := &testutil.HTTPRequestRecorder{Pattern: testutil.SketchesMetricEndpoint}
  282. server := testutil.DatadogServerMock(
  283. seriesRecorder.HandlerFunc,
  284. sketchRecorder.HandlerFunc,
  285. )
  286. defer server.Close()
  287. var (
  288. once sync.Once
  289. statsRecorder testutil.MockStatsProcessor
  290. )
  291. pusher := newTestPusher(t)
  292. reporter, err := inframetadata.NewReporter(zap.NewNop(), pusher, 1*time.Second)
  293. require.NoError(t, err)
  294. exp, err := newMetricsExporter(
  295. context.Background(),
  296. exportertest.NewNopCreateSettings(),
  297. newTestConfig(t, server.URL, tt.hostTags, tt.histogramMode),
  298. &once,
  299. &testutil.MockSourceProvider{Src: tt.source},
  300. &statsRecorder,
  301. reporter,
  302. )
  303. if tt.expectedErr == nil {
  304. assert.NoError(t, err, "unexpected error")
  305. } else {
  306. assert.Equal(t, tt.expectedErr, err, "expected error doesn't match")
  307. return
  308. }
  309. exp.getPushTime = func() uint64 { return 0 }
  310. err = exp.PushMetricsData(context.Background(), tt.metrics)
  311. if tt.expectedErr == nil {
  312. assert.NoError(t, err, "unexpected error")
  313. } else {
  314. assert.Equal(t, tt.expectedErr, err, "expected error doesn't match")
  315. return
  316. }
  317. if len(tt.expectedSeries) == 0 {
  318. assert.Nil(t, seriesRecorder.ByteBody)
  319. } else {
  320. assert.Equal(t, "gzip", seriesRecorder.Header.Get("Accept-Encoding"))
  321. assert.Equal(t, "application/json", seriesRecorder.Header.Get("Content-Type"))
  322. assert.Equal(t, "gzip", seriesRecorder.Header.Get("Content-Encoding"))
  323. assert.Equal(t, "otelcol/latest", seriesRecorder.Header.Get("User-Agent"))
  324. assert.NoError(t, err)
  325. buf := bytes.NewBuffer(seriesRecorder.ByteBody)
  326. reader, err := gzip.NewReader(buf)
  327. assert.NoError(t, err)
  328. dec := json.NewDecoder(reader)
  329. var actual map[string]any
  330. assert.NoError(t, dec.Decode(&actual))
  331. assert.EqualValues(t, tt.expectedSeries, actual)
  332. }
  333. if tt.expectedSketchPayload == nil {
  334. assert.Nil(t, sketchRecorder.ByteBody)
  335. } else {
  336. assert.Equal(t, "gzip", sketchRecorder.Header.Get("Accept-Encoding"))
  337. assert.Equal(t, "application/x-protobuf", sketchRecorder.Header.Get("Content-Type"))
  338. assert.Equal(t, "otelcol/latest", sketchRecorder.Header.Get("User-Agent"))
  339. expected, err := tt.expectedSketchPayload.Marshal()
  340. assert.NoError(t, err)
  341. assert.Equal(t, expected, sketchRecorder.ByteBody)
  342. }
  343. if tt.expectedStats == nil {
  344. assert.Len(t, statsRecorder.In, 0)
  345. } else {
  346. assert.ElementsMatch(t, statsRecorder.In, tt.expectedStats)
  347. }
  348. })
  349. }
  350. }
  351. func TestNewExporter_Zorkian(t *testing.T) {
  352. if isMetricExportV2Enabled() {
  353. require.NoError(t, enableZorkianMetricExport())
  354. defer require.NoError(t, enableNativeMetricExport())
  355. }
  356. server := testutil.DatadogServerMock()
  357. defer server.Close()
  358. cfg := &Config{
  359. API: APIConfig{
  360. Key: "ddog_32_characters_long_api_key1",
  361. },
  362. Metrics: MetricsConfig{
  363. TCPAddr: confignet.TCPAddr{
  364. Endpoint: server.URL,
  365. },
  366. DeltaTTL: 3600,
  367. HistConfig: HistogramConfig{
  368. Mode: HistogramModeDistributions,
  369. SendAggregations: false,
  370. },
  371. SumConfig: SumConfig{
  372. CumulativeMonotonicMode: CumulativeMonotonicSumModeToDelta,
  373. },
  374. },
  375. }
  376. params := exportertest.NewNopCreateSettings()
  377. f := NewFactory()
  378. // The client should have been created correctly
  379. exp, err := f.CreateMetricsExporter(context.Background(), params, cfg)
  380. require.NoError(t, err)
  381. assert.NotNil(t, exp)
  382. testMetrics := pmetric.NewMetrics()
  383. testutil.TestMetrics.CopyTo(testMetrics)
  384. err = exp.ConsumeMetrics(context.Background(), testMetrics)
  385. require.NoError(t, err)
  386. assert.Equal(t, len(server.MetadataChan), 0)
  387. cfg.HostMetadata.Enabled = true
  388. cfg.HostMetadata.HostnameSource = HostnameSourceFirstResource
  389. testMetrics = pmetric.NewMetrics()
  390. testutil.TestMetrics.CopyTo(testMetrics)
  391. err = exp.ConsumeMetrics(context.Background(), testMetrics)
  392. require.NoError(t, err)
  393. body := <-server.MetadataChan
  394. var recvMetadata payload.HostMetadata
  395. err = json.Unmarshal(body, &recvMetadata)
  396. require.NoError(t, err)
  397. assert.Equal(t, recvMetadata.InternalHostname, "custom-hostname")
  398. }
  399. func Test_metricsExporter_PushMetricsData_Zorkian(t *testing.T) {
  400. if isMetricExportV2Enabled() {
  401. require.NoError(t, enableZorkianMetricExport())
  402. t.Cleanup(func() { require.NoError(t, enableNativeMetricExport()) })
  403. }
  404. attrs := map[string]string{
  405. conventions.AttributeDeploymentEnvironment: "dev",
  406. "custom_attribute": "custom_value",
  407. }
  408. tests := []struct {
  409. metrics pmetric.Metrics
  410. source source.Source
  411. hostTags []string
  412. histogramMode HistogramMode
  413. expectedSeries map[string]any
  414. expectedSketchPayload *gogen.SketchPayload
  415. expectedErr error
  416. expectedStats []*pb.ClientStatsPayload
  417. }{
  418. {
  419. metrics: createTestMetrics(attrs),
  420. source: source.Source{
  421. Kind: source.HostnameKind,
  422. Identifier: "test-host",
  423. },
  424. histogramMode: HistogramModeNoBuckets,
  425. hostTags: []string{"key1:value1", "key2:value2"},
  426. expectedErr: errors.New("no buckets mode and no send count sum are incompatible"),
  427. },
  428. {
  429. metrics: createTestMetrics(attrs),
  430. source: source.Source{
  431. Kind: source.HostnameKind,
  432. Identifier: "test-host",
  433. },
  434. histogramMode: HistogramModeCounters,
  435. hostTags: []string{"key1:value1", "key2:value2"},
  436. expectedSeries: map[string]any{
  437. "series": []any{
  438. map[string]any{
  439. "metric": "int.gauge",
  440. "points": []any{[]any{float64(0), float64(222)}},
  441. "type": "gauge",
  442. "host": "test-host",
  443. "tags": []any{"env:dev"},
  444. },
  445. map[string]any{
  446. "metric": "otel.system.filesystem.utilization",
  447. "points": []any{[]any{float64(0), float64(333)}},
  448. "type": "gauge",
  449. "host": "test-host",
  450. "tags": []any{"env:dev"},
  451. },
  452. map[string]any{
  453. "metric": "double.histogram.bucket",
  454. "points": []any{[]any{float64(0), float64(2)}},
  455. "type": "count",
  456. "host": "test-host",
  457. "tags": []any{"lower_bound:-inf", "upper_bound:0", "env:dev"},
  458. },
  459. map[string]any{
  460. "metric": "double.histogram.bucket",
  461. "points": []any{[]any{float64(0), float64(18)}},
  462. "type": "count",
  463. "host": "test-host",
  464. "tags": []any{"lower_bound:0", "upper_bound:inf", "env:dev"},
  465. },
  466. map[string]any{
  467. "metric": "system.disk.in_use",
  468. "points": []any{[]any{float64(0), float64(333)}},
  469. "type": "gauge",
  470. "host": "test-host",
  471. "tags": []any{"env:dev"},
  472. },
  473. map[string]any{
  474. "metric": "otel.datadog_exporter.metrics.running",
  475. "points": []any{[]any{float64(0), float64(1)}},
  476. "type": "gauge",
  477. "host": "test-host",
  478. "tags": []any{"version:latest", "command:otelcol"},
  479. },
  480. },
  481. },
  482. },
  483. {
  484. metrics: createTestMetrics(attrs),
  485. source: source.Source{
  486. Kind: source.HostnameKind,
  487. Identifier: "test-host",
  488. },
  489. histogramMode: HistogramModeDistributions,
  490. hostTags: []string{"key1:value1", "key2:value2"},
  491. expectedSeries: map[string]any{
  492. "series": []any{
  493. map[string]any{
  494. "metric": "int.gauge",
  495. "points": []any{[]any{float64(0), float64(222)}},
  496. "type": "gauge",
  497. "host": "test-host",
  498. "tags": []any{"env:dev"},
  499. },
  500. map[string]any{
  501. "metric": "otel.system.filesystem.utilization",
  502. "points": []any{[]any{float64(0), float64(333)}},
  503. "type": "gauge",
  504. "host": "test-host",
  505. "tags": []any{"env:dev"},
  506. },
  507. map[string]any{
  508. "metric": "system.disk.in_use",
  509. "points": []any{[]any{float64(0), float64(333)}},
  510. "type": "gauge",
  511. "host": "test-host",
  512. "tags": []any{"env:dev"},
  513. },
  514. map[string]any{
  515. "metric": "otel.datadog_exporter.metrics.running",
  516. "points": []any{[]any{float64(0), float64(1)}},
  517. "type": "gauge",
  518. "host": "test-host",
  519. "tags": []any{"version:latest", "command:otelcol"},
  520. },
  521. },
  522. },
  523. expectedSketchPayload: &gogen.SketchPayload{
  524. Sketches: []gogen.SketchPayload_Sketch{
  525. {
  526. Metric: "double.histogram",
  527. Host: "test-host",
  528. Tags: []string{"env:dev"},
  529. Dogsketches: []gogen.SketchPayload_Sketch_Dogsketch{
  530. {
  531. Cnt: 20,
  532. Avg: 0.3,
  533. Sum: 6,
  534. K: []int32{0},
  535. N: []uint32{20},
  536. },
  537. },
  538. },
  539. },
  540. },
  541. },
  542. {
  543. metrics: createTestMetrics(attrs),
  544. source: source.Source{
  545. Kind: source.AWSECSFargateKind,
  546. Identifier: "task_arn",
  547. },
  548. histogramMode: HistogramModeCounters,
  549. hostTags: []string{"key1:value1", "key2:value2"},
  550. expectedSeries: map[string]any{
  551. "series": []any{
  552. map[string]any{
  553. "metric": "int.gauge",
  554. "points": []any{[]any{float64(0), float64(222)}},
  555. "type": "gauge",
  556. "host": "test-host",
  557. "tags": []any{"env:dev", "key1:value1", "key2:value2"},
  558. },
  559. map[string]any{
  560. "metric": "otel.system.filesystem.utilization",
  561. "points": []any{[]any{float64(0), float64(333)}},
  562. "type": "gauge",
  563. "host": "test-host",
  564. "tags": []any{"env:dev", "key1:value1", "key2:value2"},
  565. },
  566. map[string]any{
  567. "metric": "double.histogram.bucket",
  568. "points": []any{[]any{float64(0), float64(2)}},
  569. "type": "count",
  570. "host": "test-host",
  571. "tags": []any{"lower_bound:-inf", "upper_bound:0", "env:dev", "key1:value1", "key2:value2"},
  572. },
  573. map[string]any{
  574. "metric": "double.histogram.bucket",
  575. "points": []any{[]any{float64(0), float64(18)}},
  576. "type": "count",
  577. "host": "test-host",
  578. "tags": []any{"lower_bound:0", "upper_bound:inf", "env:dev", "key1:value1", "key2:value2"},
  579. },
  580. map[string]any{
  581. "metric": "system.disk.in_use",
  582. "points": []any{[]any{float64(0), float64(333)}},
  583. "type": "gauge",
  584. "host": "test-host",
  585. "tags": []any{"env:dev", "key1:value1", "key2:value2"},
  586. },
  587. map[string]any{
  588. "metric": "otel.datadog_exporter.metrics.running",
  589. "points": []any{[]any{float64(0), float64(1)}},
  590. "type": "gauge",
  591. "host": "test-host",
  592. "tags": []any{"version:latest", "command:otelcol", "key1:value1", "key2:value2"},
  593. },
  594. },
  595. },
  596. expectedSketchPayload: nil,
  597. expectedErr: nil,
  598. },
  599. {
  600. metrics: createTestMetricsWithStats(),
  601. source: source.Source{
  602. Kind: source.HostnameKind,
  603. Identifier: "test-host",
  604. },
  605. histogramMode: HistogramModeDistributions,
  606. hostTags: []string{"key1:value1", "key2:value2"},
  607. expectedSeries: map[string]any{
  608. "series": []any{
  609. map[string]any{
  610. "metric": "int.gauge",
  611. "points": []any{[]any{float64(0), float64(222)}},
  612. "type": "gauge",
  613. "host": "test-host",
  614. "tags": []any{"env:dev"},
  615. },
  616. map[string]any{
  617. "metric": "otel.system.filesystem.utilization",
  618. "points": []any{[]any{float64(0), float64(333)}},
  619. "type": "gauge",
  620. "host": "test-host",
  621. "tags": []any{"env:dev"},
  622. },
  623. map[string]any{
  624. "metric": "system.disk.in_use",
  625. "points": []any{[]any{float64(0), float64(333)}},
  626. "type": "gauge",
  627. "host": "test-host",
  628. "tags": []any{"env:dev"},
  629. },
  630. map[string]any{
  631. "metric": "otel.datadog_exporter.metrics.running",
  632. "points": []any{[]any{float64(0), float64(1)}},
  633. "type": "gauge",
  634. "host": "test-host",
  635. "tags": []any{"version:latest", "command:otelcol"},
  636. },
  637. },
  638. },
  639. expectedSketchPayload: &gogen.SketchPayload{
  640. Sketches: []gogen.SketchPayload_Sketch{
  641. {
  642. Metric: "double.histogram",
  643. Host: "test-host",
  644. Tags: []string{"env:dev"},
  645. Dogsketches: []gogen.SketchPayload_Sketch_Dogsketch{
  646. {
  647. Cnt: 20,
  648. Avg: 0.3,
  649. Sum: 6,
  650. K: []int32{0},
  651. N: []uint32{20},
  652. },
  653. },
  654. },
  655. },
  656. },
  657. expectedStats: testutil.StatsPayloads,
  658. },
  659. }
  660. for _, tt := range tests {
  661. t.Run(fmt.Sprintf("kind=%s,histgramMode=%s", tt.source.Kind, tt.histogramMode), func(t *testing.T) {
  662. seriesRecorder := &testutil.HTTPRequestRecorder{Pattern: testutil.MetricV1Endpoint}
  663. sketchRecorder := &testutil.HTTPRequestRecorder{Pattern: testutil.SketchesMetricEndpoint}
  664. server := testutil.DatadogServerMock(
  665. seriesRecorder.HandlerFunc,
  666. sketchRecorder.HandlerFunc,
  667. )
  668. defer server.Close()
  669. var (
  670. once sync.Once
  671. statsRecorder testutil.MockStatsProcessor
  672. )
  673. pusher := newTestPusher(t)
  674. reporter, err := inframetadata.NewReporter(zap.NewNop(), pusher, 1*time.Second)
  675. require.NoError(t, err)
  676. exp, err := newMetricsExporter(
  677. context.Background(),
  678. exportertest.NewNopCreateSettings(),
  679. newTestConfig(t, server.URL, tt.hostTags, tt.histogramMode),
  680. &once,
  681. &testutil.MockSourceProvider{Src: tt.source},
  682. &statsRecorder,
  683. reporter,
  684. )
  685. if tt.expectedErr == nil {
  686. assert.NoError(t, err, "unexpected error")
  687. } else {
  688. assert.Equal(t, tt.expectedErr, err, "expected error doesn't match")
  689. return
  690. }
  691. exp.getPushTime = func() uint64 { return 0 }
  692. err = exp.PushMetricsData(context.Background(), tt.metrics)
  693. if tt.expectedErr == nil {
  694. assert.NoError(t, err, "unexpected error")
  695. } else {
  696. assert.Equal(t, tt.expectedErr, err, "expected error doesn't match")
  697. return
  698. }
  699. if len(tt.expectedSeries) == 0 {
  700. assert.Nil(t, seriesRecorder.ByteBody)
  701. } else {
  702. assert.Equal(t, "gzip", seriesRecorder.Header.Get("Accept-Encoding"))
  703. assert.Equal(t, "application/json", seriesRecorder.Header.Get("Content-Type"))
  704. assert.Equal(t, "otelcol/latest", seriesRecorder.Header.Get("User-Agent"))
  705. assert.NoError(t, err)
  706. var actual map[string]any
  707. assert.NoError(t, json.Unmarshal(seriesRecorder.ByteBody, &actual))
  708. assert.EqualValues(t, tt.expectedSeries, actual)
  709. }
  710. if tt.expectedSketchPayload == nil {
  711. assert.Nil(t, sketchRecorder.ByteBody)
  712. } else {
  713. assert.Equal(t, "gzip", sketchRecorder.Header.Get("Accept-Encoding"))
  714. assert.Equal(t, "application/x-protobuf", sketchRecorder.Header.Get("Content-Type"))
  715. assert.Equal(t, "otelcol/latest", sketchRecorder.Header.Get("User-Agent"))
  716. expected, err := tt.expectedSketchPayload.Marshal()
  717. assert.NoError(t, err)
  718. assert.Equal(t, expected, sketchRecorder.ByteBody)
  719. }
  720. if tt.expectedStats == nil {
  721. assert.Len(t, statsRecorder.In, 0)
  722. } else {
  723. assert.ElementsMatch(t, statsRecorder.In, tt.expectedStats)
  724. }
  725. })
  726. }
  727. }
  728. func createTestMetricsWithStats() pmetric.Metrics {
  729. md := createTestMetrics(map[string]string{
  730. conventions.AttributeDeploymentEnvironment: "dev",
  731. "custom_attribute": "custom_value",
  732. })
  733. dest := md.ResourceMetrics()
  734. logger, _ := zap.NewDevelopment()
  735. trans, err := metrics.NewTranslator(logger)
  736. if err != nil {
  737. panic(err)
  738. }
  739. src := trans.
  740. StatsPayloadToMetrics(&pb.StatsPayload{Stats: testutil.StatsPayloads}).
  741. ResourceMetrics()
  742. src.MoveAndAppendTo(dest)
  743. return md
  744. }
  745. func createTestMetrics(additionalAttributes map[string]string) pmetric.Metrics {
  746. const (
  747. host = "test-host"
  748. name = "test-metrics"
  749. version = "v0.0.1"
  750. )
  751. md := pmetric.NewMetrics()
  752. rms := md.ResourceMetrics()
  753. rm := rms.AppendEmpty()
  754. attrs := rm.Resource().Attributes()
  755. attrs.PutStr("datadog.host.name", host)
  756. for attr, val := range additionalAttributes {
  757. attrs.PutStr(attr, val)
  758. }
  759. ilms := rm.ScopeMetrics()
  760. ilm := ilms.AppendEmpty()
  761. ilm.Scope().SetName(name)
  762. ilm.Scope().SetVersion(version)
  763. metricsArray := ilm.Metrics()
  764. metricsArray.AppendEmpty() // first one is TypeNone to test that it's ignored
  765. // IntGauge
  766. met := metricsArray.AppendEmpty()
  767. met.SetName("int.gauge")
  768. dpsInt := met.SetEmptyGauge().DataPoints()
  769. dpInt := dpsInt.AppendEmpty()
  770. dpInt.SetTimestamp(seconds(0))
  771. dpInt.SetIntValue(222)
  772. // host metric
  773. met = metricsArray.AppendEmpty()
  774. met.SetName("system.filesystem.utilization")
  775. dpsInt = met.SetEmptyGauge().DataPoints()
  776. dpInt = dpsInt.AppendEmpty()
  777. dpInt.SetTimestamp(seconds(0))
  778. dpInt.SetIntValue(333)
  779. // Histogram (delta)
  780. met = metricsArray.AppendEmpty()
  781. met.SetName("double.histogram")
  782. met.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityDelta)
  783. dpsDoubleHist := met.Histogram().DataPoints()
  784. dpDoubleHist := dpsDoubleHist.AppendEmpty()
  785. dpDoubleHist.SetCount(20)
  786. dpDoubleHist.SetSum(6)
  787. dpDoubleHist.BucketCounts().FromRaw([]uint64{2, 18})
  788. dpDoubleHist.ExplicitBounds().FromRaw([]float64{0})
  789. dpDoubleHist.SetTimestamp(seconds(0))
  790. return md
  791. }
  792. func seconds(i int) pcommon.Timestamp {
  793. return pcommon.NewTimestampFromTime(time.Unix(int64(i), 0))
  794. }
  795. func newTestConfig(t *testing.T, endpoint string, hostTags []string, histogramMode HistogramMode) *Config {
  796. t.Helper()
  797. return &Config{
  798. HostMetadata: HostMetadataConfig{
  799. Tags: hostTags,
  800. },
  801. Metrics: MetricsConfig{
  802. TCPAddr: confignet.TCPAddr{
  803. Endpoint: endpoint,
  804. },
  805. HistConfig: HistogramConfig{
  806. Mode: histogramMode,
  807. },
  808. // Set values to avoid errors. No particular intention in value selection.
  809. DeltaTTL: 3600,
  810. SumConfig: SumConfig{
  811. CumulativeMonotonicMode: CumulativeMonotonicSumModeRawValue,
  812. },
  813. },
  814. }
  815. }