Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(inputs.cloudwatch): Option to produce dense metrics #15317

Merged
merged 2 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 57 additions & 10 deletions plugins/inputs/cloudwatch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
# role_session_name = ""
# profile = ""
# shared_credential_file = ""
## If you are using CloudWatch cross-account observability, you can
## set IncludeLinkedAccounts to true in a monitoring account

## If you are using CloudWatch cross-account observability, you can
## set IncludeLinkedAccounts to true in a monitoring account
## and collect metrics from the linked source accounts
# include_linked_accounts = false

## Endpoint to make request against, the correct endpoint is automatically
## determined and this option should only be set if you wish to override the
## default.
Expand Down Expand Up @@ -102,6 +102,13 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Metric Statistic Namespaces (required)
namespaces = ["AWS/ELB"]

## Metric Format
## This determines the format of the produces metrics. 'sparse', the default
## will produce a unique field for each statistic. 'dense' will report all
## statistics will be in a field called value and have a metric_name tag
## defining the name of the statistic. See the plugin README for examples.
# metric_format = "sparse"

## Maximum requests per second. Note that the global default AWS rate limit
## is 50 reqs/sec, so if you define multiple namespaces, these should add up
## to a maximum of 50.
Expand Down Expand Up @@ -212,15 +219,53 @@ but will output five metrics timestamped one minute apart.
## Metrics

Each CloudWatch Namespace monitored records a measurement with fields for each
available Metric Statistic. Namespace and Metrics are represented in [snake
available Metric Statistic. Namespace and Metrics are represented in [snake
case](https://en.wikipedia.org/wiki/Snake_case)

### Sparse Metrics

By default, metrics generated by this plugin are sparse. Use the `metric_format`
option to override this setting.

Sparse metrics produce a set of fields for every AWS Metric.

- cloudwatch_{namespace}
- Fields
- {metric}_sum (metric Sum value)
- {metric}_average (metric Average value)
- {metric}_minimum (metric Minimum value)
- {metric}_maximum (metric Maximum value)
- {metric}_sample_count (metric SampleCount value)

For example:

```text
cloudwatch_aws_usage,class=None,resource=GetSecretValue,service=Secrets\ Manager,type=API call_count_maximum=1,call_count_minimum=1,call_count_sum=8,call_count_sample_count=8,call_count_average=1 1715097720000000000
```

### Dense Metrics

Dense metrics are generated when `metric_format` is set to `dense`.

Dense metrics use the same fields over and over for every AWS Metric and
differentiate between AWS Metrics using a tag called `metric_name` with the AWS
Metric name:

- cloudwatch_{namespace}
- {metric}_sum (metric Sum value)
- {metric}_average (metric Average value)
- {metric}_minimum (metric Minimum value)
- {metric}_maximum (metric Maximum value)
- {metric}_sample_count (metric SampleCount value)
- Tags
- metric_name (AWS Metric name)
- Fields
- sum (metric Sum value)
- average (metric Average value)
- minimum (metric Minimum value)
- maximum (metric Maximum value)
- sample_count (metric SampleCount value)

For example:

```text
cloudwatch_aws_usage,class=None,resource=GetSecretValue,service=Secrets\ Manager,metric_name=call_count,type=API sum=6,sample_count=6,average=1,maximum=1,minimum=1 1715097840000000000
```

### Tags

Expand Down Expand Up @@ -274,6 +319,8 @@ aws cloudwatch get-metric-data \

## Example Output

See the discussion above about sparse vs dense metrics for more details.

```text
cloudwatch_aws_elb,load_balancer_name=p-example,region=us-east-1 latency_average=0.004810798017284538,latency_maximum=0.1100282669067383,latency_minimum=0.0006084442138671875,latency_sample_count=4029,latency_sum=19.382705211639404 1459542420000000000
```
Expand Down
29 changes: 26 additions & 3 deletions plugins/inputs/cloudwatch/cloudwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -48,8 +49,8 @@ type CloudWatch struct {
RecentlyActive string `toml:"recently_active"`
BatchSize int `toml:"batch_size"`
IncludeLinkedAccounts bool `toml:"include_linked_accounts"`

Log telegraf.Logger `toml:"-"`
MetricFormat string `toml:"metric_format"`
Log telegraf.Logger `toml:"-"`

client cloudwatchClient
statFilter filter.Filter
Expand Down Expand Up @@ -98,6 +99,14 @@ func (c *CloudWatch) Init() error {
c.Namespaces = append(c.Namespaces, c.Namespace)
}

switch c.MetricFormat {
case "":
c.MetricFormat = "sparse"
case "dense", "sparse":
default:
return fmt.Errorf("invalid metric_format: %s", c.MetricFormat)
}

err := c.initializeCloudWatch()
if err != nil {
return err
Expand Down Expand Up @@ -462,7 +471,21 @@ func (c *CloudWatch) aggregateMetrics(
tags["region"] = c.Region

for i := range result.Values {
grouper.Add(namespace, tags, result.Timestamps[i], *result.Label, result.Values[i])
if c.MetricFormat == "dense" {
// Remove the IDs from the result ID to get the statistic type
// e.g. "average" from "average_0_0"
re := regexp.MustCompile(`_\d+_\d+$`)
statisticType := re.ReplaceAllString(*result.Id, "")

// Remove the statistic type from the label to get the AWS Metric name
// e.g. "CPUUtilization" from "CPUUtilization_average"
re = regexp.MustCompile(`_?` + regexp.QuoteMeta(statisticType) + `$`)
tags["metric_name"] = re.ReplaceAllString(*result.Label, "")

grouper.Add(namespace, tags, result.Timestamps[i], statisticType, result.Values[i])
} else {
grouper.Add(namespace, tags, result.Timestamps[i], *result.Label, result.Values[i])
}
}
}
}
Expand Down
38 changes: 38 additions & 0 deletions plugins/inputs/cloudwatch/cloudwatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,44 @@ func TestGather(t *testing.T) {
acc.AssertContainsTaggedFields(t, "cloudwatch_aws_elb", fields, tags)
}

func TestGatherDenseMetric(t *testing.T) {
duration, _ := time.ParseDuration("1m")
internalDuration := config.Duration(duration)
c := &CloudWatch{
CredentialConfig: internalaws.CredentialConfig{
Region: "us-east-1",
},
Namespace: "AWS/ELB",
Delay: internalDuration,
Period: internalDuration,
RateLimit: 200,
BatchSize: 500,
MetricFormat: "dense",
Log: testutil.Logger{},
}

var acc testutil.Accumulator

require.NoError(t, c.Init())
c.client = &mockGatherCloudWatchClient{}
require.NoError(t, acc.GatherError(c.Gather))

fields := map[string]interface{}{}
fields["minimum"] = 0.1
fields["maximum"] = 0.3
fields["average"] = 0.2
fields["sum"] = 123.0
fields["sample_count"] = 100.0

tags := map[string]string{}
tags["region"] = "us-east-1"
tags["load_balancer_name"] = "p-example1"
tags["metric_name"] = "latency"

require.True(t, acc.HasMeasurement("cloudwatch_aws_elb"))
acc.AssertContainsTaggedFields(t, "cloudwatch_aws_elb", fields, tags)
}

func TestMultiAccountGather(t *testing.T) {
duration, _ := time.ParseDuration("1m")
internalDuration := config.Duration(duration)
Expand Down
15 changes: 11 additions & 4 deletions plugins/inputs/cloudwatch/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
# role_session_name = ""
# profile = ""
# shared_credential_file = ""
## If you are using CloudWatch cross-account observability, you can
## set IncludeLinkedAccounts to true in a monitoring account

## If you are using CloudWatch cross-account observability, you can
## set IncludeLinkedAccounts to true in a monitoring account
## and collect metrics from the linked source accounts
# include_linked_accounts = false

## Endpoint to make request against, the correct endpoint is automatically
## determined and this option should only be set if you wish to override the
## default.
Expand Down Expand Up @@ -73,6 +73,13 @@
## Metric Statistic Namespaces (required)
namespaces = ["AWS/ELB"]

## Metric Format
## This determines the format of the produces metrics. 'sparse', the default
## will produce a unique field for each statistic. 'dense' will report all
## statistics will be in a field called value and have a metric_name tag
## defining the name of the statistic. See the plugin README for examples.
# metric_format = "sparse"

## Maximum requests per second. Note that the global default AWS rate limit
## is 50 reqs/sec, so if you define multiple namespaces, these should add up
## to a maximum of 50.
Expand Down