Skip to content
Merged
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
47 changes: 47 additions & 0 deletions processor/resourcedetectionprocessor/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,53 @@ func TestE2EUpcloudDetector(t *testing.T) {
}, 3*time.Minute, 1*time.Second)
}

// TestE2EVultrDetector tests the Vultr detector by deploying a metadata-server
// sidecar that simulates the Vultr IMDS and verifying that the resource attributes
// are correctly detected and attached to metrics.
func TestE2EVultrDetector(t *testing.T) {
var expected pmetric.Metrics
expectedFile := filepath.Join("testdata", "e2e", "vultr", "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", "vultr", "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)
}

func replaceWithStar(_ string) string {
return "*"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Name }}-metadata-config
namespace: default
data:
server.py: |
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse

# Vultr IMDS metadata format (see https://www.vultr.com/metadata/)
VULTR_METADATA = {
"hostname": "test-vultr-instance",
"instanceid": "12345678",
"instance-v2-id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"region": {
"regioncode": "EWR"
}
}

class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
path = parsed.path

# Health check endpoint
if 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

# Vultr IMDS endpoint (returns full metadata as JSON)
if path == "/v1.json":
body = json.dumps(VULTR_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: [vultr]
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
resourceMetrics:
- resource:
attributes:
- key: cloud.provider
value:
stringValue: vultr
- key: cloud.region
value:
stringValue: ewr
- key: host.id
value:
stringValue: a1b2c3d4-e5f6-7890-abcd-ef1234567890
- key: host.name
value:
stringValue: test-vultr-instance
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