config_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. // Copyright The OpenTelemetry Authors
  2. // SPDX-License-Identifier: Apache-2.0
  3. package signalfxexporter
  4. import (
  5. "net/url"
  6. "path/filepath"
  7. "testing"
  8. "time"
  9. "github.com/cenkalti/backoff/v4"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "go.opentelemetry.io/collector/component"
  13. "go.opentelemetry.io/collector/config/confighttp"
  14. "go.opentelemetry.io/collector/config/configopaque"
  15. "go.opentelemetry.io/collector/confmap"
  16. "go.opentelemetry.io/collector/confmap/confmaptest"
  17. "go.opentelemetry.io/collector/exporter/exporterhelper"
  18. "go.uber.org/zap"
  19. apmcorrelation "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/signalfxexporter/internal/apm/correlations"
  20. "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/signalfxexporter/internal/correlation"
  21. "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/signalfxexporter/internal/metadata"
  22. "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/signalfxexporter/internal/translation"
  23. "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/signalfxexporter/internal/translation/dpfilters"
  24. "github.com/open-telemetry/opentelemetry-collector-contrib/internal/splunk"
  25. )
  26. func TestLoadConfig(t *testing.T) {
  27. t.Parallel()
  28. cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml"))
  29. require.NoError(t, err)
  30. seventy := 70
  31. hundred := 100
  32. idleConnTimeout := 30 * time.Second
  33. tests := []struct {
  34. id component.ID
  35. expected *Config
  36. }{
  37. {
  38. id: component.NewIDWithName(metadata.Type, ""),
  39. expected: &Config{
  40. AccessToken: "testToken",
  41. Realm: "ap0",
  42. HTTPClientSettings: confighttp.HTTPClientSettings{
  43. Timeout: 10 * time.Second,
  44. Headers: nil,
  45. MaxIdleConns: &hundred,
  46. MaxIdleConnsPerHost: &hundred,
  47. IdleConnTimeout: &idleConnTimeout,
  48. },
  49. RetrySettings: exporterhelper.RetrySettings{
  50. Enabled: true,
  51. InitialInterval: 5 * time.Second,
  52. MaxInterval: 30 * time.Second,
  53. MaxElapsedTime: 5 * time.Minute,
  54. RandomizationFactor: backoff.DefaultRandomizationFactor,
  55. Multiplier: backoff.DefaultMultiplier,
  56. },
  57. QueueSettings: exporterhelper.NewDefaultQueueSettings(),
  58. AccessTokenPassthroughConfig: splunk.AccessTokenPassthroughConfig{
  59. AccessTokenPassthrough: true,
  60. },
  61. LogDimensionUpdates: false,
  62. DimensionClient: DimensionClientConfig{
  63. MaxBuffered: 10000,
  64. SendDelay: 10 * time.Second,
  65. MaxIdleConns: 20,
  66. MaxIdleConnsPerHost: 20,
  67. MaxConnsPerHost: 20,
  68. IdleConnTimeout: 30 * time.Second,
  69. Timeout: 10 * time.Second,
  70. },
  71. TranslationRules: nil,
  72. ExcludeMetrics: nil,
  73. IncludeMetrics: nil,
  74. DeltaTranslationTTL: 3600,
  75. ExcludeProperties: nil,
  76. Correlation: &correlation.Config{
  77. HTTPClientSettings: confighttp.HTTPClientSettings{
  78. Endpoint: "",
  79. Timeout: 5 * time.Second,
  80. },
  81. StaleServiceTimeout: 5 * time.Minute,
  82. SyncAttributes: map[string]string{
  83. "k8s.pod.uid": "k8s.pod.uid",
  84. "container.id": "container.id",
  85. },
  86. Config: apmcorrelation.Config{
  87. MaxRequests: 20,
  88. MaxBuffered: 10_000,
  89. MaxRetries: 2,
  90. LogUpdates: false,
  91. RetryDelay: 30 * time.Second,
  92. CleanupInterval: 1 * time.Minute,
  93. },
  94. },
  95. NonAlphanumericDimensionChars: "_-.",
  96. },
  97. },
  98. {
  99. id: component.NewIDWithName(metadata.Type, "allsettings"),
  100. expected: &Config{
  101. AccessToken: "testToken",
  102. Realm: "us1",
  103. HTTPClientSettings: confighttp.HTTPClientSettings{
  104. Timeout: 2 * time.Second,
  105. Headers: map[string]configopaque.String{
  106. "added-entry": "added value",
  107. "dot.test": "test",
  108. },
  109. MaxIdleConns: &seventy,
  110. MaxIdleConnsPerHost: &seventy,
  111. IdleConnTimeout: &idleConnTimeout,
  112. },
  113. RetrySettings: exporterhelper.RetrySettings{
  114. Enabled: true,
  115. InitialInterval: 10 * time.Second,
  116. MaxInterval: 1 * time.Minute,
  117. MaxElapsedTime: 10 * time.Minute,
  118. RandomizationFactor: backoff.DefaultRandomizationFactor,
  119. Multiplier: backoff.DefaultMultiplier,
  120. },
  121. QueueSettings: exporterhelper.QueueSettings{
  122. Enabled: true,
  123. NumConsumers: 2,
  124. QueueSize: 10,
  125. }, AccessTokenPassthroughConfig: splunk.AccessTokenPassthroughConfig{
  126. AccessTokenPassthrough: false,
  127. },
  128. LogDimensionUpdates: true,
  129. DimensionClient: DimensionClientConfig{
  130. MaxBuffered: 1,
  131. SendDelay: time.Hour,
  132. MaxIdleConns: 100,
  133. MaxIdleConnsPerHost: 10,
  134. MaxConnsPerHost: 10000,
  135. IdleConnTimeout: 2 * time.Hour,
  136. Timeout: 20 * time.Second,
  137. },
  138. TranslationRules: []translation.Rule{
  139. {
  140. Action: translation.ActionRenameDimensionKeys,
  141. Mapping: map[string]string{
  142. "k8s.cluster.name": "kubernetes_cluster",
  143. },
  144. },
  145. {
  146. Action: translation.ActionDropDimensions,
  147. DimensionPairs: map[string]map[string]bool{
  148. "foo": nil,
  149. "foo1": {"bar": true},
  150. },
  151. },
  152. {
  153. Action: translation.ActionDropDimensions,
  154. MetricName: "metric",
  155. DimensionPairs: map[string]map[string]bool{
  156. "foo": nil,
  157. "foo1": {"bar": true},
  158. },
  159. },
  160. {
  161. Action: translation.ActionDropDimensions,
  162. MetricNames: map[string]bool{
  163. "metric1": true,
  164. "metric2": true,
  165. },
  166. DimensionPairs: map[string]map[string]bool{
  167. "foo": nil,
  168. "foo1": {"bar": true},
  169. },
  170. },
  171. },
  172. ExcludeMetrics: []dpfilters.MetricFilter{
  173. {
  174. MetricName: "metric1",
  175. },
  176. {
  177. MetricNames: []string{"metric2", "metric3"},
  178. },
  179. {
  180. MetricName: "metric4",
  181. Dimensions: map[string]any{
  182. "dimension_key": "dimension_val",
  183. },
  184. },
  185. {
  186. MetricName: "metric5",
  187. Dimensions: map[string]any{
  188. "dimension_key": []any{"dimension_val1", "dimension_val2"},
  189. },
  190. },
  191. {
  192. MetricName: `/cpu\..*/`,
  193. },
  194. {
  195. MetricNames: []string{"cpu.util*", "memory.util*"},
  196. },
  197. {
  198. MetricName: "cpu.utilization",
  199. Dimensions: map[string]any{
  200. "container_name": "/^[A-Z][A-Z]$/",
  201. },
  202. },
  203. },
  204. IncludeMetrics: []dpfilters.MetricFilter{
  205. {
  206. MetricName: "metric1",
  207. },
  208. {
  209. MetricNames: []string{"metric2", "metric3"},
  210. },
  211. },
  212. DeltaTranslationTTL: 3600,
  213. ExcludeProperties: []dpfilters.PropertyFilter{
  214. {
  215. PropertyName: mustStringFilter(t, "globbed*"),
  216. },
  217. {
  218. PropertyValue: mustStringFilter(t, "!globbed*value"),
  219. },
  220. {
  221. DimensionName: mustStringFilter(t, "globbed*"),
  222. },
  223. {
  224. DimensionValue: mustStringFilter(t, "!globbed*value"),
  225. },
  226. {
  227. PropertyName: mustStringFilter(t, "globbed*"),
  228. PropertyValue: mustStringFilter(t, "!globbed*value"),
  229. DimensionName: mustStringFilter(t, "globbed*"),
  230. DimensionValue: mustStringFilter(t, "!globbed*value"),
  231. },
  232. },
  233. Correlation: &correlation.Config{
  234. HTTPClientSettings: confighttp.HTTPClientSettings{
  235. Endpoint: "",
  236. Timeout: 5 * time.Second,
  237. },
  238. StaleServiceTimeout: 5 * time.Minute,
  239. SyncAttributes: map[string]string{
  240. "k8s.pod.uid": "k8s.pod.uid",
  241. "container.id": "container.id",
  242. },
  243. Config: apmcorrelation.Config{
  244. MaxRequests: 20,
  245. MaxBuffered: 10_000,
  246. MaxRetries: 2,
  247. LogUpdates: false,
  248. RetryDelay: 30 * time.Second,
  249. CleanupInterval: 1 * time.Minute,
  250. },
  251. },
  252. NonAlphanumericDimensionChars: "_-.",
  253. },
  254. },
  255. }
  256. for _, tt := range tests {
  257. t.Run(tt.id.String(), func(t *testing.T) {
  258. factory := NewFactory()
  259. cfg := factory.CreateDefaultConfig()
  260. sub, err := cm.Sub(tt.id.String())
  261. require.NoError(t, err)
  262. require.NoError(t, component.UnmarshalConfig(sub, cfg))
  263. assert.NoError(t, component.ValidateConfig(cfg))
  264. // We need to add the default exclude rules.
  265. assert.NoError(t, setDefaultExcludes(tt.expected))
  266. assert.Equal(t, tt.expected, cfg)
  267. })
  268. }
  269. }
  270. func TestConfigGetMetricTranslator(t *testing.T) {
  271. tests := []struct {
  272. name string
  273. cfg *Config
  274. want *translation.MetricTranslator
  275. wantErr bool
  276. }{
  277. {
  278. name: "Test empty config",
  279. cfg: &Config{
  280. DeltaTranslationTTL: 3600,
  281. },
  282. want: func() *translation.MetricTranslator {
  283. translator, err := translation.NewMetricTranslator(defaultTranslationRules, 3600)
  284. require.NoError(t, err)
  285. return translator
  286. }(),
  287. },
  288. {
  289. name: "Test empty rules",
  290. cfg: &Config{
  291. TranslationRules: []translation.Rule{},
  292. DeltaTranslationTTL: 3600,
  293. },
  294. want: func() *translation.MetricTranslator {
  295. translator, err := translation.NewMetricTranslator([]translation.Rule{}, 3600)
  296. require.NoError(t, err)
  297. return translator
  298. }(),
  299. },
  300. {
  301. name: "Test disable rules",
  302. cfg: &Config{
  303. DisableDefaultTranslationRules: true,
  304. DeltaTranslationTTL: 3600,
  305. },
  306. want: func() *translation.MetricTranslator {
  307. translator, err := translation.NewMetricTranslator([]translation.Rule{}, 3600)
  308. require.NoError(t, err)
  309. return translator
  310. }(),
  311. },
  312. {
  313. name: "Test disable rules overrides rules",
  314. cfg: &Config{
  315. TranslationRules: []translation.Rule{{Action: translation.ActionDropDimensions}},
  316. DisableDefaultTranslationRules: true,
  317. DeltaTranslationTTL: 3600,
  318. },
  319. want: func() *translation.MetricTranslator {
  320. translator, err := translation.NewMetricTranslator([]translation.Rule{}, 3600)
  321. require.NoError(t, err)
  322. return translator
  323. }(),
  324. },
  325. {
  326. name: "Test invalid translation rules",
  327. cfg: &Config{
  328. Realm: "us0",
  329. AccessToken: "access_token",
  330. TranslationRules: []translation.Rule{
  331. {
  332. Action: translation.ActionRenameDimensionKeys,
  333. },
  334. },
  335. DeltaTranslationTTL: 3600,
  336. },
  337. wantErr: true,
  338. },
  339. }
  340. for _, tt := range tests {
  341. t.Run(tt.name, func(t *testing.T) {
  342. got, err := tt.cfg.getMetricTranslator(zap.NewNop())
  343. if tt.wantErr {
  344. assert.Error(t, err)
  345. return
  346. }
  347. require.NoError(t, err)
  348. assert.Equal(t, tt.want, got)
  349. })
  350. }
  351. }
  352. func TestConfigGetIngestURL(t *testing.T) {
  353. tests := []struct {
  354. name string
  355. cfg *Config
  356. want *url.URL
  357. wantErr bool
  358. }{
  359. {
  360. name: "Test URL from Realm",
  361. cfg: &Config{
  362. Realm: "us0",
  363. },
  364. want: &url.URL{
  365. Scheme: "https",
  366. Host: "ingest.us0.signalfx.com",
  367. Path: "",
  368. },
  369. },
  370. {
  371. name: "Test URL overrides",
  372. cfg: &Config{
  373. Realm: "us0",
  374. IngestURL: "https://ingest.us1.signalfx.com/",
  375. },
  376. want: &url.URL{
  377. Scheme: "https",
  378. Host: "ingest.us1.signalfx.com",
  379. Path: "/",
  380. },
  381. },
  382. {
  383. name: "Test invalid URL",
  384. cfg: &Config{
  385. IngestURL: "https://api us1 signalfx com/",
  386. },
  387. wantErr: true,
  388. },
  389. }
  390. for _, tt := range tests {
  391. t.Run(tt.name, func(t *testing.T) {
  392. got, err := tt.cfg.getIngestURL()
  393. if tt.wantErr {
  394. assert.Error(t, err)
  395. return
  396. }
  397. require.NoError(t, err)
  398. assert.Equal(t, tt.want, got)
  399. })
  400. }
  401. }
  402. func TestConfigGetAPIURL(t *testing.T) {
  403. tests := []struct {
  404. name string
  405. cfg *Config
  406. want *url.URL
  407. wantErr bool
  408. }{
  409. {
  410. name: "Test URL from Realm",
  411. cfg: &Config{
  412. Realm: "us0",
  413. },
  414. want: &url.URL{
  415. Scheme: "https",
  416. Host: "api.us0.signalfx.com",
  417. },
  418. },
  419. {
  420. name: "Test URL overrides",
  421. cfg: &Config{
  422. Realm: "us0",
  423. APIURL: "https://api.us1.signalfx.com/",
  424. },
  425. want: &url.URL{
  426. Scheme: "https",
  427. Host: "api.us1.signalfx.com",
  428. Path: "/",
  429. },
  430. },
  431. {
  432. name: "Test invalid URL",
  433. cfg: &Config{
  434. APIURL: "https://api us1 signalfx com/",
  435. },
  436. wantErr: true,
  437. },
  438. }
  439. for _, tt := range tests {
  440. t.Run(tt.name, func(t *testing.T) {
  441. got, err := tt.cfg.getAPIURL()
  442. if tt.wantErr {
  443. assert.Error(t, err)
  444. return
  445. }
  446. require.NoError(t, err)
  447. assert.Equal(t, tt.want, got)
  448. })
  449. }
  450. }
  451. func TestConfigValidateErrors(t *testing.T) {
  452. tests := []struct {
  453. name string
  454. cfg *Config
  455. }{
  456. {
  457. name: "Test empty config",
  458. cfg: &Config{},
  459. },
  460. {
  461. name: "Test empty realm and API URL",
  462. cfg: &Config{
  463. AccessToken: "access_token",
  464. IngestURL: "https://ingest.us1.signalfx.com/",
  465. },
  466. },
  467. {
  468. name: "Test empty realm and Ingest URL",
  469. cfg: &Config{
  470. AccessToken: "access_token",
  471. APIURL: "https://api.us1.signalfx.com/",
  472. },
  473. },
  474. {
  475. name: "Negative Timeout",
  476. cfg: &Config{
  477. Realm: "us0",
  478. AccessToken: "access_token",
  479. HTTPClientSettings: confighttp.HTTPClientSettings{Timeout: -1 * time.Second},
  480. },
  481. },
  482. {
  483. name: "Negative QueueSize",
  484. cfg: &Config{
  485. Realm: "us0",
  486. AccessToken: "access_token",
  487. QueueSettings: exporterhelper.QueueSettings{
  488. Enabled: true,
  489. QueueSize: -1,
  490. },
  491. },
  492. },
  493. }
  494. for _, tt := range tests {
  495. t.Run(tt.name, func(t *testing.T) {
  496. assert.Error(t, component.ValidateConfig(tt.cfg))
  497. })
  498. }
  499. }
  500. func TestUnmarshalExcludeMetrics(t *testing.T) {
  501. tests := []struct {
  502. name string
  503. cfg *Config
  504. excludeMetricsLen int
  505. }{
  506. {
  507. name: "empty config",
  508. cfg: &Config{},
  509. excludeMetricsLen: 12,
  510. },
  511. {
  512. name: "existing exclude config",
  513. cfg: &Config{
  514. ExcludeMetrics: []dpfilters.MetricFilter{
  515. {
  516. MetricNames: []string{"metric1"},
  517. },
  518. },
  519. },
  520. excludeMetricsLen: 13,
  521. },
  522. {
  523. name: "existing empty exclude config",
  524. cfg: &Config{
  525. ExcludeMetrics: []dpfilters.MetricFilter{},
  526. },
  527. excludeMetricsLen: 0,
  528. },
  529. }
  530. for _, tt := range tests {
  531. t.Run(tt.name, func(t *testing.T) {
  532. require.NoError(t, tt.cfg.Unmarshal(confmap.NewFromStringMap(map[string]any{})))
  533. assert.Len(t, tt.cfg.ExcludeMetrics, tt.excludeMetricsLen)
  534. })
  535. }
  536. }
  537. func mustStringFilter(t *testing.T, filter string) *dpfilters.StringFilter {
  538. sf, err := dpfilters.NewStringFilter([]string{filter})
  539. require.NoError(t, err)
  540. return sf
  541. }