├── .circleci └── config.yml ├── .github ├── dependabot.yml └── workflows │ ├── container_description.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile.common ├── README.md ├── SECURITY.md ├── VERSION ├── ecscollector ├── collector.go ├── collector_test.go └── testdata │ ├── fixtures │ ├── ec2_task_metadata.json │ ├── ec2_task_stats.json │ ├── fargate_task_metadata.json │ └── fargate_task_stats.json │ └── snapshots │ ├── ec2_metrics.txt │ └── fargate_metrics.txt ├── ecsmetadata └── client.go ├── go.mod ├── go.sum └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | prometheus: prometheus/prometheus@0.17.1 5 | executors: 6 | # Whenever the Go version is updated here, .promu.yml should also be updated. 7 | golang: 8 | docker: 9 | - image: cimg/go:1.24 10 | jobs: 11 | test: 12 | executor: golang 13 | steps: 14 | - prometheus/setup_environment 15 | - run: make 16 | - prometheus/store_artifact: 17 | file: ecs_exporter 18 | - run: git diff --exit-code 19 | workflows: 20 | version: 2 21 | ecs_exporter: 22 | jobs: 23 | - test: 24 | filters: 25 | tags: 26 | only: /.*/ 27 | - prometheus/build: 28 | name: build 29 | filters: 30 | tags: 31 | only: /.*/ 32 | - prometheus/publish_main: 33 | context: org-context 34 | docker_hub_organization: prometheuscommunity 35 | quay_io_organization: prometheuscommunity 36 | requires: 37 | - test 38 | - build 39 | filters: 40 | branches: 41 | only: main 42 | - prometheus/publish_release: 43 | context: org-context 44 | docker_hub_organization: prometheuscommunity 45 | quay_io_organization: prometheuscommunity 46 | requires: 47 | - test 48 | - build 49 | filters: 50 | tags: 51 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 52 | branches: 53 | ignore: /.*/ 54 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/container_description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Push README to Docker Hub 3 | on: 4 | push: 5 | paths: 6 | - "README.md" 7 | - "README-containers.md" 8 | - ".github/workflows/container_description.yml" 9 | branches: [ main, master ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | PushDockerHubReadme: 16 | runs-on: ubuntu-latest 17 | name: Push README to Docker Hub 18 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 19 | steps: 20 | - name: git checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Set docker hub repo name 23 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 24 | - name: Push README to Dockerhub 25 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 26 | env: 27 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 28 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 29 | with: 30 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 31 | provider: dockerhub 32 | short_description: ${{ env.DOCKER_REPO_NAME }} 33 | # Empty string results in README-containers.md being pushed if it 34 | # exists. Otherwise, README.md is pushed. 35 | readme_file: '' 36 | 37 | PushQuayIoReadme: 38 | runs-on: ubuntu-latest 39 | name: Push README to quay.io 40 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 41 | steps: 42 | - name: git checkout 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | - name: Set quay.io org name 45 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 46 | - name: Set quay.io repo name 47 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 48 | - name: Push README to quay.io 49 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 50 | env: 51 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 52 | with: 53 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 54 | provider: quay 55 | # Empty string results in README-containers.md being pushed if it 56 | # exists. Otherwise, README.md is pushed. 57 | readme_file: '' 58 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: Install Go 29 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 30 | with: 31 | go-version: 1.24.x 32 | - name: Install snmp_exporter/generator dependencies 33 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 34 | if: github.repository == 'prometheus/snmp_exporter' 35 | - name: Lint 36 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 37 | with: 38 | args: --verbose 39 | version: v2.1.5 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | dependencies-stamp 24 | /ecs_exporter 25 | /.build 26 | /.release 27 | /.tarballs 28 | .deps 29 | *.tar.gz 30 | /vendor 31 | .idea 32 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - misspell 5 | - revive 6 | - sloglint 7 | settings: 8 | errcheck: 9 | exclude-functions: 10 | - (net/http.ResponseWriter).Write 11 | revive: 12 | rules: 13 | - name: unused-parameter 14 | severity: warning 15 | disabled: true 16 | exclusions: 17 | generated: lax 18 | presets: 19 | - comments 20 | - common-false-positives 21 | - legacy 22 | - std-error-handling 23 | rules: 24 | - linters: 25 | - errcheck 26 | path: _test.go 27 | paths: 28 | - third_party$ 29 | - builtin$ 30 | - examples$ 31 | formatters: 32 | exclusions: 33 | generated: lax 34 | paths: 35 | - third_party$ 36 | - builtin$ 37 | - examples$ 38 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, .travis.yml and 3 | # .circle/config.yml should also be updated. 4 | version: 1.24 5 | repository: 6 | path: github.com/prometheus-community/ecs_exporter 7 | build: 8 | ldflags: | 9 | -X github.com/prometheus/common/version.Version={{.Version}} 10 | -X github.com/prometheus/common/version.Revision={{.Revision}} 11 | -X github.com/prometheus/common/version.Branch={{.Branch}} 12 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 13 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 14 | tarball: 15 | files: 16 | - LICENSE 17 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | ignore: | 4 | **/node_modules 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | commas: disable 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | indentation: 18 | spaces: consistent 19 | indent-sequences: consistent 20 | key-duplicates: 21 | ignore: | 22 | config/testdata/section_key_dup.bad.yml 23 | line-length: disable 24 | truthy: 25 | check-keys: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 / 2025-03-19 2 | 3 | Breaking Changes: 4 | 5 | Many metric names, labels, and semantics have changed to improve correctness. 6 | See the [README](./README.md#example-output) for current /metrics output. 7 | 8 | * [CHANGE] Overhaul all metrics. #81 9 | * [ENHANCEMENT] Add a counter for container restarts #87 10 | * [ENHANCEMENT] Add a flag to disable standard client_golang exporter metrics 11 | #82 12 | 13 | ## 0.3.0 / 2024-10-13 14 | 15 | * [CHANGE] Use upstream ecs-agent types for deserializing API responses #75 16 | * [CHANGE] Update exporter boilerplate #77 17 | * [ENHANCEMENT] Add additional metrics #53 18 | 19 | ## 0.2.1 / 2023-01-24 20 | 21 | * [BUGFIX] Fix cpu and memory stats #49 22 | 23 | ## 0.2.0 / 2022-08-02 24 | 25 | * [BUGFIX] Fix CPU usage metrics #37 26 | 27 | ## 0.1.1 / 2022-02-03 28 | 29 | * [FEATURE] Expose memory cache metric #25 30 | * [ENHANCEMENT] Allow ecsmetadata to work outside of ECS #18 31 | 32 | ## 0.1.0 / 2021-09-21 33 | 34 | * [FEATURE] Initial release. 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/ecs_exporter /bin/ecs_exporter 9 | 10 | EXPOSE 9779 11 | USER nobody 12 | ENTRYPOINT [ "/bin/ecs_exporter" ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Ensure that 'all' is the default target otherwise it will be the first target from Makefile.common. 2 | all:: 3 | 4 | # Needs to be defined before including Makefile.common to auto-generate targets 5 | DOCKER_ARCHS ?= amd64 arm64 6 | DOCKER_REPO ?= prometheuscommunity 7 | 8 | include Makefile.common 9 | 10 | DOCKER_IMAGE_NAME ?= ecs-exporter 11 | -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # A common Makefile that includes rules to be reused in different prometheus projects. 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! 17 | 18 | # Example usage : 19 | # Create the main Makefile in the root project directory. 20 | # include Makefile.common 21 | # customTarget: 22 | # @echo ">> Running customTarget" 23 | # 24 | 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path 26 | unexport GOBIN 27 | 28 | GO ?= go 29 | GOFMT ?= $(GO)fmt 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 31 | GOOPTS ?= 32 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 33 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 34 | 35 | GO_VERSION ?= $(shell $(GO) version) 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) 37 | PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') 38 | 39 | PROMU := $(FIRST_GOPATH)/bin/promu 40 | pkgs = ./... 41 | 42 | ifeq (arm, $(GOHOSTARCH)) 43 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 44 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 45 | else 46 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 47 | endif 48 | 49 | GOTEST := $(GO) test 50 | GOTEST_DIR := 51 | ifneq ($(CIRCLE_JOB),) 52 | ifneq ($(shell command -v gotestsum 2> /dev/null),) 53 | GOTEST_DIR := test-results 54 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 55 | endif 56 | endif 57 | 58 | PROMU_VERSION ?= 0.17.0 59 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 60 | 61 | SKIP_GOLANGCI_LINT := 62 | GOLANGCI_LINT := 63 | GOLANGCI_LINT_OPTS ?= 64 | GOLANGCI_LINT_VERSION ?= v2.1.5 65 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 66 | # windows isn't included here because of the path separator being different. 67 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 68 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 69 | # If we're in CI and there is an Actions file, that means the linter 70 | # is being run in Actions, so we don't need to run it here. 71 | ifneq (,$(SKIP_GOLANGCI_LINT)) 72 | GOLANGCI_LINT := 73 | else ifeq (,$(CIRCLE_JOB)) 74 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 75 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 76 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 77 | endif 78 | endif 79 | endif 80 | 81 | PREFIX ?= $(shell pwd) 82 | BIN_DIR ?= $(shell pwd) 83 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 84 | DOCKERFILE_PATH ?= ./Dockerfile 85 | DOCKERBUILD_CONTEXT ?= ./ 86 | DOCKER_REPO ?= prom 87 | 88 | DOCKER_ARCHS ?= amd64 89 | 90 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 91 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 92 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 93 | 94 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 95 | 96 | ifeq ($(GOHOSTARCH),amd64) 97 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 98 | # Only supported on amd64 99 | test-flags := -race 100 | endif 101 | endif 102 | 103 | # This rule is used to forward a target like "build" to "common-build". This 104 | # allows a new "build" target to be defined in a Makefile which includes this 105 | # one and override "common-build" without override warnings. 106 | %: common-% ; 107 | 108 | .PHONY: common-all 109 | common-all: precheck style check_license lint yamllint unused build test 110 | 111 | .PHONY: common-style 112 | common-style: 113 | @echo ">> checking code style" 114 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 115 | if [ -n "$${fmtRes}" ]; then \ 116 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 117 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 118 | exit 1; \ 119 | fi 120 | 121 | .PHONY: common-check_license 122 | common-check_license: 123 | @echo ">> checking license header" 124 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 125 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 126 | done); \ 127 | if [ -n "$${licRes}" ]; then \ 128 | echo "license header checking failed:"; echo "$${licRes}"; \ 129 | exit 1; \ 130 | fi 131 | 132 | .PHONY: common-deps 133 | common-deps: 134 | @echo ">> getting dependencies" 135 | $(GO) mod download 136 | 137 | .PHONY: update-go-deps 138 | update-go-deps: 139 | @echo ">> updating Go dependencies" 140 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 141 | $(GO) get -d $$m; \ 142 | done 143 | $(GO) mod tidy 144 | 145 | .PHONY: common-test-short 146 | common-test-short: $(GOTEST_DIR) 147 | @echo ">> running short tests" 148 | $(GOTEST) -short $(GOOPTS) $(pkgs) 149 | 150 | .PHONY: common-test 151 | common-test: $(GOTEST_DIR) 152 | @echo ">> running all tests" 153 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 154 | 155 | $(GOTEST_DIR): 156 | @mkdir -p $@ 157 | 158 | .PHONY: common-format 159 | common-format: 160 | @echo ">> formatting code" 161 | $(GO) fmt $(pkgs) 162 | 163 | .PHONY: common-vet 164 | common-vet: 165 | @echo ">> vetting code" 166 | $(GO) vet $(GOOPTS) $(pkgs) 167 | 168 | .PHONY: common-lint 169 | common-lint: $(GOLANGCI_LINT) 170 | ifdef GOLANGCI_LINT 171 | @echo ">> running golangci-lint" 172 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 173 | endif 174 | 175 | .PHONY: common-lint-fix 176 | common-lint-fix: $(GOLANGCI_LINT) 177 | ifdef GOLANGCI_LINT 178 | @echo ">> running golangci-lint fix" 179 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 180 | endif 181 | 182 | .PHONY: common-yamllint 183 | common-yamllint: 184 | @echo ">> running yamllint on all YAML files in the repository" 185 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 186 | @echo "yamllint not installed so skipping" 187 | else 188 | yamllint . 189 | endif 190 | 191 | # For backward-compatibility. 192 | .PHONY: common-staticcheck 193 | common-staticcheck: lint 194 | 195 | .PHONY: common-unused 196 | common-unused: 197 | @echo ">> running check for unused/missing packages in go.mod" 198 | $(GO) mod tidy 199 | @git diff --exit-code -- go.sum go.mod 200 | 201 | .PHONY: common-build 202 | common-build: promu 203 | @echo ">> building binaries" 204 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 205 | 206 | .PHONY: common-tarball 207 | common-tarball: promu 208 | @echo ">> building release tarball" 209 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 210 | 211 | .PHONY: common-docker-repo-name 212 | common-docker-repo-name: 213 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 214 | 215 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 216 | common-docker: $(BUILD_DOCKER_ARCHS) 217 | $(BUILD_DOCKER_ARCHS): common-docker-%: 218 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 219 | -f $(DOCKERFILE_PATH) \ 220 | --build-arg ARCH="$*" \ 221 | --build-arg OS="linux" \ 222 | $(DOCKERBUILD_CONTEXT) 223 | 224 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 225 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 226 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 227 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 228 | 229 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 230 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 231 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 232 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 233 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 234 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 235 | 236 | .PHONY: common-docker-manifest 237 | common-docker-manifest: 238 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) 239 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 240 | 241 | .PHONY: promu 242 | promu: $(PROMU) 243 | 244 | $(PROMU): 245 | $(eval PROMU_TMP := $(shell mktemp -d)) 246 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 247 | mkdir -p $(FIRST_GOPATH)/bin 248 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 249 | rm -r $(PROMU_TMP) 250 | 251 | .PHONY: proto 252 | proto: 253 | @echo ">> generating code from proto files" 254 | @./scripts/genproto.sh 255 | 256 | ifdef GOLANGCI_LINT 257 | $(GOLANGCI_LINT): 258 | mkdir -p $(FIRST_GOPATH)/bin 259 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 260 | | sed -e '/install -d/d' \ 261 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 262 | endif 263 | 264 | .PHONY: precheck 265 | precheck:: 266 | 267 | define PRECHECK_COMMAND_template = 268 | precheck:: $(1)_precheck 269 | 270 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 271 | .PHONY: $(1)_precheck 272 | $(1)_precheck: 273 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 274 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 275 | exit 1; \ 276 | fi 277 | endef 278 | 279 | govulncheck: install-govulncheck 280 | govulncheck ./... 281 | 282 | install-govulncheck: 283 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 284 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecs_exporter 2 | 3 | [![CircleCI](https://circleci.com/gh/prometheus-community/ecs_exporter/tree/main.svg?style=svg)](https://circleci.com/gh/prometheus-community/ecs_exporter/tree/main) 4 | [![Go package](https://pkg.go.dev/badge/github.com/prometheus-community/ecs_exporter?status.svg)](https://pkg.go.dev/github.com/prometheus-community/ecs_exporter) 5 | 6 | This repo contains a Prometheus exporter for Amazon Elastic Container Service 7 | (ECS) that publishes [ECS task infra 8 | metrics](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html) 9 | in the [Prometheus exposition 10 | formats](https://prometheus.io/docs/instrumenting/exposition_formats/). 11 | 12 | Run the following container as a sidecar on ECS tasks: 13 | 14 | ``` 15 | quay.io/prometheuscommunity/ecs-exporter:v0.4.0 16 | ``` 17 | 18 | An example Fargate task definition that includes the container 19 | is [available](#example-task-definition). 20 | 21 | To add ECS exporter to your existing ECS task: 22 | 23 | 1. Go to ECS task definitions. 24 | 1. Click on "Create new revision". 25 | 1. Scroll down to "Container definitions" and click on "Add container". 26 | 1. Set "ecs-exporter" as container name. 27 | 1. Copy the container image URL from above. (Use the tag for the [latest 28 | release](https://github.com/prometheus-community/ecs_exporter/releases).) 29 | 1. Add tcp/9779 as a port mapping. 30 | 1. Click on "Add" to return back to task definition page. 31 | 1. Click on "Create" to create a new revision. 32 | 33 | By default, it publishes Prometheus metrics on ":9779/metrics". The exporter in this repo can be a useful complementary sidecar for the scenario described in [this blog post](https://aws.amazon.com/blogs/opensource/metrics-collection-from-amazon-ecs-using-amazon-managed-service-for-prometheus/). Adding this sidecar to the ECS task definition would export task-level metrics in addition to the custom metrics described in the blog. 34 | 35 | ## Compatibility guarantees 36 | All metrics exported by ecs_exporter are sourced from the [ECS task metadata 37 | API](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html) 38 | accessible from within every ECS task. AWS can make, and on occasion [has 39 | made](https://github.com/prometheus-community/ecs_exporter/issues/74#issuecomment-2395293862), 40 | unannounced (or at least unversioned) breaking changes to the data served from 41 | this API, especially the container-level stats served from the `/task/stats` 42 | endpoint. Metrics emitted by ecs_exporter may spontaneously break as a result, 43 | in which case we may need to make breaking changes to ecs_exporter to keep up. 44 | 45 | In light of these conditions, we currently do not have plans to cut a 1.0 46 | release. When necessary, breaking changes will continue to land in minor version 47 | releases. The [release 48 | notes](https://github.com/prometheus-community/ecs_exporter/releases) will 49 | document any breaking changes as they come. 50 | 51 | ## Labels 52 | 53 | ### On task-level metrics 54 | 55 | None. You may 56 | [join](https://grafana.com/blog/2021/08/04/how-to-use-promql-joins-for-more-effective-queries-of-prometheus-metrics-at-scale/) 57 | to `ecs_task_metadata_info` to add task-level metadata (such as the task ARN) to 58 | task-level or any other metrics emitted by ecs_exporter. 59 | 60 | ### On container-level metrics 61 | 62 | * **container_name**: Name of the container (as in the ECS task definition) associated with the metric. 63 | 64 | ### On network-level metrics 65 | 66 | * **interface**: Network interface device associated with the metric. 67 | 68 | ## Example output 69 | 70 | Check out the [metrics snapshots](./ecscollector/testdata/snapshots) which 71 | contain sample metrics emitted by ecs_exporter in the [Prometheus text 72 | format](https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format) 73 | you should expect to see on /metrics. Note that these snapshots behave as if 74 | `--web.disable-exporter-metrics` were passed when running ecs_exporter, such 75 | that standard [client_golang](https://github.com/prometheus/client_golang) 76 | metrics are not included. 77 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.0 2 | -------------------------------------------------------------------------------- /ecscollector/collector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package ecscollector implements a Prometheus collector for Amazon ECS 15 | // metrics available at the ECS metadata server. 16 | package ecscollector 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/docker/docker/api/types/container" 23 | "github.com/prometheus-community/ecs_exporter/ecsmetadata" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | // ECS cpu_stats are from upstream docker/moby. These values are in nanoseconds. 28 | // https://github.com/moby/moby/blob/49f021ebf00a76d74f5ce158244083e2dfba26fb/api/types/stats.go#L18-L40 29 | const nanoseconds = 1 / 1.0e9 30 | 31 | // Task definition memory parameters are defined in MiB, while Prometheus 32 | // standard metrics use bytes. 33 | const mebibytes = 1024 * 1024 34 | 35 | var ( 36 | taskMetadataDesc = prometheus.NewDesc( 37 | "ecs_task_metadata_info", 38 | "ECS task metadata, sourced from the task metadata endpoint version 4.", 39 | taskMetadataLabels, nil) 40 | 41 | taskCpuLimitDesc = prometheus.NewDesc( 42 | "ecs_task_cpu_limit_vcpus", 43 | "Configured task CPU limit in vCPUs (1 vCPU = 1024 CPU units). This is optional when running on EC2; if no limit is set, this metric has no value.", 44 | taskLabels, nil) 45 | 46 | taskMemLimitDesc = prometheus.NewDesc( 47 | "ecs_task_memory_limit_bytes", 48 | "Configured task memory limit in bytes. This is optional when running on EC2; if no limit is set, this metric has no value.", 49 | taskLabels, nil) 50 | 51 | taskEphemeralStorageUsedDesc = prometheus.NewDesc( 52 | "ecs_task_ephemeral_storage_used_bytes", 53 | "Current Fargate task ephemeral storage usage in bytes.", 54 | taskLabels, nil) 55 | 56 | taskEphemeralStorageAllocatedDesc = prometheus.NewDesc( 57 | "ecs_task_ephemeral_storage_allocated_bytes", 58 | "Configured Fargate task ephemeral storage allocated size in bytes.", 59 | taskLabels, nil) 60 | 61 | taskImagePullStartDesc = prometheus.NewDesc( 62 | "ecs_task_image_pull_start_timestamp_seconds", 63 | "The time at which the task started pulling docker images for its containers.", 64 | taskLabels, nil) 65 | 66 | taskImagePullStopDesc = prometheus.NewDesc( 67 | "ecs_task_image_pull_stop_timestamp_seconds", 68 | "The time at which the task stopped (i.e. completed) pulling docker images for its containers.", 69 | taskLabels, nil) 70 | 71 | restartTotalDesc = prometheus.NewDesc( 72 | "ecs_container_restarts_total", 73 | "Cumulative total count of container restarts. Only has a value if the container has been configured to restart on failure.", 74 | containerLabels, nil) 75 | 76 | cpuTotalDesc = prometheus.NewDesc( 77 | "ecs_container_cpu_usage_seconds_total", 78 | "Cumulative total container CPU usage in seconds.", 79 | containerLabels, nil) 80 | 81 | memUsageDesc = prometheus.NewDesc( 82 | "ecs_container_memory_usage_bytes", 83 | "Current container memory usage in bytes.", 84 | containerLabels, nil) 85 | 86 | memLimitDesc = prometheus.NewDesc( 87 | "ecs_container_memory_limit_bytes", 88 | "Configured container memory limit in bytes, set from the container-level limit in the task definition if any, otherwise the task-level limit.", 89 | containerLabels, nil) 90 | 91 | memCacheSizeDesc = prometheus.NewDesc( 92 | "ecs_container_memory_page_cache_size_bytes", 93 | "Current container memory page cache size in bytes. This is not a subset of used bytes.", 94 | containerLabels, nil) 95 | 96 | networkRxBytesDesc = prometheus.NewDesc( 97 | "ecs_network_receive_bytes_total", 98 | "Cumulative total size of network packets received in bytes.", 99 | networkLabels, nil) 100 | 101 | networkRxPacketsDesc = prometheus.NewDesc( 102 | "ecs_network_receive_packets_total", 103 | "Cumulative total count of network packets received.", 104 | networkLabels, nil) 105 | 106 | networkRxDroppedDesc = prometheus.NewDesc( 107 | "ecs_network_receive_packets_dropped_total", 108 | "Cumulative total count of network packets dropped in receiving.", 109 | networkLabels, nil) 110 | 111 | networkRxErrorsDesc = prometheus.NewDesc( 112 | "ecs_network_receive_errors_total", 113 | "Cumulative total count of network errors in receiving.", 114 | networkLabels, nil) 115 | 116 | networkTxBytesDesc = prometheus.NewDesc( 117 | "ecs_network_transmit_bytes_total", 118 | "Cumulative total size of network packets transmitted in bytes.", 119 | networkLabels, nil) 120 | 121 | networkTxPacketsDesc = prometheus.NewDesc( 122 | "ecs_network_transmit_packets_total", 123 | "Cumulative total count of network packets transmitted.", 124 | networkLabels, nil) 125 | 126 | networkTxDroppedDesc = prometheus.NewDesc( 127 | "ecs_network_transmit_packets_dropped_total", 128 | "Cumulative total count of network packets dropped in transmit.", 129 | networkLabels, nil) 130 | 131 | networkTxErrorsDesc = prometheus.NewDesc( 132 | "ecs_network_transmit_errors_total", 133 | "Cumulative total count of network errors in transmit.", 134 | networkLabels, nil) 135 | ) 136 | 137 | var containerLabels = []string{ 138 | "container_name", 139 | } 140 | 141 | var taskLabels = []string{} 142 | 143 | var taskMetadataLabels = []string{ 144 | "cluster", 145 | "task_arn", 146 | "family", 147 | "revision", 148 | "desired_status", 149 | "known_status", 150 | "availability_zone", 151 | "launch_type", 152 | } 153 | 154 | var networkLabels = []string{ 155 | "interface", 156 | } 157 | 158 | // NewCollector returns a new Collector that queries ECS metadata server 159 | // for ECS task and container metrics. 160 | func NewCollector(client *ecsmetadata.Client, logger *slog.Logger) prometheus.Collector { 161 | return &collector{client: client, logger: logger} 162 | } 163 | 164 | type collector struct { 165 | client *ecsmetadata.Client 166 | logger *slog.Logger 167 | } 168 | 169 | func (c *collector) Describe(ch chan<- *prometheus.Desc) { 170 | ch <- taskMetadataDesc 171 | ch <- taskCpuLimitDesc 172 | ch <- taskMemLimitDesc 173 | ch <- taskEphemeralStorageUsedDesc 174 | ch <- taskEphemeralStorageAllocatedDesc 175 | ch <- taskImagePullStartDesc 176 | ch <- taskImagePullStopDesc 177 | ch <- restartTotalDesc 178 | ch <- cpuTotalDesc 179 | ch <- memUsageDesc 180 | ch <- memLimitDesc 181 | ch <- memCacheSizeDesc 182 | ch <- networkRxBytesDesc 183 | ch <- networkRxPacketsDesc 184 | ch <- networkRxDroppedDesc 185 | ch <- networkRxErrorsDesc 186 | ch <- networkTxBytesDesc 187 | ch <- networkTxPacketsDesc 188 | ch <- networkTxDroppedDesc 189 | ch <- networkTxErrorsDesc 190 | } 191 | 192 | func (c *collector) Collect(ch chan<- prometheus.Metric) { 193 | ctx := context.Background() 194 | metadata, err := c.client.RetrieveTaskMetadata(ctx) 195 | if err != nil { 196 | c.logger.Debug("Failed to retrieve metadata", "error", err) 197 | return 198 | } 199 | c.logger.Debug("Got ECS task metadata response", "metadata", metadata) 200 | 201 | ch <- prometheus.MustNewConstMetric( 202 | taskMetadataDesc, 203 | prometheus.GaugeValue, 204 | 1.0, 205 | metadata.Cluster, 206 | metadata.TaskARN, 207 | metadata.Family, 208 | metadata.Revision, 209 | metadata.DesiredStatus, 210 | metadata.KnownStatus, 211 | metadata.AvailabilityZone, 212 | metadata.LaunchType, 213 | ) 214 | 215 | // Task CPU/memory limits are optional when running on EC2 - the relevant 216 | // limits may only exist at the container level. 217 | if metadata.Limits != nil { 218 | if metadata.Limits.CPU != nil { 219 | ch <- prometheus.MustNewConstMetric( 220 | taskCpuLimitDesc, 221 | prometheus.GaugeValue, 222 | *metadata.Limits.CPU, 223 | ) 224 | } 225 | if metadata.Limits.Memory != nil { 226 | ch <- prometheus.MustNewConstMetric( 227 | taskMemLimitDesc, 228 | prometheus.GaugeValue, 229 | float64(*metadata.Limits.Memory*mebibytes), 230 | ) 231 | } 232 | } 233 | 234 | if metadata.EphemeralStorageMetrics != nil { 235 | ch <- prometheus.MustNewConstMetric( 236 | taskEphemeralStorageUsedDesc, 237 | prometheus.GaugeValue, 238 | float64(metadata.EphemeralStorageMetrics.UtilizedMiBs*mebibytes), 239 | ) 240 | ch <- prometheus.MustNewConstMetric( 241 | taskEphemeralStorageAllocatedDesc, 242 | prometheus.GaugeValue, 243 | float64(metadata.EphemeralStorageMetrics.ReservedMiBs*mebibytes), 244 | ) 245 | } 246 | 247 | if metadata.PullStartedAt != nil { 248 | ch <- prometheus.MustNewConstMetric( 249 | taskImagePullStartDesc, 250 | prometheus.GaugeValue, 251 | float64(metadata.PullStartedAt.UnixNano())*nanoseconds, 252 | ) 253 | } 254 | if metadata.PullStoppedAt != nil { 255 | ch <- prometheus.MustNewConstMetric( 256 | taskImagePullStopDesc, 257 | prometheus.GaugeValue, 258 | float64(metadata.PullStoppedAt.UnixNano())*nanoseconds, 259 | ) 260 | } 261 | 262 | stats, err := c.client.RetrieveTaskStats(ctx) 263 | if err != nil { 264 | c.logger.Debug("Failed to retrieve container stats", "error", err) 265 | return 266 | } 267 | c.logger.Debug("Got ECS task stats response", "stats", stats) 268 | 269 | networks := make(map[string]*container.NetworkStats) 270 | for _, container := range metadata.Containers { 271 | s := stats[container.ID] 272 | if s == nil || s.StatsJSON == nil { 273 | // This can happen if the container is stopped; if it's 274 | // nonessential, the task goes on. 275 | c.logger.Debug("Couldn't find stats for container", "id", container.ID) 276 | continue 277 | } 278 | 279 | containerLabelVals := []string{ 280 | container.Name, 281 | } 282 | 283 | if container.RestartCount != nil { 284 | ch <- prometheus.MustNewConstMetric( 285 | restartTotalDesc, 286 | prometheus.CounterValue, 287 | float64(*container.RestartCount), 288 | containerLabelVals..., 289 | ) 290 | } 291 | 292 | ch <- prometheus.MustNewConstMetric( 293 | cpuTotalDesc, 294 | prometheus.CounterValue, 295 | float64(s.CPUStats.CPUUsage.TotalUsage)*nanoseconds, 296 | containerLabelVals..., 297 | ) 298 | 299 | cacheValue := 0.0 300 | if val, ok := s.MemoryStats.Stats["cache"]; ok { 301 | cacheValue = float64(val) 302 | } 303 | 304 | // Report the container's memory limit as its own, if any, otherwise the 305 | // task's limit. This is correct in that this is the precise logic used 306 | // to configure the cgroups limit for the container. 307 | var containerMemoryLimitMib int64 308 | if container.Limits.Memory != nil { 309 | containerMemoryLimitMib = *container.Limits.Memory 310 | } else { 311 | // This must be set if the container limit is not set, and thus is 312 | // safe to dereference. 313 | containerMemoryLimitMib = *metadata.Limits.Memory 314 | } 315 | for desc, value := range map[*prometheus.Desc]float64{ 316 | memUsageDesc: float64(s.MemoryStats.Usage), 317 | memLimitDesc: float64(containerMemoryLimitMib * mebibytes), 318 | memCacheSizeDesc: cacheValue, 319 | } { 320 | ch <- prometheus.MustNewConstMetric( 321 | desc, 322 | prometheus.GaugeValue, 323 | value, 324 | containerLabelVals..., 325 | ) 326 | } 327 | 328 | // Network metrics per interface. 329 | for iface, netStats := range s.Networks { 330 | // While the API response attaches network stats to each container, 331 | // the container is in fact not a relevant dimension; only the 332 | // interface is. This means that if multiple containers use the same 333 | // network (extremely likely), we are redundantly writing this 334 | // metric with "last one wins" semantics. This is fine: the values 335 | // for an interface are the same across all containers. 336 | // 337 | // The collection process will error if you report the same metric 338 | // multiple times, however, so we have to stash this data in the 339 | // `netStats` map to ensure that we only send one metric per 340 | // interface. 341 | networks[iface] = &netStats 342 | } 343 | } 344 | 345 | for iface, netStats := range networks { 346 | networkLabelVals := []string{ 347 | iface, 348 | } 349 | 350 | for desc, value := range map[*prometheus.Desc]float64{ 351 | networkRxBytesDesc: float64(netStats.RxBytes), 352 | networkRxPacketsDesc: float64(netStats.RxPackets), 353 | networkRxDroppedDesc: float64(netStats.RxDropped), 354 | networkRxErrorsDesc: float64(netStats.RxErrors), 355 | networkTxBytesDesc: float64(netStats.TxBytes), 356 | networkTxPacketsDesc: float64(netStats.TxPackets), 357 | networkTxDroppedDesc: float64(netStats.TxDropped), 358 | networkTxErrorsDesc: float64(netStats.TxErrors), 359 | } { 360 | ch <- prometheus.MustNewConstMetric( 361 | desc, 362 | prometheus.CounterValue, 363 | value, 364 | networkLabelVals..., 365 | ) 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /ecscollector/collector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package ecscollector 15 | 16 | import ( 17 | "errors" 18 | "flag" 19 | "fmt" 20 | "io" 21 | "log/slog" 22 | "net/http" 23 | "net/http/httptest" 24 | "os" 25 | "path/filepath" 26 | "testing" 27 | 28 | "github.com/prometheus-community/ecs_exporter/ecsmetadata" 29 | "github.com/prometheus/client_golang/prometheus" 30 | "github.com/prometheus/client_golang/prometheus/promhttp" 31 | "github.com/prometheus/client_golang/prometheus/testutil" 32 | ) 33 | 34 | // Create a metadata client that will always receive the given fixture API 35 | // responses. 36 | func fixtureClient(taskMetadataPath, taskStatsPath string) (*ecsmetadata.Client, *httptest.Server, error) { 37 | taskMetadata, err := os.ReadFile(taskMetadataPath) 38 | if err != nil { 39 | return nil, nil, fmt.Errorf("failed to read task metadata fixture: %w", err) 40 | } 41 | taskStats, err := os.ReadFile(taskStatsPath) 42 | if err != nil { 43 | return nil, nil, fmt.Errorf("failed to read task stats fixture: %w", err) 44 | } 45 | 46 | mux := http.NewServeMux() 47 | mux.HandleFunc("GET /task", func(w http.ResponseWriter, r *http.Request) { 48 | w.Header().Add("content-type", "application/json") 49 | w.Write(taskMetadata) 50 | }) 51 | mux.HandleFunc("GET /task/stats", func(w http.ResponseWriter, r *http.Request) { 52 | w.Header().Add("content-type", "application/json") 53 | w.Write(taskStats) 54 | }) 55 | 56 | server := httptest.NewServer(mux) 57 | return ecsmetadata.NewClient(server.URL), server, nil 58 | } 59 | 60 | // Renders metrics from the given collector to the prometheus text exposition 61 | // format. 62 | func renderMetrics(collector prometheus.Collector) ([]byte, error) { 63 | registry := prometheus.NewRegistry() 64 | registry.MustRegister(collector) 65 | 66 | // It seems that the only way to really get full /metrics output is with 67 | // promhttp. There is testutil.CollectAndFormat but it requires you to 68 | // specify every metric name you want in the output, which seems to be not 69 | // worth it compared to this. 70 | promServer := httptest.NewServer(promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) 71 | defer promServer.Close() 72 | resp, err := http.Get(promServer.URL) 73 | if err != nil { 74 | return nil, fmt.Errorf("metrics request failed: %w", err) 75 | } 76 | 77 | defer resp.Body.Close() 78 | if resp.StatusCode != 200 { 79 | return nil, fmt.Errorf("non-200 metrics response: %v", resp.StatusCode) 80 | } 81 | metrics, err := io.ReadAll(resp.Body) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to read metrics response body: %w", err) 84 | } 85 | return metrics, nil 86 | } 87 | 88 | var updateSnapshots = flag.Bool("update-snapshots", false, "update snapshot files") 89 | 90 | func assertSnapshot(t *testing.T, collector prometheus.Collector, path string) { 91 | if *updateSnapshots { 92 | metrics, err := renderMetrics(collector) 93 | if err != nil { 94 | t.Fatalf("failed to render new snapshot %s: %v", path, err) 95 | } 96 | dir := filepath.Dir(path) 97 | if err := os.MkdirAll(dir, 0750); err != nil { 98 | t.Fatalf("failed to create snapshot output directory %s: %v", dir, err) 99 | } else if err := os.WriteFile(path, metrics, 0666); err != nil { 100 | t.Fatalf("failed to write snapshot file %s: %v", path, err) 101 | } else { 102 | t.Logf("updated snapshot: %s", path) 103 | } 104 | } 105 | 106 | file, err := os.Open(path) 107 | if errors.Is(err, os.ErrNotExist) { 108 | t.Fatalf("snapshot file does not exist, set the -update-snapshots flag to update: %v", err) 109 | } else if err != nil { 110 | t.Fatalf("failed to open snapshot file: %v", err) 111 | } else if err := testutil.CollectAndCompare(collector, file); err != nil { 112 | t.Fatalf("snapshot outdated, set the -update-snapshots flag to update\n%v", err) 113 | } 114 | } 115 | 116 | func TestFargateMetrics(t *testing.T) { 117 | metadataClient, metadataServer, err := fixtureClient( 118 | "testdata/fixtures/fargate_task_metadata.json", 119 | "testdata/fixtures/fargate_task_stats.json", 120 | ) 121 | if err != nil { 122 | t.Fatalf("failed to load test fixtures: %v", err) 123 | } 124 | defer metadataServer.Close() 125 | collector := NewCollector(metadataClient, slog.Default()) 126 | assertSnapshot(t, collector, "testdata/snapshots/fargate_metrics.txt") 127 | } 128 | 129 | func TestEc2Metrics(t *testing.T) { 130 | metadataClient, metadataServer, err := fixtureClient( 131 | "testdata/fixtures/ec2_task_metadata.json", 132 | "testdata/fixtures/ec2_task_stats.json", 133 | ) 134 | if err != nil { 135 | t.Fatalf("failed to load test fixtures: %v", err) 136 | } 137 | defer metadataServer.Close() 138 | collector := NewCollector(metadataClient, slog.Default()) 139 | assertSnapshot(t, collector, "testdata/snapshots/ec2_metrics.txt") 140 | } 141 | -------------------------------------------------------------------------------- /ecscollector/testdata/fixtures/ec2_task_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cluster": "prom-ecs-exporter-sandbox", 3 | "TaskARN": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9", 4 | "Family": "prom-ecs-exporter-sandbox-main-ec2", 5 | "Revision": "13", 6 | "DesiredStatus": "RUNNING", 7 | "KnownStatus": "RUNNING", 8 | "PullStartedAt": "2025-02-27T05:09:52.332595252Z", 9 | "PullStoppedAt": "2025-02-27T05:10:01.206072368Z", 10 | "AvailabilityZone": "us-east-1a", 11 | "LaunchType": "EC2", 12 | "Containers": [ 13 | { 14 | "DockerId": "213e1203f4bb72af185724d937e698d2724acf35b57ec2dd5f3c963adbd2d38c", 15 | "Name": "nonessential", 16 | "DockerName": "ecs-prom-ecs-exporter-sandbox-main-ec2-13-nonessential-9c9ab8aeb0e0dbdca601", 17 | "Image": "alpine", 18 | "ImageID": "sha256:8d591b0b7dea080ea3be9e12ae563eebf9869168ffced1cb25b2470a3d9fe15e", 19 | "Labels": { 20 | "com.amazonaws.ecs.cluster": "prom-ecs-exporter-sandbox", 21 | "com.amazonaws.ecs.container-name": "nonessential", 22 | "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9", 23 | "com.amazonaws.ecs.task-definition-family": "prom-ecs-exporter-sandbox-main-ec2", 24 | "com.amazonaws.ecs.task-definition-version": "13" 25 | }, 26 | "DesiredStatus": "RUNNING", 27 | "KnownStatus": "STOPPED", 28 | "ExitCode": 0, 29 | "Limits": { 30 | "CPU": 128, 31 | "Memory": 256 32 | }, 33 | "CreatedAt": "2025-02-27T05:09:54.959587312Z", 34 | "StartedAt": "2025-02-27T05:09:56.392336771Z", 35 | "FinishedAt": "2025-02-27T05:09:56.409399983Z", 36 | "Type": "NORMAL", 37 | "Volumes": [ 38 | { 39 | "Source": "/var/lib/ecs/deps/execute-command/config/amazon-ssm-agent-Orvj12YkCf4DKDu1cHTOVj7smDviWx1T4Kg3Q_IdNYA=.json", 40 | "Destination": "/ecs-execute-command-636764d0-0d77-44c1-96b2-207c74034dff/configuration/amazon-ssm-agent.json" 41 | }, 42 | { 43 | "Source": "/var/lib/ecs/deps/execute-command/config/seelog-gEZ-TIvHAyOLfMC5wiWRofgDMlDzaCZ6zcswnAoop84=.xml", 44 | "Destination": "/ecs-execute-command-636764d0-0d77-44c1-96b2-207c74034dff/configuration/seelog.xml" 45 | }, 46 | { 47 | "Source": "/var/lib/ecs/deps/execute-command/certs/tls-ca-bundle.pem", 48 | "Destination": "/ecs-execute-command-636764d0-0d77-44c1-96b2-207c74034dff/certs/amazon-ssm-agent.crt" 49 | }, 50 | { 51 | "Source": "/var/log/ecs/exec/506f22fab0414cde856201584703fed9/nonessential", 52 | "Destination": "/var/log/amazon/ssm" 53 | }, 54 | { 55 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/amazon-ssm-agent", 56 | "Destination": "/ecs-execute-command-636764d0-0d77-44c1-96b2-207c74034dff/amazon-ssm-agent" 57 | }, 58 | { 59 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/ssm-agent-worker", 60 | "Destination": "/ecs-execute-command-636764d0-0d77-44c1-96b2-207c74034dff/ssm-agent-worker" 61 | }, 62 | { 63 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/ssm-session-worker", 64 | "Destination": "/ecs-execute-command-636764d0-0d77-44c1-96b2-207c74034dff/ssm-session-worker" 65 | } 66 | ], 67 | "ContainerARN": "arn:aws:ecs:us-east-1:829490980523:container/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9/80b5fc27-0113-4b4f-83a4-f3d4b4b2b016", 68 | "Networks": [ 69 | { 70 | "NetworkMode": "bridge", 71 | "IPv4Addresses": [ 72 | "" 73 | ] 74 | } 75 | ] 76 | }, 77 | { 78 | "DockerId": "01cf1f3208005cda71d5ac936ded65d2ecc0a8cc8ff8a82d2e00410bf4fbbd6d", 79 | "Name": "ecs-exporter", 80 | "DockerName": "ecs-prom-ecs-exporter-sandbox-main-ec2-13-ecs-exporter-e2aeb1e6be8998c72300", 81 | "Image": "quay.io/prometheuscommunity/ecs-exporter:main", 82 | "ImageID": "sha256:1585460bf5becf755c9f45fa931283546ca62e2d51bb638010c8958158d144bc", 83 | "Ports": [ 84 | { 85 | "ContainerPort": 9779, 86 | "Protocol": "tcp", 87 | "HostPort": 32768, 88 | "HostIp": "0.0.0.0" 89 | }, 90 | { 91 | "ContainerPort": 9779, 92 | "Protocol": "tcp", 93 | "HostPort": 32768, 94 | "HostIp": "::" 95 | } 96 | ], 97 | "Labels": { 98 | "com.amazonaws.ecs.cluster": "prom-ecs-exporter-sandbox", 99 | "com.amazonaws.ecs.container-name": "ecs-exporter", 100 | "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9", 101 | "com.amazonaws.ecs.task-definition-family": "prom-ecs-exporter-sandbox-main-ec2", 102 | "com.amazonaws.ecs.task-definition-version": "13" 103 | }, 104 | "DesiredStatus": "RUNNING", 105 | "KnownStatus": "RUNNING", 106 | "Limits": { 107 | "CPU": 128, 108 | "Memory": 256 109 | }, 110 | "CreatedAt": "2025-02-27T05:10:00.313953836Z", 111 | "StartedAt": "2025-02-27T05:10:02.731563327Z", 112 | "Type": "NORMAL", 113 | "Volumes": [ 114 | { 115 | "Source": "/var/lib/ecs/deps/execute-command/config/seelog-gEZ-TIvHAyOLfMC5wiWRofgDMlDzaCZ6zcswnAoop84=.xml", 116 | "Destination": "/ecs-execute-command-36dfb910-8e80-47b9-8b3a-12c7308123b2/configuration/seelog.xml" 117 | }, 118 | { 119 | "Source": "/var/lib/ecs/deps/execute-command/certs/tls-ca-bundle.pem", 120 | "Destination": "/ecs-execute-command-36dfb910-8e80-47b9-8b3a-12c7308123b2/certs/amazon-ssm-agent.crt" 121 | }, 122 | { 123 | "Source": "/var/log/ecs/exec/506f22fab0414cde856201584703fed9/ecs-exporter", 124 | "Destination": "/var/log/amazon/ssm" 125 | }, 126 | { 127 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/amazon-ssm-agent", 128 | "Destination": "/ecs-execute-command-36dfb910-8e80-47b9-8b3a-12c7308123b2/amazon-ssm-agent" 129 | }, 130 | { 131 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/ssm-agent-worker", 132 | "Destination": "/ecs-execute-command-36dfb910-8e80-47b9-8b3a-12c7308123b2/ssm-agent-worker" 133 | }, 134 | { 135 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/ssm-session-worker", 136 | "Destination": "/ecs-execute-command-36dfb910-8e80-47b9-8b3a-12c7308123b2/ssm-session-worker" 137 | }, 138 | { 139 | "Source": "/var/lib/ecs/deps/execute-command/config/amazon-ssm-agent-Orvj12YkCf4DKDu1cHTOVj7smDviWx1T4Kg3Q_IdNYA=.json", 140 | "Destination": "/ecs-execute-command-36dfb910-8e80-47b9-8b3a-12c7308123b2/configuration/amazon-ssm-agent.json" 141 | } 142 | ], 143 | "LogDriver": "awslogs", 144 | "LogOptions": { 145 | "awslogs-group": "EcsExporterCdkStack-promecsexportersandboxmainec2taskdefinitionpromecsexportersandboxmainec2ecsexporterLogGroup874A22EF-y3iGqSSTf3sz", 146 | "awslogs-region": "us-east-1", 147 | "awslogs-stream": "ecs-exporter/ecs-exporter/506f22fab0414cde856201584703fed9" 148 | }, 149 | "ContainerARN": "arn:aws:ecs:us-east-1:829490980523:container/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9/5fba1957-462a-48b2-9295-8602b69e00be", 150 | "Networks": [ 151 | { 152 | "NetworkMode": "bridge", 153 | "IPv4Addresses": [ 154 | "172.17.0.2" 155 | ] 156 | } 157 | ] 158 | }, 159 | { 160 | "DockerId": "6b80adab0733f579594eccae31e5b0056b9544b805450ad6e278fed7f5e1c5ba", 161 | "Name": "prometheus", 162 | "DockerName": "ecs-prom-ecs-exporter-sandbox-main-ec2-13-prometheus-86f1e9bab7a8e9a65400", 163 | "Image": "prom/prometheus:v3.1.0", 164 | "ImageID": "sha256:f3d60e89ba2d4a402d1c62dccdab300f81579355e0744670c55b9ba282f3b56d", 165 | "Labels": { 166 | "com.amazonaws.ecs.cluster": "prom-ecs-exporter-sandbox", 167 | "com.amazonaws.ecs.container-name": "prometheus", 168 | "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9", 169 | "com.amazonaws.ecs.task-definition-family": "prom-ecs-exporter-sandbox-main-ec2", 170 | "com.amazonaws.ecs.task-definition-version": "13" 171 | }, 172 | "DesiredStatus": "RUNNING", 173 | "KnownStatus": "RUNNING", 174 | "Limits": { 175 | "CPU": 128, 176 | "Memory": 256 177 | }, 178 | "CreatedAt": "2025-02-27T05:10:01.22383376Z", 179 | "StartedAt": "2025-02-27T05:10:02.730952683Z", 180 | "Type": "NORMAL", 181 | "Volumes": [ 182 | { 183 | "DockerName": "b4c23c0b1e1cea0ddfeab13122e911c7f52eb67720d3ffb43adba63b817e6a1e", 184 | "Source": "/var/lib/docker/volumes/b4c23c0b1e1cea0ddfeab13122e911c7f52eb67720d3ffb43adba63b817e6a1e/_data", 185 | "Destination": "/prometheus" 186 | }, 187 | { 188 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/amazon-ssm-agent", 189 | "Destination": "/ecs-execute-command-12f856a9-3af4-4de7-ab82-671147b2a114/amazon-ssm-agent" 190 | }, 191 | { 192 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/ssm-agent-worker", 193 | "Destination": "/ecs-execute-command-12f856a9-3af4-4de7-ab82-671147b2a114/ssm-agent-worker" 194 | }, 195 | { 196 | "Source": "/var/lib/ecs/deps/execute-command/bin/3.3.1802.0/ssm-session-worker", 197 | "Destination": "/ecs-execute-command-12f856a9-3af4-4de7-ab82-671147b2a114/ssm-session-worker" 198 | }, 199 | { 200 | "Source": "/var/lib/ecs/deps/execute-command/config/amazon-ssm-agent-Orvj12YkCf4DKDu1cHTOVj7smDviWx1T4Kg3Q_IdNYA=.json", 201 | "Destination": "/ecs-execute-command-12f856a9-3af4-4de7-ab82-671147b2a114/configuration/amazon-ssm-agent.json" 202 | }, 203 | { 204 | "Source": "/var/lib/ecs/deps/execute-command/config/seelog-gEZ-TIvHAyOLfMC5wiWRofgDMlDzaCZ6zcswnAoop84=.xml", 205 | "Destination": "/ecs-execute-command-12f856a9-3af4-4de7-ab82-671147b2a114/configuration/seelog.xml" 206 | }, 207 | { 208 | "Source": "/var/lib/ecs/deps/execute-command/certs/tls-ca-bundle.pem", 209 | "Destination": "/ecs-execute-command-12f856a9-3af4-4de7-ab82-671147b2a114/certs/amazon-ssm-agent.crt" 210 | }, 211 | { 212 | "Source": "/var/log/ecs/exec/506f22fab0414cde856201584703fed9/prometheus", 213 | "Destination": "/var/log/amazon/ssm" 214 | } 215 | ], 216 | "ContainerARN": "arn:aws:ecs:us-east-1:829490980523:container/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9/7ae35d49-867b-468f-afef-916925db8dca", 217 | "Networks": [ 218 | { 219 | "NetworkMode": "bridge", 220 | "IPv4Addresses": [ 221 | "172.17.0.3" 222 | ] 223 | } 224 | ] 225 | } 226 | ], 227 | "VPCID": "vpc-0839c743edb0c009e", 228 | "ServiceName": "prom-ecs-exporter-sandbox-main-ec2" 229 | } 230 | -------------------------------------------------------------------------------- /ecscollector/testdata/fixtures/ec2_task_stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "01cf1f3208005cda71d5ac936ded65d2ecc0a8cc8ff8a82d2e00410bf4fbbd6d": { 3 | "read": "2025-02-27T05:18:07.856950259Z", 4 | "preread": "2025-02-27T05:18:06.853724878Z", 5 | "pids_stats": { 6 | "current": 33, 7 | "limit": 404 8 | }, 9 | "blkio_stats": { 10 | "io_service_bytes_recursive": [ 11 | { 12 | "major": 259, 13 | "minor": 0, 14 | "op": "read", 15 | "value": 60960768 16 | }, 17 | { 18 | "major": 259, 19 | "minor": 0, 20 | "op": "write", 21 | "value": 57344 22 | } 23 | ], 24 | "io_serviced_recursive": null, 25 | "io_queue_recursive": null, 26 | "io_service_time_recursive": null, 27 | "io_wait_time_recursive": null, 28 | "io_merged_recursive": null, 29 | "io_time_recursive": null, 30 | "sectors_recursive": null 31 | }, 32 | "num_procs": 0, 33 | "storage_stats": {}, 34 | "cpu_stats": { 35 | "cpu_usage": { 36 | "total_usage": 331125000, 37 | "usage_in_kernelmode": 66278000, 38 | "usage_in_usermode": 264846000 39 | }, 40 | "system_cpu_usage": 1045600000000, 41 | "online_cpus": 2, 42 | "throttling_data": { 43 | "periods": 0, 44 | "throttled_periods": 0, 45 | "throttled_time": 0 46 | } 47 | }, 48 | "precpu_stats": { 49 | "cpu_usage": { 50 | "total_usage": 325963000, 51 | "usage_in_kernelmode": 65245000, 52 | "usage_in_usermode": 260717000 53 | }, 54 | "system_cpu_usage": 1043620000000, 55 | "online_cpus": 2, 56 | "throttling_data": { 57 | "periods": 0, 58 | "throttled_periods": 0, 59 | "throttled_time": 0 60 | } 61 | }, 62 | "memory_stats": { 63 | "usage": 65249280, 64 | "stats": { 65 | "active_anon": 9170944, 66 | "active_file": 7655424, 67 | "anon": 37691392, 68 | "anon_thp": 0, 69 | "file": 25296896, 70 | "file_dirty": 4096, 71 | "file_mapped": 19968000, 72 | "file_writeback": 0, 73 | "inactive_anon": 28520448, 74 | "inactive_file": 17641472, 75 | "kernel_stack": 524288, 76 | "pgactivate": 6895, 77 | "pgdeactivate": 4638, 78 | "pgfault": 17631, 79 | "pglazyfree": 0, 80 | "pglazyfreed": 0, 81 | "pgmajfault": 1120, 82 | "pgrefill": 6128, 83 | "pgscan": 24707, 84 | "pgsteal": 10033, 85 | "shmem": 0, 86 | "slab": 908520, 87 | "slab_reclaimable": 383648, 88 | "slab_unreclaimable": 524872, 89 | "sock": 0, 90 | "thp_collapse_alloc": 0, 91 | "thp_fault_alloc": 0, 92 | "unevictable": 0, 93 | "workingset_activate": 0, 94 | "workingset_nodereclaim": 0, 95 | "workingset_refault": 0 96 | }, 97 | "limit": 268435456 98 | }, 99 | "name": "/ecs-prom-ecs-exporter-sandbox-main-ec2-13-ecs-exporter-e2aeb1e6be8998c72300", 100 | "id": "01cf1f3208005cda71d5ac936ded65d2ecc0a8cc8ff8a82d2e00410bf4fbbd6d", 101 | "networks": { 102 | "eth0": { 103 | "rx_bytes": 101869, 104 | "rx_packets": 284, 105 | "rx_errors": 0, 106 | "rx_dropped": 0, 107 | "tx_bytes": 43958, 108 | "tx_packets": 278, 109 | "tx_errors": 0, 110 | "tx_dropped": 0 111 | } 112 | }, 113 | "network_rate_stats": { 114 | "rx_bytes_per_sec": 2764.084716796875, 115 | "tx_bytes_per_sec": 3390.065673828125 116 | } 117 | }, 118 | "213e1203f4bb72af185724d937e698d2724acf35b57ec2dd5f3c963adbd2d38c": {}, 119 | "6b80adab0733f579594eccae31e5b0056b9544b805450ad6e278fed7f5e1c5ba": { 120 | "read": "2025-02-27T05:18:07.855390957Z", 121 | "preread": "2025-02-27T05:18:06.852213453Z", 122 | "pids_stats": { 123 | "current": 25, 124 | "limit": 404 125 | }, 126 | "blkio_stats": { 127 | "io_service_bytes_recursive": [ 128 | { 129 | "major": 259, 130 | "minor": 0, 131 | "op": "read", 132 | "value": 99926016 133 | }, 134 | { 135 | "major": 259, 136 | "minor": 0, 137 | "op": "write", 138 | "value": 274432 139 | } 140 | ], 141 | "io_serviced_recursive": null, 142 | "io_queue_recursive": null, 143 | "io_service_time_recursive": null, 144 | "io_wait_time_recursive": null, 145 | "io_merged_recursive": null, 146 | "io_time_recursive": null, 147 | "sectors_recursive": null 148 | }, 149 | "num_procs": 0, 150 | "storage_stats": {}, 151 | "cpu_stats": { 152 | "cpu_usage": { 153 | "total_usage": 566060000, 154 | "usage_in_kernelmode": 164664000, 155 | "usage_in_usermode": 401395000 156 | }, 157 | "system_cpu_usage": 1045600000000, 158 | "online_cpus": 2, 159 | "throttling_data": { 160 | "periods": 0, 161 | "throttled_periods": 0, 162 | "throttled_time": 0 163 | } 164 | }, 165 | "precpu_stats": { 166 | "cpu_usage": { 167 | "total_usage": 566060000, 168 | "usage_in_kernelmode": 164664000, 169 | "usage_in_usermode": 401395000 170 | }, 171 | "system_cpu_usage": 1043620000000, 172 | "online_cpus": 2, 173 | "throttling_data": { 174 | "periods": 0, 175 | "throttled_periods": 0, 176 | "throttled_time": 0 177 | } 178 | }, 179 | "memory_stats": { 180 | "usage": 60981248, 181 | "stats": { 182 | "active_anon": 14704640, 183 | "active_file": 8572928, 184 | "anon": 39268352, 185 | "anon_thp": 0, 186 | "file": 19619840, 187 | "file_dirty": 0, 188 | "file_mapped": 17047552, 189 | "file_writeback": 0, 190 | "inactive_anon": 24563712, 191 | "inactive_file": 11046912, 192 | "kernel_stack": 393216, 193 | "pgactivate": 13496, 194 | "pgdeactivate": 11069, 195 | "pgfault": 18768, 196 | "pglazyfree": 0, 197 | "pglazyfreed": 0, 198 | "pgmajfault": 1133, 199 | "pgrefill": 13249, 200 | "pgscan": 48430, 201 | "pgsteal": 20303, 202 | "shmem": 0, 203 | "slab": 929280, 204 | "slab_reclaimable": 519208, 205 | "slab_unreclaimable": 410072, 206 | "sock": 0, 207 | "thp_collapse_alloc": 0, 208 | "thp_fault_alloc": 0, 209 | "unevictable": 0, 210 | "workingset_activate": 0, 211 | "workingset_nodereclaim": 0, 212 | "workingset_refault": 0 213 | }, 214 | "limit": 268435456 215 | }, 216 | "name": "/ecs-prom-ecs-exporter-sandbox-main-ec2-13-prometheus-86f1e9bab7a8e9a65400", 217 | "id": "6b80adab0733f579594eccae31e5b0056b9544b805450ad6e278fed7f5e1c5ba", 218 | "networks": { 219 | "eth0": { 220 | "rx_bytes": 45368, 221 | "rx_packets": 132, 222 | "rx_errors": 0, 223 | "rx_dropped": 0, 224 | "tx_bytes": 13532, 225 | "tx_packets": 118, 226 | "tx_errors": 0, 227 | "tx_dropped": 0 228 | } 229 | }, 230 | "network_rate_stats": { 231 | "rx_bytes_per_sec": 41.86697006225586, 232 | "tx_bytes_per_sec": 41.86697006225586 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /ecscollector/testdata/fixtures/fargate_task_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cluster": "arn:aws:ecs:us-east-1:829490980523:cluster/prom-ecs-exporter-sandbox", 3 | "TaskARN": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d", 4 | "Family": "prom-ecs-exporter-sandbox-main-fargate", 5 | "Revision": "9", 6 | "DesiredStatus": "RUNNING", 7 | "KnownStatus": "RUNNING", 8 | "Limits": { 9 | "CPU": 0.25, 10 | "Memory": 512 11 | }, 12 | "PullStartedAt": "2025-02-27T05:06:03.714437592Z", 13 | "PullStoppedAt": "2025-02-27T05:06:18.599126545Z", 14 | "AvailabilityZone": "us-east-1a", 15 | "LaunchType": "FARGATE", 16 | "Containers": [ 17 | { 18 | "DockerId": "bae32def0ab64f06818e8862e58f8d6d-1585788040", 19 | "Name": "nonessential", 20 | "DockerName": "nonessential", 21 | "Image": "alpine", 22 | "ImageID": "sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c", 23 | "Labels": { 24 | "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-1:829490980523:cluster/prom-ecs-exporter-sandbox", 25 | "com.amazonaws.ecs.container-name": "nonessential", 26 | "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d", 27 | "com.amazonaws.ecs.task-definition-family": "prom-ecs-exporter-sandbox-main-fargate", 28 | "com.amazonaws.ecs.task-definition-version": "9" 29 | }, 30 | "DesiredStatus": "RUNNING", 31 | "KnownStatus": "STOPPED", 32 | "ExitCode": 0, 33 | "Limits": { 34 | "CPU": 2 35 | }, 36 | "CreatedAt": "2025-02-27T05:06:19.200809191Z", 37 | "StartedAt": "2025-02-27T05:06:19.200809191Z", 38 | "FinishedAt": "2025-02-27T05:06:19.219711004Z", 39 | "Type": "NORMAL", 40 | "ContainerARN": "arn:aws:ecs:us-east-1:829490980523:container/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d/cafd106c-dc95-4396-a466-a894961efa50", 41 | "Networks": [ 42 | { 43 | "NetworkMode": "awsvpc", 44 | "IPv4Addresses": [ 45 | "10.0.117.145" 46 | ], 47 | "IPv6Addresses": [ 48 | "2600:1f18:4ae8:400:7ca9:f2:a4c:8285" 49 | ], 50 | "AttachmentIndex": 0, 51 | "MACAddress": "0a:ff:e5:34:fa:c9", 52 | "IPv4SubnetCIDRBlock": "10.0.0.0/17", 53 | "IPv6SubnetCIDRBlock": "2600:1f18:4ae8:400::/64", 54 | "DomainNameServers": [ 55 | "10.0.0.2" 56 | ], 57 | "DomainNameSearchList": [ 58 | "ec2.internal" 59 | ], 60 | "PrivateDNSName": "ip-10-0-117-145.ec2.internal", 61 | "SubnetGatewayIpv4Address": "10.0.0.1/17" 62 | } 63 | ], 64 | "Snapshotter": "overlayfs" 65 | }, 66 | { 67 | "DockerId": "bae32def0ab64f06818e8862e58f8d6d-1819985369", 68 | "Name": "prometheus", 69 | "DockerName": "prometheus", 70 | "Image": "prom/prometheus:v3.1.0", 71 | "ImageID": "sha256:6559acbd5d770b15bb3c954629ce190ac3cbbdb2b7f1c30f0385c4e05104e218", 72 | "Labels": { 73 | "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-1:829490980523:cluster/prom-ecs-exporter-sandbox", 74 | "com.amazonaws.ecs.container-name": "prometheus", 75 | "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d", 76 | "com.amazonaws.ecs.task-definition-family": "prom-ecs-exporter-sandbox-main-fargate", 77 | "com.amazonaws.ecs.task-definition-version": "9" 78 | }, 79 | "DesiredStatus": "RUNNING", 80 | "KnownStatus": "RUNNING", 81 | "Limits": { 82 | "CPU": 2 83 | }, 84 | "CreatedAt": "2025-02-27T05:06:19.179996393Z", 85 | "StartedAt": "2025-02-27T05:06:19.179996393Z", 86 | "Type": "NORMAL", 87 | "ContainerARN": "arn:aws:ecs:us-east-1:829490980523:container/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d/50e269e1-4232-4aed-8bf4-29c4909858f9", 88 | "Networks": [ 89 | { 90 | "NetworkMode": "awsvpc", 91 | "IPv4Addresses": [ 92 | "10.0.117.145" 93 | ], 94 | "IPv6Addresses": [ 95 | "2600:1f18:4ae8:400:7ca9:f2:a4c:8285" 96 | ], 97 | "AttachmentIndex": 0, 98 | "MACAddress": "0a:ff:e5:34:fa:c9", 99 | "IPv4SubnetCIDRBlock": "10.0.0.0/17", 100 | "IPv6SubnetCIDRBlock": "2600:1f18:4ae8:400::/64", 101 | "DomainNameServers": [ 102 | "10.0.0.2" 103 | ], 104 | "DomainNameSearchList": [ 105 | "ec2.internal" 106 | ], 107 | "PrivateDNSName": "ip-10-0-117-145.ec2.internal", 108 | "SubnetGatewayIpv4Address": "10.0.0.1/17" 109 | } 110 | ], 111 | "Snapshotter": "overlayfs" 112 | }, 113 | { 114 | "DockerId": "bae32def0ab64f06818e8862e58f8d6d-4159844948", 115 | "Name": "ecs-exporter", 116 | "DockerName": "ecs-exporter", 117 | "Image": "quay.io/prometheuscommunity/ecs-exporter:main", 118 | "ImageID": "sha256:d1802fb18cb208eda88d4b23aeff903e72c091c20fcdf02596d6bec4679f676d", 119 | "Labels": { 120 | "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-1:829490980523:cluster/prom-ecs-exporter-sandbox", 121 | "com.amazonaws.ecs.container-name": "ecs-exporter", 122 | "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d", 123 | "com.amazonaws.ecs.task-definition-family": "prom-ecs-exporter-sandbox-main-fargate", 124 | "com.amazonaws.ecs.task-definition-version": "9" 125 | }, 126 | "DesiredStatus": "RUNNING", 127 | "KnownStatus": "RUNNING", 128 | "Limits": { 129 | "CPU": 2 130 | }, 131 | "CreatedAt": "2025-02-27T05:06:19.394790335Z", 132 | "StartedAt": "2025-02-27T05:06:19.394790335Z", 133 | "Type": "NORMAL", 134 | "LogDriver": "awslogs", 135 | "LogOptions": { 136 | "awslogs-group": "EcsExporterCdkStack-promecsexportersandboxmainfargatetaskdefinitionpromecsexportersandboxmainfargateecsexporterLogGroup44D32D35-DcG8HDbOu1Sl", 137 | "awslogs-region": "us-east-1", 138 | "awslogs-stream": "ecs-exporter/ecs-exporter/bae32def0ab64f06818e8862e58f8d6d" 139 | }, 140 | "ContainerARN": "arn:aws:ecs:us-east-1:829490980523:container/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d/a9b9d903-4ca1-4ce2-8138-93094e438c6b", 141 | "Networks": [ 142 | { 143 | "NetworkMode": "awsvpc", 144 | "IPv4Addresses": [ 145 | "10.0.117.145" 146 | ], 147 | "IPv6Addresses": [ 148 | "2600:1f18:4ae8:400:7ca9:f2:a4c:8285" 149 | ], 150 | "AttachmentIndex": 0, 151 | "MACAddress": "0a:ff:e5:34:fa:c9", 152 | "IPv4SubnetCIDRBlock": "10.0.0.0/17", 153 | "IPv6SubnetCIDRBlock": "2600:1f18:4ae8:400::/64", 154 | "DomainNameServers": [ 155 | "10.0.0.2" 156 | ], 157 | "DomainNameSearchList": [ 158 | "ec2.internal" 159 | ], 160 | "PrivateDNSName": "ip-10-0-117-145.ec2.internal", 161 | "SubnetGatewayIpv4Address": "10.0.0.1/17" 162 | } 163 | ], 164 | "Snapshotter": "overlayfs" 165 | } 166 | ], 167 | "ClockDrift": { 168 | "ClockErrorBound": 0.33292849999999996, 169 | "ReferenceTimestamp": "2025-02-27T05:22:43Z", 170 | "ClockSynchronizationStatus": "SYNCHRONIZED" 171 | }, 172 | "EphemeralStorageMetrics": { 173 | "Utilized": 427, 174 | "Reserved": 20496 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /ecscollector/testdata/fixtures/fargate_task_stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "bae32def0ab64f06818e8862e58f8d6d-1585788040": null, 3 | "bae32def0ab64f06818e8862e58f8d6d-1819985369": { 4 | "read": "2025-02-27T05:22:49.186680233Z", 5 | "preread": "2025-02-27T05:22:39.185738849Z", 6 | "pids_stats": {}, 7 | "blkio_stats": { 8 | "io_service_bytes_recursive": [ 9 | { 10 | "major": 259, 11 | "minor": 1, 12 | "op": "Read", 13 | "value": 23793664 14 | }, 15 | { 16 | "major": 259, 17 | "minor": 1, 18 | "op": "Write", 19 | "value": 0 20 | }, 21 | { 22 | "major": 259, 23 | "minor": 1, 24 | "op": "Sync", 25 | "value": 23793664 26 | }, 27 | { 28 | "major": 259, 29 | "minor": 1, 30 | "op": "Async", 31 | "value": 0 32 | }, 33 | { 34 | "major": 259, 35 | "minor": 1, 36 | "op": "Discard", 37 | "value": 0 38 | }, 39 | { 40 | "major": 259, 41 | "minor": 1, 42 | "op": "Total", 43 | "value": 23793664 44 | }, 45 | { 46 | "major": 259, 47 | "minor": 0, 48 | "op": "Read", 49 | "value": 65409024 50 | }, 51 | { 52 | "major": 259, 53 | "minor": 0, 54 | "op": "Write", 55 | "value": 0 56 | }, 57 | { 58 | "major": 259, 59 | "minor": 0, 60 | "op": "Sync", 61 | "value": 65409024 62 | }, 63 | { 64 | "major": 259, 65 | "minor": 0, 66 | "op": "Async", 67 | "value": 0 68 | }, 69 | { 70 | "major": 259, 71 | "minor": 0, 72 | "op": "Discard", 73 | "value": 0 74 | }, 75 | { 76 | "major": 259, 77 | "minor": 0, 78 | "op": "Total", 79 | "value": 65409024 80 | } 81 | ], 82 | "io_serviced_recursive": [ 83 | { 84 | "major": 259, 85 | "minor": 1, 86 | "op": "Read", 87 | "value": 295 88 | }, 89 | { 90 | "major": 259, 91 | "minor": 1, 92 | "op": "Write", 93 | "value": 0 94 | }, 95 | { 96 | "major": 259, 97 | "minor": 1, 98 | "op": "Sync", 99 | "value": 295 100 | }, 101 | { 102 | "major": 259, 103 | "minor": 1, 104 | "op": "Async", 105 | "value": 0 106 | }, 107 | { 108 | "major": 259, 109 | "minor": 1, 110 | "op": "Discard", 111 | "value": 0 112 | }, 113 | { 114 | "major": 259, 115 | "minor": 1, 116 | "op": "Total", 117 | "value": 295 118 | }, 119 | { 120 | "major": 259, 121 | "minor": 0, 122 | "op": "Read", 123 | "value": 682 124 | }, 125 | { 126 | "major": 259, 127 | "minor": 0, 128 | "op": "Write", 129 | "value": 0 130 | }, 131 | { 132 | "major": 259, 133 | "minor": 0, 134 | "op": "Sync", 135 | "value": 682 136 | }, 137 | { 138 | "major": 259, 139 | "minor": 0, 140 | "op": "Async", 141 | "value": 0 142 | }, 143 | { 144 | "major": 259, 145 | "minor": 0, 146 | "op": "Discard", 147 | "value": 0 148 | }, 149 | { 150 | "major": 259, 151 | "minor": 0, 152 | "op": "Total", 153 | "value": 682 154 | } 155 | ], 156 | "io_queue_recursive": [], 157 | "io_service_time_recursive": [], 158 | "io_wait_time_recursive": [], 159 | "io_merged_recursive": [], 160 | "io_time_recursive": [], 161 | "sectors_recursive": [] 162 | }, 163 | "num_procs": 0, 164 | "storage_stats": {}, 165 | "cpu_stats": { 166 | "cpu_usage": { 167 | "total_usage": 932439492, 168 | "percpu_usage": [ 169 | 484165457, 170 | 448274035 171 | ], 172 | "usage_in_kernelmode": 120000000, 173 | "usage_in_usermode": 880000000 174 | }, 175 | "system_cpu_usage": 2121200000000, 176 | "online_cpus": 2, 177 | "throttling_data": { 178 | "periods": 0, 179 | "throttled_periods": 0, 180 | "throttled_time": 0 181 | } 182 | }, 183 | "precpu_stats": { 184 | "cpu_usage": { 185 | "total_usage": 925246189, 186 | "percpu_usage": [ 187 | 479268399, 188 | 445977790 189 | ], 190 | "usage_in_kernelmode": 120000000, 191 | "usage_in_usermode": 870000000 192 | }, 193 | "system_cpu_usage": 2101320000000, 194 | "online_cpus": 2, 195 | "throttling_data": { 196 | "periods": 0, 197 | "throttled_periods": 0, 198 | "throttled_time": 0 199 | } 200 | }, 201 | "memory_stats": { 202 | "usage": 127934464, 203 | "max_usage": 128557056, 204 | "stats": { 205 | "active_anon": 0, 206 | "active_file": 32952320, 207 | "cache": 85426176, 208 | "dirty": 0, 209 | "hierarchical_memory_limit": 536870912, 210 | "hierarchical_memsw_limit": 9223372036854771712, 211 | "inactive_anon": 40820736, 212 | "inactive_file": 52383744, 213 | "mapped_file": 72179712, 214 | "pgfault": 18975, 215 | "pgmajfault": 693, 216 | "pgpgin": 34749, 217 | "pgpgout": 3936, 218 | "rss": 40820736, 219 | "rss_huge": 0, 220 | "total_active_anon": 0, 221 | "total_active_file": 32952320, 222 | "total_cache": 85426176, 223 | "total_dirty": 0, 224 | "total_inactive_anon": 40820736, 225 | "total_inactive_file": 52383744, 226 | "total_mapped_file": 72179712, 227 | "total_pgfault": 18975, 228 | "total_pgmajfault": 693, 229 | "total_pgpgin": 34749, 230 | "total_pgpgout": 3936, 231 | "total_rss": 40820736, 232 | "total_rss_huge": 0, 233 | "total_unevictable": 0, 234 | "total_writeback": 0, 235 | "unevictable": 0, 236 | "writeback": 0 237 | }, 238 | "limit": 9223372036854771712 239 | }, 240 | "name": "prometheus", 241 | "id": "bae32def0ab64f06818e8862e58f8d6d-1819985369", 242 | "networks": { 243 | "eth1": { 244 | "rx_bytes": 129045374, 245 | "rx_packets": 88933, 246 | "rx_errors": 0, 247 | "rx_dropped": 0, 248 | "tx_bytes": 347839, 249 | "tx_packets": 3501, 250 | "tx_errors": 0, 251 | "tx_dropped": 0 252 | } 253 | }, 254 | "network_rate_stats": { 255 | "rx_bytes_per_sec": 2464.6679803462657, 256 | "tx_bytes_per_sec": 1173.289548516495 257 | } 258 | }, 259 | "bae32def0ab64f06818e8862e58f8d6d-4159844948": { 260 | "read": "2025-02-27T05:22:49.406170373Z", 261 | "preread": "2025-02-27T05:22:39.406479661Z", 262 | "pids_stats": {}, 263 | "blkio_stats": { 264 | "io_service_bytes_recursive": [ 265 | { 266 | "major": 259, 267 | "minor": 1, 268 | "op": "Read", 269 | "value": 28639232 270 | }, 271 | { 272 | "major": 259, 273 | "minor": 1, 274 | "op": "Write", 275 | "value": 0 276 | }, 277 | { 278 | "major": 259, 279 | "minor": 1, 280 | "op": "Sync", 281 | "value": 28639232 282 | }, 283 | { 284 | "major": 259, 285 | "minor": 1, 286 | "op": "Async", 287 | "value": 0 288 | }, 289 | { 290 | "major": 259, 291 | "minor": 1, 292 | "op": "Discard", 293 | "value": 0 294 | }, 295 | { 296 | "major": 259, 297 | "minor": 1, 298 | "op": "Total", 299 | "value": 28639232 300 | }, 301 | { 302 | "major": 259, 303 | "minor": 0, 304 | "op": "Read", 305 | "value": 14655488 306 | }, 307 | { 308 | "major": 259, 309 | "minor": 0, 310 | "op": "Write", 311 | "value": 4096 312 | }, 313 | { 314 | "major": 259, 315 | "minor": 0, 316 | "op": "Sync", 317 | "value": 14655488 318 | }, 319 | { 320 | "major": 259, 321 | "minor": 0, 322 | "op": "Async", 323 | "value": 4096 324 | }, 325 | { 326 | "major": 259, 327 | "minor": 0, 328 | "op": "Discard", 329 | "value": 0 330 | }, 331 | { 332 | "major": 259, 333 | "minor": 0, 334 | "op": "Total", 335 | "value": 14659584 336 | } 337 | ], 338 | "io_serviced_recursive": [ 339 | { 340 | "major": 259, 341 | "minor": 1, 342 | "op": "Read", 343 | "value": 327 344 | }, 345 | { 346 | "major": 259, 347 | "minor": 1, 348 | "op": "Write", 349 | "value": 0 350 | }, 351 | { 352 | "major": 259, 353 | "minor": 1, 354 | "op": "Sync", 355 | "value": 327 356 | }, 357 | { 358 | "major": 259, 359 | "minor": 1, 360 | "op": "Async", 361 | "value": 0 362 | }, 363 | { 364 | "major": 259, 365 | "minor": 1, 366 | "op": "Discard", 367 | "value": 0 368 | }, 369 | { 370 | "major": 259, 371 | "minor": 1, 372 | "op": "Total", 373 | "value": 327 374 | }, 375 | { 376 | "major": 259, 377 | "minor": 0, 378 | "op": "Read", 379 | "value": 157 380 | }, 381 | { 382 | "major": 259, 383 | "minor": 0, 384 | "op": "Write", 385 | "value": 1 386 | }, 387 | { 388 | "major": 259, 389 | "minor": 0, 390 | "op": "Sync", 391 | "value": 157 392 | }, 393 | { 394 | "major": 259, 395 | "minor": 0, 396 | "op": "Async", 397 | "value": 1 398 | }, 399 | { 400 | "major": 259, 401 | "minor": 0, 402 | "op": "Discard", 403 | "value": 0 404 | }, 405 | { 406 | "major": 259, 407 | "minor": 0, 408 | "op": "Total", 409 | "value": 158 410 | } 411 | ], 412 | "io_queue_recursive": [], 413 | "io_service_time_recursive": [], 414 | "io_wait_time_recursive": [], 415 | "io_merged_recursive": [], 416 | "io_time_recursive": [], 417 | "sectors_recursive": [] 418 | }, 419 | "num_procs": 0, 420 | "storage_stats": {}, 421 | "cpu_stats": { 422 | "cpu_usage": { 423 | "total_usage": 322633383, 424 | "percpu_usage": [ 425 | 138347736, 426 | 184285647 427 | ], 428 | "usage_in_kernelmode": 50000000, 429 | "usage_in_usermode": 180000000 430 | }, 431 | "system_cpu_usage": 2121630000000, 432 | "online_cpus": 2, 433 | "throttling_data": { 434 | "periods": 0, 435 | "throttled_periods": 0, 436 | "throttled_time": 0 437 | } 438 | }, 439 | "precpu_stats": { 440 | "cpu_usage": { 441 | "total_usage": 242770940, 442 | "percpu_usage": [ 443 | 104627034, 444 | 138143906 445 | ], 446 | "usage_in_kernelmode": 30000000, 447 | "usage_in_usermode": 140000000 448 | }, 449 | "system_cpu_usage": 2101780000000, 450 | "online_cpus": 2, 451 | "throttling_data": { 452 | "periods": 0, 453 | "throttled_periods": 0, 454 | "throttled_time": 0 455 | } 456 | }, 457 | "memory_stats": { 458 | "usage": 84111360, 459 | "max_usage": 84238336, 460 | "stats": { 461 | "active_anon": 0, 462 | "active_file": 3379200, 463 | "cache": 42442752, 464 | "dirty": 0, 465 | "hierarchical_memory_limit": 536870912, 466 | "hierarchical_memsw_limit": 9223372036854771712, 467 | "inactive_anon": 39469056, 468 | "inactive_file": 39100416, 469 | "mapped_file": 33927168, 470 | "pgfault": 17358, 471 | "pgmajfault": 297, 472 | "pgpgin": 23430, 473 | "pgpgout": 3367, 474 | "rss": 39469056, 475 | "rss_huge": 0, 476 | "total_active_anon": 0, 477 | "total_active_file": 3379200, 478 | "total_cache": 42442752, 479 | "total_dirty": 0, 480 | "total_inactive_anon": 39469056, 481 | "total_inactive_file": 39100416, 482 | "total_mapped_file": 33927168, 483 | "total_pgfault": 17358, 484 | "total_pgmajfault": 297, 485 | "total_pgpgin": 23430, 486 | "total_pgpgout": 3367, 487 | "total_rss": 39469056, 488 | "total_rss_huge": 0, 489 | "total_unevictable": 0, 490 | "total_writeback": 0, 491 | "unevictable": 0, 492 | "writeback": 0 493 | }, 494 | "limit": 9223372036854771712 495 | }, 496 | "name": "ecs-exporter", 497 | "id": "bae32def0ab64f06818e8862e58f8d6d-4159844948", 498 | "networks": { 499 | "eth1": { 500 | "rx_bytes": 129046293, 501 | "rx_packets": 88938, 502 | "rx_errors": 0, 503 | "rx_dropped": 0, 504 | "tx_bytes": 348223, 505 | "tx_packets": 3507, 506 | "tx_errors": 0, 507 | "tx_dropped": 0 508 | } 509 | }, 510 | "network_rate_stats": { 511 | "rx_bytes_per_sec": 2556.879064581499, 512 | "tx_bytes_per_sec": 1211.8374728018853 513 | } 514 | } 515 | } 516 | -------------------------------------------------------------------------------- /ecscollector/testdata/snapshots/ec2_metrics.txt: -------------------------------------------------------------------------------- 1 | # HELP ecs_container_cpu_usage_seconds_total Cumulative total container CPU usage in seconds. 2 | # TYPE ecs_container_cpu_usage_seconds_total counter 3 | ecs_container_cpu_usage_seconds_total{container_name="ecs-exporter"} 0.331125 4 | ecs_container_cpu_usage_seconds_total{container_name="prometheus"} 0.56606 5 | # HELP ecs_container_memory_limit_bytes Configured container memory limit in bytes, set from the container-level limit in the task definition if any, otherwise the task-level limit. 6 | # TYPE ecs_container_memory_limit_bytes gauge 7 | ecs_container_memory_limit_bytes{container_name="ecs-exporter"} 2.68435456e+08 8 | ecs_container_memory_limit_bytes{container_name="prometheus"} 2.68435456e+08 9 | # HELP ecs_container_memory_page_cache_size_bytes Current container memory page cache size in bytes. This is not a subset of used bytes. 10 | # TYPE ecs_container_memory_page_cache_size_bytes gauge 11 | ecs_container_memory_page_cache_size_bytes{container_name="ecs-exporter"} 0 12 | ecs_container_memory_page_cache_size_bytes{container_name="prometheus"} 0 13 | # HELP ecs_container_memory_usage_bytes Current container memory usage in bytes. 14 | # TYPE ecs_container_memory_usage_bytes gauge 15 | ecs_container_memory_usage_bytes{container_name="ecs-exporter"} 6.524928e+07 16 | ecs_container_memory_usage_bytes{container_name="prometheus"} 6.0981248e+07 17 | # HELP ecs_network_receive_bytes_total Cumulative total size of network packets received in bytes. 18 | # TYPE ecs_network_receive_bytes_total counter 19 | ecs_network_receive_bytes_total{interface="eth0"} 45368 20 | # HELP ecs_network_receive_errors_total Cumulative total count of network errors in receiving. 21 | # TYPE ecs_network_receive_errors_total counter 22 | ecs_network_receive_errors_total{interface="eth0"} 0 23 | # HELP ecs_network_receive_packets_dropped_total Cumulative total count of network packets dropped in receiving. 24 | # TYPE ecs_network_receive_packets_dropped_total counter 25 | ecs_network_receive_packets_dropped_total{interface="eth0"} 0 26 | # HELP ecs_network_receive_packets_total Cumulative total count of network packets received. 27 | # TYPE ecs_network_receive_packets_total counter 28 | ecs_network_receive_packets_total{interface="eth0"} 132 29 | # HELP ecs_network_transmit_bytes_total Cumulative total size of network packets transmitted in bytes. 30 | # TYPE ecs_network_transmit_bytes_total counter 31 | ecs_network_transmit_bytes_total{interface="eth0"} 13532 32 | # HELP ecs_network_transmit_errors_total Cumulative total count of network errors in transmit. 33 | # TYPE ecs_network_transmit_errors_total counter 34 | ecs_network_transmit_errors_total{interface="eth0"} 0 35 | # HELP ecs_network_transmit_packets_dropped_total Cumulative total count of network packets dropped in transmit. 36 | # TYPE ecs_network_transmit_packets_dropped_total counter 37 | ecs_network_transmit_packets_dropped_total{interface="eth0"} 0 38 | # HELP ecs_network_transmit_packets_total Cumulative total count of network packets transmitted. 39 | # TYPE ecs_network_transmit_packets_total counter 40 | ecs_network_transmit_packets_total{interface="eth0"} 118 41 | # HELP ecs_task_image_pull_start_timestamp_seconds The time at which the task started pulling docker images for its containers. 42 | # TYPE ecs_task_image_pull_start_timestamp_seconds gauge 43 | ecs_task_image_pull_start_timestamp_seconds 1.7406329923325953e+09 44 | # HELP ecs_task_image_pull_stop_timestamp_seconds The time at which the task stopped (i.e. completed) pulling docker images for its containers. 45 | # TYPE ecs_task_image_pull_stop_timestamp_seconds gauge 46 | ecs_task_image_pull_stop_timestamp_seconds 1.7406330012060723e+09 47 | # HELP ecs_task_metadata_info ECS task metadata, sourced from the task metadata endpoint version 4. 48 | # TYPE ecs_task_metadata_info gauge 49 | ecs_task_metadata_info{availability_zone="us-east-1a",cluster="prom-ecs-exporter-sandbox",desired_status="RUNNING",family="prom-ecs-exporter-sandbox-main-ec2",known_status="RUNNING",launch_type="EC2",revision="13",task_arn="arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/506f22fab0414cde856201584703fed9"} 1 50 | -------------------------------------------------------------------------------- /ecscollector/testdata/snapshots/fargate_metrics.txt: -------------------------------------------------------------------------------- 1 | # HELP ecs_container_cpu_usage_seconds_total Cumulative total container CPU usage in seconds. 2 | # TYPE ecs_container_cpu_usage_seconds_total counter 3 | ecs_container_cpu_usage_seconds_total{container_name="ecs-exporter"} 0.322633383 4 | ecs_container_cpu_usage_seconds_total{container_name="prometheus"} 0.9324394920000001 5 | # HELP ecs_container_memory_limit_bytes Configured container memory limit in bytes, set from the container-level limit in the task definition if any, otherwise the task-level limit. 6 | # TYPE ecs_container_memory_limit_bytes gauge 7 | ecs_container_memory_limit_bytes{container_name="ecs-exporter"} 5.36870912e+08 8 | ecs_container_memory_limit_bytes{container_name="prometheus"} 5.36870912e+08 9 | # HELP ecs_container_memory_page_cache_size_bytes Current container memory page cache size in bytes. This is not a subset of used bytes. 10 | # TYPE ecs_container_memory_page_cache_size_bytes gauge 11 | ecs_container_memory_page_cache_size_bytes{container_name="ecs-exporter"} 4.2442752e+07 12 | ecs_container_memory_page_cache_size_bytes{container_name="prometheus"} 8.5426176e+07 13 | # HELP ecs_container_memory_usage_bytes Current container memory usage in bytes. 14 | # TYPE ecs_container_memory_usage_bytes gauge 15 | ecs_container_memory_usage_bytes{container_name="ecs-exporter"} 8.411136e+07 16 | ecs_container_memory_usage_bytes{container_name="prometheus"} 1.27934464e+08 17 | # HELP ecs_network_receive_bytes_total Cumulative total size of network packets received in bytes. 18 | # TYPE ecs_network_receive_bytes_total counter 19 | ecs_network_receive_bytes_total{interface="eth1"} 1.29046293e+08 20 | # HELP ecs_network_receive_errors_total Cumulative total count of network errors in receiving. 21 | # TYPE ecs_network_receive_errors_total counter 22 | ecs_network_receive_errors_total{interface="eth1"} 0 23 | # HELP ecs_network_receive_packets_dropped_total Cumulative total count of network packets dropped in receiving. 24 | # TYPE ecs_network_receive_packets_dropped_total counter 25 | ecs_network_receive_packets_dropped_total{interface="eth1"} 0 26 | # HELP ecs_network_receive_packets_total Cumulative total count of network packets received. 27 | # TYPE ecs_network_receive_packets_total counter 28 | ecs_network_receive_packets_total{interface="eth1"} 88938 29 | # HELP ecs_network_transmit_bytes_total Cumulative total size of network packets transmitted in bytes. 30 | # TYPE ecs_network_transmit_bytes_total counter 31 | ecs_network_transmit_bytes_total{interface="eth1"} 348223 32 | # HELP ecs_network_transmit_errors_total Cumulative total count of network errors in transmit. 33 | # TYPE ecs_network_transmit_errors_total counter 34 | ecs_network_transmit_errors_total{interface="eth1"} 0 35 | # HELP ecs_network_transmit_packets_dropped_total Cumulative total count of network packets dropped in transmit. 36 | # TYPE ecs_network_transmit_packets_dropped_total counter 37 | ecs_network_transmit_packets_dropped_total{interface="eth1"} 0 38 | # HELP ecs_network_transmit_packets_total Cumulative total count of network packets transmitted. 39 | # TYPE ecs_network_transmit_packets_total counter 40 | ecs_network_transmit_packets_total{interface="eth1"} 3507 41 | # HELP ecs_task_cpu_limit_vcpus Configured task CPU limit in vCPUs (1 vCPU = 1024 CPU units). This is optional when running on EC2; if no limit is set, this metric has no value. 42 | # TYPE ecs_task_cpu_limit_vcpus gauge 43 | ecs_task_cpu_limit_vcpus 0.25 44 | # HELP ecs_task_ephemeral_storage_allocated_bytes Configured Fargate task ephemeral storage allocated size in bytes. 45 | # TYPE ecs_task_ephemeral_storage_allocated_bytes gauge 46 | ecs_task_ephemeral_storage_allocated_bytes 2.1491613696e+10 47 | # HELP ecs_task_ephemeral_storage_used_bytes Current Fargate task ephemeral storage usage in bytes. 48 | # TYPE ecs_task_ephemeral_storage_used_bytes gauge 49 | ecs_task_ephemeral_storage_used_bytes 4.47741952e+08 50 | # HELP ecs_task_image_pull_start_timestamp_seconds The time at which the task started pulling docker images for its containers. 51 | # TYPE ecs_task_image_pull_start_timestamp_seconds gauge 52 | ecs_task_image_pull_start_timestamp_seconds 1.7406327637144377e+09 53 | # HELP ecs_task_image_pull_stop_timestamp_seconds The time at which the task stopped (i.e. completed) pulling docker images for its containers. 54 | # TYPE ecs_task_image_pull_stop_timestamp_seconds gauge 55 | ecs_task_image_pull_stop_timestamp_seconds 1.7406327785991266e+09 56 | # HELP ecs_task_memory_limit_bytes Configured task memory limit in bytes. This is optional when running on EC2; if no limit is set, this metric has no value. 57 | # TYPE ecs_task_memory_limit_bytes gauge 58 | ecs_task_memory_limit_bytes 5.36870912e+08 59 | # HELP ecs_task_metadata_info ECS task metadata, sourced from the task metadata endpoint version 4. 60 | # TYPE ecs_task_metadata_info gauge 61 | ecs_task_metadata_info{availability_zone="us-east-1a",cluster="arn:aws:ecs:us-east-1:829490980523:cluster/prom-ecs-exporter-sandbox",desired_status="RUNNING",family="prom-ecs-exporter-sandbox-main-fargate",known_status="RUNNING",launch_type="FARGATE",revision="9",task_arn="arn:aws:ecs:us-east-1:829490980523:task/prom-ecs-exporter-sandbox/bae32def0ab64f06818e8862e58f8d6d"} 1 62 | -------------------------------------------------------------------------------- /ecsmetadata/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package ecsmetadata queries ECS Metadata Server for ECS task metrics. 15 | // This package is currently experimental and is subject to change. 16 | package ecsmetadata 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | 27 | tmdsv4 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v4/state" 28 | ) 29 | 30 | type Client struct { 31 | // HTTClient is the client to use when making HTTP requests when set. 32 | HTTPClient *http.Client 33 | 34 | // metadata server endpoint 35 | endpoint string 36 | } 37 | 38 | // NewClient returns a new Client. endpoint is the metadata server endpoint. 39 | func NewClient(endpoint string) *Client { 40 | return &Client{ 41 | HTTPClient: &http.Client{}, 42 | endpoint: endpoint, 43 | } 44 | } 45 | 46 | // NewClientFromEnvironment is like NewClient but endpoint 47 | // is discovered from the environment. 48 | func NewClientFromEnvironment() (*Client, error) { 49 | const endpointEnv = "ECS_CONTAINER_METADATA_URI_V4" 50 | endpoint := os.Getenv(endpointEnv) 51 | if endpoint == "" { 52 | return nil, fmt.Errorf("%s is not set; not running on ECS?", endpointEnv) 53 | } 54 | _, err := url.Parse(endpoint) 55 | if err != nil { 56 | return nil, fmt.Errorf("can't parse %s as URL: %w", endpointEnv, err) 57 | } 58 | return NewClient(endpoint), nil 59 | } 60 | 61 | func (c *Client) RetrieveTaskStats(ctx context.Context) (map[string]*tmdsv4.StatsResponse, error) { 62 | // https://github.com/aws/amazon-ecs-agent/blob/cf8c7a6b65043c550533f330b10aef6d0a342214/agent/handlers/v4/tmdsstate.go#L202 63 | out := make(map[string]*tmdsv4.StatsResponse) 64 | err := c.request(ctx, c.endpoint+"/task/stats", &out) 65 | return out, err 66 | } 67 | 68 | func (c *Client) RetrieveTaskMetadata(ctx context.Context) (*tmdsv4.TaskResponse, error) { 69 | // https://github.com/aws/amazon-ecs-agent/blob/cf8c7a6b65043c550533f330b10aef6d0a342214/agent/handlers/v4/tmdsstate.go#L174 70 | // 71 | // Note that EC2 and Fargate return slightly different task metadata 72 | // responses. At time of writing, as per the documentation, only EC2 has `ServiceName`, 73 | // while only Fargate has `EphemeralStorageMetrics`, `ClockDrift`, and 74 | // `Containers[].Snapshotter`. Ref: 75 | // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-fargate-response.html 76 | // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-response.html 77 | // 78 | // But `TaskResponse` is the _union_ of these two responses. It has all the 79 | // fields. 80 | var out tmdsv4.TaskResponse 81 | err := c.request(ctx, c.endpoint+"/task", &out) 82 | return &out, err 83 | } 84 | 85 | func (c *Client) request(ctx context.Context, uri string, out interface{}) error { 86 | req, err := http.NewRequest("GET", uri, nil) 87 | if err != nil { 88 | return err 89 | } 90 | req = req.WithContext(ctx) 91 | resp, err := c.HTTPClient.Do(req) 92 | if err != nil { 93 | return err 94 | } 95 | defer resp.Body.Close() 96 | 97 | body, err := io.ReadAll(resp.Body) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 103 | return fmt.Errorf("%q: %s %s: %q", uri, resp.Proto, resp.Status, string(body)) 104 | } 105 | 106 | return json.Unmarshal(body, out) 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus-community/ecs_exporter 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.4.0 7 | github.com/aws/amazon-ecs-agent/ecs-agent v0.0.0-20250311191058-43b89f06b96f 8 | github.com/docker/docker v27.3.1+incompatible 9 | github.com/prometheus/client_golang v1.22.0 10 | github.com/prometheus/common v0.63.0 11 | github.com/prometheus/exporter-toolkit v0.13.2 12 | ) 13 | 14 | require ( 15 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 16 | github.com/aws/aws-sdk-go v1.51.3 // indirect 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect 20 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 21 | github.com/docker/go-connections v0.4.0 // indirect 22 | github.com/docker/go-units v0.5.0 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/gorilla/mux v1.8.0 // indirect 25 | github.com/jmespath/go-jmespath v0.4.0 // indirect 26 | github.com/jpillora/backoff v1.0.0 // indirect 27 | github.com/kylelemons/godebug v1.1.0 // indirect 28 | github.com/mdlayher/socket v0.4.1 // indirect 29 | github.com/mdlayher/vsock v1.2.1 // indirect 30 | github.com/moby/docker-image-spec v1.3.1 // indirect 31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 32 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 33 | github.com/opencontainers/go-digest v1.0.0 // indirect 34 | github.com/opencontainers/image-spec v1.1.0-rc3 // indirect 35 | github.com/pkg/errors v0.9.1 // indirect 36 | github.com/prometheus/client_model v0.6.1 // indirect 37 | github.com/prometheus/procfs v0.15.1 // indirect 38 | github.com/vishvananda/netlink v1.2.1-beta.2 // indirect 39 | github.com/vishvananda/netns v0.0.4 // indirect 40 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 41 | golang.org/x/crypto v0.33.0 // indirect 42 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 43 | golang.org/x/net v0.35.0 // indirect 44 | golang.org/x/oauth2 v0.25.0 // indirect 45 | golang.org/x/sync v0.11.0 // indirect 46 | golang.org/x/sys v0.30.0 // indirect 47 | golang.org/x/text v0.22.0 // indirect 48 | google.golang.org/protobuf v1.36.5 // indirect 49 | gopkg.in/yaml.v2 v2.4.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 2 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 3 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 4 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 5 | github.com/aws/amazon-ecs-agent/ecs-agent v0.0.0-20250311191058-43b89f06b96f h1:DJrZ85TxEVYi6K2Ao1UCzI98oyFzu2XpmpSjiHtmV8Q= 6 | github.com/aws/amazon-ecs-agent/ecs-agent v0.0.0-20250311191058-43b89f06b96f/go.mod h1:Myn1TSfJvFHEftmqtT3aw4CYktyuLijgKw8LOteQLno= 7 | github.com/aws/aws-sdk-go v1.51.3 h1:OqSyEXcJwf/XhZNVpMRgKlLA9nmbo5X8dwbll4RWxq8= 8 | github.com/aws/aws-sdk-go v1.51.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 12 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= 14 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= 15 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 16 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= 21 | github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 22 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 23 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 24 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 25 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 26 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 27 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 28 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 29 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 30 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 31 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 32 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 33 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 34 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 35 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 36 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 37 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 38 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 39 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 40 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 41 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 42 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 43 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 44 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 45 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 46 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 47 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 48 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 49 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 50 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 51 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 52 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 53 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= 54 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= 55 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 56 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 59 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 60 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 61 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 62 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 63 | github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= 64 | github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= 65 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 66 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 70 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 71 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 72 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 73 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 74 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 75 | github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= 76 | github.com/prometheus/exporter-toolkit v0.13.2/go.mod h1:tCqnfx21q6qN1KA4U3Bfb8uWzXfijIrJz3/kTIqMV7g= 77 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 78 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 79 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 80 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 81 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 82 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 83 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 84 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 85 | github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= 86 | github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= 87 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 88 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 89 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 90 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 91 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 92 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 93 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 94 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 95 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 96 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 97 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 98 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 99 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 100 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 101 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 102 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 103 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 107 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 108 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 109 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 110 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 111 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 115 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 116 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 117 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 122 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 123 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 125 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 126 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 129 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 130 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 131 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 136 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 137 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 139 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 140 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 141 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 143 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 144 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 145 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 146 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 147 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 148 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | "os" 20 | 21 | "github.com/alecthomas/kingpin/v2" 22 | "github.com/prometheus/client_golang/prometheus" 23 | promcollectors "github.com/prometheus/client_golang/prometheus/collectors" 24 | versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" 25 | "github.com/prometheus/client_golang/prometheus/promhttp" 26 | "github.com/prometheus/common/promslog" 27 | "github.com/prometheus/common/promslog/flag" 28 | "github.com/prometheus/common/version" 29 | "github.com/prometheus/exporter-toolkit/web" 30 | "github.com/prometheus/exporter-toolkit/web/kingpinflag" 31 | 32 | "github.com/prometheus-community/ecs_exporter/ecscollector" 33 | "github.com/prometheus-community/ecs_exporter/ecsmetadata" 34 | ) 35 | 36 | const exporter = "ecs_exporter" 37 | 38 | func main() { 39 | promslogConfig := &promslog.Config{} 40 | flag.AddFlags(kingpin.CommandLine, promslogConfig) 41 | 42 | metricsPath := kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() 43 | disableExporterMetrics := kingpin.Flag( 44 | "web.disable-exporter-metrics", 45 | "Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).", 46 | ).Bool() 47 | toolkitFlags := kingpinflag.AddFlags(kingpin.CommandLine, ":9779") 48 | 49 | registry := prometheus.NewRegistry() 50 | registry.MustRegister(versioncollector.NewCollector(exporter)) 51 | kingpin.Version(version.Print(exporter)) 52 | 53 | kingpin.HelpFlag.Short('h') 54 | kingpin.Parse() 55 | 56 | logger := promslog.New(promslogConfig) 57 | 58 | client, err := ecsmetadata.NewClientFromEnvironment() 59 | if err != nil { 60 | logger.Error("Error creating client", "error", err) 61 | os.Exit(1) 62 | } 63 | registry.MustRegister(ecscollector.NewCollector(client, logger)) 64 | 65 | handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 66 | if !*disableExporterMetrics { 67 | registry.MustRegister( 68 | promcollectors.NewProcessCollector(promcollectors.ProcessCollectorOpts{}), 69 | promcollectors.NewGoCollector(), 70 | ) 71 | handler = promhttp.InstrumentMetricHandler(registry, handler) 72 | } 73 | 74 | http.Handle(*metricsPath, handler) 75 | if *metricsPath != "/" && *metricsPath != "" { 76 | landingConfig := web.LandingConfig{ 77 | Name: exporter, 78 | Description: "Prometheus Exporter for ECS", 79 | Version: version.Info(), 80 | Links: []web.LandingLinks{ 81 | { 82 | Address: *metricsPath, 83 | Text: "Metrics", 84 | }, 85 | }, 86 | } 87 | landingPage, err := web.NewLandingPage(landingConfig) 88 | if err != nil { 89 | logger.Error("Error creating landing page", "err", err) 90 | os.Exit(1) 91 | } 92 | http.Handle("/", landingPage) 93 | } 94 | 95 | http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 96 | w.WriteHeader(http.StatusOK) 97 | fmt.Fprint(w, "ok") 98 | }) 99 | 100 | srv := &http.Server{} 101 | if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { 102 | logger.Error("Error starting server", "err", err) 103 | os.Exit(1) 104 | } 105 | 106 | } 107 | --------------------------------------------------------------------------------