Skip to content

Commit 9709d07

Browse files
committed
[receiver/azuremonitorreceiver] feat: Allow to not split result by dimension
Signed-off-by: Célian Garcia <[email protected]>
1 parent c645253 commit 9709d07

File tree

6 files changed

+301
-36
lines changed

6 files changed

+301
-36
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: receiver/azuremonitorreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Add dimensions.enabled and dimensions.overrides which allows to opt out from automatically split by all the dimensions of the resource type"
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: [36611]
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: []

receiver/azuremonitorreceiver/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ The following settings are optional:
3131
- `maximum_number_of_records_per_resource` (default = 10): Maximum number of records to fetch per resource.
3232
- `initial_delay` (default = `1s`): defines how long this receiver waits before starting.
3333
- `cloud` (default = `AzureCloud`): defines which Azure cloud to use. Valid values: `AzureCloud`, `AzureUSGovernment`, `AzureChinaCloud`.
34+
- `dimensions.enabled` (default = `true`): allows to opt out from automatically split by all the dimensions of the resource type.
35+
- `dimensions.overrides` (default = `{}`): if dimensions are enabled, it allows you to specify a set of dimensions for a particular metric. This is a two levels map with first key being the resource type and second key being the metric name. Programmatic value should be used for metric name https://learn.microsoft.com/en-us/azure/azure-monitor/reference/metrics-index
3436

3537
Authenticating using service principal requires following additional settings:
3638

@@ -101,6 +103,22 @@ receivers:
101103
auth: "default_credentials"
102104
```
103105
106+
Overriding dimensions for a particular metric:
107+
```yaml
108+
receivers:
109+
azuremonitor:
110+
dimensions:
111+
enabled: true
112+
overrides:
113+
"Microsoft.Network/azureFirewalls":
114+
# Real example of an Azure limitation here:
115+
# Dimensions exposed are Reason, Status, Protocol,
116+
# but when selecting Protocol in the filters, it returns nothing.
117+
# Note here that the metric display name is ``Network rules hit count`` but it's programmatic value is ``NetworkRuleHit``
118+
# Ref: https://learn.microsoft.com/en-us/azure/azure-monitor/reference/supported-metrics/microsoft-network-azurefirewalls-metrics
119+
"NetworkRuleHit": [Reason, Status]
120+
```
121+
104122
105123
## Metrics
106124

receiver/azuremonitorreceiver/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ var (
228228
}
229229
)
230230

231+
type DimensionsConfig struct {
232+
Enabled *bool `mapstructure:"enabled"`
233+
Overrides map[string]map[string][]string `mapstructure:"overrides"`
234+
}
235+
231236
// Config defines the configuration for the various elements of the receiver agent.
232237
type Config struct {
233238
scraperhelper.ControllerConfig `mapstructure:",squash"`
@@ -246,6 +251,7 @@ type Config struct {
246251
MaximumNumberOfMetricsInACall int `mapstructure:"maximum_number_of_metrics_in_a_call"`
247252
MaximumNumberOfRecordsPerResource int32 `mapstructure:"maximum_number_of_records_per_resource"`
248253
AppendTagsAsAttributes bool `mapstructure:"append_tags_as_attributes"`
254+
Dimensions DimensionsConfig `mapstructure:"dimensions"`
249255
}
250256

251257
const (
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package azuremonitorreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/azuremonitorreceiver"
5+
6+
import (
7+
"testing"
8+
9+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
10+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func newDimension(value string) *armmonitor.LocalizableString {
15+
return to.Ptr(armmonitor.LocalizableString{Value: to.Ptr(value)})
16+
}
17+
18+
func TestFilterDimensions(t *testing.T) {
19+
type args struct {
20+
dimensions []*armmonitor.LocalizableString
21+
cfg DimensionsConfig
22+
resourceType string
23+
metricName string
24+
}
25+
26+
tests := []struct {
27+
name string
28+
args args
29+
expected []string
30+
}{
31+
{
32+
name: "always empty if dimensions disabled",
33+
args: args{
34+
dimensions: []*armmonitor.LocalizableString{
35+
newDimension("foo"),
36+
newDimension("bar"),
37+
},
38+
cfg: DimensionsConfig{
39+
Enabled: to.Ptr(false),
40+
},
41+
resourceType: "rt1",
42+
metricName: "m1",
43+
},
44+
expected: nil,
45+
},
46+
{
47+
name: "split by dimensions should be enabled by default",
48+
args: args{
49+
dimensions: []*armmonitor.LocalizableString{
50+
newDimension("foo"),
51+
newDimension("bar"),
52+
},
53+
cfg: DimensionsConfig{}, // enabled by default
54+
resourceType: "rt1",
55+
metricName: "m1",
56+
},
57+
expected: []string{"foo", "bar"},
58+
},
59+
{
60+
name: "overrides takes precedence over input",
61+
args: args{
62+
dimensions: []*armmonitor.LocalizableString{
63+
newDimension("foo"),
64+
newDimension("bar"),
65+
},
66+
cfg: DimensionsConfig{
67+
Enabled: to.Ptr(true),
68+
Overrides: map[string]map[string][]string{
69+
"rt1": {
70+
"m1": {
71+
"foo",
72+
},
73+
},
74+
},
75+
},
76+
resourceType: "rt1",
77+
metricName: "m1",
78+
},
79+
expected: []string{"foo"},
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
actual := filterDimensions(tt.args.dimensions, tt.args.cfg, tt.args.resourceType, tt.args.metricName)
86+
require.Equal(t, tt.expected, actual)
87+
})
88+
}
89+
}
90+
91+
func TestBuildDimensionsFilter(t *testing.T) {
92+
type args struct {
93+
dimensionsStr string
94+
}
95+
96+
tests := []struct {
97+
name string
98+
args args
99+
expected *string
100+
}{
101+
{
102+
name: "empty given dimensions string",
103+
args: args{
104+
dimensionsStr: "",
105+
},
106+
expected: nil,
107+
},
108+
{
109+
name: "build dimensions filter",
110+
args: args{
111+
dimensionsStr: "bar,foo",
112+
},
113+
expected: to.Ptr("bar eq '*' and foo eq '*'"),
114+
},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
actual := buildDimensionsFilter(tt.args.dimensionsStr)
120+
require.EqualValues(t, tt.expected, actual)
121+
})
122+
}
123+
}
124+
125+
func TestSerializeDimensions(t *testing.T) {
126+
type args struct {
127+
dimensions []string
128+
}
129+
130+
tests := []struct {
131+
name string
132+
args args
133+
expected string
134+
}{
135+
{
136+
name: "empty given dimensions",
137+
args: args{
138+
dimensions: []string{},
139+
},
140+
expected: "",
141+
},
142+
{
143+
name: "nil given dimensions",
144+
args: args{
145+
dimensions: []string{},
146+
},
147+
expected: "",
148+
},
149+
{
150+
name: "reorder dimensions",
151+
args: args{
152+
dimensions: []string{"foo", "bar"},
153+
},
154+
expected: "bar,foo",
155+
},
156+
{
157+
name: "trim spaces dimensions",
158+
args: args{
159+
dimensions: []string{" bar", "foo "},
160+
},
161+
expected: "bar,foo",
162+
},
163+
}
164+
165+
for _, tt := range tests {
166+
t.Run(tt.name, func(t *testing.T) {
167+
actual := serializeDimensions(tt.args.dimensions)
168+
require.EqualValues(t, tt.expected, actual)
169+
})
170+
}
171+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package azuremonitorreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/azuremonitorreceiver"
5+
6+
import (
7+
"bytes"
8+
"sort"
9+
"strings"
10+
11+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor"
12+
)
13+
14+
// filterDimensions transforms a list of azure dimensions into a list of string, taking in account the DimensionConfig
15+
// given by the user.
16+
func filterDimensions(dimensions []*armmonitor.LocalizableString, cfg DimensionsConfig, resourceType, metricName string) []string {
17+
// Only skip if explicitly disabled. Enabled by default.
18+
if cfg.Enabled != nil && !*cfg.Enabled {
19+
return nil
20+
}
21+
22+
// If dimensions are overridden for that resource type and metric name, we take it
23+
if _, resourceTypeFound := cfg.Overrides[resourceType]; resourceTypeFound {
24+
if newDimensions, metricNameFound := cfg.Overrides[resourceType][metricName]; metricNameFound {
25+
return newDimensions
26+
}
27+
}
28+
// Otherwise we get all dimensions
29+
var result []string
30+
for _, dimension := range dimensions {
31+
result = append(result, *dimension.Value)
32+
}
33+
return result
34+
}
35+
36+
// serializeDimensions build a comma separated string from trimmed, sorted dimensions list.
37+
// It is designed to be used as a key in scraper maps.
38+
func serializeDimensions(dimensions []string) string {
39+
var dimensionsSlice []string
40+
for _, dimension := range dimensions {
41+
if trimmedDimension := strings.TrimSpace(dimension); len(trimmedDimension) > 0 {
42+
dimensionsSlice = append(dimensionsSlice, trimmedDimension)
43+
}
44+
}
45+
sort.Strings(dimensionsSlice)
46+
return strings.Join(dimensionsSlice, ",")
47+
}
48+
49+
// buildDimensionsFilter takes a serialized dimensions input to build an Azure Request filter that will allow us to
50+
// receive metrics values split by these dimensions.
51+
func buildDimensionsFilter(dimensionsStr string) *string {
52+
if len(dimensionsStr) == 0 {
53+
return nil
54+
}
55+
var dimensionsFilter bytes.Buffer
56+
dimensions := strings.Split(dimensionsStr, ",")
57+
for i, dimension := range dimensions {
58+
dimensionsFilter.WriteString(dimension)
59+
dimensionsFilter.WriteString(" eq '*'")
60+
if i < len(dimensions)-1 {
61+
dimensionsFilter.WriteString(" and ")
62+
}
63+
}
64+
result := dimensionsFilter.String()
65+
return &result
66+
}

0 commit comments

Comments
 (0)