Skip to content

[processor/resourcedetectionprocessor] Expose Azure resource tags #32953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .chloggen/feat_support_azure_tags.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: processor/resourcedetectionprocessor

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add support for Azure tags in ResourceDetectionProcessor.

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

# (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: [user]
20 changes: 13 additions & 7 deletions internal/metadataproviders/azure/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,21 @@ func NewProvider() Provider {
}
}

type ComputeTagsListMetadata struct {
Name string `json:"name"`
Value string `json:"value"`
}

// ComputeMetadata is the Azure IMDS compute metadata response format
type ComputeMetadata struct {
Location string `json:"location"`
Name string `json:"name"`
VMID string `json:"vmID"`
VMSize string `json:"vmSize"`
SubscriptionID string `json:"subscriptionID"`
ResourceGroupName string `json:"resourceGroupName"`
VMScaleSetName string `json:"vmScaleSetName"`
Location string `json:"location"`
Name string `json:"name"`
VMID string `json:"vmID"`
VMSize string `json:"vmSize"`
SubscriptionID string `json:"subscriptionID"`
ResourceGroupName string `json:"resourceGroupName"`
VMScaleSetName string `json:"vmScaleSetName"`
TagsList []ComputeTagsListMetadata `json:"tagsList"`
}

// Metadata queries a given endpoint and parses the output to the Azure IMDS format
Expand Down
6 changes: 6 additions & 0 deletions internal/metadataproviders/azure/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ func TestQueryEndpointCorrect(t *testing.T) {
VMSize: "vmSize",
SubscriptionID: "subscriptionID",
ResourceGroupName: "resourceGroup",
TagsList: []ComputeTagsListMetadata{
{
Name: "tag1",
Value: "value1",
},
},
}
marshalledMetadata, err := json.Marshal(sentMetadata)
require.NoError(t, err)
Expand Down
20 changes: 20 additions & 0 deletions processor/resourcedetectionprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,26 @@ processors:
override: false
```

It also can optionally gather tags from the azure instance that the collector is running on.

Azure custom configuration example:

```yaml
processors:
resourcedetection/azure:
detectors: ["azure"]
azure:
# A list of regex's to match tag keys to add as resource attributes can be specified
tags:
- ^tag1$
- ^tag2$
- ^label.*$
Comment on lines +403 to +406
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is just adding all tags acceptable in terms of performance and number? i.e. Do Azure instances typically have a lot of tags?

Asking because I would like to keep it simple: if you want to filter, you can do that in a different component in your pipeline like the transform processor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure supports a max of 50 tags per resource, how many are actually in use of course varies depending on people's use cases.
In terms of "cost", we essentially get the data for free since it is included in the resource's metadata query results, unlike on AWS which requires a secondary rest call to fetch the tag information. So we are only paying the cost of the regex matching.

When implementing this I decided to keep it aligned with how the AWS implementation was exposed to users. The thought there was if someone is in a multi-cloud environment having them both configured the same, makes it easier for them to be used but I am not glued to that implementation choice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed reply! I agree it's valuable to be aligned with the AWS detector. I guess we can always add an 'add all of them' setting if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're welcome. As for the all setting. Yup! In the interim, if people wanted to do this it would just require an unrestrictive regex .*

And CLA is finally signed. There was a hiccup on our side.

```

Matched tags are added as:

* azure.tags.<tag name>

### Azure AKS

* cloud.provider ("azure")
Expand Down
67 changes: 59 additions & 8 deletions processor/resourcedetectionprocessor/internal/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package azure // import "github.com/open-telemetry/opentelemetry-collector-contr

import (
"context"
"regexp"

"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/processor"
Expand All @@ -18,25 +19,34 @@ import (

const (
// TypeStr is type of detector.
TypeStr = "azure"
TypeStr = "azure"
tagPrefix = "azure.tag."
)

var _ internal.Detector = (*Detector)(nil)

// Detector is an Azure metadata detector
type Detector struct {
provider azure.Provider
logger *zap.Logger
rb *metadata.ResourceBuilder
provider azure.Provider
tagKeyRegexes []*regexp.Regexp
logger *zap.Logger
rb *metadata.ResourceBuilder
}

// NewDetector creates a new Azure metadata detector
func NewDetector(p processor.CreateSettings, dcfg internal.DetectorConfig) (internal.Detector, error) {
cfg := dcfg.(Config)

tagKeyRegexes, err := compileRegexes(cfg)
if err != nil {
return nil, err
}

return &Detector{
provider: azure.NewProvider(),
logger: p.Logger,
rb: metadata.NewResourceBuilder(cfg.ResourceAttributes),
provider: azure.NewProvider(),
tagKeyRegexes: tagKeyRegexes,
logger: p.Logger,
rb: metadata.NewResourceBuilder(cfg.ResourceAttributes),
}, nil
}

Expand All @@ -62,6 +72,47 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem
d.rb.SetAzureVMSize(compute.VMSize)
d.rb.SetAzureVMScalesetName(compute.VMScaleSetName)
d.rb.SetAzureResourcegroupName(compute.ResourceGroupName)
res := d.rb.Emit()

if len(d.tagKeyRegexes) != 0 {
tags := matchAzureTags(compute.TagsList, d.tagKeyRegexes)
for key, val := range tags {
res.Attributes().PutStr(tagPrefix+key, val)
}
}

return res, conventions.SchemaURL, nil
}

return d.rb.Emit(), conventions.SchemaURL, nil
func matchAzureTags(azureTags []azure.ComputeTagsListMetadata, tagKeyRegexes []*regexp.Regexp) map[string]string {
tags := make(map[string]string)
for _, tag := range azureTags {
matched := regexArrayMatch(tagKeyRegexes, tag.Name)
if matched {
tags[tag.Name] = tag.Value
}
}
return tags
}

func compileRegexes(cfg Config) ([]*regexp.Regexp, error) {
tagRegexes := make([]*regexp.Regexp, len(cfg.Tags))
for i, elem := range cfg.Tags {
regex, err := regexp.Compile(elem)
if err != nil {
return nil, err
}
tagRegexes[i] = regex
}
return tagRegexes, nil
}

func regexArrayMatch(arr []*regexp.Regexp, val string) bool {
for _, elem := range arr {
matched := elem.MatchString(val)
if matched {
return true
}
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package azure
import (
"context"
"fmt"
"regexp"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -36,9 +37,25 @@ func TestDetectAzureAvailable(t *testing.T) {
SubscriptionID: "subscriptionID",
ResourceGroupName: "resourceGroup",
VMScaleSetName: "myScaleset",
TagsList: []azure.ComputeTagsListMetadata{
{
Name: "tag1key",
Value: "value1",
},
{
Name: "tag2key",
Value: "value2",
},
},
}, nil)

detector := &Detector{provider: mp, rb: metadata.NewResourceBuilder(metadata.DefaultResourceAttributesConfig())}
detector := &Detector{
provider: mp,
tagKeyRegexes: []*regexp.Regexp{
regexp.MustCompile("^tag1key$"),
},
rb: metadata.NewResourceBuilder(metadata.DefaultResourceAttributesConfig()),
}
res, schemaURL, err := detector.Detect(context.Background())
require.NoError(t, err)
assert.Equal(t, conventions.SchemaURL, schemaURL)
Expand All @@ -55,9 +72,15 @@ func TestDetectAzureAvailable(t *testing.T) {
"azure.vm.size": "vmSize",
"azure.resourcegroup.name": "resourceGroup",
"azure.vm.scaleset.name": "myScaleset",
"azure.tag.tag1key": "value1",
}

notExpected := map[string]any{
"azure.tag.tag2key": "value2",
}

assert.Equal(t, expected, res.Attributes().AsRaw())
assert.NotEqual(t, notExpected, res.Attributes().AsRaw())
}

func TestDetectError(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import (
)

type Config struct {
// Tags is a list of regex's to match azure instance tag keys that users want
// to add as resource attributes to processed data
Tags []string `mapstructure:"tags"`
ResourceAttributes metadata.ResourceAttributesConfig `mapstructure:"resource_attributes"`
}

func CreateDefaultConfig() Config {
return Config{
Tags: []string{},
ResourceAttributes: metadata.DefaultResourceAttributesConfig(),
}
}