Skip to content

Commit cc53930

Browse files
committed
feat: contribute aws eks resource detector
1 parent 981b3da commit cc53930

File tree

5 files changed

+571
-188
lines changed

5 files changed

+571
-188
lines changed

resources/aws/README.md

+18-3
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ require 'opentelemetry/resource/detector'
3131

3232
OpenTelemetry::SDK.configure do |c|
3333
# Specify which AWS resource detectors to use
34-
c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ec2, :ecs, :lambda])
34+
c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ec2, :ecs, :eks, :lambda])
3535

3636
# Or use just one detector
3737
c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ec2])
3838
c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ecs])
39+
c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:eks])
3940
c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:lambda])
4041
end
4142
```
@@ -76,6 +77,22 @@ Populates `cloud`, `container`, and AWS ECS-specific attributes for processes ru
7677
| `aws.log.stream.names` | The CloudWatch log stream names (if awslogs driver is used) |
7778
| `aws.log.stream.arns` | The CloudWatch log stream ARNs (if awslogs driver is used) |
7879

80+
### AWS EKS Detector
81+
82+
<!-- cspell:ignore configmaps-->
83+
Populates `cloud`, `container`, and Kubernetes (k8s) attributes for processes running on Amazon EKS.
84+
| Resource Attribute | Description |
85+
|--------------------|-------------|
86+
| `cloud.platform` | The cloud platform. In this context, it's always "aws_eks" |
87+
| `cloud.provider` | The cloud provider. In this context, it's always "aws" |
88+
| `container.id` | The container ID from the `/proc/self/cgroup` file |
89+
| `k8s.cluster.name` | The name of the EKS cluster from the `cluster-info` configmap in the `amazon-cloudwatch` namespace |
90+
91+
The EKS detector verifies that the process is running on EKS by checking:
92+
1. Presence of Kubernetes service account token and certificate
93+
2. Ability to access the `aws-auth` configmap in the `kube-system` namespace
94+
3. Availability of either cluster name or container ID
95+
7996
### AWS Lambda Detector
8097
Populates `cloud` and `faas` (Function as a Service) attributes for processes running on AWS Lambda.
8198
| Resource Attribute | Description |
@@ -88,8 +105,6 @@ Populates `cloud` and `faas` (Function as a Service) attributes for processes ru
88105
| `faas.instance` | The Lambda function instance ID from the `AWS_LAMBDA_LOG_STREAM_NAME` environment variable |
89106
| `faas.max_memory` | The Lambda function memory size in MB from the `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` environment variable |
90107

91-
Additional AWS platforms (EKS) will be supported in future versions.
92-
93108
## License
94109

95110
The `opentelemetry-resource-detector-aws` gem is distributed under the Apache 2.0 license. See LICENSE for more information.

resources/aws/lib/opentelemetry/resource/detector/aws.rb

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'opentelemetry/resource/detector/aws/ec2'
88
require 'opentelemetry/resource/detector/aws/ecs'
99
require 'opentelemetry/resource/detector/aws/lambda'
10+
require 'opentelemetry/resource/detector/aws/eks'
1011

1112
module OpenTelemetry
1213
module Resource
@@ -30,6 +31,8 @@ def detect(detectors = [])
3031
EC2.detect
3132
when :ecs
3233
ECS.detect
34+
when :eks
35+
EKS.detect
3336
when :lambda
3437
Lambda.detect
3538
else
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'net/http'
8+
require 'json'
9+
require 'openssl'
10+
require 'uri'
11+
require 'opentelemetry/common'
12+
13+
module OpenTelemetry
14+
module Resource
15+
module Detector
16+
module AWS
17+
# EKS contains detect class method for determining EKS resource attributes
18+
module EKS
19+
extend self
20+
21+
# Container ID length from cgroup file
22+
CONTAINER_ID_LENGTH = 64
23+
24+
# HTTP request timeout in seconds
25+
HTTP_TIMEOUT = 5
26+
27+
# Kubernetes token and certificate paths
28+
TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'
29+
CERT_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
30+
31+
# Kubernetes API paths
32+
AWS_AUTH_PATH = '/api/v1/namespaces/kube-system/configmaps/aws-auth'
33+
CLUSTER_INFO_PATH = '/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info'
34+
35+
# Create a constant for resource semantic conventions
36+
RESOURCE = OpenTelemetry::SemanticConventions::Resource
37+
38+
def detect
39+
# Return empty resource if not running on K8s
40+
return OpenTelemetry::SDK::Resources::Resource.create({}) unless k8s?
41+
42+
resource_attributes = {}
43+
44+
begin
45+
# Get K8s credentials
46+
cred_value = k8s_cred_value
47+
48+
# Verify this is an EKS cluster
49+
unless eks?(cred_value)
50+
OpenTelemetry.logger.debug('Could not confirm process is running on EKS')
51+
return OpenTelemetry::SDK::Resources::Resource.create({})
52+
end
53+
54+
# Get cluster name and container ID
55+
cluster_name_val = cluster_name(cred_value)
56+
container_id_val = container_id
57+
58+
if container_id_val.empty? && cluster_name_val.empty?
59+
OpenTelemetry.logger.debug('Neither cluster name nor container ID found on EKS process')
60+
return OpenTelemetry::SDK::Resources::Resource.create({})
61+
end
62+
63+
# Set resource attributes
64+
resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws'
65+
resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_eks'
66+
resource_attributes[RESOURCE::K8S_CLUSTER_NAME] = cluster_name_val unless cluster_name_val.empty?
67+
resource_attributes[RESOURCE::CONTAINER_ID] = container_id_val unless container_id_val.empty?
68+
rescue StandardError => e
69+
OpenTelemetry.logger.debug("EKS resource detection failed: #{e.message}")
70+
return OpenTelemetry::SDK::Resources::Resource.create({})
71+
end
72+
73+
resource_attributes.delete_if { |_key, value| value.nil? || value.empty? }
74+
OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
75+
end
76+
77+
private
78+
79+
# Check if running on K8s
80+
#
81+
# @return [Boolean] true if running on K8s
82+
def k8s?
83+
File.exist?(TOKEN_PATH) && File.exist?(CERT_PATH)
84+
end
85+
86+
# Get K8s token
87+
#
88+
# @return [String] K8s token
89+
# @raise [StandardError] if token could not be read
90+
def k8s_cred_value
91+
token = File.read(TOKEN_PATH).strip
92+
"Bearer #{token}"
93+
rescue StandardError => e
94+
OpenTelemetry.logger.debug("Failed to get k8s token: #{e.message}")
95+
raise e
96+
end
97+
98+
# Check if running on EKS
99+
#
100+
# @param cred_value [String] K8s credentials
101+
# @return [Boolean] true if running on EKS
102+
def eks?(cred_value)
103+
# Just try to to access the aws-auth configmap
104+
# If it exists and we can access it, we're on EKS
105+
aws_http_request('GET', AWS_AUTH_PATH, cred_value)
106+
true
107+
rescue StandardError
108+
false
109+
end
110+
111+
# Get EKS cluster name
112+
#
113+
# @param cred_value [String] K8s credentials
114+
# @return [String] Cluster name or empty string if not found
115+
def cluster_name(cred_value)
116+
begin
117+
response = aws_http_request('GET', CLUSTER_INFO_PATH, cred_value)
118+
cluster_info = JSON.parse(response)
119+
return cluster_info['data']['cluster.name'] if cluster_info['data'] && cluster_info['data']['cluster.name']
120+
rescue StandardError => e
121+
OpenTelemetry.logger.debug("Cannot get cluster name on EKS: #{e.message}")
122+
end
123+
''
124+
end
125+
126+
# Get container ID from cgroup file
127+
#
128+
# @return [String] Container ID or empty string if not found
129+
def container_id
130+
begin
131+
File.open('/proc/self/cgroup', 'r') do |file|
132+
file.each_line do |line|
133+
line = line.strip
134+
# Look for container ID (64 chars) at the end of the line
135+
return line[-CONTAINER_ID_LENGTH..-1] if line.length > CONTAINER_ID_LENGTH
136+
end
137+
end
138+
rescue StandardError => e
139+
OpenTelemetry.logger.debug("Failed to get container ID on EKS: #{e.message}")
140+
end
141+
''
142+
end
143+
144+
# Make HTTP request to K8s API
145+
#
146+
# @param method [String] HTTP method
147+
# @param path [String] API path
148+
# @param cred_value [String] Authorization header value
149+
# @return [String] Response body
150+
# @raise [StandardError] if request fails
151+
def aws_http_request(method, path, cred_value)
152+
uri = URI.parse("https://kubernetes.default.svc#{path}")
153+
http = Net::HTTP.new(uri.host, uri.port)
154+
http.use_ssl = true
155+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
156+
http.ca_file = CERT_PATH
157+
http.open_timeout = HTTP_TIMEOUT
158+
http.read_timeout = HTTP_TIMEOUT
159+
160+
request = case method.upcase
161+
when 'GET'
162+
Net::HTTP::Get.new(uri)
163+
when 'POST'
164+
Net::HTTP::Post.new(uri)
165+
else
166+
raise "Unsupported HTTP method: #{method}"
167+
end
168+
169+
request['Authorization'] = cred_value
170+
171+
OpenTelemetry::Common::Utilities.untraced do
172+
response = http.request(request)
173+
raise "HTTP request failed with status #{response.code}" unless response.is_a?(Net::HTTPSuccess)
174+
175+
response.body
176+
end
177+
end
178+
end
179+
end
180+
end
181+
end
182+
end

0 commit comments

Comments
 (0)