Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

- Add support for the `aws.ec2` resource detector in `go.opentelemetry.io/contrib/otelconf/x`. (#9139)
- Add `go.opentelemetry.io/contrib/detectors/dynatrace`, a resource detector for hosts monitored by the Dynatrace OneAgent, ported from `processor/resourcedetectionprocessor/internal/dynatrace` in `opentelemetry-collector-contrib`. It reads the OneAgent host enrichment file `dt_host_metadata.properties` and sets the `dt.entity.host`, `host.name`, and `dt.smartscape.host` attributes. (#9491)

### Fixed

Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ detectors/azure/azureappservice @open-te
detectors/azure/azurecontainerapps @open-telemetry/go-approvers @iblancasa
detectors/azure/azurefunctions @open-telemetry/go-approvers @Lewis-E
detectors/azure/azurevm @open-telemetry/go-approvers @pyohannes @paulojmdias @iblancasa
detectors/dynatrace/ @open-telemetry/go-approvers @evan-bradley @paulojmdias @iblancasa
detectors/gcp/ @open-telemetry/go-approvers @dashpole
detectors/hetzner/ @open-telemetry/go-approvers @paulojmdias
detectors/ibmcloud/vpc/ @open-telemetry/go-approvers @paulojmdias @iblancasa
Expand Down
7 changes: 7 additions & 0 deletions detectors/autodetect/autodetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"go.opentelemetry.io/contrib/detectors/aws/lambda"
"go.opentelemetry.io/contrib/detectors/azure/azurecontainerapps"
"go.opentelemetry.io/contrib/detectors/azure/azurevm"
"go.opentelemetry.io/contrib/detectors/dynatrace"
"go.opentelemetry.io/contrib/detectors/gcp"
"go.opentelemetry.io/contrib/detectors/hetzner"
"go.opentelemetry.io/contrib/detectors/ibmcloud/vpc"
Expand Down Expand Up @@ -57,6 +58,10 @@ var (
// attributes on Microsoft Azure virtual machines (see azurevm.New for
// details).
IDAzureVM = ID("azure.vm")
// IDDynatrace is the ID for the Dynatrace detector that detects resource
// attributes from the enrichment files written by the Dynatrace OneAgent
// (see dynatrace.NewResourceDetector for details).
IDDynatrace = ID("dynatrace")
// IDGCP is the ID for the GCP detector that detects resource attributes on
// Google Cloud Platform (GCP) environments (see gcp.NewDetector for
// details).
Expand Down Expand Up @@ -160,6 +165,8 @@ var (
return azurevm.New()
},

IDDynatrace: func() resource.Detector { return dynatrace.NewResourceDetector() },

IDGCP: gcp.NewDetector,

IDHetzner: func() resource.Detector { return hetzner.NewResourceDetector() },
Expand Down
15 changes: 15 additions & 0 deletions detectors/autodetect/autodetect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ func TestIBMCloudVPCDetectorRegistered(t *testing.T) {
}
}

func TestDynatraceDetectorRegistered(t *testing.T) {
detector, err := Detector(IDDynatrace)
if err != nil {
t.Fatalf("got error: %v, expected no error", err)
}

c, ok := detector.(*composite)
if !ok {
t.Fatalf("got %T, expected composite detector", detector)
}
if len(c.detectors) != 1 {
t.Fatalf("got %d detectors, expected 1 detector", len(c.detectors))
}
}

func TestOptDetectorDetect(t *testing.T) {
want := attribute.String("key", "value")
opt := resource.WithAttributes(want)
Expand Down
3 changes: 3 additions & 0 deletions detectors/autodetect/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
go.opentelemetry.io/contrib/detectors/aws/lambda v0.70.0
go.opentelemetry.io/contrib/detectors/azure/azurecontainerapps v0.17.0
go.opentelemetry.io/contrib/detectors/azure/azurevm v0.17.0
go.opentelemetry.io/contrib/detectors/dynatrace v0.17.0
go.opentelemetry.io/contrib/detectors/gcp v1.45.0
go.opentelemetry.io/contrib/detectors/hetzner v0.17.0
go.opentelemetry.io/contrib/detectors/ibmcloud/vpc v0.17.0
Expand Down Expand Up @@ -122,3 +123,5 @@ replace go.opentelemetry.io/contrib/detectors/k8sapi => ../k8sapi
replace go.opentelemetry.io/contrib/detectors/vultr => ../vultr

replace go.opentelemetry.io/contrib/detectors/aws/ec2/v2 => ../aws/ec2/v2

replace go.opentelemetry.io/contrib/detectors/dynatrace => ../dynatrace
5 changes: 5 additions & 0 deletions detectors/dynatrace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Dynatrace Resource Detector for Go

<!--[![Go Reference][goref-image]][goref-url]-->
<!--[goref-image]: https://pkg.go.dev/badge/go.opentelemetry.io/contrib/detectors/dynatrace.svg-->
<!--[goref-url]: https://pkg.go.dev/go.opentelemetry.io/contrib/detectors/dynatrace-->
38 changes: 38 additions & 0 deletions detectors/dynatrace/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

/*
Package dynatrace provides a [resource.Detector] which supports detecting
attributes of hosts monitored by the Dynatrace OneAgent.

The detector reads the host enrichment file dt_host_metadata.properties which
the OneAgent writes to /var/lib/dynatrace/enrichment on non-Windows systems and
to %ProgramData%\dynatrace\enrichment on Windows. Each of the following
attributes is added if it is present in that file:

- dt.entity.host
- host.name
- dt.smartscape.host

The dt.entity.host and dt.smartscape.host attributes identify the host entity
in Dynatrace and have no equivalent in the OpenTelemetry [semantic
conventions].

No cloud.provider or cloud.platform attribute is reported: the OneAgent runs on
hosts across any infrastructure, so there is no platform to identify.

All three attributes are optional, because which of them the OneAgent writes
depends on its version and configuration. A resource holding only the subset
found in the file is returned without an error, so unlike most detectors this
one never returns [resource.ErrPartialResource]. If the enrichment file does not
exist, i.e. the process is not running on a monitored host, an empty resource is
returned without an error.

These attributes describe the host the process runs on. When telemetry is
forwarded on behalf of another host, prefer the attributes already present on
that telemetry over the ones this detector reports, otherwise the telemetry
appears to originate from the forwarding host.

[semantic conventions]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/host.md
*/
package dynatrace
180 changes: 180 additions & 0 deletions detectors/dynatrace/dynatrace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package dynatrace

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"

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

const (
// hostMetadataFile is the enrichment file the OneAgent writes.
hostMetadataFile = "dt_host_metadata.properties"

// unixEnrichmentDir is the enrichment directory on non-Windows systems.
unixEnrichmentDir = "/var/lib/dynatrace/enrichment"

// fallbackProgramData is used on Windows when the ProgramData environment
// variable is not set.
fallbackProgramData = `C:\ProgramData`
)

// Dynatrace-specific attribute keys. These identify the host entity in
// Dynatrace and have no equivalent in the OpenTelemetry semantic conventions,
// so they are declared here rather than taken from semconv.
const (
dtEntityHostKey = attribute.Key("dt.entity.host")
dtSmartscapeHostKey = attribute.Key("dt.smartscape.host")
)

// Compile-time interface assertion.
var _ resource.Detector = (*ResourceDetector)(nil)

type config struct {
filter attribute.Filter
}

// Option configures a [ResourceDetector].
type Option interface {
apply(*config)
}

type optionFunc func(*config)

func (f optionFunc) apply(c *config) { f(c) }

// WithAttributeFilter sets a filter that controls which detected attributes are
// included in the returned resource. Only attributes for which filter returns
// true are included. By default all attributes are included.
func WithAttributeFilter(filter attribute.Filter) Option {
return optionFunc(func(c *config) { c.filter = filter })
}

// ResourceDetector collects resource information from the enrichment files
// written by the Dynatrace OneAgent.
type ResourceDetector struct {
enrichmentDir string
cfg config
}

// NewResourceDetector returns a [resource.Detector] that reads the Dynatrace
// OneAgent host enrichment file.
//
// If the file is absent, i.e. the process is not running on a host monitored by
// the OneAgent, the detector returns an empty resource without an error.
func NewResourceDetector(opts ...Option) *ResourceDetector {
var cfg config
for _, opt := range opts {
opt.apply(&cfg)
}
return &ResourceDetector{
enrichmentDir: defaultEnrichmentDir(runtime.GOOS, os.Getenv("ProgramData")),
cfg: cfg,
}
}

// defaultEnrichmentDir returns the directory the OneAgent writes its enrichment
// files to on goos. On Windows the directory lives under programData, falling
// back to C:\ProgramData when the ProgramData environment variable is unset.
func defaultEnrichmentDir(goos, programData string) string {
if goos != "windows" {
return unixEnrichmentDir
}
if programData == "" {
programData = fallbackProgramData
}
return filepath.Join(programData, "dynatrace", "enrichment")
}

// Detect reads the Dynatrace OneAgent host enrichment file and returns the
// attributes it declares. It returns an empty resource and no error when the
// file does not exist, and an error when the file exists but cannot be read.
//
// All attributes are optional: a resource holding only the subset of attributes
// present in the file is returned without an error, so
// [resource.ErrPartialResource] is never returned.
func (d *ResourceDetector) Detect(context.Context) (*resource.Resource, error) {
path := filepath.Join(d.enrichmentDir, hostMetadataFile)

file, err := os.Open(path) // #nosec G304 -- path is derived from a fixed, non-user-supplied directory.
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// Not running on a host monitored by the OneAgent.
return resource.Empty(), nil
}
return nil, fmt.Errorf("dynatrace: open %s: %w", path, err)
}
defer file.Close()

attrs, err := parseProperties(file)
if err != nil {
return nil, fmt.Errorf("dynatrace: read %s: %w", path, err)
}

if d.cfg.filter != nil {
filtered := attrs[:0]
for _, kv := range attrs {
if d.cfg.filter(kv) {
filtered = append(filtered, kv)
}
}
attrs = filtered
}

if len(attrs) == 0 {
// Returning a resource carrying only a schema URL would risk a schema
// conflict when merged with other detectors for no gain.
return resource.Empty(), nil
}
return resource.NewWithAttributes(semconv.SchemaURL, attrs...), nil
}

// parseProperties reads key=value lines from r and returns the attributes for
// the keys the detector reports. Lines that do not hold a "=" separator, and
// keys that are not reported by the detector, are skipped.
//
// This deliberately mirrors the collector implementation rather than
// implementing a full Java properties parser: comments, escape sequences and
// the ":" separator are not recognized.
func parseProperties(r io.Reader) ([]attribute.KeyValue, error) {
var attrs []attribute.KeyValue

scanner := bufio.NewScanner(r)
for scanner.Scan() {
// Split on the first "=" only; any later "=" is part of the value.
key, value, found := strings.Cut(scanner.Text(), "=")
if !found {
continue
}
key, value = strings.TrimSpace(key), strings.TrimSpace(value)
if key == "" || value == "" {
continue
}

switch key {
case string(dtEntityHostKey):
attrs = append(attrs, dtEntityHostKey.String(value))
case string(semconv.HostNameKey):
attrs = append(attrs, semconv.HostName(value))
case string(dtSmartscapeHostKey):
attrs = append(attrs, dtSmartscapeHostKey.String(value))
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return attrs, nil
}
Loading
Loading