receiver_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. // Copyright The OpenTelemetry Authors
  2. // SPDX-License-Identifier: Apache-2.0
  3. //go:build !windows
  4. // +build !windows
  5. // TODO review if tests should succeed on Windows
  6. package dockerstatsreceiver
  7. import (
  8. "context"
  9. "net/http"
  10. "net/http/httptest"
  11. "os"
  12. "path/filepath"
  13. "testing"
  14. "time"
  15. "github.com/docker/docker/api/types"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. "go.opentelemetry.io/collector/component/componenttest"
  19. "go.opentelemetry.io/collector/pdata/pcommon"
  20. "go.opentelemetry.io/collector/receiver/receivertest"
  21. "go.opentelemetry.io/collector/receiver/scraperhelper"
  22. "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden"
  23. "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest"
  24. "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/dockerstatsreceiver/internal/metadata"
  25. )
  26. var mockFolder = filepath.Join("testdata", "mock")
  27. var (
  28. metricEnabled = metadata.MetricConfig{Enabled: true}
  29. allMetricsEnabled = metadata.MetricsConfig{
  30. ContainerBlockioIoMergedRecursive: metricEnabled,
  31. ContainerBlockioIoQueuedRecursive: metricEnabled,
  32. ContainerBlockioIoServiceBytesRecursive: metricEnabled,
  33. ContainerBlockioIoServiceTimeRecursive: metricEnabled,
  34. ContainerBlockioIoServicedRecursive: metricEnabled,
  35. ContainerBlockioIoTimeRecursive: metricEnabled,
  36. ContainerBlockioIoWaitTimeRecursive: metricEnabled,
  37. ContainerBlockioSectorsRecursive: metricEnabled,
  38. ContainerCPULimit: metricEnabled,
  39. ContainerCPUShares: metricEnabled,
  40. ContainerCPUUtilization: metricEnabled,
  41. ContainerCPUThrottlingDataPeriods: metricEnabled,
  42. ContainerCPUThrottlingDataThrottledPeriods: metricEnabled,
  43. ContainerCPUThrottlingDataThrottledTime: metricEnabled,
  44. ContainerCPUUsageKernelmode: metricEnabled,
  45. ContainerCPUUsagePercpu: metricEnabled,
  46. ContainerCPUUsageSystem: metricEnabled,
  47. ContainerCPUUsageTotal: metricEnabled,
  48. ContainerCPUUsageUsermode: metricEnabled,
  49. ContainerMemoryActiveAnon: metricEnabled,
  50. ContainerMemoryActiveFile: metricEnabled,
  51. ContainerMemoryCache: metricEnabled,
  52. ContainerMemoryDirty: metricEnabled,
  53. ContainerMemoryHierarchicalMemoryLimit: metricEnabled,
  54. ContainerMemoryHierarchicalMemswLimit: metricEnabled,
  55. ContainerMemoryInactiveAnon: metricEnabled,
  56. ContainerMemoryInactiveFile: metricEnabled,
  57. ContainerMemoryMappedFile: metricEnabled,
  58. ContainerMemoryPercent: metricEnabled,
  59. ContainerMemoryPgfault: metricEnabled,
  60. ContainerMemoryPgmajfault: metricEnabled,
  61. ContainerMemoryPgpgin: metricEnabled,
  62. ContainerMemoryPgpgout: metricEnabled,
  63. ContainerMemoryRss: metricEnabled,
  64. ContainerMemoryRssHuge: metricEnabled,
  65. ContainerMemoryTotalActiveAnon: metricEnabled,
  66. ContainerMemoryTotalActiveFile: metricEnabled,
  67. ContainerMemoryTotalCache: metricEnabled,
  68. ContainerMemoryTotalDirty: metricEnabled,
  69. ContainerMemoryTotalInactiveAnon: metricEnabled,
  70. ContainerMemoryTotalInactiveFile: metricEnabled,
  71. ContainerMemoryTotalMappedFile: metricEnabled,
  72. ContainerMemoryTotalPgfault: metricEnabled,
  73. ContainerMemoryTotalPgmajfault: metricEnabled,
  74. ContainerMemoryTotalPgpgin: metricEnabled,
  75. ContainerMemoryTotalPgpgout: metricEnabled,
  76. ContainerMemoryTotalRss: metricEnabled,
  77. ContainerMemoryTotalRssHuge: metricEnabled,
  78. ContainerMemoryTotalUnevictable: metricEnabled,
  79. ContainerMemoryTotalWriteback: metricEnabled,
  80. ContainerMemoryUnevictable: metricEnabled,
  81. ContainerMemoryUsageLimit: metricEnabled,
  82. ContainerMemoryUsageMax: metricEnabled,
  83. ContainerMemoryUsageTotal: metricEnabled,
  84. ContainerMemoryWriteback: metricEnabled,
  85. ContainerNetworkIoUsageRxBytes: metricEnabled,
  86. ContainerNetworkIoUsageRxDropped: metricEnabled,
  87. ContainerNetworkIoUsageRxErrors: metricEnabled,
  88. ContainerNetworkIoUsageRxPackets: metricEnabled,
  89. ContainerNetworkIoUsageTxBytes: metricEnabled,
  90. ContainerNetworkIoUsageTxDropped: metricEnabled,
  91. ContainerNetworkIoUsageTxErrors: metricEnabled,
  92. ContainerNetworkIoUsageTxPackets: metricEnabled,
  93. ContainerPidsCount: metricEnabled,
  94. ContainerPidsLimit: metricEnabled,
  95. ContainerUptime: metricEnabled,
  96. ContainerRestarts: metricEnabled,
  97. ContainerMemoryAnon: metricEnabled,
  98. ContainerMemoryFile: metricEnabled,
  99. }
  100. resourceAttributeEnabled = metadata.ResourceAttributeConfig{Enabled: true}
  101. allResourceAttributesEnabled = metadata.ResourceAttributesConfig{
  102. ContainerCommandLine: resourceAttributeEnabled,
  103. ContainerHostname: resourceAttributeEnabled,
  104. ContainerID: resourceAttributeEnabled,
  105. ContainerImageID: resourceAttributeEnabled,
  106. ContainerImageName: resourceAttributeEnabled,
  107. ContainerName: resourceAttributeEnabled,
  108. ContainerRuntime: resourceAttributeEnabled,
  109. }
  110. )
  111. func TestNewReceiver(t *testing.T) {
  112. cfg := &Config{
  113. ScraperControllerSettings: scraperhelper.ScraperControllerSettings{
  114. CollectionInterval: 1 * time.Second,
  115. },
  116. Endpoint: "unix:///run/some.sock",
  117. DockerAPIVersion: defaultDockerAPIVersion,
  118. }
  119. mr := newMetricsReceiver(receivertest.NewNopCreateSettings(), cfg)
  120. assert.NotNil(t, mr)
  121. }
  122. func TestErrorsInStart(t *testing.T) {
  123. unreachable := "unix:///not/a/thing.sock"
  124. cfg := &Config{
  125. ScraperControllerSettings: scraperhelper.ScraperControllerSettings{
  126. CollectionInterval: 1 * time.Second,
  127. },
  128. Endpoint: unreachable,
  129. DockerAPIVersion: defaultDockerAPIVersion,
  130. }
  131. recv := newMetricsReceiver(receivertest.NewNopCreateSettings(), cfg)
  132. assert.NotNil(t, recv)
  133. cfg.Endpoint = "..not/a/valid/endpoint"
  134. err := recv.start(context.Background(), componenttest.NewNopHost())
  135. assert.Error(t, err)
  136. assert.Contains(t, err.Error(), "unable to parse docker host")
  137. cfg.Endpoint = unreachable
  138. err = recv.start(context.Background(), componenttest.NewNopHost())
  139. assert.Error(t, err)
  140. assert.Contains(t, err.Error(), "context deadline exceeded")
  141. }
  142. func TestScrapeV2(t *testing.T) {
  143. testCases := []struct {
  144. desc string
  145. expectedMetricsFile string
  146. mockDockerEngine func(t *testing.T) *httptest.Server
  147. cfgBuilder *testConfigBuilder
  148. }{
  149. {
  150. desc: "scrapeV2_single_container",
  151. expectedMetricsFile: filepath.Join(mockFolder, "single_container", "expected_metrics.yaml"),
  152. mockDockerEngine: func(t *testing.T) *httptest.Server {
  153. t.Helper()
  154. containerID := "10b703fb312b25e8368ab5a3bce3a1610d1cee5d71a94920f1a7adbc5b0cb326"
  155. mockServer, err := dockerMockServer(&map[string]string{
  156. "/v1.25/containers/json": filepath.Join(mockFolder, "single_container", "containers.json"),
  157. "/v1.25/containers/" + containerID + "/json": filepath.Join(mockFolder, "single_container", "container.json"),
  158. "/v1.25/containers/" + containerID + "/stats": filepath.Join(mockFolder, "single_container", "stats.json"),
  159. })
  160. require.NoError(t, err)
  161. return mockServer
  162. },
  163. cfgBuilder: newTestConfigBuilder().
  164. withDefaultLabels().
  165. withMetrics(allMetricsEnabled),
  166. },
  167. {
  168. desc: "scrapeV2_two_containers",
  169. expectedMetricsFile: filepath.Join(mockFolder, "two_containers", "expected_metrics.yaml"),
  170. mockDockerEngine: func(t *testing.T) *httptest.Server {
  171. t.Helper()
  172. containerIDs := []string{
  173. "89d28931fd8b95c8806343a532e9e76bf0a0b76ee8f19452b8f75dee1ebcebb7",
  174. "a359c0fc87c546b42d2ad32db7c978627f1d89b49cb3827a7b19ba97a1febcce",
  175. }
  176. mockServer, err := dockerMockServer(&map[string]string{
  177. "/v1.25/containers/json": filepath.Join(mockFolder, "two_containers", "containers.json"),
  178. "/v1.25/containers/" + containerIDs[0] + "/json": filepath.Join(mockFolder, "two_containers", "container1.json"),
  179. "/v1.25/containers/" + containerIDs[1] + "/json": filepath.Join(mockFolder, "two_containers", "container2.json"),
  180. "/v1.25/containers/" + containerIDs[0] + "/stats": filepath.Join(mockFolder, "two_containers", "stats1.json"),
  181. "/v1.25/containers/" + containerIDs[1] + "/stats": filepath.Join(mockFolder, "two_containers", "stats2.json"),
  182. })
  183. require.NoError(t, err)
  184. return mockServer
  185. },
  186. cfgBuilder: newTestConfigBuilder().
  187. withDefaultLabels().
  188. withMetrics(allMetricsEnabled),
  189. },
  190. {
  191. desc: "scrapeV2_no_pids_stats",
  192. expectedMetricsFile: filepath.Join(mockFolder, "no_pids_stats", "expected_metrics.yaml"),
  193. mockDockerEngine: func(t *testing.T) *httptest.Server {
  194. t.Helper()
  195. containerID := "10b703fb312b25e8368ab5a3bce3a1610d1cee5d71a94920f1a7adbc5b0cb326"
  196. mockServer, err := dockerMockServer(&map[string]string{
  197. "/v1.25/containers/json": filepath.Join(mockFolder, "no_pids_stats", "containers.json"),
  198. "/v1.25/containers/" + containerID + "/json": filepath.Join(mockFolder, "no_pids_stats", "container.json"),
  199. "/v1.25/containers/" + containerID + "/stats": filepath.Join(mockFolder, "no_pids_stats", "stats.json"),
  200. })
  201. require.NoError(t, err)
  202. return mockServer
  203. },
  204. cfgBuilder: newTestConfigBuilder().
  205. withDefaultLabels().
  206. withMetrics(allMetricsEnabled),
  207. },
  208. {
  209. desc: "scrapeV2_pid_stats_max",
  210. expectedMetricsFile: filepath.Join(mockFolder, "pids_stats_max", "expected_metrics.yaml"),
  211. mockDockerEngine: func(t *testing.T) *httptest.Server {
  212. t.Helper()
  213. containerID := "78de07328afff50a9777b07dd36a28c709dffe081baaf67235db618843399643"
  214. mockServer, err := dockerMockServer(&map[string]string{
  215. "/v1.25/containers/json": filepath.Join(mockFolder, "pids_stats_max", "containers.json"),
  216. "/v1.25/containers/" + containerID + "/json": filepath.Join(mockFolder, "pids_stats_max", "container.json"),
  217. "/v1.25/containers/" + containerID + "/stats": filepath.Join(mockFolder, "pids_stats_max", "stats.json"),
  218. })
  219. require.NoError(t, err)
  220. return mockServer
  221. },
  222. cfgBuilder: newTestConfigBuilder().
  223. withDefaultLabels().
  224. withMetrics(allMetricsEnabled),
  225. },
  226. {
  227. desc: "scrapeV2_cpu_limit",
  228. expectedMetricsFile: filepath.Join(mockFolder, "cpu_limit", "expected_metrics.yaml"),
  229. mockDockerEngine: func(t *testing.T) *httptest.Server {
  230. t.Helper()
  231. containerID := "9b842c47c1c3e4ee931e2c9713cf4e77aa09acc2201aea60fba04b6dbba6c674"
  232. mockServer, err := dockerMockServer(&map[string]string{
  233. "/v1.25/containers/json": filepath.Join(mockFolder, "cpu_limit", "containers.json"),
  234. "/v1.25/containers/" + containerID + "/json": filepath.Join(mockFolder, "cpu_limit", "container.json"),
  235. "/v1.25/containers/" + containerID + "/stats": filepath.Join(mockFolder, "cpu_limit", "stats.json"),
  236. })
  237. require.NoError(t, err)
  238. return mockServer
  239. },
  240. cfgBuilder: newTestConfigBuilder().
  241. withDefaultLabels().
  242. withMetrics(allMetricsEnabled),
  243. },
  244. {
  245. desc: "cgroups_v2_container",
  246. expectedMetricsFile: filepath.Join(mockFolder, "cgroups_v2", "expected_metrics.yaml"),
  247. mockDockerEngine: func(t *testing.T) *httptest.Server {
  248. containerID := "f97ed5bca0a5a0b85bfd52c4144b96174e825c92a138bc0458f0e196f2c7c1b4"
  249. mockServer, err := dockerMockServer(&map[string]string{
  250. "/v1.25/containers/json": filepath.Join(mockFolder, "cgroups_v2", "containers.json"),
  251. "/v1.25/containers/" + containerID + "/json": filepath.Join(mockFolder, "cgroups_v2", "container.json"),
  252. "/v1.25/containers/" + containerID + "/stats": filepath.Join(mockFolder, "cgroups_v2", "stats.json"),
  253. })
  254. require.NoError(t, err)
  255. return mockServer
  256. },
  257. cfgBuilder: newTestConfigBuilder().
  258. withDefaultLabels().
  259. withMetrics(allMetricsEnabled),
  260. },
  261. {
  262. desc: "scrapeV2_single_container_with_optional_resource_attributes",
  263. expectedMetricsFile: filepath.Join(mockFolder, "single_container_with_optional_resource_attributes", "expected_metrics.yaml"),
  264. mockDockerEngine: func(t *testing.T) *httptest.Server {
  265. containerID := "73364842ef014441cac89fed05df19463b1230db25a31252cdf82e754f1ec581"
  266. mockServer, err := dockerMockServer(&map[string]string{
  267. "/v1.25/containers/json": filepath.Join(mockFolder, "single_container_with_optional_resource_attributes", "containers.json"),
  268. "/v1.25/containers/" + containerID + "/json": filepath.Join(mockFolder, "single_container_with_optional_resource_attributes", "container.json"),
  269. "/v1.25/containers/" + containerID + "/stats": filepath.Join(mockFolder, "single_container_with_optional_resource_attributes", "stats.json"),
  270. })
  271. require.NoError(t, err)
  272. return mockServer
  273. },
  274. cfgBuilder: newTestConfigBuilder().
  275. withDefaultLabels().
  276. withMetrics(allMetricsEnabled).
  277. withResourceAttributes(allResourceAttributesEnabled),
  278. },
  279. }
  280. for _, tc := range testCases {
  281. t.Run(tc.desc, func(t *testing.T) {
  282. mockDockerEngine := tc.mockDockerEngine(t)
  283. defer mockDockerEngine.Close()
  284. receiver := newMetricsReceiver(
  285. receivertest.NewNopCreateSettings(), tc.cfgBuilder.withEndpoint(mockDockerEngine.URL).build())
  286. err := receiver.start(context.Background(), componenttest.NewNopHost())
  287. require.NoError(t, err)
  288. actualMetrics, err := receiver.scrapeV2(context.Background())
  289. require.NoError(t, err)
  290. // Uncomment to regenerate 'expected_metrics.yaml' files
  291. // golden.WriteMetrics(t, tc.expectedMetricsFile, actualMetrics)
  292. expectedMetrics, err := golden.ReadMetrics(tc.expectedMetricsFile)
  293. assert.NoError(t, err)
  294. assert.NoError(t, pmetrictest.CompareMetrics(expectedMetrics, actualMetrics,
  295. pmetrictest.IgnoreMetricDataPointsOrder(),
  296. pmetrictest.IgnoreResourceMetricsOrder(),
  297. pmetrictest.IgnoreStartTimestamp(),
  298. pmetrictest.IgnoreTimestamp(),
  299. pmetrictest.IgnoreMetricValues(
  300. "container.uptime", // value depends on time.Now(), making it unpredictable as far as tests go
  301. ),
  302. ))
  303. })
  304. }
  305. }
  306. func TestRecordBaseMetrics(t *testing.T) {
  307. cfg := createDefaultConfig().(*Config)
  308. cfg.MetricsBuilderConfig.Metrics = metadata.MetricsConfig{
  309. ContainerUptime: metricEnabled,
  310. }
  311. r := newMetricsReceiver(receivertest.NewNopCreateSettings(), cfg)
  312. now := time.Now()
  313. started := now.Add(-2 * time.Second).Format(time.RFC3339)
  314. t.Run("ok", func(t *testing.T) {
  315. err := r.recordBaseMetrics(
  316. pcommon.NewTimestampFromTime(now),
  317. &types.ContainerJSONBase{
  318. State: &types.ContainerState{
  319. StartedAt: started,
  320. },
  321. },
  322. )
  323. require.NoError(t, err)
  324. m := r.mb.Emit().ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0)
  325. assert.Equal(t, "container.uptime", m.Name())
  326. dp := m.Gauge().DataPoints()
  327. assert.Equal(t, 1, dp.Len())
  328. assert.Equal(t, 2, int(dp.At(0).DoubleValue()))
  329. })
  330. t.Run("error", func(t *testing.T) {
  331. err := r.recordBaseMetrics(
  332. pcommon.NewTimestampFromTime(now),
  333. &types.ContainerJSONBase{
  334. State: &types.ContainerState{
  335. StartedAt: "bad date",
  336. },
  337. },
  338. )
  339. require.Error(t, err)
  340. })
  341. }
  342. func dockerMockServer(urlToFile *map[string]string) (*httptest.Server, error) {
  343. urlToFileContents := make(map[string][]byte, len(*urlToFile))
  344. for urlPath, filePath := range *urlToFile {
  345. err := func() error {
  346. fileContents, err := os.ReadFile(filepath.Clean(filePath))
  347. if err != nil {
  348. return err
  349. }
  350. urlToFileContents[urlPath] = fileContents
  351. return nil
  352. }()
  353. if err != nil {
  354. return nil, err
  355. }
  356. }
  357. return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  358. data, ok := urlToFileContents[req.URL.Path]
  359. if !ok {
  360. rw.WriteHeader(http.StatusNotFound)
  361. return
  362. }
  363. rw.WriteHeader(http.StatusOK)
  364. _, _ = rw.Write(data)
  365. })), nil
  366. }
  367. type testConfigBuilder struct {
  368. config *Config
  369. }
  370. func newTestConfigBuilder() *testConfigBuilder {
  371. return &testConfigBuilder{config: createDefaultConfig().(*Config)}
  372. }
  373. func (cb *testConfigBuilder) withEndpoint(endpoint string) *testConfigBuilder {
  374. cb.config.Endpoint = endpoint
  375. return cb
  376. }
  377. func (cb *testConfigBuilder) withMetrics(ms metadata.MetricsConfig) *testConfigBuilder {
  378. cb.config.MetricsBuilderConfig.Metrics = ms
  379. return cb
  380. }
  381. func (cb *testConfigBuilder) withResourceAttributes(ras metadata.ResourceAttributesConfig) *testConfigBuilder {
  382. cb.config.MetricsBuilderConfig.ResourceAttributes = ras
  383. return cb
  384. }
  385. func (cb *testConfigBuilder) withDefaultLabels() *testConfigBuilder {
  386. cb.config.EnvVarsToMetricLabels = map[string]string{
  387. "ENV_VAR": "env-var-metric-label",
  388. "ENV_VAR_2": "env-var-metric-label-2",
  389. }
  390. cb.config.ContainerLabelsToMetricLabels = map[string]string{
  391. "container.label": "container-metric-label",
  392. "container.label.2": "container-metric-label-2",
  393. }
  394. return cb
  395. }
  396. func (cb *testConfigBuilder) build() *Config {
  397. return cb.config
  398. }