Skip to content

Commit

Permalink
Add support for a supplemental YAML configuration for the CloudWatchA…
Browse files Browse the repository at this point in the history
…gent (#241)
  • Loading branch information
mitali-salvi authored Oct 16, 2024
1 parent c277a41 commit fc2bdf4
Show file tree
Hide file tree
Showing 43 changed files with 14,760 additions and 8,091 deletions.
3 changes: 3 additions & 0 deletions apis/v1alpha1/amazoncloudwatchagent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ type AmazonCloudWatchAgentSpec struct {
// Config is the raw JSON to be used as the collector's configuration. Refer to the OpenTelemetry Collector documentation for details.
// +required
Config string `json:"config,omitempty"`
// Config is the raw YAML to be used as the collector's configuration. Refer to the OpenTelemetry Collector documentation for details.
// +optional
OtelConfig string `json:"otelConfig,omitempty"`
// VolumeMounts represents the mount points to use in the underlying collector deployment(s)
// +optional
// +listType=atomic
Expand Down
3 changes: 3 additions & 0 deletions apis/v1alpha2/amazoncloudwatchagent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ type AmazonCloudWatchAgentSpec struct {
// Config is the raw JSON to be used as the collector's configuration. Refer to the OpenTelemetry Collector documentation for details.
// +required
Config string `json:"config,omitempty"`
// Config is the raw YAML to be used as the collector's configuration. Refer to the OpenTelemetry Collector documentation for details.
// +optional
OtelConfig string `json:"otelConfig,omitempty"`
// VolumeMounts represents the mount points to use in the underlying collector deployment(s)
// +optional
// +listType=atomic
Expand Down
6,456 changes: 3,259 additions & 3,197 deletions config/crd/bases/cloudwatch.aws.amazon.com_amazoncloudwatchagents.yaml

Large diffs are not rendered by default.

2,674 changes: 1,342 additions & 1,332 deletions config/crd/bases/cloudwatch.aws.amazon.com_dcgmexporters.yaml

Large diffs are not rendered by default.

915 changes: 485 additions & 430 deletions config/crd/bases/cloudwatch.aws.amazon.com_instrumentations.yaml

Large diffs are not rendered by default.

2,875 changes: 1,449 additions & 1,426 deletions config/crd/bases/cloudwatch.aws.amazon.com_neuronmonitors.yaml

Large diffs are not rendered by default.

8,022 changes: 6,376 additions & 1,646 deletions docs/api.md

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions internal/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
)

const (
defaultCollectorConfigMapEntry = "cwagentconfig.json"
defaultCollectorConfigMapEntry = "cwagentconfig.json"
defaultOtelCollectorConfigMapEntry = "cwagentotelconfig.yaml"
)

// Config holds the static configuration for this operator.
Expand All @@ -21,6 +22,7 @@ type Config struct {
autoInstrumentationPythonImage string
collectorImage string
collectorConfigMapEntry string
otelCollectorConfigMapEntry string
autoInstrumentationDotNetImage string
autoInstrumentationGoImage string
autoInstrumentationApacheHttpdImage string
Expand All @@ -36,9 +38,10 @@ type Config struct {
func New(opts ...Option) Config {
// initialize with the default values
o := options{
collectorConfigMapEntry: defaultCollectorConfigMapEntry,
logger: logf.Log.WithName("config"),
version: version.Get(),
collectorConfigMapEntry: defaultCollectorConfigMapEntry,
otelCollectorConfigMapEntry: defaultOtelCollectorConfigMapEntry,
logger: logf.Log.WithName("config"),
version: version.Get(),
}
for _, opt := range opts {
opt(&o)
Expand All @@ -47,6 +50,7 @@ func New(opts ...Option) Config {
return Config{
collectorImage: o.collectorImage,
collectorConfigMapEntry: o.collectorConfigMapEntry,
otelCollectorConfigMapEntry: o.otelCollectorConfigMapEntry,
logger: o.logger,
autoInstrumentationJavaImage: o.autoInstrumentationJavaImage,
autoInstrumentationNodeJSImage: o.autoInstrumentationNodeJSImage,
Expand All @@ -66,11 +70,16 @@ func (c *Config) CollectorImage() string {
return c.collectorImage
}

// CollectorConfigMapEntry represents the configuration file name for the collector. Immutable.
// CollectorConfigMapEntry represents the configuration JSON file name for the collector. Immutable.
func (c *Config) CollectorConfigMapEntry() string {
return c.collectorConfigMapEntry
}

// OtelCollectorConfigMapEntry represents the configuration YAML file name for the collector. Immutable.
func (c *Config) OtelCollectorConfigMapEntry() string {
return c.otelCollectorConfigMapEntry
}

// AutoInstrumentationJavaImage returns OpenTelemetry Java auto-instrumentation container image.
func (c *Config) AutoInstrumentationJavaImage() string {
return c.autoInstrumentationJavaImage
Expand Down
6 changes: 4 additions & 2 deletions internal/config/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ func TestNewConfig(t *testing.T) {
// prepare
cfg := config.New(
config.WithCollectorImage("some-image"),
config.WithCollectorConfigMapEntry("some-config.yaml"),
config.WithCollectorConfigMapEntry("some-config.json"),
config.WithOtelCollectorConfigMapEntry("some-otel-config.yaml"),
)

// test
assert.Equal(t, "some-image", cfg.CollectorImage())
assert.Equal(t, "some-config.yaml", cfg.CollectorConfigMapEntry())
assert.Equal(t, "some-config.json", cfg.CollectorConfigMapEntry())
assert.Equal(t, "some-otel-config.yaml", cfg.OtelCollectorConfigMapEntry())
}
6 changes: 6 additions & 0 deletions internal/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type options struct {
autoInstrumentationNginxImage string
collectorImage string
collectorConfigMapEntry string
otelCollectorConfigMapEntry string
dcgmExporterImage string
neuronMonitorImage string
labelsFilter []string
Expand All @@ -42,6 +43,11 @@ func WithCollectorConfigMapEntry(s string) Option {
o.collectorConfigMapEntry = s
}
}
func WithOtelCollectorConfigMapEntry(s string) Option {
return func(o *options) {
o.otelCollectorConfigMapEntry = s
}
}
func WithLogger(logger logr.Logger) Option {
return func(o *options) {
o.logger = logger
Expand Down
2 changes: 2 additions & 0 deletions internal/manifests/collector/adapters/config_from.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Traces struct {
type MetricsCollected struct {
StatsD *statsD `json:"statsd,omitempty"`
CollectD *collectD `json:"collectd,omitempty"`
OTLP *otlp `json:"otlp,omitempty"`
JMX *jmx `json:"jmx,omitempty"`
}

Expand All @@ -66,6 +67,7 @@ type LogMetricsCollected struct {
ApplicationSignals *AppSignals `json:"application_signals,omitempty"`
AppSignals *AppSignals `json:"app_signals,omitempty"`
Kubernetes *kubernetes `json:"kubernetes,omitempty"`
OTLP *otlp `json:"otlp,omitempty"`
}

type TracesCollected struct {
Expand Down
86 changes: 85 additions & 1 deletion internal/manifests/collector/adapters/config_to_ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@
package adapters

import (
"fmt"
"net"
"sort"
"strconv"
"strings"

"github.com/go-logr/logr"
"github.com/mitchellh/mapstructure"
corev1 "k8s.io/api/core/v1"

"github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/parser"
receiverParser "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/parser/receiver"
)

type ComponentType int

const (
ComponentTypeReceiver ComponentType = iota
ComponentTypeExporter
)

func (c ComponentType) String() string {
Expand Down Expand Up @@ -58,3 +63,82 @@ func ConfigToMetricsPort(logger logr.Logger, config map[interface{}]interface{})

return int32(i64), nil
}

func GetServicePortsFromCWAgentOtelConfig(logger logr.Logger, config map[interface{}]interface{}) ([]corev1.ServicePort, error) {
ports, err := ConfigToComponentPorts(logger, ComponentTypeReceiver, config)
if err != nil {
logger.Error(err, "there was a problem while getting the ports from the receivers")
return nil, err
}

return ports, nil
}

// ConfigToComponentPorts converts the incoming configuration object into a set of service ports required by the exporters.
func ConfigToComponentPorts(logger logr.Logger, cType ComponentType, config map[interface{}]interface{}) ([]corev1.ServicePort, error) {
// now, we gather which ports we might need to open
// for that, we get all the exporters and check their `endpoint` properties,
// extracting the port from it. The port name has to be a "DNS_LABEL", so, we try to make it follow the pattern:
// examples:
// ```yaml
// components:
// componentexample:
// endpoint: 0.0.0.0:12345
// componentexample/settings:
// endpoint: 0.0.0.0:12346
// in this case, we have 2 ports, named: "componentexample" and "componentexample-settings"
componentsProperty, ok := config[fmt.Sprintf("%ss", cType.String())]
if !ok {
return nil, fmt.Errorf("no %ss available as part of the configuration", cType)
}

components, ok := componentsProperty.(map[interface{}]interface{})
if !ok {
return nil, fmt.Errorf("%ss doesn't contain valid components", cType.String())
}

compEnabled := getEnabledComponents(config, cType)

if compEnabled == nil {
return nil, fmt.Errorf("no enabled %ss available as part of the configuration", cType)
}

ports := []corev1.ServicePort{}
for key, val := range components {
// This check will pass only the enabled components,
// then only the related ports will be opened.
if !compEnabled[key] {
continue
}
extractedComponent, ok := val.(map[interface{}]interface{})
if !ok {
logger.V(2).Info("component doesn't seem to be a map of properties", cType.String(), key)
extractedComponent = map[interface{}]interface{}{}
}

cmptName := key.(string)
var cmptParser parser.ComponentPortParser
var err error
cmptParser, err = receiverParser.For(logger, cmptName, extractedComponent)
if err != nil {
logger.V(2).Info("no parser found for '%s'", cmptName)
continue
}

exprtPorts, err := cmptParser.Ports()
if err != nil {
logger.Error(err, "parser for '%s' has returned an error: %w", cmptName, err)
continue
}

if len(exprtPorts) > 0 {
ports = append(ports, exprtPorts...)
}
}

sort.Slice(ports, func(i, j int) bool {
return ports[i].Name < ports[j].Name
})

return ports, nil
}
15 changes: 15 additions & 0 deletions internal/manifests/collector/config_replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package collector
import (
"encoding/json"

"gopkg.in/yaml.v2"

_ "github.com/prometheus/prometheus/discovery/install" // Package install has the side-effect of registering all builtin.

"github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1"
Expand All @@ -25,3 +27,16 @@ func ReplaceConfig(instance v1alpha1.AmazonCloudWatchAgent) (string, error) {

return string(out), nil
}

func ReplaceOtelConfig(instance v1alpha1.AmazonCloudWatchAgent) (string, error) {
config, err := adapters.ConfigFromString(instance.Spec.OtelConfig)
if err != nil {
return "", err
}

out, err := yaml.Marshal(config)
if err != nil {
return "", err
}
return string(out), nil
}
19 changes: 15 additions & 4 deletions internal/manifests/collector/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,30 @@ func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) {

replacedConf, err := ReplaceConfig(params.OtelCol)
if err != nil {
params.Log.V(2).Info("failed to update prometheus config to use sharded targets: ", "err", err)
params.Log.V(2).Info("failed to update config: ", "err", err)
return nil, err
}

sourceDataMap := map[string]string{
params.Config.CollectorConfigMapEntry(): replacedConf,
}

if params.OtelCol.Spec.OtelConfig != "" {
replacedOtelConfig, err := ReplaceOtelConfig(params.OtelCol)
if err != nil {
params.Log.V(2).Info("failed to update otel config: ", "err", err)
return nil, err
}
sourceDataMap[params.Config.OtelCollectorConfigMapEntry()] = replacedOtelConfig
}

return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: params.OtelCol.Namespace,
Labels: labels,
Annotations: params.OtelCol.Annotations,
},
Data: map[string]string{
"cwagentconfig.json": replacedConf,
},
Data: sourceDataMap,
}, nil
}
58 changes: 53 additions & 5 deletions internal/manifests/collector/configmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import (
)

func TestDesiredConfigMap(t *testing.T) {
expectedLables := map[string]string{
expectedLabels := map[string]string{
"app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator",
"app.kubernetes.io/instance": "default.test",
"app.kubernetes.io/part-of": "amazon-cloudwatch-agent",
"app.kubernetes.io/version": "0.47.0",
}

t.Run("should return expected cwagent config map", func(t *testing.T) {
expectedLables["app.kubernetes.io/component"] = "amazon-cloudwatch-agent"
expectedLables["app.kubernetes.io/name"] = "test"
expectedLables["app.kubernetes.io/version"] = "0.0.0"
expectedLabels["app.kubernetes.io/component"] = "amazon-cloudwatch-agent"
expectedLabels["app.kubernetes.io/name"] = "test"
expectedLabels["app.kubernetes.io/version"] = "0.0.0"

expectedData := map[string]string{
"cwagentconfig.json": `{"logs":{"metrics_collected":{"application_signals":{},"kubernetes":{"enhanced_container_insights":true}}},"traces":{"traces_collected":{"application_signals":{}}}}`,
Expand All @@ -31,8 +31,56 @@ func TestDesiredConfigMap(t *testing.T) {

assert.NoError(t, err)
assert.Equal(t, "test", actual.Name)
assert.Equal(t, expectedLables, actual.Labels)
assert.Equal(t, expectedLabels, actual.Labels)
assert.Equal(t, expectedData, actual.Data)

})
}

func TestDesiredConfigMapWithOtelConfigSupplied(t *testing.T) {
expectedLabels := map[string]string{
"app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator",
"app.kubernetes.io/instance": "default.test",
"app.kubernetes.io/part-of": "amazon-cloudwatch-agent",
"app.kubernetes.io/version": "0.47.0",
}

t.Run("should return expected cwagent config map", func(t *testing.T) {
expectedLabels["app.kubernetes.io/component"] = "amazon-cloudwatch-agent"
expectedLabels["app.kubernetes.io/name"] = "test"
expectedLabels["app.kubernetes.io/version"] = "0.0.0"

expectedData := map[string]string{
"cwagentconfig.json": `{"logs":{"metrics_collected":{"application_signals":{},"kubernetes":{"enhanced_container_insights":true}}},"traces":{"traces_collected":{"application_signals":{}}}}`,
"cwagentotelconfig.yaml": `receivers:
jaeger:
protocols:
grpc:
prometheus:
config:
scrape_configs:
- job_name: otel-collector
scrape_interval: 10s
static_configs:
- targets: [ '0.0.0.0:8888', '0.0.0.0:9999' ]
exporters:
debug:
service:
pipelines:
metrics:
receivers: [prometheus, jaeger]
exporters: [debug]`,
}

param := otelConfigParams()
actual, err := ConfigMap(param)

assert.NoError(t, err)
assert.Equal(t, "test", actual.Name)
assert.Equal(t, expectedLabels, actual.Labels)
assert.Equal(t, expectedData["cwagentconfig.json"], actual.Data["cwagentconfig.json"])
assert.YAMLEq(t, expectedData["cwagentotelconfig.yaml"], actual.Data["cwagentotelconfig.yaml"])
})
}
Loading

0 comments on commit fc2bdf4

Please sign in to comment.