Skip to content

Commit ea29673

Browse files
authored
Add support for 1-second Storage Resolution in the AWS EMF Exporter (#36057)
This change implements the capability for users of the AWS EMF Exporter to specify which metrics they would like to have sent to CloudWatch with a 1 second Storage Resolution. The EMF Exporter now explicitly states the Storage Resolution for each metric as 60 seconds, the previous implicit default, so there is no behavior change. If the user specifies a metric to have 1 second resolution it will be sent to CloudWatch EMF with the Storage Resolution set accordingly. #### Description Previously the AWS EMF Exporter sent metric data into CloudWatch without specifying the storage resolution. CloudWatch would then default to a 60 second storage resolution, even if metrics are sent more frequently than every 60 seconds. This would confuse users when they try to apply functions like AVG, SUM, MAX, or MIN to their metrics with a period of 5 seconds. The function would be applied by CloudWatch to 60 seconds worth of data and produced unexpected results and confusion for the user. This commit makes this 60 second resolution explicit in the messages sent to CloudWatch by the EMF Exporter and also gives the user the option to specify a more granular 1 second resolution per metric in the configuration file of the AWS EMF Exporter. #### Link to tracking issue Fixes #29506 #### Testing Added tests to verify that config file parsing validates a metric descriptor that specifies either a valid unit, valid storage resolution, or both and rejects other invalid metric descriptors. Added tests that the translation from metric data to CW EMF carries a storage resolution with it, defaulting to a value of 60 (current behavior) if no storage resolution valid is explicitly set in the configuration. #### Documentation Comments added in the code but have not updated the README.md pending acceptance of the PR.
1 parent 290e3b3 commit ea29673

File tree

4 files changed

+212
-109
lines changed

4 files changed

+212
-109
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: awsemfexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add support for 1 second metric resolution in CloudWatch Embedded Metrics Format based on metric attributes
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [29506]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

exporter/awsemfexporter/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ This exporter follows default credential resolution for the
8585
Follow the [guidelines](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html) for the
8686
credential configuration.
8787

88+
## Metric Attributes
89+
By setting attributes on your metrics you can change how individual metrics are sent to CloudWatch. Attributes can be set in code or using components like the [Attribute Processor](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/attributesprocessor).
90+
91+
The AWS EMF Exporter will interpret the following metric attributes to change how it publishes metrics to CloudWatch:
92+
93+
| Attribute Name | Description | Default |
94+
| :---------------- | :--------------------------------------------------------------------- | ------- |
95+
| `aws.emf.storage_resolution` | This attribute should be set to an integer value of `1` or `60`. When sending the metric value to CloudWatch use the specified storage resolution value. CloudWatch currently supports a storage resolution of `1` or `60` to indicate 1 second or 60 second resolution. | `aws.emf.storage_resolution = 60` |
8896

8997
## Configuration Examples
9098

exporter/awsemfexporter/metric_translator.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"fmt"
99
"reflect"
10+
"strconv"
1011
"time"
1112

1213
"go.opentelemetry.io/collector/pdata/pmetric"
@@ -29,6 +30,9 @@ const (
2930
prometheusReceiver = "prometheus"
3031
attributeReceiver = "receiver"
3132
fieldPrometheusMetricType = "prom_metric_type"
33+
34+
// metric attributes for AWS EMF, not to be treated as metric labels
35+
emfStorageResolutionAttribute = "aws.emf.storage_resolution"
3236
)
3337

3438
var fieldPrometheusTypes = map[pmetric.MetricType]string{
@@ -45,10 +49,16 @@ type cWMetrics struct {
4549
fields map[string]any
4650
}
4751

52+
type cWMetricInfo struct {
53+
Name string
54+
Unit string
55+
StorageResolution int
56+
}
57+
4858
type cWMeasurement struct {
4959
Namespace string
5060
Dimensions [][]string
51-
Metrics []map[string]string
61+
Metrics []cWMetricInfo
5262
}
5363

5464
type cWMetricStats struct {
@@ -156,7 +166,7 @@ func (mt metricTranslator) translateOTelToGroupedMetric(rm pmetric.ResourceMetri
156166

157167
// translateGroupedMetricToCWMetric converts Grouped Metric format to CloudWatch Metric format.
158168
func translateGroupedMetricToCWMetric(groupedMetric *groupedMetric, config *Config) *cWMetrics {
159-
labels := groupedMetric.labels
169+
labels := filterAWSEMFAttributes(groupedMetric.labels)
160170
fieldsLength := len(labels) + len(groupedMetric.metrics)
161171

162172
isPrometheusMetric := groupedMetric.metadata.receiver == prometheusReceiver
@@ -198,7 +208,7 @@ func translateGroupedMetricToCWMetric(groupedMetric *groupedMetric, config *Conf
198208

199209
// groupedMetricToCWMeasurement creates a single CW Measurement from a grouped metric.
200210
func groupedMetricToCWMeasurement(groupedMetric *groupedMetric, config *Config) cWMeasurement {
201-
labels := groupedMetric.labels
211+
labels := filterAWSEMFAttributes(groupedMetric.labels)
202212
dimensionRollupOption := config.DimensionRollupOption
203213

204214
// Create a dimension set containing list of label names
@@ -208,6 +218,7 @@ func groupedMetricToCWMeasurement(groupedMetric *groupedMetric, config *Config)
208218
dimSet[idx] = labelName
209219
idx++
210220
}
221+
211222
dimensions := [][]string{dimSet}
212223

213224
// Apply single/zero dimension rollup to labels
@@ -228,14 +239,20 @@ func groupedMetricToCWMeasurement(groupedMetric *groupedMetric, config *Config)
228239
// Add on rolled-up dimensions
229240
dimensions = append(dimensions, rollupDimensionArray...)
230241

231-
metrics := make([]map[string]string, len(groupedMetric.metrics))
242+
metrics := make([]cWMetricInfo, len(groupedMetric.metrics))
232243
idx = 0
233244
for metricName, metricInfo := range groupedMetric.metrics {
234-
metrics[idx] = map[string]string{
235-
"Name": metricName,
245+
metrics[idx] = cWMetricInfo{
246+
Name: metricName,
247+
StorageResolution: 60,
236248
}
237249
if metricInfo.unit != "" {
238-
metrics[idx]["Unit"] = metricInfo.unit
250+
metrics[idx].Unit = metricInfo.unit
251+
}
252+
if storRes, ok := groupedMetric.labels[emfStorageResolutionAttribute]; ok {
253+
if storResInt, err := strconv.Atoi(storRes); err == nil {
254+
metrics[idx].StorageResolution = storResInt
255+
}
239256
}
240257
idx++
241258
}
@@ -250,7 +267,7 @@ func groupedMetricToCWMeasurement(groupedMetric *groupedMetric, config *Config)
250267
// groupedMetricToCWMeasurementsWithFilters filters the grouped metric using the given list of metric
251268
// declarations and returns the corresponding list of CW Measurements.
252269
func groupedMetricToCWMeasurementsWithFilters(groupedMetric *groupedMetric, config *Config) (cWMeasurements []cWMeasurement) {
253-
labels := groupedMetric.labels
270+
labels := filterAWSEMFAttributes(groupedMetric.labels)
254271

255272
// Filter metric declarations by labels
256273
metricDeclarations := make([]*MetricDeclaration, 0, len(config.MetricDeclarations))
@@ -278,7 +295,7 @@ func groupedMetricToCWMeasurementsWithFilters(groupedMetric *groupedMetric, conf
278295
// Group metrics by matched metric declarations
279296
type metricDeclarationGroup struct {
280297
metricDeclIdxList []int
281-
metrics []map[string]string
298+
metrics []cWMetricInfo
282299
}
283300

284301
metricDeclGroups := make(map[string]*metricDeclarationGroup)
@@ -299,19 +316,25 @@ func groupedMetricToCWMeasurementsWithFilters(groupedMetric *groupedMetric, conf
299316
continue
300317
}
301318

302-
metric := map[string]string{
303-
"Name": metricName,
319+
metric := cWMetricInfo{
320+
Name: metricName,
321+
StorageResolution: 60,
304322
}
305323
if metricInfo.unit != "" {
306-
metric["Unit"] = metricInfo.unit
324+
metric.Unit = metricInfo.unit
325+
}
326+
if storRes, ok := groupedMetric.labels[emfStorageResolutionAttribute]; ok {
327+
if storResInt, err := strconv.Atoi(storRes); err == nil {
328+
metric.StorageResolution = storResInt
329+
}
307330
}
308331
metricDeclKey := fmt.Sprint(metricDeclIdx)
309332
if group, ok := metricDeclGroups[metricDeclKey]; ok {
310333
group.metrics = append(group.metrics, metric)
311334
} else {
312335
metricDeclGroups[metricDeclKey] = &metricDeclarationGroup{
313336
metricDeclIdxList: metricDeclIdx,
314-
metrics: []map[string]string{metric},
337+
metrics: []cWMetricInfo{metric},
315338
}
316339
}
317340
}
@@ -465,3 +488,14 @@ func translateGroupedMetricToEmf(groupedMetric *groupedMetric, config *Config, d
465488

466489
return event, nil
467490
}
491+
492+
func filterAWSEMFAttributes(labels map[string]string) map[string]string {
493+
// remove any labels that are attributes specific to AWS EMF Exporter
494+
filteredLabels := make(map[string]string)
495+
for labelName := range labels {
496+
if labelName != emfStorageResolutionAttribute {
497+
filteredLabels[labelName] = labels[labelName]
498+
}
499+
}
500+
return filteredLabels
501+
}

0 commit comments

Comments
 (0)