├── .codeclimate.yml ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── story.md ├── ct.yaml ├── renovate.json5 └── workflows │ ├── automated_release.yaml │ ├── load_test.yml │ ├── nightly.yaml │ ├── push_pr.yml │ ├── release-chart.yaml │ ├── release-integration.yml │ ├── repolinter.yml │ └── security.yml ├── .gitignore ├── .goreleaser-fips.yml ├── .goreleaser.yml ├── .trivyignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile.dev ├── Dockerfile.release ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── THIRD_PARTY_NOTICES.md ├── build ├── ci.mk ├── nix │ └── fix_archives.sh ├── release.mk ├── upload_artifacts_gh.sh └── windows │ ├── fix_archives.sh │ ├── set_exe_properties.sh │ ├── unit_tests.ps1 │ └── versioninfo.json.template ├── charts ├── load-test-environment │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── deployment.yaml │ │ └── service.yaml │ └── values.yaml └── nri-prometheus │ ├── .helmignore │ ├── Chart.lock │ ├── Chart.yaml │ ├── README.md │ ├── README.md.gotmpl │ ├── ci │ ├── test-lowdatamode-values.yaml │ ├── test-override-global-lowdatamode.yaml │ └── test-values.yaml │ ├── static │ └── lowdatamodedefaults.yaml │ ├── templates │ ├── _helpers.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── configmap.yaml │ ├── deployment.yaml │ ├── secret.yaml │ └── serviceaccount.yaml │ ├── tests │ ├── configmap_test.yaml │ ├── deployment_test.yaml │ └── labels_test.yaml │ └── values.yaml ├── cmd ├── k8s-target-retriever │ └── main.go └── nri-prometheus │ ├── config.go │ ├── config_test.go │ ├── fips.go │ ├── main.go │ └── testdata │ └── config-with-legacy-entity-definitions.yaml ├── configs └── nri-prometheus-config.yml.sample ├── deploy └── local.yaml.example ├── go.mod ├── go.sum ├── internal ├── cmd │ └── scraper │ │ ├── scraper.go │ │ ├── scraper_test.go │ │ └── testData │ │ └── testData.prometheus ├── integration │ ├── bounded_harvester.go │ ├── bounded_harvester_test.go │ ├── emitter.go │ ├── emitter_test.go │ ├── fetcher.go │ ├── fetcher_test.go │ ├── harvester_decorator.go │ ├── helpers_test.go │ ├── infra_sdk_emitter.go │ ├── infra_sdk_emitter_test.go │ ├── integration.go │ ├── integration_test.go │ ├── metrics.go │ ├── roundtripper.go │ ├── roundtripper_test.go │ ├── rules.go │ ├── rules_test.go │ ├── scrape_test.go │ ├── telemetry_sdk_emitter.go │ ├── telemetry_sdk_emitter_test.go │ └── test │ │ └── cadvisor.txt ├── pkg │ ├── endpoints │ │ ├── endpoints.go │ │ ├── endpoints_test.go │ │ ├── fixed.go │ │ ├── kubernetes.go │ │ ├── kubernetes_test.go │ │ ├── metrics.go │ │ └── self.go │ ├── labels │ │ ├── labels.go │ │ └── labels_test.go │ └── prometheus │ │ ├── metrics.go │ │ ├── prometheus.go │ │ ├── prometheus_test.go │ │ └── testdata │ │ ├── redis-metrics │ │ └── simple-metrics └── retry │ └── retry.go ├── load-test ├── README.md ├── load_test.go ├── load_test.sh └── mockexporter │ ├── Dockerfile │ ├── load_test_average_sample.data │ ├── load_test_big_sample.data │ ├── load_test_small_sample.data │ └── mockexporter.go └── skaffold.yaml /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | golint: 4 | enabled: true 5 | config: 6 | min_confidence: 0.9 7 | gofmt: 8 | enabled: true 9 | govet: 10 | enabled: true 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.go] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence. 6 | 7 | * @newrelic/ohai 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ^^ Provide a general summary of the issue in the title above. ^^ 11 | 12 | ## Description 13 | Describe the problem you're encountering. 14 | TIP: Do NOT share sensitive information, whether personal, proprietary, or otherwise! 15 | 16 | ## Expected Behavior 17 | Tell us what you expected to happen. 18 | 19 | ## [Troubleshooting](https://discuss.newrelic.com/t/troubleshooting-frameworks/108787) or [NR Diag](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/troubleshooting/new-relic-diagnostics) results 20 | Provide any other relevant log data. 21 | TIP: Scrub logs and diagnostic information for sensitive information 22 | 23 | ## Steps to Reproduce 24 | Please be as specific as possible. 25 | TIP: Link a sample application that demonstrates the issue. 26 | 27 | ## Your Environment 28 | Include as many relevant details about your environment as possible including the running version of New Relic software and any relevant configurations. 29 | 30 | ## Additional context 31 | Add any other context about the problem here. For example, relevant community posts or support tickets. 32 | 33 | ## For Maintainers Only or Hero Triaging this bug 34 | *Suggested Priority (P1,P2,P3,P4,P5):* 35 | *Suggested T-Shirt size (S, M, L, XL, Unknown):* 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Troubleshooting 4 | url: https://github.com/newrelic/nri-prometheus/blob/main/README.md#support 5 | about: Check out the README for troubleshooting directions 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Story 3 | about: Issue describing development work to fulfill a feature request 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | priority: '' 8 | --- 9 | ### Description 10 | _What's the goal of this unit of work? What is included? What isn't included?_ 11 | 12 | ### Acceptance Criteria 13 | _What tasks need to be accomplished to achieve the goal?_ 14 | 15 | ### Design Consideration/Limitations 16 | _Why is this the route we should take to achieve our goal?_ 17 | _What can't be achieved within this story?_ 18 | 19 | ### Dependencies 20 | _Do any other teams or parts of the New Relic product need to be considered?_ 21 | _Some common areas: UI, collector, documentation_ 22 | 23 | ### Additional context 24 | _What else should we know about this story that might not fit into the other categories?_ 25 | 26 | ### Estimates 27 | _Please provide initial t-shirt size. S = 1-3 days, M = 3-5 days (1 week), L = 1-2 weeks (1 sprint)_ 28 | -------------------------------------------------------------------------------- /.github/ct.yaml: -------------------------------------------------------------------------------- 1 | # Chart linter defaults to `master` branch so we need to specify this as the default branch 2 | # or `cl` will fail with a not-so-helpful error that says: 3 | # "Error linting charts: Error identifying charts to process: Error running process: exit status 128" 4 | target-branch: main 5 | 6 | # Needed to build chart with common library dependency. 7 | chart-repos: 8 | - newrelic=https://helm-charts.newrelic.com 9 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>newrelic/coreint-automation:renovate-base.json5" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/automated_release.yaml: -------------------------------------------------------------------------------- 1 | name: Automated release creation 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 12 * * 3" 7 | 8 | jobs: 9 | release_management: 10 | uses: newrelic/coreint-automation/.github/workflows/reusable_release_automation.yaml@v3 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/load_test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Load Tests 8 | jobs: 9 | load_tests: 10 | if: ${{ ! contains(github.event.pull_request.labels.*.name, 'ci/skip-load-test') }} 11 | name: Load Tests 12 | runs-on: ubuntu-22.04 # Read the comment below why this is not set to `latest`. 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version-file: "go.mod" 19 | - name: Installing dependencies 20 | run: | 21 | sudo wget -O /usr/local/bin/skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 22 | sudo chmod +x /usr/local/bin/skaffold 23 | - name: Setup Minikube 24 | uses: manusa/actions-setup-minikube@v2.14.0 25 | with: 26 | minikube version: v1.30.1 27 | kubernetes version: v1.25.6 28 | driver: docker 29 | github token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Run load tests 32 | env: 33 | NEWRELIC_LICENSE: ${{ secrets.NEWRELIC_LICENSE }} 34 | run : | 35 | source ./load-test/load_test.sh 36 | runLoadTest 37 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: Nightly build 2 | on: 3 | schedule: 4 | - cron: "0 3 * * *" 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | INTEGRATION: "prometheus" 11 | ORIGINAL_REPO_NAME: 'newrelic/nri-prometheus' 12 | TAG: nightly 13 | TAG_SUFFIX: "-nightly" 14 | 15 | jobs: 16 | nightly: 17 | uses: newrelic/coreint-automation/.github/workflows/reusable_nightly.yaml@v3 18 | secrets: 19 | docker_username: ${{ secrets.FSI_DOCKERHUB_USERNAME }} 20 | docker_password: ${{ secrets.FSI_DOCKERHUB_TOKEN }} 21 | slack_channel: ${{ secrets.COREINT_SLACK_CHANNEL }} 22 | slack_token: ${{ secrets.COREINT_SLACK_TOKEN }} 23 | with: 24 | docker_image: newrelic/nri-prometheus 25 | docker_tag: nightly 26 | target_branches: "main" 27 | integration_name: "prometheus" 28 | build_command: make release 29 | setup_qemu: true 30 | setup_buildx: true 31 | setup_go: true 32 | go_version_file: 'go.mod' 33 | trivy_scan: false 34 | generate_packages: true 35 | -------------------------------------------------------------------------------- /.github/workflows/push_pr.yml: -------------------------------------------------------------------------------- 1 | name: Push/PR pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | 10 | env: 11 | TAG: "v0.0.0" # needed for goreleaser windows builds 12 | REPO_FULL_NAME: ${{ github.event.repository.full_name }} 13 | ORIGINAL_REPO_NAME: "newrelic/nri-prometheus" 14 | DOCKER_LOGIN_AVAILABLE: ${{ secrets.OHAI_DOCKER_HUB_ID }} 15 | 16 | jobs: 17 | 18 | chart-lint: 19 | name: Helm chart Lint 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 15 22 | strategy: 23 | matrix: 24 | kubernetes-version: [ "v1.25.6", "v1.26.0" ] 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | - uses: helm/chart-testing-action@v2.7.0 30 | - name: Lint charts 31 | run: ct --config .github/ct.yaml lint --debug 32 | - name: Check for changed installable charts 33 | id: list-changed 34 | run: | 35 | changed=$(ct --config .github/ct.yaml list-changed) 36 | if [[ -n "$changed" ]]; then 37 | echo "::set-output name=changed::true" 38 | fi 39 | - name: Run helm unit tests 40 | if: steps.list-changed.outputs.changed == 'true' 41 | run: | 42 | helm plugin install https://github.com/helm-unittest/helm-unittest 43 | for chart in $(ct --config .github/ct.yaml list-changed); do 44 | if [ -d "$chart/tests/" ]; then 45 | helm unittest $chart 46 | else 47 | echo "No unit tests found for $chart" 48 | fi 49 | done 50 | - name: Setup Minikube 51 | uses: manusa/actions-setup-minikube@v2.14.0 52 | if: steps.list-changed.outputs.changed == 'true' 53 | with: 54 | minikube version: v1.33.1 55 | driver: docker 56 | kubernetes version: ${{ matrix.kubernetes-version }} 57 | github token: ${{ secrets.GITHUB_TOKEN }} 58 | - uses: actions/setup-go@v5 59 | if: steps.list-changed.outputs.changed == 'true' 60 | with: 61 | go-version-file: 'go.mod' 62 | - name: Create image for chart testing 63 | if: steps.list-changed.outputs.changed == 'true' 64 | run: | 65 | export TAG=test 66 | export GOOS=linux 67 | export GOARCH=amd64 68 | make ci/build 69 | # Find the highest versioned amd64 build directory 70 | # Sort numerically on the version suffix (e.g., _v1, _v2) 71 | latest_dir=$(ls -d ./dist/nri-prometheus-nix_linux_amd64* | sort -V | tail -n1) 72 | if [ -z "$latest_dir" ]; then 73 | echo "Error: No matching build directory found" 74 | exit 1 75 | fi 76 | sudo cp "${latest_dir}/nri-prometheus" ./bin/nri-prometheus 77 | DOCKER_BUILDKIT=1 docker build -t e2e/nri-prometheus:test . -f Dockerfile.dev 78 | minikube image load e2e/nri-prometheus:test 79 | - name: Test install charts 80 | if: steps.list-changed.outputs.changed == 'true' 81 | run: ct install --config .github/ct.yaml --debug 82 | - name: Test upgrade charts 83 | if: steps.list-changed.outputs.changed == 'true' 84 | run: ct install --config .github/ct.yaml --debug --upgrade 85 | 86 | static-analysis: 87 | name: Run all static analysis checks 88 | runs-on: ubuntu-latest 89 | steps: 90 | - uses: actions/checkout@v3 91 | - uses: actions/setup-go@v5 92 | with: 93 | go-version-file: 'go.mod' 94 | - uses: newrelic/newrelic-infra-checkers@v1 95 | with: 96 | golangci-lint-config: golangci-lint-limited 97 | - name: golangci-lint 98 | uses: golangci/golangci-lint-action@v6 99 | continue-on-error: ${{ github.event_name != 'pull_request' }} 100 | with: 101 | only-new-issues: true 102 | version: v1.63 103 | - name: Check if CHANGELOG is valid 104 | uses: newrelic/release-toolkit/validate-markdown@v1 105 | 106 | test-nix: 107 | name: Run unit tests on *Nix 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v3 111 | - name: Unit tests 112 | run: make ci/test 113 | 114 | test-windows: 115 | name: Run unit tests on Windows 116 | runs-on: windows-latest 117 | env: 118 | GOPATH: ${{ github.workspace }} 119 | defaults: 120 | run: 121 | working-directory: src/github.com/${{ env.ORIGINAL_REPO_NAME }} 122 | steps: 123 | - name: Checkout 124 | uses: actions/checkout@v3 125 | with: 126 | path: src/github.com/${{ env.ORIGINAL_REPO_NAME }} 127 | - name: Install Go 128 | uses: actions/setup-go@v5 129 | with: 130 | go-version-file: 'src/github.com/${{ env.ORIGINAL_REPO_NAME }}/go.mod' 131 | - name: Running unit tests 132 | shell: pwsh 133 | run: | 134 | .\build\windows\unit_tests.ps1 135 | 136 | # make sure code build in all platforms 137 | build: 138 | name: Build binary for all platforms:arch 139 | runs-on: ubuntu-latest 140 | steps: 141 | - uses: actions/checkout@v3 142 | - name: Build all platforms:arch 143 | run: make ci/build 144 | - name: Check if CHANGELOG is valid 145 | uses: newrelic/release-toolkit/validate-markdown@v1 146 | -------------------------------------------------------------------------------- /.github/workflows/release-chart.yaml: -------------------------------------------------------------------------------- 1 | name: Release prometheus chart 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | # Sometimes chart-releaser might fetch an outdated index.yaml from gh-pages, causing a WAW hazard on the repo 9 | # This job checks the remote file is up to date with the local one on release 10 | validate-gh-pages-index: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | ref: gh-pages 17 | - name: Download remote index file and check equality 18 | run: | 19 | curl -vsSL https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/index.yaml > index.yaml.remote 20 | LOCAL="$(md5sum < index.yaml)" 21 | REMOTE="$(md5sum < index.yaml.remote)" 22 | echo "$LOCAL" = "$REMOTE" 23 | test "$LOCAL" = "$REMOTE" 24 | 25 | chart-release: 26 | runs-on: ubuntu-latest 27 | needs: [ validate-gh-pages-index ] 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Configure Git 34 | run: | 35 | git config user.name "$GITHUB_ACTOR" 36 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 37 | - name: Add newrelic repository 38 | run: helm repo add newrelic https://helm-charts.newrelic.com 39 | - name: Release workload charts 40 | uses: helm/chart-releaser-action@v1.7.0 41 | env: 42 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 43 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | name: Release integration pipeline 2 | 3 | on: 4 | release: 5 | types: 6 | - prereleased 7 | - released 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | container-release: 13 | uses: newrelic/coreint-automation/.github/workflows/reusable_image_release.yaml@v3 14 | with: 15 | original_repo_name: "newrelic/nri-prometheus" 16 | docker_image_name: "newrelic/nri-prometheus" 17 | integration_name: "prometheus" 18 | 19 | run_nix_unit_tests: true 20 | run_windows_unit_tests: true 21 | 22 | release_command_sh: | 23 | export GENERATE_PACKAGES=true 24 | export S3_PATH=${S3_BASE_FOLDER} 25 | if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then 26 | export TAG_SUFFIX="-pre" 27 | else 28 | export TAG_SUFFIX="" 29 | fi 30 | make release 31 | make ci/prerelease-fips 32 | 33 | secrets: 34 | docker_username: ${{ secrets.FSI_DOCKERHUB_USERNAME }} 35 | docker_password: ${{ secrets.FSI_DOCKERHUB_TOKEN }} 36 | bot_token: ${{ secrets.COREINT_BOT_TOKEN }} 37 | slack_channel: ${{ secrets.COREINT_SLACK_CHANNEL }} 38 | slack_token: ${{ secrets.COREINT_SLACK_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/repolinter.yml: -------------------------------------------------------------------------------- 1 | # NOTE: This file should always be named `repolinter.yml` to allow 2 | # workflow_dispatch to work properly 3 | name: Repolinter Action 4 | 5 | # NOTE: This workflow will ONLY check the default branch! 6 | # Currently there is no elegant way to specify the default 7 | # branch in the event filtering, so branches are instead 8 | # filtered in the "Test Default Branch" step. 9 | on: [push, workflow_dispatch] 10 | 11 | jobs: 12 | repolint: 13 | name: Run Repolinter 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Test Default Branch 17 | id: default-branch 18 | uses: actions/github-script@v6.4.1 19 | with: 20 | script: | 21 | const data = await github.rest.repos.get(context.repo) 22 | return data.data && data.data.default_branch === context.ref.split('/').slice(-1)[0] 23 | - name: Checkout Self 24 | if: ${{ steps.default-branch.outputs.result == 'true' }} 25 | uses: actions/checkout@v3 26 | - name: Run Repolinter 27 | if: ${{ steps.default-branch.outputs.result == 'true' }} 28 | uses: newrelic/repolinter-action@v1 29 | with: 30 | config_url: https://raw.githubusercontent.com/newrelic/.github/main/repolinter-rulesets/community-plus.yml 31 | output_type: issue 32 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | 10 | jobs: 11 | trivy: 12 | name: Trivy security scan 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Run Trivy vulnerability scanner in repo mode 19 | uses: aquasecurity/trivy-action@master 20 | if: contains(fromJSON('["push", "pull_request"]'), github.event_name) 21 | with: 22 | scan-type: fs 23 | ignore-unfixed: true 24 | exit-code: 1 25 | severity: 'HIGH,CRITICAL' 26 | skip-dirs: 'tools' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | *.log 5 | tmp/ 6 | bin/ 7 | deploy/local*.yaml 8 | load-test/load_test.results 9 | dist/ 10 | config.yaml 11 | deploy/nri-prometheus.major.yaml 12 | deploy/nri-prometheus.minor.yaml 13 | target/ 14 | .envrc 15 | snyk-monitor-result.json 16 | snyk-result.json 17 | snyk_report.css 18 | snyk_report.html 19 | 20 | # Downloaded chart dependencies 21 | **/charts/*.tgz 22 | 23 | # Release toolkit 24 | CHANGELOG.partial.md 25 | -------------------------------------------------------------------------------- /.goreleaser-fips.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | project_name: nri-prometheus 4 | builds: 5 | - id: nri-prometheus-nix-fips 6 | main: ./cmd/nri-prometheus/ 7 | binary: nri-prometheus 8 | ldflags: 9 | - -s -w -X github.com/newrelic/nri-prometheus/internal/integration.Version={{.Version}} 10 | env: 11 | - CGO_ENABLED=1 12 | - GOEXPERIMENT=boringcrypto 13 | - >- 14 | {{- if eq .Arch "arm64" -}} 15 | CC=aarch64-linux-gnu-gcc 16 | {{- end }} 17 | goos: 18 | - linux 19 | goarch: 20 | - amd64 21 | - arm64 22 | tags: 23 | - fips 24 | 25 | archives: 26 | - id: nri-prometheus-nix-fips 27 | builds: 28 | - nri-prometheus-nix-fips 29 | name_template: "{{ .ProjectName }}-fips_{{ .Os }}_{{ .Version }}_{{ .Arch }}_dirty" 30 | format: tar.gz 31 | 32 | release: 33 | disable: true 34 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | project_name: nri-prometheus 4 | builds: 5 | - id: nri-prometheus-nix 6 | main: ./cmd/nri-prometheus/ 7 | binary: nri-prometheus 8 | ldflags: 9 | - -s -w -X github.com/newrelic/nri-prometheus/internal/integration.Version={{.Version}} #-X main.gitCommit={{.Commit}} -X main.buildDate={{.Date}} 10 | env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - darwin 15 | goarch: 16 | - 386 17 | - amd64 18 | - arm 19 | - arm64 20 | ignore: 21 | - goos: darwin 22 | goarch: 386 23 | - goos: darwin 24 | goarch: arm 25 | 26 | - id: nri-prometheus-win 27 | main: ./cmd/nri-prometheus/ 28 | binary: nri-prometheus 29 | ldflags: 30 | - -s -w -X github.com/newrelic/nri-prometheus/internal/integration.Version={{.Version}} #-X main.gitCommit={{.Commit}} -X main.buildDate={{.Date}} 31 | env: 32 | - CGO_ENABLED=0 33 | goos: 34 | - windows 35 | goarch: 36 | - 386 37 | - amd64 38 | hooks: 39 | pre: build/windows/set_exe_properties.sh {{ .Env.TAG }} "prometheus" 40 | 41 | archives: 42 | - id: nri-prometheus-nix 43 | builds: 44 | - nri-prometheus-nix 45 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}_dirty" 46 | format: tar.gz 47 | 48 | - id: nri-prometheus-win 49 | builds: 50 | - nri-prometheus-win 51 | name_template: "{{ .ProjectName }}-{{ .Arch }}.{{ .Version }}_dirty" 52 | format: zip 53 | 54 | # we use custom publisher for fixing archives and signing them 55 | release: 56 | disable: true 57 | 58 | dockers: 59 | - goos: linux 60 | goarch: amd64 61 | dockerfile: Dockerfile.release 62 | ids: 63 | - nri-prometheus-nix 64 | image_templates: 65 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-amd64' 66 | use: buildx 67 | build_flag_templates: 68 | - "--platform=linux/amd64" 69 | skip_push: false 70 | - goos: linux 71 | goarch: arm64 72 | dockerfile: Dockerfile.release 73 | ids: 74 | - nri-prometheus-nix 75 | image_templates: 76 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm64' 77 | use: buildx 78 | build_flag_templates: 79 | - "--platform=linux/arm64" 80 | skip_push: false 81 | - goos: linux 82 | goarch: arm 83 | goarm: 6 84 | dockerfile: Dockerfile.release 85 | ids: 86 | - nri-prometheus-nix 87 | image_templates: 88 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm' 89 | use: buildx 90 | build_flag_templates: 91 | - "--platform=linux/arm" 92 | skip_push: false 93 | 94 | docker_manifests: 95 | - name_template: newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }} 96 | image_templates: 97 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-amd64' 98 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm64' 99 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm' 100 | - name_template: newrelic/nri-prometheus:{{ .Major }}.{{ .Minor }}{{ .Env.TAG_SUFFIX }} 101 | image_templates: 102 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-amd64' 103 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm64' 104 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm' 105 | - name_template: newrelic/nri-prometheus:latest{{ .Env.TAG_SUFFIX }} 106 | image_templates: 107 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-amd64' 108 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm64' 109 | - 'newrelic/nri-prometheus:{{ .Version }}{{ .Env.TAG_SUFFIX }}-arm' 110 | 111 | snapshot: 112 | version_template: "{{ .Tag }}-next" 113 | 114 | changelog: 115 | sort: asc 116 | filters: 117 | exclude: 118 | - '^docs:' 119 | - '^test:' 120 | -------------------------------------------------------------------------------- /.trivyignore: -------------------------------------------------------------------------------- 1 | # We are running the 2.16.0 version of github.com/emicklei/go-restful that had the fix backported, but trivy still points it out as false-positive 2 | # This is going to be fixed by 2.15 of the kubernetes client go, they decided not to backport the fix since they are not using the impacted feature. 3 | CVE-2022-1996 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome. Before contributing please read the 4 | [code of conduct](./CODE_OF_CONDUCT.md) and [search the issue tracker](issues); your issue may have already been discussed or fixed in `main`. To contribute, 5 | [fork](https://help.github.com/articles/fork-a-repo/) this repository, commit your changes, and [send a Pull Request](https://help.github.com/articles/using-pull-requests/). 6 | 7 | Note that our [code of conduct](./CODE_OF_CONDUCT.md) applies to all platforms and venues related to this project; please follow it in all your interactions with the project and its participants. 8 | 9 | ## Feature Requests 10 | 11 | Feature requests should be submitted in the [Issue tracker](../../issues), with a description of the expected behavior & use case, where they’ll remain closed until sufficient interest, [e.g. :+1: reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/), has been [shown by the community](../../issues?q=label%3A%22votes+needed%22+sort%3Areactions-%2B1-desc). 12 | Before submitting an Issue, please search for similar ones in the 13 | [closed issues](../../issues?q=is%3Aissue+is%3Aclosed+label%3Aenhancement). 14 | 15 | ## Pull Requests 16 | 17 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 18 | 2. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 19 | 3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. 20 | 21 | ## Contributor License Agreement 22 | 23 | Keep in mind that when you submit your Pull Request, you'll need to sign the CLA via the click-through using CLA-Assistant. If you'd like to execute our corporate CLA, or if you have any questions, please drop us an email at opensource@newrelic.com. 24 | 25 | For more information about CLAs, please check out Alex Russell’s excellent post, 26 | [“Why Do I Need to Sign This?”](https://infrequently.org/2008/06/why-do-i-need-to-sign-this/). 27 | 28 | ## Slack 29 | 30 | We host a public Slack with a dedicated channel for contributors and maintainers of open source projects hosted by New Relic. If you are contributing to this project, you're welcome to request access to the #oss-contributors channel in the newrelicusers.slack.com workspace. To request access, please use this [link](https://join.slack.com/t/newrelicusers/shared_invite/zt-1ayj69rzm-~go~Eo1whIQGYnu3qi15ng). 31 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | RUN apk add --no-cache ca-certificates 3 | 4 | USER nobody 5 | ADD bin/nri-prometheus /bin/ 6 | 7 | # When standalone is set to true nri-prometheus does not require an infrastructure agent to work and send data 8 | ENV STANDALONE=TRUE 9 | 10 | ENTRYPOINT ["/bin/nri-prometheus"] 11 | -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22.0 2 | 3 | RUN apk add --no-cache --upgrade \ 4 | ca-certificates \ 5 | tini 6 | 7 | COPY ./nri-prometheus /bin/nri-prometheus 8 | USER nobody 9 | 10 | # When standalone is set to true nri-prometheus does not require an infrastructure agent to work and send data 11 | ENV STANDALONE=TRUE 12 | 13 | ENTRYPOINT ["/sbin/tini", "--", "/bin/nri-prometheus"] 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INTEGRATION := prometheus 2 | BINARY_NAME = nri-$(INTEGRATION) 3 | SRC_DIR = . 4 | INTEGRATIONS_DIR = /var/db/newrelic-infra/newrelic-integrations/ 5 | CONFIG_DIR = /etc/newrelic-infra/integrations.d 6 | GO_FILES := ./ 7 | BIN_FILES := ./cmd/nri-prometheus/ 8 | TARGET := target 9 | GOFLAGS = -mod=readonly 10 | GO_VERSION ?= $(shell grep '^go ' go.mod | awk '{print $$2}') 11 | BUILDER_IMAGE ?= "ghcr.io/newrelic/coreint-automation:latest-go$(GO_VERSION)-ubuntu16.04" 12 | 13 | all: build 14 | 15 | build: clean compile test 16 | 17 | clean: 18 | @echo "=== $(INTEGRATION) === [ clean ]: removing binaries..." 19 | @rm -rfv bin $(TARGET) 20 | 21 | compile-deps: 22 | @echo "=== $(INTEGRATION) === [ compile-deps ]: installing build dependencies..." 23 | @go get -v -d -t ./... 24 | 25 | bin/$(BINARY_NAME): 26 | @echo "=== $(INTEGRATION) === [ compile ]: building $(BINARY_NAME)..." 27 | @go build -v -o bin/$(BINARY_NAME) $(BIN_FILES) 28 | 29 | compile: compile-deps bin/$(BINARY_NAME) 30 | 31 | test: 32 | @echo "=== $(INTEGRATION) === [ test ]: running unit tests..." 33 | @go test ./... 34 | 35 | # rt-update-changelog runs the release-toolkit run.sh script by piping it into bash to update the CHANGELOG.md. 36 | # It also passes down to the script all the flags added to the make target. To check all the accepted flags, 37 | # see: https://github.com/newrelic/release-toolkit/blob/main/contrib/ohi-release-notes/run.sh 38 | # e.g. `make rt-update-changelog -- -v` 39 | rt-update-changelog: 40 | curl "https://raw.githubusercontent.com/newrelic/release-toolkit/v1/contrib/ohi-release-notes/run.sh" | bash -s -- $(filter-out $@,$(MAKECMDGOALS)) 41 | 42 | # Include thematic Makefiles 43 | include $(CURDIR)/build/ci.mk 44 | include $(CURDIR)/build/release.mk 45 | 46 | .PHONY: all build clean compile-deps compile test install rt-update-changelog 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | New Relic Open Source community plus project banner. 2 | 3 | # New Relic Prometheus OpenMetrics integration 4 | 5 | > 🚧 Important Notice 6 | > 7 | > Prometheus Open Metrics integration for Kubernetes has been replaced by the Prometheus Agent. 8 | > 9 | > See how to install the [Prometheus agent](https://docs.newrelic.com/docs/infrastructure/prometheus-integrations/install-configure-prometheus-agent/install-prometheus-agent/) to understand its benefits and get a full visibility of your Prometheus workloads running in a Kubernetes cluster. 10 | > 11 | > In case you need to migrate from the Prometheus Open Metrics integration to Open Metrics check the following [migration guide](https://docs.newrelic.com/docs/infrastructure/prometheus-integrations/install-configure-prometheus-agent/migration-guide/). 12 | 13 | Fetch metrics in the Prometheus metrics format, inside or outside Kubernetes, and send them to the New Relic platform. 14 | 15 | ## Installation and usage 16 | 17 | For documentation about how to use the integration, refer to [our documentation website](https://docs.newrelic.com/docs/new-relic-prometheus-openmetrics-integration-kubernetes). 18 | 19 | Find out more about Prometheus and New Relic in [this blog post](https://blog.newrelic.com/product-news/how-to-monitor-prometheus-metrics/). 20 | 21 | ## Helm chart 22 | 23 | You can install this chart using [`nri-bundle`](https://github.com/newrelic/helm-charts/tree/master/charts/nri-bundle) located in the 24 | [helm-charts repository](https://github.com/newrelic/helm-charts) or directly from this repository by adding this Helm repository: 25 | 26 | ```shell 27 | helm repo add nri-prometheus https://newrelic.github.io/nri-prometheus 28 | helm upgrade --install nri-prometheus/nri-prometheus -f your-custom-values.yaml 29 | ``` 30 | 31 | For further information of the configuration needed for the chart just read the [chart's README](/charts/nri-prometheus/README.md). 32 | 33 | ## Building 34 | 35 | Golang is required to build the integration. We recommend Golang 1.11 or higher. 36 | 37 | This integration requires having a Kubernetes cluster available to deploy and run it. For development, we recommend using [Docker](https://docs.docker.com/install/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), and [skaffold](https://skaffold.dev/docs/getting-started/#installing-skaffold). 38 | 39 | After cloning this repository, go to the directory of the Prometheus integration and build it: 40 | 41 | ```bash 42 | make 43 | ``` 44 | 45 | The command above executes the tests for the Prometheus integration and builds an executable file called `nri-prometheus` under the `bin` directory. 46 | 47 | To start the integration, run `nri-prometheus`: 48 | 49 | ```bash 50 | ./bin/nri-prometheus 51 | ``` 52 | 53 | If you want to know more about usage of `./bin/nri-prometheus`, pass the `-help` parameter: 54 | 55 | ```bash 56 | ./bin/nri-prometheus -help 57 | ``` 58 | 59 | External dependencies are managed through the [govendor tool](https://github.com/kardianos/govendor). Locking all external dependencies to a specific version (if possible) into the vendor directory is required. 60 | 61 | ### Build the Docker image 62 | 63 | In case you wish to push your own version of the image to a Docker registry, you can build it with: 64 | 65 | ```bash 66 | IMAGE_NAME= make docker-build 67 | ``` 68 | 69 | And push it later with `docker push` 70 | 71 | ### Executing the integration in a development cluster 72 | 73 | - You need to configure how to deploy the integration in the cluster. Copy deploy/local.yaml.example to deploy/local.yaml and edit the placeholders. 74 | - To get the New Relic license key, visit: 75 | `https://newrelic.com/accounts/`. It's located in the right sidebar. 76 | - After updating the yaml file, you need to compile the integration: `GOOS=linux make compile-only`. 77 | - Once you have it compiled, you need to deploy it in your Kubernetes cluster: `skaffold run` 78 | 79 | ### Running the Kubernetes Target Retriever locally 80 | 81 | It can be useful to run the Kubernetes Target Retriever locally against a remote/local cluster to debug the endpoints that are discovered. The binary located in `/cmd/k8s-target-retriever` is made for this. 82 | 83 | To run the program, run the following command in your terminal: 84 | 85 | ```shell script 86 | # ensure your kubectl is configured correcly & against the correct cluster 87 | kubectl config get-contexts 88 | # run the program 89 | go run cmd/k8s-target-retriever/main.go 90 | ``` 91 | 92 | ## Testing 93 | 94 | To run the tests execute: 95 | 96 | ```bash 97 | make test 98 | ``` 99 | 100 | At the moment, tests are totally isolated and you don't need a cluster to run them. 101 | 102 | ## Support 103 | 104 | Should you need assistance with New Relic products, you are in good hands with several support diagnostic tools and support channels. 105 | 106 | > New Relic offers NRDiag, [a client-side diagnostic utility](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/troubleshooting/new-relic-diagnostics) that automatically detects common problems with New Relic agents. If NRDiag detects a problem, it suggests troubleshooting steps. NRDiag can also automatically attach troubleshooting data to a New Relic Support ticket. 107 | 108 | If the issue has been confirmed as a bug or is a Feature request, please file a Github issue. 109 | 110 | **Support Channels** 111 | 112 | - [New Relic Documentation](https://docs.newrelic.com): Comprehensive guidance for using our platform 113 | - [New Relic Community](https://forum.newrelic.com): The best place to engage in troubleshooting questions 114 | - [New Relic Developer](https://developer.newrelic.com/): Resources for building a custom observability applications 115 | - [New Relic University](https://learn.newrelic.com/): A range of online training for New Relic users of every level 116 | - [New Relic Technical Support](https://support.newrelic.com/) 24/7/365 ticketed support. Read more about our [Technical Support Offerings](https://docs.newrelic.com/docs/licenses/license-information/general-usage-licenses/support-plan). 117 | 118 | ## Privacy 119 | 120 | At New Relic we take your privacy and the security of your information seriously, and are committed to protecting your information. We must emphasize the importance of not sharing personal data in public forums, and ask all users to scrub logs and diagnostic information for sensitive information, whether personal, proprietary, or otherwise. 121 | 122 | We define “Personal Data” as any information relating to an identified or identifiable individual, including, for example, your name, phone number, post code or zip code, Device ID, IP address, and email address. 123 | 124 | For more information, review [New Relic’s General Data Privacy Notice](https://newrelic.com/termsandconditions/privacy). 125 | 126 | ## Contribute 127 | 128 | We encourage your contributions to improve this project! Keep in mind that when you submit your pull request, you'll need to sign the CLA via the click-through using CLA-Assistant. You only have to sign the CLA one time per project. 129 | 130 | If you have any questions, or to execute our corporate CLA (which is required if your contribution is on behalf of a company), drop us an email at opensource@newrelic.com. 131 | 132 | **A note about vulnerabilities** 133 | 134 | As noted in our [security policy](../../security/policy), New Relic is committed to the privacy and security of our customers and their data. We believe that providing coordinated disclosure by security researchers and engaging with the security community are important means to achieve our security goals. 135 | 136 | If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [our bug bounty program](https://docs.newrelic.com/docs/security/security-privacy/information-security/report-security-vulnerabilities/). 137 | 138 | If you would like to contribute to this project, review [these guidelines](./CONTRIBUTING.md). 139 | 140 | To all contributors, we thank you! Without your contribution, this project would not be what it is today. 141 | 142 | ## License 143 | 144 | nri-prometheus is licensed under the [Apache 2.0](http://apache.org/licenses/LICENSE-2.0.txt) License. 145 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release proccess 2 | 3 | Releases are triggered by creating a new **pre-release** on github. 4 | On a successful build the job will run [GoReleaser](https://goreleaser.com). 5 | This will generate artifacts, docker images, and kubernetes manifest which will be uploaded the same step. 6 | After verifying everything is correct, the pre-release (already containing the artifacts) can be promoted to a release. 7 | Pre-release to release promotion will not trigger any additional job, as everything is done in the pre-release step. 8 | 9 | The `Update Helm Chart POMI version` (`helm.yml`) GitHub Action will be triggered creating a new PR on https://github.com/newrelic/helm-charts/ with the version specified in the tag. After POMI is released this PR should be merged and released. 10 | 11 | To create a new release you need to tag the main branch with the new release version. 12 | 13 | ## Version naming scheme 14 | 15 | All release follow the [semantic versioning](https://semver.org/) scheme. 16 | 17 | For the release in this project we tag main with the release version, with a prefix `v` before the version number. 18 | E.g. so release `1.2.3` would mean you tag main with the tag `v1.2.3` 19 | 20 | ## Tagging via the command line 21 | 22 | To tag via the cli you need to have [Git](https://git-scm.com/) installed. 23 | From a terminal run the command: 24 | 25 | ```shell script 26 | $ git tag -a vX.Y.Z -m 'New release with cool feature' 27 | ``` 28 | 29 | To kick off the release you then need to push the tag to github. 30 | This is done by running the following command: 31 | 32 | ```shell script 33 | $ git push origin vX.Y.Z 34 | ``` 35 | Once the this is done it then triggers a release on [TravisCI](https://travis-ci.org/). 36 | You can see the progress of the deployment [here](https://travis-ci.org/newrelic/nri-prometheus/builds). 37 | 38 | ## Tagging on Github 39 | 40 | 1. Click on [Releases](releases). 41 | 2. Click *Draft a new release*. 42 | 3. Type a version number (with the prefix `v`), e.g. `vX.Y.Z` 43 | 4. Set the release title, e.g. 'New release with cool feature' 44 | 5. Then hit *Publish release* 45 | 46 | Pipeline progress can be viewed in the "Actions" tab in Github. 47 | -------------------------------------------------------------------------------- /build/ci.mk: -------------------------------------------------------------------------------- 1 | .PHONY : ci/pull-builder-image 2 | ci/pull-builder-image: 3 | @docker pull $(BUILDER_IMAGE) 4 | 5 | .PHONY : ci/deps 6 | ci/deps: ci/pull-builder-image 7 | 8 | .PHONY : ci/debug-container 9 | ci/debug-container: ci/deps 10 | @docker run --rm -it \ 11 | -v $(CURDIR):/go/src/github.com/newrelic/nri-$(INTEGRATION) \ 12 | -w /go/src/github.com/newrelic/nri-$(INTEGRATION) \ 13 | -e PRERELEASE=true \ 14 | -e GITHUB_TOKEN=$(GITHUB_TOKEN) \ 15 | -e TAG \ 16 | -e GPG_MAIL \ 17 | -e GPG_PASSPHRASE \ 18 | -e GPG_PRIVATE_KEY_BASE64 \ 19 | $(BUILDER_IMAGE) bash 20 | 21 | .PHONY : ci/validate 22 | ci/validate: ci/deps 23 | @docker run --rm -t \ 24 | -v $(CURDIR):/go/src/github.com/newrelic/nri-$(INTEGRATION) \ 25 | -w /go/src/github.com/newrelic/nri-$(INTEGRATION) \ 26 | $(BUILDER_IMAGE) make validate 27 | 28 | .PHONY : ci/test 29 | ci/test: ci/deps 30 | @docker run --rm -t \ 31 | -v $(CURDIR):/go/src/github.com/newrelic/nri-$(INTEGRATION) \ 32 | -w /go/src/github.com/newrelic/nri-$(INTEGRATION) \ 33 | $(BUILDER_IMAGE) make test 34 | 35 | .PHONY : ci/snyk-test 36 | ci/snyk-test: 37 | @docker run --rm -t \ 38 | --name "nri-$(INTEGRATION)-snyk-test" \ 39 | -v $(CURDIR):/go/src/github.com/newrelic/nri-$(INTEGRATION) \ 40 | -w /go/src/github.com/newrelic/nri-$(INTEGRATION) \ 41 | -e SNYK_TOKEN \ 42 | snyk/snyk:golang snyk test --severity-threshold=high 43 | 44 | .PHONY : ci/build 45 | ci/build: ci/deps 46 | ifdef TAG 47 | @docker run --rm -t \ 48 | -v $(CURDIR):/go/src/github.com/newrelic/nri-$(INTEGRATION) \ 49 | -w /go/src/github.com/newrelic/nri-$(INTEGRATION) \ 50 | -e INTEGRATION=$(INTEGRATION) \ 51 | -e TAG \ 52 | $(BUILDER_IMAGE) make release/build 53 | else 54 | @echo "===> $(INTEGRATION) === [ci/build] TAG env variable expected to be set" 55 | exit 1 56 | endif 57 | 58 | .PHONY : ci/prerelease-fips 59 | ci/prerelease-fips: ci/deps 60 | ifdef TAG 61 | @docker run --rm -t \ 62 | --name "nri-$(INTEGRATION)-prerelease" \ 63 | -v $(CURDIR):/go/src/github.com/newrelic/nri-$(INTEGRATION) \ 64 | -w /go/src/github.com/newrelic/nri-$(INTEGRATION) \ 65 | -e INTEGRATION \ 66 | -e PRERELEASE=true \ 67 | -e GITHUB_TOKEN \ 68 | -e REPO_FULL_NAME \ 69 | -e TAG \ 70 | -e TAG_SUFFIX \ 71 | -e GENERATE_PACKAGES \ 72 | -e PRERELEASE \ 73 | $(BUILDER_IMAGE) make release-fips 74 | else 75 | @echo "===> $(INTEGRATION) === [ci/prerelease] TAG env variable expected to be set" 76 | exit 1 77 | endif 78 | -------------------------------------------------------------------------------- /build/nix/fix_archives.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # 4 | # 5 | # Gets dist/tarball_dirty created by Goreleaser (all files in root path) and reorganize files in correct path 6 | # 7 | # 8 | PROJECT_PATH=$1 9 | #PROJECT_PATH=$(PWD) 10 | 11 | for tarball_dirty in $(find dist -regex ".*_dirty\.\(tar.gz\)");do 12 | tarball=${tarball_dirty:5:${#tarball_dirty}-(5+13)} # Strips begining and end chars 13 | TARBALL_CLEAN="${tarball}.tar.gz" 14 | TARBALL_TMP="dist/tarball_temp" 15 | TARBALL_CONTENT_PATH="${TARBALL_TMP}/${tarball}_content" 16 | mkdir -p ${TARBALL_CONTENT_PATH}/var/db/newrelic-infra/newrelic-integrations/bin/ 17 | mkdir -p ${TARBALL_CONTENT_PATH}/etc/newrelic-infra/integrations.d/ 18 | echo "===> Decompress ${tarball} in ${TARBALL_CONTENT_PATH}" 19 | tar -xvf ${tarball_dirty} -C ${TARBALL_CONTENT_PATH} 20 | 21 | echo "===> Move files inside ${tarball}" 22 | mv ${TARBALL_CONTENT_PATH}/nri-${INTEGRATION} "${TARBALL_CONTENT_PATH}/var/db/newrelic-infra/newrelic-integrations/bin/" 23 | #mv ${TARBALL_CONTENT_PATH}/${INTEGRATION}-definition.yml ${TARBALL_CONTENT_PATH}/var/db/newrelic-infra/newrelic-integrations/ 24 | #mv ${TARBALL_CONTENT_PATH}/${INTEGRATION}-config.yml.sample ${TARBALL_CONTENT_PATH}/etc/newrelic-infra/integrations.d/ 25 | 26 | echo "===> Creating tarball ${TARBALL_CLEAN}" 27 | cd ${TARBALL_CONTENT_PATH} 28 | tar -czvf ../${TARBALL_CLEAN} . 29 | cd $PROJECT_PATH 30 | echo "===> Moving tarball ${TARBALL_CLEAN}" 31 | mv "${TARBALL_TMP}/${TARBALL_CLEAN}" dist/ 32 | echo "===> Cleaning dirty tarball ${tarball_dirty}" 33 | rm ${tarball_dirty} 34 | done 35 | -------------------------------------------------------------------------------- /build/release.mk: -------------------------------------------------------------------------------- 1 | BUILD_DIR := ./bin/ 2 | GORELEASER_VERSION ?= v2.4.4 3 | GORELEASER_BIN ?= bin/goreleaser 4 | 5 | bin: 6 | @mkdir -p $(BUILD_DIR) 7 | 8 | $(GORELEASER_BIN): bin 9 | @echo "===> $(INTEGRATION) === [$(GORELEASER_BIN)] Installing goreleaser $(GORELEASER_VERSION)" 10 | @(wget -qO /tmp/goreleaser.tar.gz https://github.com/goreleaser/goreleaser/releases/download/$(GORELEASER_VERSION)/goreleaser_$(OS_DOWNLOAD)_x86_64.tar.gz) 11 | @(tar -xf /tmp/goreleaser.tar.gz -C bin/) 12 | @(rm -f /tmp/goreleaser.tar.gz) 13 | @echo "===> $(INTEGRATION) === [$(GORELEASER_BIN)] goreleaser downloaded" 14 | 15 | .PHONY : release/clean 16 | release/clean: 17 | @echo "===> $(INTEGRATION) === [release/clean] remove build metadata files" 18 | rm -fv $(CURDIR)/cmd/nri-prometheus/versioninfo.json 19 | rm -fv $(CURDIR)/cmd/nri-prometheus/resource.syso 20 | 21 | .PHONY : release/deps 22 | release/deps: $(GORELEASER_BIN) 23 | @echo "===> $(INTEGRATION) === [release/deps] installing deps" 24 | @go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo 25 | @go mod tidy 26 | 27 | .PHONY : release/build 28 | release/build: release/deps release/clean 29 | ifeq ($(GENERATE_PACKAGES), true) 30 | @echo "===> $(INTEGRATION) === [release/build] PRERELEASE/RELEASE compiling all binaries, creating packages, archives" 31 | # Pre-release/release actually builds and uploads images 32 | # goreleaser will compile binaries, generate manifests, and push multi-arch docker images 33 | # TAG_SUFFIX should be set as "-pre" during prereleases 34 | @$(GORELEASER_BIN) release --config $(CURDIR)/.goreleaser.yml --skip=validate --clean 35 | else 36 | @echo "===> $(INTEGRATION) === [release/build] build compiling all binaries" 37 | # release/build with PRERELEASE unset is actually called only from push/pr pipeline to check everything builds correctly 38 | @$(GORELEASER_BIN) build --config $(CURDIR)/.goreleaser.yml --skip=validate --snapshot --clean 39 | endif 40 | 41 | .PHONY : release/build-fips 42 | release/build-fips: release/deps release/clean 43 | ifeq ($(GENERATE_PACKAGES), true) 44 | @echo "===> $(INTEGRATION) === [release/build] PRERELEASE/RELEASE compiling fips binaries, creating packages, archives" 45 | # TAG_SUFFIX should be set as "-pre" during prereleases 46 | @$(GORELEASER_BIN) release --config $(CURDIR)/.goreleaser-fips.yml --skip=validate --clean 47 | else 48 | @echo "===> $(INTEGRATION) === [release/build-fips] build compiling fips binaries" 49 | # release/build with PRERELEASE unset is actually called only from push/pr pipeline to check everything builds correctly 50 | @$(GORELEASER_BIN) build --config $(CURDIR)/.goreleaser-fips.yml --skip=validate --snapshot --clean 51 | endif 52 | 53 | .PHONY : release/fix-archive 54 | release/fix-archive: 55 | @echo "===> $(INTEGRATION) === [release/fix-archive] fixing tar.gz archives internal structure" 56 | @bash $(CURDIR)/build/nix/fix_archives.sh $(CURDIR) 57 | @echo "===> $(INTEGRATION) === [release/fix-archive] fixing zip archives internal structure" 58 | @bash $(CURDIR)/build/windows/fix_archives.sh $(CURDIR) 59 | 60 | .PHONY : release/publish 61 | release/publish: 62 | ifeq ($(PRERELEASE), true) 63 | @echo "===> $(INTEGRATION) === [release/publish] publishing packages" 64 | @bash $(CURDIR)/build/upload_artifacts_gh.sh 65 | endif 66 | # TODO: This seems like a leftover, should consider removing 67 | @echo "===> $(INTEGRATION) === [release/publish] compiling binaries" 68 | @$(GORELEASER_BIN) build --config $(CURDIR)/.goreleaser.yml --skip=validate --snapshot --clean 69 | 70 | .PHONY : release/publish-fips 71 | release/publish-fips: 72 | ifeq ($(PRERELEASE), true) 73 | @echo "===> $(INTEGRATION) === [release/publish-fips] publishing fips packages" 74 | @bash $(CURDIR)/build/upload_artifacts_gh.sh 75 | endif 76 | # TODO: This seems like a leftover, should consider removing 77 | @echo "===> $(INTEGRATION) === [release/publish-fips] compiling fips binaries" 78 | @$(GORELEASER_BIN) build --config $(CURDIR)/.goreleaser-fips.yml --skip=validate --snapshot --clean 79 | 80 | .PHONY : release 81 | release: release/build release/fix-archive release/publish release/clean 82 | @echo "===> $(INTEGRATION) === [release] full pre-release cycle complete for nix" 83 | 84 | .PHONY : release-fips 85 | release-fips: release/build-fips release/fix-archive release/publish-fips release/clean 86 | @echo "===> $(INTEGRATION) === [release-fips] fips pre-release cycle complete for nix" 87 | 88 | OS := $(shell uname -s) 89 | ifeq ($(OS), Darwin) 90 | OS_DOWNLOAD := "darwin" 91 | TAR := gtar 92 | else 93 | OS_DOWNLOAD := "linux" 94 | endif 95 | -------------------------------------------------------------------------------- /build/upload_artifacts_gh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # 4 | # Upload dist artifacts to GH Release assets 5 | # 6 | # 7 | 8 | cd dist 9 | for package in $(find . -regex ".*\.\(msi\|rpm\|deb\|zip\|tar.gz\)");do 10 | echo "===> Uploading package: '${package}'" 11 | gh release upload ${TAG} ${package} 12 | done 13 | -------------------------------------------------------------------------------- /build/windows/fix_archives.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # 4 | # 5 | # Gets dist/zip_dirty created by Goreleaser and reorganize inside files 6 | # 7 | # 8 | PROJECT_PATH=$1 9 | #PROJECT_PATH=$(PWD) 10 | for zip_dirty in $(find dist -regex ".*_dirty\.\(zip\)");do 11 | zip_file_name=${zip_dirty:5:${#zip_dirty}-(5+10)} # Strips begining and end chars 12 | ZIP_CLEAN="${zip_file_name}.zip" 13 | ZIP_TMP="dist/zip_temp" 14 | ZIP_CONTENT_PATH="${ZIP_TMP}/${zip_file_name}_content" 15 | 16 | mkdir -p "${ZIP_CONTENT_PATH}" 17 | 18 | AGENT_DIR_IN_ZIP_PATH="${ZIP_CONTENT_PATH}/New Relic/newrelic-infra/newrelic-integrations/" 19 | CONF_IN_ZIP_PATH="${ZIP_CONTENT_PATH}/New Relic/newrelic-infra/integrations.d/" 20 | 21 | mkdir -p "${AGENT_DIR_IN_ZIP_PATH}/bin" 22 | mkdir -p "${CONF_IN_ZIP_PATH}" 23 | 24 | echo "===> Decompress ${zip_file_name} in ${ZIP_CONTENT_PATH}" 25 | unzip ${zip_dirty} -d ${ZIP_CONTENT_PATH} 26 | 27 | echo "===> Move files inside ${zip_file_name}" 28 | mv ${ZIP_CONTENT_PATH}/nri-${INTEGRATION}.exe "${AGENT_DIR_IN_ZIP_PATH}/bin" 29 | #mv ${ZIP_CONTENT_PATH}/${INTEGRATION}-win-definition.yml "${AGENT_DIR_IN_ZIP_PATH}" 30 | #mv ${ZIP_CONTENT_PATH}/${INTEGRATION}-win-config.yml.sample "${CONF_IN_ZIP_PATH}" 31 | 32 | echo "===> Creating zip ${ZIP_CLEAN}" 33 | cd "${ZIP_CONTENT_PATH}" 34 | zip -r ../${ZIP_CLEAN} . 35 | cd $PROJECT_PATH 36 | echo "===> Moving zip ${ZIP_CLEAN}" 37 | mv "${ZIP_TMP}/${ZIP_CLEAN}" dist/ 38 | echo "===> Cleaning dirty zip ${zip_dirty}" 39 | rm "${zip_dirty}" 40 | done -------------------------------------------------------------------------------- /build/windows/set_exe_properties.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # 4 | # 5 | # Create the metadata for the exe's files, called by .goreleser as a hook in the build section 6 | # 7 | # 8 | TAG=$1 9 | INTEGRATION=$2 10 | 11 | if [ -n "$1" ]; then 12 | echo "===> Tag is ${TAG}" 13 | else 14 | # todo: exit here with error? 15 | echo "===> Tag not specified will be 0.0.0" 16 | TAG='0.0.0' 17 | fi 18 | 19 | MajorVersion=$(echo ${TAG:1} | cut -d "." -f 1) 20 | MinorVersion=$(echo ${TAG:1} | cut -d "." -f 2) 21 | PatchVersion=$(echo ${TAG:1} | cut -d "." -f 3) 22 | BuildVersion='0' 23 | 24 | Year=$(date +"%Y") 25 | INTEGRATION_EXE="nri-${INTEGRATION}.exe" 26 | 27 | sed \ 28 | -e "s/{MajorVersion}/$MajorVersion/g" \ 29 | -e "s/{MinorVersion}/$MinorVersion/g" \ 30 | -e "s/{PatchVersion}/$PatchVersion/g" \ 31 | -e "s/{BuildVersion}/$BuildVersion/g" \ 32 | -e "s/{Year}/$Year/g" \ 33 | -e "s/{Integration}/nri-$INTEGRATION/g" \ 34 | -e "s/{IntegrationExe}/$INTEGRATION_EXE/g" \ 35 | ./build/windows/versioninfo.json.template > ./cmd/nri-prometheus/versioninfo.json 36 | 37 | # todo: do we need this export line 38 | export PATH="$PATH:/go/bin" 39 | go generate github.com/newrelic/nri-${INTEGRATION}/cmd/nri-prometheus -------------------------------------------------------------------------------- /build/windows/unit_tests.ps1: -------------------------------------------------------------------------------- 1 | echo "--- Running tests" 2 | 3 | ## test everything excluding vendor 4 | go test $(go list ./... | sls -NotMatch '/vendor/') 5 | if (-not $?) 6 | { 7 | echo "Failed running tests" 8 | exit -1 9 | } -------------------------------------------------------------------------------- /build/windows/versioninfo.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "FixedFileInfo": 3 | { 4 | "FileVersion": { 5 | "Major": {MajorVersion}, 6 | "Minor": {MinorVersion}, 7 | "Patch": {PatchVersion}, 8 | "Build": {BuildVersion} 9 | }, 10 | "ProductVersion": { 11 | "Major": {MajorVersion}, 12 | "Minor": {MinorVersion}, 13 | "Patch": {PatchVersion}, 14 | "Build": {BuildVersion} 15 | }, 16 | "FileFlagsMask": "3f", 17 | "FileFlags ": "00", 18 | "FileOS": "040004", 19 | "FileType": "01", 20 | "FileSubType": "00" 21 | }, 22 | "StringFileInfo": 23 | { 24 | "Comments": "(c) {Year} New Relic, Inc.", 25 | "CompanyName": "New Relic, Inc.", 26 | "FileDescription": "", 27 | "FileVersion": "{MajorVersion}.{MinorVersion}.{PatchVersion}.{BuildVersion}", 28 | "InternalName": "{Integration}", 29 | "LegalCopyright": "(c) {Year} New Relic, Inc.", 30 | "LegalTrademarks": "", 31 | "OriginalFilename": "{IntegrationExe}", 32 | "PrivateBuild": "", 33 | "ProductName": "New Relic Infrastructure Integration, {Integration}", 34 | "ProductVersion": "{MajorVersion}.{MinorVersion}.{PatchVersion}.{BuildVersion}", 35 | "SpecialBuild": "" 36 | }, 37 | "VarFileInfo": 38 | { 39 | "Translation": { 40 | "LangID": "0409", 41 | "CharsetID": "04B0" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /charts/load-test-environment/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/load-test-environment/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: load-test-environment 3 | description: A Helm chart for prometheus load generator. 4 | type: application 5 | version: 0.1.1-devel 6 | maintainers: 7 | - name: alvarocabanas 8 | - name: carlossscastro 9 | - name: gsanchezgavier 10 | - name: kang-makes 11 | - name: paologallinaharbur 12 | - name: roobre 13 | -------------------------------------------------------------------------------- /charts/load-test-environment/README.md: -------------------------------------------------------------------------------- 1 | # New Relic's Prometheus Load Generator 2 | 3 | ## Chart Details 4 | 5 | This chart will deploy a prometheus load generator. 6 | 7 | ## Configuration 8 | 9 | | Parameter | Description | Default | 10 | |------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| 11 | | `numberServicesPerDeploy` | Number of services per deployment to create | | 12 | | `deployments.*` | List of specification of the deployments to create | `[]` | 13 | | `deployments.latency` | time in millisecond the /metric endpoint will wait before answering | `0` | 14 | | `deployments.latencyVariation` | ± latency variation % | `0` | 15 | | `deployments.metrics` | Metric file to download | Average Load URL* | 16 | | `deployments.maxRoutines` | Max number of goroutines the prometheus mock server will open (if 0 no limit is imposed) | `0` | 17 | 18 | *Average load URL: https://gist.githubusercontent.com/paologallinaharbur/a159ad779ca44fb9f4ff5b006ef475ee/raw/f5d8a5e7350b8d5e1d03f151fa643fb3a02cd07d/Average%2520prom%2520output 19 | 20 | ## Resources created 21 | 22 | Number of targets created `numberServicesPerDeploy * len(deployments)` 23 | Each service has the label `prometheus.io/scrape: "true"` that is automatically detected by nri-prometheus 24 | 25 | Resources are generated automatically according the following specifications 26 | - Name of deployment: `-lat-latvar-` 27 | - Name of service: `-lat-latvar--` 28 | 29 | When increasing the number of targets and the size the error is shown `Request Entity Too Large 413` 30 | Adding in the environment variables of POMI seems to solve it reducing the payload 31 | ``` 32 | - name: EMITTER_HARVEST_PERIOD 33 | value: 200ms 34 | ``` 35 | 36 | ## Example 37 | 38 | Then, to install this chart, run the following command: 39 | 40 | ```sh 41 | helm install load ./load-test-environment --values ./load-test-environment/values.yaml -n newrelic 42 | ``` 43 | 44 | Notice that when generating a high number of services it is possible the helm command fails to create/delete all resources leaving an unstable scenario. 45 | 46 | To overcome this issue `helm install load ./load-test-environment --values ./load-test-environment/values.yaml -n newrelic | kubectl apply -f -` proved to be more reliable. 47 | 48 | ## Sample prometheus outputs 49 | 50 | Test prometheus metrics, by default the deployments download the big output sample: 51 | 52 | - https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_small_sample.data Small Payload 10 Timeseries 53 | - https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_average_sample.data Average Payload 500 Timeseries 54 | - https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_big_sample.data Big payload 1000 Timeseries 55 | 56 | 57 | ## Compare with real data 58 | 59 | To compare the average size of the payload scraped by pomi you can run `SELECT average(nr_stats_metrics_total_timeseries_by_target) FROM Metric where clusterName='xxxx' SINCE 30 MINUTES AGO TIMESERIES`$ 60 | and get the number of timeseries sent (the average payload here counts 500) 61 | 62 | To compare the average time a target takes in order to answer `SELECT average(nr_stats_integration_fetch_target_duration_seconds) FROM Metric where clusterName='xxxx' SINCE 30 MINUTES AGO FACET target LIMIT 500` 63 | -------------------------------------------------------------------------------- /charts/load-test-environment/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | THIS CHART IS MEANT FOR LOAD TESTING ONLY 2 | 3 | It created {{ .Values.numberServicesPerDeploy }} services per each Deployment 4 | It created {{ len .Values.deployments }} deployments 5 | 6 | Name of deployment: `-lat-latvar-` 7 | Name of service: `-lat-latvar--` 8 | 9 | Number of targets created numberServicesPerDeploy*len(deployments) 10 | Each service has the label `prometheus.io/scrape: "true"` that is automatically detected by nri-prometheus 11 | -------------------------------------------------------------------------------- /charts/load-test-environment/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "load-test.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create chart name and version as used by the chart label. 10 | */}} 11 | {{- define "load-test.chart" -}} 12 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 13 | {{- end }} 14 | 15 | -------------------------------------------------------------------------------- /charts/load-test-environment/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- $replicaCount := .Values.replicaCount -}} 2 | {{- $chartName := .Chart.Name -}} 3 | {{- $namespace := .Values.namespace -}} 4 | 5 | {{- $index := 0 -}} 6 | 7 | {{- range .Values.deployments -}} 8 | {{- $index = add1 $index -}} 9 | {{- $latency := default "0" .latency -}} 10 | {{- $latencyVariation := default "0" .latencyVariation -}} 11 | {{- $indexString := toString $index -}} 12 | 13 | {{- $uniqueDeployName := printf "%s-lat%s-latvar%s-index%s" .name $latency $latencyVariation $indexString }} 14 | --- 15 | 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: {{ $uniqueDeployName }} 20 | namespace: {{ $namespace }} 21 | labels: 22 | app.kubernetes.io/name: {{ $uniqueDeployName }} 23 | spec: 24 | replicas: {{ $replicaCount }} 25 | selector: 26 | matchLabels: 27 | app.kubernetes.io/name: {{ $uniqueDeployName }} 28 | template: 29 | metadata: 30 | labels: 31 | app.kubernetes.io/name: {{ $uniqueDeployName }} 32 | spec: 33 | serviceAccountName: "default" 34 | containers: 35 | - name: {{ $chartName }} 36 | image: roobre/mockexporter:latest 37 | imagePullPolicy: IfNotPresent 38 | env: 39 | - name: LATENCY 40 | value: {{ $latency | quote}} 41 | - name: LATENCY_VARIATION 42 | value: {{ $latencyVariation | quote}} 43 | - name: METRICS 44 | value: "/metrics/metrics.sample" 45 | - name: MAX_ROUTINES 46 | value: {{ .maxRoutines | default "0" | quote}} 47 | - name: ADDR 48 | value: ":80" 49 | ports: 50 | - name: http 51 | containerPort: 80 52 | protocol: TCP 53 | volumeMounts: 54 | - name: metricsdir 55 | mountPath: /metrics 56 | initContainers: 57 | - name: installmetrics 58 | image: roobre/mockexporter:latest 59 | command: [ "/bin/sh","-c" ] 60 | args: 61 | - wget {{ .metrics | default "https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_big_sample.data" | quote}} -O /metrics/metrics.sample 62 | volumeMounts: 63 | - name: metricsdir 64 | mountPath: "/metrics" 65 | volumes: 66 | - name: metricsdir 67 | emptyDir: {} 68 | {{- end -}} 69 | -------------------------------------------------------------------------------- /charts/load-test-environment/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{- $values := .Values -}} 2 | {{- $numberServices := .Values.numberServicesPerDeploy | int }} 3 | {{- $numberDeploy := .Values.numberOfDeployments | int }} 4 | {{- $namespace := .Values.namespace -}} 5 | {{- $index := 0 -}} 6 | 7 | 8 | {{- range .Values.deployments }} 9 | {{- $index = add1 $index -}} 10 | {{- $latency := default "0" .latency -}} 11 | {{- $latencyVariation := default "0" .latencyVariation -}} 12 | {{- $indexString := toString $index -}} 13 | 14 | {{- $uniqueDeployName := printf "%s-lat%s-latvar%s-index%s" .name $latency $latencyVariation $indexString -}} 15 | 16 | {{- range untilStep 0 $numberServices 1 }} 17 | 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: {{ $uniqueDeployName }}-{{ . }} 22 | namespace: {{ $namespace }} 23 | labels: 24 | prometheus.io/scrape: "true" 25 | app.kubernetes.io/name: {{ $uniqueDeployName }} 26 | spec: 27 | type: ClusterIP 28 | ports: 29 | - port: 80 30 | targetPort: http 31 | protocol: TCP 32 | name: http 33 | selector: 34 | app.kubernetes.io/name: {{ $uniqueDeployName }} 35 | --- 36 | {{- end }} 37 | {{- end }} 38 | -------------------------------------------------------------------------------- /charts/load-test-environment/values.yaml: -------------------------------------------------------------------------------- 1 | # Due to the high volume helm could fail to generate all the needed resources in small clusters due to time-out 2 | # Somethimes `helm template [..] | kubectl apply -f -` seems to be more performant 3 | 4 | # When increasing the number of targets and the size the error is shown `Request Entity Too Large 413` 5 | # Adding in the environment variables of POMI seems to solve it reducing the payload 6 | # - name: EMITTER_HARVEST_PERIOD 7 | # value: 200ms 8 | 9 | # Number of targets created numberServicesPerDeploy*len(deployments) 10 | # Each service has the label `prometheus.io/scrape: "true"` that is automatically detected by nri-prometheus 11 | 12 | # Resources are generated automatically according the following specifications 13 | # Name of deployment: `-lat-latvar-` 14 | # Name of service: `-lat-latvar--` 15 | 16 | # Test prometheus metrics, by default the deployments download the average output sample: 17 | # https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_small_sample.data Small Payload 18 | # https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_average_sample.data Average Payload 19 | # https://raw.githubusercontent.com/newrelic/nri-prometheus/main/load-test/mockexporter/load_test_big_sample.data Big payload 20 | # 21 | # To compare the average size of the payload scraped by pomi you can run `SELECT average(nr_stats_metrics_total_timeseries_by_target) FROM Metric SINCE 30 MINUTES AGO TIMESERIES`$ 22 | # and get the number of timeseries sent (the average payload here counts 400) 23 | 24 | 25 | numberServicesPerDeploy: 100 # Total number service created: numberServicesPerDeploy*len(deployments) 26 | deployments: # Total number deployments created: len(deployments) 27 | - name: one # required (uniqueness is assured by adding an index) 28 | latency: "200" # not required 29 | latencyVariation: "50" # not required 30 | metrics: "" # not required 31 | # maxRoutines: "1" # not required 32 | - name: two # required (uniqueness is assured by adding an index) 33 | latency: "200" # not required 34 | latencyVariation: "50" # not required 35 | metrics: "" # not required 36 | # maxRoutines: "1" # not required 37 | - name: three # required (uniqueness is assured by adding an index) 38 | latency: "200" # not required 39 | latencyVariation: "50" # not required 40 | metrics: "" # not required 41 | # maxRoutines: "1" # not required 42 | - name: four # required (uniqueness is assured by adding an index) 43 | latency: "200" # not required 44 | latencyVariation: "50" # not required 45 | metrics: "" # not required 46 | # maxRoutines: "1" # not required 47 | - name: five # required (uniqueness is assured by adding an index) 48 | latency: "200" # not required 49 | latencyVariation: "50" # not required 50 | metrics: "" # not required 51 | # maxRoutines: "1" #not required 52 | - name: six # required (uniqueness is assured by adding an index) 53 | latency: "200" # not required 54 | latencyVariation: "50" # not required 55 | metrics: "" # not required 56 | # maxRoutines: "1" # not required 57 | - name: seven # required (uniqueness is assured by adding an index) 58 | latency: "200" # not required 59 | latencyVariation: "50" # not required 60 | metrics: "" # not required 61 | # maxRoutines: "1" #not required 62 | - name: eight # required (uniqueness is assured by adding an index) 63 | latency: "200" # not required 64 | latencyVariation: "50" # not required 65 | metrics: "" # not required 66 | # maxRoutines: "1" # not required 67 | 68 | # ---------------------------- No need to modify this 69 | 70 | namespace: "newrelic-load" 71 | replicaCount: 1 72 | nameOverride: "" 73 | fullnameOverride: "load-test-environment" 74 | -------------------------------------------------------------------------------- /charts/nri-prometheus/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/nri-prometheus/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: common-library 3 | repository: https://helm-charts.newrelic.com 4 | version: 1.3.1 5 | digest: sha256:cfa7bfb136b9bcfe87e37d3556c3fedecc58f42685c4ce39485da106408b6619 6 | generated: "2025-01-27T07:20:59.344057768Z" 7 | -------------------------------------------------------------------------------- /charts/nri-prometheus/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: nri-prometheus 3 | description: A Helm chart to deploy the New Relic Prometheus OpenMetrics integration 4 | home: https://docs.newrelic.com/docs/infrastructure/prometheus-integrations/install-configure-openmetrics/configure-prometheus-openmetrics-integrations/ 5 | icon: https://newrelic.com/themes/custom/curio/assets/mediakit/new_relic_logo_vertical.svg 6 | sources: 7 | - https://github.com/newrelic/nri-prometheus 8 | - https://github.com/newrelic/nri-prometheus/tree/main/charts/nri-prometheus 9 | 10 | version: 2.1.20 11 | appVersion: 2.21.4 12 | 13 | dependencies: 14 | - name: common-library 15 | version: 1.3.1 16 | repository: "https://helm-charts.newrelic.com" 17 | 18 | maintainers: 19 | - name: alvarocabanas 20 | url: https://github.com/alvarocabanas 21 | - name: sigilioso 22 | url: https://github.com/sigilioso 23 | - name: gsanchezgavier 24 | url: https://github.com/gsanchezgavier 25 | - name: paologallinaharbur 26 | url: https://github.com/paologallinaharbur 27 | 28 | keywords: 29 | - prometheus 30 | - newrelic 31 | - monitoring 32 | -------------------------------------------------------------------------------- /charts/nri-prometheus/README.md: -------------------------------------------------------------------------------- 1 | # nri-prometheus 2 | 3 | A Helm chart to deploy the New Relic Prometheus OpenMetrics integration 4 | 5 | **Homepage:** 6 | 7 | # Helm installation 8 | 9 | You can install this chart using [`nri-bundle`](https://github.com/newrelic/helm-charts/tree/master/charts/nri-bundle) located in the 10 | [helm-charts repository](https://github.com/newrelic/helm-charts) or directly from this repository by adding this Helm repository: 11 | 12 | ```shell 13 | helm repo add nri-prometheus https://newrelic.github.io/nri-prometheus 14 | helm upgrade --install newrelic-prometheus nri-prometheus/nri-prometheus -f your-custom-values.yaml 15 | ``` 16 | 17 | ## Source Code 18 | 19 | * 20 | * 21 | 22 | ## Scraping services and endpoints 23 | 24 | When a service is labeled or annotated with `scrape_enabled_label` (defaults to `prometheus.io/scrape`), 25 | `nri-prometheus` will attempt to hit the service directly, rather than the endpoints behind it. 26 | 27 | This is the default behavior for compatibility reasons, but is known to cause issues if more than one endpoint 28 | is behind the service, as metric queries will be load-balanced as well leading to inaccurate histograms. 29 | 30 | In order to change this behaviour set `scrape_endpoints` to `true` and `scrape_services` to `false`. 31 | This will instruct `nri-prometheus` to scrape the underlying endpoints, as Prometheus server does. 32 | 33 | Existing users that are switching to this behavior should note that, depending on the number of endpoints 34 | behind the services in the cluster the load and the metrics reported by those, data ingestion might see 35 | an increase when flipping this option. Resource requirements might also be impacted, again depending on the number of new targets. 36 | 37 | While it is technically possible to set both `scrape_services` and `scrape_endpoints` to true, we do no recommend 38 | doing so as it will lead to redundant metrics being processed, 39 | 40 | ## Values managed globally 41 | 42 | This chart implements the [New Relic's common Helm library](https://github.com/newrelic/helm-charts/tree/master/library/common-library) which 43 | means that it honors a wide range of defaults and globals common to most New Relic Helm charts. 44 | 45 | Options that can be defined globally include `affinity`, `nodeSelector`, `tolerations`, `proxy` and others. The full list can be found at 46 | [user's guide of the common library](https://github.com/newrelic/helm-charts/blob/master/library/common-library/README.md). 47 | 48 | ## Chart particularities 49 | 50 | ### Low data mode 51 | See this snippet from the `values.yaml` file: 52 | ```yaml 53 | global: 54 | lowDataMode: false 55 | lowDataMode: false 56 | ``` 57 | 58 | To reduce the amount ot metrics we send to New Relic, enabling the `lowDataMode` will add [these transformations](static/lowdatamodedefaults.yaml): 59 | ```yaml 60 | transformations: 61 | - description: "Low data mode defaults" 62 | ignore_metrics: 63 | # Ignore the following metrics. 64 | # These metrics are already collected by the New Relic Kubernetes Integration. 65 | - prefixes: 66 | - kube_ 67 | - container_ 68 | - machine_ 69 | - cadvisor_ 70 | ``` 71 | 72 | ## Values 73 | 74 | | Key | Type | Default | Description | 75 | |-----|------|---------|-------------| 76 | | affinity | object | `{}` | Sets pod/node affinities. Can be configured also with `global.affinity` | 77 | | cluster | string | `""` | Name of the Kubernetes cluster monitored. Can be configured also with `global.cluster` | 78 | | config | object | See `values.yaml` | Provides your own `config.yaml` for this integration. Ref: https://docs.newrelic.com/docs/infrastructure/prometheus-integrations/install-configure-openmetrics/configure-prometheus-openmetrics-integrations/#example-configuration-file | 79 | | containerSecurityContext | object | `{}` | Sets security context (at container level). Can be configured also with `global.containerSecurityContext` | 80 | | customSecretLicenseKey | string | `""` | In case you don't want to have the license key in you values, this allows you to point to which secret key is the license key located. Can be configured also with `global.customSecretLicenseKey` | 81 | | customSecretName | string | `""` | In case you don't want to have the license key in you values, this allows you to point to a user created secret to get the key from there. Can be configured also with `global.customSecretName` | 82 | | dnsConfig | object | `{}` | Sets pod's dnsConfig. Can be configured also with `global.dnsConfig` | 83 | | fedramp.enabled | bool | false | Enables FedRAMP. Can be configured also with `global.fedramp.enabled` | 84 | | fullnameOverride | string | `""` | Override the full name of the release | 85 | | hostNetwork | bool | `false` | Sets pod's hostNetwork. Can be configured also with `global.hostNetwork` | 86 | | image | object | See `values.yaml` | Image for the New Relic Kubernetes integration | 87 | | image.pullSecrets | list | `[]` | The secrets that are needed to pull images from a custom registry. | 88 | | labels | object | `{}` | Additional labels for chart objects. Can be configured also with `global.labels` | 89 | | licenseKey | string | `""` | This set this license key to use. Can be configured also with `global.licenseKey` | 90 | | lowDataMode | bool | false | Reduces number of metrics sent in order to reduce costs. Can be configured also with `global.lowDataMode` | 91 | | nameOverride | string | `""` | Override the name of the chart | 92 | | nodeSelector | object | `{}` | Sets pod's node selector. Can be configured also with `global.nodeSelector` | 93 | | nrStaging | bool | false | Send the metrics to the staging backend. Requires a valid staging license key. Can be configured also with `global.nrStaging` | 94 | | podAnnotations | object | `{}` | Annotations to be added to all pods created by the integration. | 95 | | podLabels | object | `{}` | Additional labels for chart pods. Can be configured also with `global.podLabels` | 96 | | podSecurityContext | object | `{}` | Sets security context (at pod level). Can be configured also with `global.podSecurityContext` | 97 | | priorityClassName | string | `""` | Sets pod's priorityClassName. Can be configured also with `global.priorityClassName` | 98 | | proxy | string | `""` | Configures the integration to send all HTTP/HTTPS request through the proxy in that URL. The URL should have a standard format like `https://user:password@hostname:port`. Can be configured also with `global.proxy` | 99 | | rbac.create | bool | `true` | Specifies whether RBAC resources should be created | 100 | | resources | object | `{}` | | 101 | | serviceAccount.annotations | object | `{}` | Add these annotations to the service account we create. Can be configured also with `global.serviceAccount.annotations` | 102 | | serviceAccount.create | bool | `true` | Configures if the service account should be created or not. Can be configured also with `global.serviceAccount.create` | 103 | | serviceAccount.name | string | `nil` | Change the name of the service account. This is honored if you disable on this cahrt the creation of the service account so you can use your own. Can be configured also with `global.serviceAccount.name` | 104 | | tolerations | list | `[]` | Sets pod's tolerations to node taints. Can be configured also with `global.tolerations` | 105 | | verboseLog | bool | false | Sets the debug logs to this integration or all integrations if it is set globally. Can be configured also with `global.verboseLog` | 106 | 107 | ## Maintainers 108 | 109 | * [alvarocabanas](https://github.com/alvarocabanas) 110 | * [carlossscastro](https://github.com/carlossscastro) 111 | * [sigilioso](https://github.com/sigilioso) 112 | * [gsanchezgavier](https://github.com/gsanchezgavier) 113 | * [kang-makes](https://github.com/kang-makes) 114 | * [marcsanmi](https://github.com/marcsanmi) 115 | * [paologallinaharbur](https://github.com/paologallinaharbur) 116 | * [roobre](https://github.com/roobre) 117 | -------------------------------------------------------------------------------- /charts/nri-prometheus/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | {{ template "chart.deprecationWarning" . }} 3 | 4 | {{ template "chart.description" . }} 5 | 6 | {{ template "chart.homepageLine" . }} 7 | 8 | # Helm installation 9 | 10 | You can install this chart using [`nri-bundle`](https://github.com/newrelic/helm-charts/tree/master/charts/nri-bundle) located in the 11 | [helm-charts repository](https://github.com/newrelic/helm-charts) or directly from this repository by adding this Helm repository: 12 | 13 | ```shell 14 | helm repo add nri-prometheus https://newrelic.github.io/nri-prometheus 15 | helm upgrade --install newrelic-prometheus nri-prometheus/nri-prometheus -f your-custom-values.yaml 16 | ``` 17 | 18 | {{ template "chart.sourcesSection" . }} 19 | 20 | ## Scraping services and endpoints 21 | 22 | When a service is labeled or annotated with `scrape_enabled_label` (defaults to `prometheus.io/scrape`), 23 | `nri-prometheus` will attempt to hit the service directly, rather than the endpoints behind it. 24 | 25 | This is the default behavior for compatibility reasons, but is known to cause issues if more than one endpoint 26 | is behind the service, as metric queries will be load-balanced as well leading to inaccurate histograms. 27 | 28 | In order to change this behaviour set `scrape_endpoints` to `true` and `scrape_services` to `false`. 29 | This will instruct `nri-prometheus` to scrape the underlying endpoints, as Prometheus server does. 30 | 31 | Existing users that are switching to this behavior should note that, depending on the number of endpoints 32 | behind the services in the cluster the load and the metrics reported by those, data ingestion might see 33 | an increase when flipping this option. Resource requirements might also be impacted, again depending on the number of new targets. 34 | 35 | While it is technically possible to set both `scrape_services` and `scrape_endpoints` to true, we do no recommend 36 | doing so as it will lead to redundant metrics being processed, 37 | 38 | ## Values managed globally 39 | 40 | This chart implements the [New Relic's common Helm library](https://github.com/newrelic/helm-charts/tree/master/library/common-library) which 41 | means that it honors a wide range of defaults and globals common to most New Relic Helm charts. 42 | 43 | Options that can be defined globally include `affinity`, `nodeSelector`, `tolerations`, `proxy` and others. The full list can be found at 44 | [user's guide of the common library](https://github.com/newrelic/helm-charts/blob/master/library/common-library/README.md). 45 | 46 | ## Chart particularities 47 | 48 | ### Low data mode 49 | See this snippet from the `values.yaml` file: 50 | ```yaml 51 | global: 52 | lowDataMode: false 53 | lowDataMode: false 54 | ``` 55 | 56 | To reduce the amount ot metrics we send to New Relic, enabling the `lowDataMode` will add [these transformations](static/lowdatamodedefaults.yaml): 57 | ```yaml 58 | transformations: 59 | - description: "Low data mode defaults" 60 | ignore_metrics: 61 | # Ignore the following metrics. 62 | # These metrics are already collected by the New Relic Kubernetes Integration. 63 | - prefixes: 64 | - kube_ 65 | - container_ 66 | - machine_ 67 | - cadvisor_ 68 | ``` 69 | 70 | {{ template "chart.valuesSection" . }} 71 | 72 | {{ if .Maintainers }} 73 | ## Maintainers 74 | {{ range .Maintainers }} 75 | {{- if .Name }} 76 | {{- if .Url }} 77 | * [{{ .Name }}]({{ .Url }}) 78 | {{- else }} 79 | * {{ .Name }} 80 | {{- end }} 81 | {{- end }} 82 | {{- end }} 83 | {{- end }} 84 | -------------------------------------------------------------------------------- /charts/nri-prometheus/ci/test-lowdatamode-values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | licenseKey: 1234567890abcdef1234567890abcdef12345678 3 | cluster: test-cluster 4 | 5 | lowDataMode: true 6 | 7 | image: 8 | repository: e2e/nri-prometheus 9 | tag: "test" # Defaults to chart's appVersion 10 | -------------------------------------------------------------------------------- /charts/nri-prometheus/ci/test-override-global-lowdatamode.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | licenseKey: 1234567890abcdef1234567890abcdef12345678 3 | cluster: test-cluster 4 | lowDataMode: true 5 | 6 | lowDataMode: false 7 | 8 | image: 9 | repository: e2e/nri-prometheus 10 | tag: "test" # Defaults to chart's appVersion 11 | -------------------------------------------------------------------------------- /charts/nri-prometheus/ci/test-values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | licenseKey: 1234567890abcdef1234567890abcdef12345678 3 | cluster: test-cluster 4 | 5 | lowDataMode: true 6 | 7 | nameOverride: my-custom-name 8 | 9 | image: 10 | registry: 11 | repository: e2e/nri-prometheus 12 | tag: "test" 13 | imagePullPolicy: IfNotPresent 14 | 15 | resources: 16 | limits: 17 | cpu: 200m 18 | memory: 512Mi 19 | requests: 20 | cpu: 100m 21 | memory: 256Mi 22 | 23 | rbac: 24 | create: true 25 | 26 | serviceAccount: 27 | # Specifies whether a ServiceAccount should be created 28 | create: true 29 | # The name of the ServiceAccount to use. 30 | # If not set and create is true, a name is generated using the name template 31 | name: "" 32 | # Specify any annotations to add to the ServiceAccount 33 | annotations: 34 | foo: bar 35 | 36 | # If you wish to provide your own config.yaml file include it under config: 37 | # the sample config file is included here as an example. 38 | config: 39 | scrape_duration: "60s" 40 | scrape_timeout: "15s" 41 | 42 | scrape_services: false 43 | scrape_endpoints: true 44 | 45 | audit: false 46 | 47 | insecure_skip_verify: false 48 | 49 | scrape_enabled_label: "prometheus.io/scrape" 50 | 51 | require_scrape_enabled_label_for_nodes: true 52 | 53 | transformations: 54 | - description: "Custom transformation Example" 55 | rename_attributes: 56 | - metric_prefix: "foo_" 57 | attributes: 58 | old_label: "newLabel" 59 | ignore_metrics: 60 | - prefixes: 61 | - bar_ 62 | copy_attributes: 63 | - from_metric: "foo_info" 64 | to_metrics: "foo_" 65 | match_by: 66 | - namespace 67 | 68 | podAnnotations: 69 | custom-pod-annotation: test 70 | 71 | podSecurityContext: 72 | runAsUser: 1000 73 | runAsGroup: 3000 74 | fsGroup: 2000 75 | 76 | containerSecurityContext: 77 | runAsUser: 2000 78 | 79 | tolerations: 80 | - key: "key1" 81 | operator: "Exists" 82 | effect: "NoSchedule" 83 | 84 | nodeSelector: 85 | kubernetes.io/os: linux 86 | 87 | affinity: 88 | nodeAffinity: 89 | requiredDuringSchedulingIgnoredDuringExecution: 90 | nodeSelectorTerms: 91 | - matchExpressions: 92 | - key: kubernetes.io/os 93 | operator: In 94 | values: 95 | - linux 96 | 97 | nrStaging: false 98 | 99 | fedramp: 100 | enabled: true 101 | 102 | proxy: 103 | 104 | verboseLog: true 105 | -------------------------------------------------------------------------------- /charts/nri-prometheus/static/lowdatamodedefaults.yaml: -------------------------------------------------------------------------------- 1 | transformations: 2 | - description: "Low data mode defaults" 3 | ignore_metrics: 4 | # Ignore the following metrics. 5 | # These metrics are already collected by the New Relic Kubernetes Integration. 6 | - prefixes: 7 | - kube_ 8 | - container_ 9 | - machine_ 10 | - cadvisor_ 11 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | 3 | {{/* 4 | Returns mergeTransformations 5 | Helm can't merge maps of different types. Need to manually create a `transformations` section. 6 | */}} 7 | {{- define "nri-prometheus.mergeTransformations" -}} 8 | {{/* Remove current `transformations` from config. */}} 9 | {{- omit .Values.config "transformations" | toYaml | nindent 4 -}} 10 | {{/* Create new `transformations` yaml section with merged configs from .Values.config.transformations and lowDataMode. */}} 11 | transformations: 12 | {{- .Values.config.transformations | toYaml | nindent 4 -}} 13 | {{ $lowDataDefault := .Files.Get "static/lowdatamodedefaults.yaml" | fromYaml }} 14 | {{- $lowDataDefault.transformations | toYaml | nindent 4 -}} 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "newrelic.common.naming.fullname" . }} 6 | labels: 7 | {{- include "newrelic.common.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: [""] 10 | resources: 11 | - "nodes" 12 | - "nodes/metrics" 13 | - "nodes/stats" 14 | - "nodes/proxy" 15 | - "pods" 16 | - "services" 17 | - "endpoints" 18 | verbs: ["get", "list", "watch"] 19 | - nonResourceURLs: 20 | - /metrics 21 | verbs: 22 | - get 23 | {{- end -}} 24 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "newrelic.common.naming.fullname" . }} 6 | labels: 7 | {{- include "newrelic.common.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: {{ include "newrelic.common.naming.fullname" . }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "newrelic.common.serviceAccount.name" . }} 15 | namespace: {{ .Release.Namespace }} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | metadata: 3 | name: {{ include "newrelic.common.naming.fullname" . }} 4 | namespace: {{ .Release.Namespace }} 5 | labels: 6 | {{- include "newrelic.common.labels" . | nindent 4 }} 7 | apiVersion: v1 8 | data: 9 | config.yaml: | 10 | cluster_name: {{ include "newrelic.common.cluster" . }} 11 | {{- if .Values.config -}} 12 | {{- if and (.Values.config.transformations) (include "newrelic.common.lowDataMode" .) -}} 13 | {{- include "nri-prometheus.mergeTransformations" . -}} 14 | {{- else if (include "newrelic.common.lowDataMode" .) -}} 15 | {{ $lowDataDefault := .Files.Get "static/lowdatamodedefaults.yaml" | fromYaml }} 16 | {{- mergeOverwrite (deepCopy .Values.config) $lowDataDefault | toYaml | nindent 4 -}} 17 | {{- else }} 18 | {{- .Values.config | toYaml | nindent 4 -}} 19 | {{- end -}} 20 | {{- end -}} 21 | 22 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "newrelic.common.naming.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "newrelic.common.labels" . | nindent 4 }} 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | {{- /* We cannot use the common library here because of a legacy issue */}} 13 | {{- /* `selector` is inmutable and the previous chart did not have all the idiomatic labels */}} 14 | app.kubernetes.io/name: {{ include "newrelic.common.naming.name" . }} 15 | template: 16 | metadata: 17 | annotations: 18 | checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 19 | {{- with .Values.podAnnotations }} 20 | {{- toYaml . | nindent 8 }} 21 | {{- end }} 22 | labels: 23 | {{- include "newrelic.common.labels.podLabels" . | nindent 8 }} 24 | spec: 25 | serviceAccountName: {{ include "newrelic.common.serviceAccount.name" . }} 26 | {{- with include "newrelic.common.securityContext.pod" . }} 27 | securityContext: 28 | {{- . | nindent 8 }} 29 | {{- end }} 30 | {{- with include "newrelic.common.images.renderPullSecrets" ( dict "pullSecrets" (list .Values.image.pullSecrets) "context" .) }} 31 | imagePullSecrets: 32 | {{- . | nindent 8 }} 33 | {{- end }} 34 | containers: 35 | - name: nri-prometheus 36 | {{- with include "newrelic.common.securityContext.container" . }} 37 | securityContext: 38 | {{- . | nindent 10 }} 39 | {{- end }} 40 | image: {{ include "newrelic.common.images.image" ( dict "imageRoot" .Values.image "context" .) }} 41 | imagePullPolicy: {{ .Values.image.pullPolicy }} 42 | args: 43 | - "--configfile=/etc/nri-prometheus/config.yaml" 44 | ports: 45 | - containerPort: 8080 46 | volumeMounts: 47 | - name: config-volume 48 | mountPath: /etc/nri-prometheus/ 49 | env: 50 | - name: "LICENSE_KEY" 51 | valueFrom: 52 | secretKeyRef: 53 | name: {{ include "newrelic.common.license.secretName" . }} 54 | key: {{ include "newrelic.common.license.secretKeyName" . }} 55 | {{- if (include "newrelic.common.nrStaging" .) }} 56 | - name: "METRIC_API_URL" 57 | value: "https://staging-metric-api.newrelic.com/metric/v1/infra" 58 | {{- else if (include "newrelic.common.fedramp.enabled" .) }} 59 | - name: "METRIC_API_URL" 60 | value: "https://gov-metric-api.newrelic.com/metric/v1" 61 | {{- end }} 62 | {{- with include "newrelic.common.proxy" . }} 63 | - name: EMITTER_PROXY 64 | value: {{ . | quote }} 65 | {{- end }} 66 | {{- with include "newrelic.common.verboseLog" . }} 67 | - name: "VERBOSE" 68 | value: {{ . | quote }} 69 | {{- end }} 70 | - name: "BEARER_TOKEN_FILE" 71 | value: "/var/run/secrets/kubernetes.io/serviceaccount/token" 72 | - name: "CA_FILE" 73 | value: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 74 | resources: 75 | {{- toYaml .Values.resources | nindent 10 }} 76 | volumes: 77 | - name: config-volume 78 | configMap: 79 | name: {{ include "newrelic.common.naming.fullname" . }} 80 | {{- with include "newrelic.common.priorityClassName" . }} 81 | priorityClassName: {{ . }} 82 | {{- end }} 83 | {{- with include "newrelic.common.dnsConfig" . }} 84 | dnsConfig: 85 | {{- . | nindent 8 }} 86 | {{- end }} 87 | {{- with include "newrelic.common.nodeSelector" . }} 88 | nodeSelector: 89 | {{- . | nindent 8 }} 90 | {{- end }} 91 | {{- with include "newrelic.common.affinity" . }} 92 | affinity: 93 | {{- . | nindent 8 }} 94 | {{- end }} 95 | {{- with include "newrelic.common.tolerations" . }} 96 | tolerations: 97 | {{- . | nindent 8 }} 98 | {{- end }} 99 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- /* Common library will take care of creating the secret or not. */}} 2 | {{- include "newrelic.common.license.secret" . }} 3 | -------------------------------------------------------------------------------- /charts/nri-prometheus/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if (include "newrelic.common.serviceAccount.create" .) }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "newrelic.common.serviceAccount.name" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "newrelic.common.labels" . | nindent 4 }} 9 | {{- with (include "newrelic.common.serviceAccount.annotations" .) }} 10 | annotations: 11 | {{- . | nindent 4 }} 12 | {{- end }} 13 | {{- end -}} 14 | -------------------------------------------------------------------------------- /charts/nri-prometheus/tests/configmap_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test nri-prometheus configmap 2 | templates: 3 | - templates/configmap.yaml 4 | - templates/deployment.yaml 5 | tests: 6 | - it: creates the config map with default config in values.yaml and cluster_name. 7 | set: 8 | licenseKey: fakeLicense 9 | cluster: my-cluster-name 10 | asserts: 11 | - equal: 12 | path: data["config.yaml"] 13 | value: |- 14 | cluster_name: my-cluster-name 15 | audit: false 16 | insecure_skip_verify: false 17 | require_scrape_enabled_label_for_nodes: true 18 | scrape_enabled_label: prometheus.io/scrape 19 | scrape_endpoints: false 20 | scrape_services: true 21 | transformations: [] 22 | template: templates/configmap.yaml 23 | 24 | - it: creates the config map with lowDataMode. 25 | set: 26 | licenseKey: fakeLicense 27 | cluster: my-cluster-name 28 | lowDataMode: true 29 | asserts: 30 | - equal: 31 | path: data["config.yaml"] 32 | value: |- 33 | cluster_name: my-cluster-name 34 | audit: false 35 | insecure_skip_verify: false 36 | require_scrape_enabled_label_for_nodes: true 37 | scrape_enabled_label: prometheus.io/scrape 38 | scrape_endpoints: false 39 | scrape_services: true 40 | transformations: 41 | - description: Low data mode defaults 42 | ignore_metrics: 43 | - prefixes: 44 | - kube_ 45 | - container_ 46 | - machine_ 47 | - cadvisor_ 48 | template: templates/configmap.yaml 49 | 50 | - it: merges existing transformation with lowDataMode. 51 | set: 52 | licenseKey: fakeLicense 53 | cluster: my-cluster-name 54 | lowDataMode: true 55 | config: 56 | transformations: 57 | - description: Custom transformation Example 58 | rename_attributes: 59 | - metric_prefix: test_ 60 | attributes: 61 | container_name: containerName 62 | asserts: 63 | - equal: 64 | path: data["config.yaml"] 65 | value: |- 66 | cluster_name: my-cluster-name 67 | audit: false 68 | insecure_skip_verify: false 69 | require_scrape_enabled_label_for_nodes: true 70 | scrape_enabled_label: prometheus.io/scrape 71 | scrape_endpoints: false 72 | scrape_services: true 73 | transformations: 74 | - description: Custom transformation Example 75 | rename_attributes: 76 | - attributes: 77 | container_name: containerName 78 | metric_prefix: test_ 79 | - description: Low data mode defaults 80 | ignore_metrics: 81 | - prefixes: 82 | - kube_ 83 | - container_ 84 | - machine_ 85 | - cadvisor_ 86 | template: templates/configmap.yaml 87 | -------------------------------------------------------------------------------- /charts/nri-prometheus/tests/deployment_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test deployment 2 | templates: 3 | - templates/deployment.yaml 4 | - templates/configmap.yaml 5 | 6 | release: 7 | name: release 8 | 9 | tests: 10 | - it: adds defaults. 11 | set: 12 | licenseKey: fakeLicense 13 | cluster: test 14 | asserts: 15 | - equal: 16 | path: spec.template.metadata.labels["app.kubernetes.io/instance"] 17 | value: release 18 | template: templates/deployment.yaml 19 | - equal: 20 | path: spec.template.metadata.labels["app.kubernetes.io/name"] 21 | value: nri-prometheus 22 | template: templates/deployment.yaml 23 | - equal: 24 | path: spec.selector.matchLabels 25 | value: 26 | app.kubernetes.io/name: nri-prometheus 27 | template: templates/deployment.yaml 28 | - isNotEmpty: 29 | path: spec.template.metadata.annotations["checksum/config"] 30 | template: templates/deployment.yaml 31 | 32 | - it: adds METRIC_API_URL when nrStaging is true. 33 | set: 34 | licenseKey: fakeLicense 35 | cluster: test 36 | nrStaging: true 37 | asserts: 38 | - contains: 39 | path: spec.template.spec.containers[0].env 40 | content: 41 | name: "METRIC_API_URL" 42 | value: "https://staging-metric-api.newrelic.com/metric/v1/infra" 43 | template: templates/deployment.yaml 44 | 45 | - it: adds FedRamp endpoint when FedRamp is enabled. 46 | set: 47 | licenseKey: fakeLicense 48 | cluster: test 49 | fedramp: 50 | enabled: true 51 | asserts: 52 | - contains: 53 | path: spec.template.spec.containers[0].env 54 | content: 55 | name: "METRIC_API_URL" 56 | value: "https://gov-metric-api.newrelic.com/metric/v1" 57 | template: templates/deployment.yaml 58 | 59 | - it: adds proxy when enabled. 60 | set: 61 | licenseKey: fakeLicense 62 | cluster: test 63 | proxy: "https://my-proxy:9999" 64 | asserts: 65 | - contains: 66 | path: spec.template.spec.containers[0].env 67 | content: 68 | name: "EMITTER_PROXY" 69 | value: "https://my-proxy:9999" 70 | template: templates/deployment.yaml 71 | 72 | - it: set priorityClassName. 73 | set: 74 | licenseKey: fakeLicense 75 | cluster: test 76 | priorityClassName: foo 77 | asserts: 78 | - equal: 79 | path: spec.template.spec.priorityClassName 80 | value: foo 81 | template: templates/deployment.yaml 82 | 83 | -------------------------------------------------------------------------------- /charts/nri-prometheus/tests/labels_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test object names 2 | templates: 3 | - templates/clusterrole.yaml 4 | - templates/clusterrolebinding.yaml 5 | - templates/configmap.yaml 6 | - templates/deployment.yaml 7 | - templates/secret.yaml 8 | - templates/serviceaccount.yaml 9 | 10 | release: 11 | name: release 12 | revision: 13 | 14 | tests: 15 | - it: adds default labels. 16 | set: 17 | licenseKey: fakeLicense 18 | cluster: test 19 | asserts: 20 | - equal: 21 | path: metadata.labels["app.kubernetes.io/instance"] 22 | value: release 23 | - equal: 24 | path: metadata.labels["app.kubernetes.io/managed-by"] 25 | value: Helm 26 | - equal: 27 | path: metadata.labels["app.kubernetes.io/name"] 28 | value: nri-prometheus 29 | - isNotEmpty: 30 | path: metadata.labels["app.kubernetes.io/version"] 31 | - isNotEmpty: 32 | path: metadata.labels["helm.sh/chart"] 33 | -------------------------------------------------------------------------------- /cmd/k8s-target-retriever/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "k8s.io/client-go/util/homedir" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "github.com/newrelic/nri-prometheus/internal/pkg/endpoints" 15 | ) 16 | 17 | var kubeConfigFile = flag.String("kubeconfig", "", "location of the kube config file. Defaults to ~/.kube/config") 18 | 19 | func init() { 20 | flag.Usage = func() { 21 | fmt.Printf("Usage of %s:\n", os.Args[0]) 22 | fmt.Println("") 23 | fmt.Println("k8s-target-retriever is a simple helper program to run the KubernetesTargetRetriever logic on your own machine, for debugging purposes.") 24 | fmt.Println("") 25 | flag.PrintDefaults() 26 | } 27 | } 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | if *kubeConfigFile == "" { 33 | *kubeConfigFile = filepath.Join(homedir.HomeDir(), ".kube", "config") 34 | } 35 | 36 | kubeconf := endpoints.WithKubeConfig(*kubeConfigFile) 37 | ktr, err := endpoints.NewKubernetesTargetRetriever("prometheus.io/scrape", false, true, true, kubeconf) 38 | if err != nil { 39 | logrus.Fatalf("could not create KubernetesTargetRetriever: %v", err) 40 | } 41 | 42 | if err := ktr.Watch(); err != nil { 43 | logrus.Fatalf("could not watch for events: %v", err) 44 | } 45 | 46 | logrus.Infoln("connected to cluster, watching for targets") 47 | 48 | for range time.Tick(time.Second * 7) { 49 | targets, err := ktr.GetTargets() 50 | logrus.Infof("###################################") 51 | 52 | if err != nil { 53 | logrus.Fatalf("could not get targets: %v", err) 54 | } 55 | for _, b := range targets { 56 | logrus.Infof("%s[%s] %s", b.Name, b.Object.Kind, b.URL.String()) 57 | } 58 | logrus.Infof("###################################") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/nri-prometheus/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "path/filepath" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | "github.com/newrelic/nri-prometheus/internal/integration" 14 | 15 | "github.com/newrelic/infra-integrations-sdk/v4/args" 16 | "github.com/newrelic/nri-prometheus/internal/cmd/scraper" 17 | "github.com/pkg/errors" 18 | "github.com/sirupsen/logrus" 19 | "github.com/spf13/viper" 20 | ) 21 | 22 | // ArgumentList Available Arguments 23 | type ArgumentList struct { 24 | ConfigPath string `default:"" help:"Path to the config file"` 25 | Configfile string `default:"" help:"Deprecated. --config_path takes precedence if both are set"` 26 | NriHostID string `default:"" help:"Host ID to be replace the targetName and scrappedTargetName if localhost"` 27 | } 28 | 29 | func loadConfig() (*scraper.Config, error) { 30 | c := ArgumentList{} 31 | err := args.SetupArgs(&c) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | cfg := viper.New() 37 | cfg.SetConfigType("yaml") 38 | 39 | if c.Configfile != "" && c.ConfigPath == "" { 40 | c.ConfigPath = c.Configfile 41 | } 42 | 43 | if c.ConfigPath != "" { 44 | cfg.AddConfigPath(filepath.Dir(c.ConfigPath)) 45 | cfg.SetConfigName(filepath.Base(c.ConfigPath)) 46 | } else { 47 | cfg.SetConfigName("config") 48 | cfg.AddConfigPath("/etc/nri-prometheus/") 49 | cfg.AddConfigPath(".") 50 | } 51 | 52 | setViperDefaults(cfg) 53 | 54 | err = cfg.ReadInConfig() 55 | if err != nil { 56 | return nil, errors.Wrap(err, "could not read configuration") 57 | } 58 | 59 | if cfg.Get("entity_definitions") != nil { 60 | logrus.Debug("entity_definitions are deprecated and won't be processed since v2.14.0") 61 | } 62 | 63 | var scraperCfg scraper.Config 64 | bindViperEnv(cfg, scraperCfg) 65 | err = cfg.Unmarshal(&scraperCfg) 66 | 67 | if err != nil { 68 | return nil, errors.Wrap(err, "could not parse configuration file") 69 | } 70 | 71 | // Set emitter default according to standalone mode. 72 | if len(scraperCfg.Emitters) == 0 { 73 | if scraperCfg.Standalone { 74 | scraperCfg.Emitters = append(scraperCfg.Emitters, "telemetry") 75 | } else { 76 | scraperCfg.Emitters = append(scraperCfg.Emitters, "infra-sdk") 77 | } 78 | } 79 | 80 | if scraperCfg.MetricAPIURL == "" { 81 | scraperCfg.MetricAPIURL = determineMetricAPIURL(string(scraperCfg.LicenseKey)) 82 | } 83 | scraperCfg.HostID = c.NriHostID 84 | 85 | return &scraperCfg, nil 86 | } 87 | 88 | // setViperDefaults loads the default configuration into the given Viper registry. 89 | func setViperDefaults(viper *viper.Viper) { 90 | viper.SetDefault("debug", false) 91 | viper.SetDefault("verbose", false) 92 | viper.SetDefault("audit", false) 93 | viper.SetDefault("scrape_enabled_label", "prometheus.io/scrape") 94 | viper.SetDefault("require_scrape_enabled_label_for_nodes", true) 95 | viper.SetDefault("scrape_timeout", 5*time.Second) 96 | viper.SetDefault("scrape_duration", "30s") 97 | // Note that this default is taken directly from the Prometheus server acceptHeader prior to the open-metrics support. https://github.com/prometheus/prometheus/commit/9c03e11c2cf2ad6c638567471faa5c0f6c11ba3d 98 | viper.SetDefault("scrape_accept_header", "text/plain;version=0.0.4;q=1,*/*;q=0.1") 99 | viper.SetDefault("emitter_harvest_period", fmt.Sprint(integration.BoundedHarvesterDefaultHarvestPeriod)) 100 | viper.SetDefault("min_emitter_harvest_period", fmt.Sprint(integration.BoundedHarvesterDefaultMinReportInterval)) 101 | viper.SetDefault("max_stored_metrics", fmt.Sprint(integration.BoundedHarvesterDefaultMetricsCap)) 102 | viper.SetDefault("auto_decorate", false) 103 | viper.SetDefault("insecure_skip_verify", false) 104 | viper.SetDefault("standalone", true) 105 | viper.SetDefault("disable_autodiscovery", false) 106 | viper.SetDefault("scrape_services", true) 107 | viper.SetDefault("scrape_endpoints", false) 108 | viper.SetDefault("percentiles", []float64{50.0, 95.0, 99.0}) 109 | viper.SetDefault("worker_threads", 4) 110 | viper.SetDefault("self_metrics_listening_address", ":8080") 111 | } 112 | 113 | // bindViperEnv automatically binds the variables in given configuration struct to environment variables. 114 | // This is needed because Viper only takes environment variables into consideration for unmarshalling if they are also 115 | // defined in the configuration file. We need to be able to use environment variables even if such variable is not in 116 | // the config file. 117 | // For more information see https://github.com/spf13/viper/issues/188. 118 | func bindViperEnv(vCfg *viper.Viper, iface interface{}, parts ...string) { 119 | ifv := reflect.ValueOf(iface) 120 | ift := reflect.TypeOf(iface) 121 | for i := 0; i < ift.NumField(); i++ { 122 | v := ifv.Field(i) 123 | t := ift.Field(i) 124 | tv, ok := t.Tag.Lookup("mapstructure") 125 | if !ok { 126 | continue 127 | } 128 | switch v.Kind() { 129 | case reflect.Struct: 130 | bindViperEnv(vCfg, v.Interface(), append(parts, tv)...) 131 | default: 132 | _ = vCfg.BindEnv(strings.Join(append(parts, tv), "_")) 133 | } 134 | } 135 | } 136 | 137 | var ( 138 | regionLicenseRegex = regexp.MustCompile(`^([a-z]{2,3})[0-9]{2}x{1,2}`) 139 | metricAPIRegionURL = "https://metric-api.%s.newrelic.com/metric/v1/infra" 140 | // for historical reasons the US datacenter is the default Metric API 141 | defaultMetricAPIURL = "https://metric-api.newrelic.com/metric/v1/infra" 142 | ) 143 | 144 | // determineMetricAPIURL determines the Metric API URL based on the license key. 145 | // The first 5 characters of the license URL indicates the region. 146 | func determineMetricAPIURL(license string) string { 147 | m := regionLicenseRegex.FindStringSubmatch(license) 148 | if len(m) > 1 { 149 | return fmt.Sprintf(metricAPIRegionURL, m[1]) 150 | } 151 | 152 | return defaultMetricAPIURL 153 | } 154 | -------------------------------------------------------------------------------- /cmd/nri-prometheus/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/newrelic/nri-prometheus/internal/cmd/scraper" 12 | "github.com/newrelic/nri-prometheus/internal/pkg/endpoints" 13 | ) 14 | 15 | func TestDetermineMetricAPIURL(t *testing.T) { 16 | testCases := []struct { 17 | license string 18 | expectedURL string 19 | }{ 20 | // empty license 21 | {license: "", expectedURL: defaultMetricAPIURL}, 22 | // non-region license 23 | {license: "0123456789012345678901234567890123456789", expectedURL: defaultMetricAPIURL}, 24 | // four letter region 25 | {license: "eu01xx6789012345678901234567890123456789", expectedURL: fmt.Sprintf(metricAPIRegionURL, "eu")}, 26 | // five letter region 27 | {license: "gov01x6789012345678901234567890123456789", expectedURL: fmt.Sprintf(metricAPIRegionURL, "gov")}, 28 | } 29 | 30 | for _, tt := range testCases { 31 | actualURL := determineMetricAPIURL(tt.license) 32 | if actualURL != tt.expectedURL { 33 | t.Fatalf("URL does not match expected URL, got=%s, expected=%s", actualURL, tt.expectedURL) 34 | } 35 | } 36 | } 37 | 38 | func TestLoadConfig(t *testing.T) { 39 | expectedScrapper := scraper.Config{ 40 | MetricAPIURL: "https://metric-api.newrelic.com/metric/v1/infra", 41 | Verbose: true, 42 | Emitters: []string{"infra-sdk"}, 43 | ScrapeEnabledLabel: "prometheus.io/scrape", 44 | RequireScrapeEnabledLabelForNodes: true, 45 | ScrapeTimeout: 5 * time.Second, 46 | ScrapeServices: true, 47 | ScrapeDuration: "5s", 48 | ScrapeAcceptHeader: "text/plain;version=0.0.4;q=1,*/*;q=0.1", 49 | EmitterHarvestPeriod: "1s", 50 | MinEmitterHarvestPeriod: "200ms", 51 | MaxStoredMetrics: 10000, 52 | SelfMetricsListeningAddress: ":8080", 53 | TargetConfigs: []endpoints.TargetConfig{ 54 | { 55 | Description: "AAA", 56 | URLs: []string{"localhost:9121"}, 57 | TLSConfig: endpoints.TLSConfig{}, 58 | UseBearer: true, 59 | }, 60 | }, 61 | InsecureSkipVerify: true, 62 | WorkerThreads: 4, 63 | HostID: "awesome-host", 64 | } 65 | t.Setenv("CONFIG_PATH", "testdata/config-with-legacy-entity-definitions.yaml") 66 | t.Setenv("NRI_HOST_ID", "awesome-host") 67 | scraperCfg, err := loadConfig() 68 | if err != nil { 69 | t.Fatalf("error was not expected %v", err) 70 | } 71 | if !reflect.DeepEqual(*scraperCfg, expectedScrapper) { 72 | t.Fatalf("scraper retrieved not as expected, got=%v, expected=%v", *scraperCfg, expectedScrapper) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/nri-prometheus/fips.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //go:build fips 5 | // +build fips 6 | 7 | package main 8 | 9 | import ( 10 | _ "crypto/tls/fipsonly" 11 | ) 12 | -------------------------------------------------------------------------------- /cmd/nri-prometheus/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package main 4 | 5 | import ( 6 | "github.com/newrelic/nri-prometheus/internal/cmd/scraper" 7 | "github.com/newrelic/nri-prometheus/internal/integration" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func main() { 12 | cfg, err := loadConfig() 13 | if err != nil { 14 | logrus.WithError(err).Fatal("while loading configuration") 15 | } 16 | 17 | logrus.Infof("Starting New Relic's Prometheus OpenMetrics Integration version %s", integration.Version) 18 | logrus.Debugf("Config: %#v", cfg) 19 | 20 | err = scraper.Run(cfg) 21 | if err != nil { 22 | logrus.WithError(err).Fatal("error occurred while running scraper") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/nri-prometheus/testdata/config-with-legacy-entity-definitions.yaml: -------------------------------------------------------------------------------- 1 | standalone: false 2 | emitters: infra-sdk 3 | entity_definitions: [ 4 | { 5 | conditions: [ 6 | { 7 | attribute: "metricName", 8 | prefix: "redis_" 9 | } 10 | ], 11 | identifier: "targetName", 12 | name: "targetName", 13 | tags: { 14 | clusterName: null, 15 | targetName: null 16 | }, 17 | type: "REDIS" 18 | }, 19 | ] 20 | targets: 21 | - description: "AAA" 22 | urls: ["localhost:9121"] 23 | use_bearer: true 24 | verbose: true 25 | scrape_duration: "5s" 26 | insecure_skip_verify: true 27 | -------------------------------------------------------------------------------- /configs/nri-prometheus-config.yml.sample: -------------------------------------------------------------------------------- 1 | integrations: 2 | - name: nri-prometheus 3 | config: 4 | # When standalone is set to false nri-prometheus requires an infrastructure agent to work and send data. Defaults to true 5 | standalone: false 6 | 7 | # When running with infrastructure agent emitters will have to include infra-sdk 8 | emitters: infra-sdk 9 | 10 | # The name of your cluster. It's important to match other New Relic products to relate the data. 11 | cluster_name: "my_exporter" 12 | 13 | #targets: 14 | # - description: Secure etcd example 15 | # urls: ["https://192.168.3.1:2379", "https://192.168.3.2:2379", "https://192.168.3.3:2379"] 16 | # tls_config: 17 | # ca_file_path: "/etc/etcd/etcd-client-ca.crt" 18 | # cert_file_path: "/etc/etcd/etcd-client.crt" 19 | # key_file_path: "/etc/etcd/etcd-client.key" 20 | 21 | # Whether the integration should run in verbose mode or not. Defaults to false. 22 | verbose: false 23 | 24 | # Whether the integration should run in audit mode or not. Defaults to false. 25 | # Audit mode logs the uncompressed data sent to New Relic. Use this to log all data sent. 26 | # It does not include verbose mode. This can lead to a high log volume, use with care. 27 | audit: false 28 | 29 | # The HTTP client timeout when fetching data from endpoints. Defaults to "5s" if it is not set. 30 | # This timeout in seconds is passed as well as a X-Prometheus-Scrape-Timeout-Seconds header to the exporters 31 | # scrape_timeout: "5s" 32 | 33 | # Length in time to distribute the scraping from the endpoints. Default to "30s" if it is not set. 34 | scrape_duration: "5s" 35 | 36 | # Number of worker threads used for scraping targets. 37 | # For large clusters with many (>400) endpoints, slowly increase until scrape 38 | # time falls between the desired `scrape_duration`. 39 | # Increasing this value too much will result in huge memory consumption if too 40 | # many metrics are being scraped. 41 | # Default: 4 42 | # worker_threads: 4 43 | 44 | # Whether the integration should skip TLS verification or not. Defaults to false. 45 | insecure_skip_verify: false 46 | 47 | timeout: 10s 48 | -------------------------------------------------------------------------------- /deploy/local.yaml.example: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: nri-prometheus 6 | namespace: default 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | name: nri-prometheus 12 | rules: 13 | - apiGroups: [""] 14 | resources: 15 | - "nodes" 16 | - "nodes/metrics" 17 | - "nodes/stats" 18 | - "nodes/proxy" 19 | - "pods" 20 | - "services" 21 | - "endpoints" 22 | verbs: ["get", "list", "watch"] 23 | - nonResourceURLs: 24 | - /metrics 25 | verbs: 26 | - get 27 | --- 28 | apiVersion: rbac.authorization.k8s.io/v1 29 | kind: ClusterRoleBinding 30 | metadata: 31 | name: nri-prometheus 32 | roleRef: 33 | apiGroup: rbac.authorization.k8s.io 34 | kind: ClusterRole 35 | name: nri-prometheus 36 | subjects: 37 | - kind: ServiceAccount 38 | name: nri-prometheus 39 | namespace: default 40 | --- 41 | apiVersion: apps/v1 42 | kind: Deployment 43 | metadata: 44 | name: nri-prometheus 45 | namespace: default 46 | labels: 47 | app: nri-prometheus 48 | spec: 49 | replicas: 1 50 | selector: 51 | matchLabels: 52 | app: nri-prometheus 53 | template: 54 | metadata: 55 | labels: 56 | app: nri-prometheus 57 | spec: 58 | serviceAccountName: nri-prometheus 59 | containers: 60 | - name: nri-prometheus 61 | image: quay.io/newrelic/nri-prometheus 62 | args: 63 | - "--config_path=/etc/nri-prometheus/config.yaml" 64 | ports: 65 | - containerPort: 8080 66 | volumeMounts: 67 | - name: config-volume 68 | mountPath: /etc/nri-prometheus/ 69 | env: 70 | - name: "LICENSE_KEY" 71 | value: "" 72 | - name: "BEARER_TOKEN_FILE" 73 | value: "/var/run/secrets/kubernetes.io/serviceaccount/token" 74 | - name: "CA_FILE" 75 | value: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 76 | volumes: 77 | - name: config-volume 78 | configMap: 79 | name: nri-prometheus-cfg 80 | --- 81 | apiVersion: v1 82 | data: 83 | config.yaml: | 84 | # The name of your cluster. It's important to match other New Relic products to relate the data. 85 | cluster_name: "local-ci-test" 86 | # When standalone is set to false nri-prometheus requires an infrastructure agent to work and send data. Defaults to true 87 | # standalone: true 88 | # How often the integration should run. Defaults to 30s. 89 | # scrape_duration: "30s" 90 | # The HTTP client timeout when fetching data from targets. Defaults to 30s. 91 | # scrape_services Allows to enable scraping the service and not the endpoints behind. 92 | # When endpoints are scraped this is no longer needed 93 | scrape_services: true 94 | # scrape_endpoints Allows to enable scraping directly endpoints instead of services as prometheus service natively does. 95 | # Please notice that depending on the number of endpoints behind a service the load can increase considerably 96 | scrape_endpoints: false 97 | # scrape_timeout: "30s" 98 | # Wether the integration should run in verbose mode or not. Defaults to false. 99 | verbose: false 100 | # Whether the integration should run in audit mode or not. Defaults to false. 101 | # Audit mode logs the uncompressed data sent to New Relic. Use this to log all data sent. 102 | # It does not include verbose mode. This can lead to a high log volume, use with care. 103 | audit: false 104 | # Wether the integration should skip TLS verification or not. Defaults to false. 105 | insecure_skip_verify: false 106 | # The label used to identify scrapable targets. Defaults to "prometheus.io/scrape". 107 | scrape_enabled_label: "prometheus.io/scrape" 108 | # Set to true in order to stop autodiscovery in the k8s cluster. It can be useful when running the Pod with a service account 109 | # having limited privileges. Defaults to false. 110 | # disable_autodiscovery: false 111 | # Wether k8s nodes needs to be labelled to be scraped or not. Defaults to false. 112 | require_scrape_enabled_label_for_nodes: true 113 | worker_threads: 8 114 | #targets: 115 | # - description: Secure etcd example 116 | # urls: ["https://192.168.3.1:2379", "https://192.168.3.2:2379", "https://192.168.3.3:2379"] 117 | # tls_config: 118 | # ca_file_path: "/etc/etcd/etcd-client-ca.crt" 119 | # cert_file_path: "/etc/etcd/etcd-client.crt" 120 | # key_file_path: "/etc/etcd/etcd-client.key" 121 | transformations: 122 | # - description: "General processing rules" 123 | # rename_attributes: 124 | # - metric_prefix: "" 125 | # attributes: 126 | # container_name: "containerName" 127 | # pod_name: "podName" 128 | # namespace: "namespaceName" 129 | # node: "nodeName" 130 | # container: "containerName" 131 | # pod: "podName" 132 | # deployment: "deploymentName" 133 | kind: ConfigMap 134 | metadata: 135 | name: nri-prometheus-cfg 136 | namespace: default 137 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/newrelic/nri-prometheus 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/newrelic/infra-integrations-sdk/v4 v4.2.1 7 | github.com/newrelic/newrelic-telemetry-sdk-go v0.8.1 8 | github.com/pkg/errors v0.9.1 9 | github.com/prometheus/client_golang v1.22.0 10 | github.com/prometheus/client_model v0.6.1 11 | github.com/prometheus/common v0.62.0 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/spf13/viper v1.20.1 14 | github.com/stretchr/testify v1.10.0 15 | k8s.io/api v0.31.1 16 | k8s.io/apimachinery v0.31.1 17 | k8s.io/client-go v0.31.1 18 | ) 19 | 20 | require ( 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 24 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 25 | github.com/fsnotify/fsnotify v1.8.0 // indirect 26 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 29 | github.com/go-openapi/jsonreference v0.20.2 // indirect 30 | github.com/go-openapi/swag v0.22.4 // indirect 31 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/gnostic-models v0.6.8 // indirect 35 | github.com/google/go-cmp v0.7.0 // indirect 36 | github.com/google/gofuzz v1.2.0 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/imdario/mergo v0.3.13 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v1.0.2 // indirect 44 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 45 | github.com/newrelic/infrastructure-agent v0.1.0 // indirect 46 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 47 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 48 | github.com/prometheus/procfs v0.15.1 // indirect 49 | github.com/sagikazarmark/locafero v0.7.0 // indirect 50 | github.com/sourcegraph/conc v0.3.0 // indirect 51 | github.com/spf13/afero v1.12.0 // indirect 52 | github.com/spf13/cast v1.7.1 // indirect 53 | github.com/spf13/pflag v1.0.6 // indirect 54 | github.com/stretchr/objx v0.5.2 // indirect 55 | github.com/subosito/gotenv v1.6.0 // indirect 56 | github.com/x448/float16 v0.8.4 // indirect 57 | go.uber.org/atomic v1.9.0 // indirect 58 | go.uber.org/multierr v1.9.0 // indirect 59 | golang.org/x/net v0.38.0 // indirect 60 | golang.org/x/oauth2 v0.25.0 // indirect 61 | golang.org/x/sys v0.31.0 // indirect 62 | golang.org/x/term v0.30.0 // indirect 63 | golang.org/x/text v0.23.0 // indirect 64 | golang.org/x/time v0.8.0 // indirect 65 | google.golang.org/protobuf v1.36.5 // indirect 66 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 67 | gopkg.in/inf.v0 v0.9.1 // indirect 68 | gopkg.in/yaml.v2 v2.4.0 // indirect 69 | gopkg.in/yaml.v3 v3.0.1 // indirect 70 | k8s.io/klog/v2 v2.130.1 // indirect 71 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 72 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 73 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 74 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 75 | sigs.k8s.io/yaml v1.4.0 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /internal/cmd/scraper/scraper_test.go: -------------------------------------------------------------------------------- 1 | // Package scraper ... 2 | // Copyright 2019 New Relic Corporation. All rights reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | package scraper 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "path" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/newrelic/nri-prometheus/internal/pkg/endpoints" 18 | "github.com/stretchr/testify/require" 19 | 20 | "github.com/sirupsen/logrus" 21 | "github.com/spf13/viper" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | const fakeToken = "fakeToken" 26 | 27 | func TestLicenseKeyMasking(t *testing.T) { 28 | const licenseKeyString = "secret" 29 | licenseKey := LicenseKey(licenseKeyString) 30 | 31 | t.Run("Masks licenseKey in fmt.Sprintf (which uses same logic as Printf)", func(t *testing.T) { 32 | masked := fmt.Sprintf("%s", licenseKey) 33 | assert.Equal(t, masked, maskedLicenseKey) 34 | }) 35 | 36 | t.Run("Masks licenseKey in fmt.Sprint (which uses same logic as Print)", func(t *testing.T) { 37 | masked := fmt.Sprint(licenseKey) 38 | assert.Equal(t, masked, maskedLicenseKey) 39 | }) 40 | 41 | t.Run("Masks licenseKey in %#v formatting", func(t *testing.T) { 42 | masked := fmt.Sprintf("%#v", licenseKey) 43 | if strings.Contains(masked, licenseKeyString) { 44 | t.Error("found licenseKey in formatted string") 45 | } 46 | if !strings.Contains(masked, maskedLicenseKey) { 47 | t.Error("could not find masked password in formatted string") 48 | } 49 | }) 50 | 51 | t.Run("Able to convert licenseKey back to string", func(t *testing.T) { 52 | unmasked := string(licenseKey) 53 | assert.Equal(t, licenseKeyString, unmasked) 54 | }) 55 | } 56 | 57 | func TestLogrusDebugPrintMasksLicenseKey(t *testing.T) { 58 | const licenseKey = "SECRET_LICENSE_KEY" 59 | 60 | cfg := Config{ 61 | LicenseKey: licenseKey, 62 | } 63 | 64 | var b bytes.Buffer 65 | 66 | logrus.SetOutput(&b) 67 | logrus.SetLevel(logrus.DebugLevel) 68 | logrus.Debugf("Config: %#v", cfg) 69 | 70 | msg := b.String() 71 | if strings.Contains(msg, licenseKey) { 72 | t.Error("Log output contains the license key") 73 | } 74 | if !strings.Contains(msg, maskedLicenseKey) { 75 | t.Error("Log output does not contain the masked licenseKey") 76 | } 77 | } 78 | 79 | func TestConfigParseWithCustomType(t *testing.T) { 80 | const licenseKey = "MY_LICENSE_KEY" 81 | cfgStr := []byte(fmt.Sprintf(`LICENSE_KEY: %s`, licenseKey)) 82 | 83 | vip := viper.New() 84 | vip.SetConfigType("yaml") 85 | err := vip.ReadConfig(bytes.NewBuffer(cfgStr)) 86 | require.NoError(t, err) 87 | 88 | var cfg Config 89 | err = vip.Unmarshal(&cfg) 90 | require.NoError(t, err) 91 | 92 | assert.Equal(t, licenseKey, string(cfg.LicenseKey)) 93 | } 94 | 95 | func TestRunIntegrationOnceNoTokenAttached(t *testing.T) { 96 | dat, err := ioutil.ReadFile("./testData/testData.prometheus") 97 | require.NoError(t, err) 98 | counter := 0 99 | headers := http.Header{} 100 | 101 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 | w.WriteHeader(200) 103 | _, _ = w.Write(dat) 104 | headers = r.Header 105 | counter++ 106 | })) 107 | defer srv.Close() 108 | 109 | c := &Config{ 110 | TargetConfigs: []endpoints.TargetConfig{ 111 | { 112 | URLs: []string{srv.URL, srv.URL}, 113 | }, 114 | }, 115 | Emitters: []string{"stdout"}, 116 | Standalone: false, 117 | Verbose: true, 118 | ScrapeDuration: "500ms", 119 | } 120 | err = Run(c) 121 | require.NoError(t, err) 122 | require.Equal(t, 2, counter, "the scraper should have hit the mock exactly twice") 123 | require.Equal(t, "", headers.Get("Authorization"), "the scraper should not add any authorization token") 124 | } 125 | 126 | func TestScrapingAnsweringWithError(t *testing.T) { 127 | counter := 0 128 | 129 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 130 | w.WriteHeader(404) 131 | _, _ = w.Write(nil) 132 | counter++ 133 | })) 134 | 135 | defer srv.Close() 136 | 137 | c := &Config{ 138 | TargetConfigs: []endpoints.TargetConfig{ 139 | { 140 | URLs: []string{srv.URL, srv.URL}, 141 | }, 142 | }, 143 | Emitters: []string{"stdout"}, 144 | Standalone: false, 145 | Verbose: true, 146 | ScrapeDuration: "500ms", 147 | } 148 | err := Run(c) 149 | // Currently no error is returned in case a scraper does not return any data / err status code 150 | require.NoError(t, err) 151 | require.Equal(t, 2, counter, "the scraper should have hit the mock exactly twice") 152 | } 153 | 154 | func TestScrapingAnsweringUnexpectedData(t *testing.T) { 155 | counter := 0 156 | 157 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 158 | w.WriteHeader(200) 159 | _, _ = w.Write([]byte("{not valid string}`n`n\n\n\n Not valid string ")) 160 | counter++ 161 | })) 162 | 163 | defer srv.Close() 164 | 165 | c := &Config{ 166 | TargetConfigs: []endpoints.TargetConfig{ 167 | { 168 | URLs: []string{srv.URL, srv.URL}, 169 | }, 170 | }, 171 | Emitters: []string{"stdout"}, 172 | Standalone: false, 173 | Verbose: true, 174 | ScrapeDuration: "500ms", 175 | } 176 | err := Run(c) 177 | // Currently no error is returned in case a scraper does not return any data / err status code 178 | require.NoError(t, err) 179 | require.Equal(t, 2, counter, "the scraper should have hit the mock exactly twice") 180 | } 181 | 182 | func TestScrapingNotAnswering(t *testing.T) { 183 | c := &Config{ 184 | TargetConfigs: []endpoints.TargetConfig{ 185 | { 186 | URLs: []string{"127.1.1.0:9012"}, 187 | }, 188 | }, 189 | Emitters: []string{"stdout"}, 190 | Standalone: false, 191 | Verbose: true, 192 | ScrapeDuration: "500ms", 193 | ScrapeTimeout: time.Duration(500) * time.Millisecond, 194 | } 195 | 196 | // when 197 | err := Run(c) 198 | 199 | // then 200 | require.NoError(t, err) 201 | } 202 | 203 | func TestScrapingWithToken(t *testing.T) { 204 | headers := http.Header{} 205 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 206 | headers = r.Header 207 | w.WriteHeader(202) 208 | })) 209 | defer srv.Close() 210 | 211 | // Populate a fake token 212 | tempDir := t.TempDir() 213 | tokenFile := path.Join(tempDir, "fakeToken") 214 | err := ioutil.WriteFile(tokenFile, []byte(fakeToken), 0o444) 215 | require.NoError(t, err) 216 | 217 | c := &Config{ 218 | TargetConfigs: []endpoints.TargetConfig{ 219 | { 220 | URLs: []string{srv.URL}, 221 | UseBearer: true, 222 | }, 223 | }, 224 | BearerTokenFile: tokenFile, 225 | Emitters: []string{"stdout"}, 226 | Standalone: false, 227 | Verbose: true, 228 | ScrapeDuration: "500ms", 229 | ScrapeTimeout: time.Duration(500) * time.Millisecond, 230 | } 231 | 232 | // when 233 | err = Run(c) 234 | require.NoError(t, err) 235 | 236 | // then 237 | require.Equal(t, "Bearer "+fakeToken, headers.Get("Authorization")) 238 | } 239 | -------------------------------------------------------------------------------- /internal/integration/bounded_harvester.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | "time" 10 | 11 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // bindHarvester creates a boundedHarvester from an existing harvester. 16 | // It also returns a cancel channel to stop the periodic harvest goroutine. 17 | // The returned boundedHarvester always runs in a loop. 18 | func bindHarvester(inner harvester, cfg BoundedHarvesterCfg) harvester { 19 | if _, ok := inner.(*telemetry.Harvester); ok { 20 | log.Debug("using telemetry.Harvester as underlying harvester, make sure to set HarvestPeriod to 0") 21 | } 22 | 23 | if cfg.MinReportInterval < BoundedHarvesterDefaultMinReportInterval { 24 | log.Warnf("Ignoring min_emitter_harvest_period %v < %v", cfg.MinReportInterval, BoundedHarvesterDefaultMinReportInterval) 25 | cfg.MinReportInterval = BoundedHarvesterDefaultMinReportInterval 26 | } 27 | 28 | if cfg.HarvestPeriod < cfg.MinReportInterval { 29 | log.Warnf("Ignoring emitter_harvest_period %v < %v, setting to default %v", cfg.HarvestPeriod, cfg.MinReportInterval, BoundedHarvesterDefaultHarvestPeriod) 30 | cfg.HarvestPeriod = BoundedHarvesterDefaultHarvestPeriod 31 | } 32 | 33 | if cfg.MetricCap == 0 { 34 | cfg.MetricCap = BoundedHarvesterDefaultMetricsCap 35 | } 36 | 37 | h := &boundedHarvester{ 38 | BoundedHarvesterCfg: cfg, 39 | mtx: sync.Mutex{}, 40 | inner: inner, 41 | } 42 | 43 | if !cfg.DisablePeriodicReporting { 44 | h.stopper = make(chan struct{}, 2) 45 | go h.periodicHarvest() 46 | } 47 | 48 | return h 49 | } 50 | 51 | // BoundedHarvesterCfg stores the configurable values for boundedHarvester 52 | type BoundedHarvesterCfg struct { 53 | // MetricCap is the number of metrics to store in memory before triggering a HarvestNow action regardless of 54 | // HarvestPeriod. It will directly influence the amount of memory that nri-prometheus allocates. 55 | // A value of 10000 is rougly equivalent to 500M in RAM in the tested scenarios 56 | MetricCap int 57 | 58 | // HarvestPeriod specifies the period that will trigger a HarvestNow action for the inner harvester. 59 | // It is not necessary to decrease this value further, as other conditions (namely the MetricCap) will also trigger 60 | // a harvest action. 61 | HarvestPeriod time.Duration 62 | 63 | // MinReportInterval Specifies the minimum amount of time to wait before reports. 64 | // This will be always enforced, regardless of HarvestPeriod and MetricCap. 65 | MinReportInterval time.Duration 66 | 67 | // DisablePeriodicReporting prevents bindHarvester from spawning the periodic report routine. 68 | // It also causes an already spawned reporting routine to be stopped on the next interval. 69 | DisablePeriodicReporting bool 70 | } 71 | 72 | // BoundedHarvesterDefaultHarvestPeriod is the default harvest period. Since harvests are also triggered by stacking 73 | // metrics, there is no need for this to be very low 74 | const BoundedHarvesterDefaultHarvestPeriod = 1 * time.Second 75 | 76 | // BoundedHarvesterDefaultMetricsCap is the default number of metrics stack before triggering a harvest. 10000 metrics 77 | // require around 500MiB in our testing setup 78 | const BoundedHarvesterDefaultMetricsCap = 10000 79 | 80 | // BoundedHarvesterDefaultMinReportInterval is the default and minimum enforced harvest interval time. No harvests will 81 | // be issued if previous harvest was less than this value ago (except for those triggered with HarvestNow) 82 | const BoundedHarvesterDefaultMinReportInterval = 200 * time.Millisecond 83 | 84 | // boundedHarvester is a harvester implementation and wrapper that keeps count of the number of metrics that are waiting 85 | // to be harvested. Every small period of time (BoundedHarvesterCfg.MinReportInterval), if the number of accumulated 86 | // metrics is above a given threshold (BoundedHarvesterCfg.MetricCap), a harvest is triggered. 87 | // A harvest is also triggered in periodic time intervals (BoundedHarvesterCfg.HarvestPeriod) 88 | // boundedHarvester will never trigger harvests more often than specified in BoundedHarvesterCfg.MinReportInterval. 89 | type boundedHarvester struct { 90 | BoundedHarvesterCfg 91 | 92 | mtx sync.Mutex 93 | 94 | storedMetrics int 95 | lastReport time.Time 96 | 97 | stopper chan struct{} 98 | stopped bool 99 | 100 | inner harvester 101 | } 102 | 103 | // RecordMetric records the metric in the underlying harvester and reports all of them if needed 104 | func (h *boundedHarvester) RecordMetric(m telemetry.Metric) { 105 | h.mtx.Lock() 106 | h.storedMetrics++ 107 | h.mtx.Unlock() 108 | 109 | h.inner.RecordMetric(m) 110 | } 111 | 112 | // HarvestNow forces a new report 113 | func (h *boundedHarvester) HarvestNow(ctx context.Context) { 114 | h.reportIfNeeded(ctx, true) 115 | } 116 | 117 | func (h *boundedHarvester) Stop() { 118 | // We need to nil the channel and flag stopped synchronously to avoid double-stop races 119 | h.mtx.Lock() 120 | defer h.mtx.Unlock() 121 | 122 | if h.stopper != nil && !h.stopped { 123 | h.stopper <- struct{}{} 124 | h.stopped = true 125 | } 126 | } 127 | 128 | // reportIfNeeded carries the logic to report metrics. 129 | // A report is triggered if: 130 | // - Force is set to true, or 131 | // - Last report occurred earlier than Now() - HarvestPeriod, or 132 | // - The number of metrics is above MetricCap and MinReportInterval has passed since last report 133 | // A report will not be triggered in any case if time since last harvest is less than MinReportInterval 134 | func (h *boundedHarvester) reportIfNeeded(ctx context.Context, force bool) { 135 | h.mtx.Lock() 136 | defer h.mtx.Unlock() 137 | 138 | if force || 139 | time.Since(h.lastReport) >= h.HarvestPeriod || 140 | (h.storedMetrics > h.MetricCap && time.Since(h.lastReport) > h.MinReportInterval) { 141 | 142 | log.Tracef("triggering harvest, last harvest: %v ago", time.Since(h.lastReport)) 143 | 144 | h.lastReport = time.Now() 145 | h.storedMetrics = 0 146 | 147 | go h.inner.HarvestNow(ctx) 148 | } 149 | } 150 | 151 | // periodicHarvest is run in a separate goroutine to periodically call reportIfNeeded every MinReportInterval 152 | func (h *boundedHarvester) periodicHarvest() { 153 | t := time.NewTicker(h.MinReportInterval) 154 | for { 155 | select { 156 | case <-h.stopper: 157 | t.Stop() 158 | return 159 | 160 | case <-t.C: 161 | if h.DisablePeriodicReporting { 162 | h.Stop() 163 | continue 164 | } 165 | 166 | h.reportIfNeeded(context.Background(), false) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /internal/integration/bounded_harvester_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 12 | ) 13 | 14 | type mockHarvester struct { 15 | metrics int 16 | harvests int 17 | } 18 | 19 | func (h *mockHarvester) RecordMetric(m telemetry.Metric) { 20 | h.metrics++ 21 | } 22 | 23 | func (h *mockHarvester) HarvestNow(ctx context.Context) { 24 | h.harvests++ 25 | } 26 | 27 | // Checks that bindHarvester returns a harvester, correctly overriding settings 28 | // Also check the async routine is spawned 29 | func TestBindHarvester(t *testing.T) { 30 | t.Parallel() 31 | 32 | cfg := BoundedHarvesterCfg{ 33 | MetricCap: 0, 34 | HarvestPeriod: 1, 35 | MinReportInterval: 10, 36 | DisablePeriodicReporting: true, 37 | } 38 | 39 | mock := &mockHarvester{} 40 | h := bindHarvester(mock, cfg) 41 | 42 | bh, ok := h.(*boundedHarvester) 43 | if !ok { 44 | t.Fatalf("returned harvester is not a boundedHarvester") 45 | } 46 | defer bh.Stop() 47 | 48 | if bh.MinReportInterval != BoundedHarvesterDefaultMinReportInterval { 49 | t.Fatalf("MinReportInterval was not overridden") 50 | } 51 | 52 | if bh.HarvestPeriod != BoundedHarvesterDefaultHarvestPeriod { 53 | t.Fatalf("HarvestPeriod was not overridden") 54 | } 55 | 56 | if bh.MetricCap != BoundedHarvesterDefaultMetricsCap { 57 | t.Fatalf("MetricCap was not overridden") 58 | } 59 | 60 | time.Sleep(time.Second) 61 | if mock.harvests != 0 { 62 | t.Fatalf("Periodic routine was called despite being disabled") 63 | } 64 | } 65 | 66 | func TestHarvestRoutine(t *testing.T) { 67 | t.Parallel() 68 | 69 | cfg := BoundedHarvesterCfg{ 70 | HarvestPeriod: 300 * time.Millisecond, 71 | MinReportInterval: BoundedHarvesterDefaultMinReportInterval, 72 | } 73 | 74 | mock := &mockHarvester{} 75 | h := bindHarvester(mock, cfg) 76 | 77 | bh, ok := h.(*boundedHarvester) 78 | if !ok { 79 | t.Fatalf("returned harvester is not a boundedHarvester") 80 | } 81 | defer bh.Stop() 82 | 83 | time.Sleep(time.Second) 84 | if mock.harvests < 1 { 85 | t.Fatalf("harvest routine was not called within 1s") 86 | } 87 | } 88 | 89 | func TestRoutineStopChannel(t *testing.T) { 90 | t.Parallel() 91 | 92 | cfg := BoundedHarvesterCfg{ 93 | HarvestPeriod: 300 * time.Millisecond, 94 | MinReportInterval: BoundedHarvesterDefaultMinReportInterval, 95 | } 96 | 97 | mock := &mockHarvester{} 98 | h := bindHarvester(mock, cfg) 99 | 100 | bh, ok := h.(*boundedHarvester) 101 | if !ok { 102 | t.Fatalf("returned harvester is not a boundedHarvester") 103 | } 104 | defer bh.Stop() 105 | 106 | time.Sleep(time.Second) 107 | bh.Stop() 108 | time.Sleep(time.Second) 109 | harvests := mock.harvests 110 | time.Sleep(time.Second) 111 | if mock.harvests != harvests { 112 | t.Fatalf("Stop() did not stop the harvest routine") 113 | } 114 | } 115 | 116 | func TestRoutineStopFlag(t *testing.T) { 117 | t.Parallel() 118 | 119 | cfg := BoundedHarvesterCfg{ 120 | HarvestPeriod: 300 * time.Millisecond, 121 | MinReportInterval: BoundedHarvesterDefaultMinReportInterval, 122 | } 123 | 124 | mock := &mockHarvester{} 125 | h := bindHarvester(mock, cfg) 126 | 127 | bh, ok := h.(*boundedHarvester) 128 | if !ok { 129 | t.Fatalf("returned harvester is not a boundedHarvester") 130 | } 131 | defer bh.Stop() 132 | 133 | time.Sleep(time.Second) 134 | bh.DisablePeriodicReporting = true 135 | time.Sleep(time.Second) 136 | harvests := mock.harvests 137 | time.Sleep(time.Second) 138 | if mock.harvests != harvests { 139 | t.Fatalf("DisablePeriodicReporting = true did not stop the harvest routine") 140 | } 141 | } 142 | 143 | func TestHarvestNow(t *testing.T) { 144 | t.Parallel() 145 | 146 | cfg := BoundedHarvesterCfg{ 147 | DisablePeriodicReporting: true, 148 | } 149 | 150 | mock := &mockHarvester{} 151 | h := bindHarvester(mock, cfg) 152 | 153 | bh, ok := h.(*boundedHarvester) 154 | if !ok { 155 | t.Fatalf("returned harvester is not a boundedHarvester") 156 | } 157 | defer bh.Stop() 158 | 159 | h.HarvestNow(context.Background()) 160 | time.Sleep(100 * time.Millisecond) // Inner HarvestNow is asynchronous 161 | if mock.harvests < 1 { 162 | t.Fatalf("HarvestNow did not trigger a harvest") 163 | } 164 | } 165 | 166 | func TestMetricCap(t *testing.T) { 167 | t.Parallel() 168 | 169 | cfg := BoundedHarvesterCfg{ 170 | HarvestPeriod: time.Hour, 171 | MetricCap: 3, 172 | } 173 | 174 | mock := &mockHarvester{} 175 | h := bindHarvester(mock, cfg) 176 | 177 | bh, ok := h.(*boundedHarvester) 178 | if !ok { 179 | t.Fatalf("returned harvester is not a boundedHarvester") 180 | } 181 | defer bh.Stop() 182 | 183 | h.RecordMetric(telemetry.Count{}) 184 | h.RecordMetric(telemetry.Count{}) 185 | h.RecordMetric(telemetry.Count{}) 186 | h.RecordMetric(telemetry.Count{}) 187 | time.Sleep(time.Second) // Wait for MinReportInterval 188 | 189 | if mock.harvests < 1 { 190 | t.Fatalf("Stacking metrics did not trigger a harvest") 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /internal/integration/emitter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | const ( 13 | defaultDeltaExpirationAge = 5 * time.Minute 14 | defaultDeltaExpirationCheckInterval = 5 * time.Minute 15 | ) 16 | 17 | // Emitter is an interface representing the ability to emit metrics. 18 | type Emitter interface { 19 | Name() string 20 | Emit([]Metric) error 21 | } 22 | 23 | // copyAttrs returns a (shallow) copy of the passed attrs. 24 | func copyAttrs(attrs map[string]interface{}) map[string]interface{} { 25 | duplicate := make(map[string]interface{}, len(attrs)) 26 | for k, v := range attrs { 27 | duplicate[k] = v 28 | } 29 | return duplicate 30 | } 31 | 32 | // StdoutEmitter emits metrics to stdout. 33 | type StdoutEmitter struct { 34 | name string 35 | } 36 | 37 | // NewStdoutEmitter returns a NewStdoutEmitter. 38 | func NewStdoutEmitter() *StdoutEmitter { 39 | return &StdoutEmitter{ 40 | name: "stdout", 41 | } 42 | } 43 | 44 | // Name is the StdoutEmitter name. 45 | func (se *StdoutEmitter) Name() string { 46 | return se.name 47 | } 48 | 49 | // Emit prints the metrics into stdout. 50 | // Note: histograms not supported due json not supporting Inf values which are present in the last bucket 51 | func (se *StdoutEmitter) Emit(metrics []Metric) error { 52 | b, err := json.Marshal(metrics) 53 | if err != nil { 54 | return err 55 | } 56 | fmt.Println(string(b)) 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/integration/emitter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/newrelic/nri-prometheus/internal/pkg/labels" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_EmitterCanEmit(t *testing.T) { 14 | t.Parallel() 15 | 16 | summary, err := newSummary(3, 10, []*quantile{{0.5, 10}, {0.999, 100}}) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | metrics := []Metric{ 22 | { 23 | name: "common-name", 24 | metricType: metricType_COUNTER, 25 | value: float64(1), 26 | attributes: labels.Set{ 27 | "name": "common-name", 28 | "targetName": "target-a", 29 | "nrMetricType": "count", 30 | "promMetricType": "counter", 31 | }, 32 | }, 33 | { 34 | name: "common-name2", 35 | metricType: metricType_COUNTER, 36 | value: float64(1), 37 | attributes: labels.Set{ 38 | "name": "common-name2", 39 | "targetName": "target-b", 40 | "nrMetricType": "count", 41 | "promMetricType": "counter", 42 | }, 43 | }, 44 | { 45 | name: "common-name3", 46 | metricType: metricType_GAUGE, 47 | value: float64(1), 48 | attributes: labels.Set{ 49 | "name": "common-name3", 50 | "targetName": "target-c", 51 | "nrMetricType": "gauge", 52 | "promMetricType": "gauge", 53 | }, 54 | }, 55 | { 56 | name: "summary-1", 57 | metricType: metricType_SUMMARY, 58 | value: summary, 59 | attributes: labels.Set{}, 60 | }, 61 | } 62 | 63 | e := NewStdoutEmitter() 64 | assert.NotNil(t, e) 65 | 66 | err = e.Emit(metrics) 67 | assert.NoError(t, err) 68 | } 69 | -------------------------------------------------------------------------------- /internal/integration/harvester_decorator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "context" 8 | "math" 9 | 10 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // harvesterDecorator is a layer on top of another harvester that filters out NaN and Infinite float values. 15 | type harvesterDecorator struct { 16 | innerHarvester harvester 17 | } 18 | 19 | func (ha harvesterDecorator) RecordMetric(m telemetry.Metric) { 20 | switch a := m.(type) { 21 | case telemetry.Count: 22 | ha.processMetric(a.Value, m) 23 | case telemetry.Summary: 24 | ha.processMetric(a.Sum, m) 25 | case telemetry.Gauge: 26 | ha.processMetric(a.Value, m) 27 | default: 28 | logrus.Debugf("Unexpected metric in harvesterDecorator: #%v", m) 29 | ha.innerHarvester.RecordMetric(m) 30 | } 31 | } 32 | 33 | func (ha harvesterDecorator) HarvestNow(ctx context.Context) { 34 | ha.innerHarvester.HarvestNow(ctx) 35 | } 36 | 37 | func (ha harvesterDecorator) processMetric(f float64, m telemetry.Metric) { 38 | if math.IsNaN(f) { 39 | logrus.Debugf("Ignoring NaN float value for metric: %v", m) 40 | return 41 | } 42 | 43 | if math.IsInf(f, 0) { 44 | logrus.Debugf("Ignoring Infinite float value for metric: %v", m) 45 | return 46 | } 47 | 48 | ha.innerHarvester.RecordMetric(m) 49 | } 50 | -------------------------------------------------------------------------------- /internal/integration/helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/newrelic/nri-prometheus/internal/pkg/endpoints" 15 | ) 16 | 17 | var prometheusInput = `# HELP redis_exporter_build_info redis exporter build_info 18 | # TYPE redis_exporter_build_info gauge 19 | redis_exporter_build_info{build_date="2018-07-03-14:18:56",commit_sha="3e15af27aac37e114b32a07f5e9dc0510f4cbfc4",golang_version="go1.9.4",version="v0.20.2"} 1 20 | # HELP redis_exporter_scrapes_total Current total redis scrapes. 21 | # TYPE redis_exporter_scrapes_total counter 22 | redis_exporter_scrapes_total{cosa="fina"} 42 23 | # HELP redis_instance_info Information about the Redis instance 24 | # TYPE redis_instance_info gauge 25 | redis_instance_info{addr="ohai-playground-redis-master:6379",alias="ohai-playground-redis",os="Linux 4.15.0 x86_64",redis_build_id="c701a4acd98ea64a",redis_mode="standalone",redis_version="4.0.10",role="master"} 1 26 | redis_instance_info{addr="ohai-playground-redis-slave:6379",alias="ohai-playground-redis",os="Linux 4.15.0 x86_64",redis_build_id="c701a4acd98ea64a",redis_mode="standalone",redis_version="4.0.10",role="slave"} 1 27 | # HELP redis_instantaneous_input_kbps instantaneous_input_kbpsmetric 28 | # TYPE redis_instantaneous_input_kbps gauge 29 | redis_instantaneous_input_kbps{addr="ohai-playground-redis-master:6379",alias="ohai-playground-redis"} 0.05 30 | redis_instantaneous_input_kbps{addr="ohai-playground-redis-slave:6379",alias="ohai-playground-redis"} 0 31 | ` 32 | 33 | func scrapeString(t *testing.T, inputMetrics string) TargetMetrics { 34 | t.Helper() 35 | 36 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | _, _ = w.Write([]byte(inputMetrics)) 38 | })) 39 | defer ts.Close() 40 | server, err := endpoints.FixedRetriever(endpoints.TargetConfig{URLs: []string{ts.URL}}) 41 | require.NoError(t, err) 42 | target, err := server.GetTargets() 43 | require.NoError(t, err) 44 | 45 | metricsCh := NewFetcher(time.Millisecond, 1*time.Second, "", workerThreads, "", "", true, queueLength).Fetch(target) 46 | 47 | var pair TargetMetrics 48 | select { 49 | case pair = <-metricsCh: 50 | case <-time.After(5 * time.Second): 51 | require.Fail(t, "timeout while waiting for a simple entity") 52 | } 53 | 54 | // we expect that only one entity is sent from the fetcher, then the channel is closed 55 | select { 56 | case p := <-metricsCh: // channel is closed 57 | require.Empty(t, p.Metrics, "no more data should have been submitted", "%#v", p) 58 | case <-time.After(100 * time.Millisecond): 59 | require.Fail(t, "scraper channel should have been closed after all entities were processed") 60 | } 61 | 62 | return pair 63 | } 64 | -------------------------------------------------------------------------------- /internal/integration/infra_sdk_emitter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | infra "github.com/newrelic/infra-integrations-sdk/v4/data/metric" 13 | sdk "github.com/newrelic/infra-integrations-sdk/v4/integration" 14 | "github.com/newrelic/nri-prometheus/internal/pkg/labels" 15 | dto "github.com/prometheus/client_model/go" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // A different regex is needed for replacing because `localhostRE` matches 20 | // IPV6 by using extra `:` that don't belong to the IP but are separators. 21 | var localhostReplaceRE = regexp.MustCompile(`(localhost|LOCALHOST|127(?:\.[0-9]+){0,2}\.[0-9]+|::1)`) 22 | 23 | // Metric attributes that are shared by all metrics of an entity. 24 | var commonAttributes = map[string]struct{}{ 25 | "scrapedTargetKind": {}, 26 | "scrapedTargetName": {}, 27 | "scrapedTargetURL": {}, 28 | "targetName": {}, 29 | } 30 | 31 | // Metric attributes not needed for the infra-agent metrics pipeline. 32 | var removedAttributes = map[string]struct{}{ 33 | "nrMetricType": {}, 34 | "promMetricType": {}, 35 | } 36 | 37 | // InfraSdkEmitter is the emitter using the infra sdk to output metrics to stdout 38 | type InfraSdkEmitter struct { 39 | integrationMetadata Metadata 40 | hostID string 41 | } 42 | 43 | // Metadata contains the name and version of the exporter that is being scraped. 44 | // The Infra-Agent use the metadata to populate instrumentation.name and instrumentation.value 45 | type Metadata struct { 46 | Name string `mapstructure:"name"` 47 | Version string `mapstructure:"version"` 48 | } 49 | 50 | func (im *Metadata) isValid() bool { 51 | return im.Name != "" && im.Version != "" 52 | } 53 | 54 | // NewInfraSdkEmitter creates a new Infra SDK emitter 55 | func NewInfraSdkEmitter(hostID string) *InfraSdkEmitter { 56 | return &InfraSdkEmitter{ 57 | // By default it uses the nri-prometheus and it version. 58 | integrationMetadata: Metadata{ 59 | Name: Name, 60 | Version: Version, 61 | }, 62 | hostID: hostID, 63 | } 64 | } 65 | 66 | // SetIntegrationMetadata overrides integrationMetadata. 67 | func (e *InfraSdkEmitter) SetIntegrationMetadata(integrationMetadata Metadata) error { 68 | if !integrationMetadata.isValid() { 69 | return fmt.Errorf("invalid integration metadata") 70 | } 71 | e.integrationMetadata = integrationMetadata 72 | return nil 73 | } 74 | 75 | // Name is the InfraSdkEmitter name. 76 | func (e *InfraSdkEmitter) Name() string { 77 | return "infra-sdk" 78 | } 79 | 80 | // Emit emits the metrics using the infra sdk 81 | func (e *InfraSdkEmitter) Emit(metrics []Metric) error { 82 | // create new Infra sdk Integration 83 | i, err := sdk.New(e.integrationMetadata.Name, e.integrationMetadata.Version) 84 | if err != nil { 85 | return err 86 | } 87 | // We want the agent to not send metrics attached to any entity in order to make the entity synthesis to take place 88 | // completely in the backend. Since V4 SDK still needs an entity (Dataset) to attach the metrics to, we are using 89 | // the default hostEntity to attach all the metrics to it but setting this flag, IgnoreEntity: true that 90 | // will cause the agent to send them unattached to any entity 91 | i.HostEntity.SetIgnoreEntity(true) 92 | 93 | now := time.Now() 94 | for _, me := range metrics { 95 | switch me.metricType { 96 | case metricType_GAUGE: 97 | err = e.emitGauge(i, me, now) 98 | break 99 | case metricType_COUNTER: 100 | err = e.emitCumulativeCounter(i, me, now) 101 | break 102 | case metricType_SUMMARY: 103 | err = e.emitSummary(i, me, now) 104 | break 105 | case metricType_HISTOGRAM: 106 | err = e.emitHistogram(i, me, now) 107 | break 108 | default: 109 | err = fmt.Errorf("unknown metric type %q", me.metricType) 110 | } 111 | 112 | if err != nil { 113 | logrus.WithError(err).Errorf("failed to create metric from '%s'", me.name) 114 | } 115 | } 116 | logrus.Debugf("%d metrics processed", len(metrics)) 117 | 118 | return i.Publish() 119 | } 120 | 121 | func (e *InfraSdkEmitter) emitGauge(i *sdk.Integration, metric Metric, timestamp time.Time) error { 122 | m, err := sdk.Gauge(timestamp, metric.name, metric.value.(float64)) 123 | if err != nil { 124 | return err 125 | } 126 | return e.addMetricToEntity(i, metric, m) 127 | } 128 | 129 | // emitCumulativeCounter calls CumulativeCount that instead of Count, in this way in the agent the delta will be 130 | // computed and reported instead of the absolute value 131 | func (e *InfraSdkEmitter) emitCumulativeCounter(i *sdk.Integration, metric Metric, timestamp time.Time) error { 132 | m, err := sdk.CumulativeCount(timestamp, metric.name, metric.value.(float64)) 133 | if err != nil { 134 | return err 135 | } 136 | return e.addMetricToEntity(i, metric, m) 137 | } 138 | 139 | func (e *InfraSdkEmitter) emitHistogram(i *sdk.Integration, metric Metric, timestamp time.Time) error { 140 | hist, ok := metric.value.(*dto.Histogram) 141 | if !ok { 142 | return fmt.Errorf("unknown histogram metric type for %q: %T", metric.name, metric.value) 143 | } 144 | 145 | ph, err := infra.NewPrometheusHistogram(timestamp, metric.name, *hist.SampleCount, *hist.SampleSum) 146 | if err != nil { 147 | return fmt.Errorf("failed to create histogram metric for %q", metric.name) 148 | } 149 | 150 | buckets := hist.Bucket 151 | for _, b := range buckets { 152 | ph.AddBucket(*b.CumulativeCount, *b.UpperBound) 153 | } 154 | 155 | return e.addMetricToEntity(i, metric, ph) 156 | } 157 | 158 | func (e *InfraSdkEmitter) emitSummary(i *sdk.Integration, metric Metric, timestamp time.Time) error { 159 | summary, ok := metric.value.(*dto.Summary) 160 | if !ok { 161 | return fmt.Errorf("unknown summary metric type for %q: %T", metric.name, metric.value) 162 | } 163 | 164 | ps, err := infra.NewPrometheusSummary(timestamp, metric.name, *summary.SampleCount, *summary.SampleSum) 165 | if err != nil { 166 | return fmt.Errorf("failed to create summary metric for %q", metric.name) 167 | } 168 | 169 | quantiles := summary.GetQuantile() 170 | for _, q := range quantiles { 171 | ps.AddQuantile(*q.Quantile, *q.Value) 172 | } 173 | 174 | return e.addMetricToEntity(i, metric, ps) 175 | } 176 | 177 | func (e *InfraSdkEmitter) addMetricToEntity(i *sdk.Integration, metric Metric, m infra.Metric) error { 178 | e.addDimensions(m, metric.attributes, i.HostEntity) 179 | i.HostEntity.AddMetric(m) 180 | return nil 181 | } 182 | 183 | func (e *InfraSdkEmitter) addDimensions(m infra.Metric, attributes labels.Set, entity *sdk.Entity) { 184 | var value string 185 | var ok bool 186 | for k, v := range attributes { 187 | if _, ok = removedAttributes[k]; ok { 188 | continue 189 | } 190 | if value, ok = v.(string); !ok { 191 | logrus.Debugf("the value (%v) of %s attribute should be a string", k, v) 192 | continue 193 | } 194 | if _, ok = commonAttributes[k]; ok { 195 | if k == "scrapedTargetName" || k == "targetName" { 196 | value = replaceLocalhost(value, e.hostID) 197 | } 198 | entity.AddCommonDimension(k, value) 199 | continue 200 | } 201 | err := m.AddDimension(k, value) 202 | if err != nil { 203 | logrus.WithError(err).Warnf("failed to add attribute %v(%v) as dimension to metric", k, v) 204 | } 205 | } 206 | } 207 | 208 | // resizeToLimit makes sure that the entity name is less than the limit of 500 209 | // it removed "full tokens" from the string so we don't get partial values in the name 210 | func resizeToLimit(sb *strings.Builder) (resized bool) { 211 | if sb.Len() < 500 { 212 | return false 213 | } 214 | 215 | tokens := strings.Split(sb.String(), ":") 216 | sb.Reset() 217 | 218 | // add tokens until we get to the limit 219 | sb.WriteString(tokens[0]) 220 | for _, t := range tokens[1:] { 221 | if sb.Len()+len(t)+1 >= 500 { 222 | resized = true 223 | break 224 | } 225 | sb.WriteRune(':') 226 | sb.WriteString(t) 227 | } 228 | return 229 | } 230 | 231 | // ReplaceLocalhost replaces the occurrence of a localhost address with 232 | // the given hostname 233 | func replaceLocalhost(originalHost, hostID string) string { 234 | if hostID != "" { 235 | return localhostReplaceRE.ReplaceAllString(originalHost, hostID) 236 | } 237 | return originalHost 238 | } 239 | -------------------------------------------------------------------------------- /internal/integration/integration.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package integration ... 5 | package integration 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/sirupsen/logrus" 12 | 13 | "github.com/newrelic/nri-prometheus/internal/pkg/endpoints" 14 | nrprom "github.com/newrelic/nri-prometheus/internal/pkg/prometheus" 15 | ) 16 | 17 | const ( 18 | // Name of the integration 19 | Name = "nri-prometheus" 20 | ) 21 | 22 | // Version of the integration 23 | var Version = "dev" 24 | 25 | var ilog = logrus.WithField("component", "integration.Execute") 26 | 27 | // Execute the integration loop. It sets the retrievers to start watching for 28 | // new targets and starts the processing pipeline. The pipeline fetches 29 | // metrics from the registered targets, transforms them according to a set 30 | // of rules and emits them. 31 | // 32 | // with first-class functions 33 | func Execute( 34 | scrapeDuration time.Duration, 35 | selfRetriever endpoints.TargetRetriever, 36 | retrievers []endpoints.TargetRetriever, 37 | fetcher Fetcher, 38 | processor Processor, 39 | emitters []Emitter, 40 | ) { 41 | for _, retriever := range retrievers { 42 | err := retriever.Watch() 43 | if err != nil { 44 | ilog.WithError(err).WithField("retriever", retriever.Name()).Error("while getting the initial list of targets") 45 | } 46 | } 47 | 48 | for { 49 | totalTimeseriesMetric.Set(0) 50 | totalTimeseriesByTargetMetric.Reset() 51 | totalTimeseriesByTargetAndTypeMetric.Reset() 52 | totalTimeseriesByTypeMetric.Reset() 53 | fetchTargetDurationMetric.Reset() 54 | fetchesTotalMetric.Reset() 55 | fetchErrorsTotalMetric.Reset() 56 | nrprom.ResetTargetSize() 57 | 58 | startTime := time.Now() 59 | process(retrievers, fetcher, processor, emitters) 60 | totalExecutionsMetric.Inc() 61 | if duration := time.Since(startTime); duration < scrapeDuration { 62 | time.Sleep(scrapeDuration - duration) 63 | } 64 | processWithoutTelemetry(selfRetriever, fetcher, processor, emitters) 65 | } 66 | } 67 | 68 | // ExecuteOnce executes the integration once. The pipeline fetches 69 | // metrics from the registered targets, transforms them according to a set 70 | // of rules and emits them. 71 | func ExecuteOnce(retrievers []endpoints.TargetRetriever, fetcher Fetcher, processor Processor, emitters []Emitter) { 72 | for _, retriever := range retrievers { 73 | err := retriever.Watch() 74 | if err != nil { 75 | ilog.WithError(err).WithField("retriever", retriever.Name()).Error("while getting the initial list of targets") 76 | } 77 | } 78 | 79 | for _, retriever := range retrievers { 80 | processWithoutTelemetry(retriever, fetcher, processor, emitters) 81 | } 82 | } 83 | 84 | // processWithoutTelemetry processes a target retriever without doing any 85 | // kind of telemetry calculation. 86 | func processWithoutTelemetry( 87 | retriever endpoints.TargetRetriever, 88 | fetcher Fetcher, 89 | processor Processor, 90 | emitters []Emitter, 91 | ) { 92 | targets, err := retriever.GetTargets() 93 | if err != nil { 94 | ilog.WithError(err).Error("error getting targets") 95 | return 96 | } 97 | pairs := fetcher.Fetch(targets) 98 | processed := processor(pairs) 99 | for pair := range processed { 100 | for _, e := range emitters { 101 | err := e.Emit(pair.Metrics) 102 | if err != nil { 103 | ilog.WithField("emitter", e.Name()).WithError(err).Warn("error emitting metrics") 104 | } 105 | } 106 | } 107 | } 108 | 109 | func process(retrievers []endpoints.TargetRetriever, fetcher Fetcher, processor Processor, emitters []Emitter) { 110 | ptimer := prometheus.NewTimer(prometheus.ObserverFunc(processDurationMetric.Set)) 111 | 112 | targets := make([]endpoints.Target, 0) 113 | for _, retriever := range retrievers { 114 | totalDiscoveriesMetric.WithLabelValues(retriever.Name()).Set(1) 115 | t, err := retriever.GetTargets() 116 | if err != nil { 117 | ilog.WithError(err).Error("error getting targets") 118 | totalErrorsDiscoveryMetric.WithLabelValues(retriever.Name()).Set(1) 119 | return 120 | } 121 | totalTargetsMetric.WithLabelValues(retriever.Name()).Set(float64(len(t))) 122 | targets = append(targets, t...) 123 | } 124 | pairs := fetcher.Fetch(targets) // fetch metrics from /metrics endpoints 125 | processed := processor(pairs) // apply processing 126 | 127 | emittedMetrics := 0 128 | for pair := range processed { 129 | emittedMetrics += len(pair.Metrics) 130 | 131 | for _, e := range emitters { 132 | err := e.Emit(pair.Metrics) 133 | if err != nil { 134 | ilog.WithField("emitter", e.Name()).WithError(err).Warn("error emitting metrics") 135 | } 136 | } 137 | } 138 | 139 | duration := ptimer.ObserveDuration() 140 | 141 | logrus.WithFields(logrus.Fields{ 142 | "duration": duration.Round(time.Second), 143 | "targetCount": len(targets), 144 | "emitterCount": len(emitters), 145 | "emittedMetricsCount": emittedMetrics, 146 | }).Debug("Processing metrics finished.") 147 | } 148 | -------------------------------------------------------------------------------- /internal/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strconv" 11 | "testing" 12 | "time" 13 | 14 | "github.com/newrelic/nri-prometheus/internal/pkg/endpoints" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | type nilEmit struct{} 19 | 20 | func (*nilEmit) Name() string { 21 | return "nil-emitter" 22 | } 23 | 24 | func (*nilEmit) Emit([]Metric) error { 25 | return nil 26 | } 27 | 28 | func BenchmarkIntegration(b *testing.B) { 29 | cachedFile, err := ioutil.ReadFile("test/cadvisor.txt") 30 | assert.NoError(b, err) 31 | contentLength := strconv.Itoa(len(cachedFile)) 32 | b.Log("payload size", contentLength) 33 | server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 34 | resp.Header().Set("Content-Length", contentLength) 35 | _, err := resp.Write(cachedFile) 36 | assert.NoError(b, err) 37 | })) 38 | defer server.Close() 39 | 40 | fr, err := endpoints.FixedRetriever(endpoints.TargetConfig{URLs: []string{server.URL}}) 41 | assert.NoError(b, err) 42 | var retrievers []endpoints.TargetRetriever 43 | for i := 0; i < 20; i++ { 44 | retrievers = append(retrievers, fr) 45 | } 46 | 47 | b.ReportAllocs() 48 | b.ResetTimer() 49 | 50 | for i := 0; i < b.N; i++ { 51 | do(b, retrievers) 52 | } 53 | } 54 | 55 | func do(b *testing.B, retrievers []endpoints.TargetRetriever) { 56 | b.ReportAllocs() 57 | process( 58 | retrievers, 59 | NewFetcher(30*time.Second, 5000000000, "", 4, "", "", false, queueLength), 60 | RuleProcessor([]ProcessingRule{}, queueLength), 61 | []Emitter{&nilEmit{}}, 62 | ) 63 | } 64 | 65 | func BenchmarkIntegrationInfraSDKEmitter(b *testing.B) { 66 | cachedFile, err := ioutil.ReadFile("test/cadvisor.txt") 67 | assert.NoError(b, err) 68 | contentLength := strconv.Itoa(len(cachedFile)) 69 | b.Log("payload size", contentLength) 70 | server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 71 | resp.Header().Set("Content-Length", contentLength) 72 | _, err := resp.Write(cachedFile) 73 | assert.NoError(b, err) 74 | })) 75 | defer server.Close() 76 | 77 | fr, err := endpoints.FixedRetriever(endpoints.TargetConfig{URLs: []string{server.URL}}) 78 | assert.NoError(b, err) 79 | var retrievers []endpoints.TargetRetriever 80 | for i := 0; i < 20; i++ { 81 | retrievers = append(retrievers, fr) 82 | } 83 | 84 | emitter := NewInfraSdkEmitter("") 85 | emitters := []Emitter{emitter} 86 | 87 | b.ReportAllocs() 88 | b.ResetTimer() 89 | 90 | for i := 0; i < b.N; i++ { 91 | ExecuteOnce( 92 | retrievers, 93 | NewFetcher(30*time.Second, 5000000000, "", 4, "", "", false, queueLength), 94 | RuleProcessor([]ProcessingRule{}, queueLength), 95 | emitters) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/integration/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import "github.com/prometheus/client_golang/prometheus" 7 | 8 | var ( 9 | totalTargetsMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 10 | Namespace: "nr_stats", 11 | Name: "targets", 12 | Help: "Discovered targets", 13 | }, 14 | []string{ 15 | "retriever", 16 | }, 17 | ) 18 | totalDiscoveriesMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 19 | Namespace: "nr_stats", 20 | Name: "discoveries_total", 21 | Help: "Attempted discoveries", 22 | }, 23 | []string{ 24 | "retriever", 25 | }, 26 | ) 27 | totalErrorsDiscoveryMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 28 | Namespace: "nr_stats", 29 | Name: "discovery_errors_total", 30 | Help: "Attempted discoveries that resulted in an error", 31 | }, 32 | []string{ 33 | "retriever", 34 | }, 35 | ) 36 | fetchesTotalMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 37 | Namespace: "nr_stats", 38 | Name: "fetches_total", 39 | Help: "Fetches attempted", 40 | }, 41 | []string{ 42 | "target", 43 | }, 44 | ) 45 | fetchErrorsTotalMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 46 | Namespace: "nr_stats", 47 | Name: "fetch_errors_total", 48 | Help: "Fetches attempted that resulted in an error", 49 | }, 50 | []string{ 51 | "target", 52 | }, 53 | ) 54 | totalTimeseriesByTargetAndTypeMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 55 | Namespace: "nr_stats", 56 | Subsystem: "metrics", 57 | Name: "total_timeseries_by_target_type", 58 | Help: "Total number of metrics by type and target", 59 | }, 60 | []string{ 61 | "type", 62 | "target", 63 | }, 64 | ) 65 | totalTimeseriesByTypeMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 66 | Namespace: "nr_stats", 67 | Subsystem: "metrics", 68 | Name: "total_timeseries_by_type", 69 | Help: "Total number of metrics by type", 70 | }, 71 | []string{ 72 | "type", 73 | }, 74 | ) 75 | totalTimeseriesMetric = prometheus.NewGauge(prometheus.GaugeOpts{ 76 | Namespace: "nr_stats", 77 | Subsystem: "metrics", 78 | Name: "total_timeseries", 79 | Help: "Total number of timeseries", 80 | }) 81 | totalTimeseriesByTargetMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 82 | Namespace: "nr_stats", 83 | Subsystem: "metrics", 84 | Name: "total_timeseries_by_target", 85 | Help: "Total number of timeseries by target", 86 | }, 87 | []string{ 88 | "target", 89 | }) 90 | fetchTargetDurationMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 91 | Namespace: "nr_stats", 92 | Subsystem: "integration", 93 | Name: "fetch_target_duration_seconds", 94 | Help: "The total time in seconds to fetch the metrics of a target", 95 | }, 96 | []string{ 97 | "target", 98 | }, 99 | ) 100 | processDurationMetric = prometheus.NewGauge(prometheus.GaugeOpts{ 101 | Namespace: "nr_stats", 102 | Subsystem: "integration", 103 | Name: "process_duration_seconds", 104 | Help: "The total time in seconds to process all the steps of the integration", 105 | }) 106 | totalExecutionsMetric = prometheus.NewCounter(prometheus.CounterOpts{ 107 | Namespace: "nr_stats", 108 | Subsystem: "integration", 109 | Name: "total_executions", 110 | Help: "The number of times the integration is executed", 111 | }) 112 | ) 113 | 114 | func init() { 115 | prometheus.MustRegister(totalTargetsMetric) 116 | prometheus.MustRegister(totalDiscoveriesMetric) 117 | prometheus.MustRegister(totalErrorsDiscoveryMetric) 118 | prometheus.MustRegister(fetchesTotalMetric) 119 | prometheus.MustRegister(totalTimeseriesByTypeMetric) 120 | prometheus.MustRegister(fetchErrorsTotalMetric) 121 | prometheus.MustRegister(totalTimeseriesByTargetAndTypeMetric) 122 | prometheus.MustRegister(totalTimeseriesMetric) 123 | prometheus.MustRegister(totalTimeseriesByTargetMetric) 124 | prometheus.MustRegister(fetchTargetDurationMetric) 125 | prometheus.MustRegister(processDurationMetric) 126 | prometheus.MustRegister(totalExecutionsMetric) 127 | } 128 | -------------------------------------------------------------------------------- /internal/integration/roundtripper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import "net/http" 7 | 8 | // licenseKeyRoundTripper adds the infra license key to every request. 9 | type licenseKeyRoundTripper struct { 10 | licenseKey string 11 | rt http.RoundTripper 12 | } 13 | 14 | // RoundTrip wraps the `RoundTrip` method removing the "Api-Key" 15 | // replacing it with "X-License-Key". 16 | func (t licenseKeyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 17 | req.Header.Del("Api-Key") 18 | req.Header.Add("X-License-Key", t.licenseKey) 19 | return t.rt.RoundTrip(req) 20 | } 21 | 22 | // newLicenseKeyRoundTripper wraps the given http.RoundTripper and inserts 23 | // the appropriate headers for using the NewRelic licenseKey. 24 | func newLicenseKeyRoundTripper( 25 | rt http.RoundTripper, 26 | licenseKey string, 27 | ) http.RoundTripper { 28 | if rt == nil { 29 | rt = http.DefaultTransport 30 | } 31 | 32 | return licenseKeyRoundTripper{ 33 | licenseKey: licenseKey, 34 | rt: rt, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/integration/roundtripper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | type mockedRoundTripper struct { 15 | mock.Mock 16 | } 17 | 18 | func (m *mockedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 19 | m.Called(req) 20 | return &http.Response{}, nil 21 | } 22 | 23 | func TestRoundTripHeaderDecoration(t *testing.T) { 24 | t.Parallel() 25 | 26 | licenseKey := "myLicenseKey" 27 | req := &http.Request{Header: make(http.Header)} 28 | req.Header.Add("Api-Key", licenseKey) 29 | 30 | rt := new(mockedRoundTripper) 31 | rt.On("RoundTrip", req).Return().Run(func(args mock.Arguments) { 32 | req := args.Get(0).(*http.Request) 33 | assert.Equal(t, licenseKey, req.Header.Get("X-License-Key")) 34 | assert.Equal(t, "", req.Header.Get("Api-Key")) 35 | }) 36 | tr := newLicenseKeyRoundTripper(rt, licenseKey) 37 | 38 | _, _ = tr.RoundTrip(req) 39 | rt.AssertExpectations(t) 40 | } 41 | -------------------------------------------------------------------------------- /internal/integration/scrape_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package integration 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/newrelic/nri-prometheus/internal/pkg/labels" 13 | ) 14 | 15 | func TestScrape(t *testing.T) { 16 | t.Parallel() 17 | 18 | // Given a set of fetched metrics 19 | input := `# HELP redis_exporter_build_info redis exporter build_info 20 | # TYPE redis_exporter_build_info gauge 21 | redis_exporter_build_info{build_date="2018-07-03-14:18:56",commit_sha="3e15af27aac37e114b32a07f5e9dc0510f4cbfc4",golang_version="go1.9.4",version="v0.20.2"} 1 22 | # HELP redis_exporter_scrapes_total Current total redis scrapes. 23 | # TYPE redis_exporter_scrapes_total counter 24 | redis_exporter_scrapes_total{cosa="fina"} 42 25 | # HELP redis_instance_info Information about the Redis instance 26 | # TYPE redis_instance_info gauge 27 | redis_instance_info{addr="ohai-playground-redis-master:6379",alias="ohai-playground-redis",os="Linux 4.15.0 x86_64",redis_build_id="c701a4acd98ea64a",redis_mode="standalone",redis_version="4.0.10",role="master"} 1 28 | redis_instance_info{addr="ohai-playground-redis-slave:6379",alias="ohai-playground-redis",os="Linux 4.15.0 x86_64",redis_build_id="c701a4acd98ea64a",redis_mode="standalone",redis_version="4.0.10",role="slave"} 1 29 | # HELP redis_instantaneous_input_kbps instantaneous_input_kbpsmetric 30 | # TYPE redis_instantaneous_input_kbps gauge 31 | redis_instantaneous_input_kbps{addr="ohai-playground-redis-master:6379",alias="ohai-playground-redis"} 0.05 32 | redis_instantaneous_input_kbps{addr="ohai-playground-redis-slave:6379",alias="ohai-playground-redis"} 0 33 | ` 34 | // when they are scraped 35 | pair := scrapeString(t, input) 36 | 37 | // The returned input contains all the expected metrics 38 | assert.NotEmpty(t, pair.Target.Name) 39 | assert.NotEmpty(t, pair.Target.URL) 40 | assert.Len(t, pair.Metrics, 6) 41 | 42 | for _, metric := range pair.Metrics { 43 | switch metric.name { 44 | case "redis_exporter_scrapes_total": 45 | case "redis_instantaneous_input_kbps": 46 | switch metric.attributes["addr"] { 47 | case "ohai-playground-redis-slave:6379": 48 | expected := labels.Set{ 49 | "addr": "ohai-playground-redis-slave:6379", 50 | "alias": "ohai-playground-redis", 51 | } 52 | AssertContainsTree(t, metric.attributes, expected) 53 | case "ohai-playground-redis-master:6379": 54 | expected := labels.Set{ 55 | "addr": "ohai-playground-redis-master:6379", 56 | "alias": "ohai-playground-redis", 57 | } 58 | AssertContainsTree(t, metric.attributes, expected) 59 | default: 60 | assert.Failf(t, "unexpected addr field:", "%#v", metric.attributes) 61 | } 62 | case "redis_exporter_build_info": 63 | expected := labels.Set{ 64 | "build_date": "2018-07-03-14:18:56", 65 | "commit_sha": "3e15af27aac37e114b32a07f5e9dc0510f4cbfc4", 66 | "golang_version": "go1.9.4", 67 | "version": "v0.20.2", 68 | } 69 | AssertContainsTree(t, metric.attributes, expected) 70 | case "redis_instance_info": 71 | switch metric.attributes["addr"] { 72 | case "ohai-playground-redis-slave:6379": 73 | expected := labels.Set{ 74 | "addr": "ohai-playground-redis-slave:6379", 75 | "alias": "ohai-playground-redis", 76 | "os": "Linux 4.15.0 x86_64", 77 | "redis_build_id": "c701a4acd98ea64a", 78 | "redis_mode": "standalone", 79 | "redis_version": "4.0.10", 80 | "role": "slave", 81 | } 82 | AssertContainsTree(t, metric.attributes, expected) 83 | case "ohai-playground-redis-master:6379": 84 | expected := labels.Set{ 85 | "addr": "ohai-playground-redis-master:6379", 86 | "alias": "ohai-playground-redis", 87 | "os": "Linux 4.15.0 x86_64", 88 | "redis_build_id": "c701a4acd98ea64a", 89 | "redis_mode": "standalone", 90 | "redis_version": "4.0.10", 91 | "role": "master", 92 | } 93 | AssertContainsTree(t, metric.attributes, expected) 94 | default: 95 | assert.Failf(t, "unexpected addr field:", "%#v", metric.attributes) 96 | } 97 | default: 98 | assert.True(t, strings.HasSuffix(metric.name, "_info"), "unexpected metric %s", metric.name) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/pkg/endpoints/endpoints.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package endpoints ... 5 | package endpoints 6 | 7 | import ( 8 | "fmt" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/newrelic/nri-prometheus/internal/pkg/labels" 13 | ) 14 | 15 | // TargetRetriever is implemented by any type that can return the URL of a set of Prometheus metrics providers 16 | type TargetRetriever interface { 17 | GetTargets() ([]Target, error) 18 | Watch() error 19 | Name() string 20 | } 21 | 22 | // Object represents a kubernetes object like a pod or a service or an endpoint. 23 | type Object struct { 24 | Name string 25 | Kind string 26 | Labels labels.Set 27 | } 28 | 29 | // Target is a prometheus endpoint which is exposed by an Object. 30 | type Target struct { 31 | Name string 32 | Object Object 33 | URL url.URL 34 | metadata labels.Set 35 | TLSConfig TLSConfig 36 | // UseBearer tells nri-prometheus whether it should send the Kubernetes Service Account token as a Bearer token in 37 | // the HTTP request. 38 | UseBearer bool 39 | } 40 | 41 | // Metadata returns the Target's metadata, if the current metadata is nil, 42 | // it's constructed from the Target's attributes, saved and returned. 43 | // Subsequent calls will returned the already saved value. 44 | func (t *Target) Metadata() labels.Set { 45 | if t.metadata == nil { 46 | metadata := labels.Set{} 47 | if targetURL := redactedURLString(&t.URL); targetURL != "" { 48 | metadata["scrapedTargetURL"] = targetURL 49 | } 50 | if t.Object.Name != "" { 51 | metadata["scrapedTargetName"] = t.Object.Name 52 | metadata["scrapedTargetKind"] = t.Object.Kind 53 | } 54 | labels.Accumulate(metadata, t.Object.Labels) 55 | 56 | t.metadata = metadata 57 | } 58 | return t.metadata 59 | } 60 | 61 | // redactedURLString returns the string representation of the URL object while redacting the password that could be present. 62 | // This code is copied from this commit https://github.com/golang/go/commit/e3323f57df1f4a44093a2d25fee33513325cbb86. 63 | // The feature is supposed to be added to the net/url.URL type in Golang 1.15. 64 | func redactedURLString(u *url.URL) string { 65 | if u == nil { 66 | return "" 67 | } 68 | ru := *u 69 | if _, has := ru.User.Password(); has { 70 | ru.User = url.UserPassword(ru.User.Username(), "xxxxx") 71 | } 72 | return ru.String() 73 | } 74 | 75 | // endpointToTarget returns a list of Targets from the provided TargetConfig struct. 76 | // The URL processing for every Target follows the next conventions: 77 | // - if no schema is provided, it assumes http 78 | // - if no path is provided, it assumes /metrics 79 | // For example, hostname:8080 will be interpreted as http://hostname:8080/metrics 80 | func endpointToTarget(tc TargetConfig) ([]Target, error) { 81 | targets := make([]Target, 0, len(tc.URLs)) 82 | for _, URL := range tc.URLs { 83 | t, err := urlToTarget(URL, tc.TLSConfig) 84 | if err != nil { 85 | return nil, err 86 | } 87 | t.UseBearer = tc.UseBearer 88 | targets = append(targets, t) 89 | } 90 | return targets, nil 91 | } 92 | 93 | func urlToTarget(URL string, TLSConfig TLSConfig) (Target, error) { 94 | if !strings.Contains(URL, "://") { 95 | URL = fmt.Sprint("http://", URL) 96 | } 97 | 98 | u, err := url.Parse(URL) 99 | if err != nil { 100 | return Target{}, err 101 | } 102 | if u.Path == "" { 103 | u.Path = "/metrics" 104 | } 105 | 106 | return Target{ 107 | Name: u.Host, 108 | Object: Object{ 109 | Name: u.Host, 110 | Kind: "user_provided", 111 | Labels: make(labels.Set), 112 | }, 113 | TLSConfig: TLSConfig, 114 | URL: *u, 115 | }, nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/pkg/endpoints/endpoints_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package endpoints 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFromURL(t *testing.T) { 13 | t.Parallel() 14 | 15 | cases := []struct { 16 | testName string 17 | input string 18 | expectedName string 19 | expectedURL string 20 | }{ 21 | { 22 | testName: "default schema and path", 23 | input: "somehost", 24 | expectedName: "somehost", 25 | expectedURL: "http://somehost/metrics", 26 | }, 27 | { 28 | testName: "default schema and path, provided port", 29 | input: "somehost:8080", 30 | expectedName: "somehost:8080", 31 | expectedURL: "http://somehost:8080/metrics", 32 | }, 33 | { 34 | testName: "default path, provided port and schema", 35 | input: "https://somehost:8080", 36 | expectedName: "somehost:8080", 37 | expectedURL: "https://somehost:8080/metrics", 38 | }, 39 | { 40 | testName: "default schema", 41 | input: "somehost:8080/path", 42 | expectedName: "somehost:8080", 43 | expectedURL: "http://somehost:8080/path", 44 | }, 45 | { 46 | testName: "with URL params", 47 | input: "somehost:8080/path/with/params?format=prometheus(123)", 48 | expectedName: "somehost:8080", 49 | expectedURL: "http://somehost:8080/path/with/params?format=prometheus(123)", 50 | }, 51 | { 52 | testName: "provided all", 53 | input: "https://somehost:8080/path", 54 | expectedName: "somehost:8080", 55 | expectedURL: "https://somehost:8080/path", 56 | }, 57 | } 58 | for _, c := range cases { 59 | c := c 60 | 61 | t.Run(c.testName, func(t *testing.T) { 62 | t.Parallel() 63 | 64 | targets, err := endpointToTarget(TargetConfig{URLs: []string{c.input}}) 65 | assert.NoError(t, err) 66 | assert.Len(t, targets, 1) 67 | assert.Equal(t, c.expectedName, targets[0].Name) 68 | assert.Equal(t, c.expectedURL, targets[0].URL.String()) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/pkg/endpoints/fixed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package endpoints 5 | 6 | import "fmt" 7 | 8 | type fixedRetriever struct { 9 | targets []Target 10 | } 11 | 12 | // TargetConfig is used to parse endpoints from the configuration file. 13 | type TargetConfig struct { 14 | Description string 15 | URLs []string `mapstructure:"urls"` 16 | TLSConfig TLSConfig `mapstructure:"tls_config"` 17 | // UseBearer tells nri-prometheus whether it should send the Kubernetes Service Account token as a Bearer token in 18 | // the HTTP request. 19 | UseBearer bool `mapstructure:"use_bearer"` 20 | } 21 | 22 | // TLSConfig is used to store all the configuration required to use Mutual TLS authentication. 23 | type TLSConfig struct { 24 | CaFilePath string `mapstructure:"ca_file_path"` 25 | CertFilePath string `mapstructure:"cert_file_path"` 26 | KeyFilePath string `mapstructure:"key_file_path"` 27 | InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` 28 | } 29 | 30 | // FixedRetriever creates a TargetRetriver that returns the targets belonging to the URLs passed as arguments 31 | func FixedRetriever(targetCfgs ...TargetConfig) (TargetRetriever, error) { 32 | fixed := make([]Target, 0, len(targetCfgs)) 33 | for _, targetCfg := range targetCfgs { 34 | targets, err := endpointToTarget(targetCfg) 35 | if err != nil { 36 | return nil, fmt.Errorf("parsing target %v: %v", targetCfg, err.Error()) 37 | } 38 | fixed = append(fixed, targets...) 39 | } 40 | return &fixedRetriever{targets: fixed}, nil 41 | } 42 | 43 | func (f fixedRetriever) GetTargets() ([]Target, error) { 44 | return f.targets, nil 45 | } 46 | 47 | func (f fixedRetriever) Watch() error { 48 | // NOOP 49 | return nil 50 | } 51 | 52 | func (f fixedRetriever) Name() string { 53 | return "fixed" 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/endpoints/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package endpoints 5 | 6 | import "github.com/prometheus/client_golang/prometheus" 7 | 8 | var listTargetsDurationByKind = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 9 | Namespace: "nr_stats", 10 | Subsystem: "integration", 11 | Name: "list_targets_duration_by_kind", 12 | Help: "The total time in seconds to get the list of targets for a resource kind", 13 | }, 14 | []string{ 15 | "retriever", 16 | "kind", 17 | }, 18 | ) 19 | 20 | func init() { 21 | prometheus.MustRegister(listTargetsDurationByKind) 22 | } 23 | -------------------------------------------------------------------------------- /internal/pkg/endpoints/self.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package endpoints 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | const ( 11 | selfEndpoint = "localhost:8080" 12 | selfDescription = "nri-prometheus" 13 | ) 14 | 15 | type selfRetriever struct { 16 | targets []Target 17 | } 18 | 19 | func newSelfTargetConfig() TargetConfig { 20 | return TargetConfig{ 21 | Description: selfDescription, 22 | URLs: []string{selfEndpoint}, 23 | } 24 | } 25 | 26 | // SelfRetriever creates a TargetRetriver that returns the targets belonging 27 | // to nri-prometheus. 28 | func SelfRetriever() (TargetRetriever, error) { 29 | targets, err := endpointToTarget(newSelfTargetConfig()) 30 | if err != nil { 31 | return nil, fmt.Errorf("parsing target %v: %v", selfDescription, err.Error()) 32 | } 33 | return &selfRetriever{targets: targets}, nil 34 | } 35 | 36 | func (f selfRetriever) GetTargets() ([]Target, error) { 37 | return f.targets, nil 38 | } 39 | 40 | func (f selfRetriever) Watch() error { 41 | // NOOP 42 | return nil 43 | } 44 | 45 | func (f selfRetriever) Name() string { 46 | return "self" 47 | } 48 | -------------------------------------------------------------------------------- /internal/pkg/labels/labels.go: -------------------------------------------------------------------------------- 1 | // Package labels ... 2 | // Copyright 2019 New Relic Corporation. All rights reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | package labels 5 | 6 | // Set structure implemented as a map. 7 | type Set map[string]interface{} 8 | 9 | // InfoSource represents a prometheus info metric, those are pseudo-metrics 10 | // that provide metadata in the form of labels. 11 | type InfoSource struct { 12 | Name string 13 | Labels Set 14 | } 15 | 16 | // DifferenceEqualValues does: 17 | // - Get all the labels that have are in both A and B label sets 18 | // - If those labels have the same values in both A and B, returns the difference A - B of label-values and "true" 19 | // - Otherwise, returns nil and false 20 | // - If there is no intersection in the label names, returns A and true 21 | func DifferenceEqualValues(a, b Set) (Set, bool) { 22 | difference := make(Set, len(a)) 23 | for k, v := range a { 24 | difference[k] = v 25 | } 26 | 27 | for key, vb := range b { 28 | if va, ok := a[key]; ok { 29 | if vb == va { 30 | delete(difference, key) 31 | } else { 32 | return nil, false 33 | } 34 | } 35 | } 36 | return difference, true 37 | } 38 | 39 | // Join returns the labels from src that should be added to dst if the label names in criteria coincide. 40 | // If criteria is empty, returns src 41 | // The function ignores the values in criteria 42 | func Join(src, dst, criteria Set) (Set, bool) { 43 | ret := Set{} 44 | for k, v := range src { 45 | ret[k] = v 46 | } 47 | for name := range criteria { 48 | vs, ok := src[name] 49 | if !ok { 50 | return nil, false 51 | } 52 | vd, ok := dst[name] 53 | if !ok { 54 | return nil, false 55 | } 56 | if vs != vd { 57 | return nil, false 58 | } 59 | delete(ret, name) 60 | } 61 | return ret, true 62 | } 63 | 64 | // ToAdd decide which labels should be added, a set of _info metrics, to the destination label 65 | // set. 66 | // It does, for each info: 67 | // - if DifferenceEqualValues(info, b) == x, true: 68 | // - suffixes info.Name to all x label names and adds it to the result 69 | // - If info1.Name == info2.Name AND DifferenceEqualValues(info1, b) == x, true and DifferenceEqualValues(info1, b) == y, true: 70 | // - no metrics neither from info1.Name nor info2.Name are added to the result 71 | func ToAdd(infos []InfoSource, dst Set) Set { 72 | // Time complexity of this implementation (assuming no hash collisions): O(IxL), where: 73 | // - I is the number of _info fields 74 | // - L is the average number of labels that should be added, from each info field 75 | 76 | // key: source info metric, value: labels to be added for this info 77 | labels := make(map[string]Set, len(infos)) 78 | // info sources that must be ignored because there would be conflicts (same label names, different values) 79 | ignoredInfos := map[string]interface{}{} 80 | 81 | iterateInfos: 82 | for _, i := range infos { 83 | if _, ok := ignoredInfos[i.Name]; ok { 84 | continue 85 | } 86 | toAdd, ok := DifferenceEqualValues(i.Labels, dst) 87 | if !ok { 88 | continue 89 | } 90 | for k, v := range toAdd { 91 | infoLabels, ok := labels[i.Name] 92 | if !ok { 93 | infoLabels = Set{} 94 | labels[i.Name] = infoLabels 95 | } 96 | if alreadyVal, ok := infoLabels[k]; ok && v != alreadyVal { 97 | // two infos have different coinciding attributes. Discarding this info name 98 | ignoredInfos[i.Name] = true 99 | continue iterateInfos 100 | } 101 | infoLabels[k] = v 102 | } 103 | } 104 | 105 | // Removed ignored _info fields from the initial tree of labels 106 | for k := range ignoredInfos { 107 | delete(labels, k) 108 | } 109 | 110 | // consolidate the tree of labels into a flat map, where each entry is: 111 | // info_name.label_name = label_value 112 | flatLabels := Set{} 113 | for infoName, infoLabels := range labels { 114 | for k, v := range infoLabels { 115 | flatLabels[k+"."+infoName] = v 116 | } 117 | } 118 | return flatLabels 119 | } 120 | 121 | // Accumulate copies the labels of the source label Set into the destination. 122 | func Accumulate(dst, src Set) { 123 | for k, v := range src { 124 | if _, ok := dst[k]; !ok { 125 | dst[k] = v 126 | } 127 | } 128 | } 129 | 130 | // AccumulateOnly copies the labels from the source set into the destination, but only those that are present 131 | // in the attrs set 132 | func AccumulateOnly(dst, src, attrs Set) { 133 | for k := range attrs { 134 | if v, ok := src[k]; ok { 135 | if _, ok := dst[k]; !ok { 136 | dst[k] = v 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /internal/pkg/labels/labels_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package labels 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDifferenceEqualValues(t *testing.T) { 13 | cases := []struct { 14 | a Set // source labels 15 | b Set // labels whose intersection with A would be subtracted from A 16 | exp Set // expected result Set 17 | match bool // if we expect that common a and b labels match 18 | }{ 19 | { 20 | a: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 21 | b: Set{"container": "c1", "namespace": "ns1", "node": "n1", "pod": "p1"}, 22 | exp: Set{"container_id": "cid1", "image": "i1", "image_id": "iid1"}, 23 | match: true, 24 | }, 25 | { 26 | a: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 27 | b: Set{"container": "different", "namespace": "ns1", "node": "n1", "pod": "p1"}, 28 | exp: nil, 29 | match: false, 30 | }, 31 | { 32 | a: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 33 | b: Set{"container": "different", "namespace": "ns1", "node": "n1", "pod": "p1"}, 34 | exp: nil, 35 | match: false, 36 | }, 37 | { 38 | a: Set{"a": "b", "c": "d", "e": "f"}, 39 | b: Set{"g": "h", "i": "j"}, 40 | exp: Set{"a": "b", "c": "d", "e": "f"}, 41 | match: true, 42 | }, 43 | { 44 | a: Set{}, 45 | b: Set{"g": "h", "i": "j"}, 46 | exp: Set{}, 47 | match: true, 48 | }, 49 | { 50 | a: Set{"a": "b", "c": "d", "e": "f"}, 51 | b: Set{}, 52 | exp: Set{"a": "b", "c": "d", "e": "f"}, 53 | match: true, 54 | }, 55 | { 56 | a: Set{}, 57 | b: Set{}, 58 | exp: Set{}, 59 | match: true, 60 | }, 61 | } 62 | for _, c := range cases { 63 | t.Run(fmt.Sprintf("match: %v exp: %v", c.match, c.exp), func(t *testing.T) { 64 | i, ok := DifferenceEqualValues(c.a, c.b) 65 | assert.Equal(t, c.match, ok) 66 | assert.Equal(t, c.exp, i) 67 | }) 68 | } 69 | } 70 | 71 | func TestToAdd(t *testing.T) { 72 | cases := []struct { 73 | name string 74 | infos []InfoSource // Info labels 75 | dst Set // Labes where info labels would be added 76 | exp Set // expected labels that should be added Set 77 | }{ 78 | { 79 | name: "same set of coinciding labels", 80 | infos: []InfoSource{ 81 | { 82 | Name: "some_info", 83 | Labels: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 84 | }, 85 | { 86 | Name: "other_info", 87 | Labels: Set{"container": "c1", "namespace": "ns1", "node": "n1", "pod": "p1", "stuff": 356}, 88 | }, 89 | }, 90 | dst: Set{"container": "c1", "namespace": "ns1", "node": "n1", "pod": "p1"}, 91 | exp: Set{"container_id.some_info": "cid1", "image.some_info": "i1", "image_id.some_info": "iid1", "stuff.other_info": 356}, 92 | }, 93 | { 94 | name: "different set of coinciding labels", 95 | infos: []InfoSource{ 96 | { 97 | Name: "some_info", 98 | Labels: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 99 | }, 100 | { 101 | Name: "other_info", 102 | Labels: Set{"container": "c1", "node": "n1", "pod": "p1", "stuff": 356}, // namespace does not coincide 103 | }, 104 | }, 105 | dst: Set{"container": "c1", "namespace": "ns1", "node": "n1", "pod": "p1"}, 106 | exp: Set{"container_id.some_info": "cid1", "image.some_info": "i1", "image_id.some_info": "iid1", "stuff.other_info": 356}, 107 | }, 108 | { 109 | name: "other_info does not coincide in a label value", 110 | infos: []InfoSource{ 111 | { 112 | Name: "some_info", 113 | Labels: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 114 | }, 115 | { 116 | Name: "other_info", 117 | Labels: Set{"container": "c1bis", "node": "n1", "pod": "p1", "stuff": 356}, 118 | }, 119 | }, 120 | dst: Set{"container": "c1", "namespace": "ns1", "node": "n1", "pod": "p1"}, 121 | exp: Set{"container_id.some_info": "cid1", "image.some_info": "i1", "image_id.some_info": "iid1"}, // other_info Labels are not added 122 | }, 123 | { 124 | name: "no label coincidence in destination label set", 125 | infos: []InfoSource{ 126 | { 127 | Name: "some_info", 128 | Labels: Set{"container": "c1", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 129 | }, 130 | { 131 | Name: "other_info", 132 | Labels: Set{"container": "c1", "node": "n1", "pod": "p1", "stuff": 356}, 133 | }, 134 | }, 135 | dst: Set{"a": "b", "c": "d", "f": "g"}, 136 | // All the labels from info sources are going to be added. Please observe that some labels will be added by duplicate 137 | exp: Set{ 138 | "container.some_info": "c1", "container_id.some_info": "cid1", "image.some_info": "i1", "image_id.some_info": "iid1", "namespace.some_info": "ns1", "pod.some_info": "p1", 139 | "container.other_info": "c1", "node.other_info": "n1", "pod.other_info": "p1", "stuff.other_info": 356, 140 | }, 141 | }, 142 | { 143 | name: "definitely not belonging to the same entity", 144 | infos: []InfoSource{ 145 | { 146 | Name: "some_info", 147 | Labels: Set{"container": "c2", "container_id": "cid1", "image": "i1", "image_id": "iid1", "namespace": "ns1", "pod": "p1"}, 148 | }, 149 | { 150 | Name: "other_info", 151 | Labels: Set{"container": "c3", "namespace": "ns1", "node": "n1", "pod": "p1", "stuff": 356}, 152 | }, 153 | }, 154 | dst: Set{"container": "c1", "namespace": "ns1", "node": "n1", "pod": "p1"}, 155 | exp: Set{}, // despite many labels in common, no labels are going to be added since container differs 156 | }, 157 | { 158 | name: "infos with the same name and some common labels", 159 | infos: []InfoSource{ 160 | { 161 | Name: "some_info", 162 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "something": "cool"}, 163 | }, 164 | { 165 | Name: "some_info", 166 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "stuff": 356}, 167 | }, 168 | }, 169 | dst: Set{"container": "c1", "namespace": "ns1", "pod": "p1"}, 170 | exp: Set{"something.some_info": "cool", "stuff.some_info": 356}, 171 | }, 172 | { 173 | name: "infos with the same name and a different-value, same-name label", 174 | infos: []InfoSource{ 175 | { 176 | Name: "some_info", 177 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "something": "cool", "discarding_id": "12345"}, 178 | }, 179 | { 180 | Name: "some_info", 181 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "stuff": 356, "discarding_id": "12345"}, 182 | }, 183 | { 184 | Name: "some_info", 185 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "discarding_id": "33333"}, 186 | }, 187 | }, 188 | dst: Set{"container": "c1", "namespace": "ns1", "pod": "p1"}, 189 | // Since we cannot be sure whether we should apply metrics from discarding_id == 12345 or 333333, we don't add any of them 190 | exp: Set{}, 191 | }, 192 | { 193 | name: "infos with the same name and a different-value, same-name label. Other infos can be added", 194 | infos: []InfoSource{ 195 | { 196 | Name: "some_info", 197 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "something": "cool", "discarding_id": "12345"}, 198 | }, 199 | { 200 | Name: "cool_stuff", 201 | Labels: Set{"container": "c1", "tracatra": "tracatra"}, 202 | }, 203 | { 204 | Name: "some_info", 205 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "stuff": 356, "discarding_id": "12345"}, 206 | }, 207 | { 208 | Name: "some_info", 209 | Labels: Set{"container": "c1", "namespace": "ns1", "pod": "p1", "discarding_id": "33333"}, 210 | }, 211 | }, 212 | dst: Set{"container": "c1", "namespace": "ns1", "pod": "p1"}, 213 | // Since we cannot be sure whether we should apply metrics from discarding_id == 12345 or 333333, we don't add any of them 214 | exp: Set{"tracatra.cool_stuff": "tracatra"}, 215 | }, 216 | } 217 | for _, c := range cases { 218 | t.Run(c.name, func(t *testing.T) { 219 | i := ToAdd(c.infos, c.dst) 220 | assert.Equal(t, c.exp, i) 221 | }) 222 | } 223 | } 224 | 225 | func TestAccumulate(t *testing.T) { 226 | cases := []struct { 227 | dst Set 228 | src Set 229 | exp Set // expected union 230 | }{ 231 | { 232 | dst: Set{"a": "b", "c": "d"}, 233 | src: Set{"e": "f", "g": "h"}, 234 | exp: Set{"a": "b", "c": "d", "e": "f", "g": "h"}, 235 | }, 236 | { 237 | dst: Set{"a": "b", "c": "d"}, 238 | src: Set{}, 239 | exp: Set{"a": "b", "c": "d"}, 240 | }, 241 | { 242 | dst: Set{}, 243 | src: Set{"e": "f", "g": "h"}, 244 | exp: Set{"e": "f", "g": "h"}, 245 | }, 246 | { 247 | dst: Set{"a": "b", "c": "d"}, 248 | src: Set{"e": "f", "a": "c"}, 249 | exp: Set{"a": "b", "c": "d", "e": "f"}, // in case of collision, old labels are kept 250 | }, 251 | } 252 | for i, c := range cases { 253 | t.Run(fmt.Sprint("case", i), func(t *testing.T) { 254 | Accumulate(c.dst, c.src) 255 | assert.Equal(t, c.exp, c.dst) 256 | }) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /internal/pkg/prometheus/metrics.go: -------------------------------------------------------------------------------- 1 | // Package prometheus ... 2 | // Copyright 2019 New Relic Corporation. All rights reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | package prometheus 5 | 6 | import prom "github.com/prometheus/client_golang/prometheus" 7 | 8 | var ( 9 | targetSize = prom.NewGaugeVec(prom.GaugeOpts{ 10 | Namespace: "nr_stats", 11 | Subsystem: "integration", 12 | Name: "payload_size", 13 | Help: "Size of target's payload", 14 | }, 15 | []string{ 16 | "target", 17 | }, 18 | ) 19 | totalScrapedPayload = prom.NewGauge(prom.GaugeOpts{ 20 | Namespace: "nr_stats", 21 | Subsystem: "integration", 22 | Name: "total_payload_size", 23 | Help: "Total size of the payloads scraped", 24 | }) 25 | ) 26 | 27 | func init() { 28 | prom.MustRegister(targetSize) 29 | prom.MustRegister(totalScrapedPayload) 30 | } 31 | -------------------------------------------------------------------------------- /internal/pkg/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | // Package prometheus ... 2 | // Copyright 2019 New Relic Corporation. All rights reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | package prometheus 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | 13 | prom "github.com/prometheus/client_golang/prometheus" 14 | dto "github.com/prometheus/client_model/go" 15 | "github.com/prometheus/common/expfmt" 16 | ) 17 | 18 | // MetricFamiliesByName is a map of Prometheus metrics family names and their 19 | // representation. 20 | type MetricFamiliesByName map[string]dto.MetricFamily 21 | 22 | // HTTPDoer executes http requests. It is implemented by *http.Client. 23 | type HTTPDoer interface { 24 | Do(req *http.Request) (*http.Response, error) 25 | } 26 | 27 | // ResetTotalScrapedPayload resets the integration totalScrapedPayload 28 | // metric. 29 | func ResetTotalScrapedPayload() { 30 | totalScrapedPayload.Set(0) 31 | } 32 | 33 | // ResetTargetSize resets the integration targetSize 34 | // metric. 35 | func ResetTargetSize() { 36 | targetSize.Reset() 37 | } 38 | 39 | const ( 40 | // XPrometheusScrapeTimeoutHeader included in all requests. It informs exporters about its timeout. 41 | XPrometheusScrapeTimeoutHeader = "X-Prometheus-Scrape-Timeout-Seconds" 42 | // AcceptHeader included in all requests 43 | AcceptHeader = "Accept" 44 | ) 45 | 46 | // Get scrapes the given URL and decodes the retrieved payload. 47 | func Get(client HTTPDoer, url string, acceptHeader string, fetchTimeout string) (MetricFamiliesByName, error) { 48 | mfs := MetricFamiliesByName{} 49 | req, err := http.NewRequest("GET", url, nil) 50 | if err != nil { 51 | return mfs, err 52 | } 53 | 54 | req.Header.Add(AcceptHeader, acceptHeader) 55 | req.Header.Add(XPrometheusScrapeTimeoutHeader, fetchTimeout) 56 | 57 | resp, err := client.Do(req) 58 | if err != nil { 59 | return mfs, err 60 | } 61 | 62 | if resp.StatusCode < 200 || resp.StatusCode > 300 { 63 | return nil, fmt.Errorf("status code returned by the prometheus exporter indicates an error occurred: %d", resp.StatusCode) 64 | } 65 | 66 | body, err := ioutil.ReadAll(resp.Body) 67 | if err != nil { 68 | return mfs, err 69 | } 70 | r := bytes.NewReader(body) 71 | 72 | d := expfmt.NewDecoder(r, expfmt.FmtText) 73 | for { 74 | var mf dto.MetricFamily 75 | if err := d.Decode(&mf); err != nil { 76 | if err == io.EOF { 77 | break 78 | } 79 | return nil, err 80 | } 81 | mfs[mf.GetName()] = mf 82 | } 83 | 84 | bodySize := float64(len(body)) 85 | targetSize.With(prom.Labels{"target": url}).Set(bodySize) 86 | totalScrapedPayload.Add(bodySize) 87 | return mfs, nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/pkg/prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package prometheus_test 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/newrelic/nri-prometheus/internal/pkg/prometheus" 13 | ) 14 | 15 | const testHeader = "application/openmetrics-text" 16 | 17 | func TestGetHeader(t *testing.T) { 18 | fetchTimeout := "15" 19 | 20 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | accept := r.Header.Get(prometheus.AcceptHeader) 22 | if accept != testHeader { 23 | t.Errorf("Expected Accept header %s, got %q", testHeader, accept) 24 | } 25 | 26 | xPrometheus := r.Header.Get(prometheus.XPrometheusScrapeTimeoutHeader) 27 | if xPrometheus != fetchTimeout { 28 | t.Errorf("Expected xPrometheus header %s, got %q", xPrometheus, fetchTimeout) 29 | } 30 | 31 | _, _ = w.Write([]byte("metric_a 1\nmetric_b 2\n")) 32 | })) 33 | defer ts.Close() 34 | 35 | expected := []string{"metric_a", "metric_b"} 36 | mfs, err := prometheus.Get(http.DefaultClient, ts.URL, testHeader, fetchTimeout) 37 | actual := []string{} 38 | for k := range mfs { 39 | actual = append(actual, k) 40 | } 41 | 42 | assert.NoError(t, err) 43 | assert.ElementsMatch(t, expected, actual) 44 | } 45 | -------------------------------------------------------------------------------- /internal/pkg/prometheus/testdata/redis-metrics: -------------------------------------------------------------------------------- 1 | # HELP redis_exporter_build_info redis exporter build_info 2 | # TYPE redis_exporter_build_info gauge 3 | redis_exporter_build_info{build_date="2018-07-03-14:18:56",commit_sha="3e15af27aac37e114b32a07f5e9dc0510f4cbfc4",golang_version="go1.9.4",version="v0.20.2"} 1 4 | # HELP redis_exporter_scrapes_total Current total redis scrapes. 5 | # TYPE redis_exporter_scrapes_total counter 6 | redis_exporter_scrapes_total 42 7 | # HELP redis_instance_info Information about the Redis instance 8 | # TYPE redis_instance_info gauge 9 | redis_instance_info{addr="ohai-playground-redis-master:6379",alias="ohai-playground-redis",os="Linux 4.15.0 x86_64",redis_build_id="c701a4acd98ea64a",redis_mode="standalone",redis_version="4.0.10",role="master"} 1 10 | redis_instance_info{addr="ohai-playground-redis-slave:6379",alias="ohai-playground-redis",os="Linux 4.15.0 x86_64",redis_build_id="c701a4acd98ea64a",redis_mode="standalone",redis_version="4.0.10",role="slave"} 1 11 | # HELP redis_instantaneous_input_kbps instantaneous_input_kbpsmetric 12 | # TYPE redis_instantaneous_input_kbps gauge 13 | redis_instantaneous_input_kbps{addr="ohai-playground-redis-master:6379",alias="ohai-playground-redis"} 0.05 14 | redis_instantaneous_input_kbps{addr="ohai-playground-redis-slave:6379",alias="ohai-playground-redis"} 0 15 | -------------------------------------------------------------------------------- /internal/pkg/prometheus/testdata/simple-metrics: -------------------------------------------------------------------------------- 1 | # HELP go_goroutines Number of goroutines that currently exist. 2 | # TYPE go_goroutines gauge 3 | go_goroutines 8 4 | # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used. 5 | # TYPE go_memstats_heap_idle_bytes gauge 6 | go_memstats_heap_idle_bytes 2.301952e+06 7 | # HELP go_gc_duration_seconds A summary of the GC invocation durations. 8 | # TYPE go_gc_duration_seconds summary 9 | go_gc_duration_seconds{quantile="0"} 7.5235e-05 10 | go_gc_duration_seconds{quantile="0.25"} 7.5235e-05 11 | go_gc_duration_seconds{quantile="0.5"} 0.000200349 12 | go_gc_duration_seconds{quantile="0.75"} 0.000200349 13 | go_gc_duration_seconds{quantile="1"} 0.000200349 14 | go_gc_duration_seconds_sum 0.000275584 15 | go_gc_duration_seconds_count 2 16 | # HELP http_requests_total Total number of HTTP requests made. 17 | # TYPE http_requests_total counter 18 | http_requests_total{code="200",handler="prometheus",method="get"} 2 19 | -------------------------------------------------------------------------------- /internal/retry/retry.go: -------------------------------------------------------------------------------- 1 | // Package retry ... 2 | // Copyright 2019 New Relic Corporation. All rights reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | package retry 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | // RetriableFunc is a function to be retried in order to get a successful 12 | // execution. In general this are functions which success depend on external 13 | // conditions that can eventually be met. 14 | type RetriableFunc func() error 15 | 16 | // OnRetryFunc is executed after a RetrieableFunc fails and receives the 17 | // returned error as argument. 18 | type OnRetryFunc func(err error) 19 | 20 | type config struct { 21 | delay time.Duration 22 | timeout time.Duration 23 | onRetry OnRetryFunc 24 | } 25 | 26 | // Option to be applied to the retry config. 27 | type Option func(*config) 28 | 29 | // Delay is the time to wait after a failed execution before retrying. 30 | func Delay(delay time.Duration) Option { 31 | return func(c *config) { 32 | c.delay = delay 33 | } 34 | } 35 | 36 | // Timeout sets the time duration to wait before aborting the retries if there 37 | // are not successful executions of the function. 38 | func Timeout(timeout time.Duration) Option { 39 | return func(c *config) { 40 | c.timeout = timeout 41 | } 42 | } 43 | 44 | // OnRetry sets a new function to be applied to the error returned by the 45 | // function execution. 46 | func OnRetry(fn OnRetryFunc) Option { 47 | return func(c *config) { 48 | c.onRetry = fn 49 | } 50 | } 51 | 52 | // Do retries the execution of the given function until it finishes 53 | // successfully, it returns a nil error, or a timeout is reached. 54 | // 55 | // If the function returns a non-nil error, a delay will be applied before 56 | // executing the onRetry function on the error and retrying the function. 57 | func Do(fn RetriableFunc, opts ...Option) error { 58 | var nRetries int 59 | c := &config{ 60 | delay: 2 * time.Second, 61 | timeout: 2 * time.Minute, 62 | onRetry: func(err error) {}, 63 | } 64 | for _, opt := range opts { 65 | opt(c) 66 | } 67 | tRetry := time.NewTicker(c.delay) 68 | tTimeout := time.NewTicker(c.timeout) 69 | for { 70 | lastError := fn() 71 | if lastError == nil { 72 | return nil 73 | } 74 | 75 | select { 76 | case <-tTimeout.C: 77 | tRetry.Stop() 78 | tTimeout.Stop() 79 | return fmt.Errorf("timeout reached, %d retries executed. last error: %s", nRetries, lastError) 80 | case <-tRetry.C: 81 | c.onRetry(lastError) 82 | nRetries++ 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /load-test/README.md: -------------------------------------------------------------------------------- 1 | ## LOAD TEST 2 | 3 | This folder contains the chart, the script and the go-test used by the load-test gh pipeline. 4 | 5 | You can also run manually the load tests against a local minikube. 6 | 7 | es from the repo root folder: 8 | ```bash 9 | minikube --memory 8192 --cpus 4 start 10 | NEWRELIC_LICENSE=xxxx 11 | source ./load-test/load_test.sh 12 | runLoadTest 13 | ``` 14 | 15 | In some environment you will need to uncomment the command"dos2unix ./load-test/load_test.results" in load_test.sh 16 | 17 | The image is compiled, deployed with `Skaffold`, the load test chart is deployed with 800 targets and the results from the 18 | prometheus output are collected and parsed with a golang help tool. 19 | 20 | Check load_test.sh to gather more information regarding the behaviour. 21 | -------------------------------------------------------------------------------- /load-test/load_test.go: -------------------------------------------------------------------------------- 1 | // +build loadtests 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "log" 8 | "os" 9 | "testing" 10 | 11 | dto "github.com/prometheus/client_model/go" 12 | "github.com/prometheus/common/expfmt" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const ( 17 | timeLimit = 40 18 | targetExpected = 800 19 | memoryLimit = 2 * 1e9 20 | filename = "load_test.results" 21 | ) 22 | 23 | func TestLoad(t *testing.T) { 24 | mfs := parsePrometheusFile(t, filename) 25 | 26 | require.LessOrEqual(t, *mfs["nr_stats_integration_process_duration_seconds"].Metric[0].Gauge.Value, float64(timeLimit), "taking too much time to process metrics") 27 | log.Printf("nr_stats_integration_process_duration_seconds: %f", *mfs["nr_stats_integration_process_duration_seconds"].Metric[0].Gauge.Value) 28 | 29 | require.LessOrEqual(t, *mfs["process_resident_memory_bytes"].Metric[0].Gauge.Value, float64(memoryLimit), "taking too much time to process metrics") 30 | log.Printf("memory consumption (process_resident_memory_bytes): %fMB", *mfs["process_resident_memory_bytes"].Metric[0].Gauge.Value/1e6) 31 | 32 | for _, m := range mfs["nr_stats_targets"].Metric { 33 | if *m.Label[0].Value == "kubernetes" { 34 | require.GreaterOrEqual(t, *m.Gauge.Value, float64(targetExpected), "missing targets") 35 | log.Printf("Number of targets scraped: %f", *m.Gauge.Value) 36 | } 37 | } 38 | } 39 | 40 | // MetricFamiliesByName is a map of Prometheus metrics family names and their representation. 41 | type MetricFamiliesByName map[string]dto.MetricFamily 42 | 43 | func parsePrometheusFile(t *testing.T, filename string) MetricFamiliesByName { 44 | mfs := MetricFamiliesByName{} 45 | 46 | file, err := os.Open(filename) 47 | defer file.Close() 48 | 49 | require.NoError(t, err, "No error expected") 50 | 51 | d := expfmt.NewDecoder(file, expfmt.TextVersion) 52 | for { 53 | var mf dto.MetricFamily 54 | if err := d.Decode(&mf); err != nil { 55 | if err == io.EOF { 56 | break 57 | } 58 | require.NoError(t, err, "The only accepted error is EOF") 59 | } 60 | mfs[mf.GetName()] = mf 61 | } 62 | return mfs 63 | } 64 | -------------------------------------------------------------------------------- /load-test/load_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Clean away old resources not useful anymore 4 | cleanOldResources(){ 5 | kubectl delete namespace newrelic-load || true 6 | rm ./load-test/load_test.results || true 7 | } 8 | 9 | #Deploy loadTest chart 10 | deployLoadTestEnvironment(){ 11 | kubectl create namespace newrelic-load 12 | ## we are using the template and not the install since helm suffers when deploying at the same time 800+ resources "http2: stream closed" 13 | helm template load ./charts/load-test-environment --values ./charts/load-test-environment/values.yaml -n newrelic-load | kubectl apply -f - -n newrelic-load 14 | } 15 | 16 | #Compile and deploy with skaffold last version of nri-prometheus 17 | deployCurrentNriPrometheus(){ 18 | # We need to statically link libraries otherwise in the current test Docker image the command could fail 19 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/nri-prometheus ./cmd/nri-prometheus/ 20 | yq eval 'select(document_index == 3) |= .spec.template.spec.containers[0].env[0].value = env(NEWRELIC_LICENSE)' ./deploy/local.yaml.example > ./deploy/local.yaml 21 | skaffold run 22 | } 23 | 24 | #Retrieve the results of the tests from the prometheus output of the integration 25 | retrieveResults(){ 26 | POD=$(kubectl get pods -n default -l app=nri-prometheus -o jsonpath="{.items[0].metadata.name}") 27 | kubectl logs ${POD} 28 | kubectl exec -n default ${POD} -- wget localhost:8080/metrics -q -O - > ./load-test/load_test.results 29 | # Debug This might be needed when developing locally 30 | #dos2unix ./load-test/load_test.results 31 | } 32 | 33 | #Verify the results of the tests (memory, time elapsed, total targets) 34 | verifyResults(){ 35 | # we need the loadtests flag in order to make sure that these tests are run only needed 36 | go test -v -tags=loadtests ./load-test/... 37 | } 38 | 39 | runLoadTest(){ 40 | if [ -z "$NEWRELIC_LICENSE" ] 41 | then 42 | echo "NEWRELIC_LICENSE environment variable should be set" 43 | else 44 | cleanOldResources 45 | deployLoadTestEnvironment 46 | deployCurrentNriPrometheus 47 | sleep 180 48 | retrieveResults 49 | verifyResults 50 | fi 51 | } 52 | -------------------------------------------------------------------------------- /load-test/mockexporter/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build 2 | 3 | LABEL maintainer="Roberto Santalla " 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o mockexporter . 9 | 10 | 11 | FROM alpine:latest 12 | 13 | RUN mkdir -p /app 14 | COPY --from=build /app/mockexporter /app 15 | 16 | WORKDIR /app 17 | ENTRYPOINT ["/app/mockexporter"] 18 | -------------------------------------------------------------------------------- /load-test/mockexporter/load_test_small_sample.data: -------------------------------------------------------------------------------- 1 | # HELP go_gc_duration_seconds A summary of the GC invocation durations. 2 | # TYPE go_gc_duration_seconds summary 3 | go_gc_duration_seconds{instance="1.2.3.4",quantile="0"} 6.1708e-05 4 | go_gc_duration_seconds{instance="1.2.3.4",quantile="0.25"} 8.1063e-05 5 | go_gc_duration_seconds{instance="1.2.3.4",quantile="0.5"} 9.5992e-05 6 | go_gc_duration_seconds{instance="1.2.3.4",quantile="0.75"} 0.000127407 7 | go_gc_duration_seconds{instance="1.2.3.4",quantile="1"} 0.015248652 8 | go_gc_duration_seconds_sum 10.254398459 9 | go_gc_duration_seconds_count 52837 10 | 11 | # HELP go_goroutines Number of goroutines that currently exist. 12 | # TYPE go_goroutines gauge 13 | go_goroutines 126 14 | 15 | # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed. 16 | # TYPE go_memstats_alloc_bytes_total counter 17 | go_memstats_alloc_bytes_total 1.52575345048e+11 18 | 19 | # A histogram, which has a pretty complex representation in the text format: 20 | # HELP http_request_duration_seconds A histogram of the request duration. 21 | # TYPE http_request_duration_seconds histogram 22 | http_request_duration_seconds_bucket{le="0.05"} 24054 23 | http_request_duration_seconds_bucket{le="0.1"} 33444 24 | http_request_duration_seconds_bucket{le="0.2"} 100392 25 | http_request_duration_seconds_bucket{le="0.5"} 129389 26 | http_request_duration_seconds_bucket{le="1"} 133988 27 | http_request_duration_seconds_bucket{le="+Inf"} 144320 28 | http_request_duration_seconds_sum 53423 29 | http_request_duration_seconds_count 144320 30 | -------------------------------------------------------------------------------- /load-test/mockexporter/mockexporter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | metrics := flagEnvString("metrics", "", "path to the metrics file to serve") 18 | latency := flagEnvInt("latency", 0, "artificial latency to induce in the responses (milliseconds)") 19 | latencyVariation := flagEnvInt("latency-variation", 0, "randomly variate latency by +- this value (percentage)") 20 | maxRoutines := flagEnvInt("max-routines", 0, "maximum number of requests to handle in parallel") 21 | listenAddress := flagEnvString("addr", ":9940", "address:port pair to listen in") 22 | flag.Parse() 23 | 24 | if *metrics == "" { 25 | fmt.Println("A metrics file (-metrics) must be specified:") 26 | flag.PrintDefaults() 27 | os.Exit(1) 28 | } 29 | 30 | ms := &metricsServer{ 31 | MetricsFile: *metrics, 32 | Latency: *latency, 33 | LatencyVariation: *latencyVariation, 34 | MaxRoutines: *maxRoutines, 35 | } 36 | 37 | log.Println(ms.ListenAndServe(*listenAddress)) 38 | } 39 | 40 | // Wrapper to get default from environment if present 41 | func flagEnvString(name, defaultValue, usage string) *string { 42 | val := os.Getenv(strings.ToUpper(strings.ReplaceAll(name, "-", "_"))) 43 | if val == "" { 44 | val = defaultValue 45 | } 46 | 47 | return flag.String( 48 | name, 49 | val, 50 | usage, 51 | ) 52 | } 53 | 54 | // Wrapper to get default from environment if present 55 | func flagEnvInt(name string, defaultValue int, usage string) *int { 56 | val, err := strconv.Atoi(os.Getenv(strings.ToUpper(strings.ReplaceAll(name, "-", "_")))) 57 | if err != nil { 58 | val = defaultValue 59 | } 60 | 61 | return flag.Int( 62 | name, 63 | val, 64 | usage, 65 | ) 66 | } 67 | 68 | type metricsServer struct { 69 | MetricsFile string 70 | Latency int 71 | LatencyVariation int 72 | MaxRoutines int 73 | 74 | metricsBuffer []byte 75 | waiter chan struct{} 76 | } 77 | 78 | func (ms *metricsServer) ListenAndServe(address string) error { 79 | metricsFile, err := os.Open(ms.MetricsFile) 80 | if err != nil { 81 | return fmt.Errorf("could not open %s: %v", ms.MetricsFile, err) 82 | } 83 | 84 | ms.metricsBuffer, err = ioutil.ReadAll(metricsFile) 85 | if err != nil { 86 | return fmt.Errorf("could not load metrics into memory: %v", err) 87 | } 88 | 89 | log.Println("metrics loaded from disk") 90 | 91 | if ms.MaxRoutines != 0 { 92 | ms.waiter = make(chan struct{}, ms.MaxRoutines) 93 | } 94 | 95 | log.Printf("starting server in " + address) 96 | return http.ListenAndServe(address, ms) 97 | } 98 | 99 | func (ms *metricsServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 100 | if ms.MaxRoutines != 0 { 101 | ms.waiter <- struct{}{} 102 | defer func() { 103 | <-ms.waiter 104 | }() 105 | } 106 | 107 | time.Sleep(ms.latency()) 108 | _, _ = rw.Write(ms.metricsBuffer) 109 | } 110 | 111 | func (ms *metricsServer) latency() time.Duration { 112 | lat := time.Duration(ms.Latency) * time.Millisecond 113 | 114 | if ms.LatencyVariation == 0 { 115 | return lat 116 | } 117 | 118 | variation := float64(ms.LatencyVariation) / 100 119 | variation = (rand.Float64() - 0.5) * variation * 2 // Random in (-variation, variation) 120 | 121 | return time.Duration(float64(lat) + float64(lat)*variation) 122 | } 123 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v1beta12 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: quay.io/newrelic/nri-prometheus 6 | docker: 7 | dockerfile: Dockerfile.dev 8 | deploy: 9 | kubectl: 10 | manifests: 11 | - deploy/local.yaml 12 | --------------------------------------------------------------------------------