├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build-docker.yaml │ ├── ci-docker.yaml │ ├── release-assets.yaml │ ├── release-docker.yaml │ └── schedule-docker.yaml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── azure-devops-client ├── agentpool.go ├── build.go ├── general.go ├── main.go ├── misc.go ├── project.go ├── pullrequest.go ├── query.go ├── release.go ├── release_definition.go ├── release_deployment.go ├── repository.go ├── resource_usage.go └── workitem.go ├── common.logger.go ├── common.system.go ├── compose.yaml ├── config └── opts.go ├── go.mod ├── go.sum ├── main.go ├── metrics_agentpool.go ├── metrics_build.go ├── metrics_deployment.go ├── metrics_latest_build.go ├── metrics_project.go ├── metrics_pullrequest.go ├── metrics_query.go ├── metrics_release.go ├── metrics_repository.go ├── metrics_resourceusage.go ├── metrics_stats.go ├── misc.go └── servicediscovery.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /azure-devops-exporter 2 | /release-assets 3 | .env -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [{*.yml,*.yaml}] 18 | indent_size = 2 19 | 20 | [*.conf] 21 | indent_size = 2 22 | 23 | [*.go] 24 | indent_size = 4 25 | indent_style = tab 26 | ij_continuation_indent_size = 4 27 | ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true 28 | ij_go_add_leading_space_to_comments = true 29 | ij_go_add_parentheses_for_single_import = true 30 | ij_go_call_parameters_new_line_after_left_paren = true 31 | ij_go_call_parameters_right_paren_on_new_line = true 32 | ij_go_call_parameters_wrap = off 33 | ij_go_fill_paragraph_width = 80 34 | ij_go_group_stdlib_imports = true 35 | ij_go_import_sorting = goimports 36 | ij_go_keep_indents_on_empty_lines = false 37 | ij_go_local_group_mode = project 38 | ij_go_move_all_imports_in_one_declaration = true 39 | ij_go_move_all_stdlib_imports_in_one_group = true 40 | ij_go_remove_redundant_import_aliases = false 41 | ij_go_run_go_fmt_on_reformat = true 42 | ij_go_use_back_quotes_for_imports = false 43 | ij_go_wrap_comp_lit = off 44 | ij_go_wrap_comp_lit_newline_after_lbrace = true 45 | ij_go_wrap_comp_lit_newline_before_rbrace = true 46 | ij_go_wrap_func_params = off 47 | ij_go_wrap_func_params_newline_after_lparen = true 48 | ij_go_wrap_func_params_newline_before_rparen = true 49 | ij_go_wrap_func_result = off 50 | ij_go_wrap_func_result_newline_after_lparen = true 51 | ij_go_wrap_func_result_newline_before_rparen = true 52 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | all-github-actions: 9 | patterns: [ "*" ] 10 | 11 | - package-ecosystem: "docker" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | groups: 16 | all-docker-versions: 17 | patterns: [ "*" ] 18 | 19 | - package-ecosystem: "gomod" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | groups: 24 | all-go-mod-patch-and-minor: 25 | patterns: [ "*" ] 26 | update-types: [ "patch", "minor" ] 27 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | name: build/docker 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | publish: 7 | required: true 8 | type: boolean 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set Swap Space 17 | uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c 18 | with: 19 | swap-size-gb: 12 20 | 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version-file: 'go.mod' 24 | cache-dependency-path: "go.sum" 25 | check-latest: true 26 | 27 | - name: Run Golangci lint 28 | uses: golangci/golangci-lint-action@v7 29 | with: 30 | version: latest 31 | args: --print-resources-usage 32 | 33 | build: 34 | name: "build ${{ matrix.Dockerfile }}:${{ matrix.target }}" 35 | needs: lint 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | include: 40 | - Dockerfile: Dockerfile 41 | target: "final-static" 42 | suffix: "" 43 | latest: "auto" 44 | 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Set Swap Space 50 | uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c 51 | with: 52 | swap-size-gb: 12 53 | 54 | - uses: actions/setup-go@v5 55 | with: 56 | go-version-file: 'go.mod' 57 | cache-dependency-path: "go.sum" 58 | check-latest: true 59 | 60 | - name: Docker meta 61 | id: docker_meta 62 | uses: docker/metadata-action@v5 63 | with: 64 | images: | 65 | ${{ github.repository }} 66 | quay.io/${{ github.repository }} 67 | labels: | 68 | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.repository.default_branch }}/README.md 69 | flavor: | 70 | latest=${{ matrix.latest }} 71 | suffix=${{ matrix.suffix }} 72 | 73 | - name: Set up QEMU 74 | uses: docker/setup-qemu-action@v3 75 | 76 | - name: Set up Docker Buildx 77 | uses: docker/setup-buildx-action@v3 78 | 79 | - name: Login to DockerHub 80 | uses: docker/login-action@v3 81 | if: ${{ inputs.publish }} 82 | with: 83 | username: ${{ secrets.DOCKERHUB_USERNAME }} 84 | password: ${{ secrets.DOCKERHUB_TOKEN }} 85 | 86 | - name: Login to Quay 87 | uses: docker/login-action@v3 88 | if: ${{ inputs.publish }} 89 | with: 90 | registry: quay.io 91 | username: ${{ secrets.QUAY_USERNAME }} 92 | password: ${{ secrets.QUAY_TOKEN }} 93 | 94 | - name: ${{ inputs.publish && 'Build and push' || 'Build' }} 95 | uses: docker/build-push-action@v6 96 | with: 97 | context: . 98 | file: ./${{ matrix.Dockerfile }} 99 | target: ${{ matrix.target }} 100 | platforms: linux/amd64,linux/arm64 101 | push: ${{ inputs.publish }} 102 | tags: ${{ steps.docker_meta.outputs.tags }} 103 | labels: ${{ steps.docker_meta.outputs.labels }} 104 | -------------------------------------------------------------------------------- /.github/workflows/ci-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "ci/docker" 2 | 3 | on: [pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | uses: ./.github/workflows/build-docker.yaml 8 | secrets: inherit 9 | with: 10 | publish: false 11 | -------------------------------------------------------------------------------- /.github/workflows/release-assets.yaml: -------------------------------------------------------------------------------- 1 | name: "release/assets" 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set Swap Space 14 | uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c 15 | with: 16 | swap-size-gb: 12 17 | 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version-file: 'go.mod' 21 | cache-dependency-path: "go.sum" 22 | check-latest: true 23 | 24 | - name: Build 25 | run: | 26 | make release-assets 27 | 28 | - name: Upload assets to release 29 | uses: svenstaro/upload-release-action@v2 30 | with: 31 | repo_token: ${{ secrets.GITHUB_TOKEN }} 32 | file: ./release-assets/* 33 | tag: ${{ github.ref }} 34 | overwrite: true 35 | file_glob: true 36 | -------------------------------------------------------------------------------- /.github/workflows/release-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "release/docker" 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'feature-**' 8 | - 'bugfix-**' 9 | tags: 10 | - '*.*.*' 11 | 12 | jobs: 13 | release: 14 | uses: ./.github/workflows/build-docker.yaml 15 | secrets: inherit 16 | with: 17 | publish: ${{ github.event_name != 'pull_request' }} 18 | -------------------------------------------------------------------------------- /.github/workflows/schedule-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "schedule/docker" 2 | 3 | on: 4 | schedule: 5 | - cron: '45 6 * * 1' 6 | 7 | jobs: 8 | schedule: 9 | uses: ./.github/workflows/build-docker.yaml 10 | secrets: inherit 11 | with: 12 | publish: true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /release-assets 3 | /azure-devops-exporter 4 | *.exe 5 | 6 | # VSCode 7 | .vscode/ 8 | __debug_* 9 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asciicheck 5 | - bidichk 6 | - bodyclose 7 | - copyloopvar 8 | - errorlint 9 | - gomodguard 10 | - gosec 11 | settings: 12 | gomodguard: 13 | blocked: 14 | modules: 15 | - github.com/Azure/go-autorest/autorest/azure/auth: 16 | reason: deprecated 17 | gosec: 18 | confidence: low 19 | config: 20 | global: 21 | audit: true 22 | exclusions: 23 | generated: lax 24 | presets: 25 | - comments 26 | - common-false-positives 27 | - legacy 28 | - std-error-handling 29 | paths: 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | formatters: 34 | enable: 35 | - gofmt 36 | - goimports 37 | exclusions: 38 | generated: lax 39 | paths: 40 | - third_party$ 41 | - builtin$ 42 | - examples$ 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################# 2 | # Build 3 | ############################################# 4 | FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS build 5 | 6 | RUN apk upgrade --no-cache --force 7 | RUN apk add --update build-base make git 8 | 9 | WORKDIR /go/src/github.com/webdevops/azure-devops-exporter 10 | 11 | # Dependencies 12 | COPY go.mod go.sum . 13 | RUN go mod download 14 | 15 | # Compile 16 | COPY . . 17 | RUN make test 18 | ARG TARGETOS TARGETARCH 19 | RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make build 20 | 21 | ############################################# 22 | # Test 23 | ############################################# 24 | FROM gcr.io/distroless/static AS test 25 | USER 0:0 26 | WORKDIR /app 27 | COPY --from=build /go/src/github.com/webdevops/azure-devops-exporter/azure-devops-exporter . 28 | RUN ["./azure-devops-exporter", "--help"] 29 | 30 | ############################################# 31 | # Final-static 32 | ############################################# 33 | FROM gcr.io/distroless/static AS final-static 34 | ENV LOG_JSON=1 35 | WORKDIR / 36 | COPY --from=test /app . 37 | USER 1000:1000 38 | ENTRYPOINT ["/azure-devops-exporter"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2021 WebDevOps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := $(shell basename $(CURDIR)) 2 | GIT_TAG := $(shell git describe --dirty --tags --always) 3 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 4 | LDFLAGS := -X "main.gitTag=$(GIT_TAG)" -X "main.gitCommit=$(GIT_COMMIT)" -extldflags "-static" -s -w 5 | 6 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell go env GOPATH))) 7 | GOLANGCI_LINT_BIN := $(FIRST_GOPATH)/bin/golangci-lint 8 | 9 | .PHONY: all 10 | all: vendor build 11 | 12 | .PHONY: clean 13 | clean: 14 | git clean -Xfd . 15 | 16 | ####################################### 17 | # builds 18 | ####################################### 19 | 20 | .PHONY: vendor 21 | vendor: 22 | go mod tidy 23 | go mod vendor 24 | go mod verify 25 | 26 | .PHONY: build-all 27 | build-all: 28 | GOOS=linux GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME)' . 29 | GOOS=darwin GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME).darwin' . 30 | GOOS=windows GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME).exe' . 31 | 32 | .PHONY: build 33 | build: 34 | GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o $(PROJECT_NAME) . 35 | 36 | .PHONY: image 37 | image: image 38 | docker build -t $(PROJECT_NAME):$(GIT_TAG) . 39 | 40 | .PHONY: build-push-development 41 | build-push-development: 42 | docker buildx create --use 43 | docker buildx build -t webdevops/$(PROJECT_NAME):development --platform linux/amd64,linux/arm,linux/arm64 --push . 44 | 45 | ####################################### 46 | # quality checks 47 | ####################################### 48 | 49 | .PHONY: check 50 | check: vendor lint test 51 | 52 | .PHONY: test 53 | test: 54 | time go test ./... 55 | 56 | .PHONY: lint 57 | lint: $(GOLANGCI_LINT_BIN) 58 | time $(GOLANGCI_LINT_BIN) run --verbose --print-resources-usage 59 | 60 | $(GOLANGCI_LINT_BIN): 61 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(FIRST_GOPATH)/bin 62 | 63 | ####################################### 64 | # release assets 65 | ####################################### 66 | 67 | RELEASE_ASSETS = \ 68 | $(foreach GOARCH,amd64 arm64,\ 69 | $(foreach GOOS,linux darwin windows,\ 70 | release-assets/$(GOOS).$(GOARCH))) \ 71 | 72 | word-dot = $(word $2,$(subst ., ,$1)) 73 | 74 | .PHONY: release-assets 75 | release-assets: clean-release-assets vendor $(RELEASE_ASSETS) 76 | 77 | .PHONY: clean-release-assets 78 | clean-release-assets: 79 | rm -rf ./release-assets 80 | mkdir -p ./release-assets 81 | 82 | release-assets/windows.%: $(SOURCE) 83 | echo 'build release-assets for windows/$(call word-dot,$*,2)' 84 | GOOS=windows \ 85 | GOARCH=$(call word-dot,$*,1) \ 86 | CGO_ENABLED=0 \ 87 | time go build -ldflags '$(LDFLAGS)' -o './release-assets/$(PROJECT_NAME).windows.$(call word-dot,$*,1).exe' . 88 | 89 | release-assets/%: $(SOURCE) 90 | echo 'build release-assets for $(call word-dot,$*,1)/$(call word-dot,$*,2)' 91 | GOOS=$(call word-dot,$*,1) \ 92 | GOARCH=$(call word-dot,$*,2) \ 93 | CGO_ENABLED=0 \ 94 | time go build -ldflags '$(LDFLAGS)' -o './release-assets/$(PROJECT_NAME).$(call word-dot,$*,1).$(call word-dot,$*,2)' . 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Azure DevOps Exporter (VSTS) 2 | ============================ 3 | 4 | [![license](https://img.shields.io/github/license/webdevops/azure-devops-exporter.svg)](https://github.com/webdevops/azure-devops-exporter/blob/master/LICENSE) 5 | [![DockerHub](https://img.shields.io/badge/DockerHub-webdevops%2Fazure--devops--exporter-blue)](https://hub.docker.com/r/webdevops/azure-devops-exporter/) 6 | [![Quay.io](https://img.shields.io/badge/Quay.io-webdevops%2Fazure--devops--exporter-blue)](https://quay.io/repository/webdevops/azure-devops-exporter) 7 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/azure-devops-exporter)](https://artifacthub.io/packages/search?repo=azure-devops-exporter) 8 | 9 | Prometheus exporter for Azure DevOps (VSTS) for projects, builds, build times (elapsed and queue wait time), agent pool utilization and active pull requests. 10 | 11 | > [!IMPORTANT] 12 | > I've lost access to Azure DevOps (ADO) instance so i cannot provide proper support anymore. 13 | > The exporter will still be updated but don't expect active development. 14 | > 15 | > Also Microsoft seems to have lost interest in ADO in favor of GitHub, the development of ADO actions has 16 | > nearly stopped, they don't fix use of deprecated Azure APIs and even features which are supported in 17 | > GitHub for a long time are not backported to ADO. 18 | 19 | Configuration 20 | ------------- 21 | 22 | ``` 23 | Usage: 24 | azure-devops-exporter [OPTIONS] 25 | 26 | Application Options: 27 | --log.debug debug mode [$LOG_DEBUG] 28 | --log.devel development mode [$LOG_DEVEL] 29 | --log.json Switch log output to json format [$LOG_JSON] 30 | --scrape.time= Default scrape time (time.duration) (default: 30m) [$SCRAPE_TIME] 31 | --scrape.time.projects= Scrape time for project metrics (time.duration) [$SCRAPE_TIME_PROJECTS] 32 | --scrape.time.repository= Scrape time for repository metrics (time.duration) [$SCRAPE_TIME_REPOSITORY] 33 | --scrape.time.build= Scrape time for build metrics (time.duration) [$SCRAPE_TIME_BUILD] 34 | --scrape.time.release= Scrape time for release metrics (time.duration) [$SCRAPE_TIME_RELEASE] 35 | --scrape.time.deployment= Scrape time for deployment metrics (time.duration) [$SCRAPE_TIME_DEPLOYMENT] 36 | --scrape.time.pullrequest= Scrape time for pullrequest metrics (time.duration) [$SCRAPE_TIME_PULLREQUEST] 37 | --scrape.time.stats= Scrape time for stats metrics (time.duration) [$SCRAPE_TIME_STATS] 38 | --scrape.time.resourceusage= Scrape time for resourceusage metrics (time.duration) [$SCRAPE_TIME_RESOURCEUSAGE] 39 | --scrape.time.query= Scrape time for query results (time.duration) [$SCRAPE_TIME_QUERY] 40 | --scrape.time.live= Scrape time for live metrics (time.duration) (default: 30s) [$SCRAPE_TIME_LIVE] 41 | --stats.summary.maxage= Stats Summary metrics max age (time.duration) [$STATS_SUMMARY_MAX_AGE] 42 | --azure.tenant-id= Azure tenant ID for Service Principal authentication [$AZURE_TENANT_ID] 43 | --azure.client-id= Client ID for Service Principal authentication [$AZURE_CLIENT_ID] 44 | --azure.client-secret= Client secret for Service Principal authentication [$AZURE_CLIENT_SECRET] 45 | --azuredevops.url= Azure DevOps URL (empty if hosted by Microsoft) [$AZURE_DEVOPS_URL] 46 | --azuredevops.access-token= Azure DevOps access token [$AZURE_DEVOPS_ACCESS_TOKEN] 47 | --azuredevops.access-token-file= Azure DevOps access token (from file) [$AZURE_DEVOPS_ACCESS_TOKEN_FILE] 48 | --azuredevops.organisation= Azure DevOps organization [$AZURE_DEVOPS_ORGANISATION] 49 | --azuredevops.apiversion= Azure DevOps API version (default: 5.1) [$AZURE_DEVOPS_APIVERSION] 50 | --azuredevops.agentpool= Enable scrape metrics for agent pool (IDs) [$AZURE_DEVOPS_AGENTPOOL] 51 | --whitelist.project= Filter projects (UUIDs) [$AZURE_DEVOPS_FILTER_PROJECT] 52 | --blacklist.project= Filter projects (UUIDs) [$AZURE_DEVOPS_BLACKLIST_PROJECT] 53 | --timeline.state= Filter timeline states (completed, inProgress, pending) (default: completed) [$AZURE_DEVOPS_FILTER_TIMELINE_STATE] 54 | --builds.all.project= Fetch all builds from projects (UUIDs or names) [$AZURE_DEVOPS_FETCH_ALL_BUILDS_FILTER_PROJECT] 55 | --list.query= Pairs of query and project UUIDs in the form: '@' [$AZURE_DEVOPS_QUERIES] 56 | --tags.schema= Tags to be extracted from builds in the format 'tagName:type' with following types: number, info, bool [$AZURE_DEVOPS_TAG_SCHEMA] 57 | --tags.build.definition= Build definition ids to query tags (IDs) [$AZURE_DEVOPS_TAG_BUILD_DEFINITION] 58 | --cache.path= Cache path (to folder, file://path... or azblob://storageaccount.blob.core.windows.net/containername or 59 | k8scm://{namespace}/{configmap}}) [$CACHE_PATH] 60 | --request.concurrency= Number of concurrent requests against dev.azure.com (default: 10) [$REQUEST_CONCURRENCY] 61 | --request.retries= Number of retried requests against dev.azure.com (default: 3) [$REQUEST_RETRIES] 62 | --servicediscovery.refresh= Refresh duration for servicediscovery (time.duration) (default: 30m) [$SERVICEDISCOVERY_REFRESH] 63 | --limit.project= Limit number of projects (default: 100) [$LIMIT_PROJECT] 64 | --limit.builds-per-project= Limit builds per project (default: 100) [$LIMIT_BUILDS_PER_PROJECT] 65 | --limit.builds-per-definition= Limit builds per definition (default: 10) [$LIMIT_BUILDS_PER_DEFINITION] 66 | --limit.releases-per-project= Limit releases per project (default: 100) [$LIMIT_RELEASES_PER_PROJECT] 67 | --limit.releases-per-definition= Limit releases per definition (default: 100) [$LIMIT_RELEASES_PER_DEFINITION] 68 | --limit.deployments-per-definition= Limit deployments per definition (default: 100) [$LIMIT_DEPLOYMENTS_PER_DEFINITION] 69 | --limit.releasedefinitions-per-project= Limit builds per definition (default: 100) [$LIMIT_RELEASEDEFINITION_PER_PROJECT] 70 | --limit.build-history-duration= Time (time.Duration) how long the exporter should look back for builds (default: 48h) [$LIMIT_BUILD_HISTORY_DURATION] 71 | --limit.release-history-duration= Time (time.Duration) how long the exporter should look back for releases (default: 48h) [$LIMIT_RELEASE_HISTORY_DURATION] 72 | --server.bind= Server address (default: :8080) [$SERVER_BIND] 73 | --server.timeout.read= Server read timeout (default: 5s) [$SERVER_TIMEOUT_READ] 74 | --server.timeout.write= Server write timeout (default: 10s) [$SERVER_TIMEOUT_WRITE] 75 | 76 | Help Options: 77 | -h, --help Show this help message 78 | ``` 79 | 80 | Authentication 81 | -------------- 82 | 83 | This exporter supports Azure DevOps PAT tokens and ServicePrincipal authentication with Client Secret and (AKS) Workload Identity. 84 | 85 | Metrics 86 | ------- 87 | 88 | | Metric | Scraper | Description | 89 | |------------------------------------------------|---------------|-----------------------------------------------------------------------------------------| 90 | | `azure_devops_stats` | live | General scraper stats | 91 | | `azure_devops_agentpool_info` | live | Agent Pool informations | 92 | | `azure_devops_agentpool_size` | live | Number of agents per agent pool | 93 | | `azure_devops_agentpool_usage` | live | Usage of agent pool (used agents; percent 0-1) | 94 | | `azure_devops_agentpool_queue_length` | live | Queue length per agent pool | 95 | | `azure_devops_agentpool_agent_info` | live | Agent information per agent pool | 96 | | `azure_devops_agentpool_agent_status` | live | Status informations (eg. created date) for each agent in a agent pool | 97 | | `azure_devops_agentpool_agent_job` | live | Currently running jobs on each agent | 98 | | `azure_devops_project_info` | live/projects | Project informations | 99 | | `azure_devops_build_latest_info` | live | Latest build information | 100 | | `azure_devops_build_latest_status` | live | Latest build status informations | 101 | | `azure_devops_pullrequest_info` | pullrequest | Active PullRequests | 102 | | `azure_devops_pullrequest_status` | pullrequest | Status informations (eg. created date) for active PullRequests | 103 | | `azure_devops_pullrequest_label` | pullrequest | Labels set on active PullRequests | 104 | | `azure_devops_build_info` | build | Build informations | 105 | | `azure_devops_build_status` | build | Build status infos (queued, started, finished time) | 106 | | `azure_devops_build_stage` | build | Build stage infos (duration, errors, warnings, started, finished time) | 107 | | `azure_devops_build_phase` | build | Build phase infos (duration, errors, warnings, started, finished time) | 108 | | `azure_devops_build_job` | build | Build job infos (duration, errors, warnings, started, finished time) | 109 | | `azure_devops_build_task` | build | Build task infos (duration, errors, warnings, started, finished time) | 110 | | `azure_devops_build_definition_info` | build | Build definition info | 111 | | `azure_devops_release_info` | release | Release informations | 112 | | `azure_devops_release_artifact` | release | Release artifcact informations | 113 | | `azure_devops_release_environment` | release | Release environment list | 114 | | `azure_devops_release_environment_status` | release | Release environment status informations | 115 | | `azure_devops_release_approval` | release | Release environment approval list | 116 | | `azure_devops_release_definition_info` | release | Release definition info | 117 | | `azure_devops_release_definition_environment` | release | Release definition environment list | 118 | | `azure_devops_repository_info` | repository | Repository informations | 119 | | `azure_devops_repository_stats` | repository | Repository stats | 120 | | `azure_devops_repository_commits` | repository | Repository commit counter | 121 | | `azure_devops_repository_pushes` | repository | Repository push counter | 122 | | `azure_devops_query_result` | live | Latest results of given queries | 123 | | `azure_devops_deployment_info` | deployment | Release deployment informations | 124 | | `azure_devops_deployment_status` | deployment | Release deployment status informations | 125 | | `azure_devops_stats_agentpool_builds` | stats | Number of buildsper agentpool, project and result (counter) | 126 | | `azure_devops_stats_agentpool_builds_wait` | stats | Build wait time per agentpool, project and result (summary) | 127 | | `azure_devops_stats_agentpool_builds_duration` | stats | Build duration per agentpool, project and result (summary) | 128 | | `azure_devops_stats_project_builds` | stats | Number of builds per project, definition and result (counter) | 129 | | `azure_devops_stats_project_builds_wait` | stats | Build wait time per project, definition and result (summary) | 130 | | `azure_devops_stats_project_builds_success` | stats | Success rating of build per project and definition (summary) | 131 | | `azure_devops_stats_project_builds_duration` | stats | Build duration per project, definition and result (summary) | 132 | | `azure_devops_stats_project_release_duration` | stats | Release environment duration per project, definition, environment and result (summary) | 133 | | `azure_devops_stats_project_release_success` | stats | Success rating of release environment per project, definition and environment (summary) | 134 | | `azure_devops_resourceusage_build` | resourceusage | Usage of limited and paid Azure DevOps resources (build) | 135 | | `azure_devops_resourceusage_license` | resourceusage | Usage of limited and paid Azure DevOps resources (license) | 136 | | `azure_devops_api_request_*` | | REST api request histogram (count, latency, statuscCodes) | 137 | 138 | 139 | Prometheus queries 140 | ------------------ 141 | 142 | Last 3 failed releases per definition for one project 143 | ``` 144 | topk by(projectID,releaseDefinitionName,path) (3, 145 | azure_devops_release_environment{projectID="XXXXXXXXXXXXXXXX", status!="succeeded", status!="inProgress"} 146 | * on (projectID,releaseID,environmentID) group_left() (azure_devops_release_environment_status{type="created"}) 147 | * on (projectID,releaseID) group_left(releaseName, releaseDefinitionID) (azure_devops_release_info) 148 | * on (projectID,releaseDefinitionID) group_left(path, releaseDefinitionName) (azure_devops_release_definition_info) 149 | ) 150 | ``` 151 | 152 | Agent pool usage (without PoolMaintenance) 153 | ``` 154 | count by(agentPoolID) ( 155 | azure_devops_agentpool_agent_job{planType!="PoolMaintenance"} 156 | * on(agentPoolAgentID) group_left(agentPoolID) (azure_devops_agentpool_agent_info) 157 | ) 158 | / on (agentPoolID) group_left() (azure_devops_agentpool_size) 159 | * on (agentPoolID) group_left(agentPoolName) (azure_devops_agentpool_info) 160 | ``` 161 | 162 | Current running jobs 163 | ``` 164 | label_replace( 165 | azure_devops_agentpool_agent_job{planType!="PoolMaintenance"} 166 | * on (agentPoolAgentID) group_left(agentPoolID,agentPoolAgentName) azure_devops_agentpool_agent_info 167 | * on (agentPoolID) group_left(agentPoolName) (azure_devops_agentpool_info) 168 | , "projectID", "$1", "scopeID", "^(.+)$" 169 | ) 170 | * on (projectID) group_left(projectName) (azure_devops_project_info) 171 | ``` 172 | 173 | Agent pool size 174 | ``` 175 | azure_devops_agentpool_info 176 | * on (agentPoolID) group_left() (azure_devops_agentpool_size) 177 | ``` 178 | 179 | Agent pool size (enabled and online) 180 | ``` 181 | azure_devops_agentpool_info 182 | * on (agentPoolID) group_left() ( 183 | count by(agentPoolID) (azure_devops_agentpool_agent_info{status="online",enabled="true"}) 184 | ) 185 | ``` 186 | -------------------------------------------------------------------------------- /azure-devops-client/agentpool.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type AgentQueueList struct { 11 | Count int `json:"count"` 12 | List []AgentQueue `json:"value"` 13 | } 14 | 15 | type AgentQueue struct { 16 | Id int64 `json:"id"` 17 | Name string `json:"name"` 18 | Pool struct { 19 | Id int64 20 | Scope string 21 | Name string 22 | IsHosted bool 23 | PoolType string 24 | Size int64 25 | } 26 | } 27 | 28 | func (c *AzureDevopsClient) ListAgentQueues(project string) (list AgentQueueList, error error) { 29 | defer c.concurrencyUnlock() 30 | c.concurrencyLock() 31 | 32 | url := fmt.Sprintf( 33 | "%v/_apis/distributedtask/queues", 34 | url.QueryEscape(project), 35 | ) 36 | response, err := c.rest().R().Get(url) 37 | if err := c.checkResponse(response, err); err != nil { 38 | error = err 39 | return 40 | } 41 | 42 | err = json.Unmarshal(response.Body(), &list) 43 | if err != nil { 44 | error = err 45 | return 46 | } 47 | 48 | return 49 | } 50 | 51 | type AgentPoolList struct { 52 | Count int `json:"count"` 53 | Value []AgentPoolEntry `json:"value"` 54 | } 55 | 56 | type AgentPoolEntry struct { 57 | CreatedOn time.Time `json:"createdOn"` 58 | AutoProvision bool `json:"autoProvision"` 59 | AutoUpdate bool `json:"autoUpdate"` 60 | AutoSize bool `json:"autoSize"` 61 | CreatedBy struct { 62 | DisplayName string `json:"displayName"` 63 | URL string `json:"url"` 64 | Links struct { 65 | Avatar struct { 66 | Href string `json:"href"` 67 | } `json:"avatar"` 68 | } `json:"_links"` 69 | ID string `json:"id"` 70 | UniqueName string `json:"uniqueName"` 71 | ImageURL string `json:"imageUrl"` 72 | Descriptor string `json:"descriptor"` 73 | } `json:"createdBy"` 74 | Owner struct { 75 | DisplayName string `json:"displayName"` 76 | URL string `json:"url"` 77 | Links struct { 78 | Avatar struct { 79 | Href string `json:"href"` 80 | } `json:"avatar"` 81 | } `json:"_links"` 82 | ID string `json:"id"` 83 | UniqueName string `json:"uniqueName"` 84 | ImageURL string `json:"imageUrl"` 85 | Descriptor string `json:"descriptor"` 86 | } `json:"owner"` 87 | ID int64 `json:"id"` 88 | Scope string `json:"scope"` 89 | Name string `json:"name"` 90 | IsHosted bool `json:"isHosted"` 91 | PoolType string `json:"poolType"` 92 | Size int `json:"size"` 93 | IsLegacy bool `json:"isLegacy"` 94 | Options string `json:"options"` 95 | } 96 | 97 | type AgentPoolAgentList struct { 98 | Count int `json:"count"` 99 | List []AgentPoolAgent `json:"value"` 100 | } 101 | 102 | type AgentPoolAgent struct { 103 | Id int64 104 | Enabled bool 105 | MaxParallelism int64 106 | Name string 107 | OsDescription string 108 | SystemCapabilities map[string]string 109 | ProvisioningState string 110 | Status string 111 | Version string 112 | CreatedOn time.Time 113 | AssignedRequest JobRequest 114 | } 115 | 116 | type JobRequest struct { 117 | RequestId int64 118 | Demands []string 119 | QueueTime time.Time 120 | AssignTime *time.Time 121 | ReceiveTime time.Time 122 | LockedUntil time.Time 123 | ServiceOwner string 124 | HostId string 125 | ScopeId string 126 | PlanType string 127 | PlanId string 128 | JobId string 129 | Definition struct { 130 | Id int64 131 | Name string 132 | Links Links `json:"_links"` 133 | } 134 | } 135 | 136 | func (c *AzureDevopsClient) ListAgentPools() (list AgentPoolList, error error) { 137 | defer c.concurrencyUnlock() 138 | c.concurrencyLock() 139 | 140 | url := fmt.Sprintf( 141 | "/_apis/distributedtask/pools?api-version=%s", 142 | url.QueryEscape(c.ApiVersion), 143 | ) 144 | response, err := c.rest().R().Get(url) 145 | if err := c.checkResponse(response, err); err != nil { 146 | error = err 147 | return 148 | } 149 | 150 | err = json.Unmarshal(response.Body(), &list) 151 | if err != nil { 152 | error = err 153 | return 154 | } 155 | 156 | return 157 | } 158 | 159 | func (c *AzureDevopsClient) ListAgentPoolAgents(agentPoolId int64) (list AgentPoolAgentList, error error) { 160 | defer c.concurrencyUnlock() 161 | c.concurrencyLock() 162 | 163 | url := fmt.Sprintf( 164 | "/_apis/distributedtask/pools/%v/agents?includeCapabilities=true&includeAssignedRequest=true", 165 | fmt.Sprintf("%d", agentPoolId), 166 | ) 167 | response, err := c.rest().R().Get(url) 168 | if err := c.checkResponse(response, err); err != nil { 169 | error = err 170 | return 171 | } 172 | 173 | err = json.Unmarshal(response.Body(), &list) 174 | if err != nil { 175 | error = err 176 | return 177 | } 178 | 179 | return 180 | } 181 | 182 | type AgentPoolJobList struct { 183 | Count int `json:"count"` 184 | List []JobRequest `json:"value"` 185 | } 186 | 187 | func (c *AzureDevopsClient) ListAgentPoolJobs(agentPoolId int64) (list AgentPoolJobList, error error) { 188 | defer c.concurrencyUnlock() 189 | c.concurrencyLock() 190 | 191 | url := fmt.Sprintf( 192 | "/_apis/distributedtask/pools/%v/jobrequests", 193 | fmt.Sprintf("%d", agentPoolId), 194 | ) 195 | response, err := c.rest().R().Get(url) 196 | if err := c.checkResponse(response, err); err != nil { 197 | error = err 198 | return 199 | } 200 | 201 | err = json.Unmarshal(response.Body(), &list) 202 | if err != nil { 203 | error = err 204 | return 205 | } 206 | 207 | return 208 | } 209 | -------------------------------------------------------------------------------- /azure-devops-client/build.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type BuildDefinitionList struct { 12 | Count int `json:"count"` 13 | List []BuildDefinition `json:"value"` 14 | } 15 | 16 | type BuildDefinition struct { 17 | Id int64 18 | Name string 19 | Path string 20 | Revision int64 21 | QueueStatus string 22 | BuildNameFormat string 23 | Links Links `json:"_links"` 24 | } 25 | 26 | type BuildList struct { 27 | Count int `json:"count"` 28 | List []Build `json:"value"` 29 | } 30 | 31 | type TimelineRecordList struct { 32 | List []TimelineRecord `json:"records"` 33 | } 34 | 35 | type TagList struct { 36 | Count int `json:"count"` 37 | List []string `json:"value"` 38 | } 39 | 40 | type Tag struct { 41 | Name string 42 | Value string 43 | Type string 44 | } 45 | 46 | type TimelineRecord struct { 47 | RecordType string `json:"type"` 48 | Name string `json:"name"` 49 | Id string `json:"id"` 50 | ParentId string `json:"parentId"` 51 | ErrorCount float64 `json:"errorCount"` 52 | WarningCount float64 `json:"warningCount"` 53 | Result string `json:"result"` 54 | WorkerName string `json:"workerName"` 55 | Identifier string `json:"identifier"` 56 | State string `json:"state"` 57 | StartTime time.Time 58 | FinishTime time.Time 59 | } 60 | 61 | type Build struct { 62 | Id int64 `json:"id"` 63 | BuildNumber string `json:"buildNumber"` 64 | BuildNumberRevision int64 `json:"buildNumberRevision"` 65 | Quality string `json:"quality"` 66 | 67 | Definition BuildDefinition 68 | 69 | Project Project 70 | 71 | Queue AgentPoolQueue 72 | 73 | Reason string 74 | Result string 75 | Status string 76 | QueueTime time.Time 77 | QueuePosition string 78 | StartTime time.Time 79 | FinishTime time.Time 80 | Uri string 81 | Url string 82 | SourceBranch string 83 | SourceVersion string 84 | 85 | RequestedBy IdentifyRef 86 | RequestedFor IdentifyRef 87 | 88 | Links Links `json:"_links"` 89 | } 90 | 91 | func (b *Build) QueueDuration() time.Duration { 92 | return b.StartTime.Sub(b.QueueTime) 93 | } 94 | 95 | func (c *AzureDevopsClient) ListBuildDefinitions(project string) (list BuildDefinitionList, error error) { 96 | defer c.concurrencyUnlock() 97 | c.concurrencyLock() 98 | 99 | url := fmt.Sprintf( 100 | "%v/_apis/build/definitions?api-version=%v&$top=9999", 101 | url.QueryEscape(project), 102 | url.QueryEscape(c.ApiVersion), 103 | ) 104 | response, err := c.rest().R().Get(url) 105 | if err := c.checkResponse(response, err); err != nil { 106 | error = err 107 | return 108 | } 109 | 110 | err = json.Unmarshal(response.Body(), &list) 111 | if err != nil { 112 | error = err 113 | return 114 | } 115 | 116 | return 117 | } 118 | 119 | func (c *AzureDevopsClient) ListBuilds(project string) (list BuildList, error error) { 120 | defer c.concurrencyUnlock() 121 | c.concurrencyLock() 122 | 123 | url := fmt.Sprintf( 124 | "%v/_apis/build/builds?api-version=%v&maxBuildsPerDefinition=%s&deletedFilter=excludeDeleted", 125 | url.QueryEscape(project), 126 | url.QueryEscape(c.ApiVersion), 127 | url.QueryEscape(int64ToString(c.LimitBuildsPerDefinition)), 128 | ) 129 | response, err := c.rest().R().Get(url) 130 | if err := c.checkResponse(response, err); err != nil { 131 | error = err 132 | return 133 | } 134 | 135 | err = json.Unmarshal(response.Body(), &list) 136 | if err != nil { 137 | error = err 138 | return 139 | } 140 | 141 | return 142 | } 143 | 144 | func (c *AzureDevopsClient) ListLatestBuilds(project string) (list BuildList, error error) { 145 | defer c.concurrencyUnlock() 146 | c.concurrencyLock() 147 | 148 | url := fmt.Sprintf( 149 | "%v/_apis/build/builds?api-version=%v&maxBuildsPerDefinition=%s&deletedFilter=excludeDeleted", 150 | url.QueryEscape(project), 151 | url.QueryEscape(c.ApiVersion), 152 | url.QueryEscape("1"), 153 | ) 154 | response, err := c.rest().R().Get(url) 155 | if err := c.checkResponse(response, err); err != nil { 156 | error = err 157 | return 158 | } 159 | 160 | err = json.Unmarshal(response.Body(), &list) 161 | if err != nil { 162 | error = err 163 | return 164 | } 165 | 166 | return 167 | } 168 | 169 | func (c *AzureDevopsClient) ListBuildHistory(project string, minTime time.Time) (list BuildList, error error) { 170 | defer c.concurrencyUnlock() 171 | c.concurrencyLock() 172 | 173 | url := fmt.Sprintf( 174 | "%v/_apis/build/builds?api-version=%v&minTime=%s&$top=%v&queryOrder=finishTimeDescending", 175 | url.QueryEscape(project), 176 | url.QueryEscape(c.ApiVersion), 177 | url.QueryEscape(minTime.UTC().Format(time.RFC3339)), 178 | url.QueryEscape(int64ToString(c.LimitBuildsPerProject)), 179 | ) 180 | response, err := c.rest().R().Get(url) 181 | if err := c.checkResponse(response, err); err != nil { 182 | error = err 183 | return 184 | } 185 | 186 | err = json.Unmarshal(response.Body(), &list) 187 | if err != nil { 188 | error = err 189 | return 190 | } 191 | 192 | return 193 | } 194 | 195 | func (c *AzureDevopsClient) ListBuildHistoryWithStatus(project string, minTime time.Time, statusFilter string) (list BuildList, error error) { 196 | defer c.concurrencyUnlock() 197 | c.concurrencyLock() 198 | 199 | requestUrl := "" 200 | 201 | if statusFilter == "all" { 202 | requestUrl = fmt.Sprintf( 203 | "%v/_apis/build/builds?api-version=%v&statusFilter=%v", 204 | url.QueryEscape(project), 205 | url.QueryEscape(c.ApiVersion), 206 | url.QueryEscape(statusFilter), 207 | ) 208 | } else { 209 | requestUrl = fmt.Sprintf( 210 | "%v/_apis/build/builds?api-version=%v&minTime=%s&statusFilter=%v", 211 | url.QueryEscape(project), 212 | url.QueryEscape(c.ApiVersion), 213 | url.QueryEscape(minTime.UTC().Format(time.RFC3339)), 214 | url.QueryEscape(statusFilter), 215 | ) 216 | } 217 | 218 | response, err := c.rest().R().Get(requestUrl) 219 | if err := c.checkResponse(response, err); err != nil { 220 | error = err 221 | return 222 | } 223 | 224 | err = json.Unmarshal(response.Body(), &list) 225 | if err != nil { 226 | error = err 227 | return 228 | } 229 | 230 | // if the status filter is "all", we need to filter the builds by minTime manually because Azure DevOps API does not support it 231 | if statusFilter == "all" { 232 | var filteredList BuildList 233 | for _, build := range list.List { 234 | if build.StartTime.After(minTime) { 235 | filteredList.List = append(filteredList.List, build) 236 | } 237 | } 238 | filteredList.Count = len(filteredList.List) 239 | list = filteredList 240 | } 241 | 242 | return 243 | } 244 | 245 | func (c *AzureDevopsClient) ListBuildTimeline(project string, buildID string) (list TimelineRecordList, error error) { 246 | defer c.concurrencyUnlock() 247 | c.concurrencyLock() 248 | 249 | url := fmt.Sprintf( 250 | "%v/_apis/build/builds/%v/Timeline", 251 | url.QueryEscape(project), 252 | url.QueryEscape(buildID), 253 | ) 254 | response, err := c.rest().R().Get(url) 255 | if err := c.checkResponse(response, err); err != nil { 256 | error = err 257 | return 258 | } 259 | 260 | err = json.Unmarshal(response.Body(), &list) 261 | if err != nil { 262 | error = err 263 | return 264 | } 265 | 266 | return 267 | } 268 | 269 | func (c *AzureDevopsClient) ListBuildTags(project string, buildID string) (list TagList, error error) { 270 | defer c.concurrencyUnlock() 271 | c.concurrencyLock() 272 | 273 | url := fmt.Sprintf( 274 | "%v/_apis/build/builds/%v/tags", 275 | url.QueryEscape(project), 276 | url.QueryEscape(buildID), 277 | ) 278 | response, err := c.rest().R().Get(url) 279 | if err := c.checkResponse(response, err); err != nil { 280 | error = err 281 | return 282 | } 283 | 284 | err = json.Unmarshal(response.Body(), &list) 285 | if err != nil { 286 | error = err 287 | return 288 | } 289 | 290 | return 291 | } 292 | 293 | func extractTagKeyValue(tag string) (k string, v string, error error) { 294 | parts := strings.Split(tag, "=") 295 | if len(parts) != 2 { 296 | error = fmt.Errorf("could not extract key value pair from tag '%s'", tag) 297 | return 298 | } 299 | k = parts[0] 300 | v = parts[1] 301 | return 302 | } 303 | 304 | func extractTagSchema(tagSchema string) (n string, t string, error error) { 305 | parts := strings.Split(tagSchema, ":") 306 | if len(parts) != 2 { 307 | error = fmt.Errorf("could not extract type from tag schema '%s'", tagSchema) 308 | return 309 | } 310 | n = parts[0] 311 | t = parts[1] 312 | return 313 | } 314 | 315 | func (t *TagList) Extract() (tags map[string]string, error error) { 316 | tags = make(map[string]string) 317 | for _, t := range t.List { 318 | k, v, err := extractTagKeyValue(t) 319 | if err != nil { 320 | error = err 321 | return 322 | } 323 | tags[k] = v 324 | } 325 | return 326 | } 327 | 328 | func (t *TagList) Parse(tagSchema []string) (pTags []Tag, error error) { 329 | tags, err := t.Extract() 330 | if err != nil { 331 | error = err 332 | return 333 | } 334 | for _, ts := range tagSchema { 335 | name, _type, err := extractTagSchema(ts) 336 | if err != nil { 337 | error = err 338 | return 339 | } 340 | 341 | value, isPresent := tags[name] 342 | if isPresent { 343 | pTags = append(pTags, Tag{ 344 | Name: name, 345 | Value: value, 346 | Type: _type, 347 | }) 348 | } 349 | } 350 | return 351 | } 352 | -------------------------------------------------------------------------------- /azure-devops-client/general.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import "time" 4 | 5 | type IdentifyRef struct { 6 | Id string 7 | DisplayName string 8 | ProfileUrl string 9 | UniqueName string 10 | Url string 11 | Descriptor string 12 | } 13 | 14 | type AgentPoolQueue struct { 15 | Id int64 16 | Name string 17 | Pool AgentPool 18 | Url string 19 | } 20 | 21 | type AgentPool struct { 22 | Id int64 23 | IsHosted bool 24 | Name string 25 | } 26 | 27 | type Link struct { 28 | Href string 29 | } 30 | 31 | type Links struct { 32 | Self Link 33 | Web Link 34 | Source Link 35 | Timeline Link 36 | Badge Link 37 | } 38 | 39 | type Author struct { 40 | Name string 41 | Email string 42 | Date time.Time 43 | } 44 | -------------------------------------------------------------------------------- /azure-devops-client/main.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 14 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 15 | resty "github.com/go-resty/resty/v2" 16 | "github.com/prometheus/client_golang/prometheus" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | const ( 21 | AZURE_DEVOPS_SCOPE = "499b84ac-1321-427f-aa17-267ca6975798/.default" 22 | ) 23 | 24 | type AzureDevopsClient struct { 25 | logger *zap.SugaredLogger 26 | 27 | // RequestCount has to be the first words 28 | // in order to be 64-aligned on 32-bit architectures. 29 | RequestCount uint64 30 | RequestRetries int 31 | 32 | organization *string 33 | collection *string 34 | 35 | // we can either use a PAT token for authentication 36 | accessToken *string 37 | 38 | // azure auth 39 | azcreds *azidentity.DefaultAzureCredential 40 | 41 | HostUrl *string 42 | 43 | ApiVersion string 44 | 45 | restClient *resty.Client 46 | restClientVsrm *resty.Client 47 | 48 | semaphore chan bool 49 | concurrency int64 50 | 51 | delayUntil *time.Time 52 | 53 | LimitProject int64 54 | LimitBuildsPerProject int64 55 | LimitBuildsPerDefinition int64 56 | LimitReleasesPerDefinition int64 57 | LimitDeploymentPerDefinition int64 58 | LimitReleaseDefinitionsPerProject int64 59 | LimitReleasesPerProject int64 60 | 61 | prometheus struct { 62 | apiRequest *prometheus.HistogramVec 63 | } 64 | } 65 | 66 | type EntraIdToken struct { 67 | TokenType *string `json:"token_type"` 68 | ExpiresIn *int64 `json:"expires_in"` 69 | ExtExpiresIn *int64 `json:"ext_expires_in"` 70 | AccessToken *string `json:"access_token"` 71 | } 72 | 73 | type EntraIdErrorResponse struct { 74 | Error *string `json:"error"` 75 | ErrorDescription *string `json:"error_description"` 76 | } 77 | 78 | func NewAzureDevopsClient(logger *zap.SugaredLogger) *AzureDevopsClient { 79 | c := AzureDevopsClient{ 80 | logger: logger, 81 | } 82 | c.Init() 83 | 84 | return &c 85 | } 86 | 87 | func (c *AzureDevopsClient) Init() { 88 | collection := "DefaultCollection" 89 | c.collection = &collection 90 | c.RequestCount = 0 91 | c.SetRetries(3) 92 | c.SetConcurrency(10) 93 | 94 | c.LimitBuildsPerProject = 100 95 | c.LimitBuildsPerDefinition = 10 96 | c.LimitReleasesPerDefinition = 100 97 | c.LimitDeploymentPerDefinition = 100 98 | c.LimitReleaseDefinitionsPerProject = 100 99 | c.LimitReleasesPerProject = 100 100 | 101 | c.prometheus.apiRequest = prometheus.NewHistogramVec( 102 | prometheus.HistogramOpts{ 103 | Name: "azure_devops_api_request", 104 | Help: "AzureDevOps API requests", 105 | Buckets: []float64{.05, .1, .25, .5, 1, 2.5, 5, 10, 30}, 106 | }, 107 | []string{"endpoint", "organization", "method", "statusCode"}, 108 | ) 109 | 110 | prometheus.MustRegister(c.prometheus.apiRequest) 111 | } 112 | 113 | func (c *AzureDevopsClient) SetConcurrency(v int64) { 114 | c.concurrency = v 115 | c.semaphore = make(chan bool, c.concurrency) 116 | } 117 | 118 | func (c *AzureDevopsClient) SetRetries(v int) { 119 | c.RequestRetries = v 120 | 121 | if c.restClient != nil { 122 | c.restClient.SetRetryCount(c.RequestRetries) 123 | } 124 | 125 | if c.restClientVsrm != nil { 126 | c.restClientVsrm.SetRetryCount(c.RequestRetries) 127 | } 128 | } 129 | 130 | func (c *AzureDevopsClient) SetUserAgent(v string) { 131 | c.rest().SetHeader("User-Agent", v) 132 | c.restVsrm().SetHeader("User-Agent", v) 133 | } 134 | 135 | func (c *AzureDevopsClient) SetApiVersion(apiversion string) { 136 | c.ApiVersion = apiversion 137 | } 138 | 139 | func (c *AzureDevopsClient) SetOrganization(url string) { 140 | c.organization = &url 141 | } 142 | 143 | func (c *AzureDevopsClient) SetAccessToken(token string) { 144 | c.accessToken = &token 145 | } 146 | 147 | func (c *AzureDevopsClient) UseAzAuth() error { 148 | opts := azidentity.DefaultAzureCredentialOptions{} 149 | cred, err := azidentity.NewDefaultAzureCredential(&opts) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | c.azcreds = cred 155 | return nil 156 | } 157 | 158 | func (c *AzureDevopsClient) SupportsPatAuthentication() bool { 159 | return c.accessToken != nil && len(*c.accessToken) > 0 160 | } 161 | 162 | func (c *AzureDevopsClient) rest() *resty.Client { 163 | var client, err = c.restWithAuthentication(c.restClient, "dev.azure.com") 164 | 165 | if err != nil { 166 | c.logger.Fatalf("could not create a rest client: %v", err) 167 | } 168 | 169 | return client 170 | } 171 | 172 | func (c *AzureDevopsClient) restVsrm() *resty.Client { 173 | var client, err = c.restWithAuthentication(c.restClientVsrm, "vsrm.dev.azure.com") 174 | 175 | if err != nil { 176 | c.logger.Fatalf("could not create a rest client: %v", err) 177 | } 178 | 179 | return client 180 | } 181 | 182 | func (c *AzureDevopsClient) restWithAuthentication(restClient *resty.Client, domain string) (*resty.Client, error) { 183 | if restClient == nil { 184 | restClient = c.restWithoutToken(domain) 185 | } 186 | 187 | if c.SupportsPatAuthentication() { 188 | restClient.SetBasicAuth("", *c.accessToken) 189 | } else { 190 | ctx := context.Background() 191 | opts := policy.TokenRequestOptions{ 192 | Scopes: []string{AZURE_DEVOPS_SCOPE}, 193 | } 194 | accessToken, err := c.azcreds.GetToken(ctx, opts) 195 | if err != nil { 196 | panic(err) 197 | } 198 | 199 | restClient.SetBasicAuth("", accessToken.Token) 200 | } 201 | 202 | return restClient, nil 203 | } 204 | 205 | func (c *AzureDevopsClient) restWithoutToken(domain string) *resty.Client { 206 | var restClient = resty.New() 207 | 208 | if c.HostUrl != nil { 209 | restClient.SetBaseURL(*c.HostUrl + "/" + *c.organization + "/") 210 | } else { 211 | restClient.SetBaseURL(fmt.Sprintf("https://%v/%v/", domain, *c.organization)) 212 | } 213 | 214 | restClient.SetHeader("Accept", "application/json") 215 | restClient.SetRetryCount(c.RequestRetries) 216 | 217 | if c.delayUntil != nil { 218 | restClient.OnBeforeRequest(c.restOnBeforeRequestDelay) 219 | } else { 220 | restClient.OnBeforeRequest(c.restOnBeforeRequest) 221 | } 222 | 223 | restClient.OnAfterResponse(c.restOnAfterResponse) 224 | 225 | return restClient 226 | } 227 | 228 | func (c *AzureDevopsClient) concurrencyLock() { 229 | c.semaphore <- true 230 | } 231 | 232 | func (c *AzureDevopsClient) concurrencyUnlock() { 233 | <-c.semaphore 234 | } 235 | 236 | // PreRequestHook is a resty hook that is called before every request 237 | // It checks that the delay is ok before requesting 238 | func (c *AzureDevopsClient) restOnBeforeRequestDelay(client *resty.Client, request *resty.Request) (err error) { 239 | atomic.AddUint64(&c.RequestCount, 1) 240 | if c.delayUntil != nil { 241 | if time.Now().Before(*c.delayUntil) { 242 | time.Sleep(time.Until(*c.delayUntil)) 243 | } 244 | c.delayUntil = nil 245 | } 246 | return 247 | } 248 | 249 | func (c *AzureDevopsClient) restOnBeforeRequest(client *resty.Client, request *resty.Request) (err error) { 250 | atomic.AddUint64(&c.RequestCount, 1) 251 | return 252 | } 253 | 254 | func (c *AzureDevopsClient) restOnAfterResponse(client *resty.Client, response *resty.Response) (err error) { 255 | requestUrl, _ := url.Parse(response.Request.URL) 256 | c.prometheus.apiRequest.With(prometheus.Labels{ 257 | "endpoint": requestUrl.Hostname(), 258 | "organization": *c.organization, 259 | "method": strings.ToLower(response.Request.Method), 260 | "statusCode": strconv.FormatInt(int64(response.StatusCode()), 10), 261 | }).Observe(response.Time().Seconds()) 262 | return 263 | } 264 | 265 | func (c *AzureDevopsClient) GetRequestCount() float64 { 266 | requestCount := atomic.LoadUint64(&c.RequestCount) 267 | return float64(requestCount) 268 | } 269 | 270 | func (c *AzureDevopsClient) GetCurrentConcurrency() float64 { 271 | return float64(len(c.semaphore)) 272 | } 273 | 274 | func (c *AzureDevopsClient) checkResponse(response *resty.Response, err error) error { 275 | if err != nil { 276 | return err 277 | } 278 | if response != nil { 279 | // check delay from usage quota 280 | if d := response.Header().Get("Retry-After"); d != "" { 281 | // convert string to int to time.Duration 282 | if dInt, err := strconv.Atoi(d); err != nil { 283 | dD := time.Now().Add(time.Duration(dInt) * time.Second) 284 | c.delayUntil = &dD 285 | } 286 | } 287 | // check status code 288 | statusCode := response.StatusCode() 289 | if statusCode != 200 { 290 | return fmt.Errorf("response status code is %v (expected 200), url: %v", statusCode, response.Request.URL) 291 | } 292 | } else { 293 | return errors.New("response is nil") 294 | } 295 | 296 | return nil 297 | } 298 | -------------------------------------------------------------------------------- /azure-devops-client/misc.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | func int64ToString(v int64) string { 9 | return strconv.FormatInt(v, 10) 10 | } 11 | 12 | func parseTime(v string) *time.Time { 13 | for _, layout := range []string{time.RFC3339Nano, time.RFC3339} { 14 | t, err := time.Parse(layout, v) 15 | if err == nil { 16 | return &t 17 | } 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /azure-devops-client/project.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type ProjectList struct { 10 | Count int `json:"count"` 11 | List []Project `json:"value"` 12 | } 13 | 14 | type Project struct { 15 | Id string `json:"id"` 16 | Name string `json:"name"` 17 | Description string `json:"description"` 18 | Url string `json:"url"` 19 | State string `json:"state"` 20 | WellFormed string `json:"wellFormed"` 21 | Revision int64 `json:"revision"` 22 | Visibility string `json:"visibility"` 23 | 24 | RepositoryList RepositoryList 25 | } 26 | 27 | func (c *AzureDevopsClient) ListProjects() (list ProjectList, error error) { 28 | defer c.concurrencyUnlock() 29 | c.concurrencyLock() 30 | 31 | url := fmt.Sprintf( 32 | "_apis/projects?$top=%v&api-version=%v", 33 | c.LimitProject, 34 | url.QueryEscape(c.ApiVersion), 35 | ) 36 | response, err := c.rest().R().Get(url) 37 | if err := c.checkResponse(response, err); err != nil { 38 | error = err 39 | return 40 | } 41 | 42 | err = json.Unmarshal(response.Body(), &list) 43 | if err != nil { 44 | error = err 45 | return 46 | } 47 | 48 | for key, project := range list.List { 49 | list.List[key].RepositoryList, _ = c.ListRepositories(project.Id) 50 | } 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /azure-devops-client/pullrequest.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type PullRequestList struct { 11 | Count int `json:"count"` 12 | List []PullRequest `json:"value"` 13 | } 14 | 15 | type PullRequest struct { 16 | Id int64 `json:"pullRequestId"` 17 | CodeReviewId int64 `json:"codeReviewId"` 18 | 19 | Title string 20 | Description string 21 | Uri string 22 | Url string 23 | 24 | CreatedBy IdentifyRef 25 | 26 | SourceRefName string 27 | TargetRefName string 28 | 29 | Reviewers []PullRequestReviewer 30 | Labels []PullRequestLabels 31 | 32 | Status string `json:"status"` 33 | CreationDate time.Time 34 | ClosedDate time.Time 35 | 36 | IsDraft bool 37 | 38 | Links Links `json:"_links"` 39 | } 40 | 41 | type PullRequestReviewer struct { 42 | Vote int64 43 | DisplayName string 44 | } 45 | 46 | type PullRequestLabels struct { 47 | Id string 48 | Name string 49 | Active bool 50 | } 51 | 52 | type PullRequestVoteSummary struct { 53 | Approved int64 54 | ApprovedSuggestions int64 55 | None int64 56 | WaitingForAuthor int64 57 | Rejected int64 58 | Count int64 59 | } 60 | 61 | func (v *PullRequest) GetVoteSummary() PullRequestVoteSummary { 62 | ret := PullRequestVoteSummary{} 63 | 64 | for _, reviewer := range v.Reviewers { 65 | ret.Count++ 66 | switch reviewer.Vote { 67 | case 10: 68 | ret.Approved++ 69 | case 5: 70 | ret.ApprovedSuggestions++ 71 | case 0: 72 | ret.None++ 73 | case -5: 74 | ret.WaitingForAuthor++ 75 | case -10: 76 | ret.Rejected++ 77 | } 78 | } 79 | 80 | return ret 81 | } 82 | 83 | func (v *PullRequestVoteSummary) HumanizeString() (status string) { 84 | status = "None" 85 | 86 | if v.Rejected >= 1 { 87 | status = "Rejected" 88 | } else if v.WaitingForAuthor >= 1 { 89 | status = "WaitingForAuthor" 90 | } else if v.ApprovedSuggestions >= 1 { 91 | status = "ApprovedSuggestions" 92 | } else if v.Approved >= 1 { 93 | status = "Approved" 94 | } 95 | 96 | return 97 | } 98 | 99 | func (c *AzureDevopsClient) ListPullrequest(project, repositoryId string) (list PullRequestList, error error) { 100 | defer c.concurrencyUnlock() 101 | c.concurrencyLock() 102 | 103 | url := fmt.Sprintf( 104 | "%v/_apis/git/repositories/%v/pullrequests?api-version=%v&searchCriteria.status=active", 105 | url.QueryEscape(project), 106 | url.QueryEscape(repositoryId), 107 | url.QueryEscape(c.ApiVersion), 108 | ) 109 | 110 | response, err := c.rest().R().Get(url) 111 | if err := c.checkResponse(response, err); err != nil { 112 | error = err 113 | return 114 | } 115 | 116 | err = json.Unmarshal(response.Body(), &list) 117 | if err != nil { 118 | error = err 119 | return 120 | } 121 | 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /azure-devops-client/query.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type Query struct { 10 | Path string `json:"path"` 11 | } 12 | 13 | type WorkItemInfoList struct { 14 | List []WorkItemInfo `json:"workItems"` 15 | } 16 | 17 | type WorkItemInfo struct { 18 | Id int `json:"id"` 19 | Url string `json:"url"` 20 | } 21 | 22 | func (c *AzureDevopsClient) QueryWorkItems(queryPath, projectId string) (list WorkItemInfoList, error error) { 23 | defer c.concurrencyUnlock() 24 | c.concurrencyLock() 25 | 26 | url := fmt.Sprintf( 27 | "%v/_apis/wit/wiql/%v?api-version=%v", 28 | projectId, 29 | queryPath, 30 | url.QueryEscape(c.ApiVersion), 31 | ) 32 | response, err := c.rest().R().Get(url) 33 | if err := c.checkResponse(response, err); err != nil { 34 | error = err 35 | return 36 | } 37 | 38 | err = json.Unmarshal(response.Body(), &list) 39 | if err != nil { 40 | error = err 41 | } 42 | 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /azure-devops-client/release.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type ReleaseList struct { 11 | Count int `json:"count"` 12 | List []Release `json:"value"` 13 | } 14 | 15 | type Release struct { 16 | Id int64 `json:"id"` 17 | Name string `json:"name"` 18 | 19 | Definition struct { 20 | Id int64 `json:"id"` 21 | Name string `json:"name"` 22 | Links Links `json:"_links"` 23 | } `json:"releaseDefinition"` 24 | 25 | Project Project `json:"projectReference"` 26 | 27 | Queue AgentPoolQueue `json:"queue"` 28 | 29 | Reason string `json:"reason"` 30 | Result bool `json:"result"` 31 | Status string `json:"status"` 32 | CreatedOn time.Time `json:"createdOn"` 33 | QueueTime time.Time `json:"queueTime"` 34 | QueuePosition string `json:"queuePosition"` 35 | StartTime time.Time `json:"startTime"` 36 | FinishTime time.Time `json:"finishTime"` 37 | Uri string `json:"uri"` 38 | Url string `json:"url"` 39 | 40 | Artifacts []ReleaseArtifact `json:"artifacts"` 41 | Environments []ReleaseEnvironment `json:"environments"` 42 | 43 | RequestedBy IdentifyRef `json:"requestedBy"` 44 | RequestedFor IdentifyRef `json:"requestedFor"` 45 | 46 | Links Links `json:"_links"` 47 | } 48 | 49 | type ReleaseArtifact struct { 50 | SourceId string `json:"sourceId"` 51 | Type string `json:"type"` 52 | Alias string `json:"alias"` 53 | 54 | DefinitionReference struct { 55 | Definition struct { 56 | Id string 57 | Name string 58 | } 59 | 60 | Project struct { 61 | Id string 62 | Name string 63 | } 64 | 65 | Repository struct { 66 | Id string 67 | Name string 68 | } 69 | 70 | Version struct { 71 | Id string 72 | Name string 73 | } 74 | 75 | Branch struct { 76 | Id string 77 | Name string 78 | } 79 | } `json:"definitionReference"` 80 | } 81 | 82 | type ReleaseEnvironment struct { 83 | Id int64 `json:"id"` 84 | ReleaseId int64 `json:"releaseId"` 85 | DefinitionEnvironmentId int64 `json:"definitionEnvironmentId"` 86 | Name string `json:"name"` 87 | Status string `json:"status"` 88 | Rank int64 `json:"rank"` 89 | 90 | TriggerReason string `json:"triggerReason"` 91 | 92 | DeploySteps []ReleaseEnvironmentDeployStep `json:"deploySteps"` 93 | 94 | PreDeployApprovals []ReleaseEnvironmentApproval `json:"preDeployApprovals"` 95 | PostDeployApprovals []ReleaseEnvironmentApproval `json:"postDeployApprovals"` 96 | 97 | CreatedOn time.Time `json:"createdOn"` 98 | QueuedOn time.Time `json:"queuedOn"` 99 | LastModifiedOn time.Time `json:"lastModifiedOn"` 100 | 101 | TimeToDeploy float64 `json:"timeToDeploy"` 102 | } 103 | 104 | type ReleaseEnvironmentDeployStep struct { 105 | Id int64 106 | DeploymentId int64 107 | Attemt int64 108 | Reason string 109 | Status string 110 | OperationStatus string 111 | 112 | ReleaseDeployPhases []ReleaseEnvironmentDeployStepPhase 113 | 114 | QueuedOn time.Time 115 | LastModifiedOn time.Time 116 | } 117 | 118 | type ReleaseEnvironmentDeployStepPhase struct { 119 | Id int64 120 | PhaseId string 121 | Name string 122 | Rank int64 123 | PhaseType string 124 | Status string 125 | StartedOn time.Time `json:"startedOn"` 126 | } 127 | 128 | type ReleaseEnvironmentApproval struct { 129 | Id int64 130 | Revision int64 131 | ApprovalType string 132 | Status string 133 | Comments string 134 | IsAutomated bool 135 | IsNotificationOn bool 136 | TrialNumber int64 `json:"trialNumber"` 137 | Attempt int64 `json:"attempt"` 138 | Rank int64 `json:"rank"` 139 | 140 | Approver IdentifyRef `json:"approver"` 141 | ApprovedBy IdentifyRef `json:"approvedBy"` 142 | 143 | CreatedOn time.Time `json:"createdOn"` 144 | ModifiedOn time.Time `json:"modifiedOn"` 145 | } 146 | 147 | func (r *Release) QueueDuration() time.Duration { 148 | return r.StartTime.Sub(r.QueueTime) 149 | } 150 | 151 | func (c *AzureDevopsClient) ListReleases(project string, releaseDefinitionId int64) (list ReleaseList, error error) { 152 | defer c.concurrencyUnlock() 153 | c.concurrencyLock() 154 | 155 | url := fmt.Sprintf( 156 | "%v/_apis/release/releases?api-version=%v&isDeleted=false&$expand=94&definitionId=%s&$top=%v", 157 | url.QueryEscape(project), 158 | url.QueryEscape(c.ApiVersion), 159 | url.QueryEscape(int64ToString(releaseDefinitionId)), 160 | url.QueryEscape(int64ToString(c.LimitReleasesPerDefinition)), 161 | ) 162 | response, err := c.restVsrm().R().Get(url) 163 | if err := c.checkResponse(response, err); err != nil { 164 | error = err 165 | return 166 | } 167 | 168 | err = json.Unmarshal(response.Body(), &list) 169 | if err != nil { 170 | error = err 171 | return 172 | } 173 | 174 | return 175 | } 176 | 177 | func (c *AzureDevopsClient) ListReleaseHistory(project string, minTime time.Time) (list ReleaseList, error error) { 178 | defer c.concurrencyUnlock() 179 | c.concurrencyLock() 180 | 181 | url := fmt.Sprintf( 182 | "%v/_apis/release/releases?api-version=%v&isDeleted=false&$expand=94&minCreatedTime=%s&$top=%v&queryOrder=descending", 183 | url.QueryEscape(project), 184 | url.QueryEscape(c.ApiVersion), 185 | url.QueryEscape(minTime.UTC().Format(time.RFC3339)), 186 | url.QueryEscape(int64ToString(c.LimitReleasesPerProject)), 187 | ) 188 | 189 | response, err := c.restVsrm().R().Get(url) 190 | if err := c.checkResponse(response, err); err != nil { 191 | error = err 192 | return 193 | } 194 | 195 | err = json.Unmarshal(response.Body(), &list) 196 | if err != nil { 197 | error = err 198 | return 199 | } 200 | 201 | continuationToken := response.Header().Get("x-ms-continuationtoken") 202 | 203 | for continuationToken != "" { 204 | continuationUrl := fmt.Sprintf( 205 | "%v&continuationToken=%v", 206 | url, 207 | continuationToken, 208 | ) 209 | 210 | response, err = c.restVsrm().R().Get(continuationUrl) 211 | if err := c.checkResponse(response, err); err != nil { 212 | error = err 213 | return 214 | } 215 | 216 | var tmpList ReleaseList 217 | err = json.Unmarshal(response.Body(), &tmpList) 218 | if err != nil { 219 | error = err 220 | return 221 | } 222 | 223 | list.Count += tmpList.Count 224 | list.List = append(list.List, tmpList.List...) 225 | 226 | continuationToken = response.Header().Get("x-ms-continuationtoken") 227 | } 228 | 229 | return 230 | } 231 | -------------------------------------------------------------------------------- /azure-devops-client/release_definition.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type ReleaseDefinitionList struct { 10 | Count int `json:"count"` 11 | List []ReleaseDefinition `json:"value"` 12 | } 13 | 14 | type ReleaseDefinition struct { 15 | Id int64 `json:"id"` 16 | Name string 17 | Path string 18 | ReleaseNameFormat string `json:"releaseNameFormat"` 19 | 20 | Environments []ReleaseDefinitionEnvironment 21 | 22 | LastRelease Release `json:"lastRelease"` 23 | 24 | Links Links `json:"_links"` 25 | } 26 | 27 | type ReleaseDefinitionEnvironment struct { 28 | Id int64 29 | Name string 30 | Rank int64 31 | 32 | Owner IdentifyRef 33 | CurrentRelease struct { 34 | Id int64 35 | Url string 36 | } `json:"currentRelease"` 37 | 38 | BadgeUrl string `json:"badgeUrl"` 39 | } 40 | 41 | func (c *AzureDevopsClient) ListReleaseDefinitions(project string) (list ReleaseDefinitionList, error error) { 42 | defer c.concurrencyUnlock() 43 | c.concurrencyLock() 44 | 45 | url := fmt.Sprintf( 46 | "%v/_apis/release/definitions?api-version=%v&isDeleted=false&$top=%v&$expand=environments,lastRelease", 47 | url.QueryEscape(project), 48 | url.QueryEscape(c.ApiVersion), 49 | url.QueryEscape(int64ToString(c.LimitReleaseDefinitionsPerProject)), 50 | ) 51 | response, err := c.restVsrm().R().Get(url) 52 | if err := c.checkResponse(response, err); err != nil { 53 | error = err 54 | return 55 | } 56 | 57 | err = json.Unmarshal(response.Body(), &list) 58 | if err != nil { 59 | error = err 60 | return 61 | } 62 | 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /azure-devops-client/release_deployment.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type ReleaseDeploymentList struct { 12 | Count int `json:"count"` 13 | List []ReleaseDeployment `json:"value"` 14 | } 15 | 16 | type ReleaseDeployment struct { 17 | Id int64 `json:"id"` 18 | Name string 19 | 20 | Release struct { 21 | Id int64 22 | Name string 23 | Links Links `json:"_links"` 24 | } `json:"release"` 25 | 26 | ReleaseDefinition struct { 27 | Id int64 28 | Name string 29 | Path string 30 | } `json:"releaseDefinition"` 31 | 32 | Artifacts []ReleaseArtifact 33 | 34 | ReleaseEnvironment ReleaseDeploymentEnvironment 35 | 36 | PreDeployApprovals []ReleaseEnvironmentApproval 37 | PostDeployApprovals []ReleaseEnvironmentApproval 38 | 39 | Reason string 40 | DeploymentStatus string 41 | OperationStatus string 42 | 43 | Attempt int64 44 | 45 | // sometimes dates are not valid here 46 | QueuedOn string `json:"queuedOn,omitempty"` 47 | StartedOn string `json:"startedOn,omitempty"` 48 | CompletedOn string `json:"completedOn,omitempty"` 49 | 50 | RequestedBy IdentifyRef 51 | RequestedFor IdentifyRef 52 | 53 | Links Links `json:"_links"` 54 | } 55 | 56 | type ReleaseDeploymentEnvironment struct { 57 | Id int64 58 | Name string 59 | } 60 | 61 | func (d *ReleaseDeployment) ApprovedBy() string { 62 | var approverList []string 63 | for _, approval := range d.PreDeployApprovals { 64 | if !approval.IsAutomated { 65 | if approval.ApprovedBy.DisplayName != "" { 66 | approverList = append(approverList, approval.ApprovedBy.DisplayName) 67 | } 68 | } 69 | } 70 | 71 | return strings.Join(approverList[:], ",") 72 | } 73 | 74 | func (d *ReleaseDeployment) QueuedOnTime() *time.Time { 75 | return parseTime(d.QueuedOn) 76 | } 77 | 78 | func (d *ReleaseDeployment) StartedOnTime() *time.Time { 79 | return parseTime(d.StartedOn) 80 | } 81 | 82 | func (d *ReleaseDeployment) CompletedOnTime() *time.Time { 83 | return parseTime(d.CompletedOn) 84 | } 85 | 86 | func (c *AzureDevopsClient) ListReleaseDeployments(project string, releaseDefinitionId int64) (list ReleaseDeploymentList, error error) { 87 | defer c.concurrencyUnlock() 88 | c.concurrencyLock() 89 | 90 | url := fmt.Sprintf( 91 | "%v/_apis/release/deployments?api-version=%v&isDeleted=false&$expand=94&definitionId=%s&$top=%v", 92 | url.QueryEscape(project), 93 | url.QueryEscape(c.ApiVersion), 94 | url.QueryEscape(int64ToString(releaseDefinitionId)), 95 | url.QueryEscape(int64ToString(c.LimitDeploymentPerDefinition)), 96 | ) 97 | response, err := c.restVsrm().R().Get(url) 98 | if err := c.checkResponse(response, err); err != nil { 99 | error = err 100 | return 101 | } 102 | 103 | err = json.Unmarshal(response.Body(), &list) 104 | if err != nil { 105 | error = err 106 | return 107 | } 108 | 109 | return 110 | } 111 | -------------------------------------------------------------------------------- /azure-devops-client/repository.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type RepositoryList struct { 11 | Count int `json:"count"` 12 | List []Repository `json:"value"` 13 | } 14 | 15 | type Repository struct { 16 | Id string 17 | Name string 18 | Url string 19 | State string 20 | WellFormed string 21 | Revision int64 22 | Visibility string 23 | Size int64 24 | 25 | IsDisabled *bool `json:"isDisabled"` 26 | 27 | Links Links `json:"_links"` 28 | } 29 | 30 | type RepositoryCommitList struct { 31 | Count int `json:"count"` 32 | List []RepositoryCommit `json:"value"` 33 | } 34 | 35 | type RepositoryCommit struct { 36 | CommitId string 37 | Author Author 38 | Committer Author 39 | Comment string 40 | CommentTruncated bool 41 | ChangeCounts struct { 42 | Add int64 43 | Edit int64 44 | Delete int64 45 | } 46 | 47 | Url string 48 | RemoteUrl string 49 | } 50 | 51 | type RepositoryPushList struct { 52 | Count int `json:"count"` 53 | List []RepositoryPush `json:"value"` 54 | } 55 | 56 | type RepositoryPush struct { 57 | PushId int64 58 | } 59 | 60 | func (c *AzureDevopsClient) ListRepositories(project string) (list RepositoryList, error error) { 61 | defer c.concurrencyUnlock() 62 | c.concurrencyLock() 63 | 64 | url := fmt.Sprintf( 65 | "%v/_apis/git/repositories", 66 | url.QueryEscape(project), 67 | ) 68 | response, err := c.rest().R().Get(url) 69 | if err := c.checkResponse(response, err); err != nil { 70 | error = err 71 | return 72 | } 73 | err = json.Unmarshal(response.Body(), &list) 74 | if err != nil { 75 | error = err 76 | return 77 | } 78 | 79 | return 80 | } 81 | 82 | func (c *AzureDevopsClient) ListCommits(project string, repository string, fromDate time.Time) (list RepositoryCommitList, error error) { 83 | defer c.concurrencyUnlock() 84 | c.concurrencyLock() 85 | 86 | url := fmt.Sprintf( 87 | "_apis/git/repositories/%s/commits?searchCriteria.fromDate=%s&api-version=%v", 88 | url.QueryEscape(repository), 89 | url.QueryEscape(fromDate.UTC().Format(time.RFC3339)), 90 | url.QueryEscape(c.ApiVersion), 91 | ) 92 | 93 | response, err := c.rest().R().Get(url) 94 | if err := c.checkResponse(response, err); err != nil { 95 | error = err 96 | return 97 | } 98 | 99 | err = json.Unmarshal(response.Body(), &list) 100 | if err != nil { 101 | error = err 102 | return 103 | } 104 | 105 | return 106 | } 107 | 108 | func (c *AzureDevopsClient) ListPushes(project string, repository string, fromDate time.Time) (list RepositoryPushList, error error) { 109 | defer c.concurrencyUnlock() 110 | c.concurrencyLock() 111 | 112 | url := fmt.Sprintf( 113 | "_apis/git/repositories/%s/pushes?searchCriteria.fromDate=%s&api-version=%v", 114 | url.QueryEscape(repository), 115 | url.QueryEscape(fromDate.UTC().Format(time.RFC3339)), 116 | url.QueryEscape(c.ApiVersion), 117 | ) 118 | 119 | response, err := c.rest().R().Get(url) 120 | if err := c.checkResponse(response, err); err != nil { 121 | error = err 122 | return 123 | } 124 | 125 | err = json.Unmarshal(response.Body(), &list) 126 | if err != nil { 127 | error = err 128 | return 129 | } 130 | 131 | return 132 | } 133 | 134 | func (r *Repository) Disabled() (ret bool) { 135 | if r.IsDisabled != nil { 136 | return *r.IsDisabled 137 | } 138 | 139 | return false 140 | } 141 | -------------------------------------------------------------------------------- /azure-devops-client/resource_usage.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type ( 10 | ResourceUsageBuild struct { 11 | DistributedTaskAgents *int `json:"distributedTaskAgents"` 12 | PaidPrivateAgentSlots *int `json:"paidPrivateAgentSlots"` 13 | TotalUsage *int `json:"totalUsage"` 14 | XamlControllers *int `json:"xamlControllers"` 15 | } 16 | 17 | ResourceUsageAgent struct { 18 | Data struct { 19 | Provider struct { 20 | IncludeResourceLimitsSection bool `json:"includeResourceLimitsSection"` 21 | IncludeConcurrentJobsSection bool `json:"includeConcurrentJobsSection"` 22 | 23 | ResourceUsages []ResourceUsageAgentUsageRow `json:"resourceUsages"` 24 | 25 | TaskHubLicenseDetails struct { 26 | FreeLicenseCount *float64 `json:"freeLicenseCount"` 27 | FreeHostedLicenseCount *float64 `json:"freeHostedLicenseCount"` 28 | EnterpriseUsersCount *float64 `json:"enterpriseUsersCount"` 29 | PurchasedLicenseCount *float64 `json:"purchasedLicenseCount"` 30 | PurchasedHostedLicenseCount *float64 `json:"purchasedHostedLicenseCount"` 31 | HostedLicensesArePremium bool `json:"hostedLicensesArePremium"` 32 | TotalLicenseCount *float64 `json:"totalLicenseCount"` 33 | HasLicenseCountEverUpdated bool `json:"hasLicenseCountEverUpdated"` 34 | MsdnUsersCount *float64 `json:"msdnUsersCount"` 35 | HostedAgentMinutesFreeCount *float64 `json:"hostedAgentMinutesFreeCount"` 36 | HostedAgentMinutesUsedCount *float64 `json:"hostedAgentMinutesUsedCount"` 37 | FailedToReachAllProviders bool `json:"failedToReachAllProviders"` 38 | TotalPrivateLicenseCount *float64 `json:"totalPrivateLicenseCount"` 39 | TotalHostedLicenseCount *float64 `json:"totalHostedLicenseCount"` 40 | } `json:"taskHubLicenseDetails"` 41 | } `json:"ms.vss-build-web.build-queue-hub-data-provider"` 42 | } `json:"data"` 43 | } 44 | 45 | ResourceUsageAgentUsageRow struct { 46 | ResourceLimit struct { 47 | ResourceLimitsData struct { 48 | FreeCount string `json:"freeCount"` 49 | PurchasedCount string `json:"purchasedCount"` 50 | } `json:"resourceLimitsData"` 51 | 52 | HostId string `json:"hostId"` 53 | ParallelismTag string `json:"parallelismTag"` 54 | IsHosted bool `json:"isHosted"` 55 | TotalCount float64 `json:"totalCount"` 56 | IsPremium bool `json:"IsPremium"` 57 | } `json:"resourceLimit"` 58 | } 59 | ) 60 | 61 | func (c *AzureDevopsClient) GetResourceUsageBuild() (ret ResourceUsageBuild, error error) { 62 | defer c.concurrencyUnlock() 63 | c.concurrencyLock() 64 | 65 | url := fmt.Sprintf( 66 | "/_apis/build/resourceusage?api-version=%v", 67 | // FIXME: hardcoded api version 68 | url.QueryEscape("5.1-preview.2"), 69 | ) 70 | response, err := c.rest().R().Get(url) 71 | if err := c.checkResponse(response, err); err != nil { 72 | error = err 73 | return 74 | } 75 | 76 | err = json.Unmarshal(response.Body(), &ret) 77 | if err != nil { 78 | error = err 79 | return 80 | } 81 | 82 | return 83 | } 84 | 85 | func (c *AzureDevopsClient) GetResourceUsageAgent() (ret ResourceUsageAgent, error error) { 86 | defer c.concurrencyUnlock() 87 | c.concurrencyLock() 88 | 89 | url := fmt.Sprintf( 90 | "/_apis/Contribution/dataProviders/query?api-version=%v", 91 | // FIXME: hardcoded api version 92 | url.QueryEscape("5.1-preview.1"), 93 | ) 94 | 95 | payload := `{"contributionIds": ["ms.vss-build-web.build-queue-hub-data-provider"]}` 96 | 97 | req := c.rest().NewRequest() 98 | req.SetHeader("Content-Type", "application/json") 99 | req.SetBody(payload) 100 | response, err := req.Post(url) 101 | if err := c.checkResponse(response, err); err != nil { 102 | error = err 103 | return 104 | } 105 | 106 | err = json.Unmarshal(response.Body(), &ret) 107 | if err != nil { 108 | error = err 109 | return 110 | } 111 | 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /azure-devops-client/workitem.go: -------------------------------------------------------------------------------- 1 | package AzureDevopsClient 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type WorkItem struct { 8 | Id int64 `json:"id"` 9 | Fields WorkItemFields `json:"fields"` 10 | } 11 | 12 | type WorkItemFields struct { 13 | Title string `json:"System.Title"` 14 | Path string `json:"System.AreaPath"` 15 | CreatedDate string `json:"System.CreatedDate"` 16 | AcceptedDate string `json:"Microsoft.VSTS.CodeReview.AcceptedDate"` 17 | ResolvedDate string `json:"Microsoft.VSTS.Common.ResolvedDate"` 18 | ClosedDate string `json:"Microsoft.VSTS.Common.ClosedDate"` 19 | } 20 | 21 | func (c *AzureDevopsClient) GetWorkItem(workItemUrl string) (workItem WorkItem, error error) { 22 | defer c.concurrencyUnlock() 23 | c.concurrencyLock() 24 | 25 | response, err := c.rest().R().Get(workItemUrl) 26 | if err := c.checkResponse(response, err); err != nil { 27 | error = err 28 | return 29 | } 30 | 31 | err = json.Unmarshal(response.Body(), &workItem) 32 | if err != nil { 33 | error = err 34 | } 35 | 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /common.logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | var ( 9 | logger *zap.SugaredLogger 10 | ) 11 | 12 | func initLogger() *zap.SugaredLogger { 13 | var config zap.Config 14 | if Opts.Logger.Development { 15 | config = zap.NewDevelopmentConfig() 16 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 17 | } else { 18 | config = zap.NewProductionConfig() 19 | } 20 | 21 | config.Encoding = "console" 22 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 23 | 24 | // debug level 25 | if Opts.Logger.Debug { 26 | config.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) 27 | } 28 | 29 | // json log format 30 | if Opts.Logger.Json { 31 | config.Encoding = "json" 32 | 33 | // if running in containers, logs already enriched with timestamp by the container runtime 34 | config.EncoderConfig.TimeKey = "" 35 | } 36 | 37 | // build logger 38 | log, err := config.Build() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | logger = log.Sugar() 44 | 45 | return logger 46 | } 47 | -------------------------------------------------------------------------------- /common.system.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/webdevops/go-common/system" 5 | ) 6 | 7 | func initSystem() { 8 | system.AutoProcMemLimit(logger) 9 | } 10 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | exporter: 3 | build: . 4 | ports: 5 | - "8000:8000" 6 | environment: 7 | SERVER_BIND: ${SERVER_BIND} 8 | AZURE_DEVOPS_URL: ${AZURE_DEVOPS_URL} 9 | AZURE_DEVOPS_ORGANISATION: ${AZURE_DEVOPS_ORGANISATION} 10 | AZURE_DEVOPS_ACCESS_TOKEN: ${AZURE_DEVOPS_ACCESS_TOKEN} 11 | AZURE_DEVOPS_FILTER_PROJECT: 72690669-de93-4a98-84a9-8300ce32a2f2 12 | LIMIT_BUILDS_PER_PROJECT: 500 13 | LIMIT_BUILDS_PER_DEFINITION: 100 14 | SERVER_TIMEOUT_READ: 15s 15 | AZURE_DEVOPS_FETCH_ALL_BUILDS: "true" 16 | #AZURE_DEVOPS_FILTER_TIMELINE_STATE: "completed inProgress pending" -------------------------------------------------------------------------------- /config/opts.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type ( 9 | Opts struct { 10 | // logger 11 | Logger struct { 12 | Debug bool `long:"log.debug" env:"LOG_DEBUG" description:"debug mode"` 13 | Development bool `long:"log.devel" env:"LOG_DEVEL" description:"development mode"` 14 | Json bool `long:"log.json" env:"LOG_JSON" description:"Switch log output to json format"` 15 | } 16 | 17 | // scrape time settings 18 | Scrape struct { 19 | Time time.Duration `long:"scrape.time" env:"SCRAPE_TIME" description:"Default scrape time (time.duration)" default:"30m"` 20 | TimeProjects *time.Duration `long:"scrape.time.projects" env:"SCRAPE_TIME_PROJECTS" description:"Scrape time for project metrics (time.duration)"` 21 | TimeRepository *time.Duration `long:"scrape.time.repository" env:"SCRAPE_TIME_REPOSITORY" description:"Scrape time for repository metrics (time.duration)"` 22 | TimeBuild *time.Duration `long:"scrape.time.build" env:"SCRAPE_TIME_BUILD" description:"Scrape time for build metrics (time.duration)"` 23 | TimeRelease *time.Duration `long:"scrape.time.release" env:"SCRAPE_TIME_RELEASE" description:"Scrape time for release metrics (time.duration)"` 24 | TimeDeployment *time.Duration `long:"scrape.time.deployment" env:"SCRAPE_TIME_DEPLOYMENT" description:"Scrape time for deployment metrics (time.duration)"` 25 | TimePullRequest *time.Duration `long:"scrape.time.pullrequest" env:"SCRAPE_TIME_PULLREQUEST" description:"Scrape time for pullrequest metrics (time.duration)"` 26 | TimeStats *time.Duration `long:"scrape.time.stats" env:"SCRAPE_TIME_STATS" description:"Scrape time for stats metrics (time.duration)"` 27 | TimeResourceUsage *time.Duration `long:"scrape.time.resourceusage" env:"SCRAPE_TIME_RESOURCEUSAGE" description:"Scrape time for resourceusage metrics (time.duration)"` 28 | TimeQuery *time.Duration `long:"scrape.time.query" env:"SCRAPE_TIME_QUERY" description:"Scrape time for query results (time.duration)"` 29 | TimeLive *time.Duration `long:"scrape.time.live" env:"SCRAPE_TIME_LIVE" description:"Scrape time for live metrics (time.duration)" default:"30s"` 30 | } 31 | 32 | // summary options 33 | Stats struct { 34 | SummaryMaxAge *time.Duration `long:"stats.summary.maxage" env:"STATS_SUMMARY_MAX_AGE" description:"Stats Summary metrics max age (time.duration)"` 35 | } 36 | 37 | // azure settings 38 | Azure struct { 39 | TenantId string `long:"azure.tenant-id" env:"AZURE_TENANT_ID" description:"Azure tenant ID for Service Principal authentication"` 40 | ClientId string `long:"azure.client-id" env:"AZURE_CLIENT_ID" description:"Client ID for Service Principal authentication"` 41 | ClientSecret string `long:"azure.client-secret" env:"AZURE_CLIENT_SECRET" description:"Client secret for Service Principal authentication" json:"-"` 42 | } 43 | 44 | // azure settings 45 | AzureDevops struct { 46 | Url *string `long:"azuredevops.url" env:"AZURE_DEVOPS_URL" description:"Azure DevOps URL (empty if hosted by Microsoft)"` 47 | AccessToken string `long:"azuredevops.access-token" env:"AZURE_DEVOPS_ACCESS_TOKEN" description:"Azure DevOps access token" json:"-"` 48 | AccessTokenFile *string `long:"azuredevops.access-token-file" env:"AZURE_DEVOPS_ACCESS_TOKEN_FILE" description:"Azure DevOps access token (from file)"` 49 | Organisation string `long:"azuredevops.organisation" env:"AZURE_DEVOPS_ORGANISATION" description:"Azure DevOps organization" required:"true"` 50 | ApiVersion string `long:"azuredevops.apiversion" env:"AZURE_DEVOPS_APIVERSION" description:"Azure DevOps API version" default:"5.1"` 51 | 52 | // agentpool 53 | AgentPoolIdList *[]int64 `long:"azuredevops.agentpool" env:"AZURE_DEVOPS_AGENTPOOL" env-delim:" " description:"Enable scrape metrics for agent pool (IDs)"` 54 | 55 | // ignore settings 56 | FilterProjects []string `long:"whitelist.project" env:"AZURE_DEVOPS_FILTER_PROJECT" env-delim:" " description:"Filter projects (UUIDs)"` 57 | BlacklistProjects []string `long:"blacklist.project" env:"AZURE_DEVOPS_BLACKLIST_PROJECT" env-delim:" " description:"Filter projects (UUIDs)"` 58 | 59 | FilterTimelineState []string `long:"timeline.state" env:"AZURE_DEVOPS_FILTER_TIMELINE_STATE" env-delim:" " description:"Filter timeline states (completed, inProgress, pending)" default:"completed"` 60 | FetchAllBuildsFilter []string `long:"builds.all.project" env:"AZURE_DEVOPS_FETCH_ALL_BUILDS_FILTER_PROJECT" env-delim:" " description:"Fetch all builds from projects (UUIDs or names)"` 61 | 62 | // query settings 63 | QueriesWithProjects []string `long:"list.query" env:"AZURE_DEVOPS_QUERIES" env-delim:" " description:"Pairs of query and project UUIDs in the form: '@'"` 64 | 65 | // tag settings 66 | TagsSchema *[]string `long:"tags.schema" env:"AZURE_DEVOPS_TAG_SCHEMA" env-delim:" " description:"Tags to be extracted from builds in the format 'tagName:type' with following types: number, info, bool"` 67 | TagsBuildDefinitionIdList *[]int64 `long:"tags.build.definition" env:"AZURE_DEVOPS_TAG_BUILD_DEFINITION" env-delim:" " description:"Build definition ids to query tags (IDs)"` 68 | } 69 | 70 | // cache settings 71 | Cache struct { 72 | Path string `long:"cache.path" env:"CACHE_PATH" description:"Cache path (to folder, file://path... or azblob://storageaccount.blob.core.windows.net/containername or k8scm://{namespace}/{configmap}})"` 73 | } 74 | 75 | Request struct { 76 | ConcurrencyLimit int64 `long:"request.concurrency" env:"REQUEST_CONCURRENCY" description:"Number of concurrent requests against dev.azure.com" default:"10"` 77 | Retries int `long:"request.retries" env:"REQUEST_RETRIES" description:"Number of retried requests against dev.azure.com" default:"3"` 78 | } 79 | 80 | ServiceDiscovery struct { 81 | RefreshDuration time.Duration `long:"servicediscovery.refresh" env:"SERVICEDISCOVERY_REFRESH" description:"Refresh duration for servicediscovery (time.duration)" default:"30m"` 82 | } 83 | 84 | Limit struct { 85 | Project int64 `long:"limit.project" env:"LIMIT_PROJECT" description:"Limit number of projects" default:"100"` 86 | BuildsPerProject int64 `long:"limit.builds-per-project" env:"LIMIT_BUILDS_PER_PROJECT" description:"Limit builds per project" default:"100"` 87 | BuildsPerDefinition int64 `long:"limit.builds-per-definition" env:"LIMIT_BUILDS_PER_DEFINITION" description:"Limit builds per definition" default:"10"` 88 | ReleasesPerProject int64 `long:"limit.releases-per-project" env:"LIMIT_RELEASES_PER_PROJECT" description:"Limit releases per project" default:"100"` 89 | ReleasesPerDefinition int64 `long:"limit.releases-per-definition" env:"LIMIT_RELEASES_PER_DEFINITION" description:"Limit releases per definition" default:"100"` 90 | DeploymentPerDefinition int64 `long:"limit.deployments-per-definition" env:"LIMIT_DEPLOYMENTS_PER_DEFINITION" description:"Limit deployments per definition" default:"100"` 91 | ReleaseDefinitionsPerProject int64 `long:"limit.releasedefinitions-per-project" env:"LIMIT_RELEASEDEFINITION_PER_PROJECT" description:"Limit builds per definition" default:"100"` 92 | BuildHistoryDuration time.Duration `long:"limit.build-history-duration" env:"LIMIT_BUILD_HISTORY_DURATION" description:"Time (time.Duration) how long the exporter should look back for builds" default:"48h"` 93 | ReleaseHistoryDuration time.Duration `long:"limit.release-history-duration" env:"LIMIT_RELEASE_HISTORY_DURATION" description:"Time (time.Duration) how long the exporter should look back for releases" default:"48h"` 94 | } 95 | 96 | Server struct { 97 | // general options 98 | Bind string `long:"server.bind" env:"SERVER_BIND" description:"Server address" default:":8080"` 99 | ReadTimeout time.Duration `long:"server.timeout.read" env:"SERVER_TIMEOUT_READ" description:"Server read timeout" default:"5s"` 100 | WriteTimeout time.Duration `long:"server.timeout.write" env:"SERVER_TIMEOUT_WRITE" description:"Server write timeout" default:"10s"` 101 | } 102 | } 103 | ) 104 | 105 | func (o *Opts) GetCachePath(path string) (ret *string) { 106 | if o.Cache.Path != "" { 107 | tmp := o.Cache.Path + "/" + path 108 | ret = &tmp 109 | } 110 | 111 | return 112 | } 113 | 114 | func (o *Opts) GetJson() []byte { 115 | jsonBytes, err := json.Marshal(o) 116 | if err != nil { 117 | panic(err) 118 | } 119 | return jsonBytes 120 | } 121 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webdevops/azure-devops-exporter 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/go-resty/resty/v2 v2.16.5 9 | github.com/prometheus/client_golang v1.22.0 10 | github.com/prometheus/common v0.63.0 // indirect 11 | github.com/prometheus/procfs v0.16.1 // indirect 12 | golang.org/x/net v0.39.0 // indirect 13 | golang.org/x/sys v0.32.0 // indirect 14 | google.golang.org/protobuf v1.36.6 // indirect 15 | ) 16 | 17 | require ( 18 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 19 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 20 | github.com/jessevdk/go-flags v1.6.1 21 | github.com/patrickmn/go-cache v2.1.0+incompatible 22 | github.com/remeh/sizedwaitgroup v1.0.0 23 | github.com/webdevops/go-common v0.0.0-20250501225441-53b22a3a9550 24 | go.uber.org/zap v1.27.0 25 | ) 26 | 27 | require ( 28 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 29 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect 30 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect 31 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 // indirect 32 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect 33 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect 34 | github.com/KimMachineGun/automemlimit v0.7.1 // indirect 35 | github.com/beorn7/perks v1.0.1 // indirect 36 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 38 | github.com/dustin/go-humanize v1.0.1 // indirect 39 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 40 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 41 | github.com/go-logr/logr v1.4.2 // indirect 42 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 43 | github.com/go-openapi/jsonreference v0.21.0 // indirect 44 | github.com/go-openapi/swag v0.23.1 // indirect 45 | github.com/gogo/protobuf v1.3.2 // indirect 46 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 47 | github.com/google/gnostic-models v0.6.9 // indirect 48 | github.com/google/go-cmp v0.7.0 // indirect 49 | github.com/google/uuid v1.6.0 // indirect 50 | github.com/josharian/intern v1.0.0 // indirect 51 | github.com/json-iterator/go v1.1.12 // indirect 52 | github.com/kylelemons/godebug v1.1.0 // indirect 53 | github.com/mailru/easyjson v0.9.0 // indirect 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 55 | github.com/modern-go/reflect2 v1.0.2 // indirect 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 57 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 58 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 59 | github.com/pkg/errors v0.9.1 // indirect 60 | github.com/prometheus/client_model v0.6.2 // indirect 61 | github.com/robfig/cron v1.2.0 // indirect 62 | github.com/x448/float16 v0.8.4 // indirect 63 | go.uber.org/automaxprocs v1.6.0 // indirect 64 | go.uber.org/multierr v1.11.0 // indirect 65 | go.uber.org/zap/exp v0.3.0 // indirect 66 | golang.org/x/crypto v0.37.0 // indirect 67 | golang.org/x/oauth2 v0.29.0 // indirect 68 | golang.org/x/term v0.31.0 // indirect 69 | golang.org/x/text v0.24.0 // indirect 70 | golang.org/x/time v0.11.0 // indirect 71 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | k8s.io/api v0.33.0 // indirect 75 | k8s.io/apimachinery v0.33.0 // indirect 76 | k8s.io/client-go v0.33.0 // indirect 77 | k8s.io/klog/v2 v2.130.1 // indirect 78 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 79 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 80 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 81 | sigs.k8s.io/randfill v1.0.0 // indirect 82 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 83 | sigs.k8s.io/yaml v1.4.0 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 3 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= 4 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= 5 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= 6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= 7 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= 8 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= 10 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= 11 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= 12 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= 13 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= 14 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= 15 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= 16 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= 17 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= 18 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= 19 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo= 20 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA= 21 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= 22 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= 23 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= 24 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= 25 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 26 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 27 | github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= 28 | github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 29 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 30 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 31 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 32 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 33 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 38 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 39 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 40 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 41 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 42 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 43 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 44 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 45 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 46 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 47 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 48 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 49 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 50 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 51 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 52 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 53 | github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= 54 | github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= 55 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 56 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 57 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 58 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 59 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 60 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 61 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 62 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 63 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 64 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 65 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 66 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 67 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 68 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 69 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 70 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 71 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 72 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 73 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 74 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 75 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 76 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 77 | github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= 78 | github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= 79 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 80 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 81 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 82 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 83 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 84 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 85 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 86 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 87 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 88 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 89 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 90 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 91 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 93 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 94 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 95 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 96 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 97 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 98 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 99 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 100 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 101 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 102 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 103 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 104 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 105 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 106 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 107 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 108 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 109 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 111 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 112 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 113 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 114 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 115 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 116 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 117 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 118 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 119 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 120 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 121 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 122 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 123 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 124 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 125 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= 126 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= 127 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 128 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 129 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 130 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 131 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 132 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 135 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 136 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 137 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 138 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 139 | github.com/webdevops/go-common v0.0.0-20250501225441-53b22a3a9550 h1:9Rhejj9T4vEVq7wwL/IPRBqC51Tt6SDmSxgAqXJT7MI= 140 | github.com/webdevops/go-common v0.0.0-20250501225441-53b22a3a9550/go.mod h1:GzD/xLtTZ5Vh3aHTi02g0OlfDUoiDx44OHeUnqWO2CI= 141 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 142 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 143 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 144 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 145 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 146 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 147 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 148 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 149 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 150 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 151 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 152 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 153 | go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= 154 | go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= 155 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 156 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 157 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 158 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 159 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 160 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 161 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 162 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 163 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 164 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 165 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 166 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 167 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 168 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 169 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 170 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 178 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 179 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 180 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 181 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 182 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 183 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 184 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 185 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 186 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 187 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 188 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 189 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 190 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 191 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 192 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 193 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 194 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 195 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 196 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 198 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 199 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 200 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 201 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 202 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 203 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 204 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 205 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 206 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 207 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 208 | k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= 209 | k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= 210 | k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 211 | k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 212 | k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= 213 | k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= 214 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 215 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 216 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 217 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 218 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 219 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 220 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 221 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 222 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 223 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 224 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 225 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 226 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 227 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 228 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 229 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/jessevdk/go-flags" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | "github.com/webdevops/go-common/prometheus/collector" 14 | "go.uber.org/zap" 15 | 16 | AzureDevops "github.com/webdevops/azure-devops-exporter/azure-devops-client" 17 | "github.com/webdevops/azure-devops-exporter/config" 18 | ) 19 | 20 | const ( 21 | Author = "webdevops.io" 22 | 23 | cacheTag = "v1" 24 | ) 25 | 26 | var ( 27 | argparser *flags.Parser 28 | Opts config.Opts 29 | 30 | AzureDevopsClient *AzureDevops.AzureDevopsClient 31 | AzureDevopsServiceDiscovery *azureDevopsServiceDiscovery 32 | 33 | // Git version information 34 | gitCommit = "" 35 | gitTag = "" 36 | ) 37 | 38 | func main() { 39 | initArgparser() 40 | initLogger() 41 | parseArguments() 42 | 43 | logger.Infof("starting azure-devops-exporter v%s (%s; %s; by %v)", gitTag, gitCommit, runtime.Version(), Author) 44 | logger.Info(string(Opts.GetJson())) 45 | initSystem() 46 | 47 | logger.Infof("init AzureDevOps connection") 48 | initAzureDevOpsConnection() 49 | AzureDevopsServiceDiscovery = NewAzureDevopsServiceDiscovery() 50 | AzureDevopsServiceDiscovery.Update() 51 | 52 | logger.Info("init metrics collection") 53 | initMetricCollector() 54 | 55 | logger.Infof("starting http server on %s", Opts.Server.Bind) 56 | startHttpServer() 57 | } 58 | 59 | // init argparser and parse/validate arguments 60 | func initArgparser() { 61 | argparser = flags.NewParser(&Opts, flags.Default) 62 | _, err := argparser.Parse() 63 | 64 | // check if there is an parse error 65 | if err != nil { 66 | var flagsErr *flags.Error 67 | if ok := errors.As(err, &flagsErr); ok && flagsErr.Type == flags.ErrHelp { 68 | os.Exit(0) 69 | } else { 70 | fmt.Println() 71 | argparser.WriteHelp(os.Stdout) 72 | os.Exit(1) 73 | } 74 | } 75 | } 76 | 77 | // parses and validates the arguments 78 | func parseArguments() { 79 | // load accesstoken from file 80 | if Opts.AzureDevops.AccessTokenFile != nil && len(*Opts.AzureDevops.AccessTokenFile) > 0 { 81 | logger.Infof("reading access token from file \"%s\"", *Opts.AzureDevops.AccessTokenFile) 82 | // load access token from file 83 | if val, err := os.ReadFile(*Opts.AzureDevops.AccessTokenFile); err == nil { 84 | Opts.AzureDevops.AccessToken = strings.TrimSpace(string(val)) 85 | } else { 86 | logger.Fatalf("unable to read access token file \"%s\": %v", *Opts.AzureDevops.AccessTokenFile, err) 87 | } 88 | } 89 | 90 | if len(Opts.AzureDevops.AccessToken) == 0 && (len(Opts.Azure.TenantId) == 0 || len(Opts.Azure.ClientId) == 0) { 91 | logger.Fatalf("neither an Azure DevOps PAT token nor client credentials (tenant ID, client ID) for service principal authentication have been provided") 92 | } 93 | 94 | // ensure query paths and projects are splitted by '@' 95 | if Opts.AzureDevops.QueriesWithProjects != nil { 96 | queryError := false 97 | for _, query := range Opts.AzureDevops.QueriesWithProjects { 98 | if strings.Count(query, "@") != 1 { 99 | fmt.Println("Query path '", query, "' is malformed; should be '@'") 100 | queryError = true 101 | } 102 | } 103 | if queryError { 104 | os.Exit(1) 105 | } 106 | } 107 | 108 | // use default scrape time if null 109 | if Opts.Scrape.TimeProjects == nil { 110 | Opts.Scrape.TimeProjects = &Opts.Scrape.Time 111 | } 112 | 113 | if Opts.Scrape.TimeRepository == nil { 114 | Opts.Scrape.TimeRepository = &Opts.Scrape.Time 115 | } 116 | 117 | if Opts.Scrape.TimePullRequest == nil { 118 | Opts.Scrape.TimePullRequest = &Opts.Scrape.Time 119 | } 120 | 121 | if Opts.Scrape.TimeBuild == nil { 122 | Opts.Scrape.TimeBuild = &Opts.Scrape.Time 123 | } 124 | 125 | if Opts.Scrape.TimeRelease == nil { 126 | Opts.Scrape.TimeRelease = &Opts.Scrape.Time 127 | } 128 | 129 | if Opts.Scrape.TimeDeployment == nil { 130 | Opts.Scrape.TimeDeployment = &Opts.Scrape.Time 131 | } 132 | 133 | if Opts.Scrape.TimeStats == nil { 134 | Opts.Scrape.TimeStats = &Opts.Scrape.Time 135 | } 136 | 137 | if Opts.Scrape.TimeResourceUsage == nil { 138 | Opts.Scrape.TimeResourceUsage = &Opts.Scrape.Time 139 | } 140 | 141 | if Opts.Stats.SummaryMaxAge == nil { 142 | Opts.Stats.SummaryMaxAge = Opts.Scrape.TimeStats 143 | } 144 | 145 | if Opts.Scrape.TimeQuery == nil { 146 | Opts.Scrape.TimeQuery = &Opts.Scrape.Time 147 | } 148 | 149 | if v := os.Getenv("AZURE_DEVOPS_FILTER_AGENTPOOL"); v != "" { 150 | logger.Fatal("deprecated env var AZURE_DEVOPS_FILTER_AGENTPOOL detected, please use AZURE_DEVOPS_AGENTPOOL") 151 | } 152 | } 153 | 154 | // Init and build Azure authorzier 155 | func initAzureDevOpsConnection() { 156 | AzureDevopsClient = AzureDevops.NewAzureDevopsClient(logger) 157 | if Opts.AzureDevops.Url != nil { 158 | AzureDevopsClient.HostUrl = Opts.AzureDevops.Url 159 | } 160 | 161 | logger.Infof("using organization: %v", Opts.AzureDevops.Organisation) 162 | logger.Infof("using apiversion: %v", Opts.AzureDevops.ApiVersion) 163 | logger.Infof("using concurrency: %v", Opts.Request.ConcurrencyLimit) 164 | logger.Infof("using retries: %v", Opts.Request.Retries) 165 | 166 | // ensure AZURE env vars are populated for azidentity 167 | if Opts.Azure.TenantId != "" { 168 | if err := os.Setenv("AZURE_TENANT_ID", Opts.Azure.TenantId); err != nil { 169 | panic(err) 170 | } 171 | } 172 | 173 | if Opts.Azure.ClientId != "" { 174 | if err := os.Setenv("AZURE_CLIENT_ID", Opts.Azure.ClientId); err != nil { 175 | panic(err) 176 | } 177 | } 178 | 179 | if Opts.Azure.ClientSecret != "" { 180 | if err := os.Setenv("AZURE_CLIENT_SECRET", Opts.Azure.ClientSecret); err != nil { 181 | panic(err) 182 | } 183 | } 184 | 185 | AzureDevopsClient.SetOrganization(Opts.AzureDevops.Organisation) 186 | if Opts.AzureDevops.AccessToken != "" { 187 | AzureDevopsClient.SetAccessToken(Opts.AzureDevops.AccessToken) 188 | } else { 189 | if err := AzureDevopsClient.UseAzAuth(); err != nil { 190 | logger.Fatalf(err.Error()) 191 | } 192 | } 193 | AzureDevopsClient.SetApiVersion(Opts.AzureDevops.ApiVersion) 194 | AzureDevopsClient.SetConcurrency(Opts.Request.ConcurrencyLimit) 195 | AzureDevopsClient.SetRetries(Opts.Request.Retries) 196 | AzureDevopsClient.SetUserAgent(fmt.Sprintf("azure-devops-exporter/%v", gitTag)) 197 | 198 | AzureDevopsClient.LimitProject = Opts.Limit.Project 199 | AzureDevopsClient.LimitBuildsPerProject = Opts.Limit.BuildsPerProject 200 | AzureDevopsClient.LimitBuildsPerDefinition = Opts.Limit.BuildsPerDefinition 201 | AzureDevopsClient.LimitReleasesPerDefinition = Opts.Limit.ReleasesPerDefinition 202 | AzureDevopsClient.LimitDeploymentPerDefinition = Opts.Limit.DeploymentPerDefinition 203 | AzureDevopsClient.LimitReleaseDefinitionsPerProject = Opts.Limit.ReleaseDefinitionsPerProject 204 | AzureDevopsClient.LimitReleasesPerProject = Opts.Limit.ReleasesPerProject 205 | } 206 | 207 | func initMetricCollector() { 208 | var collectorName string 209 | 210 | collectorName = "Project" 211 | if Opts.Scrape.TimeLive.Seconds() > 0 { 212 | c := collector.New(collectorName, &MetricsCollectorProject{}, logger) 213 | c.SetScapeTime(*Opts.Scrape.TimeLive) 214 | c.SetCache(Opts.GetCachePath("project.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 215 | if err := c.Start(); err != nil { 216 | logger.Fatal(err.Error()) 217 | } 218 | } else { 219 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 220 | } 221 | 222 | collectorName = "AgentPool" 223 | if Opts.Scrape.TimeLive.Seconds() > 0 { 224 | c := collector.New(collectorName, &MetricsCollectorAgentPool{}, logger) 225 | c.SetScapeTime(*Opts.Scrape.TimeLive) 226 | c.SetCache(Opts.GetCachePath("agentpool.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 227 | if err := c.Start(); err != nil { 228 | logger.Fatal(err.Error()) 229 | } 230 | } else { 231 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 232 | } 233 | 234 | collectorName = "LatestBuild" 235 | if Opts.Scrape.TimeLive.Seconds() > 0 { 236 | c := collector.New(collectorName, &MetricsCollectorLatestBuild{}, logger) 237 | c.SetScapeTime(*Opts.Scrape.TimeLive) 238 | c.SetCache(Opts.GetCachePath("latestbuild.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 239 | if err := c.Start(); err != nil { 240 | logger.Fatal(err.Error()) 241 | } 242 | } else { 243 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 244 | } 245 | 246 | collectorName = "Repository" 247 | if Opts.Scrape.TimeRepository.Seconds() > 0 { 248 | c := collector.New(collectorName, &MetricsCollectorRepository{}, logger) 249 | c.SetScapeTime(*Opts.Scrape.TimeRepository) 250 | c.SetCache(Opts.GetCachePath("latestbuild.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 251 | if err := c.Start(); err != nil { 252 | logger.Fatal(err.Error()) 253 | } 254 | } else { 255 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 256 | } 257 | 258 | collectorName = "PullRequest" 259 | if Opts.Scrape.TimePullRequest.Seconds() > 0 { 260 | c := collector.New(collectorName, &MetricsCollectorPullRequest{}, logger) 261 | c.SetScapeTime(*Opts.Scrape.TimePullRequest) 262 | c.SetCache(Opts.GetCachePath("pullrequest.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 263 | if err := c.Start(); err != nil { 264 | logger.Fatal(err.Error()) 265 | } 266 | } else { 267 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 268 | } 269 | 270 | collectorName = "Build" 271 | if Opts.Scrape.TimeBuild.Seconds() > 0 { 272 | c := collector.New(collectorName, &MetricsCollectorBuild{}, logger) 273 | c.SetScapeTime(*Opts.Scrape.TimeBuild) 274 | c.SetCache(Opts.GetCachePath("build.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 275 | if err := c.Start(); err != nil { 276 | logger.Fatal(err.Error()) 277 | } 278 | } else { 279 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 280 | } 281 | 282 | collectorName = "Release" 283 | if Opts.Scrape.TimeRelease.Seconds() > 0 { 284 | c := collector.New(collectorName, &MetricsCollectorRelease{}, logger) 285 | c.SetScapeTime(*Opts.Scrape.TimeRelease) 286 | c.SetCache(Opts.GetCachePath("release.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 287 | if err := c.Start(); err != nil { 288 | logger.Fatal(err.Error()) 289 | } 290 | } else { 291 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 292 | } 293 | 294 | collectorName = "Deployment" 295 | if Opts.Scrape.TimeDeployment.Seconds() > 0 { 296 | c := collector.New(collectorName, &MetricsCollectorDeployment{}, logger) 297 | c.SetScapeTime(*Opts.Scrape.TimeDeployment) 298 | c.SetCache(Opts.GetCachePath("deployment.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 299 | if err := c.Start(); err != nil { 300 | logger.Fatal(err.Error()) 301 | } 302 | } else { 303 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 304 | } 305 | 306 | collectorName = "Stats" 307 | if Opts.Scrape.TimeStats.Seconds() > 0 { 308 | c := collector.New(collectorName, &MetricsCollectorStats{}, logger) 309 | c.SetScapeTime(*Opts.Scrape.TimeStats) 310 | c.SetCache(Opts.GetCachePath("stats.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 311 | if err := c.Start(); err != nil { 312 | logger.Fatal(err.Error()) 313 | } 314 | } else { 315 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 316 | } 317 | 318 | collectorName = "ResourceUsage" 319 | if Opts.Scrape.TimeResourceUsage.Seconds() > 0 { 320 | c := collector.New(collectorName, &MetricsCollectorResourceUsage{}, logger) 321 | c.SetScapeTime(*Opts.Scrape.TimeResourceUsage) 322 | c.SetCache(Opts.GetCachePath("resourceusage.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 323 | if err := c.Start(); err != nil { 324 | logger.Fatal(err.Error()) 325 | } 326 | } else { 327 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 328 | } 329 | 330 | collectorName = "Query" 331 | if Opts.Scrape.TimeQuery.Seconds() > 0 { 332 | c := collector.New(collectorName, &MetricsCollectorQuery{}, logger) 333 | c.SetScapeTime(*Opts.Scrape.TimeQuery) 334 | c.SetCache(Opts.GetCachePath("query.json"), collector.BuildCacheTag(cacheTag, Opts.AzureDevops)) 335 | if err := c.Start(); err != nil { 336 | logger.Fatal(err.Error()) 337 | } 338 | } else { 339 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 340 | } 341 | } 342 | 343 | // start and handle prometheus handler 344 | func startHttpServer() { 345 | mux := http.NewServeMux() 346 | 347 | // healthz 348 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 349 | if _, err := fmt.Fprint(w, "Ok"); err != nil { 350 | logger.Error(err) 351 | } 352 | }) 353 | 354 | // readyz 355 | mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { 356 | if _, err := fmt.Fprint(w, "Ok"); err != nil { 357 | logger.Error(err) 358 | } 359 | }) 360 | 361 | mux.Handle("/metrics", promhttp.Handler()) 362 | 363 | srv := &http.Server{ 364 | Addr: Opts.Server.Bind, 365 | Handler: mux, 366 | ReadTimeout: Opts.Server.ReadTimeout, 367 | WriteTimeout: Opts.Server.WriteTimeout, 368 | } 369 | logger.Fatal(srv.ListenAndServe()) 370 | } 371 | -------------------------------------------------------------------------------- /metrics_agentpool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/webdevops/go-common/prometheus/collector" 8 | "github.com/webdevops/go-common/utils/to" 9 | "go.uber.org/zap" 10 | 11 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 12 | ) 13 | 14 | type MetricsCollectorAgentPool struct { 15 | collector.Processor 16 | 17 | prometheus struct { 18 | agentPool *prometheus.GaugeVec 19 | agentPoolSize *prometheus.GaugeVec 20 | agentPoolUsage *prometheus.GaugeVec 21 | agentPoolAgent *prometheus.GaugeVec 22 | agentPoolAgentStatus *prometheus.GaugeVec 23 | agentPoolAgentJob *prometheus.GaugeVec 24 | agentPoolQueueLength *prometheus.GaugeVec 25 | } 26 | } 27 | 28 | func (m *MetricsCollectorAgentPool) Setup(collector *collector.Collector) { 29 | m.Processor.Setup(collector) 30 | 31 | m.prometheus.agentPool = prometheus.NewGaugeVec( 32 | prometheus.GaugeOpts{ 33 | Name: "azure_devops_agentpool_info", 34 | Help: "Azure DevOps agentpool", 35 | }, 36 | []string{ 37 | "agentPoolID", 38 | "agentPoolName", 39 | "agentPoolType", 40 | "isHosted", 41 | }, 42 | ) 43 | m.Collector.RegisterMetricList("agentPool", m.prometheus.agentPool, true) 44 | 45 | m.prometheus.agentPoolSize = prometheus.NewGaugeVec( 46 | prometheus.GaugeOpts{ 47 | Name: "azure_devops_agentpool_size", 48 | Help: "Azure DevOps agentpool", 49 | }, 50 | []string{ 51 | "agentPoolID", 52 | }, 53 | ) 54 | m.Collector.RegisterMetricList("agentPoolSize", m.prometheus.agentPoolSize, true) 55 | 56 | m.prometheus.agentPoolUsage = prometheus.NewGaugeVec( 57 | prometheus.GaugeOpts{ 58 | Name: "azure_devops_agentpool_usage", 59 | Help: "Azure DevOps agentpool usage", 60 | }, 61 | []string{ 62 | "agentPoolID", 63 | }, 64 | ) 65 | m.Collector.RegisterMetricList("agentPoolUsage", m.prometheus.agentPoolUsage, true) 66 | 67 | m.prometheus.agentPoolAgent = prometheus.NewGaugeVec( 68 | prometheus.GaugeOpts{ 69 | Name: "azure_devops_agentpool_agent_info", 70 | Help: "Azure DevOps agentpool", 71 | }, 72 | []string{ 73 | "agentPoolID", 74 | "agentPoolAgentID", 75 | "agentPoolAgentName", 76 | "agentPoolAgentVersion", 77 | "provisioningState", 78 | "maxParallelism", 79 | "agentPoolAgentOs", 80 | "agentPoolAgentComputerName", 81 | "enabled", 82 | "status", 83 | "hasAssignedRequest", 84 | }, 85 | ) 86 | m.Collector.RegisterMetricList("agentPoolAgent", m.prometheus.agentPoolAgent, true) 87 | 88 | m.prometheus.agentPoolAgentStatus = prometheus.NewGaugeVec( 89 | prometheus.GaugeOpts{ 90 | Name: "azure_devops_agentpool_agent_status", 91 | Help: "Azure DevOps agentpool", 92 | }, 93 | []string{ 94 | "agentPoolAgentID", 95 | "type", 96 | }, 97 | ) 98 | m.Collector.RegisterMetricList("agentPoolAgentStatus", m.prometheus.agentPoolAgentStatus, true) 99 | 100 | m.prometheus.agentPoolAgentJob = prometheus.NewGaugeVec( 101 | prometheus.GaugeOpts{ 102 | Name: "azure_devops_agentpool_agent_job", 103 | Help: "Azure DevOps agentpool", 104 | }, 105 | []string{ 106 | "agentPoolAgentID", 107 | "jobRequestId", 108 | "definitionID", 109 | "definitionName", 110 | "planType", 111 | "scopeID", 112 | }, 113 | ) 114 | m.Collector.RegisterMetricList("agentPoolAgentJob", m.prometheus.agentPoolAgentJob, true) 115 | 116 | m.prometheus.agentPoolQueueLength = prometheus.NewGaugeVec( 117 | prometheus.GaugeOpts{ 118 | Name: "azure_devops_agentpool_queue_length", 119 | Help: "Azure DevOps agentpool", 120 | }, 121 | []string{ 122 | "agentPoolID", 123 | }, 124 | ) 125 | m.Collector.RegisterMetricList("agentPoolQueueLength", m.prometheus.agentPoolQueueLength, true) 126 | } 127 | 128 | func (m *MetricsCollectorAgentPool) Reset() {} 129 | 130 | func (m *MetricsCollectorAgentPool) Collect(callback chan<- func()) { 131 | ctx := m.Context() 132 | logger := m.Logger() 133 | 134 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 135 | projectLogger := logger.With(zap.String("project", project.Name)) 136 | m.collectAgentInfo(ctx, projectLogger, callback, project) 137 | } 138 | 139 | for _, agentPoolId := range AzureDevopsServiceDiscovery.AgentPoolList() { 140 | agentPoolLogger := logger.With(zap.Int64("agentPoolId", agentPoolId)) 141 | m.collectAgentQueues(ctx, agentPoolLogger, callback, agentPoolId) 142 | m.collectAgentPoolJobs(ctx, agentPoolLogger, callback, agentPoolId) 143 | } 144 | } 145 | 146 | func (m *MetricsCollectorAgentPool) collectAgentInfo(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 147 | list, err := AzureDevopsClient.ListAgentQueues(project.Id) 148 | if err != nil { 149 | logger.Error(err) 150 | return 151 | } 152 | 153 | agentPoolInfoMetric := m.Collector.GetMetricList("agentPool") 154 | agentPoolSizeMetric := m.Collector.GetMetricList("agentPoolSize") 155 | 156 | for _, agentQueue := range list.List { 157 | agentPoolInfoMetric.Add(prometheus.Labels{ 158 | "agentPoolID": int64ToString(agentQueue.Pool.Id), 159 | "agentPoolName": agentQueue.Name, 160 | "isHosted": to.BoolString(agentQueue.Pool.IsHosted), 161 | "agentPoolType": agentQueue.Pool.PoolType, 162 | }, 1) 163 | 164 | agentPoolSizeMetric.Add(prometheus.Labels{ 165 | "agentPoolID": int64ToString(agentQueue.Pool.Id), 166 | }, float64(agentQueue.Pool.Size)) 167 | } 168 | } 169 | 170 | func (m *MetricsCollectorAgentPool) collectAgentQueues(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), agentPoolId int64) { 171 | list, err := AzureDevopsClient.ListAgentPoolAgents(agentPoolId) 172 | if err != nil { 173 | logger.Error(err) 174 | return 175 | } 176 | 177 | agentPoolUsageMetric := m.Collector.GetMetricList("agentPoolUsage") 178 | agentPoolAgentMetric := m.Collector.GetMetricList("agentPoolAgent") 179 | agentPoolAgentStatusMetric := m.Collector.GetMetricList("agentPoolAgentStatus") 180 | agentPoolAgentJobMetric := m.Collector.GetMetricList("agentPoolAgentJob") 181 | 182 | agentPoolSize := 0 183 | agentPoolUsed := 0 184 | for _, agentPoolAgent := range list.List { 185 | agentPoolSize++ 186 | 187 | agentComputerName := "" 188 | if val, exists := agentPoolAgent.SystemCapabilities["Agent.ComputerName"]; exists { 189 | agentComputerName = val 190 | } 191 | 192 | infoLabels := prometheus.Labels{ 193 | "agentPoolID": int64ToString(agentPoolId), 194 | "agentPoolAgentID": int64ToString(agentPoolAgent.Id), 195 | "agentPoolAgentName": agentPoolAgent.Name, 196 | "agentPoolAgentVersion": agentPoolAgent.Version, 197 | "provisioningState": agentPoolAgent.ProvisioningState, 198 | "maxParallelism": int64ToString(agentPoolAgent.MaxParallelism), 199 | "agentPoolAgentOs": agentPoolAgent.OsDescription, 200 | "agentPoolAgentComputerName": agentComputerName, 201 | "enabled": to.BoolString(agentPoolAgent.Enabled), 202 | "status": agentPoolAgent.Status, 203 | "hasAssignedRequest": to.BoolString(agentPoolAgent.AssignedRequest.RequestId > 0), 204 | } 205 | 206 | agentPoolAgentMetric.Add(infoLabels, 1) 207 | 208 | statusCreatedLabels := prometheus.Labels{ 209 | "agentPoolAgentID": int64ToString(agentPoolAgent.Id), 210 | "type": "created", 211 | } 212 | agentPoolAgentStatusMetric.Add(statusCreatedLabels, timeToFloat64(agentPoolAgent.CreatedOn)) 213 | 214 | if agentPoolAgent.AssignedRequest.RequestId > 0 { 215 | agentPoolUsed++ 216 | jobLabels := prometheus.Labels{ 217 | "agentPoolAgentID": int64ToString(agentPoolAgent.Id), 218 | "planType": agentPoolAgent.AssignedRequest.PlanType, 219 | "jobRequestId": int64ToString(agentPoolAgent.AssignedRequest.RequestId), 220 | "definitionID": int64ToString(agentPoolAgent.AssignedRequest.Definition.Id), 221 | "definitionName": agentPoolAgent.AssignedRequest.Definition.Name, 222 | "scopeID": agentPoolAgent.AssignedRequest.ScopeId, 223 | } 224 | agentPoolAgentJobMetric.Add(jobLabels, timeToFloat64(*agentPoolAgent.AssignedRequest.AssignTime)) 225 | } 226 | } 227 | 228 | usage := float64(0) 229 | if agentPoolSize > 0 { 230 | usage = float64(agentPoolUsed) / float64(agentPoolSize) 231 | } 232 | agentPoolUsageMetric.Add(prometheus.Labels{ 233 | "agentPoolID": int64ToString(agentPoolId), 234 | }, usage) 235 | } 236 | 237 | func (m *MetricsCollectorAgentPool) collectAgentPoolJobs(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), agentPoolId int64) { 238 | list, err := AzureDevopsClient.ListAgentPoolJobs(agentPoolId) 239 | if err != nil { 240 | logger.Error(err) 241 | return 242 | } 243 | 244 | agentPoolQueueLengthMetric := m.Collector.GetMetricList("agentPoolQueueLength") 245 | 246 | notStartedJobCount := 0 247 | 248 | for _, agentPoolJob := range list.List { 249 | if agentPoolJob.AssignTime == nil { 250 | notStartedJobCount++ 251 | } 252 | } 253 | 254 | infoLabels := prometheus.Labels{ 255 | "agentPoolID": int64ToString(agentPoolId), 256 | } 257 | 258 | agentPoolQueueLengthMetric.Add(infoLabels, float64(notStartedJobCount)) 259 | } 260 | -------------------------------------------------------------------------------- /metrics_build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/webdevops/go-common/prometheus/collector" 11 | "go.uber.org/zap" 12 | 13 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 14 | ) 15 | 16 | type MetricsCollectorBuild struct { 17 | collector.Processor 18 | 19 | prometheus struct { 20 | build *prometheus.GaugeVec 21 | buildStatus *prometheus.GaugeVec 22 | 23 | buildDefinition *prometheus.GaugeVec 24 | 25 | buildStage *prometheus.GaugeVec 26 | buildPhase *prometheus.GaugeVec 27 | buildJob *prometheus.GaugeVec 28 | buildTask *prometheus.GaugeVec 29 | buildTag *prometheus.GaugeVec 30 | 31 | buildTimeProject *prometheus.SummaryVec 32 | jobTimeProject *prometheus.SummaryVec 33 | } 34 | } 35 | 36 | func (m *MetricsCollectorBuild) Setup(collector *collector.Collector) { 37 | m.Processor.Setup(collector) 38 | 39 | m.prometheus.build = prometheus.NewGaugeVec( 40 | prometheus.GaugeOpts{ 41 | Name: "azure_devops_build_info", 42 | Help: "Azure DevOps build", 43 | }, 44 | []string{ 45 | "projectID", 46 | "buildDefinitionID", 47 | "buildID", 48 | "agentPoolID", 49 | "requestedBy", 50 | "buildNumber", 51 | "buildName", 52 | "sourceBranch", 53 | "sourceVersion", 54 | "status", 55 | "reason", 56 | "result", 57 | "url", 58 | }, 59 | ) 60 | m.Collector.RegisterMetricList("build", m.prometheus.build, true) 61 | 62 | m.prometheus.buildStatus = prometheus.NewGaugeVec( 63 | prometheus.GaugeOpts{ 64 | Name: "azure_devops_build_status", 65 | Help: "Azure DevOps build", 66 | }, 67 | []string{ 68 | "projectID", 69 | "buildID", 70 | "buildDefinitionID", 71 | "buildNumber", 72 | "result", 73 | "type", 74 | }, 75 | ) 76 | m.Collector.RegisterMetricList("buildStatus", m.prometheus.buildStatus, true) 77 | 78 | m.prometheus.buildStage = prometheus.NewGaugeVec( 79 | prometheus.GaugeOpts{ 80 | Name: "azure_devops_build_stage", 81 | Help: "Azure DevOps build stages", 82 | }, 83 | []string{ 84 | "projectID", 85 | "buildID", 86 | "buildDefinitionID", 87 | "buildNumber", 88 | "name", 89 | "id", 90 | "identifier", 91 | "result", 92 | "type", 93 | }, 94 | ) 95 | m.Collector.RegisterMetricList("buildStage", m.prometheus.buildStage, true) 96 | 97 | m.prometheus.buildPhase = prometheus.NewGaugeVec( 98 | prometheus.GaugeOpts{ 99 | Name: "azure_devops_build_phase", 100 | Help: "Azure DevOps build phases", 101 | }, 102 | []string{ 103 | "projectID", 104 | "buildID", 105 | "buildDefinitionID", 106 | "buildNumber", 107 | "name", 108 | "id", 109 | "parentId", 110 | "identifier", 111 | "result", 112 | "type", 113 | }, 114 | ) 115 | m.Collector.RegisterMetricList("buildPhase", m.prometheus.buildPhase, true) 116 | 117 | m.prometheus.buildJob = prometheus.NewGaugeVec( 118 | prometheus.GaugeOpts{ 119 | Name: "azure_devops_build_job", 120 | Help: "Azure DevOps build jobs", 121 | }, 122 | []string{ 123 | "projectID", 124 | "buildID", 125 | "buildDefinitionID", 126 | "buildNumber", 127 | "name", 128 | "id", 129 | "parentId", 130 | "identifier", 131 | "result", 132 | "type", 133 | }, 134 | ) 135 | m.Collector.RegisterMetricList("buildJob", m.prometheus.buildJob, true) 136 | 137 | m.prometheus.buildTask = prometheus.NewGaugeVec( 138 | prometheus.GaugeOpts{ 139 | Name: "azure_devops_build_task", 140 | Help: "Azure DevOps build tasks", 141 | }, 142 | []string{ 143 | "projectID", 144 | "buildID", 145 | "buildDefinitionID", 146 | "buildNumber", 147 | "name", 148 | "id", 149 | "parentId", 150 | "workerName", 151 | "result", 152 | "type", 153 | }, 154 | ) 155 | m.Collector.RegisterMetricList("buildTask", m.prometheus.buildTask, true) 156 | 157 | m.prometheus.buildTag = prometheus.NewGaugeVec( 158 | prometheus.GaugeOpts{ 159 | Name: "azure_devops_build_tag", 160 | Help: "Azure DevOps build tags", 161 | }, 162 | []string{ 163 | "projectID", 164 | "buildID", 165 | "buildDefinitionID", 166 | "buildNumber", 167 | "name", 168 | "type", 169 | "info", 170 | }, 171 | ) 172 | m.Collector.RegisterMetricList("buildTag", m.prometheus.buildTag, true) 173 | 174 | m.prometheus.buildDefinition = prometheus.NewGaugeVec( 175 | prometheus.GaugeOpts{ 176 | Name: "azure_devops_build_definition_info", 177 | Help: "Azure DevOps build definition", 178 | }, 179 | []string{ 180 | "projectID", 181 | "buildDefinitionID", 182 | "buildNameFormat", 183 | "buildDefinitionName", 184 | "path", 185 | "url", 186 | }, 187 | ) 188 | m.Collector.RegisterMetricList("buildDefinition", m.prometheus.buildDefinition, true) 189 | } 190 | 191 | func (m *MetricsCollectorBuild) Reset() {} 192 | 193 | func (m *MetricsCollectorBuild) Collect(callback chan<- func()) { 194 | ctx := m.Context() 195 | logger := m.Logger() 196 | 197 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 198 | projectLogger := logger.With(zap.String("project", project.Name)) 199 | m.collectDefinition(ctx, projectLogger, callback, project) 200 | m.collectBuilds(ctx, projectLogger, callback, project) 201 | m.collectBuildsTimeline(ctx, projectLogger, callback, project) 202 | if nil != Opts.AzureDevops.TagsSchema { 203 | m.collectBuildsTags(ctx, projectLogger, callback, project) 204 | } 205 | } 206 | } 207 | 208 | func (m *MetricsCollectorBuild) collectDefinition(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 209 | list, err := AzureDevopsClient.ListBuildDefinitions(project.Id) 210 | if err != nil { 211 | logger.Error(err) 212 | return 213 | } 214 | 215 | buildDefinitonMetric := m.Collector.GetMetricList("buildDefinition") 216 | 217 | for _, buildDefinition := range list.List { 218 | buildDefinitonMetric.Add(prometheus.Labels{ 219 | "projectID": project.Id, 220 | "buildDefinitionID": int64ToString(buildDefinition.Id), 221 | "buildNameFormat": buildDefinition.BuildNameFormat, 222 | "buildDefinitionName": buildDefinition.Name, 223 | "path": buildDefinition.Path, 224 | "url": buildDefinition.Links.Web.Href, 225 | }, 1) 226 | } 227 | } 228 | 229 | func (m *MetricsCollectorBuild) collectBuilds(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 230 | minTime := time.Now().Add(-Opts.Limit.BuildHistoryDuration) 231 | 232 | list, err := AzureDevopsClient.ListBuildHistory(project.Id, minTime) 233 | if err != nil { 234 | logger.Error(err) 235 | return 236 | } 237 | 238 | buildMetric := m.Collector.GetMetricList("build") 239 | buildStatusMetric := m.Collector.GetMetricList("buildStatus") 240 | 241 | for _, build := range list.List { 242 | buildMetric.AddInfo(prometheus.Labels{ 243 | "projectID": project.Id, 244 | "buildDefinitionID": int64ToString(build.Definition.Id), 245 | "buildID": int64ToString(build.Id), 246 | "buildNumber": build.BuildNumber, 247 | "buildName": build.Definition.Name, 248 | "agentPoolID": int64ToString(build.Queue.Pool.Id), 249 | "requestedBy": build.RequestedBy.DisplayName, 250 | "sourceBranch": build.SourceBranch, 251 | "sourceVersion": build.SourceVersion, 252 | "status": build.Status, 253 | "reason": build.Reason, 254 | "result": build.Result, 255 | "url": build.Links.Web.Href, 256 | }) 257 | 258 | buildStatusMetric.AddBool(prometheus.Labels{ 259 | "projectID": project.Id, 260 | "buildID": int64ToString(build.Id), 261 | "buildDefinitionID": int64ToString(build.Definition.Id), 262 | "buildNumber": build.BuildNumber, 263 | "result": build.Result, 264 | "type": "succeeded", 265 | }, build.Result == "succeeded") 266 | 267 | buildStatusMetric.AddTime(prometheus.Labels{ 268 | "projectID": project.Id, 269 | "buildID": int64ToString(build.Id), 270 | "buildDefinitionID": int64ToString(build.Definition.Id), 271 | "buildNumber": build.BuildNumber, 272 | "result": build.Result, 273 | "type": "queued", 274 | }, build.QueueTime) 275 | 276 | buildStatusMetric.AddTime(prometheus.Labels{ 277 | "projectID": project.Id, 278 | "buildID": int64ToString(build.Id), 279 | "buildDefinitionID": int64ToString(build.Definition.Id), 280 | "buildNumber": build.BuildNumber, 281 | "result": build.Result, 282 | "type": "started", 283 | }, build.StartTime) 284 | 285 | buildStatusMetric.AddTime(prometheus.Labels{ 286 | "projectID": project.Id, 287 | "buildID": int64ToString(build.Id), 288 | "buildDefinitionID": int64ToString(build.Definition.Id), 289 | "buildNumber": build.BuildNumber, 290 | "result": build.Result, 291 | "type": "finished", 292 | }, build.FinishTime) 293 | 294 | buildStatusMetric.AddDuration(prometheus.Labels{ 295 | "projectID": project.Id, 296 | "buildID": int64ToString(build.Id), 297 | "buildDefinitionID": int64ToString(build.Definition.Id), 298 | "buildNumber": build.BuildNumber, 299 | "result": build.Result, 300 | "type": "jobDuration", 301 | }, build.FinishTime.Sub(build.StartTime)) 302 | } 303 | } 304 | 305 | func (m *MetricsCollectorBuild) collectBuildsTimeline(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 306 | minTime := time.Now().Add(-Opts.Limit.BuildHistoryDuration) 307 | 308 | statusFilter := "completed" 309 | if arrayStringContains(Opts.AzureDevops.FetchAllBuildsFilter, project.Name) || arrayStringContains(Opts.AzureDevops.FetchAllBuildsFilter, project.Id) { 310 | logger.Info("fetching all builds for project " + project.Name) 311 | statusFilter = "all" 312 | } 313 | 314 | list, err := AzureDevopsClient.ListBuildHistoryWithStatus(project.Id, minTime, statusFilter) 315 | if err != nil { 316 | logger.Error(err) 317 | return 318 | } 319 | 320 | buildStageMetric := m.Collector.GetMetricList("buildStage") 321 | buildPhaseMetric := m.Collector.GetMetricList("buildPhase") 322 | buildJobMetric := m.Collector.GetMetricList("buildJob") 323 | buildTaskMetric := m.Collector.GetMetricList("buildTask") 324 | 325 | for _, build := range list.List { 326 | 327 | timelineRecordList, _ := AzureDevopsClient.ListBuildTimeline(project.Id, int64ToString(build.Id)) 328 | for _, timelineRecord := range timelineRecordList.List { 329 | 330 | if Opts.AzureDevops.FilterTimelineState != nil && !arrayStringContains(Opts.AzureDevops.FilterTimelineState, timelineRecord.State) { 331 | continue 332 | } 333 | 334 | if timelineRecord.Result == "" { 335 | timelineRecord.Result = "unknown" 336 | } 337 | 338 | recordType := timelineRecord.RecordType 339 | switch strings.ToLower(recordType) { 340 | case "stage": 341 | buildStageMetric.Add(prometheus.Labels{ 342 | "projectID": project.Id, 343 | "buildID": int64ToString(build.Id), 344 | "buildDefinitionID": int64ToString(build.Definition.Id), 345 | "buildNumber": build.BuildNumber, 346 | "name": timelineRecord.Name, 347 | "id": timelineRecord.Id, 348 | "identifier": timelineRecord.Identifier, 349 | "result": timelineRecord.Result, 350 | "type": "errorCount", 351 | }, timelineRecord.ErrorCount) 352 | 353 | buildStageMetric.Add(prometheus.Labels{ 354 | "projectID": project.Id, 355 | "buildID": int64ToString(build.Id), 356 | "buildDefinitionID": int64ToString(build.Definition.Id), 357 | "buildNumber": build.BuildNumber, 358 | "name": timelineRecord.Name, 359 | "id": timelineRecord.Id, 360 | "identifier": timelineRecord.Identifier, 361 | "result": timelineRecord.Result, 362 | "type": "warningCount", 363 | }, timelineRecord.WarningCount) 364 | 365 | buildStageMetric.AddBool(prometheus.Labels{ 366 | "projectID": project.Id, 367 | "buildID": int64ToString(build.Id), 368 | "buildDefinitionID": int64ToString(build.Definition.Id), 369 | "buildNumber": build.BuildNumber, 370 | "name": timelineRecord.Name, 371 | "id": timelineRecord.Id, 372 | "identifier": timelineRecord.Identifier, 373 | "result": timelineRecord.Result, 374 | "type": "succeeded", 375 | }, timelineRecord.Result == "succeeded") 376 | 377 | buildStageMetric.AddTime(prometheus.Labels{ 378 | "projectID": project.Id, 379 | "buildID": int64ToString(build.Id), 380 | "buildDefinitionID": int64ToString(build.Definition.Id), 381 | "buildNumber": build.BuildNumber, 382 | "name": timelineRecord.Name, 383 | "id": timelineRecord.Id, 384 | "identifier": timelineRecord.Identifier, 385 | "result": timelineRecord.Result, 386 | "type": "started", 387 | }, timelineRecord.StartTime) 388 | 389 | buildStageMetric.AddTime(prometheus.Labels{ 390 | "projectID": project.Id, 391 | "buildID": int64ToString(build.Id), 392 | "buildDefinitionID": int64ToString(build.Definition.Id), 393 | "buildNumber": build.BuildNumber, 394 | "name": timelineRecord.Name, 395 | "id": timelineRecord.Id, 396 | "identifier": timelineRecord.Identifier, 397 | "result": timelineRecord.Result, 398 | "type": "finished", 399 | }, timelineRecord.FinishTime) 400 | 401 | buildStageMetric.AddDuration(prometheus.Labels{ 402 | "projectID": project.Id, 403 | "buildID": int64ToString(build.Id), 404 | "buildDefinitionID": int64ToString(build.Definition.Id), 405 | "buildNumber": build.BuildNumber, 406 | "name": timelineRecord.Name, 407 | "id": timelineRecord.Id, 408 | "identifier": timelineRecord.Identifier, 409 | "result": timelineRecord.Result, 410 | "type": "duration", 411 | }, timelineRecord.FinishTime.Sub(timelineRecord.StartTime)) 412 | 413 | case "phase": 414 | buildPhaseMetric.Add(prometheus.Labels{ 415 | "projectID": project.Id, 416 | "buildID": int64ToString(build.Id), 417 | "buildDefinitionID": int64ToString(build.Definition.Id), 418 | "buildNumber": build.BuildNumber, 419 | "name": timelineRecord.Name, 420 | "id": timelineRecord.Id, 421 | "parentId": timelineRecord.ParentId, 422 | "identifier": timelineRecord.Identifier, 423 | "result": timelineRecord.Result, 424 | "type": "errorCount", 425 | }, timelineRecord.ErrorCount) 426 | 427 | buildPhaseMetric.Add(prometheus.Labels{ 428 | "projectID": project.Id, 429 | "buildID": int64ToString(build.Id), 430 | "buildDefinitionID": int64ToString(build.Definition.Id), 431 | "buildNumber": build.BuildNumber, 432 | "name": timelineRecord.Name, 433 | "id": timelineRecord.Id, 434 | "parentId": timelineRecord.ParentId, 435 | "identifier": timelineRecord.Identifier, 436 | "result": timelineRecord.Result, 437 | "type": "warningCount", 438 | }, timelineRecord.WarningCount) 439 | 440 | buildPhaseMetric.AddBool(prometheus.Labels{ 441 | "projectID": project.Id, 442 | "buildID": int64ToString(build.Id), 443 | "buildDefinitionID": int64ToString(build.Definition.Id), 444 | "buildNumber": build.BuildNumber, 445 | "name": timelineRecord.Name, 446 | "id": timelineRecord.Id, 447 | "parentId": timelineRecord.ParentId, 448 | "identifier": timelineRecord.Identifier, 449 | "result": timelineRecord.Result, 450 | "type": "succeeded", 451 | }, timelineRecord.Result == "succeeded") 452 | 453 | buildPhaseMetric.AddTime(prometheus.Labels{ 454 | "projectID": project.Id, 455 | "buildID": int64ToString(build.Id), 456 | "buildDefinitionID": int64ToString(build.Definition.Id), 457 | "buildNumber": build.BuildNumber, 458 | "name": timelineRecord.Name, 459 | "id": timelineRecord.Id, 460 | "parentId": timelineRecord.ParentId, 461 | "identifier": timelineRecord.Identifier, 462 | "result": timelineRecord.Result, 463 | "type": "started", 464 | }, timelineRecord.StartTime) 465 | 466 | buildPhaseMetric.AddTime(prometheus.Labels{ 467 | "projectID": project.Id, 468 | "buildID": int64ToString(build.Id), 469 | "buildDefinitionID": int64ToString(build.Definition.Id), 470 | "buildNumber": build.BuildNumber, 471 | "name": timelineRecord.Name, 472 | "id": timelineRecord.Id, 473 | "parentId": timelineRecord.ParentId, 474 | "identifier": timelineRecord.Identifier, 475 | "result": timelineRecord.Result, 476 | "type": "finished", 477 | }, timelineRecord.FinishTime) 478 | 479 | buildPhaseMetric.AddDuration(prometheus.Labels{ 480 | "projectID": project.Id, 481 | "buildID": int64ToString(build.Id), 482 | "buildDefinitionID": int64ToString(build.Definition.Id), 483 | "buildNumber": build.BuildNumber, 484 | "name": timelineRecord.Name, 485 | "id": timelineRecord.Id, 486 | "parentId": timelineRecord.ParentId, 487 | "identifier": timelineRecord.Identifier, 488 | "result": timelineRecord.Result, 489 | "type": "duration", 490 | }, timelineRecord.FinishTime.Sub(timelineRecord.StartTime)) 491 | 492 | case "job": 493 | buildJobMetric.Add(prometheus.Labels{ 494 | "projectID": project.Id, 495 | "buildID": int64ToString(build.Id), 496 | "buildDefinitionID": int64ToString(build.Definition.Id), 497 | "buildNumber": build.BuildNumber, 498 | "name": timelineRecord.Name, 499 | "id": timelineRecord.Id, 500 | "parentId": timelineRecord.ParentId, 501 | "identifier": timelineRecord.Identifier, 502 | "result": timelineRecord.Result, 503 | "type": "errorCount", 504 | }, timelineRecord.ErrorCount) 505 | 506 | buildJobMetric.Add(prometheus.Labels{ 507 | "projectID": project.Id, 508 | "buildID": int64ToString(build.Id), 509 | "buildDefinitionID": int64ToString(build.Definition.Id), 510 | "buildNumber": build.BuildNumber, 511 | "name": timelineRecord.Name, 512 | "id": timelineRecord.Id, 513 | "parentId": timelineRecord.ParentId, 514 | "identifier": timelineRecord.Identifier, 515 | "result": timelineRecord.Result, 516 | "type": "warningCount", 517 | }, timelineRecord.WarningCount) 518 | 519 | buildJobMetric.AddBool(prometheus.Labels{ 520 | "projectID": project.Id, 521 | "buildID": int64ToString(build.Id), 522 | "buildDefinitionID": int64ToString(build.Definition.Id), 523 | "buildNumber": build.BuildNumber, 524 | "name": timelineRecord.Name, 525 | "id": timelineRecord.Id, 526 | "parentId": timelineRecord.ParentId, 527 | "identifier": timelineRecord.Identifier, 528 | "result": timelineRecord.Result, 529 | "type": "succeeded", 530 | }, timelineRecord.Result == "succeeded") 531 | 532 | buildJobMetric.AddTime(prometheus.Labels{ 533 | "projectID": project.Id, 534 | "buildID": int64ToString(build.Id), 535 | "buildDefinitionID": int64ToString(build.Definition.Id), 536 | "buildNumber": build.BuildNumber, 537 | "name": timelineRecord.Name, 538 | "id": timelineRecord.Id, 539 | "parentId": timelineRecord.ParentId, 540 | "identifier": timelineRecord.Identifier, 541 | "result": timelineRecord.Result, 542 | "type": "started", 543 | }, timelineRecord.StartTime) 544 | 545 | buildJobMetric.AddTime(prometheus.Labels{ 546 | "projectID": project.Id, 547 | "buildID": int64ToString(build.Id), 548 | "buildDefinitionID": int64ToString(build.Definition.Id), 549 | "buildNumber": build.BuildNumber, 550 | "name": timelineRecord.Name, 551 | "id": timelineRecord.Id, 552 | "parentId": timelineRecord.ParentId, 553 | "identifier": timelineRecord.Identifier, 554 | "result": timelineRecord.Result, 555 | "type": "finished", 556 | }, timelineRecord.FinishTime) 557 | 558 | buildJobMetric.AddDuration(prometheus.Labels{ 559 | "projectID": project.Id, 560 | "buildID": int64ToString(build.Id), 561 | "buildDefinitionID": int64ToString(build.Definition.Id), 562 | "buildNumber": build.BuildNumber, 563 | "name": timelineRecord.Name, 564 | "id": timelineRecord.Id, 565 | "parentId": timelineRecord.ParentId, 566 | "identifier": timelineRecord.Identifier, 567 | "result": timelineRecord.Result, 568 | "type": "duration", 569 | }, timelineRecord.FinishTime.Sub(timelineRecord.StartTime)) 570 | 571 | case "task": 572 | buildTaskMetric.Add(prometheus.Labels{ 573 | "projectID": project.Id, 574 | "buildID": int64ToString(build.Id), 575 | "buildDefinitionID": int64ToString(build.Definition.Id), 576 | "buildNumber": build.BuildNumber, 577 | "name": timelineRecord.Name, 578 | "id": timelineRecord.Id, 579 | "parentId": timelineRecord.ParentId, 580 | "workerName": timelineRecord.WorkerName, 581 | "result": timelineRecord.Result, 582 | "type": "errorCount", 583 | }, timelineRecord.ErrorCount) 584 | 585 | buildTaskMetric.Add(prometheus.Labels{ 586 | "projectID": project.Id, 587 | "buildID": int64ToString(build.Id), 588 | "buildDefinitionID": int64ToString(build.Definition.Id), 589 | "buildNumber": build.BuildNumber, 590 | "name": timelineRecord.Name, 591 | "id": timelineRecord.Id, 592 | "parentId": timelineRecord.ParentId, 593 | "workerName": timelineRecord.WorkerName, 594 | "result": timelineRecord.Result, 595 | "type": "warningCount", 596 | }, timelineRecord.WarningCount) 597 | 598 | buildTaskMetric.AddBool(prometheus.Labels{ 599 | "projectID": project.Id, 600 | "buildID": int64ToString(build.Id), 601 | "buildDefinitionID": int64ToString(build.Definition.Id), 602 | "buildNumber": build.BuildNumber, 603 | "name": timelineRecord.Name, 604 | "id": timelineRecord.Id, 605 | "parentId": timelineRecord.ParentId, 606 | "workerName": timelineRecord.WorkerName, 607 | "result": timelineRecord.Result, 608 | "type": "succeeded", 609 | }, timelineRecord.Result == "succeeded") 610 | 611 | buildTaskMetric.AddTime(prometheus.Labels{ 612 | "projectID": project.Id, 613 | "buildID": int64ToString(build.Id), 614 | "buildDefinitionID": int64ToString(build.Definition.Id), 615 | "buildNumber": build.BuildNumber, 616 | "name": timelineRecord.Name, 617 | "id": timelineRecord.Id, 618 | "parentId": timelineRecord.ParentId, 619 | "workerName": timelineRecord.WorkerName, 620 | "result": timelineRecord.Result, 621 | "type": "started", 622 | }, timelineRecord.StartTime) 623 | 624 | buildTaskMetric.AddTime(prometheus.Labels{ 625 | "projectID": project.Id, 626 | "buildID": int64ToString(build.Id), 627 | "buildDefinitionID": int64ToString(build.Definition.Id), 628 | "buildNumber": build.BuildNumber, 629 | "name": timelineRecord.Name, 630 | "id": timelineRecord.Id, 631 | "parentId": timelineRecord.ParentId, 632 | "workerName": timelineRecord.WorkerName, 633 | "result": timelineRecord.Result, 634 | "type": "finished", 635 | }, timelineRecord.FinishTime) 636 | 637 | buildTaskMetric.AddDuration(prometheus.Labels{ 638 | "projectID": project.Id, 639 | "buildID": int64ToString(build.Id), 640 | "buildDefinitionID": int64ToString(build.Definition.Id), 641 | "buildNumber": build.BuildNumber, 642 | "name": timelineRecord.Name, 643 | "id": timelineRecord.Id, 644 | "parentId": timelineRecord.ParentId, 645 | "workerName": timelineRecord.WorkerName, 646 | "result": timelineRecord.Result, 647 | "type": "duration", 648 | }, timelineRecord.FinishTime.Sub(timelineRecord.StartTime)) 649 | } 650 | } 651 | } 652 | } 653 | 654 | func (m *MetricsCollectorBuild) collectBuildsTags(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 655 | minTime := time.Now().Add(-Opts.Limit.BuildHistoryDuration) 656 | 657 | statusFilter := "completed" 658 | if arrayStringContains(Opts.AzureDevops.FetchAllBuildsFilter, project.Name) || arrayStringContains(Opts.AzureDevops.FetchAllBuildsFilter, project.Id) { 659 | logger.Info("fetching all builds for project " + project.Name) 660 | statusFilter = "all" 661 | } 662 | 663 | list, err := AzureDevopsClient.ListBuildHistoryWithStatus(project.Id, minTime, statusFilter) 664 | if err != nil { 665 | logger.Error(err) 666 | return 667 | } 668 | 669 | buildTag := m.Collector.GetMetricList("buildTag") 670 | 671 | for _, build := range list.List { 672 | if nil == Opts.AzureDevops.TagsBuildDefinitionIdList || arrayIntContains(*Opts.AzureDevops.TagsBuildDefinitionIdList, build.Definition.Id) { 673 | tagRecordList, _ := AzureDevopsClient.ListBuildTags(project.Id, int64ToString(build.Id)) 674 | tagList, err := tagRecordList.Parse(*Opts.AzureDevops.TagsSchema) 675 | if err != nil { 676 | m.Logger().Error(err) 677 | continue 678 | } 679 | for _, tag := range tagList { 680 | 681 | switch tag.Type { 682 | case "number": 683 | value, _ := strconv.ParseFloat(tag.Value, 64) 684 | buildTag.Add(prometheus.Labels{ 685 | "projectID": project.Id, 686 | "buildID": int64ToString(build.Id), 687 | "buildDefinitionID": int64ToString(build.Definition.Id), 688 | "buildNumber": build.BuildNumber, 689 | "name": tag.Name, 690 | "type": "number", 691 | "info": "", 692 | }, value) 693 | case "bool": 694 | value, _ := strconv.ParseBool(tag.Value) 695 | buildTag.AddBool(prometheus.Labels{ 696 | "projectID": project.Id, 697 | "buildID": int64ToString(build.Id), 698 | "buildDefinitionID": int64ToString(build.Definition.Id), 699 | "buildNumber": build.BuildNumber, 700 | "name": tag.Name, 701 | "type": "bool", 702 | "info": "", 703 | }, value) 704 | case "info": 705 | buildTag.AddInfo(prometheus.Labels{ 706 | "projectID": project.Id, 707 | "buildID": int64ToString(build.Id), 708 | "buildDefinitionID": int64ToString(build.Definition.Id), 709 | "buildNumber": build.BuildNumber, 710 | "name": tag.Name, 711 | "type": "info", 712 | "info": tag.Value, 713 | }) 714 | } 715 | 716 | } 717 | } 718 | } 719 | } 720 | -------------------------------------------------------------------------------- /metrics_deployment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/webdevops/go-common/prometheus/collector" 8 | "go.uber.org/zap" 9 | 10 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 11 | ) 12 | 13 | type MetricsCollectorDeployment struct { 14 | collector.Processor 15 | 16 | prometheus struct { 17 | deployment *prometheus.GaugeVec 18 | deploymentStatus *prometheus.GaugeVec 19 | } 20 | } 21 | 22 | func (m *MetricsCollectorDeployment) Setup(collector *collector.Collector) { 23 | m.Processor.Setup(collector) 24 | 25 | m.prometheus.deployment = prometheus.NewGaugeVec( 26 | prometheus.GaugeOpts{ 27 | Name: "azure_devops_deployment_info", 28 | Help: "Azure DevOps deployment", 29 | }, 30 | []string{ 31 | "projectID", 32 | "deploymentID", 33 | "releaseID", 34 | "releaseName", 35 | "releaseDefinitionID", 36 | "requestedBy", 37 | "deploymentName", 38 | "deploymentStatus", 39 | "operationStatus", 40 | "reason", 41 | "attempt", 42 | "environmentId", 43 | "environmentName", 44 | "approvedBy", 45 | }, 46 | ) 47 | m.Collector.RegisterMetricList("deployment", m.prometheus.deployment, true) 48 | 49 | m.prometheus.deploymentStatus = prometheus.NewGaugeVec( 50 | prometheus.GaugeOpts{ 51 | Name: "azure_devops_deployment_status", 52 | Help: "Azure DevOps deployment status", 53 | }, 54 | []string{ 55 | "projectID", 56 | "deploymentID", 57 | "type", 58 | }, 59 | ) 60 | m.Collector.RegisterMetricList("deploymentStatus", m.prometheus.deploymentStatus, true) 61 | } 62 | 63 | func (m *MetricsCollectorDeployment) Reset() { 64 | m.prometheus.deployment.Reset() 65 | m.prometheus.deploymentStatus.Reset() 66 | } 67 | 68 | func (m *MetricsCollectorDeployment) Collect(callback chan<- func()) { 69 | ctx := m.Context() 70 | logger := m.Logger() 71 | 72 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 73 | projectLogger := logger.With(zap.String("project", project.Name)) 74 | m.collectDeployments(ctx, projectLogger, callback, project) 75 | } 76 | } 77 | 78 | func (m *MetricsCollectorDeployment) collectDeployments(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 79 | list, err := AzureDevopsClient.ListReleaseDefinitions(project.Id) 80 | if err != nil { 81 | logger.Error(err) 82 | return 83 | } 84 | 85 | deploymentMetric := m.Collector.GetMetricList("deployment") 86 | deploymentStatusMetric := m.Collector.GetMetricList("deploymentStatus") 87 | 88 | for _, releaseDefinition := range list.List { 89 | contextLogger := logger.With(zap.String("releaseDefinition", releaseDefinition.Name)) 90 | 91 | deploymentList, err := AzureDevopsClient.ListReleaseDeployments(project.Id, releaseDefinition.Id) 92 | if err != nil { 93 | contextLogger.Error(err) 94 | return 95 | } 96 | 97 | for _, deployment := range deploymentList.List { 98 | deploymentMetric.AddInfo(prometheus.Labels{ 99 | "projectID": project.Id, 100 | "deploymentID": int64ToString(deployment.Id), 101 | "releaseID": int64ToString(deployment.Release.Id), 102 | "releaseName": deployment.Release.Name, 103 | "releaseDefinitionID": int64ToString(releaseDefinition.Id), 104 | "requestedBy": deployment.RequestedBy.DisplayName, 105 | "deploymentName": deployment.Name, 106 | "deploymentStatus": deployment.DeploymentStatus, 107 | "operationStatus": deployment.OperationStatus, 108 | "reason": deployment.Reason, 109 | "attempt": int64ToString(deployment.Attempt), 110 | "environmentId": int64ToString(deployment.ReleaseEnvironment.Id), 111 | "environmentName": deployment.ReleaseEnvironment.Name, 112 | "approvedBy": deployment.ApprovedBy(), 113 | }) 114 | 115 | queuedOn := deployment.QueuedOnTime() 116 | startedOn := deployment.StartedOnTime() 117 | completedOn := deployment.CompletedOnTime() 118 | 119 | if queuedOn != nil { 120 | deploymentStatusMetric.AddTime(prometheus.Labels{ 121 | "projectID": project.Id, 122 | "deploymentID": int64ToString(deployment.Id), 123 | "type": "queued", 124 | }, *queuedOn) 125 | } 126 | 127 | if startedOn != nil { 128 | deploymentStatusMetric.AddTime(prometheus.Labels{ 129 | "projectID": project.Id, 130 | "deploymentID": int64ToString(deployment.Id), 131 | "type": "started", 132 | }, *startedOn) 133 | } 134 | 135 | if completedOn != nil { 136 | deploymentStatusMetric.AddTime(prometheus.Labels{ 137 | "projectID": project.Id, 138 | "deploymentID": int64ToString(deployment.Id), 139 | "type": "finished", 140 | }, *completedOn) 141 | } 142 | 143 | if completedOn != nil && startedOn != nil { 144 | deploymentStatusMetric.AddDuration(prometheus.Labels{ 145 | "projectID": project.Id, 146 | "deploymentID": int64ToString(deployment.Id), 147 | "type": "jobDuration", 148 | }, completedOn.Sub(*startedOn)) 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /metrics_latest_build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/webdevops/go-common/prometheus/collector" 8 | "go.uber.org/zap" 9 | 10 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 11 | ) 12 | 13 | type MetricsCollectorLatestBuild struct { 14 | collector.Processor 15 | 16 | prometheus struct { 17 | build *prometheus.GaugeVec 18 | buildStatus *prometheus.GaugeVec 19 | } 20 | } 21 | 22 | func (m *MetricsCollectorLatestBuild) Setup(collector *collector.Collector) { 23 | m.Processor.Setup(collector) 24 | 25 | m.prometheus.build = prometheus.NewGaugeVec( 26 | prometheus.GaugeOpts{ 27 | Name: "azure_devops_build_latest_info", 28 | Help: "Azure DevOps build (latest)", 29 | }, 30 | []string{ 31 | "projectID", 32 | "buildDefinitionID", 33 | "buildID", 34 | "agentPoolID", 35 | "requestedBy", 36 | "buildNumber", 37 | "buildName", 38 | "sourceBranch", 39 | "sourceVersion", 40 | "status", 41 | "reason", 42 | "result", 43 | "url", 44 | }, 45 | ) 46 | m.Collector.RegisterMetricList("build", m.prometheus.build, true) 47 | 48 | m.prometheus.buildStatus = prometheus.NewGaugeVec( 49 | prometheus.GaugeOpts{ 50 | Name: "azure_devops_build_latest_status", 51 | Help: "Azure DevOps build (latest)", 52 | }, 53 | []string{ 54 | "projectID", 55 | "buildID", 56 | "buildNumber", 57 | "type", 58 | }, 59 | ) 60 | m.Collector.RegisterMetricList("buildStatus", m.prometheus.buildStatus, true) 61 | } 62 | 63 | func (m *MetricsCollectorLatestBuild) Reset() {} 64 | 65 | func (m *MetricsCollectorLatestBuild) Collect(callback chan<- func()) { 66 | ctx := m.Context() 67 | logger := m.Logger() 68 | 69 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 70 | projectLogger := logger.With(zap.String("project", project.Name)) 71 | m.collectLatestBuilds(ctx, projectLogger, project, callback) 72 | } 73 | } 74 | 75 | func (m *MetricsCollectorLatestBuild) collectLatestBuilds(ctx context.Context, logger *zap.SugaredLogger, project devopsClient.Project, callback chan<- func()) { 76 | list, err := AzureDevopsClient.ListLatestBuilds(project.Id) 77 | if err != nil { 78 | logger.Error(err) 79 | return 80 | } 81 | 82 | buildMetric := m.Collector.GetMetricList("build") 83 | buildStatusMetric := m.Collector.GetMetricList("buildStatus") 84 | 85 | for _, build := range list.List { 86 | buildMetric.AddInfo(prometheus.Labels{ 87 | "projectID": project.Id, 88 | "buildDefinitionID": int64ToString(build.Definition.Id), 89 | "buildID": int64ToString(build.Id), 90 | "buildNumber": build.BuildNumber, 91 | "buildName": build.Definition.Name, 92 | "agentPoolID": int64ToString(build.Queue.Pool.Id), 93 | "requestedBy": build.RequestedBy.DisplayName, 94 | "sourceBranch": build.SourceBranch, 95 | "sourceVersion": build.SourceVersion, 96 | "status": build.Status, 97 | "reason": build.Reason, 98 | "result": build.Result, 99 | "url": build.Links.Web.Href, 100 | }) 101 | 102 | buildStatusMetric.AddTime(prometheus.Labels{ 103 | "projectID": project.Id, 104 | "buildID": int64ToString(build.Id), 105 | "buildNumber": build.BuildNumber, 106 | "type": "started", 107 | }, build.StartTime) 108 | 109 | buildStatusMetric.AddTime(prometheus.Labels{ 110 | "projectID": project.Id, 111 | "buildID": int64ToString(build.Id), 112 | "buildNumber": build.BuildNumber, 113 | "type": "queued", 114 | }, build.QueueTime) 115 | 116 | buildStatusMetric.AddTime(prometheus.Labels{ 117 | "projectID": project.Id, 118 | "buildID": int64ToString(build.Id), 119 | "buildNumber": build.BuildNumber, 120 | "type": "finished", 121 | }, build.FinishTime) 122 | 123 | buildStatusMetric.AddDuration(prometheus.Labels{ 124 | "projectID": project.Id, 125 | "buildID": int64ToString(build.Id), 126 | "buildNumber": build.BuildNumber, 127 | "type": "jobDuration", 128 | }, build.FinishTime.Sub(build.StartTime)) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /metrics_project.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/webdevops/go-common/prometheus/collector" 8 | "go.uber.org/zap" 9 | 10 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 11 | ) 12 | 13 | type MetricsCollectorProject struct { 14 | collector.Processor 15 | 16 | prometheus struct { 17 | project *prometheus.GaugeVec 18 | repository *prometheus.GaugeVec 19 | } 20 | } 21 | 22 | func (m *MetricsCollectorProject) Setup(collector *collector.Collector) { 23 | m.Processor.Setup(collector) 24 | 25 | m.prometheus.project = prometheus.NewGaugeVec( 26 | prometheus.GaugeOpts{ 27 | Name: "azure_devops_project_info", 28 | Help: "Azure DevOps project", 29 | }, 30 | []string{ 31 | "projectID", 32 | "projectName", 33 | }, 34 | ) 35 | m.Collector.RegisterMetricList("project", m.prometheus.project, true) 36 | } 37 | 38 | func (m *MetricsCollectorProject) Reset() {} 39 | 40 | func (m *MetricsCollectorProject) Collect(callback chan<- func()) { 41 | ctx := m.Context() 42 | logger := m.Logger() 43 | 44 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 45 | projectLogger := logger.With(zap.String("project", project.Name)) 46 | m.collectProject(ctx, projectLogger, callback, project) 47 | } 48 | } 49 | 50 | func (m *MetricsCollectorProject) collectProject(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 51 | projectMetric := m.Collector.GetMetricList("project") 52 | 53 | projectMetric.AddInfo(prometheus.Labels{ 54 | "projectID": project.Id, 55 | "projectName": project.Name, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /metrics_pullrequest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/webdevops/go-common/prometheus/collector" 8 | "github.com/webdevops/go-common/utils/to" 9 | "go.uber.org/zap" 10 | 11 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 12 | ) 13 | 14 | type MetricsCollectorPullRequest struct { 15 | collector.Processor 16 | 17 | prometheus struct { 18 | pullRequest *prometheus.GaugeVec 19 | pullRequestStatus *prometheus.GaugeVec 20 | pullRequestLabel *prometheus.GaugeVec 21 | } 22 | } 23 | 24 | func (m *MetricsCollectorPullRequest) Setup(collector *collector.Collector) { 25 | m.Processor.Setup(collector) 26 | 27 | m.prometheus.pullRequest = prometheus.NewGaugeVec( 28 | prometheus.GaugeOpts{ 29 | Name: "azure_devops_pullrequest_info", 30 | Help: "Azure DevOps pullrequest", 31 | }, 32 | []string{ 33 | "projectID", 34 | "repositoryID", 35 | "pullrequestID", 36 | "pullrequestTitle", 37 | "sourceBranch", 38 | "targetBranch", 39 | "status", 40 | "isDraft", 41 | "voteStatus", 42 | "creator", 43 | }, 44 | ) 45 | m.Collector.RegisterMetricList("pullRequest", m.prometheus.pullRequest, true) 46 | 47 | m.prometheus.pullRequestStatus = prometheus.NewGaugeVec( 48 | prometheus.GaugeOpts{ 49 | Name: "azure_devops_pullrequest_status", 50 | Help: "Azure DevOps pullrequest status", 51 | }, 52 | []string{ 53 | "projectID", 54 | "repositoryID", 55 | "pullrequestID", 56 | "type", 57 | }, 58 | ) 59 | m.Collector.RegisterMetricList("pullRequestStatus", m.prometheus.pullRequestStatus, true) 60 | 61 | m.prometheus.pullRequestLabel = prometheus.NewGaugeVec( 62 | prometheus.GaugeOpts{ 63 | Name: "azure_devops_pullrequest_label", 64 | Help: "Azure DevOps pullrequest labels", 65 | }, 66 | []string{ 67 | "projectID", 68 | "repositoryID", 69 | "pullrequestID", 70 | "label", 71 | "active", 72 | }, 73 | ) 74 | m.Collector.RegisterMetricList("pullRequestLabel", m.prometheus.pullRequestLabel, true) 75 | } 76 | 77 | func (m *MetricsCollectorPullRequest) Reset() {} 78 | 79 | func (m *MetricsCollectorPullRequest) Collect(callback chan<- func()) { 80 | ctx := m.Context() 81 | logger := m.Logger() 82 | 83 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 84 | projectLogger := logger.With(zap.String("project", project.Name)) 85 | 86 | for _, repository := range project.RepositoryList.List { 87 | if repository.Disabled() { 88 | continue 89 | } 90 | 91 | repoLogger := projectLogger.With(zap.String("repository", repository.Name)) 92 | m.collectPullRequests(ctx, repoLogger, callback, project, repository) 93 | } 94 | } 95 | } 96 | 97 | func (m *MetricsCollectorPullRequest) collectPullRequests(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project, repository devopsClient.Repository) { 98 | list, err := AzureDevopsClient.ListPullrequest(project.Id, repository.Id) 99 | if err != nil { 100 | logger.Error(err) 101 | return 102 | } 103 | 104 | pullRequestMetric := m.Collector.GetMetricList("pullRequest") 105 | pullRequestStatusMetric := m.Collector.GetMetricList("pullRequestStatus") 106 | pullRequestLabelMetric := m.Collector.GetMetricList("pullRequestLabel") 107 | 108 | for _, pullRequest := range list.List { 109 | voteSummary := pullRequest.GetVoteSummary() 110 | 111 | pullRequestMetric.AddInfo(prometheus.Labels{ 112 | "projectID": project.Id, 113 | "repositoryID": repository.Id, 114 | "pullrequestID": int64ToString(pullRequest.Id), 115 | "pullrequestTitle": pullRequest.Title, 116 | "status": pullRequest.Status, 117 | "voteStatus": voteSummary.HumanizeString(), 118 | "creator": pullRequest.CreatedBy.DisplayName, 119 | "isDraft": to.BoolString(pullRequest.IsDraft), 120 | "sourceBranch": pullRequest.SourceRefName, 121 | "targetBranch": pullRequest.TargetRefName, 122 | }) 123 | 124 | pullRequestStatusMetric.AddTime(prometheus.Labels{ 125 | "projectID": project.Id, 126 | "repositoryID": repository.Id, 127 | "pullrequestID": int64ToString(pullRequest.Id), 128 | "type": "created", 129 | }, pullRequest.CreationDate) 130 | 131 | for _, label := range pullRequest.Labels { 132 | pullRequestLabelMetric.AddInfo(prometheus.Labels{ 133 | "projectID": project.Id, 134 | "repositoryID": repository.Id, 135 | "pullrequestID": int64ToString(pullRequest.Id), 136 | "label": label.Name, 137 | "active": to.BoolString(label.Active), 138 | }) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /metrics_query.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/webdevops/go-common/prometheus/collector" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type MetricsCollectorQuery struct { 13 | collector.Processor 14 | 15 | prometheus struct { 16 | workItemCount *prometheus.GaugeVec 17 | workItemData *prometheus.GaugeVec 18 | } 19 | } 20 | 21 | func (m *MetricsCollectorQuery) Setup(collector *collector.Collector) { 22 | m.Processor.Setup(collector) 23 | 24 | m.prometheus.workItemCount = prometheus.NewGaugeVec( 25 | prometheus.GaugeOpts{ 26 | Name: "azure_devops_query_result", 27 | Help: "Azure DevOps Query Result", 28 | }, 29 | []string{ 30 | // We use this only for bugs. Add more fields as needed. 31 | "projectId", 32 | "queryPath", 33 | }, 34 | ) 35 | m.Collector.RegisterMetricList("workItemCount", m.prometheus.workItemCount, true) 36 | 37 | m.prometheus.workItemData = prometheus.NewGaugeVec( 38 | prometheus.GaugeOpts{ 39 | Name: "azure_devops_workitem_data", 40 | Help: "Azure DevOps WorkItems", 41 | }, 42 | []string{ 43 | "projectId", 44 | "queryPath", 45 | "id", 46 | "title", 47 | "path", 48 | "createdDate", 49 | "acceptedDate", 50 | "resolvedDate", 51 | "closedDate", 52 | }, 53 | ) 54 | m.Collector.RegisterMetricList("workItemData", m.prometheus.workItemData, true) 55 | } 56 | 57 | func (m *MetricsCollectorQuery) Reset() {} 58 | 59 | func (m *MetricsCollectorQuery) Collect(callback chan<- func()) { 60 | ctx := m.Context() 61 | logger := m.Logger() 62 | 63 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 64 | projectLogger := logger.With(zap.String("project", project.Name)) 65 | 66 | for _, query := range Opts.AzureDevops.QueriesWithProjects { 67 | queryPair := strings.Split(query, "@") 68 | m.collectQueryResults(ctx, projectLogger, callback, queryPair[0], queryPair[1]) 69 | } 70 | } 71 | } 72 | 73 | func (m *MetricsCollectorQuery) collectQueryResults(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), queryPath string, projectID string) { 74 | workItemsMetric := m.Collector.GetMetricList("workItemCount") 75 | workItemsDataMetric := m.Collector.GetMetricList("workItemData") 76 | 77 | workItemInfoList, err := AzureDevopsClient.QueryWorkItems(queryPath, projectID) 78 | if err != nil { 79 | logger.Error(err) 80 | return 81 | } 82 | 83 | workItemsMetric.Add(prometheus.Labels{ 84 | "projectId": projectID, 85 | "queryPath": queryPath, 86 | }, float64(len(workItemInfoList.List))) 87 | 88 | for _, workItemInfo := range workItemInfoList.List { 89 | workItem, err := AzureDevopsClient.GetWorkItem(workItemInfo.Url) 90 | if err != nil { 91 | logger.Error(err) 92 | return 93 | } 94 | 95 | workItemsDataMetric.AddInfo(prometheus.Labels{ 96 | "projectId": projectID, 97 | "queryPath": queryPath, 98 | "id": int64ToString(workItem.Id), 99 | "title": workItem.Fields.Title, 100 | "path": workItem.Fields.Path, 101 | "createdDate": workItem.Fields.CreatedDate, 102 | "acceptedDate": workItem.Fields.AcceptedDate, 103 | "resolvedDate": workItem.Fields.ResolvedDate, 104 | "closedDate": workItem.Fields.ClosedDate, 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /metrics_release.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/webdevops/go-common/prometheus/collector" 9 | "github.com/webdevops/go-common/utils/to" 10 | "go.uber.org/zap" 11 | 12 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 13 | ) 14 | 15 | type MetricsCollectorRelease struct { 16 | collector.Processor 17 | 18 | prometheus struct { 19 | release *prometheus.GaugeVec 20 | releaseArtifact *prometheus.GaugeVec 21 | releaseEnvironment *prometheus.GaugeVec 22 | releaseEnvironmentApproval *prometheus.GaugeVec 23 | releaseEnvironmentStatus *prometheus.GaugeVec 24 | 25 | releaseDefinition *prometheus.GaugeVec 26 | releaseDefinitionEnvironment *prometheus.GaugeVec 27 | } 28 | } 29 | 30 | func (m *MetricsCollectorRelease) Setup(collector *collector.Collector) { 31 | m.Processor.Setup(collector) 32 | 33 | m.prometheus.release = prometheus.NewGaugeVec( 34 | prometheus.GaugeOpts{ 35 | Name: "azure_devops_release_info", 36 | Help: "Azure DevOps release", 37 | }, 38 | []string{ 39 | "projectID", 40 | "releaseID", 41 | "releaseDefinitionID", 42 | "requestedBy", 43 | "releaseName", 44 | "status", 45 | "reason", 46 | "result", 47 | "url", 48 | }, 49 | ) 50 | m.Collector.RegisterMetricList("release", m.prometheus.release, true) 51 | 52 | m.prometheus.releaseArtifact = prometheus.NewGaugeVec( 53 | prometheus.GaugeOpts{ 54 | Name: "azure_devops_release_artifact", 55 | Help: "Azure DevOps release", 56 | }, 57 | []string{ 58 | "projectID", 59 | "releaseID", 60 | "releaseDefinitionID", 61 | "sourceId", 62 | "repositoryID", 63 | "branch", 64 | "type", 65 | "alias", 66 | "version", 67 | }, 68 | ) 69 | m.Collector.RegisterMetricList("releaseArtifact", m.prometheus.releaseArtifact, true) 70 | 71 | m.prometheus.releaseEnvironment = prometheus.NewGaugeVec( 72 | prometheus.GaugeOpts{ 73 | Name: "azure_devops_release_environment", 74 | Help: "Azure DevOps release environment", 75 | }, 76 | []string{ 77 | "projectID", 78 | "releaseID", 79 | "releaseDefinitionID", 80 | "environmentID", 81 | "environmentName", 82 | "status", 83 | "triggerReason", 84 | "rank", 85 | }, 86 | ) 87 | m.Collector.RegisterMetricList("releaseEnvironment", m.prometheus.releaseEnvironment, true) 88 | 89 | m.prometheus.releaseEnvironmentStatus = prometheus.NewGaugeVec( 90 | prometheus.GaugeOpts{ 91 | Name: "azure_devops_release_environment_status", 92 | Help: "Azure DevOps release environment status", 93 | }, 94 | []string{ 95 | "projectID", 96 | "releaseID", 97 | "releaseDefinitionID", 98 | "environmentID", 99 | "type", 100 | }, 101 | ) 102 | m.Collector.RegisterMetricList("releaseEnvironmentStatus", m.prometheus.releaseEnvironmentStatus, true) 103 | 104 | m.prometheus.releaseEnvironmentApproval = prometheus.NewGaugeVec( 105 | prometheus.GaugeOpts{ 106 | Name: "azure_devops_release_approval", 107 | Help: "Azure DevOps release approval", 108 | }, 109 | []string{ 110 | "projectID", 111 | "releaseID", 112 | "releaseDefinitionID", 113 | "environmentID", 114 | "approvalType", 115 | "status", 116 | "isAutomated", 117 | "trialNumber", 118 | "attempt", 119 | "rank", 120 | "approver", 121 | "approvedBy", 122 | }, 123 | ) 124 | m.Collector.RegisterMetricList("releaseEnvironmentApproval", m.prometheus.releaseEnvironmentApproval, true) 125 | 126 | m.prometheus.releaseDefinition = prometheus.NewGaugeVec( 127 | prometheus.GaugeOpts{ 128 | Name: "azure_devops_release_definition_info", 129 | Help: "Azure DevOps release definition", 130 | }, 131 | []string{ 132 | "projectID", 133 | "releaseDefinitionID", 134 | "releaseNameFormat", 135 | "releaseDefinitionName", 136 | "path", 137 | "url", 138 | }, 139 | ) 140 | m.Collector.RegisterMetricList("releaseDefinition", m.prometheus.releaseDefinition, true) 141 | 142 | m.prometheus.releaseDefinitionEnvironment = prometheus.NewGaugeVec( 143 | prometheus.GaugeOpts{ 144 | Name: "azure_devops_release_definition_environment", 145 | Help: "Azure DevOps release definition environment", 146 | }, 147 | []string{ 148 | "projectID", 149 | "releaseDefinitionID", 150 | "environmentID", 151 | "environmentName", 152 | "rank", 153 | "owner", 154 | "releaseID", 155 | "badgeUrl", 156 | }, 157 | ) 158 | m.Collector.RegisterMetricList("releaseDefinitionEnvironment", m.prometheus.releaseDefinitionEnvironment, true) 159 | } 160 | 161 | func (m *MetricsCollectorRelease) Reset() {} 162 | 163 | func (m *MetricsCollectorRelease) Collect(callback chan<- func()) { 164 | ctx := m.Context() 165 | logger := m.Logger() 166 | 167 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 168 | projectLogger := logger.With(zap.String("project", project.Name)) 169 | m.collectReleases(ctx, projectLogger, callback, project) 170 | } 171 | } 172 | 173 | func (m *MetricsCollectorRelease) collectReleases(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 174 | list, err := AzureDevopsClient.ListReleaseDefinitions(project.Id) 175 | if err != nil { 176 | logger.Error(err) 177 | return 178 | } 179 | 180 | releaseDefinitionMetric := m.Collector.GetMetricList("releaseDefinition") 181 | releaseDefinitionEnvironmentMetric := m.Collector.GetMetricList("releaseDefinitionEnvironment") 182 | 183 | releaseMetric := m.Collector.GetMetricList("release") 184 | releaseArtifactMetric := m.Collector.GetMetricList("releaseArtifact") 185 | releaseEnvironmentMetric := m.Collector.GetMetricList("releaseEnvironment") 186 | releaseEnvironmentApprovalMetric := m.Collector.GetMetricList("releaseEnvironmentApproval") 187 | releaseEnvironmentStatusMetric := m.Collector.GetMetricList("releaseEnvironmentStatus") 188 | 189 | for _, releaseDefinition := range list.List { 190 | // -------------------------------------- 191 | // Release definition 192 | releaseDefinitionMetric.AddInfo(prometheus.Labels{ 193 | "projectID": project.Id, 194 | "releaseDefinitionID": int64ToString(releaseDefinition.Id), 195 | "releaseNameFormat": releaseDefinition.ReleaseNameFormat, 196 | "releaseDefinitionName": releaseDefinition.Name, 197 | "path": releaseDefinition.Path, 198 | "url": releaseDefinition.Links.Web.Href, 199 | }) 200 | 201 | for _, environment := range releaseDefinition.Environments { 202 | releaseDefinitionEnvironmentMetric.AddInfo(prometheus.Labels{ 203 | "projectID": project.Id, 204 | "releaseDefinitionID": int64ToString(releaseDefinition.Id), 205 | "environmentID": int64ToString(environment.Id), 206 | "environmentName": environment.Name, 207 | "rank": int64ToString(environment.Rank), 208 | "owner": environment.Owner.DisplayName, 209 | "releaseID": int64ToString(environment.CurrentRelease.Id), 210 | "badgeUrl": environment.BadgeUrl, 211 | }) 212 | } 213 | } 214 | 215 | // -------------------------------------- 216 | // Releases 217 | minTime := time.Now().Add(-Opts.Limit.ReleaseHistoryDuration) 218 | 219 | releaseList, err := AzureDevopsClient.ListReleaseHistory(project.Id, minTime) 220 | if err != nil { 221 | logger.Error(err) 222 | return 223 | } 224 | 225 | for _, release := range releaseList.List { 226 | releaseMetric.AddInfo(prometheus.Labels{ 227 | "projectID": project.Id, 228 | "releaseID": int64ToString(release.Id), 229 | "releaseDefinitionID": int64ToString(release.Definition.Id), 230 | "requestedBy": release.RequestedBy.DisplayName, 231 | "releaseName": release.Name, 232 | "status": release.Status, 233 | "reason": release.Reason, 234 | "result": to.BoolString(release.Result), 235 | "url": release.Links.Web.Href, 236 | }) 237 | 238 | for _, artifact := range release.Artifacts { 239 | releaseArtifactMetric.AddInfo(prometheus.Labels{ 240 | "projectID": project.Id, 241 | "releaseID": int64ToString(release.Id), 242 | "releaseDefinitionID": int64ToString(release.Definition.Id), 243 | "sourceId": artifact.SourceId, 244 | "repositoryID": artifact.DefinitionReference.Repository.Name, 245 | "branch": artifact.DefinitionReference.Branch.Name, 246 | "type": artifact.Type, 247 | "alias": artifact.Alias, 248 | "version": artifact.DefinitionReference.Version.Name, 249 | }) 250 | } 251 | 252 | for _, environment := range release.Environments { 253 | releaseEnvironmentMetric.AddInfo(prometheus.Labels{ 254 | "projectID": project.Id, 255 | "releaseID": int64ToString(release.Id), 256 | "releaseDefinitionID": int64ToString(release.Definition.Id), 257 | "environmentID": int64ToString(environment.DefinitionEnvironmentId), 258 | "environmentName": environment.Name, 259 | "status": environment.Status, 260 | "triggerReason": environment.TriggerReason, 261 | "rank": int64ToString(environment.Rank), 262 | }) 263 | 264 | releaseEnvironmentStatusMetric.AddBool(prometheus.Labels{ 265 | "projectID": project.Id, 266 | "releaseID": int64ToString(release.Id), 267 | "releaseDefinitionID": int64ToString(release.Definition.Id), 268 | "environmentID": int64ToString(environment.DefinitionEnvironmentId), 269 | "type": "succeeded", 270 | }, environment.Status == "succeeded") 271 | 272 | releaseEnvironmentStatusMetric.AddTime(prometheus.Labels{ 273 | "projectID": project.Id, 274 | "releaseID": int64ToString(release.Id), 275 | "releaseDefinitionID": int64ToString(release.Definition.Id), 276 | "environmentID": int64ToString(environment.DefinitionEnvironmentId), 277 | "type": "created", 278 | }, environment.CreatedOn) 279 | 280 | releaseEnvironmentStatusMetric.AddIfNotZero(prometheus.Labels{ 281 | "projectID": project.Id, 282 | "releaseID": int64ToString(release.Id), 283 | "releaseDefinitionID": int64ToString(release.Definition.Id), 284 | "environmentID": int64ToString(environment.DefinitionEnvironmentId), 285 | "type": "jobDuration", 286 | }, environment.TimeToDeploy*60) 287 | 288 | for _, approval := range environment.PreDeployApprovals { 289 | // skip automated approvals 290 | if approval.IsAutomated { 291 | continue 292 | } 293 | 294 | releaseEnvironmentApprovalMetric.AddTime(prometheus.Labels{ 295 | "projectID": project.Id, 296 | "releaseID": int64ToString(release.Id), 297 | "releaseDefinitionID": int64ToString(release.Definition.Id), 298 | "environmentID": int64ToString(environment.DefinitionEnvironmentId), 299 | "approvalType": approval.ApprovalType, 300 | "status": approval.Status, 301 | "isAutomated": to.BoolString(approval.IsAutomated), 302 | "trialNumber": int64ToString(approval.TrialNumber), 303 | "attempt": int64ToString(approval.Attempt), 304 | "rank": int64ToString(approval.Rank), 305 | "approver": approval.Approver.DisplayName, 306 | "approvedBy": approval.ApprovedBy.DisplayName, 307 | }, approval.CreatedOn) 308 | } 309 | 310 | for _, approval := range environment.PostDeployApprovals { 311 | // skip automated approvals 312 | if approval.IsAutomated { 313 | continue 314 | } 315 | 316 | releaseEnvironmentApprovalMetric.AddTime(prometheus.Labels{ 317 | "projectID": project.Id, 318 | "releaseID": int64ToString(release.Id), 319 | "releaseDefinitionID": int64ToString(release.Definition.Id), 320 | "environmentID": int64ToString(environment.DefinitionEnvironmentId), 321 | "approvalType": approval.ApprovalType, 322 | "status": approval.Status, 323 | "isAutomated": to.BoolString(approval.IsAutomated), 324 | "trialNumber": int64ToString(approval.TrialNumber), 325 | "attempt": int64ToString(approval.Attempt), 326 | "rank": int64ToString(approval.Rank), 327 | "approver": approval.Approver.DisplayName, 328 | "approvedBy": approval.ApprovedBy.DisplayName, 329 | }, approval.CreatedOn) 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /metrics_repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/remeh/sizedwaitgroup" 9 | "github.com/webdevops/go-common/prometheus/collector" 10 | "go.uber.org/zap" 11 | 12 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 13 | ) 14 | 15 | type MetricsCollectorRepository struct { 16 | collector.Processor 17 | 18 | prometheus struct { 19 | repository *prometheus.GaugeVec 20 | repositoryStats *prometheus.GaugeVec 21 | repositoryCommits *prometheus.CounterVec 22 | repositoryPushes *prometheus.CounterVec 23 | } 24 | } 25 | 26 | func (m *MetricsCollectorRepository) Setup(collector *collector.Collector) { 27 | m.Processor.Setup(collector) 28 | 29 | m.prometheus.repository = prometheus.NewGaugeVec( 30 | prometheus.GaugeOpts{ 31 | Name: "azure_devops_repository_info", 32 | Help: "Azure DevOps repository", 33 | }, 34 | []string{ 35 | "projectID", 36 | "repositoryID", 37 | "repositoryName", 38 | }, 39 | ) 40 | m.Collector.RegisterMetricList("repository", m.prometheus.repository, true) 41 | 42 | m.prometheus.repositoryStats = prometheus.NewGaugeVec( 43 | prometheus.GaugeOpts{ 44 | Name: "azure_devops_repository_stats", 45 | Help: "Azure DevOps repository", 46 | }, 47 | []string{ 48 | "projectID", 49 | "repositoryID", 50 | "type", 51 | }, 52 | ) 53 | m.Collector.RegisterMetricList("repositoryStats", m.prometheus.repositoryStats, true) 54 | 55 | m.prometheus.repositoryCommits = prometheus.NewCounterVec( 56 | prometheus.CounterOpts{ 57 | Name: "azure_devops_repository_commits", 58 | Help: "Azure DevOps repository commits", 59 | }, 60 | []string{ 61 | "projectID", 62 | "repositoryID", 63 | }, 64 | ) 65 | m.Collector.RegisterMetricList("repositoryCommits", m.prometheus.repositoryCommits, false) 66 | 67 | m.prometheus.repositoryPushes = prometheus.NewCounterVec( 68 | prometheus.CounterOpts{ 69 | Name: "azure_devops_repository_pushes", 70 | Help: "Azure DevOps repository pushes", 71 | }, 72 | []string{ 73 | "projectID", 74 | "repositoryID", 75 | }, 76 | ) 77 | m.Collector.RegisterMetricList("repositoryPushes", m.prometheus.repositoryPushes, false) 78 | } 79 | 80 | func (m *MetricsCollectorRepository) Reset() {} 81 | 82 | func (m *MetricsCollectorRepository) Collect(callback chan<- func()) { 83 | ctx := m.Context() 84 | logger := m.Logger() 85 | 86 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 87 | projectLogger := logger.With(zap.String("project", project.Name)) 88 | 89 | wg := sizedwaitgroup.New(5) 90 | for _, repository := range project.RepositoryList.List { 91 | if repository.Disabled() { 92 | continue 93 | } 94 | 95 | wg.Add() 96 | go func(ctx context.Context, callback chan<- func(), project devopsClient.Project, repository devopsClient.Repository) { 97 | defer wg.Done() 98 | repositoryLogger := projectLogger.With(zap.String("repository", repository.Name)) 99 | m.collectRepository(ctx, repositoryLogger, callback, project, repository) 100 | }(ctx, callback, project, repository) 101 | } 102 | wg.Wait() 103 | } 104 | } 105 | 106 | func (m *MetricsCollectorRepository) collectRepository(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project, repository devopsClient.Repository) { 107 | fromTime := time.Now().Add(-*m.Collector.GetScapeTime()) 108 | if val := m.Collector.GetLastScapeTime(); val != nil { 109 | fromTime = *val 110 | } 111 | 112 | repositoryMetric := m.Collector.GetMetricList("repository") 113 | repositoryStatsMetric := m.Collector.GetMetricList("repositoryStats") 114 | repositoryCommitsMetric := m.Collector.GetMetricList("repositoryCommits") 115 | repositoryPushesMetric := m.Collector.GetMetricList("repositoryPushes") 116 | 117 | repositoryMetric.AddInfo(prometheus.Labels{ 118 | "projectID": project.Id, 119 | "repositoryID": repository.Id, 120 | "repositoryName": repository.Name, 121 | }) 122 | 123 | if repository.Size > 0 { 124 | repositoryStatsMetric.Add(prometheus.Labels{ 125 | "projectID": project.Id, 126 | "repositoryID": repository.Id, 127 | "type": "size", 128 | }, float64(repository.Size)) 129 | } 130 | 131 | // get commit delta list 132 | commitList, err := AzureDevopsClient.ListCommits(project.Id, repository.Id, fromTime) 133 | if err == nil { 134 | repositoryCommitsMetric.Add(prometheus.Labels{ 135 | "projectID": project.Id, 136 | "repositoryID": repository.Id, 137 | }, float64(commitList.Count)) 138 | } else { 139 | logger.Error(err) 140 | } 141 | 142 | // get pushes delta list 143 | pushList, err := AzureDevopsClient.ListPushes(project.Id, repository.Id, fromTime) 144 | if err == nil { 145 | repositoryPushesMetric.Add(prometheus.Labels{ 146 | "projectID": project.Id, 147 | "repositoryID": repository.Id, 148 | }, float64(pushList.Count)) 149 | } else { 150 | logger.Error(err) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /metrics_resourceusage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/webdevops/go-common/prometheus/collector" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type MetricsCollectorResourceUsage struct { 12 | collector.Processor 13 | 14 | prometheus struct { 15 | resourceUsageBuild *prometheus.GaugeVec 16 | resourceUsageLicense *prometheus.GaugeVec 17 | } 18 | } 19 | 20 | func (m *MetricsCollectorResourceUsage) Setup(collector *collector.Collector) { 21 | m.Processor.Setup(collector) 22 | 23 | m.prometheus.resourceUsageBuild = prometheus.NewGaugeVec( 24 | prometheus.GaugeOpts{ 25 | Name: "azure_devops_resourceusage_build", 26 | Help: "Azure DevOps resource usage for build", 27 | }, 28 | []string{ 29 | "name", 30 | }, 31 | ) 32 | m.Collector.RegisterMetricList("resourceUsageBuild", m.prometheus.resourceUsageBuild, true) 33 | 34 | m.prometheus.resourceUsageLicense = prometheus.NewGaugeVec( 35 | prometheus.GaugeOpts{ 36 | Name: "azure_devops_resourceusage_license", 37 | Help: "Azure DevOps resource usage for license informations", 38 | }, 39 | []string{ 40 | "name", 41 | }, 42 | ) 43 | m.Collector.RegisterMetricList("resourceUsageLicense", m.prometheus.resourceUsageLicense, true) 44 | } 45 | 46 | func (m *MetricsCollectorResourceUsage) Reset() {} 47 | 48 | func (m *MetricsCollectorResourceUsage) Collect(callback chan<- func()) { 49 | ctx := m.Context() 50 | logger := m.Logger() 51 | 52 | m.collectResourceUsageBuild(ctx, logger, callback) 53 | m.collectResourceUsageAgent(ctx, logger, callback) 54 | } 55 | 56 | func (m *MetricsCollectorResourceUsage) collectResourceUsageAgent(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func()) { 57 | resourceUsage, err := AzureDevopsClient.GetResourceUsageAgent() 58 | if err != nil { 59 | logger.Error(err) 60 | return 61 | } 62 | 63 | resourceUsageMetric := m.Collector.GetMetricList("resourceUsageLicense") 64 | 65 | licenseDetails := resourceUsage.Data.Provider.TaskHubLicenseDetails 66 | 67 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 68 | "name": "FreeLicenseCount", 69 | }, licenseDetails.FreeLicenseCount) 70 | 71 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 72 | "name": "FreeHostedLicenseCount", 73 | }, licenseDetails.FreeHostedLicenseCount) 74 | 75 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 76 | "name": "EnterpriseUsersCount", 77 | }, licenseDetails.EnterpriseUsersCount) 78 | 79 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 80 | "name": "EnterpriseUsersCount", 81 | }, licenseDetails.EnterpriseUsersCount) 82 | 83 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 84 | "name": "PurchasedHostedLicenseCount", 85 | }, licenseDetails.PurchasedHostedLicenseCount) 86 | 87 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 88 | "name": "PurchasedHostedLicenseCount", 89 | }, licenseDetails.PurchasedHostedLicenseCount) 90 | 91 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 92 | "name": "TotalLicenseCount", 93 | }, licenseDetails.TotalLicenseCount) 94 | 95 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 96 | "name": "MsdnUsersCount", 97 | }, licenseDetails.MsdnUsersCount) 98 | 99 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 100 | "name": "HostedAgentMinutesFreeCount", 101 | }, licenseDetails.HostedAgentMinutesFreeCount) 102 | 103 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 104 | "name": "HostedAgentMinutesUsedCount", 105 | }, licenseDetails.HostedAgentMinutesUsedCount) 106 | 107 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 108 | "name": "TotalPrivateLicenseCount", 109 | }, licenseDetails.TotalPrivateLicenseCount) 110 | 111 | resourceUsageMetric.AddIfNotNil(prometheus.Labels{ 112 | "name": "TotalHostedLicenseCount", 113 | }, licenseDetails.TotalHostedLicenseCount) 114 | } 115 | 116 | func (m *MetricsCollectorResourceUsage) collectResourceUsageBuild(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func()) { 117 | resourceUsage, err := AzureDevopsClient.GetResourceUsageBuild() 118 | if err != nil { 119 | logger.Error(err) 120 | return 121 | } 122 | 123 | resourceUsageMetric := m.Collector.GetMetricList("resourceUsageBuild") 124 | 125 | if resourceUsage.DistributedTaskAgents != nil { 126 | resourceUsageMetric.Add(prometheus.Labels{ 127 | "name": "DistributedTaskAgents", 128 | }, float64(*resourceUsage.DistributedTaskAgents)) 129 | } 130 | 131 | if resourceUsage.PaidPrivateAgentSlots != nil { 132 | resourceUsageMetric.Add(prometheus.Labels{ 133 | "name": "PaidPrivateAgentSlots", 134 | }, float64(*resourceUsage.PaidPrivateAgentSlots)) 135 | } 136 | 137 | if resourceUsage.TotalUsage != nil { 138 | resourceUsageMetric.Add(prometheus.Labels{ 139 | "name": "TotalUsage", 140 | }, float64(*resourceUsage.TotalUsage)) 141 | } 142 | 143 | if resourceUsage.XamlControllers != nil { 144 | resourceUsageMetric.Add(prometheus.Labels{ 145 | "name": "XamlControllers", 146 | }, float64(*resourceUsage.XamlControllers)) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /metrics_stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/webdevops/go-common/prometheus/collector" 9 | "go.uber.org/zap" 10 | 11 | devopsClient "github.com/webdevops/azure-devops-exporter/azure-devops-client" 12 | ) 13 | 14 | type MetricsCollectorStats struct { 15 | collector.Processor 16 | 17 | prometheus struct { 18 | agentPoolBuildCount *prometheus.CounterVec 19 | agentPoolBuildWait *prometheus.SummaryVec 20 | agentPoolBuildDuration *prometheus.SummaryVec 21 | 22 | projectBuildCount *prometheus.CounterVec 23 | projectBuildWait *prometheus.SummaryVec 24 | projectBuildDuration *prometheus.SummaryVec 25 | projectBuildSuccess *prometheus.SummaryVec 26 | projectReleaseDuration *prometheus.SummaryVec 27 | projectReleaseSuccess *prometheus.SummaryVec 28 | } 29 | } 30 | 31 | func (m *MetricsCollectorStats) Setup(collector *collector.Collector) { 32 | m.Processor.Setup(collector) 33 | 34 | // ------------------------------------------ 35 | // AgentPool 36 | // ------------------------------------------ 37 | 38 | m.prometheus.agentPoolBuildCount = prometheus.NewCounterVec( 39 | prometheus.CounterOpts{ 40 | Name: "azure_devops_stats_agentpool_builds", 41 | Help: "Azure DevOps stats agentpool builds counter", 42 | }, 43 | []string{ 44 | "agentPoolID", 45 | "projectID", 46 | "result", 47 | }, 48 | ) 49 | m.Collector.RegisterMetricList("agentPoolBuildCount", m.prometheus.agentPoolBuildCount, false) 50 | 51 | m.prometheus.agentPoolBuildWait = prometheus.NewSummaryVec( 52 | prometheus.SummaryOpts{ 53 | Name: "azure_devops_stats_agentpool_builds_wait", 54 | Help: "Azure DevOps stats agentpool builds wait duration", 55 | MaxAge: *Opts.Stats.SummaryMaxAge, 56 | }, 57 | []string{ 58 | "agentPoolID", 59 | "projectID", 60 | "buildDefinitionID", 61 | "result", 62 | }, 63 | ) 64 | m.Collector.RegisterMetricList("agentPoolBuildWait", m.prometheus.agentPoolBuildWait, false) 65 | 66 | m.prometheus.agentPoolBuildDuration = prometheus.NewSummaryVec( 67 | prometheus.SummaryOpts{ 68 | Name: "azure_devops_stats_agentpool_builds_duration", 69 | Help: "Azure DevOps stats agentpool builds process duration", 70 | MaxAge: *Opts.Stats.SummaryMaxAge, 71 | }, 72 | []string{ 73 | "agentPoolID", 74 | "projectID", 75 | "result", 76 | }, 77 | ) 78 | m.Collector.RegisterMetricList("agentPoolBuildDuration", m.prometheus.agentPoolBuildDuration, false) 79 | 80 | // ------------------------------------------ 81 | // Project 82 | // ------------------------------------------ 83 | 84 | m.prometheus.projectBuildCount = prometheus.NewCounterVec( 85 | prometheus.CounterOpts{ 86 | Name: "azure_devops_stats_project_builds", 87 | Help: "Azure DevOps stats project builds counter", 88 | }, 89 | []string{ 90 | "projectID", 91 | "buildDefinitionID", 92 | "result", 93 | }, 94 | ) 95 | m.Collector.RegisterMetricList("projectBuildCount", m.prometheus.projectBuildCount, false) 96 | 97 | m.prometheus.projectBuildSuccess = prometheus.NewSummaryVec( 98 | prometheus.SummaryOpts{ 99 | Name: "azure_devops_stats_project_success", 100 | Help: "Azure DevOps stats project success", 101 | }, 102 | []string{ 103 | "projectID", 104 | "buildDefinitionID", 105 | }, 106 | ) 107 | m.Collector.RegisterMetricList("projectBuildSuccess", m.prometheus.projectBuildSuccess, false) 108 | 109 | m.prometheus.projectBuildWait = prometheus.NewSummaryVec( 110 | prometheus.SummaryOpts{ 111 | Name: "azure_devops_stats_project_builds_wait", 112 | Help: "Azure DevOps stats project builds wait duration", 113 | MaxAge: *Opts.Stats.SummaryMaxAge, 114 | }, 115 | []string{ 116 | "projectID", 117 | "buildDefinitionID", 118 | "result", 119 | }, 120 | ) 121 | m.Collector.RegisterMetricList("projectBuildWait", m.prometheus.projectBuildWait, false) 122 | 123 | m.prometheus.projectBuildDuration = prometheus.NewSummaryVec( 124 | prometheus.SummaryOpts{ 125 | Name: "azure_devops_stats_project_builds_duration", 126 | Help: "Azure DevOps stats project builds process duration", 127 | MaxAge: *Opts.Stats.SummaryMaxAge, 128 | }, 129 | []string{ 130 | "projectID", 131 | "buildDefinitionID", 132 | "result", 133 | }, 134 | ) 135 | m.Collector.RegisterMetricList("projectBuildDuration", m.prometheus.projectBuildDuration, false) 136 | 137 | m.prometheus.projectReleaseDuration = prometheus.NewSummaryVec( 138 | prometheus.SummaryOpts{ 139 | Name: "azure_devops_stats_project_release_duration", 140 | Help: "Azure DevOps stats project release process duration", 141 | MaxAge: *Opts.Stats.SummaryMaxAge, 142 | }, 143 | []string{ 144 | "projectID", 145 | "releaseDefinitionID", 146 | "definitionEnvironmentID", 147 | "status", 148 | }, 149 | ) 150 | m.Collector.RegisterMetricList("projectReleaseDuration", m.prometheus.projectReleaseDuration, false) 151 | 152 | m.prometheus.projectReleaseSuccess = prometheus.NewSummaryVec( 153 | prometheus.SummaryOpts{ 154 | Name: "azure_devops_stats_project_release_success", 155 | Help: "Azure DevOps stats project release success", 156 | MaxAge: *Opts.Stats.SummaryMaxAge, 157 | }, 158 | []string{ 159 | "projectID", 160 | "releaseDefinitionID", 161 | "definitionEnvironmentID", 162 | }, 163 | ) 164 | m.Collector.RegisterMetricList("projectReleaseSuccess", m.prometheus.projectReleaseSuccess, false) 165 | } 166 | 167 | func (m *MetricsCollectorStats) Reset() {} 168 | 169 | func (m *MetricsCollectorStats) Collect(callback chan<- func()) { 170 | ctx := m.Context() 171 | logger := m.Logger() 172 | 173 | for _, project := range AzureDevopsServiceDiscovery.ProjectList() { 174 | projectLogger := logger.With(zap.String("project", project.Name)) 175 | m.CollectBuilds(ctx, projectLogger, callback, project) 176 | m.CollectReleases(ctx, projectLogger, callback, project) 177 | } 178 | } 179 | 180 | func (m *MetricsCollectorStats) CollectReleases(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 181 | minTime := time.Now().Add(-*m.Collector.GetScapeTime()) 182 | if val := m.Collector.GetLastScapeTime(); val != nil { 183 | minTime = *val 184 | } 185 | 186 | releaseList, err := AzureDevopsClient.ListReleaseHistory(project.Id, minTime) 187 | if err != nil { 188 | logger.Error(err) 189 | return 190 | } 191 | 192 | for _, release := range releaseList.List { 193 | for _, environment := range release.Environments { 194 | switch environment.Status { 195 | case "succeeded": 196 | m.prometheus.projectReleaseSuccess.With(prometheus.Labels{ 197 | "projectID": release.Project.Id, 198 | "releaseDefinitionID": int64ToString(release.Definition.Id), 199 | "definitionEnvironmentID": int64ToString(environment.DefinitionEnvironmentId), 200 | }).Observe(1) 201 | case "failed", "partiallySucceeded": 202 | m.prometheus.projectReleaseSuccess.With(prometheus.Labels{ 203 | "projectID": release.Project.Id, 204 | "releaseDefinitionID": int64ToString(release.Definition.Id), 205 | "definitionEnvironmentID": int64ToString(environment.DefinitionEnvironmentId), 206 | }).Observe(0) 207 | } 208 | 209 | timeToDeploy := environment.TimeToDeploy * 60 210 | if timeToDeploy > 0 { 211 | m.prometheus.projectReleaseDuration.With(prometheus.Labels{ 212 | "projectID": release.Project.Id, 213 | "releaseDefinitionID": int64ToString(release.Definition.Id), 214 | "definitionEnvironmentID": int64ToString(environment.DefinitionEnvironmentId), 215 | "status": environment.Status, 216 | }).Observe(timeToDeploy) 217 | } 218 | } 219 | } 220 | } 221 | 222 | func (m *MetricsCollectorStats) CollectBuilds(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) { 223 | minTime := time.Now().Add(-Opts.Limit.BuildHistoryDuration) 224 | 225 | buildList, err := AzureDevopsClient.ListBuildHistoryWithStatus(project.Id, minTime, "completed") 226 | if err != nil { 227 | logger.Error(err) 228 | return 229 | } 230 | 231 | for _, build := range buildList.List { 232 | waitDuration := build.QueueDuration().Seconds() 233 | 234 | m.prometheus.agentPoolBuildCount.With(prometheus.Labels{ 235 | "agentPoolID": int64ToString(build.Queue.Pool.Id), 236 | "projectID": build.Project.Id, 237 | "result": build.Result, 238 | }).Inc() 239 | 240 | m.prometheus.projectBuildCount.With(prometheus.Labels{ 241 | "projectID": build.Project.Id, 242 | "buildDefinitionID": int64ToString(build.Definition.Id), 243 | "result": build.Result, 244 | }).Inc() 245 | 246 | switch build.Result { 247 | case "succeeded": 248 | m.prometheus.projectBuildSuccess.With(prometheus.Labels{ 249 | "projectID": build.Project.Id, 250 | "buildDefinitionID": int64ToString(build.Definition.Id), 251 | }).Observe(1) 252 | case "failed": 253 | m.prometheus.projectBuildSuccess.With(prometheus.Labels{ 254 | "projectID": build.Project.Id, 255 | "buildDefinitionID": int64ToString(build.Definition.Id), 256 | }).Observe(0) 257 | } 258 | 259 | if build.FinishTime.Second() >= 0 { 260 | jobDuration := build.FinishTime.Sub(build.StartTime) 261 | 262 | m.prometheus.agentPoolBuildDuration.With(prometheus.Labels{ 263 | "agentPoolID": int64ToString(build.Queue.Pool.Id), 264 | "projectID": build.Project.Id, 265 | "result": build.Result, 266 | }).Observe(jobDuration.Seconds()) 267 | 268 | m.prometheus.projectBuildDuration.With(prometheus.Labels{ 269 | "projectID": build.Project.Id, 270 | "buildDefinitionID": int64ToString(build.Definition.Id), 271 | "result": build.Result, 272 | }).Observe(jobDuration.Seconds()) 273 | } 274 | 275 | if waitDuration >= 0 { 276 | m.prometheus.agentPoolBuildWait.With(prometheus.Labels{ 277 | "agentPoolID": int64ToString(build.Queue.Pool.Id), 278 | "projectID": build.Project.Id, 279 | "buildDefinitionID": int64ToString(build.Definition.Id), 280 | "result": build.Result, 281 | }).Observe(waitDuration) 282 | 283 | m.prometheus.projectBuildWait.With(prometheus.Labels{ 284 | "projectID": build.Project.Id, 285 | "buildDefinitionID": int64ToString(build.Definition.Id), 286 | "result": build.Result, 287 | }).Observe(waitDuration) 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | func int64ToString(v int64) string { 9 | return strconv.FormatInt(v, 10) 10 | } 11 | 12 | func arrayStringContains(s []string, e string) bool { 13 | for _, a := range s { 14 | if a == e { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func arrayIntContains(s []int64, e int64) bool { 22 | for _, a := range s { 23 | if a == e { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | func timeToFloat64(v time.Time) float64 { 31 | return float64(v.Unix()) 32 | } 33 | -------------------------------------------------------------------------------- /servicediscovery.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | cache "github.com/patrickmn/go-cache" 8 | "go.uber.org/zap" 9 | 10 | AzureDevops "github.com/webdevops/azure-devops-exporter/azure-devops-client" 11 | ) 12 | 13 | const ( 14 | azureDevopsServiceDiscoveryCacheKeyProjectList = "projects" 15 | azureDevopsServiceDiscoveryCacheKeyAgentPoolList = "agentpools" 16 | ) 17 | 18 | type ( 19 | azureDevopsServiceDiscovery struct { 20 | cache *cache.Cache 21 | cacheExpiry time.Duration 22 | 23 | logger *zap.SugaredLogger 24 | 25 | lock struct { 26 | projectList sync.Mutex 27 | agentpoolList sync.Mutex 28 | } 29 | } 30 | ) 31 | 32 | func NewAzureDevopsServiceDiscovery() *azureDevopsServiceDiscovery { 33 | sd := &azureDevopsServiceDiscovery{} 34 | sd.cacheExpiry = Opts.ServiceDiscovery.RefreshDuration 35 | sd.cache = cache.New(sd.cacheExpiry, time.Duration(1*time.Minute)) 36 | sd.logger = logger.With(zap.String("component", "servicediscovery")) 37 | 38 | sd.logger.Infof("init AzureDevops servicediscovery with %v cache", sd.cacheExpiry.String()) 39 | return sd 40 | } 41 | 42 | func (sd *azureDevopsServiceDiscovery) Update() { 43 | sd.cache.Flush() 44 | sd.ProjectList() 45 | sd.AgentPoolList() 46 | } 47 | 48 | func (sd *azureDevopsServiceDiscovery) ProjectList() (list []AzureDevops.Project) { 49 | sd.lock.projectList.Lock() 50 | defer sd.lock.projectList.Unlock() 51 | 52 | if val, ok := sd.cache.Get(azureDevopsServiceDiscoveryCacheKeyProjectList); ok { 53 | // fetched from cache 54 | list = val.([]AzureDevops.Project) 55 | return 56 | } 57 | 58 | // cache was invalid, fetch data from api 59 | sd.logger.Infof("updating project list") 60 | result, err := AzureDevopsClient.ListProjects() 61 | if err != nil { 62 | sd.logger.Panic(err) 63 | } 64 | 65 | sd.logger.Infof("fetched %v projects", result.Count) 66 | 67 | list = result.List 68 | 69 | // whitelist 70 | if len(Opts.AzureDevops.FilterProjects) > 0 { 71 | rawList := list 72 | list = []AzureDevops.Project{} 73 | for _, project := range rawList { 74 | if arrayStringContains(Opts.AzureDevops.FilterProjects, project.Id) { 75 | list = append(list, project) 76 | } 77 | } 78 | } 79 | 80 | // blacklist 81 | if len(Opts.AzureDevops.BlacklistProjects) > 0 { 82 | // filter ignored azure devops projects 83 | rawList := list 84 | list = []AzureDevops.Project{} 85 | for _, project := range rawList { 86 | if !arrayStringContains(Opts.AzureDevops.BlacklistProjects, project.Id) { 87 | list = append(list, project) 88 | } 89 | } 90 | } 91 | 92 | // save to cache 93 | sd.cache.SetDefault(azureDevopsServiceDiscoveryCacheKeyProjectList, list) 94 | 95 | return 96 | } 97 | 98 | func (sd *azureDevopsServiceDiscovery) AgentPoolList() (list []int64) { 99 | sd.lock.agentpoolList.Lock() 100 | defer sd.lock.agentpoolList.Unlock() 101 | 102 | if val, ok := sd.cache.Get(azureDevopsServiceDiscoveryCacheKeyAgentPoolList); ok { 103 | // fetched from cache 104 | list = val.([]int64) 105 | return 106 | } 107 | 108 | if Opts.AzureDevops.AgentPoolIdList != nil { 109 | sd.logger.Infof("using predefined AgentPool list") 110 | list = *Opts.AzureDevops.AgentPoolIdList 111 | } else { 112 | sd.logger.Infof("upading AgentPool list") 113 | 114 | result, err := AzureDevopsClient.ListAgentPools() 115 | if err != nil { 116 | sd.logger.Panic(err) 117 | return 118 | } 119 | sd.logger.Infof("fetched %v agentpools", result.Count) 120 | 121 | for _, agentPool := range result.Value { 122 | list = append(list, agentPool.ID) 123 | } 124 | } 125 | 126 | // save to cache 127 | sd.cache.SetDefault(azureDevopsServiceDiscoveryCacheKeyAgentPoolList, list) 128 | 129 | return 130 | } 131 | --------------------------------------------------------------------------------