Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
47 changes: 47 additions & 0 deletions processor/resourcedetectionprocessor/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

// TestE2EHetznerDetector tests the Hetzner detector by deploying a metadata-server
// sidecar that simulates the Hetzner IMDS and verifying that the resource attributes
// are correctly detected and attached to metrics.
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Name }}-sa
namespace: default
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Name }}
namespace: default
spec:
selector:
app: {{ .Name }}
ports:
- name: health
port: 13133
targetPort: 13133
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading