Skip to content

Commit fe6dbce

Browse files
[receiver/azuremonitorreceiver] feat: Allow to not split result by dimension (#37168)
Recreated from a diffferent fork. Context: #36240 > <!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. > Ex. Adding a feature - Explain what this achieves.--> > #### Description > > Currently there is a mechanism allowing to split the result by dimension, thanks to a filter param hack. > This is great that all the dimensions are collected as labels in the metrics, but for some resources types it could be unwanted. In cause some concern about cardinality, or continuity of the queries between one version to another (e.g with prometheus exporter, if one does not do a "sum by ..." and the otelcol version is updated, the query can display different results) > > To mitigate that I propose to put an optout for this collection so if we want for some resource types and not for others, we can create two separate receivers for example. Or we completely opt out if we don't want additional labels. > > ### Edit: > After a first review, we agreed on the fact that it could do a bit more like allowing us to specify a list of dimensions for a particular metric. > e.g from added documentation > ```yaml > receivers: > azuremonitor: > dimensions: > enabled: true > overrides: > "Microsoft.Network/azureFirewalls": > # Real example of an Azure limitation here: > # Dimensions exposed are Reason, Status, Protocol, > # but when selecting Protocol in the filters, it returns nothing. > # Note here that the metric display name is ``Network rules hit count`` but it's programmatic value is ``NetworkRuleHit`` > # Ref: https://learn.microsoft.com/en-us/azure/azure-monitor/reference/supported-metrics/microsoft-network-azurefirewalls-metrics > "NetworkRuleHit": [Reason, Status] > ``` > Without this config you won't have the ``azure_networkrulehit_average_Count`` metric at all in your results > But with the config we have it with reason and status labels: > > ```# HELP azure_networkrulehit_average_Count``` > ```# TYPE azure_networkrulehit_average_Count gauge``` > ```azure_networkrulehit_average_Count{azuremonitor_resource_id="/subscriptions/<redacted>/resourceGroups/<redacted>/providers/Microsoft.Network/azureFirewalls/<redacted>",location="<redacted>",metadata_reason="<redacted>",metadata_status="Allow",name="<redacted>",resource_group="<redacted>",type="Microsoft.Network/azureFirewalls"} 21.875``` > ```azure_networkrulehit_average_Count{azuremonitor_resource_id="/subscriptions/<redacted>/resourceGroups/<redacted>/providers/Microsoft.Network/azureFirewalls/<redacted>",location="<redacted>",metadata_reason="<redacted>",metadata_status="Deny",name="<redacted>",resource_group="<redacted>",type="Microsoft.Network/azureFirewalls"} 10``` > > <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> > #### Link to tracking issue > Fixes #36611 > > <!--Describe what testing was performed and which tests were added.--> > #### Testing > > <!--Describe the documentation added.--> > #### Documentation > > <!--Please delete paragraphs that you did not use before submitting.--> Signed-off-by: Célian Garcia <[email protected]>
1 parent db099f2 commit fe6dbce

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)