Skip to content

Commit

Permalink
Add a resource detector for Azure VMs (#5422)
Browse files Browse the repository at this point in the history
Fixes #5421 

This PR adds a resource detector for Azure VMs which sets values
according to 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)
resources:

* `cloud.provider`
* `cloud.platform`
* `host.id`
* `host.name`
* `host.type`
* `os.type`
* `os.version`

---------

Co-authored-by: David Ashpole <[email protected]>
Co-authored-by: Tyler Yahn <[email protected]>
  • Loading branch information
3 people authored Jun 16, 2024
1 parent c588e57 commit 6692488
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- The `go.opentelemetry.io/contrib/config` add support to configure periodic reader interval and timeout. (#5661)
- Add the new `go.opentelemetry.io/contrib/detectors/azure/azurevm` package to provide a resource detector for Azure VMs. (#5422)
- Add support to configure views when creating MeterProvider using the config package. (#5654)
- Add log support for the autoexport package. (#5733)
- Add support for disabling the old runtime metrics using the `OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=false` environment variable. (#5747)
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ detectors/aws/ec2 @open-te
detectors/aws/ecs @open-telemetry/go-approvers @pyohannes @akats7
detectors/aws/eks @open-telemetry/go-approvers @pyohannes
detectors/aws/lambda @open-telemetry/go-approvers @akats7
detectors/azure/ @open-telemetry/go-approvers @pyohannes
detectors/gcp/ @open-telemetry/go-approvers @dashpole

exporters/autoexport @open-telemetry/go-approvers @MikeGoldsmith @pellared
Expand Down
3 changes: 3 additions & 0 deletions detectors/azure/azurevm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Azure VM Resource detector

<!--[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/contrib/detectors/azure/azurevm)](https://pkg.go.dev/go.opentelemetry.io/contrib/detectors/azure/azurevm)-->
25 changes: 25 additions & 0 deletions detectors/azure/azurevm/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

/*
Package azurevm provides a [resource.Detector] which supports detecting
attributes specific to Azure VMs.
According to semantic conventions for [host], [cloud], and [os] attributes,
each of the following attributes is added if it is available:
- cloud.provider
- cloud.platform
- cloud.region
- cloud.resource_id
- host.id
- host.name
- host.type
- os.type
- os.version
[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
[os]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/os.md
*/
package azurevm // import "go.opentelemetry.io/contrib/detectors/azure/azurevm"
22 changes: 22 additions & 0 deletions detectors/azure/azurevm/example_new_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package azurevm_test

import (
"context"
"fmt"

"go.opentelemetry.io/contrib/detectors/azure/azurevm"
)

func ExampleNew() {
azureVMResourceDetector := azurevm.New()
resource, err := azureVMResourceDetector.Detect(context.Background())
if err != nil {
panic(err)
}

// Now, you can use the resource (e.g. pass it to a tracer or meter provider).
fmt.Println(resource.SchemaURL())
}
20 changes: 20 additions & 0 deletions detectors/azure/azurevm/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module go.opentelemetry.io/contrib/detectors/azure/azurevm

go 1.21

require (
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel v1.27.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.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.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/azurevm/go.sum
Original file line number Diff line number Diff line change
@@ -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.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
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.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
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=
114 changes: 114 additions & 0 deletions detectors/azure/azurevm/vm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

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

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

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

const defaultAzureVMMetadataEndpoint = "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json"

// ResourceDetector collects resource information of Azure VMs.
type ResourceDetector struct {
endpoint string
}

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 [ResourceDetector] that will detect Azure VM resources.
func New() *ResourceDetector {
return &ResourceDetector{defaultAzureVMMetadataEndpoint}
}

// Detect detects associated resources when running on an Azure VM.
func (detector *ResourceDetector) Detect(ctx context.Context) (*resource.Resource, error) {
jsonMetadata, runningInAzure, err := detector.getJSONMetadata(ctx)
if err != nil {
if !runningInAzure {
return resource.Empty(), 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
}

func (detector *ResourceDetector) getJSONMetadata(ctx context.Context) ([]byte, bool, error) {
pTransport := &http.Transport{Proxy: nil}

client := http.Client{Transport: pTransport}

req, err := http.NewRequestWithContext(ctx, "GET", detector.endpoint, nil)
if err != nil {
return nil, false, err
}

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

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

defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
bytes, err := io.ReadAll(resp.Body)
return bytes, true, err
}

runningInAzure := resp.StatusCode < 400 || resp.StatusCode > 499

return nil, runningInAzure, errors.New(http.StatusText(resp.StatusCode))
}
114 changes: 114 additions & 0 deletions detectors/azure/azurevm/vm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package azurevm

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"

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

func TestDetect(t *testing.T) {
type input struct {
jsonMetadata string
statusCode int
}
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"
}`,
statusCode: http.StatusOK,
},
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: `{`,
statusCode: http.StatusOK,
},
expected: expected{
resource: nil,
err: true,
},
},
{
input: input{
jsonMetadata: "",
statusCode: http.StatusNotFound,
},
expected: expected{
resource: resource.Empty(),
err: false,
},
},
{
input: input{
jsonMetadata: "",
statusCode: http.StatusInternalServerError,
},
expected: expected{
resource: nil,
err: true,
},
},
}

for _, tCase := range testTable {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tCase.input.statusCode)

if r.Header.Get("Metadata") == "True" {
fmt.Fprintf(w, tCase.input.jsonMetadata)
}
}))

detector := New()
detector.endpoint = svr.URL

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

svr.Close()

assert.Equal(t, err != nil, tCase.expected.err)
assert.Equal(t, tCase.expected.resource, azureResource)
}
}
4 changes: 4 additions & 0 deletions versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ module-sets:
version: v0.0.1
modules:
- go.opentelemetry.io/contrib/processors/baggage/baggagetrace
experimental-detectors:
version: v0.0.1
modules:
- go.opentelemetry.io/contrib/detectors/azure/azurevm
excluded-modules:
- go.opentelemetry.io/contrib/bridges/otelzap
- go.opentelemetry.io/contrib/instrgen
Expand Down

0 comments on commit 6692488

Please sign in to comment.