1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,255 @@
# Amazon Elastic Container Service Input Plugin
This plugin gathers statistics on running containers in a Task from the
[Amazon Elastic Container Service][ecs] using the [Amazon ECS metadata][metadata]
and the [v2][v2_endpoint] or [v3][v3_endpoint] statistics API endpoints.
> [!IMPORTANT]
> The telegraf container must be run in the same Task as the workload it is
> inspecting.
The amazon-ecs-agent (though it _is_ a container running on the host) is not
present in the metadata/stats endpoints.
⭐ Telegraf v1.11.0
🏷️ cloud
💻 all
[ecs]: https://aws.amazon.com/ecs/
[metadata]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint.html
[v2_endpoint]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v2.html
[v3_endpoint]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Read metrics about ECS containers
[[inputs.ecs]]
## ECS metadata url.
## Metadata v2 API is used if set explicitly. Otherwise,
## v3 metadata endpoint API is used if available.
# endpoint_url = ""
## Containers to include and exclude. Globs accepted.
## Note that an empty array for both will include all containers
# container_name_include = []
# container_name_exclude = []
## Container states to include and exclude. Globs accepted.
## When empty only containers in the "RUNNING" state will be captured.
## Possible values are "NONE", "PULLED", "CREATED", "RUNNING",
## "RESOURCES_PROVISIONED", "STOPPED".
# container_status_include = []
# container_status_exclude = []
## ecs labels to include and exclude as tags. Globs accepted.
## Note that an empty array for both will include all labels as tags
ecs_label_include = [ "com.amazonaws.ecs.*" ]
ecs_label_exclude = []
## Timeout for queries.
# timeout = "5s"
```
## Configuration (enforce v2 metadata)
```toml
# Read metrics about ECS containers
[[inputs.ecs]]
## ECS metadata url.
## Metadata v2 API is used if set explicitly. Otherwise,
## v3 metadata endpoint API is used if available.
endpoint_url = "http://169.254.170.2"
## Containers to include and exclude. Globs accepted.
## Note that an empty array for both will include all containers
# container_name_include = []
# container_name_exclude = []
## Container states to include and exclude. Globs accepted.
## When empty only containers in the "RUNNING" state will be captured.
## Possible values are "NONE", "PULLED", "CREATED", "RUNNING",
## "RESOURCES_PROVISIONED", "STOPPED".
# container_status_include = []
# container_status_exclude = []
## ecs labels to include and exclude as tags. Globs accepted.
## Note that an empty array for both will include all labels as tags
ecs_label_include = [ "com.amazonaws.ecs.*" ]
ecs_label_exclude = []
## Timeout for queries.
# timeout = "5s"
```
## Metrics
- ecs_task
- tags:
- cluster
- task_arn
- family
- revision
- id
- name
- fields:
- desired_status (string)
- known_status (string)
- limit_cpu (float)
- limit_mem (float)
- ecs_container_mem
- tags:
- cluster
- task_arn
- family
- revision
- id
- name
- fields:
- container_id
- active_anon
- active_file
- cache
- hierarchical_memory_limit
- inactive_anon
- inactive_file
- mapped_file
- pgfault
- pgmajfault
- pgpgin
- pgpgout
- rss
- rss_huge
- total_active_anon
- total_active_file
- total_cache
- total_inactive_anon
- total_inactive_file
- total_mapped_file
- total_pgfault
- total_pgmajfault
- total_pgpgin
- total_pgpgout
- total_rss
- total_rss_huge
- total_unevictable
- total_writeback
- unevictable
- writeback
- fail_count
- limit
- max_usage
- usage
- usage_percent
- ecs_container_cpu
- tags:
- cluster
- task_arn
- family
- revision
- id
- name
- cpu
- fields:
- container_id
- usage_total
- usage_in_usermode
- usage_in_kernelmode
- usage_system
- throttling_periods
- throttling_throttled_periods
- throttling_throttled_time
- usage_percent
- usage_total
- ecs_container_net
- tags:
- cluster
- task_arn
- family
- revision
- id
- name
- network
- fields:
- container_id
- rx_packets
- rx_dropped
- rx_bytes
- rx_errors
- tx_packets
- tx_dropped
- tx_bytes
- tx_errors
- ecs_container_blkio
- tags:
- cluster
- task_arn
- family
- revision
- id
- name
- device
- fields:
- container_id
- io_service_bytes_recursive_async
- io_service_bytes_recursive_read
- io_service_bytes_recursive_sync
- io_service_bytes_recursive_total
- io_service_bytes_recursive_write
- io_serviced_recursive_async
- io_serviced_recursive_read
- io_serviced_recursive_sync
- io_serviced_recursive_total
- io_serviced_recursive_write
- ecs_container_meta
- tags:
- cluster
- task_arn
- family
- revision
- id
- name
- fields:
- container_id
- docker_name
- image
- image_id
- desired_status
- known_status
- limit_cpu
- limit_mem
- created_at
- started_at
- type
## Example Output
```text
ecs_task,cluster=test,family=nginx,host=c4b301d4a123,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a desired_status="RUNNING",known_status="RUNNING",limit_cpu=0.5,limit_mem=512 1542641488000000000
ecs_container_mem,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a active_anon=40960i,active_file=8192i,cache=790528i,pgpgin=1243i,total_pgfault=1298i,total_rss=40960i,limit=1033658368i,max_usage=4825088i,hierarchical_memory_limit=536870912i,rss=40960i,total_active_file=8192i,total_mapped_file=618496i,usage_percent=0.05349543109392212,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",pgfault=1298i,pgmajfault=6i,pgpgout=1040i,total_active_anon=40960i,total_inactive_file=782336i,total_pgpgin=1243i,usage=552960i,inactive_file=782336i,mapped_file=618496i,total_cache=790528i,total_pgpgout=1040i 1542642001000000000
ecs_container_cpu,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,cpu=cpu-total,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a usage_in_kernelmode=0i,throttling_throttled_periods=0i,throttling_periods=0i,throttling_throttled_time=0i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",usage_percent=0,usage_total=26426156i,usage_in_usermode=20000000i,usage_system=2336100000000i 1542642001000000000
ecs_container_cpu,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,cpu=cpu0,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",usage_total=26426156i 1542642001000000000
ecs_container_net,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,network=eth0,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a rx_errors=0i,rx_packets=36i,tx_errors=0i,tx_bytes=648i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",rx_dropped=0i,rx_bytes=5338i,tx_packets=8i,tx_dropped=0i 1542642001000000000
ecs_container_net,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,network=eth5,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a rx_errors=0i,tx_packets=9i,rx_packets=26i,tx_errors=0i,rx_bytes=4641i,tx_dropped=0i,tx_bytes=690i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",rx_dropped=0i 1542642001000000000
ecs_container_net,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,network=total,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a rx_dropped=0i,rx_bytes=9979i,rx_errors=0i,rx_packets=62i,tx_bytes=1338i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",tx_packets=17i,tx_dropped=0i,tx_errors=0i 1542642001000000000
ecs_container_blkio,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,device=253:1,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a io_service_bytes_recursive_sync=790528i,io_service_bytes_recursive_total=790528i,io_serviced_recursive_sync=10i,io_serviced_recursive_write=0i,io_serviced_recursive_async=0i,io_serviced_recursive_total=10i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",io_service_bytes_recursive_read=790528i,io_service_bytes_recursive_write=0i,io_service_bytes_recursive_async=0i,io_serviced_recursive_read=10i 1542642001000000000
ecs_container_blkio,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,device=253:2,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a io_service_bytes_recursive_sync=790528i,io_service_bytes_recursive_total=790528i,io_serviced_recursive_async=0i,io_serviced_recursive_total=10i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",io_service_bytes_recursive_read=790528i,io_service_bytes_recursive_write=0i,io_service_bytes_recursive_async=0i,io_serviced_recursive_read=10i,io_serviced_recursive_write=0i,io_serviced_recursive_sync=10i 1542642001000000000
ecs_container_blkio,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,device=253:4,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a io_service_bytes_recursive_write=0i,io_service_bytes_recursive_sync=790528i,io_service_bytes_recursive_async=0i,io_service_bytes_recursive_total=790528i,io_serviced_recursive_async=0i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",io_service_bytes_recursive_read=790528i,io_serviced_recursive_read=10i,io_serviced_recursive_write=0i,io_serviced_recursive_sync=10i,io_serviced_recursive_total=10i 1542642001000000000
ecs_container_blkio,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,device=202:26368,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a io_serviced_recursive_read=10i,io_serviced_recursive_write=0i,io_serviced_recursive_sync=10i,io_serviced_recursive_async=0i,io_serviced_recursive_total=10i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",io_service_bytes_recursive_sync=790528i,io_service_bytes_recursive_total=790528i,io_service_bytes_recursive_async=0i,io_service_bytes_recursive_read=790528i,io_service_bytes_recursive_write=0i 1542642001000000000
ecs_container_blkio,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,device=total,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a io_serviced_recursive_async=0i,io_serviced_recursive_read=40i,io_serviced_recursive_sync=40i,io_serviced_recursive_write=0i,io_serviced_recursive_total=40i,io_service_bytes_recursive_read=3162112i,io_service_bytes_recursive_write=0i,io_service_bytes_recursive_async=0i,container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",io_service_bytes_recursive_sync=3162112i,io_service_bytes_recursive_total=3162112i 1542642001000000000
ecs_container_meta,cluster=test,com.amazonaws.ecs.cluster=test,com.amazonaws.ecs.container-name=~internal~ecs~pause,com.amazonaws.ecs.task-arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a,com.amazonaws.ecs.task-definition-family=nginx,com.amazonaws.ecs.task-definition-version=2,family=nginx,host=c4b301d4a123,id=e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba,name=~internal~ecs~pause,revision=2,task_arn=arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a limit_mem=0,type="CNI_PAUSE",container_id="e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",docker_name="ecs-nginx-2-internalecspause",limit_cpu=0,known_status="RESOURCES_PROVISIONED",image="amazon/amazon-ecs-pause:0.1.0",image_id="",desired_status="RESOURCES_PROVISIONED" 1542642001000000000
```

View file

@ -0,0 +1,61 @@
package ecs
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil"
)
const cgroupID = "c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55"
func TestParseCgroupV2Stats(t *testing.T) {
parser := &influx.Parser{}
require.NoError(t, parser.Init())
expected, err := testutil.ParseMetricsFromFile("testdata/cgroupv2/stats.out", parser)
require.NoError(t, err)
stats, err := os.Open("testdata/cgroupv2/stats.json")
require.NoError(t, err)
parsedStats, err := unmarshalStats(stats)
require.NoError(t, err)
tags := map[string]string{
"test_tag": "test",
}
var acc testutil.Accumulator
memstats(cgroupID, parsedStats[cgroupID], &acc, tags, time.Now())
cpustats(cgroupID, parsedStats[cgroupID], &acc, tags, time.Now())
netstats(cgroupID, parsedStats[cgroupID], &acc, tags, time.Now())
blkstats(cgroupID, parsedStats[cgroupID], &acc, tags, time.Now())
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
}
func TestParseCgroupV2Meta(t *testing.T) {
parser := &influx.Parser{}
require.NoError(t, parser.Init())
expected, err := testutil.ParseMetricsFromFile("testdata/cgroupv2/meta.out", parser)
require.NoError(t, err)
meta, err := os.Open("testdata/cgroupv2/meta.json")
require.NoError(t, err)
validMeta, err := unmarshalTask(meta)
require.NoError(t, err)
tags := map[string]string{
"test_tag": "test",
}
var acc testutil.Accumulator
metastats(cgroupID, &validMeta.Containers[0], &acc, tags, time.Now())
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
}

View file

@ -0,0 +1,173 @@
package ecs
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/docker/docker/api/types/container"
)
var (
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v2.html
ecsMetadataPathV2 = "/v2/metadata"
ecsMetaStatsPathV2 = "/v2/stats"
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
ecsMetadataPath = "/task"
ecsMetaStatsPath = "/task/stats"
)
// client is the ECS client contract
type client interface {
task() (*ecsTask, error)
containerStats() (map[string]*container.StatsResponse, error)
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// newClient constructs an ECS client with the passed configuration params
func newClient(timeout time.Duration, endpoint string, version int) (*ecsClient, error) {
if version < 2 || version > 4 {
const msg = "expected metadata version 2, 3 or 4, got %d"
return nil, fmt.Errorf(msg, version)
}
baseURL, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
c := &http.Client{
Timeout: timeout,
}
return &ecsClient{
client: c,
baseURL: baseURL,
taskURL: resolveTaskURL(baseURL, version),
statsURL: resolveStatsURL(baseURL, version),
version: version,
}, nil
}
func resolveTaskURL(base *url.URL, version int) string {
var path string
switch version {
case 2:
path = ecsMetadataPathV2
case 3:
path = ecsMetadataPath
case 4:
path = ecsMetadataPath
default:
const msg = "resolveTaskURL: unexpected version %d"
panic(fmt.Errorf(msg, version))
}
return resolveURL(base, path)
}
func resolveStatsURL(base *url.URL, version int) string {
var path string
switch version {
case 2:
path = ecsMetaStatsPathV2
case 3:
path = ecsMetaStatsPath
case 4:
path = ecsMetaStatsPath
default:
// Should never happen.
const msg = "resolveStatsURL: unexpected version %d"
panic(fmt.Errorf(msg, version))
}
return resolveURL(base, path)
}
// resolveURL returns a URL string by concatenating the string representation of base
// and path. This is consistent with AWS metadata documentation:
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html#task-metadata-endpoint-v3-paths
func resolveURL(base *url.URL, path string) string {
return base.String() + path
}
// ecsClient contains ECS connection config
type ecsClient struct {
client httpClient
version int
baseURL *url.URL
taskURL string
statsURL string
}
// task calls the ECS metadata endpoint and returns a populated task
func (c *ecsClient) task() (*ecsTask, error) {
req, err := http.NewRequest("GET", c.taskURL, nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//nolint:errcheck // LimitReader returns io.EOF and we're not interested in read errors.
body, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
return nil, fmt.Errorf("%s returned HTTP status %s: %q", c.taskURL, resp.Status, body)
}
task, err := unmarshalTask(resp.Body)
if err != nil {
return nil, err
}
return task, nil
}
// containerStats calls the ECS stats endpoint and returns a populated container stats map
func (c *ecsClient) containerStats() (map[string]*container.StatsResponse, error) {
req, err := http.NewRequest("GET", c.statsURL, nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//nolint:errcheck // LimitReader returns io.EOF and we're not interested in read errors.
body, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
return nil, fmt.Errorf("%s returned HTTP status %s: %q", c.statsURL, resp.Status, body)
}
return unmarshalStats(resp.Body)
}
// pollSync executes task and containerStats in parallel.
// If both succeed, both structs are returned.
// If either errors, a single error is returned.
func pollSync(c client) (*ecsTask, map[string]*container.StatsResponse, error) {
var task *ecsTask
var stats map[string]*container.StatsResponse
var err error
if stats, err = c.containerStats(); err != nil {
return nil, nil, err
}
if task, err = c.task(); err != nil {
return nil, nil, err
}
return task, stats, nil
}

View file

@ -0,0 +1,320 @@
package ecs
import (
"bytes"
"errors"
"io"
"net/http"
"net/url"
"os"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/require"
)
type pollMock struct {
getTask func() (*ecsTask, error)
getStats func() (map[string]*container.StatsResponse, error)
}
func (p *pollMock) task() (*ecsTask, error) {
return p.getTask()
}
func (p *pollMock) containerStats() (map[string]*container.StatsResponse, error) {
return p.getStats()
}
func TestEcsClient_PollSync(t *testing.T) {
tests := []struct {
name string
mock *pollMock
want *ecsTask
want1 map[string]*container.StatsResponse
wantErr bool
}{
{
name: "success",
mock: &pollMock{
getTask: func() (*ecsTask, error) {
return &validMeta, nil
},
getStats: func() (map[string]*container.StatsResponse, error) {
return validStats, nil
},
},
want: &validMeta,
want1: validStats,
},
{
name: "task err",
mock: &pollMock{
getTask: func() (*ecsTask, error) {
return nil, errors.New("err")
},
getStats: func() (map[string]*container.StatsResponse, error) {
return validStats, nil
},
},
wantErr: true,
},
{
name: "stats err",
mock: &pollMock{
getTask: func() (*ecsTask, error) {
return &validMeta, nil
},
getStats: func() (map[string]*container.StatsResponse, error) {
return nil, errors.New("err")
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := pollSync(tt.mock)
if (err != nil) != tt.wantErr {
t.Errorf("ecsClient.pollSync() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.Equal(t, tt.want, got, "ecsClient.pollSync() got = %v, want %v", got, tt.want)
require.Equal(t, tt.want1, got1, "ecsClient.pollSync() got1 = %v, want %v", got1, tt.want1)
})
}
}
type mockDo struct {
do func() (*http.Response, error)
}
func (m mockDo) Do(*http.Request) (*http.Response, error) {
return m.do()
}
func TestEcsClient_Task(t *testing.T) {
tests := []struct {
name string
client httpClient
want *ecsTask
wantErr bool
}{
{
name: "happy",
client: mockDo{
do: func() (*http.Response, error) {
rc, err := os.Open("testdata/metadata.golden")
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(rc),
}, nil
},
},
want: &validMeta,
},
{
name: "do err",
client: mockDo{
do: func() (*http.Response, error) {
return nil, errors.New("err")
},
},
wantErr: true,
},
{
name: "malformed 500 resp",
client: mockDo{
do: func() (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(bytes.NewReader([]byte("foo"))),
}, nil
},
},
wantErr: true,
},
{
name: "malformed 200 resp",
client: mockDo{
do: func() (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte("foo"))),
}, nil
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &ecsClient{
client: tt.client,
taskURL: "abc",
}
got, err := c.task()
if (err != nil) != tt.wantErr {
t.Errorf("ecsClient.task() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.Equal(t, tt.want, got, "ecsClient.task() = %v, want %v", got, tt.want)
})
}
}
func TestEcsClient_ContainerStats(t *testing.T) {
tests := []struct {
name string
client httpClient
want map[string]*container.StatsResponse
wantErr bool
}{
{
name: "happy",
client: mockDo{
do: func() (*http.Response, error) {
rc, err := os.Open("testdata/stats.golden")
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(rc),
}, nil
},
},
want: validStats,
},
{
name: "do err",
client: mockDo{
do: func() (*http.Response, error) {
return nil, errors.New("err")
},
},
want: nil,
wantErr: true,
},
{
name: "malformed 200 resp",
client: mockDo{
do: func() (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte("foo"))),
}, nil
},
},
want: nil,
wantErr: true,
},
{
name: "malformed 500 resp",
client: mockDo{
do: func() (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(bytes.NewReader([]byte("foo"))),
}, nil
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &ecsClient{
client: tt.client,
statsURL: "abc",
}
got, err := c.containerStats()
if (err != nil) != tt.wantErr {
t.Errorf("ecsClient.containerStats() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.Equal(t, tt.want, got, "ecsClient.containerStats() = %v, want %v", got, tt.want)
})
}
}
func TestResolveTaskURL(t *testing.T) {
tests := []struct {
name string
base string
ver int
exp string
}{
{
name: "default v2 endpoint",
base: v2Endpoint,
ver: 2,
exp: "http://169.254.170.2/v2/metadata",
},
{
name: "custom v2 endpoint",
base: "http://192.168.0.1",
ver: 2,
exp: "http://192.168.0.1/v2/metadata",
},
{
name: "theoretical v3 endpoint",
base: "http://169.254.170.2/v3/metadata",
ver: 3,
exp: "http://169.254.170.2/v3/metadata/task",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
baseURL, err := url.Parse(tt.base)
require.NoError(t, err)
act := resolveTaskURL(baseURL, tt.ver)
require.Equal(t, tt.exp, act)
})
}
}
func TestResolveStatsURL(t *testing.T) {
tests := []struct {
name string
base string
ver int
exp string
}{
{
name: "default v2 endpoint",
base: v2Endpoint,
ver: 2,
exp: "http://169.254.170.2/v2/stats",
},
{
name: "custom v2 endpoint",
base: "http://192.168.0.1",
ver: 2,
exp: "http://192.168.0.1/v2/stats",
},
{
name: "theoretical v3 endpoint",
base: "http://169.254.170.2/v3/metadata",
ver: 3,
exp: "http://169.254.170.2/v3/metadata/task/stats",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
baseURL, err := url.Parse(tt.base)
require.NoError(t, err)
act := resolveStatsURL(baseURL, tt.ver)
require.Equal(t, tt.exp, act)
})
}
}

244
plugins/inputs/ecs/ecs.go Normal file
View file

@ -0,0 +1,244 @@
//go:generate ../../../tools/readme_config_includer/generator
package ecs
import (
_ "embed"
"os"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
const (
v2Endpoint = "http://169.254.170.2"
)
type Ecs struct {
EndpointURL string `toml:"endpoint_url"`
Timeout config.Duration `toml:"timeout"`
ContainerNameInclude []string `toml:"container_name_include"`
ContainerNameExclude []string `toml:"container_name_exclude"`
ContainerStatusInclude []string `toml:"container_status_include"`
ContainerStatusExclude []string `toml:"container_status_exclude"`
LabelInclude []string `toml:"ecs_label_include"`
LabelExclude []string `toml:"ecs_label_exclude"`
newClient func(timeout time.Duration, endpoint string, version int) (*ecsClient, error)
client client
filtersCreated bool
labelFilter filter.Filter
containerNameFilter filter.Filter
statusFilter filter.Filter
metadataVersion int
}
func (*Ecs) SampleConfig() string {
return sampleConfig
}
func (ecs *Ecs) Gather(acc telegraf.Accumulator) error {
err := initSetup(ecs)
if err != nil {
return err
}
task, stats, err := pollSync(ecs.client)
if err != nil {
return err
}
mergeTaskStats(task, stats)
taskTags := map[string]string{
"cluster": task.Cluster,
"task_arn": task.TaskARN,
"family": task.Family,
"revision": task.Revision,
}
// accumulate metrics
accTask(task, taskTags, acc)
ecs.accContainers(task, taskTags, acc)
return nil
}
func initSetup(ecs *Ecs) error {
if ecs.client == nil {
resolveEndpoint(ecs)
c, err := ecs.newClient(time.Duration(ecs.Timeout), ecs.EndpointURL, ecs.metadataVersion)
if err != nil {
return err
}
ecs.client = c
}
// Create filters
if !ecs.filtersCreated {
err := ecs.createContainerNameFilters()
if err != nil {
return err
}
err = ecs.createContainerStatusFilters()
if err != nil {
return err
}
err = ecs.createLabelFilters()
if err != nil {
return err
}
ecs.filtersCreated = true
}
return nil
}
func resolveEndpoint(ecs *Ecs) {
if ecs.EndpointURL != "" {
// Use metadata v2 API since endpoint is set explicitly.
ecs.metadataVersion = 2
return
}
// Auto-detect metadata endpoint version.
// Use metadata v4 if available.
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
v4Endpoint := os.Getenv("ECS_CONTAINER_METADATA_URI_V4")
if v4Endpoint != "" {
ecs.EndpointURL = v4Endpoint
ecs.metadataVersion = 4
return
}
// Use metadata v3 if available.
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html
v3Endpoint := os.Getenv("ECS_CONTAINER_METADATA_URI")
if v3Endpoint != "" {
ecs.EndpointURL = v3Endpoint
ecs.metadataVersion = 3
return
}
// Use v2 endpoint if nothing else is available.
ecs.EndpointURL = v2Endpoint
ecs.metadataVersion = 2
}
func accTask(task *ecsTask, tags map[string]string, acc telegraf.Accumulator) {
taskFields := map[string]interface{}{
"desired_status": task.DesiredStatus,
"known_status": task.KnownStatus,
"limit_cpu": task.Limits["CPU"],
"limit_mem": task.Limits["Memory"],
}
acc.AddFields("ecs_task", taskFields, tags)
}
func (ecs *Ecs) accContainers(task *ecsTask, taskTags map[string]string, acc telegraf.Accumulator) {
for i := range task.Containers {
c := &task.Containers[i]
if !ecs.containerNameFilter.Match(c.Name) {
continue
}
if !ecs.statusFilter.Match(strings.ToUpper(c.KnownStatus)) {
continue
}
// add matching ECS container Labels
containerTags := map[string]string{
"id": c.ID,
"name": c.Name,
}
for k, v := range c.Labels {
if ecs.labelFilter.Match(k) {
containerTags[k] = v
}
}
tags := mergeTags(taskTags, containerTags)
parseContainerStats(c, acc, tags)
}
}
// returns a new map with the same content values as the input map
func copyTags(in map[string]string) map[string]string {
out := make(map[string]string)
for k, v := range in {
out[k] = v
}
return out
}
// returns a new map with the merged content values of the two input maps
func mergeTags(a, b map[string]string) map[string]string {
c := copyTags(a)
for k, v := range b {
c[k] = v
}
return c
}
func (ecs *Ecs) createContainerNameFilters() error {
containerNameFilter, err := filter.NewIncludeExcludeFilter(ecs.ContainerNameInclude, ecs.ContainerNameExclude)
if err != nil {
return err
}
ecs.containerNameFilter = containerNameFilter
return nil
}
func (ecs *Ecs) createLabelFilters() error {
labelFilter, err := filter.NewIncludeExcludeFilter(ecs.LabelInclude, ecs.LabelExclude)
if err != nil {
return err
}
ecs.labelFilter = labelFilter
return nil
}
func (ecs *Ecs) createContainerStatusFilters() error {
if len(ecs.ContainerStatusInclude) == 0 && len(ecs.ContainerStatusExclude) == 0 {
ecs.ContainerStatusInclude = []string{"RUNNING"}
}
// ECS uses uppercase status names, normalizing for comparison.
for i, include := range ecs.ContainerStatusInclude {
ecs.ContainerStatusInclude[i] = strings.ToUpper(include)
}
for i, exclude := range ecs.ContainerStatusExclude {
ecs.ContainerStatusExclude[i] = strings.ToUpper(exclude)
}
statusFilter, err := filter.NewIncludeExcludeFilter(ecs.ContainerStatusInclude, ecs.ContainerStatusExclude)
if err != nil {
return err
}
ecs.statusFilter = statusFilter
return nil
}
func init() {
inputs.Add("ecs", func() telegraf.Input {
return &Ecs{
EndpointURL: "",
Timeout: config.Duration(5 * time.Second),
newClient: newClient,
filtersCreated: false,
}
})
}

View file

@ -0,0 +1,840 @@
package ecs
import (
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/require"
)
// codified golden objects for tests
// stats
const pauseStatsKey = "e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba"
const nginxStatsKey = "fffe894e232d46c76475cfeabf4907f712e8b92618a37fca3ef0805bbbfb0299"
var pauseStatsRead = mustParseNano("2018-11-19T15:40:00.936081344Z")
var pauseStatsPreRead = mustParseNano("2018-11-19T15:39:59.933000984Z")
var nginxStatsRead = mustParseNano("2018-11-19T15:40:00.93733207Z")
var nginxStatsPreRead = mustParseNano("2018-11-19T15:39:59.934291009Z")
func mustParseNano(value string) time.Time {
t, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
panic(err)
}
return t
}
var validStats = map[string]*container.StatsResponse{
pauseStatsKey: {
Read: pauseStatsRead,
PreRead: pauseStatsPreRead,
BlkioStats: container.BlkioStats{
IoServiceBytesRecursive: []container.BlkioStatEntry{
{
Major: 202,
Minor: 26368,
Op: "Read",
Value: 790528,
},
{
Major: 202,
Minor: 26368,
Op: "Write",
},
{
Major: 202,
Minor: 26368,
Op: "Sync",
Value: 790528,
},
{
Major: 202,
Minor: 26368,
Op: "Async",
},
{
Major: 202,
Minor: 26368,
Op: "Total",
Value: 790528,
},
{
Major: 253,
Minor: 1,
Op: "Read",
Value: 790528,
},
{
Major: 253,
Minor: 1,
Op: "Write",
},
{
Major: 253,
Minor: 1,
Op: "Sync",
Value: 790528,
},
{
Major: 253,
Minor: 1,
Op: "Async",
},
{
Major: 253,
Minor: 1,
Op: "Total",
Value: 790528,
},
{
Major: 253,
Minor: 2,
Op: "Read",
Value: 790528,
},
{
Major: 253,
Minor: 2,
Op: "Write",
},
{
Major: 253,
Minor: 2,
Op: "Sync",
Value: 790528,
},
{
Major: 253,
Minor: 2,
Op: "Async",
},
{
Major: 253,
Minor: 2,
Op: "Total",
Value: 790528,
},
{
Major: 253,
Minor: 4,
Op: "Read",
Value: 790528,
},
{
Major: 253,
Minor: 4,
Op: "Write",
},
{
Major: 253,
Minor: 4,
Op: "Sync",
Value: 790528,
},
{
Major: 253,
Minor: 4,
Op: "Async",
},
{
Major: 253,
Minor: 4,
Op: "Total",
Value: 790528,
},
},
IoServicedRecursive: []container.BlkioStatEntry{
{
Major: 202,
Minor: 26368,
Op: "Read",
Value: 10,
},
{
Major: 202,
Minor: 26368,
Op: "Write",
},
{
Major: 202,
Minor: 26368,
Op: "Sync",
Value: 10,
},
{
Major: 202,
Minor: 26368,
Op: "Async",
},
{
Major: 202,
Minor: 26368,
Op: "Total",
Value: 10,
},
{
Major: 253,
Minor: 1,
Op: "Read",
Value: 10,
},
{
Major: 253,
Minor: 1,
Op: "Write",
},
{
Major: 253,
Minor: 1,
Op: "Sync",
Value: 10,
},
{
Major: 253,
Minor: 1,
Op: "Async",
},
{
Major: 253,
Minor: 1,
Op: "Total",
Value: 10,
},
{
Major: 253,
Minor: 2,
Op: "Read",
Value: 10,
},
{
Major: 253,
Minor: 2,
Op: "Write",
},
{
Major: 253,
Minor: 2,
Op: "Sync",
Value: 10,
},
{
Major: 253,
Minor: 2,
Op: "Async",
},
{
Major: 253,
Minor: 2,
Op: "Total",
Value: 10,
},
{
Major: 253,
Minor: 4,
Op: "Read",
Value: 10,
},
{
Major: 253,
Minor: 4,
Op: "Write",
},
{
Major: 253,
Minor: 4,
Op: "Sync",
Value: 10,
},
{
Major: 253,
Minor: 4,
Op: "Async",
},
{
Major: 253,
Minor: 4,
Op: "Total",
Value: 10,
},
},
},
CPUStats: container.CPUStats{
CPUUsage: container.CPUUsage{
PercpuUsage: []uint64{
26426156,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
},
UsageInUsermode: 20000000,
TotalUsage: 26426156,
},
SystemUsage: 2336100000000,
OnlineCPUs: 1,
ThrottlingData: container.ThrottlingData{},
},
PreCPUStats: container.CPUStats{
CPUUsage: container.CPUUsage{
PercpuUsage: []uint64{
26426156,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
},
UsageInUsermode: 20000000,
TotalUsage: 26426156,
},
SystemUsage: 2335090000000,
OnlineCPUs: 1,
ThrottlingData: container.ThrottlingData{},
},
MemoryStats: container.MemoryStats{
Stats: map[string]uint64{
"cache": 790528,
"mapped_file": 618496,
"total_inactive_file": 782336,
"pgpgout": 1040,
"rss": 40960,
"total_mapped_file": 618496,
"pgpgin": 1243,
"pgmajfault": 6,
"total_rss": 40960,
"hierarchical_memory_limit": 536870912,
"total_pgfault": 1298,
"total_active_file": 8192,
"active_anon": 40960,
"total_active_anon": 40960,
"total_pgpgout": 1040,
"total_cache": 790528,
"active_file": 8192,
"pgfault": 1298,
"inactive_file": 782336,
"total_pgpgin": 1243,
"hierarchical_memsw_limit": 9223372036854772000,
},
MaxUsage: 4825088,
Usage: 1343488,
Limit: 1033658368,
},
Networks: map[string]container.NetworkStats{
"eth0": {
RxBytes: uint64(5338),
RxDropped: uint64(0),
RxErrors: uint64(0),
RxPackets: uint64(36),
TxBytes: uint64(648),
TxDropped: uint64(0),
TxErrors: uint64(0),
TxPackets: uint64(8),
},
"eth5": {
RxBytes: uint64(4641),
RxDropped: uint64(0),
RxErrors: uint64(0),
RxPackets: uint64(26),
TxBytes: uint64(690),
TxDropped: uint64(0),
TxErrors: uint64(0),
TxPackets: uint64(9),
},
},
},
nginxStatsKey: {
Read: nginxStatsRead,
PreRead: nginxStatsPreRead,
BlkioStats: container.BlkioStats{
IoServiceBytesRecursive: []container.BlkioStatEntry{
{
Major: 202,
Minor: 26368,
Op: "Read",
Value: 5730304,
},
{
Major: 202,
Minor: 26368,
Op: "Write",
},
{
Major: 202,
Minor: 26368,
Op: "Sync",
Value: 5730304,
},
{
Major: 202,
Minor: 26368,
Op: "Async",
},
{
Major: 202,
Minor: 26368,
Op: "Total",
Value: 5730304,
},
{
Major: 253,
Minor: 1,
Op: "Read",
Value: 5730304,
},
{
Major: 253,
Minor: 1,
Op: "Write",
},
{
Major: 253,
Minor: 1,
Op: "Sync",
Value: 5730304,
},
{
Major: 253,
Minor: 1,
Op: "Async",
},
{
Major: 253,
Minor: 1,
Op: "Total",
Value: 5730304,
},
{
Major: 253,
Minor: 2,
Op: "Read",
Value: 5730304,
},
{
Major: 253,
Minor: 2,
Op: "Write",
},
{
Major: 253,
Minor: 2,
Op: "Sync",
Value: 5730304,
},
{
Major: 253,
Minor: 2,
Op: "Async",
},
{
Major: 253,
Minor: 2,
Op: "Total",
Value: 5730304,
},
{
Major: 253,
Minor: 5,
Op: "Read",
Value: 5730304,
},
{
Major: 253,
Minor: 5,
Op: "Write",
},
{
Major: 253,
Minor: 5,
Op: "Sync",
Value: 5730304,
},
{
Major: 253,
Minor: 5,
Op: "Async",
},
{
Major: 253,
Minor: 5,
Op: "Total",
Value: 5730304,
},
},
IoServicedRecursive: []container.BlkioStatEntry{
{
Major: 202,
Minor: 26368,
Op: "Read",
Value: 156,
},
{
Major: 202,
Minor: 26368,
Op: "Write",
},
{
Major: 202,
Minor: 26368,
Op: "Sync",
Value: 156,
},
{
Major: 202,
Minor: 26368,
Op: "Async",
},
{
Major: 202,
Minor: 26368,
Op: "Total",
Value: 156,
},
{
Major: 253,
Minor: 1,
Op: "Read",
Value: 156,
},
{
Major: 253,
Minor: 1,
Op: "Write",
},
{
Major: 253,
Minor: 1,
Op: "Sync",
Value: 156,
},
{
Major: 253,
Minor: 1,
Op: "Async",
},
{
Major: 253,
Minor: 1,
Op: "Total",
Value: 156,
},
{
Major: 253,
Minor: 2,
Op: "Read",
Value: 156,
},
{
Major: 253,
Minor: 2,
Op: "Write",
},
{
Major: 253,
Minor: 2,
Op: "Sync",
Value: 156,
},
{
Major: 253,
Minor: 2,
Op: "Async",
},
{
Major: 253,
Minor: 2,
Op: "Total",
Value: 156,
},
{
Major: 253,
Minor: 5,
Op: "Read",
Value: 147,
},
{
Major: 253,
Minor: 5,
Op: "Write",
},
{
Major: 253,
Minor: 5,
Op: "Sync",
Value: 147,
},
{
Major: 253,
Minor: 5,
Op: "Async",
},
{
Major: 253,
Minor: 5,
Op: "Total",
Value: 147,
},
},
},
CPUStats: container.CPUStats{
CPUUsage: container.CPUUsage{
PercpuUsage: []uint64{
65599511,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
},
UsageInUsermode: 40000000,
TotalUsage: 65599511,
UsageInKernelmode: 10000000,
},
SystemUsage: 2336100000000,
OnlineCPUs: 1,
ThrottlingData: container.ThrottlingData{},
},
PreCPUStats: container.CPUStats{
CPUUsage: container.CPUUsage{
PercpuUsage: []uint64{
65599511,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
},
UsageInUsermode: 40000000,
TotalUsage: 65599511,
UsageInKernelmode: 10000000,
},
SystemUsage: 2335090000000,
OnlineCPUs: 1,
ThrottlingData: container.ThrottlingData{},
},
MemoryStats: container.MemoryStats{
Stats: map[string]uint64{
"cache": 5787648,
"mapped_file": 3616768,
"total_inactive_file": 4321280,
"pgpgout": 1674,
"rss": 1597440,
"total_mapped_file": 3616768,
"pgpgin": 3477,
"pgmajfault": 40,
"total_rss": 1597440,
"total_inactive_anon": 4096,
"hierarchical_memory_limit": 536870912,
"total_pgfault": 2924,
"total_active_file": 1462272,
"active_anon": 1597440,
"total_active_anon": 1597440,
"total_pgpgout": 1674,
"total_cache": 5787648,
"inactive_anon": 4096,
"active_file": 1462272,
"pgfault": 2924,
"inactive_file": 4321280,
"total_pgpgin": 3477,
"hierarchical_memsw_limit": 9223372036854772000,
},
MaxUsage: 8667136,
Usage: 8179712,
Limit: 1033658368,
},
},
}
// meta
var metaPauseCreated = mustParseNano("2018-11-19T15:31:26.641964373Z")
var metaPauseStarted = mustParseNano("2018-11-19T15:31:27.035698679Z")
var metaCreated = mustParseNano("2018-11-19T15:31:27.614884084Z")
var metaStarted = mustParseNano("2018-11-19T15:31:27.975996351Z")
var metaPullStart = mustParseNano("2018-11-19T15:31:27.197327103Z")
var metaPullStop = mustParseNano("2018-11-19T15:31:27.609089471Z")
var validMeta = ecsTask{
Cluster: "test",
TaskARN: "arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a",
Family: "nginx",
Revision: "2",
DesiredStatus: "RUNNING",
KnownStatus: "RUNNING",
Containers: []ecsContainer{
{
ID: pauseStatsKey,
Name: "~internal~ecs~pause",
DockerName: "ecs-nginx-2-internalecspause",
Image: "amazon/amazon-ecs-pause:0.1.0",
ImageID: "",
Labels: map[string]string{
"com.amazonaws.ecs.cluster": "test",
"com.amazonaws.ecs.container-name": "~internal~ecs~pause",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a",
"com.amazonaws.ecs.task-definition-family": "nginx",
"com.amazonaws.ecs.task-definition-version": "2",
},
DesiredStatus: "RESOURCES_PROVISIONED",
KnownStatus: "RESOURCES_PROVISIONED",
Limits: map[string]float64{
"CPU": 0,
"Memory": 0,
},
CreatedAt: metaPauseCreated,
StartedAt: metaPauseStarted,
Type: "CNI_PAUSE",
Networks: []network{
{
NetworkMode: "awsvpc",
IPv4Addresses: []string{
"172.31.25.181",
},
},
},
},
{
ID: nginxStatsKey,
Name: "nginx",
DockerName: "ecs-nginx-2-nginx",
Image: "nginx:alpine",
ImageID: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Labels: map[string]string{
"com.amazonaws.ecs.cluster": "test",
"com.amazonaws.ecs.container-name": "nginx",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a",
"com.amazonaws.ecs.task-definition-family": "nginx",
"com.amazonaws.ecs.task-definition-version": "2",
},
DesiredStatus: "RUNNING",
KnownStatus: "RUNNING",
Limits: map[string]float64{
"CPU": 0,
"Memory": 0,
},
CreatedAt: metaCreated,
StartedAt: metaStarted,
Type: "NORMAL",
Networks: []network{
{
NetworkMode: "awsvpc",
IPv4Addresses: []string{
"172.31.25.181",
},
},
},
},
},
Limits: map[string]float64{
"CPU": 0.5,
"Memory": 512,
},
PullStartedAt: metaPullStart,
PullStoppedAt: metaPullStop,
}
func TestResolveEndpoint(t *testing.T) {
tests := []struct {
name string
given Ecs
exp Ecs
setEnv func(*testing.T)
}{
{
name: "Endpoint is explicitly set => use v2 metadata",
given: Ecs{
EndpointURL: "192.162.0.1/custom_endpoint",
},
exp: Ecs{
EndpointURL: "192.162.0.1/custom_endpoint",
metadataVersion: 2,
},
},
{
name: "Endpoint is not set, ECS_CONTAINER_METADATA_URI is not set => use v2 metadata",
given: Ecs{
EndpointURL: "",
},
exp: Ecs{
EndpointURL: v2Endpoint,
metadataVersion: 2,
},
},
{
name: "Endpoint is not set, ECS_CONTAINER_METADATA_URI is set => use v3 metadata",
setEnv: func(t *testing.T) {
t.Setenv("ECS_CONTAINER_METADATA_URI", "v3-endpoint.local")
},
given: Ecs{
EndpointURL: "",
},
exp: Ecs{
EndpointURL: "v3-endpoint.local",
metadataVersion: 3,
},
},
{
name: "Endpoint is not set, ECS_CONTAINER_METADATA_URI_V4 is set => use v4 metadata",
setEnv: func(t *testing.T) {
t.Setenv("ECS_CONTAINER_METADATA_URI_V4", "v4-endpoint.local")
},
given: Ecs{
EndpointURL: "",
},
exp: Ecs{
EndpointURL: "v4-endpoint.local",
metadataVersion: 4,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setEnv != nil {
tt.setEnv(t)
}
act := tt.given
resolveEndpoint(&act)
require.Equal(t, tt.exp, act)
})
}
}

View file

@ -0,0 +1,26 @@
# Read metrics about ECS containers
[[inputs.ecs]]
## ECS metadata url.
## Metadata v2 API is used if set explicitly. Otherwise,
## v3 metadata endpoint API is used if available.
# endpoint_url = ""
## Containers to include and exclude. Globs accepted.
## Note that an empty array for both will include all containers
# container_name_include = []
# container_name_exclude = []
## Container states to include and exclude. Globs accepted.
## When empty only containers in the "RUNNING" state will be captured.
## Possible values are "NONE", "PULLED", "CREATED", "RUNNING",
## "RESOURCES_PROVISIONED", "STOPPED".
# container_status_include = []
# container_status_exclude = []
## ecs labels to include and exclude as tags. Globs accepted.
## Note that an empty array for both will include all labels as tags
ecs_label_include = [ "com.amazonaws.ecs.*" ]
ecs_label_exclude = []
## Timeout for queries.
# timeout = "5s"

295
plugins/inputs/ecs/stats.go Normal file
View file

@ -0,0 +1,295 @@
package ecs
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/common/docker"
)
func parseContainerStats(c *ecsContainer, acc telegraf.Accumulator, tags map[string]string) {
id := c.ID
stats := &c.Stats
tm := stats.Read
if tm.Before(time.Unix(0, 0)) {
tm = time.Now()
}
metastats(id, c, acc, tags, tm)
memstats(id, stats, acc, tags, tm)
cpustats(id, stats, acc, tags, tm)
netstats(id, stats, acc, tags, tm)
blkstats(id, stats, acc, tags, tm)
}
func metastats(id string, c *ecsContainer, acc telegraf.Accumulator, tags map[string]string, tm time.Time) {
metafields := map[string]interface{}{
"container_id": id,
"docker_name": c.DockerName,
"image": c.Image,
"image_id": c.ImageID,
"desired_status": c.DesiredStatus,
"known_status": c.KnownStatus,
"limit_cpu": c.Limits["CPU"],
"limit_mem": c.Limits["Memory"],
"created_at": c.CreatedAt,
"started_at": c.StartedAt,
"type": c.Type,
}
acc.AddFields("ecs_container_meta", metafields, tags, tm)
}
func memstats(id string, stats *container.StatsResponse, acc telegraf.Accumulator, tags map[string]string, tm time.Time) {
memfields := map[string]interface{}{
"container_id": id,
}
memstats := []string{
"active_anon",
"active_file",
"cache",
"hierarchical_memory_limit",
"inactive_anon",
"inactive_file",
"mapped_file",
"pgfault",
"pgmajfault",
"pgpgin",
"pgpgout",
"rss",
"rss_huge",
"total_active_anon",
"total_active_file",
"total_cache",
"total_inactive_anon",
"total_inactive_file",
"total_mapped_file",
"total_pgfault",
"total_pgmajfault",
"total_pgpgin",
"total_pgpgout",
"total_rss",
"total_rss_huge",
"total_unevictable",
"total_writeback",
"unevictable",
"writeback",
}
for _, field := range memstats {
if value, ok := stats.MemoryStats.Stats[field]; ok {
memfields[field] = value
}
}
if stats.MemoryStats.Failcnt != 0 {
memfields["fail_count"] = stats.MemoryStats.Failcnt
}
memfields["limit"] = stats.MemoryStats.Limit
memfields["max_usage"] = stats.MemoryStats.MaxUsage
mem := docker.CalculateMemUsageUnixNoCache(stats.MemoryStats)
memLimit := float64(stats.MemoryStats.Limit)
memfields["usage"] = uint64(mem)
memfields["usage_percent"] = docker.CalculateMemPercentUnixNoCache(memLimit, mem)
acc.AddFields("ecs_container_mem", memfields, tags, tm)
}
func cpustats(id string, stats *container.StatsResponse, acc telegraf.Accumulator, tags map[string]string, tm time.Time) {
cpufields := map[string]interface{}{
"usage_total": stats.CPUStats.CPUUsage.TotalUsage,
"usage_in_usermode": stats.CPUStats.CPUUsage.UsageInUsermode,
"usage_in_kernelmode": stats.CPUStats.CPUUsage.UsageInKernelmode,
"usage_system": stats.CPUStats.SystemUsage,
"throttling_periods": stats.CPUStats.ThrottlingData.Periods,
"throttling_throttled_periods": stats.CPUStats.ThrottlingData.ThrottledPeriods,
"throttling_throttled_time": stats.CPUStats.ThrottlingData.ThrottledTime,
"container_id": id,
}
previousCPU := stats.PreCPUStats.CPUUsage.TotalUsage
previousSystem := stats.PreCPUStats.SystemUsage
cpuPercent := docker.CalculateCPUPercentUnix(previousCPU, previousSystem, stats)
cpufields["usage_percent"] = cpuPercent
cputags := copyTags(tags)
cputags["cpu"] = "cpu-total"
acc.AddFields("ecs_container_cpu", cpufields, cputags, tm)
// If we have OnlineCPUs field, then use it to restrict stats gathering to only Online CPUs
// (https://github.com/moby/moby/commit/115f91d7575d6de6c7781a96a082f144fd17e400)
var percpuusage []uint64
if stats.CPUStats.OnlineCPUs > 0 && len(stats.CPUStats.CPUUsage.PercpuUsage) > 0 {
percpuusage = stats.CPUStats.CPUUsage.PercpuUsage[:stats.CPUStats.OnlineCPUs]
} else {
percpuusage = stats.CPUStats.CPUUsage.PercpuUsage
}
for i, percpu := range percpuusage {
percputags := copyTags(tags)
percputags["cpu"] = fmt.Sprintf("cpu%d", i)
fields := map[string]interface{}{
"usage_total": percpu,
"container_id": id,
}
acc.AddFields("ecs_container_cpu", fields, percputags, tm)
}
}
func netstats(id string, stats *container.StatsResponse, acc telegraf.Accumulator, tags map[string]string, tm time.Time) {
totalNetworkStatMap := make(map[string]interface{})
for network, netstats := range stats.Networks {
netfields := map[string]interface{}{
"rx_dropped": netstats.RxDropped,
"rx_bytes": netstats.RxBytes,
"rx_errors": netstats.RxErrors,
"tx_packets": netstats.TxPackets,
"tx_dropped": netstats.TxDropped,
"rx_packets": netstats.RxPackets,
"tx_errors": netstats.TxErrors,
"tx_bytes": netstats.TxBytes,
"container_id": id,
}
nettags := copyTags(tags)
nettags["network"] = network
acc.AddFields("ecs_container_net", netfields, nettags, tm)
for field, value := range netfields {
if field == "container_id" {
continue
}
var uintV uint64
switch v := value.(type) {
case uint64:
uintV = v
case int64:
uintV = uint64(v)
default:
continue
}
_, ok := totalNetworkStatMap[field]
if ok {
totalNetworkStatMap[field] = totalNetworkStatMap[field].(uint64) + uintV
} else {
totalNetworkStatMap[field] = uintV
}
}
}
// totalNetworkStatMap could be empty if container is running with --net=host.
if len(totalNetworkStatMap) != 0 {
nettags := copyTags(tags)
nettags["network"] = "total"
totalNetworkStatMap["container_id"] = id
acc.AddFields("ecs_container_net", totalNetworkStatMap, nettags, tm)
}
}
func blkstats(id string, stats *container.StatsResponse, acc telegraf.Accumulator, tags map[string]string, tm time.Time) {
blkioStats := stats.BlkioStats
// Make a map of devices to their block io stats
deviceStatMap := make(map[string]map[string]interface{})
for _, metric := range blkioStats.IoServiceBytesRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
_, ok := deviceStatMap[device]
if !ok {
deviceStatMap[device] = make(map[string]interface{})
}
field := "io_service_bytes_recursive_" + strings.ToLower(metric.Op)
deviceStatMap[device][field] = metric.Value
}
for _, metric := range blkioStats.IoServicedRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
_, ok := deviceStatMap[device]
if !ok {
deviceStatMap[device] = make(map[string]interface{})
}
field := "io_serviced_recursive_" + strings.ToLower(metric.Op)
deviceStatMap[device][field] = metric.Value
}
for _, metric := range blkioStats.IoQueuedRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
field := "io_queue_recursive_" + strings.ToLower(metric.Op)
deviceStatMap[device][field] = metric.Value
}
for _, metric := range blkioStats.IoServiceTimeRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
field := "io_service_time_recursive_" + strings.ToLower(metric.Op)
deviceStatMap[device][field] = metric.Value
}
for _, metric := range blkioStats.IoWaitTimeRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
field := "io_wait_time_" + strings.ToLower(metric.Op)
deviceStatMap[device][field] = metric.Value
}
for _, metric := range blkioStats.IoMergedRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
field := "io_merged_recursive_" + strings.ToLower(metric.Op)
deviceStatMap[device][field] = metric.Value
}
for _, metric := range blkioStats.IoTimeRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
deviceStatMap[device]["io_time_recursive"] = metric.Value
}
for _, metric := range blkioStats.SectorsRecursive {
device := fmt.Sprintf("%d:%d", metric.Major, metric.Minor)
deviceStatMap[device]["sectors_recursive"] = metric.Value
}
totalStatMap := make(map[string]interface{})
for device, fields := range deviceStatMap {
fields["container_id"] = id
iotags := copyTags(tags)
iotags["device"] = device
acc.AddFields("ecs_container_blkio", fields, iotags, tm)
for field, value := range fields {
if field == "container_id" {
continue
}
var uintV uint64
switch v := value.(type) {
case uint64:
uintV = v
case int64:
uintV = uint64(v)
default:
continue
}
_, ok := totalStatMap[field]
if ok {
totalStatMap[field] = totalStatMap[field].(uint64) + uintV
} else {
totalStatMap[field] = uintV
}
}
}
totalStatMap["container_id"] = id
iotags := copyTags(tags)
iotags["device"] = "total"
acc.AddFields("ecs_container_blkio", totalStatMap, iotags, tm)
}

View file

@ -0,0 +1,226 @@
package ecs
import (
"testing"
"time"
"github.com/influxdata/telegraf/testutil"
)
func Test_metastats(t *testing.T) {
var mockAcc testutil.Accumulator
tags := map[string]string{
"test_tag": "test",
}
tm := time.Now()
metastats(nginxStatsKey, &validMeta.Containers[1], &mockAcc, tags, tm)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_meta",
map[string]interface{}{
"container_id": nginxStatsKey,
"docker_name": "ecs-nginx-2-nginx",
"image": "nginx:alpine",
"image_id": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"desired_status": "RUNNING",
"known_status": "RUNNING",
"limit_cpu": float64(0),
"limit_mem": float64(0),
"created_at": metaCreated,
"started_at": metaStarted,
"type": "NORMAL",
},
tags,
)
}
func Test_memstats(t *testing.T) {
var mockAcc testutil.Accumulator
tags := map[string]string{
"test_tag": "test",
}
tm := time.Now()
memstats(nginxStatsKey, validStats[nginxStatsKey], &mockAcc, tags, tm)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_mem",
map[string]interface{}{
"active_anon": uint64(1597440),
"active_file": uint64(1462272),
"cache": uint64(5787648),
"container_id": nginxStatsKey,
"hierarchical_memory_limit": uint64(536870912),
"inactive_anon": uint64(4096),
"inactive_file": uint64(4321280),
"limit": uint64(1033658368),
"mapped_file": uint64(3616768),
"max_usage": uint64(8667136),
"pgmajfault": uint64(40),
"pgpgin": uint64(3477),
"pgpgout": uint64(1674),
"pgfault": uint64(2924),
"rss": uint64(1597440),
"total_active_anon": uint64(1597440),
"total_active_file": uint64(1462272),
"total_cache": uint64(5787648),
"total_inactive_anon": uint64(4096),
"total_inactive_file": uint64(4321280),
"total_mapped_file": uint64(3616768),
"total_pgfault": uint64(2924),
"total_pgpgout": uint64(1674),
"total_pgpgin": uint64(3477),
"total_rss": uint64(1597440),
"usage": uint64(2392064),
"usage_percent": float64(0.23141727228778164),
},
map[string]string{
"test_tag": "test",
},
)
}
func Test_cpustats(t *testing.T) {
var mockAcc testutil.Accumulator
tags := map[string]string{
"test_tag": "test",
}
tm := time.Now()
cpustats(nginxStatsKey, validStats[nginxStatsKey], &mockAcc, tags, tm)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_cpu",
map[string]interface{}{
"container_id": nginxStatsKey,
"throttling_periods": uint64(0),
"throttling_throttled_periods": uint64(0),
"throttling_throttled_time": uint64(0),
"usage_in_usermode": uint64(40000000),
"usage_in_kernelmode": uint64(10000000),
"usage_percent": float64(0),
"usage_system": uint64(2336100000000),
"usage_total": uint64(65599511),
},
map[string]string{
"test_tag": "test",
"cpu": "cpu-total",
},
)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_cpu",
map[string]interface{}{
"container_id": nginxStatsKey,
"usage_total": uint64(65599511),
},
map[string]string{
"test_tag": "test",
"cpu": "cpu0",
},
)
}
func Test_netstats(t *testing.T) {
var mockAcc testutil.Accumulator
tags := map[string]string{
"test_tag": "test",
}
tm := time.Now()
netstats(pauseStatsKey, validStats[pauseStatsKey], &mockAcc, tags, tm)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_net",
map[string]interface{}{
"container_id": pauseStatsKey,
"rx_bytes": uint64(5338),
"rx_dropped": uint64(0),
"rx_errors": uint64(0),
"rx_packets": uint64(36),
"tx_bytes": uint64(648),
"tx_dropped": uint64(0),
"tx_errors": uint64(0),
"tx_packets": uint64(8),
},
map[string]string{
"test_tag": "test",
"network": "eth0",
},
)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_net",
map[string]interface{}{
"container_id": pauseStatsKey,
"rx_bytes": uint64(4641),
"rx_dropped": uint64(0),
"rx_errors": uint64(0),
"rx_packets": uint64(26),
"tx_bytes": uint64(690),
"tx_dropped": uint64(0),
"tx_errors": uint64(0),
"tx_packets": uint64(9),
},
map[string]string{
"test_tag": "test",
"network": "eth5",
},
)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_net",
map[string]interface{}{
"container_id": pauseStatsKey,
"rx_bytes": uint64(9979),
"rx_dropped": uint64(0),
"rx_errors": uint64(0),
"rx_packets": uint64(62),
"tx_bytes": uint64(1338),
"tx_dropped": uint64(0),
"tx_errors": uint64(0),
"tx_packets": uint64(17),
},
map[string]string{
"test_tag": "test",
"network": "total",
},
)
}
func Test_blkstats(t *testing.T) {
var mockAcc testutil.Accumulator
tags := map[string]string{
"test_tag": "test",
}
tm := time.Now()
blkstats(nginxStatsKey, validStats[nginxStatsKey], &mockAcc, tags, tm)
mockAcc.AssertContainsTaggedFields(
t,
"ecs_container_blkio",
map[string]interface{}{
"container_id": nginxStatsKey,
"io_service_bytes_recursive_read": uint64(5730304),
"io_service_bytes_recursive_write": uint64(0),
"io_service_bytes_recursive_sync": uint64(5730304),
"io_service_bytes_recursive_async": uint64(0),
"io_service_bytes_recursive_total": uint64(5730304),
"io_serviced_recursive_read": uint64(156),
"io_serviced_recursive_write": uint64(0),
"io_serviced_recursive_sync": uint64(156),
"io_serviced_recursive_async": uint64(0),
"io_serviced_recursive_total": uint64(156),
},
map[string]string{
"test_tag": "test",
"device": "202:26368",
},
)
}

View file

@ -0,0 +1,48 @@
{
"Cluster": "ecs-cluster-prod",
"TaskARN": "arn:aws:ecs:eu-west-2:<account_id>:task/ecs-cluster-prod/a22cfc58cdb04b27ada2caa3fd526463",
"Family": "nginx-telegraf",
"Revision": "3",
"DesiredStatus": "RUNNING",
"KnownStatus": "RUNNING",
"Containers": [
{
"DockerId": "c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",
"Name": "~internal~ecs~pause",
"DockerName": "ecs-nginx-telegraf-3-internalecspause-f0cc8fe3bfc9a8efd601",
"Image": "amazon/amazon-ecs-pause:0.1.0",
"ImageID": "",
"Labels": {
"com.amazonaws.ecs.cluster": "ecs-cluster-prod",
"com.amazonaws.ecs.container-name": "~internal~ecs~pause",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:eu-west-2:<account_id>:task/ecs-cluster-prod/a22cfc58cdb04b27ada2caa3fd526463",
"com.amazonaws.ecs.task-definition-family": "nginx-telegraf",
"com.amazonaws.ecs.task-definition-version": "3"
},
"DesiredStatus": "RESOURCES_PROVISIONED",
"KnownStatus": "RESOURCES_PROVISIONED",
"Limits": {
"CPU": 2,
"Memory": 0
},
"CreatedAt": "2023-10-26T15:57:02.520698803Z",
"StartedAt": "2023-10-26T15:57:02.806357646Z",
"Type": "CNI_PAUSE",
"Networks": [
{
"NetworkMode": "awsvpc",
"IPv4Addresses": [
"10.1.4.120"
]
}
]
}
],
"Limits": {
"CPU": 0.5,
"Memory": 1024
},
"PullStartedAt": "2023-10-26T15:57:03.528549391Z",
"PullStoppedAt": "2023-10-26T15:57:18.310985676Z",
"AvailabilityZone": "eu-west-2b"
}

View file

@ -0,0 +1 @@
ecs_container_meta,test_tag=test container_id="c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",desired_status="RESOURCES_PROVISIONED",docker_name="ecs-nginx-telegraf-3-internalecspause-f0cc8fe3bfc9a8efd601",image="amazon/amazon-ecs-pause:0.1.0",image_id="",known_status="RESOURCES_PROVISIONED",limit_cpu=2,limit_mem=0,type="CNI_PAUSE" 1698343335413572143

View file

@ -0,0 +1,101 @@
{
"c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55": {
"read": "2023-10-26T15:58:32.690109934Z",
"preread": "2023-10-26T15:58:31.642796089Z",
"pids_stats": {
"current": 1,
"limit": 4618
},
"blkio_stats": {
"io_service_bytes_recursive": null,
"io_serviced_recursive": null,
"io_queue_recursive": null,
"io_service_time_recursive": null,
"io_wait_time_recursive": null,
"io_merged_recursive": null,
"io_time_recursive": null,
"sectors_recursive": null
},
"num_procs": 0,
"storage_stats": {},
"cpu_stats": {
"cpu_usage": {
"total_usage": 34818000,
"usage_in_kernelmode": 5397000,
"usage_in_usermode": 29420000
},
"system_cpu_usage": 16970500000000,
"online_cpus": 2,
"throttling_data": {
"periods": 0,
"throttled_periods": 0,
"throttled_time": 0
}
},
"precpu_stats": {
"cpu_usage": {
"total_usage": 34818000,
"usage_in_kernelmode": 5397000,
"usage_in_usermode": 29420000
},
"system_cpu_usage": 16968420000000,
"online_cpus": 2,
"throttling_data": {
"periods": 0,
"throttled_periods": 0,
"throttled_time": 0
}
},
"memory_stats": {
"usage": 303104,
"stats": {
"active_anon": 4096,
"active_file": 0,
"anon": 45056,
"anon_thp": 0,
"file": 0,
"file_dirty": 0,
"file_mapped": 0,
"file_writeback": 0,
"inactive_anon": 40960,
"inactive_file": 0,
"kernel_stack": 16384,
"pgactivate": 1,
"pgdeactivate": 0,
"pgfault": 1113,
"pglazyfree": 0,
"pglazyfreed": 0,
"pgmajfault": 0,
"pgrefill": 0,
"pgscan": 0,
"pgsteal": 0,
"shmem": 0,
"slab": 190768,
"slab_reclaimable": 66320,
"slab_unreclaimable": 124448,
"sock": 0,
"thp_collapse_alloc": 0,
"thp_fault_alloc": 0,
"unevictable": 0,
"workingset_activate": 0,
"workingset_nodereclaim": 0,
"workingset_refault": 0
},
"limit": 4051226624
},
"name": "/ecs-nginx-telegraf-3-internalecspause-f0cc8fe3bfc9a8efd601",
"id": "c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",
"networks": {
"eth0": {
"rx_bytes": 14,
"rx_packets": 0,
"rx_errors": 0,
"rx_dropped": 0,
"tx_bytes": 21,
"tx_packets": 0,
"tx_errors": 0,
"tx_dropped": 0
}
}
}
}

View file

@ -0,0 +1,5 @@
ecs_container_mem,test_tag=test active_anon=4096u,active_file=0u,container_id="c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",inactive_anon=40960u,inactive_file=0u,limit=4051226624u,max_usage=0u,pgfault=1113u,pgmajfault=0u,unevictable=0u,usage=303104u,usage_percent=0.00748178337406187 1698343335413563073
ecs_container_cpu,cpu=cpu-total,test_tag=test container_id="c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",throttling_periods=0u,throttling_throttled_periods=0u,throttling_throttled_time=0u,usage_in_kernelmode=5397000u,usage_in_usermode=29420000u,usage_percent=0,usage_system=16970500000000u,usage_total=34818000u 1698343335413566953
ecs_container_net,network=eth0,test_tag=test container_id="c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",rx_bytes=14u,rx_dropped=0u,rx_errors=0u,rx_packets=0u,tx_bytes=21u,tx_dropped=0u,tx_errors=0u,tx_packets=0u 1698343335413572143
ecs_container_net,network=total,test_tag=test container_id="c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55",rx_bytes=14u,rx_dropped=0u,rx_errors=0u,rx_packets=0u,tx_bytes=21u,tx_dropped=0u,tx_errors=0u,tx_packets=0u 1698343335413572143
ecs_container_blkio,device=total,test_tag=test container_id="c69461b2c836cc3f0e3e5deb07b1f16e25f6009da2a48bb0adc7dd580befaf55" 1698343335413575563

View file

@ -0,0 +1,78 @@
{
"Cluster": "test",
"TaskARN": "arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a",
"Family": "nginx",
"Revision": "2",
"DesiredStatus": "RUNNING",
"KnownStatus": "RUNNING",
"Containers": [
{
"DockerId": "e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba",
"Name": "~internal~ecs~pause",
"DockerName": "ecs-nginx-2-internalecspause",
"Image": "amazon/amazon-ecs-pause:0.1.0",
"ImageID": "",
"Labels": {
"com.amazonaws.ecs.cluster": "test",
"com.amazonaws.ecs.container-name": "~internal~ecs~pause",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a",
"com.amazonaws.ecs.task-definition-family": "nginx",
"com.amazonaws.ecs.task-definition-version": "2"
},
"DesiredStatus": "RESOURCES_PROVISIONED",
"KnownStatus": "RESOURCES_PROVISIONED",
"Limits": {
"CPU": 0,
"Memory": 0
},
"CreatedAt": "2018-11-19T15:31:26.641964373Z",
"StartedAt": "2018-11-19T15:31:27.035698679Z",
"Type": "CNI_PAUSE",
"Networks": [
{
"NetworkMode": "awsvpc",
"IPv4Addresses": [
"172.31.25.181"
]
}
]
},
{
"DockerId": "fffe894e232d46c76475cfeabf4907f712e8b92618a37fca3ef0805bbbfb0299",
"Name": "nginx",
"DockerName": "ecs-nginx-2-nginx",
"Image": "nginx:alpine",
"ImageID": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"Labels": {
"com.amazonaws.ecs.cluster": "test",
"com.amazonaws.ecs.container-name": "nginx",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:aws-region-1:012345678901:task/a1234abc-a0a0-0a01-ab01-0abc012a0a0a",
"com.amazonaws.ecs.task-definition-family": "nginx",
"com.amazonaws.ecs.task-definition-version": "2"
},
"DesiredStatus": "RUNNING",
"KnownStatus": "RUNNING",
"Limits": {
"CPU": 0,
"Memory": 0
},
"CreatedAt": "2018-11-19T15:31:27.614884084Z",
"StartedAt": "2018-11-19T15:31:27.975996351Z",
"Type": "NORMAL",
"Networks": [
{
"NetworkMode": "awsvpc",
"IPv4Addresses": [
"172.31.25.181"
]
}
]
}
],
"Limits": {
"CPU": 0.5,
"Memory": 512
},
"PullStartedAt": "2018-11-19T15:31:27.197327103Z",
"PullStoppedAt": "2018-11-19T15:31:27.609089471Z"
}

663
plugins/inputs/ecs/testdata/stats.golden vendored Normal file
View file

@ -0,0 +1,663 @@
{
"e6af031b91deb3136a2b7c42f262ed2ab554e2fe2736998c7d8edf4afe708dba": {
"read": "2018-11-19T15:40:00.936081344Z",
"preread": "2018-11-19T15:39:59.933000984Z",
"num_procs": 0,
"pids_stats": {},
"networks": {
"eth0": {
"rx_bytes": 5338,
"rx_dropped": 0,
"rx_errors": 0,
"rx_packets": 36,
"tx_bytes": 648,
"tx_dropped": 0,
"tx_errors": 0,
"tx_packets": 8
},
"eth5": {
"rx_bytes": 4641,
"rx_dropped": 0,
"rx_errors": 0,
"rx_packets": 26,
"tx_bytes": 690,
"tx_dropped": 0,
"tx_errors": 0,
"tx_packets": 9
}
},
"memory_stats": {
"stats": {
"cache": 790528,
"mapped_file": 618496,
"total_inactive_file": 782336,
"pgpgout": 1040,
"rss": 40960,
"total_mapped_file": 618496,
"pgpgin": 1243,
"pgmajfault": 6,
"total_rss": 40960,
"hierarchical_memory_limit": 536870912,
"total_pgfault": 1298,
"total_active_file": 8192,
"active_anon": 40960,
"total_active_anon": 40960,
"total_pgpgout": 1040,
"total_cache": 790528,
"active_file": 8192,
"pgfault": 1298,
"inactive_file": 782336,
"total_pgpgin": 1243,
"hierarchical_memsw_limit": 9223372036854772000
},
"max_usage": 4825088,
"usage": 1343488,
"limit": 1033658368
},
"blkio_stats": {
"io_service_bytes_recursive": [
{
"major": 202,
"minor": 26368,
"op": "Read",
"value": 790528
},
{
"major": 202,
"minor": 26368,
"op": "Write"
},
{
"major": 202,
"minor": 26368,
"op": "Sync",
"value": 790528
},
{
"major": 202,
"minor": 26368,
"op": "Async"
},
{
"major": 202,
"minor": 26368,
"op": "Total",
"value": 790528
},
{
"major": 253,
"minor": 1,
"op": "Read",
"value": 790528
},
{
"major": 253,
"minor": 1,
"op": "Write"
},
{
"major": 253,
"minor": 1,
"op": "Sync",
"value": 790528
},
{
"major": 253,
"minor": 1,
"op": "Async"
},
{
"major": 253,
"minor": 1,
"op": "Total",
"value": 790528
},
{
"major": 253,
"minor": 2,
"op": "Read",
"value": 790528
},
{
"major": 253,
"minor": 2,
"op": "Write"
},
{
"major": 253,
"minor": 2,
"op": "Sync",
"value": 790528
},
{
"major": 253,
"minor": 2,
"op": "Async"
},
{
"major": 253,
"minor": 2,
"op": "Total",
"value": 790528
},
{
"major": 253,
"minor": 4,
"op": "Read",
"value": 790528
},
{
"major": 253,
"minor": 4,
"op": "Write"
},
{
"major": 253,
"minor": 4,
"op": "Sync",
"value": 790528
},
{
"major": 253,
"minor": 4,
"op": "Async"
},
{
"major": 253,
"minor": 4,
"op": "Total",
"value": 790528
}
],
"io_serviced_recursive": [
{
"major": 202,
"minor": 26368,
"op": "Read",
"value": 10
},
{
"major": 202,
"minor": 26368,
"op": "Write"
},
{
"major": 202,
"minor": 26368,
"op": "Sync",
"value": 10
},
{
"major": 202,
"minor": 26368,
"op": "Async"
},
{
"major": 202,
"minor": 26368,
"op": "Total",
"value": 10
},
{
"major": 253,
"minor": 1,
"op": "Read",
"value": 10
},
{
"major": 253,
"minor": 1,
"op": "Write"
},
{
"major": 253,
"minor": 1,
"op": "Sync",
"value": 10
},
{
"major": 253,
"minor": 1,
"op": "Async"
},
{
"major": 253,
"minor": 1,
"op": "Total",
"value": 10
},
{
"major": 253,
"minor": 2,
"op": "Read",
"value": 10
},
{
"major": 253,
"minor": 2,
"op": "Write"
},
{
"major": 253,
"minor": 2,
"op": "Sync",
"value": 10
},
{
"major": 253,
"minor": 2,
"op": "Async"
},
{
"major": 253,
"minor": 2,
"op": "Total",
"value": 10
},
{
"major": 253,
"minor": 4,
"op": "Read",
"value": 10
},
{
"major": 253,
"minor": 4,
"op": "Write"
},
{
"major": 253,
"minor": 4,
"op": "Sync",
"value": 10
},
{
"major": 253,
"minor": 4,
"op": "Async"
},
{
"major": 253,
"minor": 4,
"op": "Total",
"value": 10
}
]
},
"cpu_stats": {
"cpu_usage": {
"percpu_usage": [
26426156,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"usage_in_usermode": 20000000,
"total_usage": 26426156
},
"system_cpu_usage": 2336100000000,
"online_cpus": 1,
"throttling_data": {}
},
"precpu_stats": {
"cpu_usage": {
"percpu_usage": [
26426156,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"usage_in_usermode": 20000000,
"total_usage": 26426156
},
"system_cpu_usage": 2335090000000,
"online_cpus": 1,
"throttling_data": {}
},
"storage_stats": {}
},
"fffe894e232d46c76475cfeabf4907f712e8b92618a37fca3ef0805bbbfb0299": {
"read": "2018-11-19T15:40:00.93733207Z",
"preread": "2018-11-19T15:39:59.934291009Z",
"num_procs": 0,
"pids_stats": {},
"network": {},
"memory_stats": {
"stats": {
"cache": 5787648,
"mapped_file": 3616768,
"total_inactive_file": 4321280,
"pgpgout": 1674,
"rss": 1597440,
"total_mapped_file": 3616768,
"pgpgin": 3477,
"pgmajfault": 40,
"total_rss": 1597440,
"total_inactive_anon": 4096,
"hierarchical_memory_limit": 536870912,
"total_pgfault": 2924,
"total_active_file": 1462272,
"active_anon": 1597440,
"total_active_anon": 1597440,
"total_pgpgout": 1674,
"total_cache": 5787648,
"inactive_anon": 4096,
"active_file": 1462272,
"pgfault": 2924,
"inactive_file": 4321280,
"total_pgpgin": 3477,
"hierarchical_memsw_limit": 9223372036854772000
},
"max_usage": 8667136,
"usage": 8179712,
"limit": 1033658368
},
"blkio_stats": {
"io_service_bytes_recursive": [
{
"major": 202,
"minor": 26368,
"op": "Read",
"value": 5730304
},
{
"major": 202,
"minor": 26368,
"op": "Write"
},
{
"major": 202,
"minor": 26368,
"op": "Sync",
"value": 5730304
},
{
"major": 202,
"minor": 26368,
"op": "Async"
},
{
"major": 202,
"minor": 26368,
"op": "Total",
"value": 5730304
},
{
"major": 253,
"minor": 1,
"op": "Read",
"value": 5730304
},
{
"major": 253,
"minor": 1,
"op": "Write"
},
{
"major": 253,
"minor": 1,
"op": "Sync",
"value": 5730304
},
{
"major": 253,
"minor": 1,
"op": "Async"
},
{
"major": 253,
"minor": 1,
"op": "Total",
"value": 5730304
},
{
"major": 253,
"minor": 2,
"op": "Read",
"value": 5730304
},
{
"major": 253,
"minor": 2,
"op": "Write"
},
{
"major": 253,
"minor": 2,
"op": "Sync",
"value": 5730304
},
{
"major": 253,
"minor": 2,
"op": "Async"
},
{
"major": 253,
"minor": 2,
"op": "Total",
"value": 5730304
},
{
"major": 253,
"minor": 5,
"op": "Read",
"value": 5730304
},
{
"major": 253,
"minor": 5,
"op": "Write"
},
{
"major": 253,
"minor": 5,
"op": "Sync",
"value": 5730304
},
{
"major": 253,
"minor": 5,
"op": "Async"
},
{
"major": 253,
"minor": 5,
"op": "Total",
"value": 5730304
}
],
"io_serviced_recursive": [
{
"major": 202,
"minor": 26368,
"op": "Read",
"value": 156
},
{
"major": 202,
"minor": 26368,
"op": "Write"
},
{
"major": 202,
"minor": 26368,
"op": "Sync",
"value": 156
},
{
"major": 202,
"minor": 26368,
"op": "Async"
},
{
"major": 202,
"minor": 26368,
"op": "Total",
"value": 156
},
{
"major": 253,
"minor": 1,
"op": "Read",
"value": 156
},
{
"major": 253,
"minor": 1,
"op": "Write"
},
{
"major": 253,
"minor": 1,
"op": "Sync",
"value": 156
},
{
"major": 253,
"minor": 1,
"op": "Async"
},
{
"major": 253,
"minor": 1,
"op": "Total",
"value": 156
},
{
"major": 253,
"minor": 2,
"op": "Read",
"value": 156
},
{
"major": 253,
"minor": 2,
"op": "Write"
},
{
"major": 253,
"minor": 2,
"op": "Sync",
"value": 156
},
{
"major": 253,
"minor": 2,
"op": "Async"
},
{
"major": 253,
"minor": 2,
"op": "Total",
"value": 156
},
{
"major": 253,
"minor": 5,
"op": "Read",
"value": 147
},
{
"major": 253,
"minor": 5,
"op": "Write"
},
{
"major": 253,
"minor": 5,
"op": "Sync",
"value": 147
},
{
"major": 253,
"minor": 5,
"op": "Async"
},
{
"major": 253,
"minor": 5,
"op": "Total",
"value": 147
}
]
},
"cpu_stats": {
"cpu_usage": {
"percpu_usage": [
65599511,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"usage_in_usermode": 40000000,
"total_usage": 65599511,
"usage_in_kernelmode": 10000000
},
"system_cpu_usage": 2336100000000,
"online_cpus": 1,
"throttling_data": {}
},
"precpu_stats": {
"cpu_usage": {
"percpu_usage": [
65599511,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"usage_in_usermode": 40000000,
"total_usage": 65599511,
"usage_in_kernelmode": 10000000
},
"system_cpu_usage": 2335090000000,
"online_cpus": 1,
"throttling_data": {}
},
"storage_stats": {}
}
}

View file

@ -0,0 +1,78 @@
package ecs
import (
"encoding/json"
"io"
"strings"
"time"
"github.com/docker/docker/api/types/container"
)
// ecsTask is the ECS task representation
type ecsTask struct {
Cluster string
TaskARN string
Family string
Revision string
DesiredStatus string
KnownStatus string
Containers []ecsContainer
Limits map[string]float64
PullStartedAt time.Time
PullStoppedAt time.Time
}
// ecsContainer is the ECS metadata container representation
type ecsContainer struct {
ID string `json:"DockerId"`
Name string
DockerName string
Image string
ImageID string
Labels map[string]string
DesiredStatus string
KnownStatus string
Limits map[string]float64
CreatedAt time.Time
StartedAt time.Time
Stats container.StatsResponse
Type string
Networks []network
}
// network is a docker network configuration
type network struct {
NetworkMode string
IPv4Addresses []string
}
func unmarshalTask(r io.Reader) (*ecsTask, error) {
task := &ecsTask{}
err := json.NewDecoder(r).Decode(task)
return task, err
}
// docker parsers
func unmarshalStats(r io.Reader) (map[string]*container.StatsResponse, error) {
var statsMap map[string]*container.StatsResponse
if err := json.NewDecoder(r).Decode(&statsMap); err != nil {
return nil, err
}
return statsMap, nil
}
// interleaves Stats in to the Container objects in the ecsTask
func mergeTaskStats(task *ecsTask, stats map[string]*container.StatsResponse) {
for i := range task.Containers {
c := &task.Containers[i]
if strings.Trim(c.ID, " ") == "" {
continue
}
stat, ok := stats[c.ID]
if !ok || stat == nil {
continue
}
c.Stats = *stat
}
}

View file

@ -0,0 +1,47 @@
package ecs
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func Test_parseTask(t *testing.T) {
r, err := os.Open("testdata/metadata.golden")
require.NoError(t, err)
parsed, err := unmarshalTask(r)
require.NoError(t, err)
require.Equal(t, validMeta, *parsed)
}
func Test_parseStats(t *testing.T) {
r, err := os.Open("testdata/stats.golden")
require.NoError(t, err)
parsed, err := unmarshalStats(r)
require.NoError(t, err)
require.Equal(t, validStats, parsed)
}
func Test_mergeTaskStats(t *testing.T) {
metadata, err := os.Open("testdata/metadata.golden")
require.NoError(t, err)
parsedMetadata, err := unmarshalTask(metadata)
require.NoError(t, err)
stats, err := os.Open("testdata/stats.golden")
require.NoError(t, err)
parsedStats, err := unmarshalStats(stats)
require.NoError(t, err)
mergeTaskStats(parsedMetadata, parsedStats)
for i := range parsedMetadata.Containers {
require.Equal(t, validStats[parsedMetadata.Containers[i].ID], &parsedMetadata.Containers[i].Stats)
}
}