diff --git a/processor/resourcedetectionprocessor/config.go b/processor/resourcedetectionprocessor/config.go index 78fb07a423ba..4da50fbc2b5a 100644 --- a/processor/resourcedetectionprocessor/config.go +++ b/processor/resourcedetectionprocessor/config.go @@ -15,6 +15,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/azure" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/azure/aks" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/consul" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/cycleio" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/docker" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/heroku" @@ -85,6 +86,9 @@ type DetectorConfig struct { // K8SNode contains user-specified configurations for the K8SNode detector K8SNodeConfig k8snode.Config `mapstructure:"k8snode"` + + // CycleIO contains user-specified configurations for the CycleIO detector + CycleIOConfig cycleio.Config `mapstructure:"cycleio"` } func detectorCreateDefaultConfig() DetectorConfig { @@ -136,6 +140,8 @@ func (d *DetectorConfig) GetConfigFromType(detectorType internal.DetectorType) i return d.OpenShiftConfig case k8snode.TypeStr: return d.K8SNodeConfig + case cycleio.TypeStr: + return d.CycleIOConfig default: return nil } diff --git a/processor/resourcedetectionprocessor/factory.go b/processor/resourcedetectionprocessor/factory.go index 05ff7934f81b..c619f664bfd9 100644 --- a/processor/resourcedetectionprocessor/factory.go +++ b/processor/resourcedetectionprocessor/factory.go @@ -27,6 +27,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/azure" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/azure/aks" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/consul" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/cycleio" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/docker" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/env" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" @@ -66,6 +67,7 @@ func NewFactory() processor.Factory { system.TypeStr: system.NewDetector, openshift.TypeStr: openshift.NewDetector, k8snode.TypeStr: k8snode.NewDetector, + cycleio.TypeStr: cycleio.NewDetector, }) f := &factory{ diff --git a/processor/resourcedetectionprocessor/internal/cycleio/config.go b/processor/resourcedetectionprocessor/internal/cycleio/config.go new file mode 100644 index 000000000000..61927d2f2129 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/cycleio/config.go @@ -0,0 +1,8 @@ +package cycleio + +type Config struct { +} + +func CreateDefaultConfig() Config { + return Config{} +} diff --git a/processor/resourcedetectionprocessor/internal/cycleio/cycleio.go b/processor/resourcedetectionprocessor/internal/cycleio/cycleio.go new file mode 100644 index 000000000000..057275c9feb7 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/cycleio/cycleio.go @@ -0,0 +1,184 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package env provides a detector that loads resource information from +// the OTEL_RESOURCE environment variable. A list of labels of the form +// `=,=,...` is accepted. Domain names and +// paths are accepted as label keys. +package cycleio // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/cycleio" + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/processor" + semconv "go.opentelemetry.io/collector/semconv/v1.5.0" + + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" +) + +// TypeStr is type of detector. +const ( + TypeStr = "cycleio" + providerVendorEnvVar = "CYCLE_PROVIDER_VENDOR" + providerLocation = "CYCLE_PROVIDER_LOCATION" + hostnameEnvVar = "CYCLE_SERVER_ID" + clusterEnvVar = "CYCLE_CLUSTER" + + cycleAPITokenEnvVar = "CYCLE_API_TOKEN" + cycleAPIUnixSocket = "/var/run/cycle/api/api.sock" + cycleAPIHost = "localhost" + cycleAPIServerEndpoint = "/v1/server" + + cycleTokenHeader = "X-CYCLE-TOKEN" +) + +var _ internal.Detector = (*Detector)(nil) + +type cycleProvider struct { + Vendor string `json:"vendor"` + Model string `json:"model"` + Location string `json:"location"` + Zone string `json:"zone"` + Server string `json:"server"` + InitIPs []any `json:"init_ips"` +} + +type cycleServerData struct { + ID string `json:"id"` + Hostname string `json:"hostname"` + Provider cycleProvider `json:"provider"` + Cluster string `json:"cluster"` +} + +type cycleServerInfo struct { + Data cycleServerData `json:"data"` +} + +type Detector struct{} + +func NewDetector(processor.Settings, internal.DetectorConfig) (internal.Detector, error) { + return &Detector{}, nil +} + +func (d *Detector) Detect(context.Context) (resource pcommon.Resource, schemaURL string, err error) { + res := pcommon.NewResource() + + res.Attributes().PutStr(semconv.AttributeOSType, semconv.AttributeOSTypeLinux) + cycleAPIToken := os.Getenv(cycleAPITokenEnvVar) + if cycleAPIToken != "" { + serverInfo, err := getServerInfo(cycleAPIToken) + if err != nil { + return res, "", err + } + data := serverInfo.Data + + res.Attributes().PutStr(semconv.AttributeCloudProvider, getCloudProvider(data.Provider.Vendor)) + res.Attributes().PutStr(semconv.AttributeCloudRegion, data.Provider.Location) + res.Attributes().PutStr(semconv.AttributeCloudAvailabilityZone, data.Provider.Zone) + res.Attributes().PutStr(semconv.AttributeHostID, data.ID) + res.Attributes().PutStr(semconv.AttributeHostName, data.Hostname) + res.Attributes().PutStr(semconv.AttributeHostType, data.Provider.Model) + res.Attributes().PutEmptySlice("host.ip").FromRaw(data.Provider.InitIPs) + + res.Attributes().PutStr("cycle.cluster.id", data.Cluster) + } else { + + vendor := os.Getenv(providerVendorEnvVar) + if vendor == "" { + vendor = "unknown" + } + res.Attributes().PutStr(semconv.AttributeCloudProvider, getCloudProvider(vendor)) + + region := os.Getenv(providerLocation) + if region == "" { + region = "unknown" + } + res.Attributes().PutStr(semconv.AttributeCloudRegion, region) + + hostID := os.Getenv(hostnameEnvVar) + if hostID == "" { + hostID = "cycleio-server" + } + res.Attributes().PutStr(semconv.AttributeHostID, hostID) + res.Attributes().PutStr(semconv.AttributeHostName, hostID) + + cluster := os.Getenv(clusterEnvVar) + if cluster == "" { + cluster = "unknown" + } + res.Attributes().PutStr("cycle.cluster.id", cluster) + } + + return res, "", nil +} + +func getCloudProvider(provider string) string { + switch provider { + case "aws": + return semconv.AttributeCloudProviderAWS + case "gcp": + return semconv.AttributeCloudProviderGCP + case "azure": + return semconv.AttributeCloudProviderAzure + default: + return provider + } +} + +func getServerInfo(token string) (*cycleServerInfo, error) { + var serverInfo cycleServerInfo + + // Create a custom HTTP transport that uses the Unix socket + transport := &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", cycleAPIUnixSocket) + }, + } + + // Create an HTTP client with the custom transport + client := &http.Client{ + Transport: transport, + } + + // Construct the request URL + u := &url.URL{ + Scheme: "http", + Host: cycleAPIHost, // This is ignored but required for forming a valid URL + Path: cycleAPIServerEndpoint, + } + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return &serverInfo, err + } + req.Header.Add(cycleTokenHeader, token) + resp, err := client.Do(req) + if err != nil { + return &serverInfo, err + } + + defer resp.Body.Close() + + // Read and print the response + body, err := io.ReadAll(resp.Body) + if err != nil { + return &serverInfo, err + } + + fmt.Println("###", string(body)) + err = json.Unmarshal(body, &serverInfo) + if err != nil { + return &serverInfo, err + } + + fmt.Println("###", serverInfo) + return &serverInfo, nil +} diff --git a/processor/resourcedetectionprocessor/internal/cycleio/cycleio_test.go b/processor/resourcedetectionprocessor/internal/cycleio/cycleio_test.go new file mode 100644 index 000000000000..b6a8f7f3b5cf --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/cycleio/cycleio_test.go @@ -0,0 +1,130 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package cycleio + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/processor" + semconv "go.opentelemetry.io/collector/semconv/v1.5.0" +) + +// TODO +func TestDetect(t *testing.T) { + tests := []struct { + name string + env map[string]string + want map[string]interface{} + }{ + { + name: "empty env", + env: map[string]string{}, + want: map[string]interface{}{ + semconv.AttributeCloudProvider: "unknown", + semconv.AttributeCloudRegion: "unknown", + semconv.AttributeHostID: "cycleio-server", + semconv.AttributeHostName: "cycleio-server", + semconv.AttributeOSType: semconv.AttributeOSTypeLinux, + "cycle.cluster.id": "unknown", + }, + }, + { + name: "only provider vendor", + env: map[string]string{ + providerVendorEnvVar: "aws", + }, + want: map[string]interface{}{ + semconv.AttributeCloudProvider: "aws", + semconv.AttributeCloudRegion: "unknown", + semconv.AttributeHostID: "cycleio-server", + semconv.AttributeHostName: "cycleio-server", + semconv.AttributeOSType: semconv.AttributeOSTypeLinux, + "cycle.cluster.id": "unknown", + }, + }, + { + name: "only provider location", + env: map[string]string{ + providerVendorEnvVar: "aws", + providerLocation: "us-east-1", + }, + want: map[string]interface{}{ + semconv.AttributeCloudProvider: "aws", + semconv.AttributeCloudRegion: "us-east-1", + semconv.AttributeHostID: "cycleio-server", + semconv.AttributeHostName: "cycleio-server", + semconv.AttributeOSType: semconv.AttributeOSTypeLinux, + "cycle.cluster.id": "unknown", + }, + }, + { + name: "only hostname", + env: map[string]string{ + hostnameEnvVar: "acme-host", + }, + want: map[string]interface{}{ + semconv.AttributeCloudProvider: "unknown", + semconv.AttributeCloudRegion: "unknown", + semconv.AttributeHostID: "acme-host", + semconv.AttributeHostName: "acme-host", + semconv.AttributeOSType: semconv.AttributeOSTypeLinux, + "cycle.cluster.id": "unknown", + }, + }, + { + name: "only cluster", + env: map[string]string{ + clusterEnvVar: "acme-cluster", + }, + want: map[string]interface{}{ + semconv.AttributeCloudProvider: "unknown", + semconv.AttributeCloudRegion: "unknown", + semconv.AttributeHostID: "cycleio-server", + semconv.AttributeHostName: "cycleio-server", + semconv.AttributeOSType: semconv.AttributeOSTypeLinux, + "cycle.cluster.id": "acme-cluster", + }, + }, + { + name: "all env vars", + env: map[string]string{ + providerVendorEnvVar: "aws", + providerLocation: "us-east-1", + hostnameEnvVar: "acme-host", + clusterEnvVar: "acme-cluster", + }, + want: map[string]interface{}{ + semconv.AttributeCloudProvider: "aws", + semconv.AttributeCloudRegion: "us-east-1", + semconv.AttributeHostID: "acme-host", + semconv.AttributeHostName: "acme-host", + semconv.AttributeOSType: semconv.AttributeOSTypeLinux, + "cycle.cluster.id": "acme-cluster", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for key, value := range tt.env { + os.Setenv(key, value) + } + defer func() { + for key := range tt.env { + os.Unsetenv(key) + } + }() + + d, err := NewDetector(processor.Settings{}, CreateDefaultConfig()) + require.NoError(t, err) + got, _, err := d.Detect(context.Background()) + require.NoError(t, err) + + require.Equal(t, tt.want, got.Attributes().AsRaw()) + }) + } +} diff --git a/processor/resourcedetectionprocessor/internal/cycleio/package_test.go b/processor/resourcedetectionprocessor/internal/cycleio/package_test.go new file mode 100644 index 000000000000..cc1c71bb1949 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/cycleio/package_test.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package cycleio + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +}