Skip to content

Commit ec4989c

Browse files
Paramadondependabot[bot]
authored andcommitted
Adding ECS Observer translator (#1755)
Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent f7bcd28 commit ec4989c

File tree

10 files changed

+765
-9
lines changed

10 files changed

+765
-9
lines changed

translator/tocwconfig/sampleConfig/prometheus_combined_config_linux.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,44 @@ extensions:
131131
usage_flags:
132132
mode: EC2
133133
region_type: ACJ
134+
ecs_observer:
135+
cluster_name: ecs-cluster-a
136+
cluster_region: us-west-2
137+
docker_labels:
138+
- job_name: ""
139+
job_name_label: ECS_PROMETHEUS_JOB_NAME_1
140+
metrics_path: ""
141+
metrics_path_label: ECS_PROMETHEUS_METRICS_PATH
142+
port_label: ECS_PROMETHEUS_EXPORTER_PORT_SUBSET
143+
job_label_name: ""
144+
refresh_interval: 1m0s
145+
result_file: "{ecsSdFileName}"
146+
services:
147+
- container_name_pattern: nginx-prometheus-exporter
148+
job_name: service_name_1
149+
metrics_path: /metrics
150+
metrics_ports:
151+
- 9113
152+
name_pattern: .*-application-stack
153+
- container_name_pattern: ""
154+
job_name: ""
155+
metrics_path: /stats/metrics
156+
metrics_ports:
157+
- 9114
158+
name_pattern: run-application-stack
159+
task_definitions:
160+
- arn_pattern: .*task_def_1:[0-9]+
161+
container_name_pattern: ""
162+
job_name: task_def_1
163+
metrics_path: /stats/metrics
164+
metrics_ports:
165+
- 9901
166+
- arn_pattern: task_def_2
167+
container_name_pattern: ^envoy$
168+
job_name: ""
169+
metrics_path: /metrics
170+
metrics_ports:
171+
- 9902
134172
entitystore:
135173
mode: ec2
136174
region: us-west-2
@@ -244,6 +282,7 @@ service:
244282
extensions:
245283
- agenthealth/metrics
246284
- agenthealth/statuscode
285+
- ecs_observer
247286
- sigv4auth
248287
- agenthealth/logs
249288
- entitystore

translator/tocwconfig/sampleConfig/prometheus_config_linux.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
endpoint_override = "https://fake_endpoint"
2121
force_flush_interval = "30s"
2222
log_stream_name = "host_name_from_env"
23+
mode = "EC2"
2324
region = "us-east-1"
24-
25-
[processors]
25+
region_type = "ACJ"

translator/tocwconfig/sampleConfig/prometheus_config_linux.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,44 @@ extensions:
8686
usage_flags:
8787
mode: EC2
8888
region_type: ACJ
89+
ecs_observer:
90+
cluster_name: ecs-cluster-a
91+
cluster_region: us-west-1
92+
docker_labels:
93+
- job_name: ""
94+
job_name_label: ECS_PROMETHEUS_JOB_NAME_1
95+
metrics_path: ""
96+
metrics_path_label: ECS_PROMETHEUS_METRICS_PATH
97+
port_label: ECS_PROMETHEUS_EXPORTER_PORT_SUBSET
98+
job_label_name: ""
99+
refresh_interval: 1m0s
100+
result_file: "{ecsSdFileName}"
101+
services:
102+
- container_name_pattern: nginx-prometheus-exporter
103+
job_name: service_name_1
104+
metrics_path: /metrics
105+
metrics_ports:
106+
- 9113
107+
name_pattern: .*-application-stack
108+
- container_name_pattern: ""
109+
job_name: ""
110+
metrics_path: /stats/metrics
111+
metrics_ports:
112+
- 9114
113+
name_pattern: run-application-stack
114+
task_definitions:
115+
- arn_pattern: .*task_def_1:[0-9]+
116+
container_name_pattern: ""
117+
job_name: task_def_1
118+
metrics_path: /stats/metrics
119+
metrics_ports:
120+
- 9901
121+
- arn_pattern: task_def_2
122+
container_name_pattern: ^envoy$
123+
job_name: ""
124+
metrics_path: /metrics
125+
metrics_ports:
126+
- 9902
89127
entitystore:
90128
mode: ec2
91129
region: us-east-1
@@ -138,6 +176,7 @@ service:
138176
extensions:
139177
- agenthealth/logs
140178
- agenthealth/statuscode
179+
- ecs_observer
141180
- entitystore
142181
pipelines:
143182
metrics/prometheus/cloudwatchlogs:

translator/tocwconfig/sampleConfig/prometheus_config_windows.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
endpoint_override = "https://fake_endpoint"
2121
force_flush_interval = "5s"
2222
log_stream_name = "host_name_from_env"
23+
mode = "EC2"
2324
region = "us-east-1"
24-
25-
[processors]
25+
region_type = "ACJ"

translator/tocwconfig/sampleConfig/prometheus_config_windows.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,44 @@ extensions:
6868
usage_flags:
6969
mode: EC2
7070
region_type: ACJ
71+
ecs_observer:
72+
cluster_name: ecs-cluster-a
73+
cluster_region: us-west-1
74+
docker_labels:
75+
- job_name: ""
76+
job_name_label: ECS_PROMETHEUS_JOB_NAME_1
77+
metrics_path: ""
78+
metrics_path_label: ECS_PROMETHEUS_METRICS_PATH
79+
port_label: ECS_PROMETHEUS_EXPORTER_PORT_SUBSET
80+
job_label_name: ""
81+
refresh_interval: 1m0s
82+
result_file: "{ecsSdFileName}"
83+
services:
84+
- container_name_pattern: nginx-prometheus-exporter
85+
job_name: service_name_1
86+
metrics_path: /metrics
87+
metrics_ports:
88+
- 9113
89+
name_pattern: .*-application-stack
90+
- container_name_pattern: ""
91+
job_name: ""
92+
metrics_path: /stats/metrics
93+
metrics_ports:
94+
- 9114
95+
name_pattern: run-application-stack
96+
task_definitions:
97+
- arn_pattern: .*task_def_1:[0-9]+
98+
container_name_pattern: ""
99+
job_name: task_def_1
100+
metrics_path: /stats/metrics
101+
metrics_ports:
102+
- 9901
103+
- arn_pattern: task_def_2
104+
container_name_pattern: ^envoy$
105+
job_name: ""
106+
metrics_path: /metrics
107+
metrics_ports:
108+
- 9902
71109
entitystore:
72110
mode: ec2
73111
region: us-east-1
@@ -120,6 +158,7 @@ service:
120158
extensions:
121159
- agenthealth/logs
122160
- agenthealth/statuscode
161+
- ecs_observer
123162
- entitystore
124163
pipelines:
125164
metrics/prometheus/cloudwatchlogs:

translator/tocwconfig/tocwconfig_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ func verifyToTomlTranslation(t *testing.T, input interface{}, desiredTomlPath st
924924
_, decodeError2 := toml.Decode(tomlStr, &actual)
925925
assert.NoError(t, decodeError2)
926926

927-
//assert.NoError(t, os.WriteFile(desiredTomlPath, []byte(tomlStr), 0644)) // useful for regenerating TOML
927+
// assert.NoError(t, os.WriteFile(desiredTomlPath, []byte(tomlStr), 0644)) // useful for regenerating TOML
928928

929929
// This less function sort the content of string slice in alphabetical order so the
930930
// cmp.Equal method will compare the two struct with slices in them, regardless the elements within the slices
@@ -956,8 +956,7 @@ func verifyToYamlTranslation(t *testing.T, input interface{}, expectedYamlFilePa
956956
require.NoError(t, err)
957957
yamlStr := toyamlconfig.ToYamlConfig(yamlConfig)
958958
require.NoError(t, yaml.Unmarshal([]byte(yamlStr), &actual))
959-
960-
//assert.NoError(t, os.WriteFile(expectedYamlFilePath, []byte(yamlStr), 0644)) // useful for regenerating YAML
959+
// assert.NoError(t, os.WriteFile(expectedYamlFilePath, []byte(yamlStr), 0644)) // useful for regenerating YAML
961960
opt := cmpopts.SortSlices(func(x, y interface{}) bool {
962961
return pretty.Sprint(x) < pretty.Sprint(y)
963962
})

translator/translate/otel/common/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const (
3131
TracesCollectedKey = "traces_collected"
3232
MetricsDestinationsKey = "metrics_destinations"
3333
ECSKey = "ecs"
34+
ECSServiceDiscovery = "ecs_service_discovery"
3435
KubernetesKey = "kubernetes"
3536
CloudWatchKey = "cloudwatch"
3637
CloudWatchLogsKey = "cloudwatchlogs"
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package ecsobserver
5+
6+
import (
7+
"fmt"
8+
"strconv"
9+
"strings"
10+
"time"
11+
12+
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/observer/ecsobserver"
13+
"go.opentelemetry.io/collector/component"
14+
"go.opentelemetry.io/collector/confmap"
15+
16+
"github.com/aws/amazon-cloudwatch-agent/translator/translate/otel/common"
17+
)
18+
19+
const (
20+
defaultMetricsPath = "/metrics"
21+
defaultPortLabel = "ECS_PROMETHEUS_EXPORTER_PORT"
22+
defaultMetricsPathLabel = "ECS_PROMETHEUS_METRICS_PATH"
23+
defaultJobNameLabel = ""
24+
)
25+
26+
var ecsSDKey = common.ConfigKey(common.LogsKey, common.MetricsCollectedKey, common.PrometheusKey, common.ECSServiceDiscovery)
27+
28+
type translator struct {
29+
factory component.Factory
30+
name string
31+
}
32+
33+
var _ common.ComponentTranslator = (*translator)(nil)
34+
var _ common.NameSetter = (*translator)(nil)
35+
36+
func NewTranslator(opts ...common.TranslatorOption) common.ComponentTranslator {
37+
t := &translator{
38+
factory: ecsobserver.NewFactory(),
39+
}
40+
for _, opt := range opts {
41+
opt(t)
42+
}
43+
return t
44+
}
45+
46+
func (t *translator) ID() component.ID {
47+
return component.NewIDWithName(t.factory.Type(), t.name)
48+
}
49+
50+
func (t *translator) SetName(name string) {
51+
t.name = name
52+
}
53+
54+
func (t *translator) Translate(conf *confmap.Conf) (component.Config, error) {
55+
if conf == nil || !conf.IsSet(ecsSDKey) {
56+
return nil, &common.MissingKeyError{ID: t.ID(), JsonKey: ecsSDKey}
57+
}
58+
59+
ecsSDValue := conf.Get(ecsSDKey)
60+
ecsSD, ok := ecsSDValue.(map[string]interface{})
61+
if !ok {
62+
return nil, &common.MissingKeyError{ID: t.ID(), JsonKey: ecsSDKey}
63+
}
64+
65+
requiredFields := []string{"sd_target_cluster", "sd_cluster_region", "sd_result_file"}
66+
for _, field := range requiredFields {
67+
if _, ok := ecsSD[field]; !ok {
68+
return nil, &common.MissingKeyError{ID: t.ID(), JsonKey: field}
69+
}
70+
}
71+
72+
refreshDuration, err := time.ParseDuration(getStringWithDefault(ecsSD, "sd_frequency", "10s"))
73+
if err != nil {
74+
return nil, fmt.Errorf("invalid refresh interval: %w", err)
75+
}
76+
77+
cfg := &ecsobserver.Config{
78+
RefreshInterval: refreshDuration,
79+
ClusterName: getString(ecsSD, "sd_target_cluster"),
80+
ClusterRegion: getString(ecsSD, "sd_cluster_region"),
81+
ResultFile: getString(ecsSD, "sd_result_file"),
82+
}
83+
// Docker label based service discovery
84+
if dockerLabel, ok := ecsSD["docker_label"].(map[string]interface{}); ok {
85+
dockerConfig := ecsobserver.DockerLabelConfig{
86+
MetricsPathLabel: getStringWithDefault(dockerLabel, "sd_metrics_path_label", defaultMetricsPath),
87+
PortLabel: getStringWithDefault(dockerLabel, "sd_port_label", defaultPortLabel),
88+
JobNameLabel: getString(dockerLabel, "sd_job_name_label"),
89+
}
90+
cfg.DockerLabels = []ecsobserver.DockerLabelConfig{dockerConfig} // Initialize as slice with single element
91+
}
92+
93+
// Task definition based service discovery
94+
if taskDefs, ok := ecsSD["task_definition_list"].([]interface{}); ok {
95+
for _, td := range taskDefs {
96+
if tdMap, ok := td.(map[string]interface{}); ok {
97+
ports := parseMetricsPorts(getString(tdMap, "sd_metrics_ports"))
98+
taskConfig := ecsobserver.TaskDefinitionConfig{
99+
CommonExporterConfig: ecsobserver.CommonExporterConfig{
100+
JobName: getString(tdMap, "sd_job_name"),
101+
MetricsPath: getStringWithDefault(tdMap, "sd_metrics_path", defaultMetricsPath),
102+
MetricsPorts: convertStringPortsToInt(ports),
103+
},
104+
ArnPattern: getString(tdMap, "sd_task_definition_arn_pattern"),
105+
ContainerNamePattern: getString(tdMap, "sd_container_name_pattern"),
106+
}
107+
cfg.TaskDefinitions = append(cfg.TaskDefinitions, taskConfig)
108+
}
109+
}
110+
}
111+
112+
// Service name based service discovery
113+
if services, ok := ecsSD["service_name_list_for_tasks"].([]interface{}); ok {
114+
for _, svc := range services {
115+
if svcMap, ok := svc.(map[string]interface{}); ok {
116+
ports := parseMetricsPorts(getString(svcMap, "sd_metrics_ports"))
117+
serviceConfig := ecsobserver.ServiceConfig{
118+
CommonExporterConfig: ecsobserver.CommonExporterConfig{
119+
JobName: getString(svcMap, "sd_job_name"),
120+
MetricsPath: getStringWithDefault(svcMap, "sd_metrics_path", defaultMetricsPath),
121+
MetricsPorts: convertStringPortsToInt(ports),
122+
},
123+
NamePattern: getString(svcMap, "sd_service_name_pattern"),
124+
ContainerNamePattern: getString(svcMap, "sd_container_name_pattern"),
125+
}
126+
cfg.Services = append(cfg.Services, serviceConfig)
127+
}
128+
}
129+
}
130+
131+
return cfg, nil
132+
}
133+
134+
// Add helper function to convert string ports to int
135+
func convertStringPortsToInt(ports []string) []int {
136+
result := make([]int, 0, len(ports))
137+
for _, port := range ports {
138+
if p, err := strconv.Atoi(port); err == nil {
139+
result = append(result, p)
140+
}
141+
}
142+
return result
143+
}
144+
145+
// Helper functions
146+
func parseMetricsPorts(ports string) []string {
147+
if ports == "" {
148+
return nil
149+
}
150+
portList := strings.Split(ports, ";")
151+
var cleanPorts []string
152+
for _, port := range portList {
153+
if trimmed := strings.TrimSpace(port); trimmed != "" {
154+
cleanPorts = append(cleanPorts, trimmed)
155+
}
156+
}
157+
return cleanPorts
158+
}
159+
160+
func getString(m map[string]interface{}, key string) string {
161+
if val, ok := m[key].(string); ok {
162+
return val
163+
}
164+
return ""
165+
}
166+
167+
func getStringWithDefault(m map[string]interface{}, key, defaultValue string) string {
168+
if val := getString(m, key); val != "" {
169+
return val
170+
}
171+
return defaultValue
172+
}

0 commit comments

Comments
 (0)