Skip to content

Commit

Permalink
[receiver/azuremonitorreceiver] feat: Allow to not split result by di…
Browse files Browse the repository at this point in the history
…mension

Signed-off-by: Célian Garcia <[email protected]>
  • Loading branch information
celian-garcia committed Dec 2, 2024
1 parent bc7d967 commit 221eded
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 36 deletions.
27 changes: 27 additions & 0 deletions .chloggen/split-dimensions-optout.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: receiver/azuremonitorreceiver

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Add dimensions.enabled and dimensions.overrides which allows to opt out from automatically split by all the dimensions of the resource type"

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [36240]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
18 changes: 18 additions & 0 deletions receiver/azuremonitorreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The following settings are optional:
- `maximum_number_of_records_per_resource` (default = 10): Maximum number of records to fetch per resource.
- `initial_delay` (default = `1s`): defines how long this receiver waits before starting.
- `cloud` (default = `AzureCloud`): defines which Azure cloud to use. Valid values: `AzureCloud`, `AzureUSGovernment`, `AzureChinaCloud`.
- `dimensions.enabled` (default = `true`): allows to opt out from automatically split by all the dimensions of the resource type.
- `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

Authenticating using service principal requires following additional settings:

Expand Down Expand Up @@ -101,6 +103,22 @@ receivers:
auth: "default_credentials"
```
Overriding dimensions for a particular metric:
```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]
```
## Metrics
Expand Down
6 changes: 6 additions & 0 deletions receiver/azuremonitorreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ var (
}
)

type DimensionsConfig struct {
Enabled *bool `mapstructure:"enabled"`
Overrides map[string]map[string][]string `mapstructure:"overrides"`
}

// Config defines the configuration for the various elements of the receiver agent.
type Config struct {
scraperhelper.ControllerConfig `mapstructure:",squash"`
Expand All @@ -246,6 +251,7 @@ type Config struct {
MaximumNumberOfMetricsInACall int `mapstructure:"maximum_number_of_metrics_in_a_call"`
MaximumNumberOfRecordsPerResource int32 `mapstructure:"maximum_number_of_records_per_resource"`
AppendTagsAsAttributes bool `mapstructure:"append_tags_as_attributes"`
Dimensions DimensionsConfig `mapstructure:"dimensions"`
}

const (
Expand Down
171 changes: 171 additions & 0 deletions receiver/azuremonitorreceiver/dimension_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package azuremonitorreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/azuremonitorreceiver"

import (
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor"
"github.com/stretchr/testify/require"
)

func newDimension(value string) *armmonitor.LocalizableString {
return to.Ptr(armmonitor.LocalizableString{Value: to.Ptr(value)})
}

func TestFilterDimensions(t *testing.T) {
type args struct {
dimensions []*armmonitor.LocalizableString
cfg DimensionsConfig
resourceType string
metricName string
}

tests := []struct {
name string
args args
expected []string
}{
{
name: "always empty if dimensions disabled",
args: args{
dimensions: []*armmonitor.LocalizableString{
newDimension("foo"),
newDimension("bar"),
},
cfg: DimensionsConfig{
Enabled: to.Ptr(false),
},
resourceType: "rt1",
metricName: "m1",
},
expected: nil,
},
{
name: "split by dimensions should be enabled by default",
args: args{
dimensions: []*armmonitor.LocalizableString{
newDimension("foo"),
newDimension("bar"),
},
cfg: DimensionsConfig{}, // enabled by default
resourceType: "rt1",
metricName: "m1",
},
expected: []string{"foo", "bar"},
},
{
name: "overrides takes precedence over input",
args: args{
dimensions: []*armmonitor.LocalizableString{
newDimension("foo"),
newDimension("bar"),
},
cfg: DimensionsConfig{
Enabled: to.Ptr(true),
Overrides: map[string]map[string][]string{
"rt1": {
"m1": {
"foo",
},
},
},
},
resourceType: "rt1",
metricName: "m1",
},
expected: []string{"foo"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := filterDimensions(tt.args.dimensions, tt.args.cfg, tt.args.resourceType, tt.args.metricName)
require.Equal(t, tt.expected, actual)
})
}
}

func TestBuildDimensionsFilter(t *testing.T) {
type args struct {
dimensionsStr string
}

tests := []struct {
name string
args args
expected *string
}{
{
name: "empty given dimensions string",
args: args{
dimensionsStr: "",
},
expected: nil,
},
{
name: "build dimensions filter",
args: args{
dimensionsStr: "bar,foo",
},
expected: to.Ptr("bar eq '*' and foo eq '*'"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := buildDimensionsFilter(tt.args.dimensionsStr)
require.EqualValues(t, tt.expected, actual)
})
}
}

func TestSerializeDimensions(t *testing.T) {
type args struct {
dimensions []string
}

tests := []struct {
name string
args args
expected string
}{
{
name: "empty given dimensions",
args: args{
dimensions: []string{},
},
expected: "",
},
{
name: "nil given dimensions",
args: args{
dimensions: []string{},
},
expected: "",
},
{
name: "reorder dimensions",
args: args{
dimensions: []string{"foo", "bar"},
},
expected: "bar,foo",
},
{
name: "trim spaces dimensions",
args: args{
dimensions: []string{" bar", "foo "},
},
expected: "bar,foo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := serializeDimensions(tt.args.dimensions)
require.EqualValues(t, tt.expected, actual)
})
}
}
66 changes: 66 additions & 0 deletions receiver/azuremonitorreceiver/dimensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package azuremonitorreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/azuremonitorreceiver"

import (
"bytes"
"sort"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor"
)

// filterDimensions transforms a list of azure dimensions into a list of string, taking in account the DimensionConfig
// given by the user.
func filterDimensions(dimensions []*armmonitor.LocalizableString, cfg DimensionsConfig, resourceType, metricName string) []string {
// Only skip if explicitly disabled. Enabled by default.
if cfg.Enabled != nil && !*cfg.Enabled {
return nil
}

// If dimensions are overridden for that resource type and metric name, we take it
if _, resourceTypeFound := cfg.Overrides[resourceType]; resourceTypeFound {
if newDimensions, metricNameFound := cfg.Overrides[resourceType][metricName]; metricNameFound {
return newDimensions
}
}
// Otherwise we get all dimensions
var result []string
for _, dimension := range dimensions {
result = append(result, *dimension.Value)
}
return result
}

// serializeDimensions build a comma separated string from trimmed, sorted dimensions list.
// It is designed to be used as a key in scraper maps.
func serializeDimensions(dimensions []string) string {
var dimensionsSlice []string
for _, dimension := range dimensions {
if trimmedDimension := strings.TrimSpace(dimension); len(trimmedDimension) > 0 {
dimensionsSlice = append(dimensionsSlice, trimmedDimension)
}
}
sort.Strings(dimensionsSlice)
return strings.Join(dimensionsSlice, ",")
}

// buildDimensionsFilter takes a serialized dimensions input to build an Azure Request filter that will allow us to
// receive metrics values split by these dimensions.
func buildDimensionsFilter(dimensionsStr string) *string {
if len(dimensionsStr) == 0 {
return nil
}
var dimensionsFilter bytes.Buffer
dimensions := strings.Split(dimensionsStr, ",")
for i, dimension := range dimensions {
dimensionsFilter.WriteString(dimension)
dimensionsFilter.WriteString(" eq '*'")
if i < len(dimensions)-1 {
dimensionsFilter.WriteString(" and ")
}
}
result := dimensionsFilter.String()
return &result
}
Loading

0 comments on commit 221eded

Please sign in to comment.