Skip to content

Support configurable log group and EMF with OTLP#1993

Open
movence wants to merge 6 commits intomainfrom
otlp-emf
Open

Support configurable log group and EMF with OTLP#1993
movence wants to merge 6 commits intomainfrom
otlp-emf

Conversation

@movence
Copy link
Contributor

@movence movence commented Jan 22, 2026

Description of the issue

Agent does not allow customers to customize EMF (Embedded Metric Format) logging settings when using OTLP metrics

Description of changes

Add support for log_group_name and emf_processor blurb

License

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Tests

Using agent config LogGroup="OtlpEMFTest" AND MetricNamespace=OTLPEMF/Test"

{
  "agent": {
    "debug": true
  },
  "metrics": {
    "namespace": "CWAgent/OTLP",
    "metrics_collected": {
      "otlp": {
        "http_endpoint": "0.0.0.0:4318",
        "grpc_endpoint": "0.0.0.0:4315"
      }
    },
    "append_dimensions": {
      "InstanceId": "${aws:InstanceId}"
    }
  },
  "logs": {
    "metrics_collected": {
      "otlp": {
        "http_endpoint": "0.0.0.0:4318",
        "grpc_endpoint": "0.0.0.0:4315",
	"log_group_name": "OtlpEMFTest",
	"emf_processor": {
           "metric_namespace": "OTLPEMF/Test"
	}
      }
    }
  }
}

With a dummy bash sending EMF logs via OTLP

{
  "resourceMetrics": [
    {
      "resource": {
        "attributes": [
          {
            "key": "service.name",
            "value": {
              "stringValue": "otlp-test-service"
            }
          },
          {
            "key": "service.version",
            "value": {
              "stringValue": "1.0.0"
            }
          }
        ]
      },
      "scopeMetrics": [
        {
          "scope": {
            "name": "otlp-test-metrics",
            "version": "1.0.0"
          },
          "metrics": [
            {
              "name": "otlp_emf_counter",
              "unit": "1",
              "description": "OTLP test counter metric",
              "sum": {
                "aggregationTemporality": 1,
                "isMonotonic": true,
                "dataPoints": [
                  {
                    "asInt": "COUNTER_VALUE",
                    "startTimeUnixNano": "START_TIME_NANO",
                    "timeUnixNano": "TIMESTAMP_NANO",
                    "attributes": [
                      {
                        "key": "InstanceId",
                        "value": {
                          "stringValue": "$INSTANCE_ID"
                        }
                      },
                      {
                        "key": "test.type",
                        "value": {
                          "stringValue": "otlp_logs_$TEST_TYPE"
                        }
                      }
                    ]
                  }
                ]
              }
            },
            {
              "name": "otlp_emf_gauge",
              "unit": "1",
              "description": "OTLP test gauge metric",
              "gauge": {
                "dataPoints": [
                  {
                    "asDouble": "GAUGE_VALUE",
                    "timeUnixNano": "TIMESTAMP_NANO",
                    "attributes": [
                      {
                        "key": "InstanceId",
                        "value": {
                          "stringValue": "$INSTANCE_ID"
                        }
                      },
                      {
                        "key": "test.type",
                        "value": {
                          "stringValue": "otlp_logs_$TEST_TYPE"
                        }
                      }
                    ]
                  }
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}

Screenshot 2026-01-22 at 3 43 02 PM Screenshot 2026-01-22 at 3 42 10 PM

Requirements

Before commiting your code, please do the following steps.

  1. Run make fmt and make fmt-sh
  2. Run make lint

Integration Tests

To run integration tests against this PR, add the ready for testing label.

@movence movence requested a review from a team as a code owner January 22, 2026 20:45
@movence movence added the ready for testing Indicates this PR is ready for integration tests to run label Jan 22, 2026
Comment on lines 11 to 21
"metric_declaration": [
{
"source_labels": ["job"],
"label_matcher": "^kubernetes-.*",
"dimensions": [["job"], ["job", "instance"]],
"metric_selectors": [
"^container_.*",
"^pod_.*"
]
}
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we validate that the metric_declarations work as expected? Just need to make sure that the EMF exporter isn't normalizing any of the metric attributes.

otlpDefaultLogGroupFormat = "/aws/cwagent"
)

func setOTLPLogGroup(conf *confmap.Conf, cfg *awsemfexporter.Config) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why does this return an error if it's always nil?

Comment on lines 31 to 39
func setOTLPNamespace(conf *confmap.Conf, cfg *awsemfexporter.Config) error {
if namespace, ok := common.GetString(conf, common.ConfigKey(otlpEMFProcessorBasePathKey, metricNamespace)); ok {
cfg.Namespace = namespace
return nil
}

cfg.Namespace = otlpDefaultCloudWatchNamespace
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could create a common.GetOrDefaultString similar to common.GetOrDefaultBool to simplify this a bit.

Comment on lines 41 to 64
func setOTLPMetricDescriptors(conf *confmap.Conf, cfg *awsemfexporter.Config) error {
metricUnitKey := common.ConfigKey(otlpEMFProcessorBasePathKey, metricUnit)
if !conf.IsSet(metricUnitKey) {
return nil
}

mus := conf.Get(metricUnitKey)
metricUnits := mus.(map[string]interface{})
var metricDescriptors []map[string]string
for mName, unit := range metricUnits {
metricDescriptors = append(metricDescriptors, map[string]string{
"metric_name": mName,
"unit": unit.(string),
})
}
c := confmap.NewFromStringMap(map[string]interface{}{
"metric_descriptors": metricDescriptors,
})
cfg.MetricDescriptors = []awsemfexporter.MetricDescriptor{}
if err := c.Unmarshal(&cfg); err != nil {
return fmt.Errorf("unable to unmarshal metric_descriptors: %w", err)
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This is exactly the same as setPrometheusMetricDescriptors can we combine it?

Comment on lines 66 to 112
func setOTLPMetricDeclarations(conf *confmap.Conf, cfg *awsemfexporter.Config) error {
metricDeclarationKey := common.ConfigKey(otlpEMFProcessorBasePathKey, metricDeclartion)
if !conf.IsSet(metricDeclarationKey) {
return nil
}
metricDeclarations := conf.Get(metricDeclarationKey)
var declarations []map[string]interface{}
for _, md := range metricDeclarations.([]interface{}) {
metricDeclaration := md.(map[string]interface{})
declaration := map[string]interface{}{}
if dimensions, ok := metricDeclaration["dimensions"]; ok {
declaration["dimensions"] = dimensions
}
if metricSelectors, ok := metricDeclaration["metric_selectors"]; ok {
declaration["metric_name_selectors"] = metricSelectors
} else {
// If no metric selectors are provided, that particular metric declaration is invalid
continue
}
labelMatcher, ok := metricDeclaration["label_matcher"]
if !ok {
labelMatcher = ".*"
}
sourceLabels, ok := metricDeclaration["source_labels"]
if ok {
// OTel awsemfexporter allows specifying multiple label_matchers but CWA only allows specifying one
declaration["label_matchers"] = [...]map[string]interface{}{
{
"label_names": sourceLabels,
"regex": labelMatcher,
},
}
} else {
// If no source labels or label matchers are provided, that particular metric declaration is invalid
continue
}
declarations = append(declarations, declaration)
}
c := confmap.NewFromStringMap(map[string]interface{}{
"metric_declarations": declarations,
})
cfg.MetricDeclarations = []*awsemfexporter.MetricDeclaration{} // Clear out any existing declarations
if err := c.Unmarshal(&cfg); err != nil {
return fmt.Errorf("unable to unmarshal metric_declarations: %w", err)
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Same as the other one. Exactly the same as the prometheus version. Could have a shared helper function.

}

func setOTLPFields(conf *confmap.Conf, cfg *awsemfexporter.Config) error {
setDisableMetricExtraction(otlpBasePathKey, conf, cfg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know it's not part of this PR, but this function name feels misleading. It's disabling DisableMetricExtraction, but it made me think this was disabling metric extraction.

return conf.IsSet(common.OTLPLogsKey)
}

func isOTLPEMF(conf *confmap.Conf, pipelineName string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTLP should have its own awsemfexporter. What if metric declarations are set for OTLP? Won't other metrics be impacted that are sent by the shared EMF exporter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTLP already gets its own emf exporter so there is no impact to other emf exporters (prom, CI or etc)

},
"log_group_name": {
"description": "CloudWatch log group name for OTLP metrics",
"type": "string"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably place a restriction on length like for the other fields

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can make it to use already existing logGroupNameDefinition but the current schema is already inconsistent where prometheus contains log_group_name without any restrictions. Maybe it's better to use logGroupNameDefinition everywhere but this might affect agent configs already using longer names with prometheus.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't have to be in this PR, but based on documentation https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogGroup.html

Log group names can be between 1 and 512 characters long.

},
"log_group_name": {
"description": "CloudWatch log group name for OTLP metrics",
"type": "string"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't have to be in this PR, but based on documentation https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogGroup.html

Log group names can be between 1 and 512 characters long.

imds_retries: 1
local_mode: false
log_group_name: /aws/cwagent
log_group_name: /aws/application/otlp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could we have a test that shows it fallsback to defaults if not configured or does that already exist in a different sample config?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: So the EMF exporter is unnamed. Do we want to name it (awsemf/otlp) so we don't accidentally share the exporter instance in the future with other pipelines?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added? Why do we need this now when it wasn't required before?

}

// setNamespaceWithDefault is a shared function to set namespace from config or use a default
func setNamespaceWithDefault(conf *confmap.Conf, namespaceKey string, defaultNamespace string, cfg *awsemfexporter.Config) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why does this return an error if it's always nil?

@github-actions
Copy link
Contributor

This PR was marked stale due to lack of activity.

@github-actions github-actions bot added the Stale label Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for testing Indicates this PR is ready for integration tests to run Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants