diff --git a/processor/resourcedetectionprocessor/e2e_test.go b/processor/resourcedetectionprocessor/e2e_test.go index c976aaecf358f..391519feeff08 100644 --- a/processor/resourcedetectionprocessor/e2e_test.go +++ b/processor/resourcedetectionprocessor/e2e_test.go @@ -712,6 +712,53 @@ func TestE2EDynatraceDetector(t *testing.T) { }, 3*time.Minute, 1*time.Second) } +// TestE2EAkamaiDetector tests the Akamai detector by deploying a metadata-server +// sidecar that simulates the Akamai/Linode IMDS (at 169.254.169.254) and verifying +// that the resource attributes are correctly detected and attached to metrics. +func TestE2EAkamaiDetector(t *testing.T) { + var expected pmetric.Metrics + expectedFile := filepath.Join("testdata", "e2e", "akamai", "expected.yaml") + expected, err := golden.ReadMetrics(expectedFile) + require.NoError(t, err) + + k8sClient, err := k8stest.NewK8sClient(testKubeConfig) + require.NoError(t, err) + + metricsConsumer := new(consumertest.MetricsSink) + shutdownSink := startUpSink(t, metricsConsumer) + defer shutdownSink() + + testID := uuid.NewString()[:8] + collectorObjs := k8stest.CreateCollectorObjects(t, k8sClient, testID, filepath.Join(".", "testdata", "e2e", "akamai", "collector"), map[string]string{}, "") + + defer func() { + for _, obj := range collectorObjs { + require.NoErrorf(t, k8stest.DeleteObject(k8sClient, obj), "failed to delete object %s", obj.GetName()) + } + }() + + wantEntries := 10 + waitForData(t, wantEntries, metricsConsumer) + + // Uncomment to regenerate golden file + // golden.WriteMetrics(t, expectedFile+".actual", metricsConsumer.AllMetrics()[len(metricsConsumer.AllMetrics())-1]) + + require.EventuallyWithT(t, func(tt *assert.CollectT) { + assert.NoError(tt, pmetrictest.CompareMetrics(expected, metricsConsumer.AllMetrics()[len(metricsConsumer.AllMetrics())-1], + pmetrictest.IgnoreTimestamp(), + pmetrictest.IgnoreStartTimestamp(), + pmetrictest.IgnoreScopeVersion(), + pmetrictest.IgnoreResourceMetricsOrder(), + pmetrictest.IgnoreMetricsOrder(), + pmetrictest.IgnoreScopeMetricsOrder(), + pmetrictest.IgnoreMetricDataPointsOrder(), + pmetrictest.IgnoreMetricValues(), + pmetrictest.IgnoreSubsequentDataPoints("system.cpu.time"), + ), + ) + }, 3*time.Minute, 1*time.Second) +} + // TestE2EElasticBeanstalkDetector tests the Elastic Beanstalk detector by mounting a mock // environment.conf file and verifying that the resource attributes are correctly detected // and attached to metrics. diff --git a/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/01-metadata-configmap.yaml b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/01-metadata-configmap.yaml new file mode 100644 index 0000000000000..2cf679478cf7d --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/01-metadata-configmap.yaml @@ -0,0 +1,113 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Name }}-metadata-config + namespace: default +data: + server.py: | + import json + import os + import secrets + from http.server import BaseHTTPRequestHandler, HTTPServer + + # Akamai/Linode instance metadata format + # Based on https://techdocs.akamai.com/cloud-computing/docs/metadata-service-api + AKAMAI_INSTANCE_METADATA = { + "id": 12345678, + "host_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "label": "test-akamai-instance", + "region": "us-east", + "type": "g6-standard-2", + "tags": ["environment:testing", "team:observability"], + "specs": { + "vcpus": 2, + "memory": 4096, + "disk": 81920, + "transfer": 4000, + "gpus": 0 + }, + "backups": { + "enabled": True, + "status": "completed" + }, + "account_euuid": "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV", + "image": { + "id": "linode/ubuntu22.04", + "label": "Ubuntu 22.04 LTS" + } + } + + # Simple token storage (in-memory for testing) + valid_tokens = set() + + class Handler(BaseHTTPRequestHandler): + def do_PUT(self): + # Token endpoint - /v1/token + if self.path == "/v1/token": + # Check for required header + expiry_header = self.headers.get("Metadata-Token-Expiry-Seconds") + if not expiry_header: + self.send_response(400) + self.send_header("Content-Type", "text/plain") + self.end_headers() + self.wfile.write(b"Missing Metadata-Token-Expiry-Seconds header") + return + + # Generate a token + token = secrets.token_hex(32) + valid_tokens.add(token) + + # go-metadata client expects a JSON array of strings + body = json.dumps([token]).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + # Not found + self.send_response(404) + self.end_headers() + + def do_GET(self): + # Health check endpoint + if self.path == "/healthz": + body = b"ok" + self.send_response(200) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + # Instance endpoint - /v1/instance + if self.path == "/v1/instance": + # Check for required Metadata-Token header + token = self.headers.get("Metadata-Token") + if not token or token not in valid_tokens: + self.send_response(401) + self.send_header("Content-Type", "text/plain") + self.end_headers() + self.wfile.write(b"Invalid or missing Metadata-Token") + return + + body = json.dumps(AKAMAI_INSTANCE_METADATA).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + # Not found + self.send_response(404) + self.end_headers() + + def log_message(self, fmt, *args): + return + + if __name__ == "__main__": + port = int(os.environ.get("PORT", "80")) + server = HTTPServer(("", port), Handler) + server.serve_forever() diff --git a/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/02-configmap.yaml b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/02-configmap.yaml new file mode 100644 index 0000000000000..292c37b392d3a --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/02-configmap.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Name }}-config + namespace: default +data: + relay: | + exporters: + otlp: + endpoint: {{ .HostEndpoint }}:4317 + tls: + insecure: true + extensions: + health_check: + endpoint: 0.0.0.0:13133 + processors: + resourcedetection: + detectors: [akamai] + timeout: 2s + override: false + receivers: + hostmetrics: + collection_interval: 1s + scrapers: + cpu: + service: + telemetry: + logs: + level: "debug" + extensions: + - health_check + pipelines: + metrics: + receivers: + - hostmetrics + processors: + - resourcedetection + exporters: + - otlp diff --git a/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/03-serviceaccount.yaml b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/03-serviceaccount.yaml new file mode 100644 index 0000000000000..7a9803b445b6c --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/03-serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Name }}-sa + namespace: default diff --git a/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/04-service.yaml b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/04-service.yaml new file mode 100644 index 0000000000000..94587d231bbf0 --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/04-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Name }} + namespace: default +spec: + selector: + app: {{ .Name }} + ports: + - name: health + port: 13133 + targetPort: 13133 diff --git a/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/05-deployment.yaml b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/05-deployment.yaml new file mode 100644 index 0000000000000..7214d769be5a8 --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/akamai/collector/05-deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Name }} + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Name }} + template: + metadata: + labels: + app: {{ .Name }} + spec: + serviceAccountName: {{ .Name }}-sa + initContainers: + - name: metadata-server + image: python:3.13-alpine + imagePullPolicy: IfNotPresent + restartPolicy: Always + securityContext: + runAsUser: 0 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + command: + - python3 + - /scripts/server.py + env: + - name: PORT + value: "80" + ports: + - containerPort: 80 + name: metadata + startupProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 1 + periodSeconds: 1 + failureThreshold: 10 + volumeMounts: + - name: metadata-script + mountPath: /scripts + - name: setup-network + image: alpine:3.21 + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - NET_ADMIN + command: + - sh + - -c + - | + apk add --no-cache iptables + iptables -t nat -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:80 + echo "iptables rule added to redirect 169.254.169.254:80 -> 127.0.0.1:80" + containers: + - name: otelcol + image: otelcontribcol:latest + imagePullPolicy: Never + args: + - "--config=/conf/relay" + volumeMounts: + - name: config + mountPath: /conf + volumes: + - name: config + configMap: + name: {{ .Name }}-config + items: + - key: relay + path: relay + - name: metadata-script + configMap: + name: {{ .Name }}-metadata-config diff --git a/processor/resourcedetectionprocessor/testdata/e2e/akamai/expected.yaml b/processor/resourcedetectionprocessor/testdata/e2e/akamai/expected.yaml new file mode 100644 index 0000000000000..fc212357d3acc --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/akamai/expected.yaml @@ -0,0 +1,122 @@ +resourceMetrics: + - resource: + attributes: + - key: cloud.account.id + value: + stringValue: ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV + - key: cloud.platform + value: + stringValue: akamai_cloud_platform + - key: cloud.provider + value: + stringValue: akamai_cloud + - key: cloud.region + value: + stringValue: us-east + - key: host.id + value: + stringValue: "12345678" + - key: host.image.id + value: + stringValue: linode/ubuntu22.04 + - key: host.image.name + value: + stringValue: Ubuntu 22.04 LTS + - key: host.name + value: + stringValue: test-akamai-instance + - key: host.type + value: + stringValue: g6-standard-2 + schemaUrl: https://opentelemetry.io/schemas/1.9.0 + scopeMetrics: + - metrics: + - description: Total seconds each logical CPU spent on each mode. + name: system.cpu.time + sum: + aggregationTemporality: 2 + dataPoints: + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: idle + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: interrupt + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: nice + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: softirq + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: steal + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: system + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: user + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: wait + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + isMonotonic: true + unit: s + scope: + name: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper