Skip to content
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

Add a resource detector for Azure VMs #5422

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Expand Up @@ -163,6 +163,15 @@ updates:
schedule:
interval: weekly
day: sunday
- package-ecosystem: gomod
directory: /detectors/azure/vm
labels:
- dependencies
- go
- Skip Changelog
schedule:
interval: weekly
day: sunday
- package-ecosystem: gomod
directory: /detectors/gcp
labels:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108)
- `NewSDK` in `go.opentelemetry.io/contrib/config` now returns a configured SDK with a valid `MeterProvider`. (#4804)
- Add the new `go.opentelemetry.io/contrib/detectors/azure/vm` package to provide a resource detector for Azure VMs. (#5421)

### Changed

Expand Down
28 changes: 28 additions & 0 deletions detectors/azure/vm/README.md
@@ -0,0 +1,28 @@
# Azure VM Resource detector

The Azure VM resource detector supports detecting attributes specific to Azure VMs.

## Usage

```golang
// Instantiate a new host resource detector
azureVmResourceDetector := vm.New()
resource, err := azureVmResourceDetector.Detect(context.Background())
```

## Supported attributes

According to semantic conventions for
[host](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/host.md),
[cloud](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/cloud.md),
and
[os](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/os.md)
attributes, each of the following attributes is added if it is available:

* `cloud.provider`
* `cloud.platform`

Choose a reason for hiding this comment

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

I see that you're also adding cloud.region and cloud.resource_id, but they're not listed here

* `host.id`
* `host.name`
* `host.type`
* `os.type`
* `os.version`
20 changes: 20 additions & 0 deletions detectors/azure/vm/go.mod
@@ -0,0 +1,20 @@
module go.opentelemetry.io/contrib/detectors/azure/vm

go 1.21

require (
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel v1.25.0
go.opentelemetry.io/otel/sdk v1.25.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
golang.org/x/sys v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
27 changes: 27 additions & 0 deletions detectors/azure/vm/go.sum
@@ -0,0 +1,27 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
144 changes: 144 additions & 0 deletions detectors/azure/vm/vm.go
@@ -0,0 +1,144 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package vm // import "go.opentelemetry.io/contrib/detectors/azure/vm"

import (
"context"
"encoding/json"
"io"
"net/http"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

type config struct {
client Client
}

func newConfig(options ...Option) config {
c := config{&azureInstanceMetadataClient{}}
for _, option := range options {
c = option.apply(c)
}

return c
}

// Option applies an Azure VM detector configuration option.
type Option interface {
apply(config) config
}

type optionFunc func(config) config

func (fn optionFunc) apply(c config) config {
return fn(c)
}

// WithClient sets the client for obtaining a Azure instance metadata JSON.
func WithClient(t Client) Option {
return optionFunc(func(c config) config {
c.client = t

return c
})
}

func (cfg config) getClient() Client {
return cfg.client
}

type resourceDetector struct {
client Client
}

type vmMetadata struct {
VMId *string `json:"vmId"`
Location *string `json:"location"`
ResourceId *string `json:"resourceId"`
Name *string `json:"name"`
VMSize *string `json:"vmSize"`
OsType *string `json:"osType"`
Version *string `json:"version"`
}

// New returns a [resource.Detector] that will detect Azure VM resources.
func New(opts ...Option) resource.Detector {
c := newConfig(opts...)
return &resourceDetector{c.getClient()}
}

// Detect detects associated resources when running on an Azure VM.
func (detector *resourceDetector) Detect(ctx context.Context) (*resource.Resource, error) {
jsonMetadata, err := detector.client.GetJSONMetadata()
if err != nil {
return nil, err
}

var metadata vmMetadata
err = json.Unmarshal(jsonMetadata, &metadata)
if err != nil {
return nil, err
}

attributes := []attribute.KeyValue{
semconv.CloudProviderAzure,
semconv.CloudPlatformAzureVM,
}

if metadata.VMId != nil {
attributes = append(attributes, semconv.HostID(*metadata.VMId))
}
if metadata.Location != nil {
attributes = append(attributes, semconv.CloudRegion(*metadata.Location))
}
if metadata.ResourceId != nil {
attributes = append(attributes, semconv.CloudResourceID(*metadata.ResourceId))
}
if metadata.Name != nil {
attributes = append(attributes, semconv.HostName(*metadata.Name))
}
if metadata.VMSize != nil {
attributes = append(attributes, semconv.HostType(*metadata.VMSize))
}
if metadata.OsType != nil {
attributes = append(attributes, semconv.OSTypeKey.String(*metadata.OsType))
}
if metadata.Version != nil {
attributes = append(attributes, semconv.OSVersion(*metadata.Version))
}

return resource.NewWithAttributes(semconv.SchemaURL, attributes...), nil
}

// Client is an interface that allows mocking for testing.
type Client interface {
GetJSONMetadata() ([]byte, error)
}

type azureInstanceMetadataClient struct{}

func (c *azureInstanceMetadataClient) GetJSONMetadata() ([]byte, error) {
PTransport := &http.Transport{Proxy: nil}

client := http.Client{Transport: PTransport}

req, err := http.NewRequest("GET", "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json", nil)
if err != nil {
return nil, err
}

req.Header.Add("Metadata", "True")

resp, err := client.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()

return io.ReadAll(resp.Body)
}
103 changes: 103 additions & 0 deletions detectors/azure/vm/vm_test.go
@@ -0,0 +1,103 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package vm

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

func TestDetect(t *testing.T) {
type input struct {
jsonMetadata string
err error
}
type expected struct {
resource *resource.Resource
err bool
}
type testCase struct {
input input
expected expected
}

testTable := []testCase{
{
input: input{
jsonMetadata: `{
"location": "us-west3",
"resourceId": "/subscriptions/sid/resourceGroups/rid/providers/pname/name",
"vmId": "43f65c49-8715-4639-88a9-be6d7eb749a5",
"name": "localhost-3",
"vmSize": "Standard_D2s_v3",
"osType": "linux",
"version": "6.5.0-26-generic"
}`,
err: nil,
},
expected: expected{
resource: resource.NewWithAttributes(semconv.SchemaURL, []attribute.KeyValue{
semconv.CloudProviderAzure,
semconv.CloudPlatformAzureVM,
semconv.CloudRegion("us-west3"),
semconv.CloudResourceID("/subscriptions/sid/resourceGroups/rid/providers/pname/name"),
semconv.HostID("43f65c49-8715-4639-88a9-be6d7eb749a5"),
semconv.HostName("localhost-3"),
semconv.HostType("Standard_D2s_v3"),
semconv.OSTypeKey.String("linux"),
semconv.OSVersion("6.5.0-26-generic"),
}...),
err: false,
},
},
{
input: input{
jsonMetadata: `{`,
err: nil,
},
expected: expected{
resource: nil,
err: true,
},
},
{
input: input{
jsonMetadata: "",
err: errors.New("cannot get metadata"),
},
expected: expected{
resource: nil,
err: true,
},
},
}

for _, tCase := range testTable {
detector := New(WithClient(&mockClient{
jsonMetadata: []byte(tCase.input.jsonMetadata),
err: tCase.input.err,
}))

azureResource, err := detector.Detect(context.Background())

assert.Equal(t, err != nil, tCase.expected.err)
assert.Equal(t, tCase.expected.resource, azureResource)
}
}

type mockClient struct {
jsonMetadata []byte
err error
}

func (c *mockClient) GetJSONMetadata() ([]byte, error) {
return c.jsonMetadata, c.err
}
1 change: 1 addition & 0 deletions versions.yaml
Expand Up @@ -20,6 +20,7 @@ module-sets:
modules:
- go.opentelemetry.io/contrib/bridges/prometheus
- go.opentelemetry.io/contrib/detectors/aws/lambda
- go.opentelemetry.io/contrib/detectors/azure/vm
- go.opentelemetry.io/contrib/exporters/autoexport
- go.opentelemetry.io/contrib/propagators/autoprop
- go.opentelemetry.io/contrib/propagators/opencensus
Expand Down