Skip to content

Commit ee3a095

Browse files
committed
fix(azuremonitorreceiver): Azure Monitor receiver should not produce gaps in data points for PT1M time grains
1 parent a204ecf commit ee3a095

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: azuremonitorreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Azure Monitor receiver doesn't honor time grain"
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [37337]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]
28+

receiver/azuremonitorreceiver/scraper.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const (
5757
attributeResourceType = "type"
5858
metadataPrefix = "metadata_"
5959
tagPrefix = "tags_"
60+
truncateTimeGrain = time.Minute
6061
)
6162

6263
type azureResource struct {
@@ -78,6 +79,16 @@ type azureResourceMetrics struct {
7879

7980
type void struct{}
8081

82+
type timeNowIface interface {
83+
Now() time.Time
84+
}
85+
86+
type timeWrapper struct{}
87+
88+
func (*timeWrapper) Now() time.Time {
89+
return time.Now()
90+
}
91+
8192
func newScraper(conf *Config, settings receiver.Settings) *azureScraper {
8293
return &azureScraper{
8394
cfg: conf,
@@ -91,6 +102,7 @@ func newScraper(conf *Config, settings receiver.Settings) *azureScraper {
91102
armMonitorDefinitionsClientFunc: armmonitor.NewMetricDefinitionsClient,
92103
armMonitorMetricsClientFunc: armmonitor.NewMetricsClient,
93104
mutex: &sync.Mutex{},
105+
time: &timeWrapper{},
94106
}
95107
}
96108

@@ -115,6 +127,7 @@ type azureScraper struct {
115127
armMonitorDefinitionsClientFunc func(string, azcore.TokenCredential, *arm.ClientOptions) (*armmonitor.MetricDefinitionsClient, error)
116128
armMonitorMetricsClientFunc func(string, azcore.TokenCredential, *arm.ClientOptions) (*armmonitor.MetricsClient, error)
117129
mutex *sync.Mutex
130+
time timeNowIface
118131
}
119132

120133
type armClient interface {
@@ -369,12 +382,13 @@ func (s *azureScraper) storeMetricsDefinition(resourceID, name string, composite
369382

370383
func (s *azureScraper) getResourceMetricsValues(ctx context.Context, resourceID string) {
371384
res := *s.resources[resourceID]
385+
updatedAt := s.time.Now().Truncate(truncateTimeGrain)
372386

373387
for compositeKey, metricsByGrain := range res.metricsByCompositeKey {
374-
if time.Since(metricsByGrain.metricsValuesUpdated).Seconds() < float64(timeGrains[compositeKey.timeGrain]) {
388+
if updatedAt.Sub(metricsByGrain.metricsValuesUpdated).Seconds() < float64(timeGrains[compositeKey.timeGrain]) {
375389
continue
376390
}
377-
metricsByGrain.metricsValuesUpdated = time.Now()
391+
metricsByGrain.metricsValuesUpdated = updatedAt
378392

379393
start := 0
380394

receiver/azuremonitorreceiver/scraper_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"sync"
1212
"testing"
13+
"time"
1314

1415
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1516
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
@@ -65,6 +66,7 @@ func armMonitorMetricsClientFuncMock(string, azcore.TokenCredential, *arm.Client
6566

6667
func TestAzureScraperStart(t *testing.T) {
6768
cfg := createDefaultConfig().(*Config)
69+
timeMock := getTimeMock()
6870

6971
tests := []struct {
7072
name string
@@ -76,6 +78,7 @@ func TestAzureScraperStart(t *testing.T) {
7678
testFunc: func(t *testing.T) {
7779
s := &azureScraper{
7880
cfg: cfg,
81+
time: timeMock,
7982
azIDCredentialsFunc: azIDCredentialsFuncMock,
8083
azIDWorkloadFunc: azIDWorkloadFuncMock,
8184
armClientFunc: armClientFuncMock,
@@ -104,6 +107,7 @@ func TestAzureScraperStart(t *testing.T) {
104107
}
105108
s := &azureScraper{
106109
cfg: customCfg,
110+
time: timeMock,
107111
azIDCredentialsFunc: azIDCredentialsFuncMock,
108112
azIDWorkloadFunc: azIDWorkloadFuncMock,
109113
armClientFunc: armClientFuncMock,
@@ -132,6 +136,7 @@ func TestAzureScraperStart(t *testing.T) {
132136
}
133137
s := &azureScraper{
134138
cfg: customCfg,
139+
time: timeMock,
135140
azIDCredentialsFunc: azIDCredentialsFuncMock,
136141
azIDWorkloadFunc: azIDWorkloadFuncMock,
137142
armClientFunc: armClientFuncMock,
@@ -160,6 +165,7 @@ func TestAzureScraperStart(t *testing.T) {
160165
}
161166
s := &azureScraper{
162167
cfg: customCfg,
168+
time: timeMock,
163169
azIDCredentialsFunc: azIDCredentialsFuncMock,
164170
azManagedIdentityFunc: azManagedIdentityFuncMock,
165171
armClientFunc: armClientFuncMock,
@@ -188,6 +194,7 @@ func TestAzureScraperStart(t *testing.T) {
188194
}
189195
s := &azureScraper{
190196
cfg: customCfg,
197+
time: timeMock,
191198
azIDCredentialsFunc: azIDCredentialsFuncMock,
192199
azDefaultCredentialsFunc: azDefaultCredentialsFuncMock,
193200
armClientFunc: armClientFuncMock,
@@ -319,6 +326,7 @@ func TestAzureScraperScrape(t *testing.T) {
319326
clientMetricsValues: metricsValuesClientMock,
320327
mb: metadata.NewMetricsBuilder(metadata.DefaultMetricsBuilderConfig(), settings),
321328
mutex: &sync.Mutex{},
329+
time: getTimeMock(),
322330
}
323331
s.resources = map[string]*azureResource{}
324332

@@ -342,6 +350,95 @@ func TestAzureScraperScrape(t *testing.T) {
342350
}
343351
}
344352

353+
func TestAzureScraperScrapeHonorTimeGrain(t *testing.T) {
354+
getTestScraper := func() *azureScraper {
355+
armClientMock := &armClientMock{
356+
current: 0,
357+
pages: getResourcesMockData(false),
358+
}
359+
counters, pages := getMetricsDefinitionsMockData()
360+
metricsDefinitionsClientMock := &metricsDefinitionsClientMock{
361+
current: counters,
362+
pages: pages,
363+
}
364+
metricsValuesClientMock := &metricsValuesClientMock{
365+
lists: getMetricsValuesMockData(),
366+
}
367+
368+
return &azureScraper{
369+
cfg: createDefaultConfig().(*Config),
370+
clientResources: armClientMock,
371+
clientMetricsDefinitions: metricsDefinitionsClientMock,
372+
clientMetricsValues: metricsValuesClientMock,
373+
mb: metadata.NewMetricsBuilder(
374+
metadata.DefaultMetricsBuilderConfig(),
375+
receivertest.NewNopSettings(),
376+
),
377+
mutex: &sync.Mutex{},
378+
resources: map[string]*azureResource{},
379+
time: getTimeMock(),
380+
}
381+
}
382+
383+
ctx := context.Background()
384+
385+
t.Run("do_not_fetch_in_same_interval", func(t *testing.T) {
386+
s := getTestScraper()
387+
388+
metrics, err := s.scrape(ctx)
389+
390+
require.NoError(t, err, "should not fail")
391+
require.Positive(t, metrics.MetricCount(), "should return metrics on first call")
392+
393+
metrics, err = s.scrape(ctx)
394+
395+
require.NoError(t, err, "should not fail")
396+
require.Equal(t, 0, metrics.MetricCount(), "should not return metrics on second call")
397+
})
398+
399+
t.Run("fetch_each_new_interval", func(t *testing.T) {
400+
timeJitter := time.Second
401+
timeInterval := 50 * time.Second
402+
timeIntervals := []time.Time{
403+
time.Now().Add(time.Minute),
404+
time.Now().Add(time.Minute + 1*timeInterval - timeJitter),
405+
time.Now().Add(time.Minute + 2*timeInterval + timeJitter),
406+
time.Now().Add(time.Minute + 3*timeInterval - timeJitter),
407+
time.Now().Add(time.Minute + 4*timeInterval + timeJitter),
408+
}
409+
s := getTestScraper()
410+
mockedTime := s.time.(*timeMock)
411+
412+
for _, timeNowNew := range timeIntervals {
413+
// implementation uses time.Sub to check if timeWrapper.Now satisfy time grain
414+
// we can travel in time by adjusting result of timeWrapper.Now
415+
prevTime := mockedTime.time
416+
mockedTime.time = timeNowNew
417+
418+
metrics, err := s.scrape(ctx)
419+
420+
require.NoError(t, err, "should not fail")
421+
if prevTime.Minute() == timeNowNew.Minute() {
422+
require.Equal(t, 0, metrics.MetricCount(), "should not fetch metrics in the same minute")
423+
} else {
424+
require.Positive(t, metrics.MetricCount(), "should fetch metrics in a new minute")
425+
}
426+
}
427+
})
428+
}
429+
430+
type timeMock struct {
431+
time time.Time
432+
}
433+
434+
func (t *timeMock) Now() time.Time {
435+
return t.time
436+
}
437+
438+
func getTimeMock() timeNowIface {
439+
return &timeMock{time: time.Now()}
440+
}
441+
345442
func getResourcesMockData(tags bool) []armresources.ClientListResponse {
346443
id1, id2, id3, location1, name1, type1 := "/resourceGroups/group1/resourceId1",
347444
"/resourceGroups/group1/resourceId2", "/resourceGroups/group1/resourceId3", "location1", "name1", "type1"

0 commit comments

Comments
 (0)