├── .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 |
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 |
--------------------------------------------------------------------------------