├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── RELEASE.md ├── analytics ├── analytics.go └── remote_write.go ├── bump-fork.sh ├── config.yaml ├── config ├── config.go └── config_test.go ├── deploy ├── .gitignore ├── Makefile ├── dev.jsonnet ├── e2e.jsonnet ├── generate.sh ├── jsonnetfile.json ├── jsonnetfile.lock.json ├── kube-prometheus │ ├── .gitignore │ ├── Makefile │ ├── build.sh │ ├── grafana-dashboard-api-export.json │ ├── jsonnetfile.json │ ├── jsonnetfile.lock.json │ ├── monitoring-deploy.sh │ ├── parca.jsonnet │ └── restore-grafana-dashboard.sh ├── lib │ └── parca-agent │ │ └── parca-agent.libsonnet ├── main.jsonnet └── openshift.jsonnet ├── env-jsonnet.sh ├── env.sh ├── flags ├── codec.go ├── flags.go ├── grpc.go └── tracer.go ├── go.mod ├── go.sum ├── kubernetes-config.yaml ├── main.go ├── metrics ├── all.go ├── genschema │ └── gen.py ├── metrics.json └── types.go ├── reporter ├── arrow.go ├── elfwriter │ ├── elfwriter.go │ ├── extract.go │ ├── helpers.go │ ├── nullifying_elfwriter.go │ └── options.go ├── grpc_upload_client.go ├── metadata │ ├── agent.go │ ├── containermetadata.go │ ├── process.go │ └── system.go ├── parca_reporter.go ├── parca_reporter_test.go ├── parca_uploader.go └── parca_uploader_test.go ├── snap ├── README.md ├── hooks │ └── configure ├── local │ └── parca-agent-wrapper └── snapcraft.yaml └── uploader └── log_uploader.go /.gitattributes: -------------------------------------------------------------------------------- 1 | 3rdparty/ linguist-vendored 2 | bpf/*/vmlinux.h linguist-generated 3 | internal/pprof/ linguist-vendored 4 | internal/go/ linguist-vendored 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Artifacts 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | tags: 9 | - v* 10 | branches: 11 | - '**' 12 | 13 | jobs: 14 | artifacts: 15 | name: Goreleaser release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | packages: write 19 | contents: write 20 | container: 21 | image: docker.io/goreleaser/goreleaser-cross:v1.22.4 22 | options: --privileged 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | steps: 26 | - name: Check out the code 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | # https://github.com/actions/checkout/issues/766 32 | - name: Add repository directory to the git global config as a safe directory 33 | run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version-file: go.mod 39 | 40 | - name: Fetch all tags 41 | run: git fetch --force --tags 42 | 43 | - name: Login to registry 44 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/') 45 | run: | 46 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 47 | 48 | - name: Login to registry 49 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 50 | run: | 51 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 52 | 53 | - name: Run Goreleaser release 54 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 55 | run: goreleaser release --clean --verbose 56 | 57 | - name: Run Goreleaser snapshot 58 | run: | 59 | goreleaser release --clean --verbose --snapshot 60 | 61 | - name: Set snapshot tag 62 | id: vars 63 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/') 64 | run: | 65 | echo "tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}-$(git show -s --format=%ct)-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 66 | echo "git_tag=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT 67 | 68 | - name: Push snapshot images 69 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/') 70 | run: | 71 | docker tag ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.git_tag }}-amd64 ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-amd64 72 | docker tag ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.git_tag }}-arm64 ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-arm64 73 | docker push ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-amd64 74 | docker push ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-arm64 75 | docker manifest create ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }} --amend ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-amd64 --amend ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-arm64 76 | docker manifest annotate ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }} ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-arm64 --os linux --arch arm64 77 | docker manifest annotate ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }} ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }}-amd64 --os linux --arch amd64 78 | docker manifest push ghcr.io/parca-dev/parca-agent:${{ steps.vars.outputs.tag }} 79 | 80 | - name: Set up Jsonnet 81 | run: ./env-jsonnet.sh 82 | 83 | - name: Set up environment 84 | run: ./env.sh 85 | 86 | - name: Generate 87 | run: cd deploy && make --always-make vendor manifests 88 | 89 | - name: Prepare manifests for uploads 90 | run: | 91 | cp deploy/manifests/kubernetes-manifest.yaml deploy/manifests/kubernetes/manifest.yaml 92 | cp deploy/manifests/openshift-manifest.yaml deploy/manifests/openshift/manifest.yaml 93 | tar -zcvf deploy/manifests.tar.gz deploy/manifests 94 | 95 | - uses: actions/upload-artifact@v4 96 | with: 97 | name: manifests 98 | path: | 99 | deploy/manifests.tar.gz 100 | 101 | - name: Archive generated artifacts 102 | uses: actions/upload-artifact@v4 103 | with: 104 | name: parca-agent-dist-release 105 | if-no-files-found: error 106 | path: | 107 | dist/ 108 | 109 | - name: Release 110 | uses: softprops/action-gh-release@v0.1.15 111 | if: startsWith(github.ref, 'refs/tags/') 112 | with: 113 | files: | 114 | deploy/manifests.tar.gz 115 | deploy/manifests/kubernetes-manifest.yaml 116 | deploy/manifests/openshift-manifest.yaml 117 | 118 | snap: 119 | name: Build Snap 120 | runs-on: ubuntu-latest 121 | needs: artifacts 122 | outputs: 123 | snap: ${{ steps.snapcraft.outputs.snap }} 124 | steps: 125 | - name: Checkout repository 126 | uses: actions/checkout@v4 127 | 128 | - uses: actions/download-artifact@v4 129 | with: 130 | name: parca-agent-dist-release 131 | path: dist 132 | 133 | - name: Setup LXD (for Snapcraft) 134 | uses: canonical/setup-lxd@v0.1.1 135 | with: 136 | channel: latest/stable 137 | 138 | - name: Setup Snapcraft 139 | run: | 140 | sudo snap install snapcraft --channel 8.x/stable --classic 141 | 142 | - name: Build snaps 143 | run: | 144 | # Copy the metadata.json is so snapcraft can parse it for version info 145 | cp ./dist/metadata.json snap/local/metadata.json 146 | 147 | # Build the amd64 snap 148 | cp ./dist/linux-amd64_linux_amd64_v1/parca-agent snap/local/parca-agent 149 | snapcraft pack --verbose --build-for amd64 150 | 151 | # Build the arm64 snap 152 | cp ./dist/linux-arm64_linux_arm64/parca-agent snap/local/parca-agent 153 | snapcraft pack --verbose --build-for arm64 154 | 155 | - name: Upload locally built snap artifact 156 | uses: actions/upload-artifact@v4 157 | with: 158 | name: built-snaps 159 | path: | 160 | *.snap 161 | 162 | test-snap: 163 | name: Test Snap 164 | needs: snap 165 | runs-on: ubuntu-latest 166 | steps: 167 | - name: Fetch built snap 168 | uses: actions/download-artifact@v4 169 | with: 170 | name: built-snaps 171 | 172 | - name: Install snap & invoke Parca Agent 173 | run: | 174 | sudo snap install --classic --dangerous *_amd64.snap 175 | 176 | sudo snap set parca-agent log-level=debug 177 | parca-agent --help 178 | 179 | - name: Start Parca Agent - default config 180 | run: | 181 | sudo snap start parca-agent 182 | 183 | # Set some options to allow retries while Parca Agent comes back up 184 | CURL_OPTS=(--max-time 20 --retry 5 --retry-delay 3 --retry-connrefused) 185 | 186 | curl ${CURL_OPTS[@]} http://localhost:7071/ 187 | curl ${CURL_OPTS[@]} http://localhost:7071/metrics 188 | 189 | - name: Configure snap - node name 190 | run: | 191 | sudo snap set parca-agent node=foobar 192 | sudo snap restart parca-agent 193 | 194 | # Set some options to allow retries while Parca Agent comes back up 195 | CURL_OPTS=(--max-time 20 --retry 5 --retry-delay 3 --retry-connrefused) 196 | 197 | curl ${CURL_OPTS[@]} http://localhost:7071/ 198 | curl ${CURL_OPTS[@]} http://localhost:7071/metrics 199 | 200 | - name: Configure snap - http address 201 | run: | 202 | sudo snap set parca-agent http-address=":8081" 203 | sudo snap restart parca-agent 204 | 205 | # Set some options to allow retries while Parca comes back up 206 | CURL_OPTS=(--max-time 20 --retry 5 --retry-delay 3 --retry-connrefused) 207 | 208 | curl ${CURL_OPTS[@]} http://localhost:8081/ 209 | curl ${CURL_OPTS[@]} http://localhost:8081/metrics 210 | 211 | # In case the above tests fail, dump the logs for inspection 212 | - name: Dump snap service logs 213 | if: failure() 214 | run: | 215 | sudo snap logs parca-agent -n=all 216 | 217 | release-snap-edge: 218 | name: Release Snap (latest/edge) 219 | needs: test-snap 220 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 221 | runs-on: ubuntu-latest 222 | steps: 223 | - uses: actions/download-artifact@v4 224 | with: 225 | name: built-snaps 226 | 227 | - name: Install snapcraft 228 | run: | 229 | sudo snap install snapcraft --classic --channel=8.x/stable 230 | 231 | - name: Release to latest/edge 232 | env: 233 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} 234 | run: | 235 | snapcraft upload *_amd64.snap --release edge 236 | snapcraft upload *_arm64.snap --release edge 237 | 238 | docs: 239 | if: startsWith(github.ref, 'refs/tags/') 240 | name: Update Docs with new manifests 241 | runs-on: ubuntu-latest 242 | needs: artifacts 243 | steps: 244 | - uses: actions/checkout@v4 245 | 246 | - name: Publish Vercel 247 | run: | 248 | curl -X POST "https://api.vercel.com/v1/integrations/deploy/${{ secrets.VERCEL_WEBHOOK }}" 249 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out the code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: go.mod 23 | 24 | - name: Run Tests 25 | run: go test -race -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /goreleaser 3 | /dist 4 | /target 5 | /tmp 6 | /out 7 | /bin 8 | *.bpf.o 9 | TODO.md 10 | minikube-* 11 | /data 12 | 13 | # Snap Packaging Artifacts 14 | *.snap 15 | snap/local/parca-agent 16 | snap/local/metadata.json 17 | 18 | # kernel test stuff 19 | kerneltest/*.test 20 | kerneltest/initramfs.cpio 21 | kerneltest/logs/vm_log_*.txt 22 | kerneltest/kernels/linux-*.bz 23 | kerneltest/ci-kernels 24 | 25 | # direnv / Nix 26 | .direnv 27 | .envrc 28 | result* 29 | 30 | # debug logs 31 | *.log 32 | *.plan 33 | *.token 34 | 35 | # java 36 | *.class 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "github.com/libbpf/libbpf"] 2 | path = 3rdparty/libbpf 3 | url = https://github.com/libbpf/libbpf.git 4 | [submodule "github.com/parca-dev/testdata"] 5 | path = testdata 6 | url = https://github.com/parca-dev/testdata 7 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | env: 3 | - CGO_ENABLED=1 4 | builds: 5 | - id: linux-amd64 6 | main: ./ 7 | binary: parca-agent 8 | goos: 9 | - linux 10 | goarch: 11 | - amd64 12 | env: 13 | - CC=x86_64-linux-gnu-gcc 14 | - CXX=x86_64-linux-gnu-g++ 15 | flags: 16 | - -mod=readonly 17 | - -trimpath 18 | - -v 19 | ldflags: 20 | # https://goreleaser.com/customization/build/#reproducible-builds 21 | # {{.CommitDate}} is the date of the commit to make builds reproducible. 22 | - -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date={{.CommitDate}} -X main.goArch=amd64 23 | - -extldflags=-static 24 | tags: 25 | - osusergo 26 | - netgo 27 | - debugtracer 28 | - id: linux-arm64 29 | main: ./ 30 | binary: parca-agent 31 | goos: 32 | - linux 33 | goarch: 34 | - arm64 35 | env: 36 | - CC=aarch64-linux-gnu-gcc 37 | - CXX=aarch64-linux-gnu-g++ 38 | flags: 39 | - -mod=readonly 40 | - -trimpath 41 | - -v 42 | ldflags: 43 | # https://goreleaser.com/customization/build/#reproducible-builds 44 | # {{.CommitDate}} is the date of the commit to make builds reproducible. 45 | - -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date={{.CommitDate}} -X main.goArch=arm64 46 | - -extldflags=-static 47 | tags: 48 | - osusergo 49 | - netgo 50 | archives: 51 | - name_template: >- 52 | {{ .ProjectName }}_ 53 | {{- trimprefix .Version "v" }}_ 54 | {{- title .Os }}_ 55 | {{- if eq .Arch "amd64" }}x86_64 56 | {{- else if eq .Arch "arm64" }}aarch64 57 | {{- else }}{{ .Arch }}{{ end }} 58 | format: binary 59 | format_overrides: 60 | - goos: windows 61 | format: zip 62 | files: 63 | - 'LICENSE*' 64 | - 'README*' 65 | checksum: 66 | name_template: 'checksums.txt' 67 | snapshot: 68 | name_template: "{{ .Branch }}-{{ .ShortCommit }}" 69 | changelog: 70 | sort: asc 71 | filters: 72 | exclude: 73 | - '^docs:' 74 | - '^test:' 75 | release: 76 | github: 77 | owner: parca-dev 78 | name: parca-agent 79 | prerelease: auto 80 | draft: false 81 | name_template: '{{ .Tag }}' 82 | dockers: 83 | - image_templates: ["ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }}-amd64"] 84 | dockerfile: Dockerfile 85 | use: buildx 86 | build_flag_templates: 87 | - --pull 88 | - --platform=linux/amd64 89 | - --label=org.opencontainers.image.title={{ .ProjectName }} 90 | - --label=org.opencontainers.image.description={{ .ProjectName }} 91 | - --label=org.opencontainers.image.url=https://parca.dev/ 92 | - --label=org.opencontainers.image.source=https://github.com/parca-dev/{{ .ProjectName }} 93 | - --label=org.opencontainers.image.version={{ .Tag }} 94 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 95 | - --label=org.opencontainers.image.licenses=Apache-2.0 96 | - image_templates: ["ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }}-arm64"] 97 | goarch: arm64 98 | dockerfile: Dockerfile 99 | use: buildx 100 | build_flag_templates: 101 | - --pull 102 | - --platform=linux/arm64 103 | - --label=org.opencontainers.image.title={{ .ProjectName }} 104 | - --label=org.opencontainers.image.description={{ .ProjectName }} 105 | - --label=org.opencontainers.image.url=https://parca.dev/ 106 | - --label=org.opencontainers.image.source=https://github.com/parca-dev/{{ .ProjectName }} 107 | - --label=org.opencontainers.image.version={{ .Tag }} 108 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 109 | - --label=org.opencontainers.image.licenses=Apache-2.0 110 | docker_manifests: 111 | - name_template: ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }} 112 | image_templates: 113 | - ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }}-amd64 114 | - ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }}-arm64 115 | - name_template: ghcr.io/parca-dev/{{ .ProjectName }}:latest 116 | image_templates: 117 | - ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }}-amd64 118 | - ghcr.io/parca-dev/{{ .ProjectName }}:{{ .Tag }}-arm64 119 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This project is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license and accepts contributions via GitHub pull requests. This document outlines some of the conventions on development workflow, commit message formatting, contact points and other resources to make it easier to get your contribution accepted. 4 | 5 | # Certificate of Origin 6 | 7 | By contributing to this project you agree to sign a Contributor License Agreement(CLA). 8 | 9 | # Code of Conduct 10 | 11 | Parca-agent follows [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 12 | 13 | 14 | 15 | # Prerequisites 16 | 17 | - Linux Kernel version 5.3+ with BTF 18 | - A source of targets to discover from: Kubernetes or systemd. 19 | 20 | Install the following dependencies (Instructions are linked for each dependency). 21 | 22 | - [Go](https://golang.org/doc/install) 23 | - [Docker](https://docs.docker.com/engine/install/) 24 | - [minikube](https://kubernetes.io/docs/tasks/tools/#minikube) 25 | - [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) 26 | - [LLVM](https://apt.llvm.org/) 27 | 28 | > **Note:** LLVM version 11 is enough to compile libbpf. 29 | 30 | For the debian based distributions: 31 | ```console 32 | $ sudo apt-get update 33 | 34 | $ sudo apt-get install make zlib1g libzstd-dev pkg-config libclang-11-dev llvm-11-dev libbpf-dev libelf-dev 35 | ``` 36 | 37 | Alternatively, [Nix](https://nixos.org/download.html#download-nix) can be used to avoid installing system packages, 38 | simply run `nix-shell` or `nix develop` to load the dependencies. Docker and VirtualBox are required to be installed as system packages. 39 | 40 | # Getting Started 41 | 42 | Fork the [parca-agent](https://github.com/parca-dev/parca-agent) and [parca](https://github.com/parca-dev/parca) repositories on GitHub. 43 | Clone the repositories on to your machine. 44 | 45 | ```console 46 | $ git clone git@github.com:parca-dev/parca-agent.git 47 | ``` 48 | 49 | ## Run parca-agent 50 | 51 | Code changes can be tested locally by building parca-agent and running it to profile systemd units. 52 | The following code snippet profiles the docker daemon, i.e. `docker.service` systemd unit: 53 | 54 | ```console 55 | $ cd parca-agent 56 | 57 | $ make 58 | 59 | # Assumes Parca server runs on localhost:7070 60 | $ sudo dist/parca-agent --node=test --log-level=debug --remote-store-address=localhost:7070 --remote-store-insecure 61 | ``` 62 | 63 | The generated profiles can be seen at http://localhost:7071 . 64 | 65 | ## Working with parca server 66 | 67 | Clone the parca server repository and copy the parca-agent repository (where you have made changes) to `parca/tmp/`: 68 | 69 | ```console 70 | $ git clone git@github.com:parca-dev/parca.git 71 | 72 | $ cp -Rf parca-agent parca/tmp/parca-agent 73 | ``` 74 | 75 | Then depending on whether you would like to test changes to Parca Agent or Parca, you can run `make dev/up` in Parca Agent or follow [the server's `CONTRIBUTING.md`](https://github.com/parca-dev/parca/blob/main/CONTRIBUTING.md#prerequisites) to get your development Kubernetes cluster running with Tilt. 76 | 77 | Test your changes by running: 78 | 79 | ```console 80 | $ cd parca-agent && make test 81 | ``` 82 | 83 | 88 | 89 | # Making a PR 90 | 91 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. If you are not entirely sure about this, you can discuss this on the [Parca Discord](https://discord.gg/ZgUpYgpzXy) server as well. RFCs are used to document all things architecture and design for the Parca project. You can find an index of the RFCs [here](https://docs.google.com/document/d/171XgH4l_gxvGnETVKQBddo75jQz5aTSDOqO0EZ7LLqE/edit?usp=share_link). 92 | 93 | Please make sure to update tests as appropriate. 94 | 95 | This is roughly what the contribution workflow should look like: 96 | 97 | - Create a topic branch from where you want to base your work (usually main). 98 | - Make commits of logical units. 99 | - Make sure the tests pass, and add any new tests as appropriate. 100 | - Use `make test` and `make test-e2e` to run unit tests and smoke tests respectively. 101 | - Make sure the code is properly formatted. (`make format` could be useful here.) 102 | - Make sure the code is properly linted. (`make lint` could be useful here.) 103 | - Make sure your commit messages follow the commit guidelines (see below). 104 | - Push your changes to a topic branch in your fork of the repository. 105 | - Submit a pull request to the original repository. 106 | 107 | Thank you for your contributions! 108 | 109 | 110 | # Commit Guidelines 111 | 112 | We follow a rough convention for commit messages that is designed to answer two 113 | questions: what changed and why. The subject line should feature the what and 114 | the body of the commit should describe the why. 115 | 116 | 117 | ``` 118 | 119 | scripts: add the test-cluster command 120 | 121 | this uses tmux to setup a test cluster that you can easily kill and 122 | start for debugging. 123 | 124 | Fixes #38 125 | 126 | ``` 127 | 128 | The first line is the subject and should be no longer than 70 characters, the second line is always blank, and other lines should be wrapped at 80 characters. This allows the message to be easier to read on GitHub as well as in various git tools. 129 | 130 | # pre-commit 131 | 132 | [pre-commit](https://pre-commit.com) hooks can installed to help with the linting and formatting of your code: 133 | 134 | ``` 135 | pre-commit install 136 | ``` 137 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cgr.dev/chainguard/static:latest 2 | USER root 3 | 4 | COPY parca-agent /parca-agent 5 | 6 | ENTRYPOINT ["/parca-agent"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Document that refers to a specific project within the Parca ecosystem 2 | is maintained by the maintainers of the respective project. For example, refer 3 | to the maintainers specified in Parca server's 4 | [MAINTAINERS.md](https://github.com/parca/parca/blob/main/MAINTAINERS.md) 5 | file for the Parca server. 6 | 7 | Note that the documentation for the Parca Agent is located in the [parca.dev website](https://www.parca.dev/docs/overview). 8 | 9 | Refer to the following maintainers with their focus areas: 10 | 11 | * Frederic Branczyk @brancz: Everything (eBPF, profiler, debuginfo, symbolisation) 12 | * Kemal Akkoyun @kakkoyun: Everything (eBPF, profiler, debuginfo, CI/CD) 13 | * Sumera Priyadarsini @Sylfrena: Everything (eBPF, profiler, CI/CD) 14 | * Tommy Reilly @gnurizen: Everything (eBPF, profiler, debuginfo, CI/CD) 15 | * Brennan Vincent @umanwizard: Everything (eBPF, profiler, debuginfo) 16 | 17 | ### Emeritus Maintainers 18 | 19 | * Javier Honduvilla Coto @javierhonduco: Everything (eBPF, profiler, symbolization, ci/cd) 20 | 21 | * Vaishali Thakkar @v-thakkar: Everything (eBPF, profiler, ci/cd) 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all crossbuild build build-debug snap 2 | 3 | all: crossbuild 4 | 5 | crossbuild: 6 | DOCKER_CLI_EXPERIMENTAL="enabled" docker run \ 7 | --rm \ 8 | --privileged \ 9 | -v "/var/run/docker.sock:/var/run/docker.sock" \ 10 | -v "$(PWD):/__w/parca-agent/parca-agent" \ 11 | -v "$(GOPATH)/pkg/mod":/go/pkg/mod \ 12 | -w "/__w/parca-agent/parca-agent" \ 13 | docker.io/goreleaser/goreleaser-cross:v1.22.4 \ 14 | release --snapshot --clean --skip=publish --verbose 15 | 16 | build: 17 | go build -o parca-agent -buildvcs=false -ldflags="-extldflags=-static" -tags osusergo,netgo,debugtracer 18 | 19 | build-debug: 20 | go build -o parca-agent-debug -buildvcs=false -ldflags="-extldflags=-static" -tags osusergo,netgo,debugtracer -gcflags "all=-N -l" 21 | 22 | snap: crossbuild 23 | cp ./dist/metadata.json snap/local/metadata.json 24 | 25 | cp ./dist/linux-amd64_linux_amd64_v1/parca-agent snap/local/parca-agent 26 | snapcraft pack --verbose --build-for amd64 27 | 28 | cp ./dist/linux-arm64_linux_arm64/parca-agent snap/local/parca-agent 29 | snapcraft pack --verbose --build-for arm64 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Apache 2 License](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE) 2 | ![Build](https://github.com/parca-dev/parca-agent/actions/workflows/build.yml/badge.svg) 3 | ![Container](https://github.com/parca-dev/parca-agent/actions/workflows/container.yml/badge.svg) 4 | [![parca-agent](https://snapcraft.io/parca-agent/badge.svg)](https://snapcraft.io/parca-agent) 5 | 6 | # Parca Agent 7 | 8 | Parca Agent is an always-on sampling profiler that uses eBPF to capture raw profiling data with very low overhead. It observes user-space and kernel-space stacktraces [19 times per second](https://www.parca.dev/docs/parca-agent-design#cpu-sampling-frequency) and builds [pprof](https://github.com/google/pprof) formatted profiles from the extracted data. Read more details in the [design documentation](https://www.parca.dev/docs/parca-agent-design). 9 | 10 | The collected data can be viewed locally via HTTP endpoints and then be configured to be sent to a [Parca](https://github.com/parca-dev/parca) server to be queried and analyzed over time. 11 | 12 | ## Requirements 13 | 14 | - Linux Kernel version 5.3+ with BTF 15 | 16 | ## Quickstart 17 | 18 | See the [Kubernetes Getting Started](https://www.parca.dev/docs/kubernetes). 19 | 20 | ## Language Support 21 | 22 | Parca Agent is continuously enhancing its support for multiple languages. 23 | Incomplete list of languages we currently support: 24 | 25 | - C, C++, Go (with extended support), Rust 26 | - .NET, Deno, Erlang, Java, Julia, Node.js, Wasmtime, PHP 8 and above 27 | - Ruby, Python 28 | 29 | Please check [our docs](https://www.parca.dev/docs/parca-agent-language-support) for further information. 30 | 31 | > [!NOTE] 32 | > [Further language support](https://github.com/parca-dev/parca-agent/issues?q=is%3Aissue+is%3Aopen+label%3Afeature%2Flanguage-support) is coming in the upcoming weeks and months. 33 | 34 | ## Supported Profiles 35 | 36 | Types of profiles that are available: 37 | 38 | - On-CPU 39 | - Soon: Network usage, Allocations 40 | 41 | > [!NOTE] 42 | > Please check [our docs](https://www.parca.dev/docs/parca-agent-language-support) if your language is supported. 43 | 44 | The following types of profiles require explicit instrumentation: 45 | 46 | - Runtime specific information such as Goroutines 47 | 48 | ## Debugging 49 | 50 | ### Logging 51 | 52 | To debug potential errors, enable debug logging using `--log-level=debug`. 53 | 54 | ## Configuration 55 | 56 |
Flags: 57 |

58 | 59 | [embedmd]:# (dist/help.txt) 60 | ```txt 61 | Usage: parca-agent 62 | 63 | Flags: 64 | -h, --help Show context-sensitive help. 65 | --log-level="info" Log level. 66 | --log-format="logfmt" Configure if structured logging as JSON or as 67 | logfmt 68 | --http-address="127.0.0.1:7071" 69 | Address to bind HTTP server to. 70 | --version Show application version. 71 | --node="hostname" The name of the node that the process is 72 | running on. If on Kubernetes, this must match 73 | the Kubernetes node name. 74 | --config-path="" Path to config file. 75 | --memlock-rlimit=0 The value for the maximum number of bytes 76 | of memory that may be locked into RAM. It is 77 | used to ensure the agent can lock memory for 78 | eBPF maps. 0 means no limit. 79 | --mutex-profile-fraction=0 80 | Fraction of mutex profile samples to collect. 81 | --block-profile-rate=0 Sample rate for block profile. 82 | --profiling-duration=10s The agent profiling duration to use. Leave 83 | this empty to use the defaults. 84 | --profiling-cpu-sampling-frequency=19 85 | The frequency at which profiling data is 86 | collected, e.g., 19 samples per second. 87 | --profiling-perf-event-buffer-poll-interval=250ms 88 | The interval at which the perf event buffer 89 | is polled for new events. 90 | --profiling-perf-event-buffer-processing-interval=100ms 91 | The interval at which the perf event buffer 92 | is processed. 93 | --profiling-perf-event-buffer-worker-count=4 94 | The number of workers that process the perf 95 | event buffer. 96 | --metadata-external-labels=KEY=VALUE;... 97 | Label(s) to attach to all profiles. 98 | --metadata-container-runtime-socket-path=STRING 99 | The filesystem path to the container runtimes 100 | socket. Leave this empty to use the defaults. 101 | --metadata-disable-caching 102 | Disable caching of metadata. 103 | --local-store-directory=STRING 104 | The local directory to store the profiling 105 | data. 106 | --remote-store-address=STRING 107 | gRPC address to send profiles and symbols to. 108 | --remote-store-bearer-token=STRING 109 | Bearer token to authenticate with store 110 | ($PARCA_BEARER_TOKEN). 111 | --remote-store-bearer-token-file=STRING 112 | File to read bearer token from to 113 | authenticate with store. 114 | --remote-store-insecure Send gRPC requests via plaintext instead of 115 | TLS. 116 | --remote-store-insecure-skip-verify 117 | Skip TLS certificate verification. 118 | --remote-store-batch-write-interval=10s 119 | Interval between batch remote client writes. 120 | Leave this empty to use the default value of 121 | 10s. 122 | --remote-store-rpc-logging-enable 123 | Enable gRPC logging. 124 | --remote-store-rpc-unary-timeout=5m 125 | Maximum timeout window for unary gRPC 126 | requests including retries. 127 | --debuginfo-directories=/usr/lib/debug,... 128 | Ordered list of local directories to search 129 | for debuginfo files. 130 | --debuginfo-temp-dir="/tmp" 131 | The local directory path to store the interim 132 | debuginfo files. 133 | --debuginfo-strip Only upload information needed for 134 | symbolization. If false the exact binary the 135 | agent sees will be uploaded unmodified. 136 | --debuginfo-compress Compress debuginfo files' DWARF sections 137 | before uploading. 138 | --debuginfo-upload-disable 139 | Disable debuginfo collection and upload. 140 | --debuginfo-upload-max-parallel=25 141 | The maximum number of debuginfo upload 142 | requests to make in parallel. 143 | --debuginfo-upload-timeout-duration=2m 144 | The timeout duration to cancel upload 145 | requests. 146 | --debuginfo-upload-cache-duration=5m 147 | The duration to cache debuginfo upload 148 | responses for. 149 | --debuginfo-disable-caching 150 | Disable caching of debuginfo. 151 | --symbolizer-jit-disable Disable JIT symbolization. 152 | --otlp-address=STRING The endpoint to send OTLP traces to. 153 | --otlp-exporter="grpc" The OTLP exporter to use. 154 | --object-file-pool-eviction-policy="lru" 155 | The eviction policy to use for the object 156 | file pool. 157 | --object-file-pool-size=100 158 | The maximum number of object files to keep in 159 | the pool. This is used to avoid re-reading 160 | object files from disk. It keeps FDs open, 161 | so it should be kept in sync with ulimits. 162 | 0 means no limit. 163 | --dwarf-unwinding-disable Do not unwind using .eh_frame information. 164 | --dwarf-unwinding-mixed Unwind using .eh_frame information and frame 165 | pointers. 166 | --python-unwinding-disable 167 | Disable Python unwinder. 168 | --ruby-unwinding-disable Disable Ruby unwinder. 169 | --analytics-opt-out Opt out of sending anonymous usage 170 | statistics. 171 | --telemetry-disable-panic-reporting 172 | 173 | --telemetry-stderr-buffer-size-kb=4096 174 | 175 | --bpf-verbose-logging Enable verbose BPF logging. 176 | --bpf-events-buffer-size=8192 177 | Size in pages of the events buffer. 178 | --verbose-bpf-logging [deprecated] Use --bpf-verbose-logging. 179 | Enable verbose BPF logging. 180 | ``` 181 | 182 |

183 |
184 | 185 | ## Metadata Labels 186 | 187 | Parca Agent supports [Prometheus relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config). The following labels are always attached to profiles: 188 | 189 | * `node`: The name of the node that the process is running on as specified by the `--node` flag. 190 | * `comm`: The command name of the process being profiled. 191 | 192 | And optionally you can attach additional labels using the `--metadata-external-labels` flag. 193 | 194 | Using relabeling the following labels can be attached to profiles: 195 | 196 | * `__meta_process_pid`: The process ID of the process being profiled. 197 | * `__meta_process_cmdline`: The command line arguments of the process being profiled. 198 | * `__meta_env_var_*`: The value of the environment variable `*` of the process being profiled. Note that the environment variables to pick up must be specified by `--include-env-var` flag. 199 | * `__meta_process_cgroup`: The (main) cgroup of the process being profiled. 200 | * `__meta_process_ppid`: The parent process ID of the process being profiled. 201 | * `__meta_process_executable_file_id`: The file ID (a hash) of the executable of the process being profiled. 202 | * `__meta_process_executable_name`: The basename of the executable of the process being profiled. 203 | * `__meta_process_executable_build_id`: The build ID of the executable of the process being profiled. 204 | * `__meta_process_executable_compiler`: The compiler used to build the executable of the process being profiled. 205 | * `__meta_process_executable_static`: Whether the executable of the process being profiled is statically linked. 206 | * `__meta_process_executable_stripped`: Whether the executable of the process being profiled is stripped from debuginfo. 207 | * `__meta_system_kernel_release`: The kernel release of the system. 208 | * `__meta_system_kernel_machine`: The kernel machine of the system (typically the architecture). 209 | * `__meta_thread_comm`: The command name of the thread being profiled. 210 | * `__meta_thread_id`: The PID of the thread being profiled. 211 | * `__meta_agent_revision`: The revision of the agent. 212 | * `__meta_kubernetes_namespace`: The namespace of the pod the process is running in. 213 | * `__meta_kubernetes_pod_name`: The name of the pod the process is running in. 214 | * `__meta_kubernetes_pod_label_*`: The value of the label `*` of the pod the process is running in. 215 | * `__meta_kubernetes_pod_labelpresent_*`: Whether the label `*` of the pod the process is running in is present. 216 | * `__meta_kubernetes_pod_annotation_*`: The value of the annotation `*` of the pod the process is running in. 217 | * `__meta_kubernetes_pod_annotationpresent_*`: Whether the annotation `*` of the pod the process is running in is present. 218 | * `__meta_kubernetes_pod_ip`: The IP of the pod the process is running in. 219 | * `__meta_kubernetes_pod_container_name`: The name of the container the process is running in. 220 | * `__meta_kubernetes_pod_container_id`: The ID of the container the process is running in. 221 | * `__meta_kubernetes_pod_container_image`: The image of the container the process is running in. 222 | * `__meta_kubernetes_pod_container_init`: Whether the container the process is running in is an init container. 223 | * `__meta_kubernetes_pod_ready`: Whether the pod the process is running in is ready. 224 | * `__meta_kubernetes_pod_phase`: The phase of the pod the process is running in. 225 | * `__meta_kubernetes_node_name`: The name of the node the process is running on. 226 | * `__meta_kubernetes_pod_host_ip`: The host IP of the pod the process is running in. 227 | * `__meta_kubernetes_pod_uid`: The UID of the pod the process is running in. 228 | * `__meta_kubernetes_pod_controller_kind`: The kind of the controller of the pod the process is running in. 229 | * `__meta_kubernetes_pod_controller_name`: The name of the controller of the pod the process is running in. 230 | * `__meta_kubernetes_node_label_*`: The value of the label `*` of the node the process is running on. 231 | * `__meta_kubernetes_node_labelpresent_*`: Whether the label `*` of the node the process is running on is present. 232 | * `__meta_kubernetes_node_annotation_*`: The value of the annotation `*` of the node the process is running on. 233 | * `__meta_kubernetes_node_annotationpresent_*`: Whether the annotation `*` of the node the process is running on is present. 234 | * `__meta_docker_container_id`: The ID of the container the process is running in. 235 | * `__meta_docker_container_name`: The name of the container the process is running in. 236 | * `__meta_docker_build_kit_container_id`: The ID of the container the process is running in. 237 | * `__meta_containerd_container_id`: The ID of the container the process is running in. 238 | * `__meta_containerd_container_name`: The name of the container the process is running in. 239 | * `__meta_containerd_pod_name`: The name of the pod the process is running in. 240 | * `__meta_lxc_container_id`: The ID of the container the process is running in. 241 | * `__meta_cpu`: The CPU the sample was taken on. 242 | 243 | ## Security 244 | 245 | Parca Agent is required to be running as `root` user (or `CAP_SYS_ADMIN`). Various security precautions have been taken to protect users running Parca Agent. See details in [Security Considerations](https://www.parca.dev/docs/parca-agent-security). 246 | 247 | To report a security vulnerability, see [this guide](https://www.parca.dev/docs/parca-agent-security#report-security-vulnerabilities). 248 | 249 | ## Contributing 250 | 251 | Check out our [Contributing Guide](CONTRIBUTING.md) to get started! 252 | 253 | ## License 254 | 255 | User-space code: Apache 2 256 | 257 | Kernel-space code (eBPF profilers): GNU General Public License, version 2 258 | 259 | ## Credits 260 | 261 | Thanks to: 262 | 263 | - Kinvolk for creating [Inspektor Gadget](https://github.com/kinvolk/inspektor-gadget); some parts of this project were inspired by parts of it. 264 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | This page describes the release process and the currently planned schedule for upcoming releases. 4 | 5 | ## Release schedule 6 | 7 | To be discussed. Currently, there is no specific schedule or cadence for releases. 8 | 9 | # How to cut a new release 10 | 11 | > This guide is strongly based on the [Prometheus release instructions](https://github.com/prometheus/prometheus/blob/main/RELEASE.md). 12 | 13 | ## Branch management and versioning strategy 14 | 15 | We use [Semantic Versioning](http://semver.org/). 16 | 17 | We maintain a separate branch for each minor release, named `release-.`, e.g. `release-1.1`, `release-2.0`. 18 | 19 | The usual flow is to merge new features and changes into the main branch and to merge bug fixes into the latest release branch. Bug fixes are then merged into main from the latest release branch. The main branch should always contain all commits from the latest release branch. 20 | 21 | If a bug fix got accidentally merged into main, cherry-pick commits have to be created in the latest release branch, which then have to be merged back into main. Try to avoid that situation. 22 | 23 | Maintaining the release branches for older minor releases happens on a best effort basis. 24 | 25 | ## Prepare your release 26 | 27 | The process formally starts with the initial pre-release, but some preparations should be done a few days in advance. 28 | 29 | > For a new major or minor release, work from the `main` branch. For a patch release, work in the branch of the minor release you want to patch (e.g. `release-0.3` if you're releasing `v0.3.2`). 30 | 31 | We aim to keep the main branch in a working state as much as possible. In principle, it should be possible to cut a release from main at any time. In practice, things might not work out as nicely. A few days before the release is scheduled, the releaser should check the state of main. Following their best judgement, the releaser should try to expedite bug fixes that are still in progress but should make it into the release. On the other hand, the releaser may hold back merging last-minute invasive and risky changes that are better suited for the next minor release. 32 | 33 | ### Publish a new pre-release 34 | 35 | Suppose the releaser thinks the latest changes in the main could create unpredictable behavior on the platform besides the tested ones, or the release needs further testing. In that case, they could choose to cut a release candidate. 36 | 37 | - The first pre-release (using the suffix `-rc.0`) and creates a new branch called `release-.` starting at the commit tagged for the pre-release. In general, a pre-release is considered a release candidate (that's what `rc` stands for) and should therefore not contain any known bugs that are planned to be fixed in the final release. 38 | - With the pre-release, the releaser is responsible for running and monitoring a benchmark run of the pre-release for 1 day (https://demo.parca.dev should be used), after which, if successful, the pre-release is promoted to a stable release. 39 | - If regressions or critical bugs are detected, they need to get fixed before cutting a new pre-release (called `-rc.1`, `-rc.2`, etc.). 40 | 41 | ### Publish a new release 42 | 43 | For new minor and major releases, create the `release-.` branch starting at the PR merge commit. 44 | 45 | From now on, all work happens on the `release-.` branch. 46 | 47 | ### Via GitHub's UI 48 | 49 | Go to https://github.com/parca-dev/parca-agent/releases/new and click on "Choose a tag" where you can type the new tag name. 50 | 51 | Click on "Create new tag" in the dropdown and make sure `main` is selected for a new major or minor release or the `release-.` branch for a patch release. 52 | 53 | The title of the release is the tag itself. 54 | 55 | You can generate the changelog and then add additional contents from previous a release (like social media links and more). 56 | 57 | ### Via CLI 58 | 59 | Alternatively, you can do the tagging on the commandline: 60 | 61 | Tag the new release with a tag named `v..`, e.g. `v2.1.3`. Note the `v` prefix. 62 | 63 | ```bash 64 | git tag -s "v2.1.3" -m "v2.1.3" 65 | git push origin "v2.1.3" 66 | ``` 67 | 68 | Signed tag with a GPG key is appreciated, but in case you can't add a GPG key to your Github account using the following [procedure](https://help.github.com/articles/generating-a-gpg-key/), you can replace the `-s` flag by `-a` flag of the `git tag` command to only annotate the tag without signing. 69 | 70 | ## Final steps 71 | 72 | Our CI pipeline will automatically push the container images to [ghcr.io](ghcr.io/parca-dev/parca-agent). 73 | 74 | Go to https://github.com/parca-dev/parca-dev/releases and check the created release. 75 | 76 | For patch releases, submit a pull request to merge back the release branch into the `main` branch. 77 | 78 | Take a breath. You're done releasing. 79 | 80 | ## Generating Snapcraft tokens 81 | 82 | The pipeline is configured to release Snap packages automatically to 83 | [snapcraft.io](https://snapcraft.io). 84 | 85 | The token for the store has a limited life. It can be regenerated like so (replace date 86 | placeholders!): 87 | 88 | ```shell 89 | snapcraft export-login \ 90 | --snaps=parca-agent \ 91 | --acls package_access,package_push,package_update,package_release \ 92 | --expires "YYYY-MM-DD" \ 93 | /tmp/parca_agent_snap_token 94 | ``` 95 | 96 | The contents of `/tmp/parca_agent_snap_token` should then be added to a Github Secret called `SNAPCRAFT_STORE_CREDENTIALS`. 97 | -------------------------------------------------------------------------------- /analytics/analytics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package analytics 15 | 16 | import ( 17 | "context" 18 | "math/rand" 19 | "strconv" 20 | "time" 21 | 22 | prometheus "buf.build/gen/go/prometheus/prometheus/protocolbuffers/go" 23 | log "github.com/sirupsen/logrus" 24 | "github.com/zcalusic/sysinfo" 25 | ) 26 | 27 | type AnalyticsSender struct { 28 | client *Client 29 | 30 | machineID string 31 | arch string 32 | cpuCores float64 33 | version string 34 | si sysinfo.SysInfo 35 | isContainer string 36 | } 37 | 38 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 39 | 40 | func randSeq(n int) string { 41 | //nolint:gosec 42 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 43 | b := make([]rune, n) 44 | for i := range b { 45 | b[i] = letters[r.Intn(len(letters))] 46 | } 47 | return string(b) 48 | } 49 | 50 | func NewSender( 51 | client *Client, 52 | arch string, 53 | cpuCores int, 54 | version string, 55 | si sysinfo.SysInfo, 56 | isContainer bool, 57 | ) *AnalyticsSender { 58 | return &AnalyticsSender{ 59 | client: client, 60 | machineID: randSeq(20), 61 | arch: arch, 62 | cpuCores: float64(cpuCores), 63 | version: version, 64 | si: si, 65 | isContainer: strconv.FormatBool(isContainer), 66 | } 67 | } 68 | 69 | func (s *AnalyticsSender) Run(ctx context.Context) { 70 | ticker := time.NewTicker(time.Second * 10) 71 | defer ticker.Stop() 72 | 73 | for { 74 | select { 75 | case <-ctx.Done(): 76 | return 77 | case <-ticker.C: 78 | now := FromTime(time.Now()) 79 | if err := s.client.Send(ctx, &prometheus.WriteRequest{ 80 | Timeseries: []*prometheus.TimeSeries{{ 81 | Labels: []*prometheus.Label{{ 82 | Name: "__name__", 83 | Value: "parca_agent_info", 84 | }, { 85 | Name: "machine_id", 86 | Value: s.machineID, 87 | }, { 88 | Name: "arch", 89 | Value: s.arch, 90 | }, { 91 | Name: "version", 92 | Value: s.version, 93 | }, { 94 | Name: "kernel_version", 95 | Value: s.si.Kernel.Version, 96 | }, { 97 | Name: "kernel_osrelease", 98 | Value: s.si.Kernel.Release, 99 | }, { 100 | Name: "os_name", 101 | Value: s.si.OS.Name, 102 | }, { 103 | Name: "os_vendor", 104 | Value: s.si.OS.Vendor, 105 | }, { 106 | Name: "os_version", 107 | Value: s.si.OS.Version, 108 | }, { 109 | Name: "os_release", 110 | Value: s.si.OS.Release, 111 | }, { 112 | Name: "is_container", 113 | Value: s.isContainer, 114 | }}, 115 | Samples: []*prometheus.Sample{{ 116 | Value: 1, 117 | Timestamp: now, 118 | }}, 119 | }, { 120 | Labels: []*prometheus.Label{{ 121 | Name: "__name__", 122 | Value: "parca_agent_cpu_cores", 123 | }, { 124 | Name: "machine_id", 125 | Value: s.machineID, 126 | }}, 127 | Samples: []*prometheus.Sample{{ 128 | Value: s.cpuCores, 129 | Timestamp: now, 130 | }}, 131 | }}, 132 | }); err != nil { 133 | log.Debugf("failed to send analytics: %v", err) 134 | } 135 | } 136 | } 137 | } 138 | 139 | func FromTime(t time.Time) int64 { 140 | return t.Unix()*1000 + int64(t.Nanosecond())/int64(time.Millisecond) 141 | } 142 | -------------------------------------------------------------------------------- /analytics/remote_write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package analytics 15 | 16 | import ( 17 | "bufio" 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | "net/http/httptrace" 24 | "os" 25 | "time" 26 | 27 | prometheus "buf.build/gen/go/prometheus/prometheus/protocolbuffers/go" 28 | "github.com/gogo/protobuf/proto" 29 | "github.com/golang/snappy" 30 | "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" 31 | "go.opentelemetry.io/otel/trace" 32 | ) 33 | 34 | const ( 35 | maxErrMsgLen = 1024 36 | defaultURL = "https://analytics.parca.dev/api/v1/write" 37 | ) 38 | 39 | type Client struct { 40 | tp trace.TracerProvider 41 | tracer trace.Tracer 42 | 43 | client *http.Client 44 | urlString string 45 | userAgent string 46 | timeout time.Duration 47 | 48 | buf []byte 49 | pBuf *proto.Buffer 50 | } 51 | 52 | func NewClient( 53 | tp trace.TracerProvider, 54 | client *http.Client, 55 | userAgent string, 56 | timeout time.Duration, 57 | ) *Client { 58 | analyticsURL := defaultURL 59 | customAnalyticsURL := os.Getenv("ANALYTICS_URL") 60 | if customAnalyticsURL != "" { 61 | analyticsURL = customAnalyticsURL 62 | } 63 | 64 | return &Client{ 65 | tp: tp, 66 | tracer: tp.Tracer("parca/analytics"), 67 | 68 | client: client, 69 | 70 | urlString: analyticsURL, 71 | userAgent: userAgent, 72 | timeout: timeout, 73 | 74 | pBuf: proto.NewBuffer(nil), 75 | buf: make([]byte, 1024), 76 | } 77 | } 78 | 79 | func (c *Client) Send(ctx context.Context, wreq *prometheus.WriteRequest) error { 80 | ctx, span := c.tracer.Start(ctx, "Send", trace.WithSpanKind(trace.SpanKindClient)) 81 | defer span.End() 82 | 83 | c.pBuf.Reset() 84 | err := c.pBuf.Marshal(wreq) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // snappy uses len() to see if it needs to allocate a new slice. Make the 90 | // buffer as long as possible. 91 | if c.buf != nil { 92 | c.buf = c.buf[0:cap(c.buf)] 93 | } 94 | c.buf = snappy.Encode(c.buf, c.pBuf.Bytes()) 95 | 96 | return c.sendReq(ctx, c.buf) 97 | } 98 | 99 | // Store sends a batch of samples to the HTTP endpoint, the request is the proto marshaled 100 | // and encoded bytes from codec.go. 101 | func (c *Client) sendReq(ctx context.Context, req []byte) error { 102 | ctx, span := c.tracer.Start(ctx, "sendReq", trace.WithSpanKind(trace.SpanKindClient)) 103 | defer span.End() 104 | 105 | httpReq, err := http.NewRequest(http.MethodPost, c.urlString, bytes.NewReader(req)) 106 | if err != nil { 107 | // Errors from NewRequest are from unparsable URLs, so are not 108 | // recoverable. 109 | return err 110 | } 111 | 112 | httpReq.Header.Add("Content-Encoding", "snappy") 113 | httpReq.Header.Set("Content-Type", "application/x-protobuf") 114 | httpReq.Header.Set("User-Agent", c.userAgent) 115 | httpReq.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0") 116 | 117 | ctx, cancel := context.WithTimeout(ctx, c.timeout) 118 | defer cancel() 119 | ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithTracerProvider(c.tp))) 120 | 121 | httpResp, err := c.client.Do(httpReq.WithContext(ctx)) 122 | if err != nil { 123 | return fmt.Errorf("error sending request: %w", err) 124 | } 125 | defer func() { 126 | io.Copy(io.Discard, httpResp.Body) //nolint:errcheck 127 | httpResp.Body.Close() 128 | }() 129 | 130 | if httpResp.StatusCode/100 != 2 { 131 | scanner := bufio.NewScanner(io.LimitReader(httpResp.Body, maxErrMsgLen)) 132 | line := "" 133 | if scanner.Scan() { 134 | line = scanner.Text() 135 | } 136 | err = fmt.Errorf("server returned HTTP status %s: %s", httpResp.Status, line) 137 | } 138 | return err 139 | } 140 | -------------------------------------------------------------------------------- /bump-fork.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | go mod edit -replace go.opentelemetry.io/ebpf-profiler=github.com/parca-dev/opentelemetry-ebpf-profiler@latest 3 | go mod tidy 4 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | relabel_configs: 2 | - source_labels: [__meta_process_executable_name] 3 | target_label: exec 4 | action: replace 5 | - source_labels: [__meta_process_executable_compiler] 6 | target_label: compiler 7 | action: replace 8 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package config 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "os" 20 | 21 | "github.com/prometheus/prometheus/model/relabel" 22 | "gopkg.in/yaml.v3" 23 | ) 24 | 25 | var ErrEmptyConfig = errors.New("empty config") 26 | 27 | // Config holds all the configuration information for Parca Agent. 28 | type Config struct { 29 | RelabelConfigs []*relabel.Config `yaml:"relabel_configs,omitempty"` 30 | } 31 | 32 | func (c Config) String() string { 33 | b, err := yaml.Marshal(c) 34 | if err != nil { 35 | return fmt.Sprintf("", err) 36 | } 37 | return string(b) 38 | } 39 | 40 | // Load parses the YAML input s into a Config. 41 | func Load(b []byte) (*Config, error) { 42 | if len(b) == 0 { 43 | return nil, ErrEmptyConfig 44 | } 45 | 46 | // TODO: Sanitize the input labels if it becomes a problem for the user. 47 | // https://github.com/prometheus/prometheus/blob/1bfb3ed062e99bd3c74e05d9ff9a7fa4e30bbe21/util/strutil/strconv.go#L51 48 | cfg := &Config{} 49 | 50 | if err := yaml.Unmarshal(b, cfg); err != nil { 51 | return nil, fmt.Errorf("unmarshaling YAML: %w", err) 52 | } 53 | 54 | return cfg, nil 55 | } 56 | 57 | // LoadFile parses the given YAML file into a Config. 58 | func LoadFile(filename string) (*Config, error) { 59 | content, err := os.ReadFile(filename) 60 | if err != nil { 61 | return nil, err 62 | } 63 | cfg, err := Load(content) 64 | if err != nil { 65 | return nil, fmt.Errorf("parsing YAML file %s: %w", filename, err) 66 | } 67 | return cfg, nil 68 | } 69 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package config 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/prometheus/common/model" 20 | "github.com/prometheus/prometheus/model/relabel" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | func TestLoad(t *testing.T) { 25 | t.Parallel() 26 | tests := []struct { 27 | name string 28 | input string 29 | want *Config 30 | wantErr bool 31 | }{ 32 | { 33 | input: ``, 34 | want: nil, 35 | wantErr: true, 36 | }, 37 | { 38 | input: `# comment`, 39 | want: &Config{ 40 | RelabelConfigs: nil, 41 | }, 42 | }, 43 | { 44 | input: `relabel_configs: []`, 45 | want: &Config{ 46 | RelabelConfigs: []*relabel.Config{}, 47 | }, 48 | }, 49 | { 50 | input: `relabel_configs: 51 | - source_labels: [systemd_unit] 52 | regex: "" 53 | action: drop 54 | `, 55 | want: &Config{ 56 | RelabelConfigs: []*relabel.Config{ 57 | { 58 | SourceLabels: model.LabelNames{"systemd_unit"}, 59 | Separator: ";", 60 | Regex: relabel.MustNewRegexp(``), 61 | Replacement: "$1", 62 | Action: relabel.Drop, 63 | }, 64 | }, 65 | }, 66 | }, 67 | { 68 | input: `relabel_configs: 69 | - source_labels: [app] 70 | regex: coolaicompany-isolate-controller 71 | action: keep 72 | `, 73 | want: &Config{ 74 | RelabelConfigs: []*relabel.Config{ 75 | { 76 | SourceLabels: model.LabelNames{"app"}, 77 | Separator: ";", 78 | Regex: relabel.MustNewRegexp("coolaicompany-isolate-controller"), 79 | Replacement: "$1", 80 | Action: relabel.Keep, 81 | }, 82 | }, 83 | }, 84 | }, 85 | { 86 | input: `"relabel_configs": 87 | - "action": "keep" 88 | "regex": "parca-agent" 89 | "source_labels": 90 | - "app_kubernetes_io_name" 91 | `, 92 | want: &Config{ 93 | RelabelConfigs: []*relabel.Config{ 94 | { 95 | SourceLabels: model.LabelNames{"app_kubernetes_io_name"}, 96 | Separator: ";", 97 | Regex: relabel.MustNewRegexp("parca-agent"), 98 | Replacement: "$1", 99 | Action: relabel.Keep, 100 | }, 101 | }, 102 | }, 103 | }, 104 | { 105 | input: `relabel_configs: 106 | - action: keep 107 | regex: parca-agent 108 | source_labels: 109 | - app_kubernetes_io_name 110 | `, 111 | want: &Config{ 112 | RelabelConfigs: []*relabel.Config{ 113 | { 114 | SourceLabels: model.LabelNames{"app_kubernetes_io_name"}, 115 | Separator: ";", 116 | Regex: relabel.MustNewRegexp("parca-agent"), 117 | Replacement: "$1", 118 | Action: relabel.Keep, 119 | }, 120 | }, 121 | }, 122 | }, 123 | { 124 | input: `relabel_configs: 125 | - action: keep 126 | regex: parca-agent 127 | source_labels: 128 | - app.kubernetes.io/name 129 | `, 130 | want: &Config{ 131 | RelabelConfigs: []*relabel.Config{ 132 | { 133 | SourceLabels: model.LabelNames{"app.kubernetes.io/name"}, 134 | Separator: ";", 135 | Regex: relabel.MustNewRegexp("parca-agent"), 136 | Replacement: "$1", 137 | Action: relabel.Keep, 138 | }, 139 | }, 140 | }, 141 | }, 142 | } 143 | for _, tt := range tests { 144 | tt := tt 145 | t.Run(tt.name, func(t *testing.T) { 146 | t.Parallel() 147 | got, err := Load([]byte(tt.input)) 148 | if tt.wantErr { 149 | require.Error(t, err) 150 | return 151 | } 152 | require.NoError(t, err) 153 | require.Equal(t, tt.want, got) 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | manifests 3 | tilt 4 | jsonnet.lock.json 5 | manifests.tar.gz 6 | kubernetes-manifest.yaml 7 | openshift-manifest.yaml 8 | -------------------------------------------------------------------------------- /deploy/Makefile: -------------------------------------------------------------------------------- 1 | JSONNET_FMT := jsonnetfmt -n 2 --max-blank-lines 2 --string-style s --comment-style s 2 | VERSION ?= $(shell git describe --exact-match --tags $$(git log -n1 --pretty='%h') 2>/dev/null || echo "$$(git rev-parse --abbrev-ref HEAD)-$$(git rev-parse --short HEAD)") 3 | SERVER_VERSION ?= $(shell curl -s https://api.github.com/repos/parca-dev/parca/releases/latest | grep -oE '"tag_name":(.*)' | grep -o 'v[0-9.]*' | xargs echo -n) 4 | 5 | .PHONY: vendor 6 | vendor: 7 | jb install 8 | 9 | .PHONY: manifests 10 | manifests: vendor $(shell find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print) 11 | rm -rf manifests tilt 12 | mkdir -p manifests/openshift manifests/kubernetes tilt 13 | export VERSION=$(VERSION) SERVER_VERSION=$(SERVER_VERSION) && ./generate.sh 14 | 15 | fmt: 16 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 17 | xargs -n 1 -- $(JSONNET_FMT) -i 18 | -------------------------------------------------------------------------------- /deploy/dev.jsonnet: -------------------------------------------------------------------------------- 1 | function(serverVersion='v0.4.2') 2 | local ns = { 3 | apiVersion: 'v1', 4 | kind: 'Namespace', 5 | metadata: { 6 | name: 'parca', 7 | labels: { 8 | 'pod-security.kubernetes.io/enforce': 'privileged', 9 | }, 10 | }, 11 | }; 12 | 13 | local server = (import 'parca/parca.libsonnet')({ 14 | name: 'parca', 15 | namespace: ns.metadata.name, 16 | image: 'ghcr.io/parca-dev/parca:' + serverVersion, 17 | version: serverVersion, 18 | replicas: 1, 19 | corsAllowedOrigins: '*', 20 | serviceMonitor: true, 21 | debugInfodUpstreamServers: ['https://debuginfod.systemtap.org'], 22 | }); 23 | 24 | local agent = (import 'parca-agent/parca-agent.libsonnet')({ 25 | name: 'parca-agent', 26 | namespace: ns.metadata.name, 27 | version: 'dev', 28 | image: 'localhost:5000/parca-agent:dev', 29 | stores: ['%s.%s.svc.cluster.local:%d' % [server.service.metadata.name, server.service.metadata.namespace, server.config.port]], 30 | logLevel: 'debug', 31 | insecure: true, 32 | insecureSkipVerify: true, 33 | profilingCPUSamplingFrequency: 97, // Better it to be a prime number. 34 | podMonitor: true, 35 | debuginfoUploadTimeout: '2m', 36 | // podSecurityPolicy: true, 37 | // config: { 38 | // relabel_configs: [ 39 | // { 40 | // source_labels: ['pid'], 41 | // regex: '.*', 42 | // action: 'keep', 43 | // }, 44 | // ], 45 | // }, 46 | // debuginfoUploadDisable: true, 47 | // containerRuntimeSocketPath: '/run/docker.sock', 48 | }); 49 | 50 | { 51 | '0namespace': ns, 52 | } + { 53 | ['parca-server-' + name]: server[name] 54 | for name in std.objectFields(server) 55 | if server[name] != null 56 | } + { 57 | ['parca-agent-' + name]: agent[name] 58 | for name in std.objectFields(agent) 59 | if agent[name] != null 60 | } 61 | -------------------------------------------------------------------------------- /deploy/e2e.jsonnet: -------------------------------------------------------------------------------- 1 | function(version='v0.0.1-alpha.3', serverVersion='v0.0.3-alpha.2') 2 | local ns = { 3 | apiVersion: 'v1', 4 | kind: 'Namespace', 5 | metadata: { 6 | name: 'parca', 7 | labels: { 8 | 'pod-security.kubernetes.io/enforce': 'privileged', 9 | 'pod-security.kubernetes.io/audit': 'privileged', 10 | 'pod-security.kubernetes.io/warn': 'privileged', 11 | }, 12 | }, 13 | }; 14 | 15 | local server = (import 'parca/parca.libsonnet')({ 16 | name: 'parca', 17 | namespace: ns.metadata.name, 18 | image: 'ghcr.io/parca-dev/parca:' + self.version, 19 | version: serverVersion, 20 | replicas: 1, 21 | corsAllowedOrigins: '*', 22 | debugInfodUpstreamServers: ['https://debuginfod.systemtap.org'], 23 | }); 24 | 25 | local agent = (import 'parca-agent/parca-agent.libsonnet')({ 26 | name: 'parca-agent', 27 | namespace: ns.metadata.name, 28 | version: version, 29 | image: 'ghcr.io/parca-dev/parca-agent:' + self.version, 30 | // This assumes there's a running parca in the cluster. 31 | stores: ['parca.parca.svc.cluster.local:7070'], 32 | insecure: true, 33 | insecureSkipVerify: true, 34 | // token: "", 35 | // stores: [ 36 | // 'grpc.polarsignals.com:443', 37 | // ], 38 | tempDir: '/tmp', 39 | // Available Options: 40 | // samplingRatio: 0.5, 41 | // Docs for usage of Label Selector 42 | // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 43 | // podLabelSelector: 'app=my-web-app,version=v1', 44 | }); 45 | 46 | 47 | { 48 | kind: 'List', 49 | apiVersion: 'v1', 50 | items: 51 | [ 52 | ns, 53 | ] + [ 54 | server[name] 55 | for name in std.objectFields(server) 56 | if server[name] != null 57 | ] + [ 58 | agent[name] 59 | for name in std.objectFields(agent) 60 | if agent[name] != null 61 | ], 62 | } 63 | -------------------------------------------------------------------------------- /deploy/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | jsonnet --tla-str version="${VERSION}" -J vendor main.jsonnet -m manifests/kubernetes | xargs -I{} sh -c 'cat {} | gojsontoyaml > {}.yaml; rm -f {}' -- {} 5 | for f in manifests/kubernetes/*; do cat ${f} >> manifests/kubernetes-manifest.yaml; echo '---' >> manifests/kubernetes-manifest.yaml; done 6 | jsonnet --tla-str version="${VERSION}" -J vendor openshift.jsonnet -m manifests/openshift | xargs -I{} sh -c 'cat {} | gojsontoyaml > {}.yaml; rm -f {}' -- {} 7 | for f in manifests/openshift/*; do cat ${f} >> manifests/openshift-manifest.yaml; echo '---' >> manifests/openshift-manifest.yaml; done 8 | jsonnet --tla-str serverVersion="${SERVER_VERSION}" -J vendor dev.jsonnet -m tilt | xargs -I{} sh -c 'cat {} | gojsontoyaml > {}.yaml; rm -f {}' -- {} 9 | -------------------------------------------------------------------------------- /deploy/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/parca-dev/parca.git", 8 | "subdir": "deploy/lib/parca" 9 | } 10 | }, 11 | "version": "main" 12 | }, 13 | { 14 | "source": { 15 | "local": { 16 | "directory": "lib/parca-agent" 17 | } 18 | }, 19 | "version": "" 20 | } 21 | ], 22 | "legacyImports": true 23 | } 24 | -------------------------------------------------------------------------------- /deploy/jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/parca-dev/parca.git", 8 | "subdir": "deploy/lib/parca" 9 | } 10 | }, 11 | "version": "ba668d046e2eea3514961fac022c19b21ff16412", 12 | "sum": "3jHwGjkrJ39XMNlGNMS+/O3CbW+Dl6CuwH1Xp0sL4M0=" 13 | }, 14 | { 15 | "source": { 16 | "local": { 17 | "directory": "lib/parca-agent" 18 | } 19 | }, 20 | "version": "" 21 | } 22 | ], 23 | "legacyImports": false 24 | } 25 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/.gitignore: -------------------------------------------------------------------------------- 1 | manifests 2 | vendor 3 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/Makefile: -------------------------------------------------------------------------------- 1 | vendor: jb 2 | jb install 3 | 4 | .PHONY: fmt 5 | fmt: jsonnetfmt 6 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 7 | xargs -n 1 -- jsonnetfmt -i 8 | 9 | .PHONY: manifests 10 | manifests: vendor jsonnet 11 | ./build.sh 12 | 13 | .PHONY: deploy 14 | deploy: vendor manifests 15 | ./monitoring-deploy.sh 16 | 17 | .PHONY: restore-dashboard 18 | restore-dashboard: 19 | ./restore-grafana-dashboard.sh 20 | 21 | jb: 22 | go install -v github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@latest 23 | 24 | jsonnet: 25 | go install -v github.com/google/go-jsonnet/cmd/jsonnet@latest 26 | 27 | jsonnetfmt: 28 | go install -v github.com/google/go-jsonnet/cmd/jsonnetfmt@latest 29 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2023-2024 The Parca Authors 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This script uses arg $1 (name of *.jsonnet file to use) to generate the manifests/*.yaml files. 17 | 18 | set -e 19 | set -x 20 | # only exit with zero if all commands of the pipeline exit successfully 21 | set -o pipefail 22 | 23 | # Make sure to use project tooling 24 | PATH="$(pwd)/tmp/bin:${PATH}" 25 | 26 | # Make sure to start with a clean 'manifests' dir 27 | rm -rf manifests 28 | mkdir -p manifests/setup 29 | 30 | # Calling gojsontoyaml is optional, but we would like to generate yaml, not json 31 | jsonnet -J vendor -m manifests "${1-parca.jsonnet}" | xargs -I{} sh -c 'cat {} | gojsontoyaml > {}.yaml' -- {} 32 | 33 | # Make sure to remove json files 34 | find manifests -type f ! -name '*.yaml' -delete 35 | rm -f kustomization 36 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/prometheus-operator/kube-prometheus.git", 8 | "subdir": "jsonnet/kube-prometheus" 9 | } 10 | }, 11 | "version": "main" 12 | } 13 | ], 14 | "legacyImports": true 15 | } 16 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/brancz/kubernetes-grafana.git", 8 | "subdir": "grafana" 9 | } 10 | }, 11 | "version": "5698c8940b6dadca3f42107b7839557bc041761f", 12 | "sum": "l6fPvh3tW6fWot308w71QY/amrYsFPeitvz1IgJxqQA=" 13 | }, 14 | { 15 | "source": { 16 | "git": { 17 | "remote": "https://github.com/etcd-io/etcd.git", 18 | "subdir": "contrib/mixin" 19 | } 20 | }, 21 | "version": "d6c0127d264d18255090b4a51e04c5b8e84d5a05", 22 | "sum": "IXI3LQIT9NmTPJAk8WLUJd5+qZfcGpeNCyWIK7oEpws=" 23 | }, 24 | { 25 | "source": { 26 | "git": { 27 | "remote": "https://github.com/grafana/grafana.git", 28 | "subdir": "grafana-mixin" 29 | } 30 | }, 31 | "version": "1120f9e255760a3c104b57871fcb91801e934382", 32 | "sum": "MkjR7zCgq6MUZgjDzop574tFKoTX2OBr7DTwm1K+Ofs=" 33 | }, 34 | { 35 | "source": { 36 | "git": { 37 | "remote": "https://github.com/grafana/grafonnet-lib.git", 38 | "subdir": "grafonnet" 39 | } 40 | }, 41 | "version": "a1d61cce1da59c71409b99b5c7568511fec661ea", 42 | "sum": "342u++/7rViR/zj2jeJOjshzglkZ1SY+hFNuyCBFMdc=" 43 | }, 44 | { 45 | "source": { 46 | "git": { 47 | "remote": "https://github.com/grafana/grafonnet-lib.git", 48 | "subdir": "grafonnet-7.0" 49 | } 50 | }, 51 | "version": "a1d61cce1da59c71409b99b5c7568511fec661ea", 52 | "sum": "gCtR9s/4D5fxU9aKXg0Bru+/njZhA0YjLjPiASc61FM=" 53 | }, 54 | { 55 | "source": { 56 | "git": { 57 | "remote": "https://github.com/grafana/grafonnet.git", 58 | "subdir": "gen/grafonnet-latest" 59 | } 60 | }, 61 | "version": "5a66b0f6a0f4f7caec754dd39a0e263b56a0f90a", 62 | "sum": "eyuJ0jOXeA4MrobbNgU4/v5a7ASDHslHZ0eS6hDdWoI=" 63 | }, 64 | { 65 | "source": { 66 | "git": { 67 | "remote": "https://github.com/grafana/grafonnet.git", 68 | "subdir": "gen/grafonnet-v10.0.0" 69 | } 70 | }, 71 | "version": "5a66b0f6a0f4f7caec754dd39a0e263b56a0f90a", 72 | "sum": "xdcrJPJlpkq4+5LpGwN4tPAuheNNLXZjE6tDcyvFjr0=" 73 | }, 74 | { 75 | "source": { 76 | "git": { 77 | "remote": "https://github.com/grafana/grafonnet.git", 78 | "subdir": "gen/grafonnet-v11.0.0" 79 | } 80 | }, 81 | "version": "5a66b0f6a0f4f7caec754dd39a0e263b56a0f90a", 82 | "sum": "Fuo+qTZZzF+sHDBWX/8fkPsUmwW6qhH8hRVz45HznfI=" 83 | }, 84 | { 85 | "source": { 86 | "git": { 87 | "remote": "https://github.com/grafana/jsonnet-libs.git", 88 | "subdir": "grafana-builder" 89 | } 90 | }, 91 | "version": "02db06f540086fa3f67d487bd01e1b314853fb8f", 92 | "sum": "B49EzIY2WZsFxNMJcgRxE/gcZ9ltnS8pkOOV6Q5qioc=" 93 | }, 94 | { 95 | "source": { 96 | "git": { 97 | "remote": "https://github.com/jsonnet-libs/docsonnet.git", 98 | "subdir": "doc-util" 99 | } 100 | }, 101 | "version": "6ac6c69685b8c29c54515448eaca583da2d88150", 102 | "sum": "BrAL/k23jq+xy9oA7TWIhUx07dsA/QLm3g7ktCwe//U=" 103 | }, 104 | { 105 | "source": { 106 | "git": { 107 | "remote": "https://github.com/jsonnet-libs/xtd.git", 108 | "subdir": "" 109 | } 110 | }, 111 | "version": "63d430b69a95741061c2f7fc9d84b1a778511d9c", 112 | "sum": "qiZi3axUSXCVzKUF83zSAxklwrnitMmrDK4XAfjPMdE=" 113 | }, 114 | { 115 | "source": { 116 | "git": { 117 | "remote": "https://github.com/kubernetes-monitoring/kubernetes-mixin.git", 118 | "subdir": "" 119 | } 120 | }, 121 | "version": "3dfa72d1d1ab31a686b1f52ec28bbf77c972bd23", 122 | "sum": "7ufhpvzoDqAYLrfAsGkTAIRmu2yWQkmHukTE//jOsJU=" 123 | }, 124 | { 125 | "source": { 126 | "git": { 127 | "remote": "https://github.com/kubernetes/kube-state-metrics.git", 128 | "subdir": "jsonnet/kube-state-metrics" 129 | } 130 | }, 131 | "version": "a3e9266c8fa7dbe19fb3f1ee95517d3316c6ee58", 132 | "sum": "pvInhJNQVDOcC3NGWRMKRIP954mAvLXCQpTlafIg7fA=" 133 | }, 134 | { 135 | "source": { 136 | "git": { 137 | "remote": "https://github.com/kubernetes/kube-state-metrics.git", 138 | "subdir": "jsonnet/kube-state-metrics-mixin" 139 | } 140 | }, 141 | "version": "a3e9266c8fa7dbe19fb3f1ee95517d3316c6ee58", 142 | "sum": "qclI7LwucTjBef3PkGBkKxF0mfZPbHnn4rlNWKGtR4c=" 143 | }, 144 | { 145 | "source": { 146 | "git": { 147 | "remote": "https://github.com/prometheus-operator/kube-prometheus.git", 148 | "subdir": "jsonnet/kube-prometheus" 149 | } 150 | }, 151 | "version": "defa2bd1e242519c62a5c2b3b786b1caa6d906d4", 152 | "sum": "INKeZ+QIIPImq+TrfHT8CpYdoRzzxRk0txG07XlOo/Q=" 153 | }, 154 | { 155 | "source": { 156 | "git": { 157 | "remote": "https://github.com/prometheus-operator/prometheus-operator.git", 158 | "subdir": "jsonnet/mixin" 159 | } 160 | }, 161 | "version": "609424db53853b992277b7a9a0e5cf59f4cc24f3", 162 | "sum": "gi+knjdxs2T715iIQIntrimbHRgHnpM8IFBJDD1gYfs=", 163 | "name": "prometheus-operator-mixin" 164 | }, 165 | { 166 | "source": { 167 | "git": { 168 | "remote": "https://github.com/prometheus-operator/prometheus-operator.git", 169 | "subdir": "jsonnet/prometheus-operator" 170 | } 171 | }, 172 | "version": "609424db53853b992277b7a9a0e5cf59f4cc24f3", 173 | "sum": "z2/5LjQpWC7snhT+n/mtQqoy5986uI95sTqcKQziwGU=" 174 | }, 175 | { 176 | "source": { 177 | "git": { 178 | "remote": "https://github.com/prometheus/alertmanager.git", 179 | "subdir": "doc/alertmanager-mixin" 180 | } 181 | }, 182 | "version": "eb8369ec510d76f63901379a8437c4b55885d6c5", 183 | "sum": "IpF46ZXsm+0wJJAPtAre8+yxTNZA57mBqGpBP/r7/kw=", 184 | "name": "alertmanager" 185 | }, 186 | { 187 | "source": { 188 | "git": { 189 | "remote": "https://github.com/prometheus/node_exporter.git", 190 | "subdir": "docs/node-mixin" 191 | } 192 | }, 193 | "version": "b9d0932179a0c5b3a8863f3d6cdafe8584cedc8e", 194 | "sum": "rhUvbqviGjQ2mwsRhHKMN0TiS3YvnYpUXHew3XlQ+Wg=" 195 | }, 196 | { 197 | "source": { 198 | "git": { 199 | "remote": "https://github.com/prometheus/prometheus.git", 200 | "subdir": "documentation/prometheus-mixin" 201 | } 202 | }, 203 | "version": "1fa9ba838a3b07ba441cda82c3e282ffa866aba6", 204 | "sum": "dYLcLzGH4yF3qB7OGC/7z4nqeTNjv42L7Q3BENU8XJI=", 205 | "name": "prometheus" 206 | }, 207 | { 208 | "source": { 209 | "git": { 210 | "remote": "https://github.com/pyrra-dev/pyrra.git", 211 | "subdir": "config/crd/bases" 212 | } 213 | }, 214 | "version": "551856d42dff02ec38c5b0ea6a2d99c4cb127e82", 215 | "sum": "bY/Pcrrbynguq8/HaI88cQ3B2hLv/xc+76QILY7IL+g=", 216 | "name": "pyrra" 217 | }, 218 | { 219 | "source": { 220 | "git": { 221 | "remote": "https://github.com/thanos-io/thanos.git", 222 | "subdir": "mixin" 223 | } 224 | }, 225 | "version": "5765d3c1c97eb38d326fcf677c370c1b52eb6e22", 226 | "sum": "HhSSbGGCNHCMy1ee5jElYDm0yS9Vesa7QB2/SHKdjsY=", 227 | "name": "thanos-mixin" 228 | } 229 | ], 230 | "legacyImports": false 231 | } 232 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/monitoring-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2023-2024 The Parca Authors 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set -euo pipefail 16 | 17 | PARENT_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 18 | cd "${PARENT_DIR}" 19 | 20 | kubectl apply --server-side -f ./manifests/setup 21 | until kubectl get servicemonitors --all-namespaces; do 22 | date 23 | sleep 1 24 | echo 25 | done 26 | 27 | kubectl apply -f ./manifests/ 28 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/parca.jsonnet: -------------------------------------------------------------------------------- 1 | local kp = 2 | (import 'kube-prometheus/main.libsonnet') + 3 | (import 'kube-prometheus/addons/all-namespaces.libsonnet') + { 4 | values+:: { 5 | common+: { 6 | namespace: 'monitoring', 7 | }, 8 | prometheus+: { 9 | enableFeatures: ['native-histograms'], 10 | }, 11 | }, 12 | }; 13 | 14 | { 'setup/0namespace-namespace': kp.kubePrometheus.namespace } + 15 | { 16 | ['setup/prometheus-operator-' + name]: kp.prometheusOperator[name] 17 | for name in std.filter((function(name) name != 'serviceMonitor' && name != 'prometheusRule'), std.objectFields(kp.prometheusOperator)) 18 | } + 19 | // serviceMonitor and prometheusRule are separated so that they can be created after the CRDs are ready 20 | { 'prometheus-operator-serviceMonitor': kp.prometheusOperator.serviceMonitor } + 21 | { 'prometheus-operator-prometheusRule': kp.prometheusOperator.prometheusRule } + 22 | { 'kube-prometheus-prometheusRule': kp.kubePrometheus.prometheusRule } + 23 | { ['alertmanager-' + name]: kp.alertmanager[name] for name in std.objectFields(kp.alertmanager) } + 24 | { ['blackbox-exporter-' + name]: kp.blackboxExporter[name] for name in std.objectFields(kp.blackboxExporter) } + 25 | { ['grafana-' + name]: kp.grafana[name] for name in std.objectFields(kp.grafana) } + 26 | { ['kube-state-metrics-' + name]: kp.kubeStateMetrics[name] for name in std.objectFields(kp.kubeStateMetrics) } + 27 | { ['kubernetes-' + name]: kp.kubernetesControlPlane[name] for name in std.objectFields(kp.kubernetesControlPlane) } 28 | { ['node-exporter-' + name]: kp.nodeExporter[name] for name in std.objectFields(kp.nodeExporter) } + 29 | { ['prometheus-' + name]: kp.prometheus[name] for name in std.objectFields(kp.prometheus) } + 30 | { ['prometheus-adapter-' + name]: kp.prometheusAdapter[name] for name in std.objectFields(kp.prometheusAdapter) } 31 | -------------------------------------------------------------------------------- /deploy/kube-prometheus/restore-grafana-dashboard.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2023-2024 The Parca Authors 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set -euo pipefail 16 | 17 | # Make sure the exported dashboard JSON wrapped with "{"dashboard": {...}}" and dashboard ID is set to null. 18 | curl -X POST --insecure -H "Content-Type: application/json" -d @grafana-dashboard-api-export.json http://admin:admin@localhost:3000/api/dashboards/import 19 | -------------------------------------------------------------------------------- /deploy/lib/parca-agent/parca-agent.libsonnet: -------------------------------------------------------------------------------- 1 | // These are the defaults for this components configuration. 2 | // When calling the function to generate the component's manifest, 3 | // you can pass an object structured like the default to overwrite default values. 4 | local defaults = { 5 | local defaults = self, 6 | name: 'parca-agent', 7 | namespace: error 'must provide namespace', 8 | version: error 'must provide version', 9 | image: error 'must provide image', 10 | stores: ['dnssrv+_grpc._tcp.parca'], 11 | 12 | resources: {}, 13 | port: 7071, 14 | 15 | config: { 16 | relabel_configs: [{ 17 | source_labels: ['__meta_process_executable_compiler'], 18 | target_label: 'compiler', 19 | }, { 20 | source_labels: ['__meta_system_kernel_machine'], 21 | target_label: 'arch', 22 | }, { 23 | source_labels: ['__meta_system_kernel_release'], 24 | target_label: 'kernel_version', 25 | }, { 26 | source_labels: ['__meta_kubernetes_namespace'], 27 | target_label: 'namespace', 28 | }, { 29 | source_labels: ['__meta_kubernetes_pod_name'], 30 | target_label: 'pod', 31 | }, { 32 | source_labels: ['__meta_kubernetes_pod_container_name'], 33 | target_label: 'container', 34 | }, { 35 | source_labels: ['__meta_kubernetes_pod_container_image'], 36 | target_label: 'container_image', 37 | }, { 38 | source_labels: ['__meta_kubernetes_node_label_topology_kubernetes_io_region'], 39 | target_label: 'region', 40 | }, { 41 | source_labels: ['__meta_kubernetes_node_label_topology_kubernetes_io_zone'], 42 | target_label: 'zone', 43 | }, { 44 | action: 'labelmap', 45 | regex: '__meta_kubernetes_pod_label_(.+)', 46 | replacement: '${1}', 47 | }, { 48 | action: 'labeldrop', 49 | regex: 'apps_kubernetes_io_pod_index|controller_revision_hash|statefulset_kubernetes_io_pod_name|pod_template_hash', 50 | 51 | }], 52 | }, 53 | logLevel: 'info', 54 | socketPath: '', 55 | 56 | profilingDuration: '', 57 | profilingCPUSamplingFrequency: '', 58 | 59 | token: '', 60 | insecure: false, 61 | insecureSkipVerify: false, 62 | 63 | metadataDisableCaching: false, 64 | 65 | debuginfoUploadDisable: false, 66 | debuginfoStrip: true, 67 | 68 | hostDbusSystem: true, 69 | hostDbusSystemSocket: '/var/run/dbus/system_bus_socket', 70 | 71 | commonLabels:: { 72 | 'app.kubernetes.io/name': 'parca-agent', 73 | 'app.kubernetes.io/instance': defaults.name, 74 | 'app.kubernetes.io/version': defaults.version, 75 | 'app.kubernetes.io/component': 'observability', 76 | }, 77 | 78 | externalLabels:: {}, 79 | 80 | // Container level security context. 81 | securityContext: { 82 | privileged: true, 83 | allowPrivilegeEscalation: true, 84 | capabilities: { 85 | add: ['SYS_ADMIN'], // 'BPF', 'PERFMON' 86 | }, 87 | }, 88 | 89 | podMonitor: false, 90 | podSecurityPolicy: false, 91 | }; 92 | 93 | function(params) { 94 | local pa = self, 95 | 96 | // Combine the defaults and the passed params to make the component's config. 97 | config:: defaults + params, 98 | // Safety checks for combined config of defaults and params 99 | assert std.isObject(pa.config.resources), 100 | assert std.isBoolean(pa.config.podMonitor), 101 | 102 | metadata:: { 103 | name: pa.config.name, 104 | namespace: pa.config.namespace, 105 | labels: pa.config.commonLabels, 106 | }, 107 | 108 | serviceAccount: { 109 | apiVersion: 'v1', 110 | kind: 'ServiceAccount', 111 | metadata: pa.metadata, 112 | }, 113 | 114 | clusterRoleBinding: { 115 | apiVersion: 'rbac.authorization.k8s.io/v1', 116 | kind: 'ClusterRoleBinding', 117 | metadata: pa.metadata, 118 | subjects: [{ 119 | kind: 'ServiceAccount', 120 | name: pa.config.name, 121 | namespace: pa.config.namespace, 122 | }], 123 | roleRef: { 124 | kind: 'ClusterRole', 125 | name: pa.config.name, 126 | apiGroup: 'rbac.authorization.k8s.io', 127 | }, 128 | }, 129 | 130 | clusterRole: { 131 | apiVersion: 'rbac.authorization.k8s.io/v1', 132 | kind: 'ClusterRole', 133 | metadata: pa.metadata, 134 | rules: [ 135 | { 136 | apiGroups: [''], 137 | resources: ['pods'], 138 | verbs: ['list', 'watch'], 139 | }, 140 | { 141 | apiGroups: [''], 142 | resources: ['nodes'], 143 | verbs: ['get'], 144 | }, 145 | ], 146 | }, 147 | 148 | [if std.length((defaults + params).config) > 0 then 'configMap']: { 149 | apiVersion: 'v1', 150 | kind: 'ConfigMap', 151 | metadata: pa.metadata, 152 | data: { 153 | 'parca-agent.yaml': std.manifestYamlDoc(pa.config.config), 154 | }, 155 | }, 156 | 157 | daemonSet: 158 | local c = { 159 | name: 'parca-agent', 160 | image: pa.config.image, 161 | args: [ 162 | // http-address optionally specifies the TCP address for the server to listen on, in the form "host:port". 163 | '--http-address=' + ':' + pa.config.port, 164 | '--node=$(NODE_NAME)', 165 | ] + ( 166 | if (std.length(pa.config.config) > 0) then [ 167 | '--config-path=/etc/parca-agent/parca-agent.yaml', 168 | ] else [] 169 | ) + ( 170 | if pa.config.logLevel != 'info' then [ 171 | '--log-level=' + pa.config.logLevel, 172 | ] else [] 173 | ) + ( 174 | if pa.config.profilingDuration != '' then [ 175 | '--profiling-duration=%s' % pa.config.profilingDuration, 176 | ] else [] 177 | ) + ( 178 | if pa.config.profilingCPUSamplingFrequency != '' then [ 179 | '--profiling-cpu-sampling-frequency=%s' % pa.config.profilingCPUSamplingFrequency, 180 | ] else [] 181 | ) + ( 182 | if pa.config.token != '' then [ 183 | '--remote-store-bearer-token=%s' % pa.config.token, 184 | ] else [] 185 | ) + [ 186 | '--remote-store-address=%s' % store 187 | for store in pa.config.stores 188 | ] + ( 189 | if pa.config.insecure then [ 190 | '--remote-store-insecure', 191 | ] else [] 192 | ) + ( 193 | if pa.config.insecureSkipVerify then [ 194 | '--remote-store-insecure-skip-verify', 195 | ] else [] 196 | ) + ( 197 | if pa.config.debuginfoUploadDisable then [ 198 | '--debuginfo-upload-disable', 199 | ] else [] 200 | ) + ( 201 | if !pa.config.debuginfoStrip then [ 202 | '--debuginfo-strip=false', 203 | ] else [] 204 | ) + ( 205 | if pa.config.socketPath != '' then [ 206 | '--container-runtime-socket-path=' + pa.config.socketPath, 207 | ] else [] 208 | ) + ( 209 | if std.length(pa.config.externalLabels) > 0 then [ 210 | '--metadata-external-label=%s=%s' % [labelName, pa.config.externalLabels[labelName]] 211 | for labelName in std.objectFields(pa.config.externalLabels) 212 | ] else [] 213 | ) + ( 214 | if pa.config.metadataDisableCaching then [ 215 | '--metadata-disable-caching', 216 | ] else [] 217 | ), 218 | // Container level security context. 219 | securityContext: pa.config.securityContext, 220 | ports: [ 221 | { 222 | name: 'http', 223 | containerPort: pa.config.port, 224 | }, 225 | ], 226 | volumeMounts: [ 227 | { 228 | name: 'tmp', 229 | mountPath: '/tmp', 230 | }, 231 | { 232 | name: 'run', 233 | mountPath: '/run', 234 | }, 235 | { 236 | name: 'boot', 237 | mountPath: '/boot', 238 | readOnly: true, 239 | }, 240 | { 241 | name: 'modules', 242 | mountPath: '/lib/modules', 243 | }, 244 | { 245 | name: 'debugfs', 246 | mountPath: '/sys/kernel/debug', 247 | }, 248 | { 249 | name: 'cgroup', 250 | mountPath: '/sys/fs/cgroup', 251 | }, 252 | { 253 | name: 'bpffs', 254 | mountPath: '/sys/fs/bpf', 255 | }, 256 | ] + ( 257 | if std.length(pa.config.config) > 0 then [{ 258 | name: 'config', 259 | mountPath: '/etc/parca-agent', 260 | }] else [] 261 | ) + ( 262 | if pa.config.hostDbusSystem then [{ 263 | name: 'dbus-system', 264 | mountPath: '/var/run/dbus/system_bus_socket', 265 | }] else [] 266 | ), 267 | env: [ 268 | { 269 | name: 'NODE_NAME', 270 | valueFrom: { 271 | fieldRef: { 272 | fieldPath: 'spec.nodeName', 273 | }, 274 | }, 275 | }, 276 | ], 277 | resources: if pa.config.resources != {} then pa.config.resources else {}, 278 | }; 279 | 280 | { 281 | apiVersion: 'apps/v1', 282 | kind: 'DaemonSet', 283 | metadata: pa.metadata, 284 | spec: { 285 | selector: { 286 | matchLabels: { 287 | [labelName]: pa.config.commonLabels[labelName] 288 | for labelName in std.objectFields(pa.config.commonLabels) 289 | if labelName != 'app.kubernetes.io/version' 290 | }, 291 | }, 292 | template: { 293 | metadata: { 294 | labels: pa.config.commonLabels, 295 | }, 296 | spec: { 297 | containers: [c], 298 | hostPID: true, 299 | serviceAccountName: pa.serviceAccount.metadata.name, 300 | // Pod level security context. 301 | securityContext: { 302 | seccompProfile: { 303 | type: 'RuntimeDefault', 304 | }, 305 | }, 306 | nodeSelector: { 307 | 'kubernetes.io/os': 'linux', 308 | }, 309 | tolerations: [ 310 | { 311 | operator: 'Exists', 312 | }, 313 | ], 314 | volumes: [ 315 | { 316 | name: 'tmp', 317 | emptyDir: {}, 318 | }, 319 | // Needed for reading the container runtime metadata. 320 | { 321 | name: 'run', 322 | hostPath: { 323 | path: '/run', 324 | }, 325 | }, 326 | // Needed for reading kernel configuration. 327 | { 328 | name: 'boot', 329 | hostPath: { 330 | path: '/boot', 331 | }, 332 | }, 333 | // Deprecated by v0.10.0 release. Remove in a couple of releases. 334 | { 335 | name: 'cgroup', 336 | hostPath: { 337 | path: '/sys/fs/cgroup', 338 | }, 339 | }, 340 | { 341 | name: 'modules', 342 | hostPath: { 343 | path: '/lib/modules', 344 | }, 345 | }, 346 | // Needed for reading the pinned eBPF maps and programs. 347 | { 348 | name: 'bpffs', 349 | hostPath: { 350 | path: '/sys/fs/bpf', 351 | }, 352 | }, 353 | // Needed for writing logs from eBPF programs. 354 | { 355 | name: 'debugfs', 356 | hostPath: { 357 | path: '/sys/kernel/debug', 358 | }, 359 | }, 360 | ] + ( 361 | if std.length(pa.config.config) > 0 then [{ 362 | name: 'config', 363 | configMap: { name: pa.configMap.metadata.name }, 364 | }] else [] 365 | ) + ( 366 | if pa.config.hostDbusSystem then [{ 367 | name: 'dbus-system', 368 | hostPath: { 369 | path: pa.config.hostDbusSystemSocket, 370 | }, 371 | }] else [] 372 | ), 373 | }, 374 | }, 375 | }, 376 | }, 377 | 378 | [if std.objectHas(params, 'podMonitor') && params.podMonitor then 'podMonitor']: { 379 | apiVersion: 'monitoring.coreos.com/v1', 380 | kind: 'PodMonitor', 381 | metadata: { 382 | name: pa.config.name, 383 | namespace: pa.config.namespace, 384 | labels: pa.config.commonLabels, 385 | }, 386 | spec: { 387 | podMetricsEndpoints: [{ 388 | port: pa.daemonSet.spec.template.spec.containers[0].ports[0].name, 389 | }], 390 | selector: { 391 | matchLabels: pa.daemonSet.spec.template.metadata.labels, 392 | }, 393 | }, 394 | }, 395 | 396 | [if std.objectHas(params, 'podSecurityPolicy') && params.podSecurityPolicy then 'podSecurityPolicy']: { 397 | apiVersion: 'policy/v1', 398 | kind: 'PodSecurityPolicy', 399 | metadata: pa.metadata, 400 | spec: { 401 | allowPrivilegeEscalation: true, 402 | allowedCapabilities: ['*'], 403 | fsGroup: { 404 | rule: 'RunAsAny', 405 | }, 406 | runAsUser: { 407 | rule: 'RunAsAny', 408 | }, 409 | seLinux: { 410 | rule: 'RunAsAny', 411 | }, 412 | supplementalGroups: { 413 | rule: 'RunAsAny', 414 | }, 415 | privileged: true, 416 | hostIPC: true, 417 | hostNetwork: true, 418 | hostPID: true, 419 | readOnlyRootFilesystem: true, 420 | volumes: [ 421 | 'configMap', 422 | 'emptyDir', 423 | 'projected', 424 | 'secret', 425 | 'downwardAPI', 426 | 'persistentVolumeClaim', 427 | 'hostPath', 428 | ], 429 | allowedHostPaths+: [ 430 | { 431 | pathPrefix: '/sys', 432 | }, 433 | { 434 | pathPrefix: '/boot', 435 | }, 436 | { 437 | pathPrefix: '/var/run/dbus', 438 | }, 439 | { 440 | pathPrefix: '/run', 441 | }, 442 | { 443 | pathPrefix: '/lib/modules', 444 | }, 445 | ], 446 | }, 447 | }, 448 | 449 | [if std.objectHas(params, 'podSecurityPolicy') && params.podSecurityPolicy then 'role']: { 450 | apiVersion: 'rbac.authorization.k8s.io/v1', 451 | kind: 'Role', 452 | metadata: pa.metadata, 453 | rules: [ 454 | { 455 | apiGroups: [ 456 | 'policy', 457 | ], 458 | resourceNames: [ 459 | pa.config.name, 460 | ], 461 | resources: [ 462 | 'podsecuritypolicies', 463 | ], 464 | verbs: [ 465 | 'use', 466 | ], 467 | }, 468 | ], 469 | }, 470 | 471 | [if std.objectHas(params, 'podSecurityPolicy') && params.podSecurityPolicy then 'roleBinding']: { 472 | apiVersion: 'rbac.authorization.k8s.io/v1', 473 | kind: 'RoleBinding', 474 | metadata: pa.metadata, 475 | roleRef: { 476 | apiGroup: 'rbac.authorization.k8s.io', 477 | kind: 'Role', 478 | name: pa.role.metadata.name, 479 | }, 480 | subjects: [ 481 | { 482 | kind: 'ServiceAccount', 483 | name: pa.serviceAccount.metadata.name, 484 | }, 485 | ], 486 | }, 487 | } 488 | -------------------------------------------------------------------------------- /deploy/main.jsonnet: -------------------------------------------------------------------------------- 1 | function(version='v0.0.1-alpha.3') 2 | local ns = { 3 | apiVersion: 'v1', 4 | kind: 'Namespace', 5 | metadata: { 6 | name: 'parca', 7 | labels: { 8 | 'pod-security.kubernetes.io/enforce': 'privileged', 9 | 'pod-security.kubernetes.io/audit': 'privileged', 10 | 'pod-security.kubernetes.io/warn': 'privileged', 11 | }, 12 | }, 13 | }; 14 | 15 | local agent = (import 'parca-agent/parca-agent.libsonnet')({ 16 | name: 'parca-agent', 17 | namespace: ns.metadata.name, 18 | version: version, 19 | image: 'ghcr.io/parca-dev/parca-agent:' + version, 20 | // This assumes there's a running parca in the cluster. 21 | stores: ['parca.parca.svc.cluster.local:7070'], 22 | insecure: true, 23 | // token: "", 24 | // stores: [ 25 | // 'grpc.polarsignals.com:443', 26 | // ], 27 | // Available Options: 28 | // samplingRatio: 0.5, 29 | // Docs for usage of Label Selector 30 | // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 31 | // podLabelSelector: 'app=my-web-app,version=v1', 32 | }); 33 | 34 | { 35 | '0namespace': ns, 36 | } + { 37 | ['parca-agent-' + name]: agent[name] 38 | for name in std.objectFields(agent) 39 | if agent[name] != null 40 | } 41 | -------------------------------------------------------------------------------- /deploy/openshift.jsonnet: -------------------------------------------------------------------------------- 1 | function(version='v0.0.1-alpha.3') 2 | local ns = { 3 | apiVersion: 'v1', 4 | kind: 'Namespace', 5 | metadata: { 6 | name: 'parca', 7 | labels: { 8 | 'pod-security.kubernetes.io/enforce': 'privileged', 9 | 'pod-security.kubernetes.io/audit': 'privileged', 10 | 'pod-security.kubernetes.io/warn': 'privileged', 11 | }, 12 | }, 13 | }; 14 | 15 | local agent = (import 'parca-agent/parca-agent.libsonnet')({ 16 | name: 'parca-agent', 17 | namespace: ns.metadata.name, 18 | version: version, 19 | image: 'ghcr.io/parca-dev/parca-agent:' + version, 20 | // This assumes there's a running parca in the cluster. 21 | stores: ['parca.parca.svc.cluster.local:7070'], 22 | insecure: true, 23 | insecureSkipVerify: true, 24 | // token: "", 25 | // stores: [ 26 | // 'grpc.polarsignals.com:443', 27 | // ], 28 | tempDir: 'tmp', 29 | securityContext: { 30 | capabilities: { 31 | add: ['SYS_ADMIN'], 32 | }, 33 | privileged: true, 34 | runAsUser: 0, 35 | }, 36 | // Available Options: 37 | // samplingRatio: 0.5, 38 | // podLabelSelector: { 39 | // app: 'my-web-app' 40 | // }, 41 | }); 42 | 43 | local clusterRole = { 44 | apiVersion: 'rbac.authorization.k8s.io/v1', 45 | kind: 'ClusterRole', 46 | metadata: { 47 | name: agent.config.name + '-scc', 48 | }, 49 | rules: [{ 50 | apiGroups: ['security.openshift.io'], 51 | resourceNames: ['privileged'], 52 | resources: ['securitycontextconstraints'], 53 | verbs: ['use'], 54 | }], 55 | }; 56 | 57 | local clusterRoleBinding = { 58 | apiVersion: 'rbac.authorization.k8s.io/v1', 59 | kind: 'ClusterRoleBinding', 60 | metadata: { 61 | name: agent.config.name + '-scc', 62 | namespace: agent.config.namespace, 63 | }, 64 | subjects: [{ 65 | kind: 'ServiceAccount', 66 | name: agent.config.name, 67 | namespace: agent.config.namespace, 68 | }], 69 | roleRef: { 70 | kind: 'ClusterRole', 71 | name: clusterRole.metadata.name, 72 | apiGroup: 'rbac.authorization.k8s.io', 73 | }, 74 | }; 75 | 76 | { 77 | '0namespace': ns, 78 | } + { 79 | ['parca-agent-' + name]: agent[name] 80 | for name in std.objectFields(agent) 81 | if agent[name] != null && name != 'podSecurityPolicy' 82 | } + { 83 | 'parca-agent-openshift-clusterrole': clusterRole, 84 | 'parca-agent-openshift-clusterrolebinding': clusterRoleBinding, 85 | } 86 | -------------------------------------------------------------------------------- /env-jsonnet.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Copyright 2022-2024 The Parca Authors 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | set -euo pipefail 17 | 18 | # renovate: datasource=go depName=github.com/brancz/gojsontoyaml 19 | GOJSONTOYAML_VERSION='v0.1.0' 20 | go install "github.com/brancz/gojsontoyaml@${GOJSONTOYAML_VERSION}" 21 | 22 | # renovate: datasource=go depName=github.com/google/go-jsonnet 23 | JSONNET_VERSION='v0.20.0' 24 | go install "github.com/google/go-jsonnet/cmd/jsonnet@${JSONNET_VERSION}" 25 | go install "github.com/google/go-jsonnet/cmd/jsonnetfmt@${JSONNET_VERSION}" 26 | 27 | # renovate: datasource=go depName=github.com/jsonnet-bundler/jsonnet-bundler 28 | JB_VERSION='v0.5.1' 29 | go install github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@${JB_VERSION} 30 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Copyright 2023-2024 The Parca Authors 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set -euo pipefail 16 | 17 | # renovate: datasource=go depName=github.com/campoy/embedmd 18 | EMBEDMD_VERSION='v2.0.0' 19 | go install "github.com/campoy/embedmd/v2@${EMBEDMD_VERSION}" 20 | 21 | # renovate: datasource=go depName=mvdan.cc/gofumpt 22 | GOFUMPT_VERSION='v0.6.0' 23 | go install "mvdan.cc/gofumpt@${GOFUMPT_VERSION}" 24 | 25 | # renovate: datasource=go depName=github.com/golangci/golangci-lint 26 | GOLANGCI_LINT_VERSION='v1.56.2' 27 | go install "github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}" 28 | 29 | # renovate: datasource=go depName=github.com/florianl/bluebox 30 | BLUEBOX_VERSION='v0.0.2' 31 | go install "github.com/florianl/bluebox@${BLUEBOX_VERSION}" 32 | 33 | # renovate: datasource=go depName=golang.org/x/vuln 34 | GOVULNCHECK_VERSION='v1.1.2' 35 | go install "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" 36 | -------------------------------------------------------------------------------- /flags/codec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Based on: 15 | // https://github.com/planetscale/vtprotobuf#mixing-protobuf-implementations-with-grpc 16 | // https://github.com/vitessio/vitess/blob/main/go/vt/servenv/grpc_codec.go 17 | 18 | package flags 19 | 20 | import ( 21 | "fmt" 22 | 23 | gogoproto "github.com/gogo/protobuf/proto" 24 | "google.golang.org/protobuf/proto" 25 | 26 | _ "google.golang.org/grpc/encoding/proto" 27 | ) 28 | 29 | // Name is the name registered for the proto compressor. 30 | const Name = "proto" 31 | 32 | type vtprotoCodec struct{} 33 | 34 | type vtprotoMessage interface { 35 | MarshalVT() ([]byte, error) 36 | UnmarshalVT(data []byte) error 37 | } 38 | 39 | func (vtprotoCodec) Marshal(v any) ([]byte, error) { 40 | switch v := v.(type) { 41 | case vtprotoMessage: 42 | return v.MarshalVT() 43 | case proto.Message: 44 | return proto.Marshal(v) 45 | case gogoproto.Message: 46 | return gogoproto.Marshal(v) 47 | default: 48 | return nil, fmt.Errorf("failed to marshal, message is %T, must satisfy the vtprotoMessage interface or want proto.Message, gogoproto.Message", v) 49 | } 50 | } 51 | 52 | func (vtprotoCodec) Unmarshal(data []byte, v any) error { 53 | switch v := v.(type) { 54 | case vtprotoMessage: 55 | return v.UnmarshalVT(data) 56 | case proto.Message: 57 | return proto.Unmarshal(data, v) 58 | case gogoproto.Message: 59 | return gogoproto.Unmarshal(data, v) 60 | default: 61 | return fmt.Errorf("failed to unmarshal, message is %T, must satisfy the vtprotoMessage interface or want proto.Message, gogoproto.Message", v) 62 | } 63 | } 64 | 65 | func (vtprotoCodec) Name() string { 66 | return Name 67 | } 68 | -------------------------------------------------------------------------------- /flags/grpc.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" 12 | "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" 13 | "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry" 14 | "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/timeout" 15 | "github.com/prometheus/client_golang/prometheus" 16 | log "github.com/sirupsen/logrus" 17 | tracing "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 18 | "go.opentelemetry.io/ebpf-profiler/libpf" 19 | "go.opentelemetry.io/otel/propagation" 20 | "go.opentelemetry.io/otel/trace" 21 | "google.golang.org/grpc" 22 | "google.golang.org/grpc/credentials" 23 | "google.golang.org/grpc/credentials/insecure" 24 | "google.golang.org/grpc/encoding" 25 | ) 26 | 27 | // WaitGrpcEndpoint waits until the gRPC connection is established. 28 | func (f FlagsRemoteStore) WaitGrpcEndpoint(ctx context.Context, reg prometheus.Registerer, tp trace.TracerProvider) (*grpc.ClientConn, error) { 29 | // Sleep with a fixed backoff time added of +/- 20% jitter 30 | tick := time.NewTicker(libpf.AddJitter(f.GRPCStartupBackoffTime, 0.2)) 31 | defer tick.Stop() 32 | 33 | // metrics 34 | metrics := grpc_prometheus.NewClientMetrics( 35 | grpc_prometheus.WithClientHandlingTimeHistogram( 36 | grpc_prometheus.WithHistogramOpts(&prometheus.HistogramOpts{ 37 | NativeHistogramBucketFactor: 1.1, 38 | Buckets: nil, 39 | }), 40 | ), 41 | ) 42 | reg.MustRegister(metrics) 43 | 44 | var retries uint32 45 | for { 46 | if grpcConn, err := f.setupGrpcConnection(ctx, metrics, tp); err != nil { 47 | if retries >= f.GRPCMaxConnectionRetries { 48 | return nil, err 49 | } 50 | retries++ 51 | 52 | log.Warnf( 53 | "Failed to setup gRPC connection (try %d of %d): %v", 54 | retries, 55 | f.GRPCMaxConnectionRetries, 56 | err, 57 | ) 58 | select { 59 | case <-ctx.Done(): 60 | return nil, ctx.Err() 61 | case <-tick.C: 62 | continue 63 | } 64 | } else { 65 | return grpcConn, nil 66 | } 67 | } 68 | } 69 | 70 | // setupGrpcConnection sets up a gRPC connection instrumented with our auth interceptor 71 | func (f FlagsRemoteStore) setupGrpcConnection(parent context.Context, metrics *grpc_prometheus.ClientMetrics, tp trace.TracerProvider) (*grpc.ClientConn, error) { 72 | encoding.RegisterCodec(vtprotoCodec{}) 73 | 74 | //nolint:staticcheck 75 | opts := []grpc.DialOption{grpc.WithBlock(), 76 | grpc.WithDefaultCallOptions( 77 | grpc.MaxCallRecvMsgSize(f.GRPCMaxCallRecvMsgSize), 78 | grpc.MaxCallSendMsgSize(f.GRPCMaxCallSendMsgSize)), 79 | grpc.WithReturnConnectionError(), 80 | } 81 | 82 | if f.Insecure { 83 | opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) 84 | } else { 85 | opts = append(opts, 86 | grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ 87 | // Support only TLS1.3+ with valid CA certificates 88 | MinVersion: tls.VersionTLS13, 89 | InsecureSkipVerify: f.InsecureSkipVerify, 90 | }))) 91 | } 92 | 93 | // Auth 94 | if f.BearerToken != "" { 95 | opts = append(opts, grpc.WithPerRPCCredentials( 96 | NewPerRequestBearerToken(f.BearerToken, f.Insecure)), 97 | ) 98 | } 99 | 100 | if f.BearerTokenFile != "" { 101 | b, err := os.ReadFile(f.BearerTokenFile) 102 | if err != nil { 103 | panic(fmt.Errorf("failed to read bearer token from file: %w", err)) 104 | } 105 | 106 | opts = append(opts, grpc.WithPerRPCCredentials( 107 | NewPerRequestBearerToken(strings.TrimSpace(string(b)), f.Insecure)), 108 | ) 109 | } 110 | 111 | // tracing 112 | exemplarFromContext := func(ctx context.Context) prometheus.Labels { 113 | if span := trace.SpanContextFromContext(ctx); span.IsSampled() { 114 | return prometheus.Labels{"traceID": span.TraceID().String()} 115 | } 116 | return nil 117 | } 118 | logTraceID := func(ctx context.Context) logging.Fields { 119 | if span := trace.SpanContextFromContext(ctx); span.IsSampled() { 120 | return logging.Fields{"traceID", span.TraceID().String()} 121 | } 122 | return nil 123 | } 124 | propagators := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) 125 | 126 | opts = append(opts, 127 | grpc.WithChainUnaryInterceptor( 128 | timeout.UnaryClientInterceptor(f.RPCUnaryTimeout), // 5m by default. 129 | retry.UnaryClientInterceptor( 130 | // Back-off with Jitter: scalar: 1s, jitterFraction: 0,1, 10 runs 131 | // i: 1 t:969.91774ms total:969.91774ms 132 | // i: 2 t:1.914221005s total:2.884138745s 133 | // i: 3 t:3.788704363s total:6.672843108s 134 | // i: 4 t:8.285062088s total:14.957905196s 135 | // i: 5 t:14.480256611s total:29.438161807s 136 | // i: 6 t:32.586249789s total:1m2.024411596s 137 | // i: 7 t:1m6.755804584s total:2m8.78021618s 138 | // i: 8 t:2m3.116345957s total:4m11.896562137s 139 | // i: 9 t:4m3.895083732s total:8m15.791645869s 140 | // i: 10 t:9m19.350609671s total:17m35.14225554s 141 | retry.WithBackoff(retry.BackoffExponentialWithJitter(time.Second, 0.1)), 142 | retry.WithMax(10), 143 | // The passed in context has a `5m` timeout (see above), the whole invocation should finish within that time. 144 | // However, by default all retried calls will use the parent context for their deadlines. 145 | // This means, that unless you shorten the deadline of each call of the retry, you won't be able to retry the first call at all. 146 | // `WithPerRetryTimeout` allows you to shorten the deadline of each retry call, allowing you to fit multiple retries in the single parent deadline. 147 | retry.WithPerRetryTimeout(2*time.Minute), 148 | ), 149 | metrics.UnaryClientInterceptor( 150 | grpc_prometheus.WithExemplarFromContext(exemplarFromContext), 151 | ), 152 | logging.UnaryClientInterceptor(interceptorLogger(), logging.WithFieldsFromContext(logTraceID)), 153 | ), 154 | grpc.WithChainStreamInterceptor( 155 | metrics.StreamClientInterceptor( 156 | grpc_prometheus.WithExemplarFromContext(exemplarFromContext), 157 | ), 158 | logging.StreamClientInterceptor(interceptorLogger(), logging.WithFieldsFromContext(logTraceID)), 159 | ), 160 | grpc.WithStatsHandler(tracing.NewClientHandler( 161 | tracing.WithTracerProvider(tp), 162 | tracing.WithPropagators(propagators), 163 | )), 164 | ) 165 | 166 | ctx, cancel := context.WithTimeout(parent, f.GRPCConnectionTimeout) 167 | defer cancel() 168 | //nolint:staticcheck 169 | return grpc.DialContext(ctx, f.Address, opts...) 170 | } 171 | 172 | type perRequestBearerToken struct { 173 | token string 174 | insecure bool 175 | } 176 | 177 | func NewPerRequestBearerToken(token string, insecure bool) *perRequestBearerToken { 178 | return &perRequestBearerToken{ 179 | token: token, 180 | insecure: insecure, 181 | } 182 | } 183 | 184 | func (t *perRequestBearerToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { 185 | return map[string]string{ 186 | "authorization": "Bearer " + t.token, 187 | }, nil 188 | } 189 | 190 | func (t *perRequestBearerToken) RequireTransportSecurity() bool { 191 | return !t.insecure 192 | } 193 | 194 | // interceptorLogger adapts go-kit logger to interceptor logger. 195 | func interceptorLogger() logging.Logger { 196 | return logging.LoggerFunc(func(_ context.Context, lvl logging.Level, msg string, fields ...any) { 197 | largs := append([]any{msg}, fields...) 198 | switch lvl { 199 | case logging.LevelDebug: 200 | log.Debug(largs...) 201 | case logging.LevelInfo: 202 | log.Info(largs...) 203 | case logging.LevelWarn: 204 | log.Warn(largs...) 205 | case logging.LevelError: 206 | log.Error(largs...) 207 | default: 208 | panic(fmt.Sprintf("unknown level %v", lvl)) 209 | } 210 | }) 211 | } 212 | -------------------------------------------------------------------------------- /flags/tracer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package flags 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "io" 20 | "os" 21 | "strings" 22 | 23 | "go.opentelemetry.io/otel" 24 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 25 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 26 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 27 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 28 | "go.opentelemetry.io/otel/propagation" 29 | "go.opentelemetry.io/otel/sdk/resource" 30 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 31 | semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 32 | "go.opentelemetry.io/otel/trace" 33 | "go.opentelemetry.io/otel/trace/noop" 34 | ) 35 | 36 | type ( 37 | ExporterType string 38 | SamplerType string 39 | ) 40 | 41 | const ( 42 | ExporterTypeGRPC ExporterType = "grpc" 43 | ExporterTypeHTTP ExporterType = "http" 44 | ExporterTypeStdio ExporterType = "stdout" 45 | 46 | SamplerTypeAlways SamplerType = "always" 47 | SamplerTypeNever SamplerType = "never" 48 | SamplerTypeRatioBased SamplerType = "ratio_based" 49 | ) 50 | 51 | type NoopExporter struct{} 52 | 53 | func (n NoopExporter) ExportSpans(_ context.Context, _ []sdktrace.ReadOnlySpan) error { 54 | return nil 55 | } 56 | 57 | func (n NoopExporter) MarshalLog() interface{} { 58 | return nil 59 | } 60 | 61 | func (n NoopExporter) Shutdown(_ context.Context) error { 62 | return nil 63 | } 64 | 65 | func (n NoopExporter) Start(_ context.Context) error { 66 | return nil 67 | } 68 | 69 | func NewNoopExporter() *NoopExporter { 70 | return &NoopExporter{} 71 | } 72 | 73 | type Exporter interface { 74 | sdktrace.SpanExporter 75 | 76 | Start(ctx context.Context) error 77 | } 78 | 79 | // NewProvider returns an OTLP exporter based tracer. 80 | func NewProvider(ctx context.Context, version string, exporter sdktrace.SpanExporter, opts ...sdktrace.TracerProviderOption) (trace.TracerProvider, error) { 81 | if exporter == nil { 82 | return noop.NewTracerProvider(), nil 83 | } 84 | 85 | res, err := resources(ctx, version) 86 | if err != nil { 87 | return nil, fmt.Errorf("failed to create resource: %w", err) 88 | } 89 | 90 | // Register the trace exporter with a TracerProvider, using a batch 91 | // span processor to aggregate spans before export. 92 | provider := sdktrace.NewTracerProvider( 93 | // Sampler options: 94 | // - sdktrace.NeverSample() 95 | // - sdktrace.TraceIDRatioBased(0.01) 96 | sdktrace.WithSampler(sdktrace.AlwaysSample()), 97 | sdktrace.WithResource(res), 98 | sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(exporter)), 99 | ) 100 | 101 | // Set global propagator to tracecontext (the default is no-op). 102 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 103 | otel.SetTracerProvider(provider) 104 | 105 | return provider, nil 106 | } 107 | 108 | func NewExporter(exType, otlpAddress string) (Exporter, error) { 109 | switch strings.ToLower(exType) { 110 | case string(ExporterTypeGRPC): 111 | return NewGRPCExporter(otlpAddress) 112 | case string(ExporterTypeHTTP): 113 | return NewHTTPExporter(otlpAddress) 114 | case string(ExporterTypeStdio): 115 | return NewConsoleExporter(os.Stdout) 116 | default: 117 | return NewNoopExporter(), fmt.Errorf("unknown exporter type: %s", exType) 118 | } 119 | } 120 | 121 | type consoleExporter struct { 122 | *stdouttrace.Exporter 123 | } 124 | 125 | func (c *consoleExporter) Start(_ context.Context) error { 126 | return nil 127 | } 128 | 129 | // NewConsoleExporter returns a console exporter. 130 | func NewConsoleExporter(w io.Writer) (Exporter, error) { 131 | exp, err := stdouttrace.New( 132 | stdouttrace.WithWriter(w), 133 | // Use human-readable output. 134 | stdouttrace.WithPrettyPrint(), 135 | // Do not print timestamps for the demo. 136 | stdouttrace.WithoutTimestamps(), 137 | ) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return &consoleExporter{exp}, nil 142 | } 143 | 144 | // NewGRPCExporter returns a gRPC exporter. 145 | func NewGRPCExporter(otlpAddress string) (Exporter, error) { 146 | return otlptracegrpc.NewUnstarted( 147 | otlptracegrpc.WithInsecure(), 148 | otlptracegrpc.WithEndpoint(otlpAddress), 149 | ), nil 150 | } 151 | 152 | // NewHTTPExporter returns a HTTP exporter. 153 | func NewHTTPExporter(otlpAddress string) (Exporter, error) { 154 | return otlptrace.NewUnstarted(otlptracehttp.NewClient( 155 | otlptracehttp.WithInsecure(), 156 | otlptracehttp.WithEndpoint(otlpAddress), 157 | )), nil 158 | } 159 | 160 | func resources(ctx context.Context, version string) (*resource.Resource, error) { 161 | res, err := resource.New(ctx, 162 | resource.WithAttributes( 163 | semconv.ServiceNameKey.String("parca-agent"), 164 | semconv.ServiceVersionKey.String(version), 165 | ), 166 | resource.WithFromEnv(), // pull attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables 167 | resource.WithProcess(), // This option configures a set of Detectors that discover process information 168 | resource.WithOS(), // This option configures a set of Detectors that discover OS information 169 | resource.WithContainer(), // This option configures a set of Detectors that discover container information 170 | resource.WithHost(), // This option configures a set of Detectors that discover host information 171 | ) 172 | if err != nil { 173 | return nil, fmt.Errorf("failed to create resource: %w", err) 174 | } 175 | return res, nil 176 | } 177 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/parca-dev/parca-agent 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | buf.build/gen/go/parca-dev/parca/grpc/go v1.5.1-20250212095114-4db6f2d46517.2 7 | buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.6-20250212095114-4db6f2d46517.1 8 | buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.6-20250320161912-af2aab87b1b3.1 9 | github.com/KimMachineGun/automemlimit v0.7.1 10 | github.com/alecthomas/kong v1.10.0 11 | github.com/apache/arrow/go/v16 v16.1.0 12 | github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 13 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 14 | github.com/containerd/containerd v1.7.27 15 | github.com/docker/docker v28.0.4+incompatible 16 | github.com/dustin/go-humanize v1.0.1 17 | github.com/elastic/go-freelru v0.16.0 18 | github.com/gogo/protobuf v1.3.2 19 | github.com/golang/snappy v1.0.0 20 | github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 21 | github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 22 | github.com/klauspost/compress v1.18.0 23 | github.com/prometheus/client_golang v1.22.0 24 | github.com/prometheus/common v0.63.0 25 | github.com/prometheus/prometheus v0.303.0 26 | github.com/sirupsen/logrus v1.9.3 27 | github.com/stretchr/testify v1.10.0 28 | github.com/tklauser/numcpus v0.10.0 29 | github.com/xyproto/ainur v1.3.3 30 | github.com/zcalusic/sysinfo v1.1.3 31 | github.com/zeebo/xxh3 v1.0.2 32 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 33 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 34 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 35 | go.opentelemetry.io/ebpf-profiler v0.0.0-20250416113750-7ddc23ea135a 36 | go.opentelemetry.io/otel v1.35.0 37 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 38 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 39 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 40 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 41 | go.opentelemetry.io/otel/sdk v1.35.0 42 | go.opentelemetry.io/otel/trace v1.35.0 43 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 44 | golang.org/x/sync v0.13.0 45 | golang.org/x/sys v0.32.0 46 | google.golang.org/grpc v1.71.1 47 | google.golang.org/protobuf v1.36.6 48 | gopkg.in/yaml.v3 v3.0.1 49 | k8s.io/api v0.32.3 50 | k8s.io/apimachinery v0.32.3 51 | k8s.io/client-go v0.32.3 52 | ) 53 | 54 | require ( 55 | buf.build/gen/go/gogo/protobuf/protocolbuffers/go v1.36.6-20240617172848-e1dbca2775a7.1 // indirect 56 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 57 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 // indirect 58 | github.com/Microsoft/go-winio v0.6.2 // indirect 59 | github.com/Microsoft/hcsshim v0.12.9 // indirect 60 | github.com/beorn7/perks v1.0.1 // indirect 61 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 62 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 63 | github.com/cilium/ebpf v0.18.0 // indirect 64 | github.com/containerd/cgroups/v3 v3.0.5 // indirect 65 | github.com/containerd/containerd/api v1.8.0 // indirect 66 | github.com/containerd/continuity v0.4.5 // indirect 67 | github.com/containerd/errdefs v1.0.0 // indirect 68 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 69 | github.com/containerd/fifo v1.1.0 // indirect 70 | github.com/containerd/log v0.1.0 // indirect 71 | github.com/containerd/platforms v0.2.1 // indirect 72 | github.com/containerd/ttrpc v1.2.7 // indirect 73 | github.com/containerd/typeurl/v2 v2.2.3 // indirect 74 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 75 | github.com/distribution/reference v0.6.0 // indirect 76 | github.com/docker/go-connections v0.5.0 // indirect 77 | github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect 78 | github.com/docker/go-units v0.5.0 // indirect 79 | github.com/elastic/go-perf v0.0.0-20241029065020-30bec95324b8 // indirect 80 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 81 | github.com/felixge/httpsnoop v1.0.4 // indirect 82 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 83 | github.com/go-logr/logr v1.4.2 // indirect 84 | github.com/go-logr/stdr v1.2.2 // indirect 85 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 86 | github.com/go-openapi/jsonreference v0.21.0 // indirect 87 | github.com/go-openapi/swag v0.23.1 // indirect 88 | github.com/goccy/go-json v0.10.5 // indirect 89 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 90 | github.com/golang/protobuf v1.5.4 // indirect 91 | github.com/google/flatbuffers v25.2.10+incompatible // indirect 92 | github.com/google/gnostic-models v0.6.9 // indirect 93 | github.com/google/go-cmp v0.7.0 // indirect 94 | github.com/google/gofuzz v1.2.0 // indirect 95 | github.com/google/uuid v1.6.0 // indirect 96 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect 97 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect 98 | github.com/josharian/intern v1.0.0 // indirect 99 | github.com/jpillora/backoff v1.0.0 // indirect 100 | github.com/json-iterator/go v1.1.12 // indirect 101 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 102 | github.com/mailru/easyjson v0.9.0 // indirect 103 | github.com/minio/sha256-simd v1.0.1 // indirect 104 | github.com/moby/docker-image-spec v1.3.1 // indirect 105 | github.com/moby/locker v1.0.1 // indirect 106 | github.com/moby/sys/mountinfo v0.7.2 // indirect 107 | github.com/moby/sys/sequential v0.6.0 // indirect 108 | github.com/moby/sys/signal v0.7.1 // indirect 109 | github.com/moby/sys/user v0.4.0 // indirect 110 | github.com/moby/sys/userns v0.1.0 // indirect 111 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 112 | github.com/modern-go/reflect2 v1.0.2 // indirect 113 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 114 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 115 | github.com/opencontainers/go-digest v1.0.0 // indirect 116 | github.com/opencontainers/image-spec v1.1.1 // indirect 117 | github.com/opencontainers/runtime-spec v1.2.1 // indirect 118 | github.com/opencontainers/selinux v1.12.0 // indirect 119 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 120 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 121 | github.com/pkg/errors v0.9.1 // indirect 122 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 123 | github.com/prometheus/client_model v0.6.2 // indirect 124 | github.com/prometheus/procfs v0.16.0 // indirect 125 | github.com/x448/float16 v0.8.4 // indirect 126 | go.opencensus.io v0.24.0 // indirect 127 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 128 | go.opentelemetry.io/collector/consumer v1.30.0 // indirect 129 | go.opentelemetry.io/collector/consumer/xconsumer v0.124.0 // indirect 130 | go.opentelemetry.io/collector/pdata v1.30.0 // indirect 131 | go.opentelemetry.io/collector/pdata/pprofile v0.124.0 // indirect 132 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 133 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 134 | go.uber.org/multierr v1.11.0 // indirect 135 | golang.org/x/arch v0.16.0 // indirect 136 | golang.org/x/mod v0.24.0 // indirect 137 | golang.org/x/net v0.39.0 // indirect 138 | golang.org/x/oauth2 v0.29.0 // indirect 139 | golang.org/x/term v0.31.0 // indirect 140 | golang.org/x/text v0.24.0 // indirect 141 | golang.org/x/time v0.11.0 // indirect 142 | golang.org/x/tools v0.32.0 // indirect 143 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 144 | google.golang.org/genproto v0.0.0-20250414145226-207652e42e2e // indirect 145 | google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect 146 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect 147 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 148 | gopkg.in/inf.v0 v0.9.1 // indirect 149 | gopkg.in/yaml.v2 v2.4.0 // indirect 150 | k8s.io/klog/v2 v2.130.1 // indirect 151 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 152 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 153 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 154 | sigs.k8s.io/randfill v1.0.0 // indirect 155 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 156 | sigs.k8s.io/yaml v1.4.0 // indirect 157 | ) 158 | 159 | replace go.opentelemetry.io/ebpf-profiler => github.com/parca-dev/opentelemetry-ebpf-profiler v0.0.0-20250502192423-911d0dc189d8 160 | -------------------------------------------------------------------------------- /kubernetes-config.yaml: -------------------------------------------------------------------------------- 1 | relabel_configs: 2 | - source_labels: [__meta_process_executable_compiler] 3 | target_label: compiler 4 | - source_labels: [__meta_system_kernel_machine] 5 | target_label: arch 6 | - source_labels: [__meta_system_kernel_release] 7 | target_label: kernel_version 8 | - source_labels: [__meta_kubernetes_namespace] 9 | target_label: namespace 10 | - source_labels: [__meta_kubernetes_pod_name] 11 | target_label: pod 12 | - source_labels: [__meta_kubernetes_pod_container_name] 13 | target_label: container 14 | - source_labels: [__meta_kubernetes_pod_container_image] 15 | target_label: container_image 16 | - source_labels: [__meta_kubernetes_node_label_topology_kubernetes_io_region] 17 | target_label: region 18 | - source_labels: [__meta_kubernetes_node_label_topology_kubernetes_io_zone] 19 | target_label: zone 20 | - action: labelmap 21 | regex: __meta_kubernetes_pod_label_(.+) 22 | replacement: ${1} 23 | - action: labeldrop 24 | regex: apps_kubernetes_io_pod_index|controller_revision_hash|statefulset_kubernetes_io_pod_name|pod_template_hash 25 | 26 | -------------------------------------------------------------------------------- /metrics/genschema/gen.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | def read_json_array(filename): 5 | with open(filename, 'r') as file: 6 | data = json.load(file) 7 | return data 8 | 9 | if __name__ == "__main__": 10 | infile = sys.argv[1] 11 | outfile = sys.argv[2] 12 | data = read_json_array(infile) 13 | 14 | with open(outfile, 'w') as file: 15 | file.write( 16 | """// Code generated from metrics.json. DO NOT EDIT. 17 | // NOTE: metrics.json was copied from opentelemetry-ebpf.profiler. 18 | // It should be kept in sync when upgrading versions. 19 | 20 | package metrics 21 | 22 | import ( 23 | \totelmetrics "go.opentelemetry.io/ebpf-profiler/metrics" 24 | ) 25 | 26 | const ( 27 | \tMetricUnitNone = 0 28 | \tMetricUnitPercent = 1 29 | \tMetricUnitByte = 2 30 | \tMetricUnitMicroseconds = 3 31 | \tMetricUnitMilliseconds = 4 32 | \tMetricUnitSeconds = 5 33 | ) 34 | 35 | const ( 36 | \tMetricTypeGauge = 0 37 | \tMetricTypeCounter = 1 38 | ) 39 | 40 | var AllMetrics = map[otelmetrics.MetricID]Metric { 41 | """) 42 | def get_type(s): 43 | match s: 44 | case "gauge": 45 | return "MetricTypeGauge" 46 | case "counter": 47 | return "MetricTypeCounter" 48 | case _: 49 | raise ValueError(f"Unknown metric type: {s}") 50 | 51 | def get_unit(s): 52 | match s: 53 | case None: 54 | return "MetricUnitNone" 55 | case "percent": 56 | return "MetricUnitPercent" 57 | case "byte": 58 | return "MetricUnitByte" 59 | case "micros": 60 | return "MetricUnitMicroseconds" 61 | case "ms": 62 | return "MetricUnitMilliseconds" 63 | case "s": 64 | return "MetricUnitSeconds" 65 | case _: 66 | raise ValueError(f"Unknown metric unit: {s}") 67 | 68 | for metric in data: 69 | if not "name" in metric: 70 | continue 71 | if not "field" in metric: 72 | continue 73 | if metric.get("obsolete"): 74 | continue 75 | file.write(f"""\totelmetrics.ID{metric["name"]}: {{ 76 | \t\tDesc: "{metric["description"]}", 77 | \t\tField: "{metric["field"]}", 78 | \t\tType: {get_type(metric.get("type"))}, 79 | \t\tUnit: {get_unit(metric.get("unit"))}, 80 | \t}}, 81 | """) 82 | file.write("}\n") 83 | -------------------------------------------------------------------------------- /metrics/types.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | //go:generate python3 genschema/gen.py metrics.json all.go 4 | 5 | // MetricUnit is the type for metric units (e.g. millis). 6 | type MetricUnit uint64 7 | 8 | // MetricType is the type for metric types (e.g. gauge). 9 | type MetricType uint64 10 | 11 | type Metric struct { 12 | Desc string 13 | Field string 14 | Type MetricType 15 | Unit MetricUnit 16 | } 17 | -------------------------------------------------------------------------------- /reporter/arrow.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "bytes" 5 | "slices" 6 | "unsafe" 7 | 8 | "github.com/apache/arrow/go/v16/arrow" 9 | "github.com/apache/arrow/go/v16/arrow/array" 10 | "github.com/apache/arrow/go/v16/arrow/memory" 11 | "golang.org/x/exp/maps" 12 | ) 13 | 14 | func binaryDictionaryRunEndBuilder(arr array.Builder) *BinaryDictionaryRunEndBuilder { 15 | ree := arr.(*array.RunEndEncodedBuilder) 16 | bd := ree.ValueBuilder().(*array.BinaryDictionaryBuilder) 17 | idx := bd.IndexBuilder().Builder.(*array.Uint32Builder) 18 | return &BinaryDictionaryRunEndBuilder{ 19 | ree: ree, 20 | bd: ree.ValueBuilder().(*array.BinaryDictionaryBuilder), 21 | idx: idx, 22 | } 23 | } 24 | 25 | type BinaryDictionaryRunEndBuilder struct { 26 | ree *array.RunEndEncodedBuilder 27 | bd *array.BinaryDictionaryBuilder 28 | idx *array.Uint32Builder 29 | } 30 | 31 | func (b *BinaryDictionaryRunEndBuilder) Release() { 32 | b.ree.Release() 33 | } 34 | 35 | func (b *BinaryDictionaryRunEndBuilder) NewArray() arrow.Array { 36 | return b.ree.NewArray() 37 | } 38 | 39 | func (b *BinaryDictionaryRunEndBuilder) Append(v []byte) { 40 | if b.idx.Len() > 0 && 41 | !b.idx.IsNull(b.idx.Len()-1) && 42 | bytes.Equal(v, b.bd.Value(int(b.idx.Value(b.idx.Len()-1)))) { 43 | b.ree.ContinueRun(1) 44 | return 45 | } 46 | b.ree.Append(1) 47 | b.bd.Append(v) 48 | } 49 | 50 | func (b *BinaryDictionaryRunEndBuilder) AppendN(v []byte, n uint64) { 51 | if b.idx.Len() > 0 && 52 | !b.idx.IsNull(b.idx.Len()-1) && 53 | bytes.Equal(v, b.bd.Value(int(b.idx.Value(b.idx.Len()-1)))) { 54 | b.ree.ContinueRun(n) 55 | return 56 | } 57 | b.ree.Append(n) 58 | b.bd.Append(v) 59 | } 60 | 61 | func (b *BinaryDictionaryRunEndBuilder) Len() int { 62 | return b.ree.Len() 63 | } 64 | 65 | func (b *BinaryDictionaryRunEndBuilder) EnsureLength(l int) { 66 | for b.ree.Len() < l { 67 | b.AppendNull() 68 | } 69 | } 70 | 71 | func (b *BinaryDictionaryRunEndBuilder) AppendNull() { 72 | b.ree.AppendNull() 73 | } 74 | 75 | func (b *BinaryDictionaryRunEndBuilder) AppendString(v string) { 76 | b.Append(unsafeStringToBytes(v)) 77 | } 78 | 79 | func (b *BinaryDictionaryRunEndBuilder) AppendStringN(v string, n uint64) { 80 | b.AppendN(unsafeStringToBytes(v), n) 81 | } 82 | 83 | func unsafeStringToBytes(s string) []byte { 84 | return unsafe.Slice(unsafe.StringData(s), len(s)) 85 | } 86 | 87 | func uint64RunEndBuilder(arr array.Builder) *Uint64RunEndBuilder { 88 | ree := arr.(*array.RunEndEncodedBuilder) 89 | return &Uint64RunEndBuilder{ 90 | ree: ree, 91 | ub: ree.ValueBuilder().(*array.Uint64Builder), 92 | } 93 | } 94 | 95 | type Uint64RunEndBuilder struct { 96 | ree *array.RunEndEncodedBuilder 97 | ub *array.Uint64Builder 98 | } 99 | 100 | func (b *Uint64RunEndBuilder) Release() { 101 | b.ree.Release() 102 | } 103 | 104 | func (b *Uint64RunEndBuilder) NewArray() arrow.Array { 105 | return b.ree.NewArray() 106 | } 107 | 108 | func (b *Uint64RunEndBuilder) AppendN(v uint64, n uint64) { 109 | if b.ub.Len() > 0 && v == b.ub.Value(b.ub.Len()-1) { 110 | b.ree.ContinueRun(n) 111 | return 112 | } 113 | b.ree.Append(n) 114 | b.ub.Append(v) 115 | } 116 | 117 | type Int64RunEndBuilder struct { 118 | ree *array.RunEndEncodedBuilder 119 | ib *array.Int64Builder 120 | } 121 | 122 | func (b *Int64RunEndBuilder) Release() { 123 | b.ree.Release() 124 | } 125 | 126 | func (b *Int64RunEndBuilder) NewArray() arrow.Array { 127 | return b.ree.NewArray() 128 | } 129 | 130 | func int64RunEndBuilder(arr array.Builder) *Int64RunEndBuilder { 131 | ree := arr.(*array.RunEndEncodedBuilder) 132 | return &Int64RunEndBuilder{ 133 | ree: ree, 134 | ib: ree.ValueBuilder().(*array.Int64Builder), 135 | } 136 | } 137 | 138 | func (b *Int64RunEndBuilder) Append(v int64) { 139 | if b.ib.Len() > 0 && v == b.ib.Value(b.ib.Len()-1) { 140 | b.ree.ContinueRun(1) 141 | return 142 | } 143 | b.ree.Append(1) 144 | b.ib.Append(v) 145 | } 146 | 147 | type LocationsWriter struct { 148 | IsComplete *array.BooleanBuilder 149 | LocationsList *array.ListBuilder 150 | Locations *array.StructBuilder 151 | Address *array.Uint64Builder 152 | FrameType *BinaryDictionaryRunEndBuilder 153 | MappingStart *Uint64RunEndBuilder 154 | MappingLimit *Uint64RunEndBuilder 155 | MappingOffset *Uint64RunEndBuilder 156 | MappingFile *BinaryDictionaryRunEndBuilder 157 | MappingBuildID *BinaryDictionaryRunEndBuilder 158 | Lines *array.ListBuilder 159 | Line *array.StructBuilder 160 | LineNumber *array.Int64Builder 161 | FunctionName *array.BinaryDictionaryBuilder 162 | FunctionSystemName *array.BinaryDictionaryBuilder 163 | FunctionFilename *BinaryDictionaryRunEndBuilder 164 | FunctionStartLine *array.Int64Builder 165 | } 166 | 167 | func (w *LocationsWriter) NewRecord(stacktraceIDs *array.Binary) arrow.Record { 168 | numMappings := uint64(w.MappingFile.Len()) 169 | 170 | // Setting mapping start, limit and offset to 0 signals to the backend that 171 | // in the case of a native frame the address no longer has to be adjusted 172 | // to the symbol table address. 173 | w.MappingStart.AppendN(0, numMappings) 174 | w.MappingLimit.AppendN(0, numMappings) 175 | w.MappingOffset.AppendN(0, numMappings) 176 | return array.NewRecord( 177 | arrow.NewSchema([]arrow.Field{{ 178 | Name: "stacktrace_id", 179 | Type: arrow.BinaryTypes.Binary, 180 | }, { 181 | Name: "is_complete", 182 | Type: arrow.FixedWidthTypes.Boolean, 183 | }, LocationsField}, newV1Metadata()), 184 | []arrow.Array{ 185 | stacktraceIDs, 186 | w.IsComplete.NewArray(), 187 | w.LocationsList.NewArray(), 188 | }, 189 | int64(stacktraceIDs.Len()), 190 | ) 191 | } 192 | 193 | func (w *LocationsWriter) Release() { 194 | w.LocationsList.Release() 195 | } 196 | 197 | type SampleWriter struct { 198 | mem memory.Allocator 199 | 200 | labelBuilders map[string]*BinaryDictionaryRunEndBuilder 201 | 202 | StacktraceID *BinaryDictionaryRunEndBuilder 203 | Value *array.Int64Builder 204 | Producer *BinaryDictionaryRunEndBuilder 205 | SampleType *BinaryDictionaryRunEndBuilder 206 | SampleUnit *BinaryDictionaryRunEndBuilder 207 | PeriodType *BinaryDictionaryRunEndBuilder 208 | PeriodUnit *BinaryDictionaryRunEndBuilder 209 | Temporality *BinaryDictionaryRunEndBuilder 210 | Period *Int64RunEndBuilder 211 | Duration *Int64RunEndBuilder 212 | Timestamp *Int64RunEndBuilder 213 | } 214 | 215 | func (w *SampleWriter) NewRecord() arrow.Record { 216 | labelNames := maps.Keys(w.labelBuilders) 217 | slices.Sort(labelNames) 218 | 219 | labelArrays := make([]arrow.Array, 0, len(labelNames)) 220 | labelFields := make([]arrow.Field, 0, len(labelNames)) 221 | 222 | length := w.Value.Len() 223 | for _, labelName := range labelNames { 224 | b := w.labelBuilders[labelName] 225 | 226 | // Need to ensure that all label arrays are backfilled to match the 227 | // length of the rest of the arrays, the value array taken as the most 228 | // reliabile reference. 229 | b.EnsureLength(length) 230 | labelFields = append(labelFields, w.labelField(labelName)) 231 | labelArrays = append(labelArrays, b.NewArray()) 232 | } 233 | 234 | return array.NewRecord( 235 | SampleSchema(labelFields), 236 | append( 237 | labelArrays, 238 | w.StacktraceID.NewArray(), 239 | w.Value.NewArray(), 240 | w.Producer.NewArray(), 241 | w.SampleType.NewArray(), 242 | w.SampleUnit.NewArray(), 243 | w.PeriodType.NewArray(), 244 | w.PeriodUnit.NewArray(), 245 | w.Temporality.NewArray(), 246 | w.Period.NewArray(), 247 | w.Duration.NewArray(), 248 | w.Timestamp.NewArray(), 249 | ), 250 | int64(length), 251 | ) 252 | } 253 | 254 | func (w *SampleWriter) Release() { 255 | for _, b := range w.labelBuilders { 256 | b.Release() 257 | } 258 | w.StacktraceID.Release() 259 | w.Value.Release() 260 | w.Producer.Release() 261 | w.SampleType.Release() 262 | w.SampleUnit.Release() 263 | w.PeriodType.Release() 264 | w.PeriodUnit.Release() 265 | w.Temporality.Release() 266 | w.Period.Release() 267 | w.Duration.Release() 268 | w.Timestamp.Release() 269 | } 270 | 271 | var ( 272 | LocationsField = arrow.Field{ 273 | Name: "locations", 274 | Type: arrow.ListOf(arrow.StructOf([]arrow.Field{{ 275 | Name: "address", 276 | Type: arrow.PrimitiveTypes.Uint64, 277 | }, { 278 | Name: "frame_type", 279 | Type: arrow.RunEndEncodedOf( 280 | arrow.PrimitiveTypes.Int32, 281 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 282 | ), 283 | }, { 284 | Name: "mapping_start", 285 | Type: arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.PrimitiveTypes.Uint64), 286 | }, { 287 | Name: "mapping_limit", 288 | Type: arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.PrimitiveTypes.Uint64), 289 | }, { 290 | Name: "mapping_offset", 291 | Type: arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.PrimitiveTypes.Uint64), 292 | }, { 293 | Name: "mapping_file", 294 | Type: arrow.RunEndEncodedOf( 295 | arrow.PrimitiveTypes.Int32, 296 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 297 | ), 298 | }, { 299 | Name: "mapping_build_id", 300 | Type: arrow.RunEndEncodedOf( 301 | arrow.PrimitiveTypes.Int32, 302 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 303 | ), 304 | }, { 305 | Name: "lines", 306 | Type: arrow.ListOf(arrow.StructOf([]arrow.Field{{ 307 | Name: "line", 308 | Type: arrow.PrimitiveTypes.Int64, 309 | }, { 310 | Name: "function_name", 311 | Type: &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 312 | }, { 313 | Name: "function_system_name", 314 | Type: &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 315 | }, { 316 | Name: "function_filename", 317 | Type: arrow.RunEndEncodedOf( 318 | arrow.PrimitiveTypes.Int32, 319 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 320 | ), 321 | }, { 322 | Name: "function_start_line", 323 | Type: arrow.PrimitiveTypes.Int64, 324 | }}...)), 325 | }}...)), 326 | } 327 | 328 | StacktraceIDField = arrow.Field{ 329 | Name: "stacktrace_id", 330 | Type: arrow.RunEndEncodedOf( 331 | arrow.PrimitiveTypes.Int32, 332 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 333 | ), 334 | } 335 | 336 | ValueField = arrow.Field{ 337 | Name: "value", 338 | Type: arrow.PrimitiveTypes.Int64, 339 | } 340 | 341 | ProducerField = arrow.Field{ 342 | Name: "producer", 343 | Type: arrow.RunEndEncodedOf( 344 | arrow.PrimitiveTypes.Int32, 345 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 346 | ), 347 | } 348 | 349 | SampleTypeField = arrow.Field{ 350 | Name: "sample_type", 351 | Type: arrow.RunEndEncodedOf( 352 | arrow.PrimitiveTypes.Int32, 353 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 354 | ), 355 | } 356 | 357 | SampleUnitField = arrow.Field{ 358 | Name: "sample_unit", 359 | Type: arrow.RunEndEncodedOf( 360 | arrow.PrimitiveTypes.Int32, 361 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 362 | ), 363 | } 364 | 365 | PeriodTypeField = arrow.Field{ 366 | Name: "period_type", 367 | Type: arrow.RunEndEncodedOf( 368 | arrow.PrimitiveTypes.Int32, 369 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 370 | ), 371 | } 372 | 373 | PeriodUnitField = arrow.Field{ 374 | Name: "period_unit", 375 | Type: arrow.RunEndEncodedOf( 376 | arrow.PrimitiveTypes.Int32, 377 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 378 | ), 379 | } 380 | 381 | TemporalityField = arrow.Field{ 382 | Name: "temporality", 383 | Type: arrow.RunEndEncodedOf( 384 | arrow.PrimitiveTypes.Int32, 385 | &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint32, ValueType: arrow.BinaryTypes.Binary}, 386 | ), 387 | } 388 | 389 | PeriodField = arrow.Field{ 390 | Name: "period", 391 | Type: arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.PrimitiveTypes.Int64), 392 | } 393 | 394 | DurationField = arrow.Field{ 395 | Name: "duration", 396 | Type: arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.PrimitiveTypes.Int64), 397 | } 398 | 399 | TimestampField = arrow.Field{ 400 | Name: "timestamp", 401 | Type: arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.PrimitiveTypes.Int64), 402 | } 403 | 404 | labelArrowType = arrow.RunEndEncodedOf( 405 | arrow.PrimitiveTypes.Int32, 406 | &arrow.DictionaryType{ 407 | IndexType: arrow.PrimitiveTypes.Uint32, 408 | ValueType: arrow.BinaryTypes.Binary, 409 | }, 410 | ) 411 | ) 412 | 413 | const ( 414 | MetadataSchemaVersion = "parca_write_schema_version" 415 | MetadataSchemaVersionV1 = "v1" 416 | ColumnLabelsPrefix = "labels." 417 | ) 418 | 419 | func ArrowSamplesField(profileLabelFields []arrow.Field) []arrow.Field { 420 | // +11 for stacktrace IDs, value, producer, sample_type, sample_unit, period_type, period_unit, temporality, period, duration, timestamp 421 | numFields := len(profileLabelFields) + 11 422 | fields := make([]arrow.Field, numFields) 423 | copy(fields, profileLabelFields) 424 | 425 | fields[numFields-11] = StacktraceIDField 426 | fields[numFields-10] = ValueField 427 | fields[numFields-9] = ProducerField 428 | fields[numFields-8] = SampleTypeField 429 | fields[numFields-7] = SampleUnitField 430 | fields[numFields-6] = PeriodTypeField 431 | fields[numFields-5] = PeriodUnitField 432 | fields[numFields-4] = TemporalityField 433 | fields[numFields-3] = PeriodField 434 | fields[numFields-2] = DurationField 435 | fields[numFields-1] = TimestampField 436 | 437 | return fields 438 | } 439 | 440 | func newV1Metadata() *arrow.Metadata { 441 | m := arrow.NewMetadata([]string{MetadataSchemaVersion}, []string{MetadataSchemaVersionV1}) 442 | return &m 443 | } 444 | 445 | func SampleSchema(profileLabelFields []arrow.Field) *arrow.Schema { 446 | return arrow.NewSchema(ArrowSamplesField(profileLabelFields), newV1Metadata()) 447 | } 448 | 449 | func (w *SampleWriter) labelField(labelName string) arrow.Field { 450 | return arrow.Field{ 451 | Name: ColumnLabelsPrefix + labelName, 452 | Type: labelArrowType, 453 | Nullable: true, 454 | } 455 | } 456 | 457 | func (w *SampleWriter) Label(labelName string) *BinaryDictionaryRunEndBuilder { 458 | b, ok := w.labelBuilders[labelName] 459 | if !ok { 460 | b = binaryDictionaryRunEndBuilder(array.NewBuilder(w.mem, labelArrowType)) 461 | w.labelBuilders[labelName] = b 462 | } 463 | 464 | b.EnsureLength(w.Value.Len()) 465 | return b 466 | } 467 | 468 | func (w *SampleWriter) LabelAll(labelName, labelValue string) { 469 | b, ok := w.labelBuilders[labelName] 470 | if !ok { 471 | b = binaryDictionaryRunEndBuilder(array.NewBuilder(w.mem, labelArrowType)) 472 | w.labelBuilders[labelName] = b 473 | } 474 | 475 | b.ree.Append(uint64(w.Value.Len() - b.ree.Len())) 476 | b.bd.AppendString(labelValue) 477 | } 478 | 479 | func NewLocationsWriter(mem memory.Allocator) *LocationsWriter { 480 | isComplete := array.NewBuilder(mem, arrow.FixedWidthTypes.Boolean).(*array.BooleanBuilder) 481 | 482 | locationsList := array.NewBuilder(mem, LocationsField.Type).(*array.ListBuilder) 483 | locations := locationsList.ValueBuilder().(*array.StructBuilder) 484 | 485 | addresses := locations.FieldBuilder(0).(*array.Uint64Builder) 486 | frameType := binaryDictionaryRunEndBuilder(locations.FieldBuilder(1)) 487 | 488 | mappingStart := uint64RunEndBuilder(locations.FieldBuilder(2)) 489 | mappingLimit := uint64RunEndBuilder(locations.FieldBuilder(3)) 490 | mappingOffset := uint64RunEndBuilder(locations.FieldBuilder(4)) 491 | mappingFile := binaryDictionaryRunEndBuilder(locations.FieldBuilder(5)) 492 | mappingBuildID := binaryDictionaryRunEndBuilder(locations.FieldBuilder(6)) 493 | 494 | lines := locations.FieldBuilder(7).(*array.ListBuilder) 495 | line := lines.ValueBuilder().(*array.StructBuilder) 496 | lineNumber := line.FieldBuilder(0).(*array.Int64Builder) 497 | functionName := line.FieldBuilder(1).(*array.BinaryDictionaryBuilder) 498 | functionSystemName := line.FieldBuilder(2).(*array.BinaryDictionaryBuilder) 499 | functionFilename := binaryDictionaryRunEndBuilder(line.FieldBuilder(3)) 500 | functionStartLine := line.FieldBuilder(4).(*array.Int64Builder) 501 | 502 | return &LocationsWriter{ 503 | IsComplete: isComplete, 504 | LocationsList: locationsList, 505 | Locations: locations, 506 | Address: addresses, 507 | FrameType: frameType, 508 | MappingStart: mappingStart, 509 | MappingLimit: mappingLimit, 510 | MappingOffset: mappingOffset, 511 | MappingFile: mappingFile, 512 | MappingBuildID: mappingBuildID, 513 | Lines: lines, 514 | Line: line, 515 | LineNumber: lineNumber, 516 | FunctionName: functionName, 517 | FunctionSystemName: functionSystemName, 518 | FunctionFilename: functionFilename, 519 | FunctionStartLine: functionStartLine, 520 | } 521 | } 522 | 523 | func NewSampleWriter(mem memory.Allocator) *SampleWriter { 524 | stacktraceID := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, StacktraceIDField.Type)) 525 | value := array.NewBuilder(mem, ValueField.Type).(*array.Int64Builder) 526 | producer := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, ProducerField.Type)) 527 | sampleType := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, SampleTypeField.Type)) 528 | sampleUnit := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, SampleUnitField.Type)) 529 | periodType := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, PeriodTypeField.Type)) 530 | periodUnit := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, PeriodUnitField.Type)) 531 | temporality := binaryDictionaryRunEndBuilder(array.NewBuilder(mem, TemporalityField.Type)) 532 | period := int64RunEndBuilder(array.NewBuilder(mem, PeriodField.Type)) 533 | duration := int64RunEndBuilder(array.NewBuilder(mem, DurationField.Type)) 534 | timestamp := int64RunEndBuilder(array.NewBuilder(mem, TimestampField.Type)) 535 | 536 | return &SampleWriter{ 537 | mem: mem, 538 | 539 | labelBuilders: map[string]*BinaryDictionaryRunEndBuilder{}, 540 | 541 | StacktraceID: stacktraceID, 542 | Value: value, 543 | Producer: producer, 544 | SampleType: sampleType, 545 | SampleUnit: sampleUnit, 546 | PeriodType: periodType, 547 | PeriodUnit: periodUnit, 548 | Temporality: temporality, 549 | Period: period, 550 | Duration: duration, 551 | Timestamp: timestamp, 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /reporter/elfwriter/extract.go: -------------------------------------------------------------------------------- 1 | package elfwriter 2 | 3 | import ( 4 | "debug/elf" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type ReadAtCloser interface { 10 | io.ReaderAt 11 | io.Closer 12 | } 13 | 14 | func OnlyKeepDebug(dst io.WriteSeeker, src ReadAtCloser) error { 15 | w, err := NewNullifyingWriter(dst, src) 16 | if err != nil { 17 | return fmt.Errorf("initialize nullifying writer: %w", err) 18 | } 19 | w.FilterPrograms(func(p *elf.Prog) bool { 20 | return p.Type == elf.PT_NOTE 21 | }) 22 | w.KeepSections( 23 | isDWARF, 24 | isSymbolTable, 25 | isGoSymbolTable, 26 | isPltSymbolTable, // NOTICE: gostd debug/elf.DWARF applies relocations. 27 | func(s *elf.Section) bool { 28 | return s.Name == ".comment" 29 | }, 30 | func(s *elf.Section) bool { 31 | return s.Type == elf.SHT_NOTE 32 | }, 33 | ) 34 | 35 | if err := w.Flush(); err != nil { 36 | return fmt.Errorf("flush ELF file: %w", err) 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /reporter/elfwriter/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | // 14 | 15 | package elfwriter 16 | 17 | import ( 18 | "debug/elf" 19 | "encoding/binary" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "strings" 24 | 25 | "github.com/klauspost/compress/zlib" 26 | ) 27 | 28 | type zeroReader struct{} 29 | 30 | func (*zeroReader) ReadAt(p []byte, off int64) (_ int, _ error) { 31 | for i := range p { 32 | p[i] = 0 33 | } 34 | return len(p), nil 35 | } 36 | 37 | type countingWriter struct { 38 | w io.Writer 39 | written int64 40 | } 41 | 42 | func (cw *countingWriter) Write(p []byte) (int, error) { 43 | n, err := cw.w.Write(p) 44 | cw.written += int64(n) 45 | return n, err 46 | } 47 | 48 | func isCompressed(sec *elf.Section) bool { 49 | return sec.Type == elf.SHT_PROGBITS && 50 | (sec.Flags&elf.SHF_COMPRESSED != 0 || strings.HasPrefix(sec.Name, ".zdebug_")) 51 | } 52 | 53 | type compressionHeader struct { 54 | byteOrder binary.ByteOrder 55 | class elf.Class 56 | headerSize int 57 | 58 | Type uint32 59 | Size uint64 60 | Addralign uint64 61 | } 62 | 63 | func NewCompressionHeaderFromSource(fhdr *elf.FileHeader, src io.ReaderAt, offset int64) (*compressionHeader, error) { 64 | hdr := &compressionHeader{} 65 | 66 | switch fhdr.Class { 67 | case elf.ELFCLASS32: 68 | ch := new(elf.Chdr32) 69 | hdr.headerSize = binary.Size(ch) 70 | sr := io.NewSectionReader(src, offset, int64(hdr.headerSize)) 71 | if err := binary.Read(sr, fhdr.ByteOrder, ch); err != nil { 72 | return nil, err 73 | } 74 | hdr.class = elf.ELFCLASS32 75 | hdr.Type = ch.Type 76 | hdr.Size = uint64(ch.Size) 77 | hdr.Addralign = uint64(ch.Addralign) 78 | hdr.byteOrder = fhdr.ByteOrder 79 | case elf.ELFCLASS64: 80 | ch := new(elf.Chdr64) 81 | hdr.headerSize = binary.Size(ch) 82 | sr := io.NewSectionReader(src, offset, int64(hdr.headerSize)) 83 | if err := binary.Read(sr, fhdr.ByteOrder, ch); err != nil { 84 | return nil, err 85 | } 86 | hdr.class = elf.ELFCLASS64 87 | hdr.Type = ch.Type 88 | hdr.Size = ch.Size 89 | hdr.Addralign = ch.Addralign 90 | hdr.byteOrder = fhdr.ByteOrder 91 | case elf.ELFCLASSNONE: 92 | fallthrough 93 | default: 94 | return nil, fmt.Errorf("unknown ELF class: %v", fhdr.Class) 95 | } 96 | 97 | if elf.CompressionType(hdr.Type) != elf.COMPRESS_ZLIB { 98 | // TODO(kakkoyun): COMPRESS_ZSTD 99 | // https://github.com/golang/go/issues/55107 100 | return nil, errors.New("section should be zlib compressed, we are reading from the wrong offset or debug data is corrupt") 101 | } 102 | 103 | return hdr, nil 104 | } 105 | 106 | func (hdr compressionHeader) WriteTo(w io.Writer) (int64, error) { 107 | var written int 108 | switch hdr.class { 109 | case elf.ELFCLASS32: 110 | ch := new(elf.Chdr32) 111 | ch.Type = uint32(elf.COMPRESS_ZLIB) 112 | ch.Size = uint32(hdr.Size) 113 | ch.Addralign = uint32(hdr.Addralign) 114 | if err := binary.Write(w, hdr.byteOrder, ch); err != nil { 115 | return 0, err 116 | } 117 | written = binary.Size(ch) // headerSize 118 | case elf.ELFCLASS64: 119 | ch := new(elf.Chdr64) 120 | ch.Type = uint32(elf.COMPRESS_ZLIB) 121 | ch.Size = hdr.Size 122 | ch.Addralign = hdr.Addralign 123 | if err := binary.Write(w, hdr.byteOrder, ch); err != nil { 124 | return 0, err 125 | } 126 | written = binary.Size(ch) // headerSize 127 | case elf.ELFCLASSNONE: 128 | fallthrough 129 | default: 130 | return 0, fmt.Errorf("unknown ELF class: %v", hdr.class) 131 | } 132 | 133 | return int64(written), nil 134 | } 135 | 136 | func copyCompressed(w io.Writer, r io.Reader) (int64, error) { 137 | if r == nil { 138 | return 0, errors.New("reader is nil") 139 | } 140 | 141 | pr, pw := io.Pipe() 142 | 143 | // write in writer end of pipe. 144 | var wErr error 145 | go func() { 146 | defer pw.Close() 147 | defer func() { 148 | if r := recover(); r != nil { 149 | err, ok := r.(error) 150 | if ok { 151 | wErr = fmt.Errorf("panic occurred: %w", err) 152 | } 153 | } 154 | }() 155 | _, wErr = io.Copy(pw, r) 156 | }() 157 | 158 | // read from reader end of pipe. 159 | defer pr.Close() 160 | 161 | cw := &countingWriter{w: w} 162 | zw := zlib.NewWriter(cw) 163 | _, err := io.Copy(zw, pr) 164 | if err != nil { 165 | zw.Close() 166 | return 0, err 167 | } 168 | zw.Close() 169 | 170 | if wErr != nil { 171 | return 0, wErr 172 | } 173 | return cw.written, nil 174 | } 175 | 176 | func isDWARF(s *elf.Section) bool { 177 | return strings.HasPrefix(s.Name, ".debug_") || 178 | strings.HasPrefix(s.Name, ".zdebug_") || 179 | strings.HasPrefix(s.Name, "__debug_") // macos 180 | } 181 | 182 | func isSymbolTable(s *elf.Section) bool { 183 | return s.Type == elf.SHT_SYMTAB || s.Type == elf.SHT_DYNSYM || 184 | s.Type == elf.SHT_STRTAB || 185 | s.Name == ".symtab" || 186 | s.Name == ".dynsym" || 187 | s.Name == ".strtab" || 188 | s.Name == ".dynstr" 189 | } 190 | 191 | func isGoSymbolTable(s *elf.Section) bool { 192 | return s.Name == ".gosymtab" || 193 | s.Name == ".gopclntab" || 194 | s.Name == ".go.buildinfo" || 195 | s.Name == ".data.rel.ro.gosymtab" || 196 | s.Name == ".data.rel.ro.gopclntab" 197 | } 198 | 199 | func isPltSymbolTable(s *elf.Section) bool { 200 | return s.Type == elf.SHT_RELA || s.Type == elf.SHT_REL || // nolint:misspell 201 | // Redundant 202 | s.Name == ".plt" || 203 | s.Name == ".plt.got" || 204 | s.Name == ".rela.plt" || 205 | s.Name == ".rela.dyn" // nolint:goconst 206 | } 207 | 208 | func match[T *elf.Prog | *elf.Section | *elf.SectionHeader](elem T, predicates ...func(T) bool) bool { 209 | for _, pred := range predicates { 210 | if pred(elem) { 211 | return true 212 | } 213 | } 214 | return false 215 | } 216 | -------------------------------------------------------------------------------- /reporter/elfwriter/nullifying_elfwriter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | // 14 | 15 | package elfwriter 16 | 17 | import ( 18 | "debug/elf" 19 | "fmt" 20 | "io" 21 | ) 22 | 23 | // NullifyingWriter is a wrapper around another Writer that nullifies all the sections 24 | // except the whitelisted ones. 25 | type NullifyingWriter struct { 26 | Writer 27 | src io.ReaderAt 28 | 29 | progPredicates []func(*elf.Prog) bool 30 | sectionPredicates []func(*elf.Section) bool 31 | } 32 | 33 | // NewNullifyingWriter creates a new NullifyingWriter. 34 | func NewNullifyingWriter(dst io.WriteSeeker, src io.ReaderAt, opts ...Option) (*NullifyingWriter, error) { 35 | f, err := elf.NewFile(src) 36 | if err != nil { 37 | return nil, fmt.Errorf("error reading ELF file: %w", err) 38 | } 39 | defer f.Close() 40 | 41 | w, err := newWriter(dst, &f.FileHeader, newNullifyingWriterSectionReader(src), opts...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | w.progs = f.Progs 46 | w.sections = f.Sections 47 | 48 | return &NullifyingWriter{ 49 | Writer: *w, 50 | src: src, 51 | }, nil 52 | } 53 | 54 | // FilterPrograms filters out programs from the source. 55 | func (w *NullifyingWriter) FilterPrograms(predicates ...func(*elf.Prog) bool) { 56 | w.progPredicates = append(w.progPredicates, predicates...) 57 | } 58 | 59 | // KeepSections keeps only the sections that match the predicates. 60 | // If no predicates are given, all sections are nullified. 61 | func (w *NullifyingWriter) KeepSections(predicates ...func(*elf.Section) bool) { 62 | w.sectionPredicates = append(w.sectionPredicates, predicates...) 63 | } 64 | 65 | func newNullifyingWriterSectionReader(src io.ReaderAt) sectionReaderProviderFn { 66 | return func(sec elf.Section) (io.Reader, error) { 67 | if sec.Type == elf.SHT_NOBITS { 68 | return nil, nil 69 | } 70 | return io.NewSectionReader(src, int64(sec.Offset), int64(sec.FileSize)), nil 71 | } 72 | } 73 | 74 | func (w *NullifyingWriter) Flush() error { 75 | if len(w.progPredicates) > 0 { 76 | newProgs := []*elf.Prog{} 77 | for _, prog := range w.progs { 78 | if match(prog, w.progPredicates...) { 79 | newProgs = append(newProgs, prog) 80 | } 81 | } 82 | w.progs = newProgs 83 | } 84 | 85 | for _, sec := range w.sections { 86 | if match(sec, w.sectionPredicates...) || 87 | sec.Type == elf.SHT_NOBITS || sec.Type == elf.SHT_NULL || isSectionStringTable(sec) { 88 | continue 89 | } 90 | sec.Type = elf.SHT_NOBITS 91 | } 92 | 93 | return w.Writer.Flush() 94 | } 95 | -------------------------------------------------------------------------------- /reporter/elfwriter/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | // 14 | 15 | package elfwriter 16 | 17 | type Option func(w *Writer) 18 | 19 | // WithCompressDWARFSections compresses DWARF sections. 20 | func WithCompressDWARFSections() Option { 21 | return func(w *Writer) { 22 | w.compressDWARFSections = true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /reporter/grpc_upload_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package reporter 15 | 16 | import ( 17 | "bufio" 18 | "context" 19 | "errors" 20 | "fmt" 21 | "io" 22 | 23 | debuginfogrpc "buf.build/gen/go/parca-dev/parca/grpc/go/parca/debuginfo/v1alpha1/debuginfov1alpha1grpc" 24 | debuginfopb "buf.build/gen/go/parca-dev/parca/protocolbuffers/go/parca/debuginfo/v1alpha1" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/codes" 27 | "google.golang.org/grpc/status" 28 | ) 29 | 30 | var ErrDebuginfoAlreadyExists = errors.New("debug info already exists") 31 | 32 | const ( 33 | // ChunkSize 8MB is the size of the chunks in which debuginfo files are 34 | // uploaded and downloaded. AWS S3 has a minimum of 5MB for multi-part uploads 35 | // and a maximum of 15MB, and a default of 8MB. 36 | ChunkSize = 1024 * 1024 * 8 37 | // MaxMsgSize is the maximum message size the server can receive or send. By default, it is 64MB. 38 | MaxMsgSize = 1024 * 1024 * 64 39 | ) 40 | 41 | type GrpcDebuginfoUploadServiceClient interface { 42 | Upload(ctx context.Context, opts ...grpc.CallOption) (debuginfogrpc.DebuginfoService_UploadClient, error) 43 | } 44 | 45 | type GrpcUploadClient struct { 46 | GrpcDebuginfoUploadServiceClient 47 | } 48 | 49 | func NewGrpcUploadClient(client GrpcDebuginfoUploadServiceClient) *GrpcUploadClient { 50 | return &GrpcUploadClient{client} 51 | } 52 | 53 | func (c *GrpcUploadClient) Upload(ctx context.Context, uploadInstructions *debuginfopb.UploadInstructions, r io.Reader) (uint64, error) { 54 | return c.grpcUpload(ctx, uploadInstructions, r) 55 | } 56 | 57 | func (c *GrpcUploadClient) grpcUpload(ctx context.Context, uploadInstructions *debuginfopb.UploadInstructions, r io.Reader) (uint64, error) { 58 | stream, err := c.GrpcDebuginfoUploadServiceClient.Upload(ctx, grpc.MaxCallSendMsgSize(MaxMsgSize)) 59 | if err != nil { 60 | return 0, fmt.Errorf("initiate upload: %w", err) 61 | } 62 | 63 | err = stream.Send(&debuginfopb.UploadRequest{ 64 | Data: &debuginfopb.UploadRequest_Info{ 65 | Info: &debuginfopb.UploadInfo{ 66 | UploadId: uploadInstructions.UploadId, 67 | BuildId: uploadInstructions.BuildId, 68 | Type: uploadInstructions.Type, 69 | }, 70 | }, 71 | }) 72 | if err != nil { 73 | if err := sentinelError(err); err != nil { 74 | return 0, err 75 | } 76 | return 0, fmt.Errorf("send upload info: %w", err) 77 | } 78 | 79 | reader := bufio.NewReader(r) 80 | 81 | buffer := make([]byte, ChunkSize) 82 | 83 | bytesSent := 0 84 | for { 85 | n, err := reader.Read(buffer) 86 | if errors.Is(err, io.EOF) { 87 | break 88 | } 89 | if err != nil { 90 | return 0, fmt.Errorf("read next chunk (%d bytes sent so far): %w", bytesSent, err) 91 | } 92 | 93 | err = stream.Send(&debuginfopb.UploadRequest{ 94 | Data: &debuginfopb.UploadRequest_ChunkData{ 95 | ChunkData: buffer[:n], 96 | }, 97 | }) 98 | bytesSent += n 99 | if errors.Is(err, io.EOF) { 100 | // When the stream is closed, the server will send an EOF. 101 | // To get the correct error code, we need the status. 102 | // So receive the message and check the status. 103 | err = stream.RecvMsg(nil) 104 | if err := sentinelError(err); err != nil { 105 | return 0, err 106 | } 107 | return 0, fmt.Errorf("send chunk: %w", err) 108 | } 109 | if err != nil { 110 | return 0, fmt.Errorf("send next chunk (%d bytes sent so far): %w", bytesSent, err) 111 | } 112 | } 113 | 114 | // It returns io.EOF when the stream completes successfully. 115 | res, err := stream.CloseAndRecv() 116 | if errors.Is(err, io.EOF) { 117 | return res.Size, nil 118 | } 119 | if err != nil { 120 | // On any other error, the stream is aborted and the error contains the RPC status. 121 | if err := sentinelError(err); err != nil { 122 | return 0, err 123 | } 124 | return 0, fmt.Errorf("close and receive: %w", err) 125 | } 126 | return res.Size, nil 127 | } 128 | 129 | // sentinelError checks underlying error for grpc.StatusCode and returns if it's a known and expected error. 130 | func sentinelError(err error) error { 131 | if sts, ok := status.FromError(err); ok { 132 | if sts.Code() == codes.AlreadyExists { 133 | return ErrDebuginfoAlreadyExists 134 | } 135 | if sts.Code() == codes.FailedPrecondition { 136 | return err 137 | } 138 | } 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /reporter/metadata/agent.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "go.opentelemetry.io/ebpf-profiler/libpf" 5 | "github.com/prometheus/prometheus/model/labels" 6 | ) 7 | 8 | type agentMetadataProvider struct { 9 | revision string 10 | } 11 | 12 | func NewAgentMetadataProvider(revision string) MetadataProvider { 13 | return &agentMetadataProvider{revision: revision} 14 | } 15 | 16 | func (p *agentMetadataProvider) AddMetadata(_ libpf.PID, lb *labels.Builder) bool { 17 | lb.Set("__meta_agent_revision", p.revision) 18 | return true 19 | } 20 | -------------------------------------------------------------------------------- /reporter/metadata/process.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | 14 | lru "github.com/elastic/go-freelru" 15 | "go.opentelemetry.io/ebpf-profiler/libpf" 16 | "github.com/prometheus/prometheus/model/labels" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | var ErrFileParse = errors.New("Error Parsing File") 21 | 22 | // ExecInfo enriches an executable with additional metadata. 23 | type ExecInfo struct { 24 | FileName string 25 | BuildID string 26 | Compiler string 27 | Static bool 28 | Stripped bool 29 | } 30 | 31 | // cgroup models one line from /proc/[pid]/cgroup. Each cgroup struct describes the placement of a PID inside a 32 | // specific control hierarchy. The kernel has two cgroup APIs, v1 and v2. v1 has one hierarchy per available resource 33 | // controller, while v2 has one unified hierarchy shared by all controllers. Regardless of v1 or v2, all hierarchies 34 | // contain all running processes, so the question answerable with a cgroup struct is 'where is this process in 35 | // this hierarchy' (where==what path on the specific cgroupfs). By prefixing this path with the mount point of 36 | // *this specific* hierarchy, you can locate the relevant pseudo-files needed to read/set the data for this PID 37 | // in this hierarchy 38 | // 39 | // Also see http://man7.org/linux/man-pages/man7/cgroups.7.html 40 | type cgroup struct { 41 | // HierarchyID that can be matched to a named hierarchy using /proc/cgroups. Cgroups V2 only has one 42 | // hierarchy, so HierarchyID is always 0. For cgroups v1 this is a unique ID number 43 | hierarchyID int 44 | // Controllers using this hierarchy of processes. Controllers are also known as subsystems. For 45 | // Cgroups V2 this may be empty, as all active controllers use the same hierarchy 46 | controllers []string 47 | // Path of this control group, relative to the mount point of the cgroupfs representing this specific 48 | // hierarchy 49 | path string 50 | } 51 | 52 | // parseCgroupString parses each line of the /proc/[pid]/cgroup file 53 | // Line format is hierarchyID:[controller1,controller2]:path. 54 | func parseCgroupString(cgroupStr string) (*cgroup, error) { 55 | var err error 56 | 57 | fields := strings.SplitN(cgroupStr, ":", 3) 58 | if len(fields) < 3 { 59 | return nil, fmt.Errorf("%w: 3+ fields required, found %d fields in cgroup string: %s", ErrFileParse, len(fields), cgroupStr) 60 | } 61 | 62 | cgroup := &cgroup{ 63 | path: fields[2], 64 | controllers: nil, 65 | } 66 | cgroup.hierarchyID, err = strconv.Atoi(fields[0]) 67 | if err != nil { 68 | return nil, fmt.Errorf("%w: hierarchy ID: %q", ErrFileParse, cgroup.hierarchyID) 69 | } 70 | if fields[1] != "" { 71 | ssNames := strings.Split(fields[1], ",") 72 | cgroup.controllers = append(cgroup.controllers, ssNames...) 73 | } 74 | return cgroup, nil 75 | } 76 | 77 | // parseCgroups reads each line of the /proc/[pid]/cgroup file. 78 | func parseCgroups(data []byte) ([]cgroup, error) { 79 | var cgroups []cgroup 80 | scanner := bufio.NewScanner(bytes.NewReader(data)) 81 | for scanner.Scan() { 82 | mountString := scanner.Text() 83 | parsedMounts, err := parseCgroupString(mountString) 84 | if err != nil { 85 | return nil, err 86 | } 87 | cgroups = append(cgroups, *parsedMounts) 88 | } 89 | 90 | err := scanner.Err() 91 | return cgroups, err 92 | } 93 | 94 | // readFileNoStat uses io.ReadAll to read contents of entire file. 95 | // This is similar to os.ReadFile but without the call to os.Stat, because 96 | // many files in /proc and /sys report incorrect file sizes (either 0 or 4096). 97 | // Reads a max file size of 1024kB. For files larger than this, a scanner 98 | // should be used. 99 | func readFileNoStat(filename string) ([]byte, error) { 100 | const maxBufferSize = 1024 * 1024 101 | 102 | f, err := os.Open(filename) 103 | if err != nil { 104 | return nil, err 105 | } 106 | defer f.Close() 107 | 108 | reader := io.LimitReader(f, maxBufferSize) 109 | return io.ReadAll(reader) 110 | } 111 | 112 | // findContainerGroup returns the cgroup with the cpu controller or first systemd slice cgroup. 113 | func findContainerGroup(cgroups []cgroup) cgroup { 114 | // If only 1 cgroup, simply return it 115 | if len(cgroups) == 1 { 116 | return cgroups[0] 117 | } 118 | 119 | for _, cg := range cgroups { 120 | // Find first cgroup v1 with cpu controller 121 | for _, ctlr := range cg.controllers { 122 | if ctlr == "cpu" { 123 | return cg 124 | } 125 | } 126 | 127 | // Find first systemd slice 128 | // https://systemd.io/CGROUP_DELEGATION/#systemds-unit-types 129 | if strings.HasPrefix(cg.path, "/system.slice/") || strings.HasPrefix(cg.path, "/user.slice/") { 130 | return cg 131 | } 132 | 133 | // FIXME: what are we looking for here? 134 | // https://systemd.io/CGROUP_DELEGATION/#controller-support 135 | for _, ctlr := range cg.controllers { 136 | if strings.Contains(ctlr, "systemd") { 137 | return cg 138 | } 139 | } 140 | } 141 | 142 | return cgroup{} 143 | } 144 | 145 | type process int32 146 | 147 | func (p process) path(path string) string { 148 | return filepath.Join("/proc", strconv.Itoa(int(p)), path) 149 | } 150 | 151 | func (p process) readMainExecutableFileID() (libpf.FileID, error) { 152 | return libpf.FileIDFromExecutableFile(p.path("exe")) 153 | } 154 | 155 | type mainExecutableMetadataProvider struct { 156 | executableCache *lru.SyncedLRU[libpf.FileID, ExecInfo] 157 | } 158 | 159 | // NewMainExecutableMetadataProvider creates a new mainExecutableMetadataProvider. 160 | func NewMainExecutableMetadataProvider( 161 | executableCache *lru.SyncedLRU[libpf.FileID, ExecInfo], 162 | ) MetadataProvider { 163 | return &mainExecutableMetadataProvider{ 164 | executableCache: executableCache, 165 | } 166 | } 167 | 168 | // AddMetadata adds metadata labels for the main executable of a process to the given labels.Builder. 169 | func (p *mainExecutableMetadataProvider) AddMetadata( 170 | pid libpf.PID, 171 | lb *labels.Builder, 172 | ) bool { 173 | cacheable := true 174 | 175 | fileID, err := process(pid).readMainExecutableFileID() 176 | if err != nil { 177 | log.Debugf("Failed to get fileID for PID %d: %v", pid, err) 178 | cacheable = false 179 | } 180 | lb.Set("__meta_process_executable_file_id", fileID.StringNoQuotes()) 181 | 182 | mainExecInfo, exists := p.executableCache.Get(fileID) 183 | if !exists { 184 | log.Debugf("Failed to get main executable metadata for PID %d, continuing but metadata might be incomplete", pid) 185 | cacheable = false 186 | } 187 | 188 | lb.Set("__meta_process_executable_name", mainExecInfo.FileName) 189 | lb.Set("__meta_process_executable_build_id", mainExecInfo.BuildID) 190 | lb.Set("__meta_process_executable_compiler", mainExecInfo.Compiler) 191 | lb.Set("__meta_process_executable_static", strconv.FormatBool(mainExecInfo.Static)) 192 | lb.Set("__meta_process_executable_stripped", strconv.FormatBool(mainExecInfo.Stripped)) 193 | 194 | return cacheable 195 | } 196 | 197 | type processMetadataProvider struct{} 198 | 199 | // NewProcessMetadataProvider creates a new processMetadataProvider. 200 | func NewProcessMetadataProvider() MetadataProvider { 201 | return &processMetadataProvider{} 202 | } 203 | 204 | // AddMetadata adds metadata labels for a process to the given labels.Builder. 205 | func (pmp *processMetadataProvider) AddMetadata(pid libpf.PID, lb *labels.Builder) bool { 206 | cache := true 207 | lb.Set("__meta_process_pid", strconv.Itoa(int(pid))) 208 | 209 | p := process(pid) 210 | 211 | cmdline, err := p.cmdline() 212 | if err != nil { 213 | log.Debugf("Failed to get cmdline for PID %d: %v", pid, err) 214 | cache = false 215 | } else { 216 | lb.Set("__meta_process_cmdline", strings.Join(cmdline, " ")) 217 | } 218 | 219 | comm, err := p.comm() 220 | if err != nil { 221 | log.Debugf("Failed to get comm for PID %d: %v", pid, err) 222 | cache = false 223 | } else { 224 | lb.Set("comm", comm) 225 | } 226 | 227 | cgroup, err := p.cgroup() 228 | if err != nil { 229 | log.Debugf("Failed to get cgroups for PID %d: %v", pid, err) 230 | cache = false 231 | } else { 232 | lb.Set("__meta_process_cgroup", cgroup.path) 233 | } 234 | 235 | stat, err := p.stat() 236 | if err != nil { 237 | log.Debugf("Failed to get stat for PID %d: %v", pid, err) 238 | cache = false 239 | } else { 240 | lb.Set("__meta_process_ppid", strconv.Itoa(stat.PPID)) 241 | } 242 | 243 | return cache 244 | } 245 | 246 | // cgroup reads from /proc//cgroups and returns a []*cgroup struct locating this PID in each process 247 | // control hierarchy running on this system. On every system (v1 and v2), all hierarchies contain all processes, 248 | // so the len of the returned struct is equal to the number of active hierarchies on this system. 249 | func (p process) cgroup() (cgroup, error) { 250 | data, err := readFileNoStat(p.path("cgroup")) 251 | if err != nil { 252 | return cgroup{}, err 253 | } 254 | cgroups, err := parseCgroups(data) 255 | if err != nil { 256 | return cgroup{}, err 257 | } 258 | 259 | return findContainerGroup(cgroups), nil 260 | } 261 | 262 | // cmdline reads from /proc//cmdline and returns the command line arguments of this process. 263 | func (p process) cmdline() ([]string, error) { 264 | data, err := readFileNoStat(p.path("cmdline")) 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | if len(data) < 1 { 270 | return []string{}, nil 271 | } 272 | 273 | return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil 274 | } 275 | 276 | // Comm reads from /proc//comm and returns the command name of this process. 277 | func (p process) comm() (string, error) { 278 | data, err := readFileNoStat(p.path("comm")) 279 | if err != nil { 280 | return "", err 281 | } 282 | 283 | return strings.TrimSpace(string(data)), nil 284 | } 285 | 286 | // procStat provides status information about the process, 287 | // read from /proc/[pid]/stat. 288 | type procStat struct { 289 | // The process ID. 290 | PID int 291 | // The filename of the executable. 292 | Comm string 293 | // The process state. 294 | State string 295 | // The PID of the parent of this process. 296 | PPID int 297 | // The process group ID of the process. 298 | PGRP int 299 | // The session ID of the process. 300 | Session int 301 | // The controlling terminal of the process. 302 | TTY int 303 | // The ID of the foreground process group of the controlling terminal of 304 | // the process. 305 | TPGID int 306 | // The kernel flags word of the process. 307 | Flags uint 308 | // The number of minor faults the process has made which have not required 309 | // loading a memory page from disk. 310 | MinFlt uint 311 | // The number of minor faults that the process's waited-for children have 312 | // made. 313 | CMinFlt uint 314 | // The number of major faults the process has made which have required 315 | // loading a memory page from disk. 316 | MajFlt uint 317 | // The number of major faults that the process's waited-for children have 318 | // made. 319 | CMajFlt uint 320 | // Amount of time that this process has been scheduled in user mode, 321 | // measured in clock ticks. 322 | UTime uint 323 | // Amount of time that this process has been scheduled in kernel mode, 324 | // measured in clock ticks. 325 | STime uint 326 | // Amount of time that this process's waited-for children have been 327 | // scheduled in user mode, measured in clock ticks. 328 | CUTime int 329 | // Amount of time that this process's waited-for children have been 330 | // scheduled in kernel mode, measured in clock ticks. 331 | CSTime int 332 | // For processes running a real-time scheduling policy, this is the negated 333 | // scheduling priority, minus one. 334 | Priority int 335 | // The nice value, a value in the range 19 (low priority) to -20 (high 336 | // priority). 337 | Nice int 338 | // Number of threads in this process. 339 | NumThreads int 340 | // The time the process started after system boot, the value is expressed 341 | // in clock ticks. 342 | Starttime uint64 343 | // Virtual memory size in bytes. 344 | VSize uint 345 | // Resident set size in pages. 346 | RSS int 347 | // Soft limit in bytes on the rss of the process. 348 | RSSLimit uint64 349 | // CPU number last executed on. 350 | Processor uint 351 | // Real-time scheduling priority, a number in the range 1 to 99 for processes 352 | // scheduled under a real-time policy, or 0, for non-real-time processes. 353 | RTPriority uint 354 | // Scheduling policy. 355 | Policy uint 356 | // Aggregated block I/O delays, measured in clock ticks (centiseconds). 357 | DelayAcctBlkIOTicks uint64 358 | // Guest time of the process (time spent running a virtual CPU for a guest 359 | // operating system), measured in clock ticks. 360 | GuestTime int 361 | // Guest time of the process's children, measured in clock ticks. 362 | CGuestTime int 363 | } 364 | 365 | // Stat returns the current status information of the process. 366 | func (p process) stat() (procStat, error) { 367 | data, err := readFileNoStat(p.path("stat")) 368 | if err != nil { 369 | return procStat{}, err 370 | } 371 | 372 | var ( 373 | ignoreInt64 int64 374 | ignoreUint64 uint64 375 | 376 | s = procStat{PID: int(p)} 377 | l = bytes.Index(data, []byte("(")) 378 | r = bytes.LastIndex(data, []byte(")")) 379 | ) 380 | 381 | if l < 0 || r < 0 { 382 | return procStat{}, fmt.Errorf("%w: unexpected format, couldn't extract comm %q", ErrFileParse, data) 383 | } 384 | 385 | s.Comm = string(data[l+1 : r]) 386 | 387 | // Check the following resources for the details about the particular stat 388 | // fields and their data types: 389 | // * https://man7.org/linux/man-pages/man5/proc.5.html 390 | // * https://man7.org/linux/man-pages/man3/scanf.3.html 391 | _, err = fmt.Fscan( 392 | bytes.NewBuffer(data[r+2:]), 393 | &s.State, 394 | &s.PPID, 395 | &s.PGRP, 396 | &s.Session, 397 | &s.TTY, 398 | &s.TPGID, 399 | &s.Flags, 400 | &s.MinFlt, 401 | &s.CMinFlt, 402 | &s.MajFlt, 403 | &s.CMajFlt, 404 | &s.UTime, 405 | &s.STime, 406 | &s.CUTime, 407 | &s.CSTime, 408 | &s.Priority, 409 | &s.Nice, 410 | &s.NumThreads, 411 | &ignoreInt64, 412 | &s.Starttime, 413 | &s.VSize, 414 | &s.RSS, 415 | &s.RSSLimit, 416 | &ignoreUint64, 417 | &ignoreUint64, 418 | &ignoreUint64, 419 | &ignoreUint64, 420 | &ignoreUint64, 421 | &ignoreUint64, 422 | &ignoreUint64, 423 | &ignoreUint64, 424 | &ignoreUint64, 425 | &ignoreUint64, 426 | &ignoreUint64, 427 | &ignoreUint64, 428 | &ignoreInt64, 429 | &s.Processor, 430 | &s.RTPriority, 431 | &s.Policy, 432 | &s.DelayAcctBlkIOTicks, 433 | &s.GuestTime, 434 | &s.CGuestTime, 435 | ) 436 | if err != nil { 437 | return procStat{}, err 438 | } 439 | 440 | return s, nil 441 | } 442 | -------------------------------------------------------------------------------- /reporter/metadata/system.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "strings" 5 | "syscall" 6 | 7 | "go.opentelemetry.io/ebpf-profiler/libpf" 8 | "github.com/prometheus/prometheus/model/labels" 9 | ) 10 | 11 | type systemMetadataProvider struct { 12 | kernelMachine string 13 | kernelRelease string 14 | } 15 | 16 | func int8SliceToString(arr []int8) string { 17 | var b strings.Builder 18 | for _, v := range arr { 19 | // NUL byte, as it's a C string. 20 | if v == 0 { 21 | break 22 | } 23 | b.WriteByte(byte(v)) 24 | } 25 | return b.String() 26 | } 27 | 28 | func NewSystemMetadataProvider() (MetadataProvider, error) { 29 | var uname syscall.Utsname 30 | if err := syscall.Uname(&uname); err != nil { 31 | return nil, err 32 | } 33 | 34 | return &systemMetadataProvider{ 35 | kernelMachine: int8SliceToString(uname.Machine[:]), 36 | kernelRelease: int8SliceToString(uname.Release[:]), 37 | }, nil 38 | } 39 | 40 | func (p *systemMetadataProvider) AddMetadata(_ libpf.PID, lb *labels.Builder) bool { 41 | lb.Set("__meta_system_kernel_machine", p.kernelMachine) 42 | lb.Set("__meta_system_kernel_release", p.kernelRelease) 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /reporter/parca_reporter_test.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | const Chinese string = "Go(又稱Golang[4])是Google開發的一种静态强类型、編譯型、并发型,并具有垃圾回收功能的编程语言。" 10 | const Chinese2 string = "Linux是一种自由和开放源码的类Unix操作系统。" 11 | 12 | func TestMaybeFixTruncation(t *testing.T) { 13 | for _, test := range []struct { 14 | s string 15 | result string 16 | ok bool 17 | }{ 18 | {"ASCII string", "ASCII string", true}, 19 | // truncated, but too early -- can't be valid utf8 20 | {Chinese[0:4], "", false}, 21 | // truncated at the limit, in the middle of a rune 22 | {Chinese[0:48], Chinese[0:47], true}, 23 | // Too long string that happened to be 24 | // truncated on a rune boundary 25 | {Chinese2[0:48], Chinese2[0:48], true}, 26 | // Too long string but valid UTF-8 -- 27 | // the function should pass it through unscathed 28 | // (it is not responsible for doing its own truncation) 29 | {Chinese2, Chinese2, true}, 30 | } { 31 | result, ok := maybeFixTruncation(test.s, 48) 32 | require.Equal(t, test.result, result) 33 | require.Equal(t, test.ok, ok) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /reporter/parca_uploader.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "maps" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "sync" 13 | "time" 14 | 15 | debuginfogrpc "buf.build/gen/go/parca-dev/parca/grpc/go/parca/debuginfo/v1alpha1/debuginfov1alpha1grpc" 16 | debuginfopb "buf.build/gen/go/parca-dev/parca/protocolbuffers/go/parca/debuginfo/v1alpha1" 17 | lru "github.com/elastic/go-freelru" 18 | "go.opentelemetry.io/ebpf-profiler/libpf" 19 | "go.opentelemetry.io/ebpf-profiler/process" 20 | log "github.com/sirupsen/logrus" 21 | "golang.org/x/sync/errgroup" 22 | "google.golang.org/grpc/codes" 23 | "google.golang.org/grpc/status" 24 | 25 | "github.com/parca-dev/parca-agent/reporter/elfwriter" 26 | ) 27 | 28 | type uploadRequest struct { 29 | fileID libpf.FileID 30 | buildID string 31 | open func() (process.ReadAtCloser, error) 32 | } 33 | 34 | type ParcaSymbolUploader struct { 35 | client debuginfogrpc.DebuginfoServiceClient 36 | grpcUploadClient *GrpcUploadClient 37 | httpClient *http.Client 38 | 39 | retry *lru.SyncedLRU[libpf.FileID, struct{}] 40 | 41 | stripTextSection bool 42 | tmp string 43 | 44 | queue chan uploadRequest 45 | inProgressTracker *inProgressTracker 46 | workerNum int 47 | } 48 | 49 | func NewParcaSymbolUploader( 50 | client debuginfogrpc.DebuginfoServiceClient, 51 | cacheSize uint32, 52 | stripTextSection bool, 53 | queueSize uint32, 54 | workerNum int, 55 | cacheDir string, 56 | ) (*ParcaSymbolUploader, error) { 57 | retryCache, err := lru.NewSynced[libpf.FileID, struct{}](cacheSize, libpf.FileID.Hash32) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | cacheDirectory := filepath.Join(cacheDir, "symuploader") 63 | if _, err := os.Stat(cacheDirectory); os.IsNotExist(err) { 64 | log.Debugf("Creating cache directory '%s'", cacheDirectory) 65 | if err := os.MkdirAll(cacheDirectory, os.ModePerm); err != nil { 66 | return nil, fmt.Errorf("failed to create cache directory (%s): %s", cacheDirectory, err) 67 | } 68 | } 69 | 70 | if err := filepath.Walk(cacheDirectory, func(path string, info os.FileInfo, err error) error { 71 | if info.IsDir() { 72 | return nil 73 | } 74 | 75 | if os.Remove(path) != nil { 76 | log.Warnf("Failed to remove cached file: %s", path) 77 | } 78 | 79 | return nil 80 | }); err != nil { 81 | return nil, fmt.Errorf("failed to clean cache directory (%s): %s", cacheDirectory, err) 82 | } 83 | 84 | return &ParcaSymbolUploader{ 85 | httpClient: http.DefaultClient, 86 | client: client, 87 | grpcUploadClient: NewGrpcUploadClient(client), 88 | retry: retryCache, 89 | stripTextSection: stripTextSection, 90 | tmp: cacheDirectory, 91 | queue: make(chan uploadRequest, queueSize), 92 | inProgressTracker: newInProgressTracker(0.2), 93 | workerNum: workerNum, 94 | }, nil 95 | } 96 | 97 | const ( 98 | ReasonUploadInProgress = "A previous upload is still in-progress and not stale yet (only stale uploads can be retried)." 99 | ) 100 | 101 | // inProgressTracker is a simple in-progress tracker that keeps track of which 102 | // fileIDs are currently in-progress/enqueued to be uploaded. 103 | type inProgressTracker struct { 104 | mu sync.Mutex 105 | m map[libpf.FileID]struct{} 106 | 107 | // tracking metadata to know when to shrink the map as otherwise the map 108 | // may grow indefinitely. 109 | maxSizeSeen int 110 | shrinkLimitRatio float64 111 | } 112 | 113 | // newInProgressTracker returns a new in-progress tracker that shrinks the 114 | // tracking map when the maximum size seen is larger than the current size by 115 | // the shrinkLimitRatio. 116 | func newInProgressTracker(shrinkLimitRatio float64) *inProgressTracker { 117 | return &inProgressTracker{ 118 | m: make(map[libpf.FileID]struct{}), 119 | shrinkLimitRatio: shrinkLimitRatio, 120 | } 121 | } 122 | 123 | // GetOrAdd returns ensures that the fileID is in the in-progress state. If the 124 | // fileID is already in the in-progress state it returns true. 125 | func (i *inProgressTracker) GetOrAdd(fileID libpf.FileID) (alreadyInProgress bool) { 126 | i.mu.Lock() 127 | defer i.mu.Unlock() 128 | 129 | _, alreadyInProgress = i.m[fileID] 130 | i.m[fileID] = struct{}{} 131 | 132 | if len(i.m) > i.maxSizeSeen { 133 | i.maxSizeSeen = len(i.m) 134 | } 135 | 136 | return 137 | } 138 | 139 | // Remove removes the fileID from the in-progress state. 140 | func (i *inProgressTracker) Remove(fileID libpf.FileID) { 141 | i.mu.Lock() 142 | defer i.mu.Unlock() 143 | 144 | delete(i.m, fileID) 145 | 146 | if i.shrinkLimitRatio > 0 && 147 | int(float64(len(i.m))+float64(len(i.m))*i.shrinkLimitRatio) < i.maxSizeSeen { 148 | i.m = maps.Clone(i.m) 149 | i.maxSizeSeen = len(i.m) 150 | } 151 | } 152 | 153 | // Start starts the upload workers. 154 | func (u *ParcaSymbolUploader) Run(ctx context.Context) error { 155 | var g errgroup.Group 156 | 157 | for i := 0; i < u.workerNum; i++ { 158 | g.Go(func() error { 159 | for { 160 | select { 161 | case <-ctx.Done(): 162 | return nil 163 | case req := <-u.queue: 164 | if err := u.attemptUpload(ctx, req.fileID, req.buildID, req.open); err != nil { 165 | log.Warnf("Failed to upload with file ID %q and build ID %q: %v", req.fileID.StringNoQuotes(), req.buildID, err) 166 | } 167 | } 168 | } 169 | }) 170 | } 171 | 172 | return g.Wait() 173 | } 174 | 175 | // Upload enqueues a file for upload if it's not already in progress, or if it 176 | // is marked not to be retried. 177 | func (u *ParcaSymbolUploader) Upload(ctx context.Context, fileID libpf.FileID, buildID string, 178 | open func() (process.ReadAtCloser, error)) { 179 | _, ok := u.retry.Get(fileID) 180 | if ok { 181 | return 182 | } 183 | 184 | // Attempting to enqueue each fileID only once. 185 | alreadyInProgress := u.inProgressTracker.GetOrAdd(fileID) 186 | if alreadyInProgress { 187 | return 188 | } 189 | 190 | select { 191 | case <-ctx.Done(): 192 | u.inProgressTracker.Remove(fileID) 193 | case u.queue <- uploadRequest{fileID: fileID, buildID: buildID, open: open}: 194 | // Nothing to do, we enqueued the request successfully. 195 | default: 196 | // The queue is full, we can't enqueue the request. 197 | u.inProgressTracker.Remove(fileID) 198 | log.Warnf("Failed to enqueue upload request with file ID %q and build ID %q: queue is full", fileID.StringNoQuotes(), buildID) 199 | } 200 | } 201 | 202 | // attemptUpload attempts to upload the file with the given fileID and buildID. 203 | func (u *ParcaSymbolUploader) attemptUpload(ctx context.Context, fileID libpf.FileID, buildID string, 204 | open func() (process.ReadAtCloser, error)) error { 205 | defer u.inProgressTracker.Remove(fileID) 206 | 207 | buildIDType := debuginfopb.BuildIDType_BUILD_ID_TYPE_GNU 208 | if buildID == "" { 209 | buildIDType = debuginfopb.BuildIDType_BUILD_ID_TYPE_HASH 210 | buildID = fileID.StringNoQuotes() 211 | } 212 | 213 | shouldInitiateUploadResp, err := u.client.ShouldInitiateUpload(ctx, &debuginfopb.ShouldInitiateUploadRequest{ 214 | BuildId: buildID, 215 | BuildIdType: buildIDType, 216 | Type: debuginfopb.DebuginfoType_DEBUGINFO_TYPE_DEBUGINFO_UNSPECIFIED, 217 | }) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | if !shouldInitiateUploadResp.ShouldInitiateUpload { 223 | // This can happen when two agents simultaneously try to upload the 224 | // same file. The other agent already started the upload so we don't 225 | // need to do it again, however the upload may fail so we should retry 226 | // after a while. 227 | if shouldInitiateUploadResp.Reason == ReasonUploadInProgress { 228 | u.retry.AddWithLifetime(fileID, struct{}{}, 5*time.Minute) 229 | return nil 230 | } 231 | u.retry.Add(fileID, struct{}{}) 232 | return nil 233 | } 234 | 235 | var ( 236 | r io.Reader 237 | size int64 238 | ) 239 | if !u.stripTextSection { 240 | // We're not stripping the text section so we can upload the original file. 241 | f, err := open() 242 | if err != nil { 243 | if os.IsNotExist(err) { 244 | // File doesn't exist, likely because the process is already 245 | // gone. 246 | return nil 247 | } 248 | if err.Error() == "no backing file for anonymous memory" { 249 | // This is an anonymous memory mapping, it's not backed by 250 | // a file so we will never be able to extract debuginfo. 251 | u.retry.Add(fileID, struct{}{}) 252 | return nil 253 | } 254 | return fmt.Errorf("open file: %w", err) 255 | } 256 | defer f.Close() 257 | 258 | size, err := readAtCloserSize(f) 259 | if err != nil { 260 | return err 261 | } 262 | if size == 0 { 263 | // The original file is empty no need to ever upload it. 264 | u.retry.Add(fileID, struct{}{}) 265 | return nil 266 | } 267 | 268 | r = io.NewSectionReader(f, 0, size) 269 | } else { 270 | f, err := os.Create(filepath.Join(u.tmp, fileID.StringNoQuotes())) 271 | if err != nil { 272 | os.Remove(f.Name()) 273 | return fmt.Errorf("create file: %w", err) 274 | } 275 | defer os.Remove(f.Name()) 276 | defer f.Close() 277 | 278 | original, err := open() 279 | if err != nil { 280 | os.Remove(f.Name()) 281 | if os.IsNotExist(err) { 282 | // Original file doesn't exist the process is likely 283 | // already gone. 284 | return nil 285 | } 286 | if err.Error() == "no backing file for anonymous memory" { 287 | // This is an anonymous memory mapping, it's not backed by 288 | // a file so we will never be able to extract debuginfo. 289 | u.retry.Add(fileID, struct{}{}) 290 | return nil 291 | } 292 | return fmt.Errorf("open original file: %w", err) 293 | } 294 | defer original.Close() 295 | 296 | if err := elfwriter.OnlyKeepDebug(f, original); err != nil { 297 | os.Remove(f.Name()) 298 | // If we can't extract the debuginfo we can't upload the file. 299 | u.retry.Add(fileID, struct{}{}) 300 | return fmt.Errorf("extract debuginfo: %w", err) 301 | } 302 | 303 | if _, err := f.Seek(0, io.SeekStart); err != nil { 304 | os.Remove(f.Name()) 305 | // Something is probably seriously wrong so don't retry. 306 | u.retry.Add(fileID, struct{}{}) 307 | return fmt.Errorf("seek extracted debuginfo to start: %w", err) 308 | } 309 | 310 | stat, err := f.Stat() 311 | if err != nil { 312 | os.Remove(f.Name()) 313 | // Something is probably seriously wrong so don't retry. 314 | u.retry.Add(fileID, struct{}{}) 315 | return fmt.Errorf("stat file to upload: %w", err) 316 | } 317 | size = stat.Size() 318 | 319 | if size == 0 { 320 | os.Remove(f.Name()) 321 | // Extraction is a deterministic process so if the file is empty we 322 | // will never be able to extract non-zero debuginfo the original 323 | // binary. 324 | u.retry.Add(fileID, struct{}{}) 325 | return nil 326 | } 327 | 328 | r = f 329 | } 330 | 331 | initiateUploadResp, err := u.client.InitiateUpload(ctx, &debuginfopb.InitiateUploadRequest{ 332 | BuildId: buildID, 333 | BuildIdType: buildIDType, 334 | Type: debuginfopb.DebuginfoType_DEBUGINFO_TYPE_DEBUGINFO_UNSPECIFIED, 335 | Hash: fileID.StringNoQuotes(), 336 | Size: size, 337 | }) 338 | if err != nil { 339 | if status.Code(err) == codes.FailedPrecondition { 340 | // This is a race that can happen when multiple agents are trying 341 | // to upload the same file. This happens when another upload is 342 | // still in progress. Since we don't know if it will succeed or not 343 | // we retry after a while. 344 | u.retry.AddWithLifetime(fileID, struct{}{}, 5*time.Minute) 345 | return nil 346 | } 347 | if status.Code(err) == codes.AlreadyExists { 348 | // This is a race that can happen when multiple agents are trying 349 | // to upload the same file. The other upload already succeeded so 350 | // we don't need to upload it again. 351 | u.retry.Add(fileID, struct{}{}) 352 | return nil 353 | } 354 | if status.Code(err) == codes.InvalidArgument { 355 | // This will never succeed, no need to retry. 356 | u.retry.Add(fileID, struct{}{}) 357 | return nil 358 | } 359 | return err 360 | } 361 | 362 | if initiateUploadResp.UploadInstructions == nil { 363 | u.retry.Add(fileID, struct{}{}) 364 | return nil 365 | } 366 | 367 | instructions := initiateUploadResp.UploadInstructions 368 | switch instructions.UploadStrategy { 369 | case debuginfopb.UploadInstructions_UPLOAD_STRATEGY_SIGNED_URL: 370 | if err := u.uploadViaSignedURL(ctx, instructions.SignedUrl, r, size); err != nil { 371 | return err 372 | } 373 | case debuginfopb.UploadInstructions_UPLOAD_STRATEGY_GRPC: 374 | if _, err := u.grpcUploadClient.Upload(ctx, instructions, r); err != nil { 375 | return err 376 | } 377 | default: 378 | // No clue what to do with this upload strategy. 379 | log.Warnf("Unknown upload strategy: %v", instructions.UploadStrategy) 380 | u.retry.Add(fileID, struct{}{}) 381 | return nil 382 | } 383 | 384 | _, err = u.client.MarkUploadFinished(ctx, &debuginfopb.MarkUploadFinishedRequest{ 385 | BuildId: buildID, 386 | UploadId: initiateUploadResp.UploadInstructions.UploadId, 387 | }) 388 | if err != nil { 389 | return err 390 | } 391 | 392 | u.retry.Add(fileID, struct{}{}) 393 | return nil 394 | } 395 | 396 | type Stater interface { 397 | Stat() (os.FileInfo, error) 398 | } 399 | 400 | // readAtCloserSize attempts to determine the size of the reader. 401 | func readAtCloserSize(r process.ReadAtCloser) (int64, error) { 402 | stater, ok := r.(Stater) 403 | if !ok { 404 | log.Debugf("ReadAtCloser is not a Stater, can't determine size") 405 | return 0, nil 406 | } 407 | 408 | stat, err := stater.Stat() 409 | if err != nil { 410 | return 0, fmt.Errorf("stat file to upload: %w", err) 411 | } 412 | 413 | return stat.Size(), nil 414 | } 415 | 416 | // uploadViaSignedURL uploads the reader to the signed URL. 417 | func (u *ParcaSymbolUploader) uploadViaSignedURL(ctx context.Context, url string, r io.Reader, size int64) error { 418 | // Client is closing the reader if the reader is also closer. 419 | // We need to wrap the reader to avoid this. 420 | // We want to have total control over the reader. 421 | r = bufio.NewReader(r) 422 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, r) 423 | if err != nil { 424 | return fmt.Errorf("create request: %w", err) 425 | } 426 | 427 | req.ContentLength = size 428 | resp, err := u.httpClient.Do(req) 429 | if err != nil { 430 | return fmt.Errorf("do upload request: %w", err) 431 | } 432 | defer func() { 433 | _, _ = io.Copy(io.Discard, resp.Body) 434 | _ = resp.Body.Close() 435 | }() 436 | 437 | if resp.StatusCode/100 != 2 { 438 | data, _ := io.ReadAll(resp.Body) 439 | return fmt.Errorf("unexpected status code: %d, msg: %s", resp.StatusCode, string(data)) 440 | } 441 | 442 | return nil 443 | } 444 | -------------------------------------------------------------------------------- /reporter/parca_uploader_test.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "go.opentelemetry.io/ebpf-profiler/libpf" 8 | ) 9 | 10 | func TestMapShrink(t *testing.T) { 11 | tr := newInProgressTracker(0.2) 12 | r := rand.New(rand.NewSource(0)) 13 | 14 | items := make([]libpf.FileID, 100) 15 | for i := 0; i < 100; i++ { 16 | items[i] = libpf.NewFileID( 17 | r.Uint64(), 18 | r.Uint64(), 19 | ) 20 | 21 | tr.GetOrAdd(items[i]) 22 | } 23 | 24 | if tr.maxSizeSeen != 100 { 25 | t.Errorf("expected 100, got %d", tr.maxSizeSeen) 26 | } 27 | 28 | for i := 0; i < 10; i++ { 29 | tr.Remove(items[i]) 30 | } 31 | 32 | if tr.maxSizeSeen != 100 { 33 | t.Errorf("expected 100, got %d", tr.maxSizeSeen) 34 | } 35 | 36 | for i := 10; i < 20; i++ { 37 | tr.Remove(items[i]) 38 | } 39 | 40 | if tr.maxSizeSeen != 83 { 41 | t.Errorf("expected 83, got %d", tr.maxSizeSeen) 42 | } 43 | 44 | // adding up to 83 doesn't change anything 45 | for i := 10; i < 13; i++ { 46 | tr.GetOrAdd(items[i]) 47 | } 48 | 49 | if tr.maxSizeSeen != 83 { 50 | t.Errorf("expected 83, got %d", tr.maxSizeSeen) 51 | } 52 | 53 | // adding 84th item should increases the max size 54 | tr.GetOrAdd(items[13]) 55 | 56 | if tr.maxSizeSeen != 84 { 57 | t.Errorf("expected 84, got %d", tr.maxSizeSeen) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /snap/README.md: -------------------------------------------------------------------------------- 1 | # Parca Agent Snap 2 | 3 | This directory contains files used to build the [Parca Agent](https://parca.dev) snap. 4 | 5 | ## Parca Agent App 6 | 7 | The snap provides a base `parca-agent` app, which can be executed as per the upstream 8 | documentation. 9 | 10 | You can start Parca Agent manually like so: 11 | 12 | ```bash 13 | # Install from the 'edge' channel 14 | $ sudo snap install parca-agent --channel edge 15 | 16 | # Start the agent with simple defaults for testing 17 | parca-agent --node="foobar" --remote-store-address="localhost:7070" --remote-store-insecure 18 | ``` 19 | 20 | ## Parca Agent Service 21 | 22 | Additionally, the snap provides a service for Parca Agent with a limited set of configuration 23 | options. You can start the service like so: 24 | 25 | ```bash 26 | $ snap start parca-agent 27 | ``` 28 | 29 | There are a small number of config options: 30 | 31 | | Name | Valid Options | Default | Description | 32 | | :---------------------- | :------------------------------- | :--------------- | :----------------------------------------------------------- | 33 | | `node` | Any string | `$(hostname)` | Name node the process is running on. | 34 | | `log-level` | `error`, `warn`, `info`, `debug` | `info` | Log level for Parca Agent. | 35 | | `http-address` | Any string | `:7071` | Address for HTTP server to bind to. | 36 | | `remote-store-address` | Any string | `localhost:7071` | Remote store (gRPC) address to send profiles and symbols to. | 37 | | `remote-store-insecure` | `true`, `false` | `false` | Send gRPC requests via plaintext instead of TLS. | 38 | | `config-path` | Any string | `` | Path to config file. | 39 | 40 | Config options can be set with `sudo snap set parca-agent