diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d1a5eb2b07..4ddc9358b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/CODEOWNERS b/CODEOWNERS index e3d4f0ec39e..bb111d307fd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/detectors/azure/azurevm/README.md b/detectors/azure/azurevm/README.md new file mode 100644 index 00000000000..f5b09e28f89 --- /dev/null +++ b/detectors/azure/azurevm/README.md @@ -0,0 +1,3 @@ +# Azure VM Resource detector + + diff --git a/detectors/azure/azurevm/doc.go b/detectors/azure/azurevm/doc.go new file mode 100644 index 00000000000..779068e92d9 --- /dev/null +++ b/detectors/azure/azurevm/doc.go @@ -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" diff --git a/detectors/azure/azurevm/example_new_test.go b/detectors/azure/azurevm/example_new_test.go new file mode 100644 index 00000000000..a5f34f34ada --- /dev/null +++ b/detectors/azure/azurevm/example_new_test.go @@ -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()) +} diff --git a/detectors/azure/azurevm/go.mod b/detectors/azure/azurevm/go.mod new file mode 100644 index 00000000000..56931217416 --- /dev/null +++ b/detectors/azure/azurevm/go.mod @@ -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 +) diff --git a/detectors/azure/azurevm/go.sum b/detectors/azure/azurevm/go.sum new file mode 100644 index 00000000000..377a5231a9f --- /dev/null +++ b/detectors/azure/azurevm/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.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= diff --git a/detectors/azure/azurevm/vm.go b/detectors/azure/azurevm/vm.go new file mode 100644 index 00000000000..7787427ea08 --- /dev/null +++ b/detectors/azure/azurevm/vm.go @@ -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)) +} diff --git a/detectors/azure/azurevm/vm_test.go b/detectors/azure/azurevm/vm_test.go new file mode 100644 index 00000000000..035ca6e318d --- /dev/null +++ b/detectors/azure/azurevm/vm_test.go @@ -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) + } +} diff --git a/versions.yaml b/versions.yaml index d11eba4e4fc..26e0311c742 100644 --- a/versions.yaml +++ b/versions.yaml @@ -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