├── .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 ├── common.logger.go ├── common.system.go ├── config └── opts.go ├── example.azure.yaml ├── example.yaml ├── global-metrics.go ├── go.mod ├── go.sum ├── main.go ├── probe.go └── templates └── query.html /.dockerignore: -------------------------------------------------------------------------------- 1 | /azure-resourcegraph-exporter 2 | /example 3 | /release-assets 4 | -------------------------------------------------------------------------------- /.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 | /azure-resourcegraph-exporter 3 | /*.exe 4 | /release-assets 5 | -------------------------------------------------------------------------------- /.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-resourcegraph-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-resourcegraph-exporter/azure-resourcegraph-exporter . 28 | RUN ["./azure-resourcegraph-exporter", "--help"] 29 | 30 | ############################################# 31 | # Final 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-resourcegraph-exporter"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 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 ResourceGraph exporter 2 | 3 | [![license](https://img.shields.io/github/license/webdevops/azure-resourcegraph-exporter.svg)](https://github.com/webdevops/azure-resourcegraph-exporter/blob/master/LICENSE) 4 | [![DockerHub](https://img.shields.io/badge/DockerHub-webdevops%2Fazure--resourcegraph--exporter-blue)](https://hub.docker.com/r/webdevops/azure-resourcegraph-exporter/) 5 | [![Quay.io](https://img.shields.io/badge/Quay.io-webdevops%2Fazure--resourcegraph--exporter-blue)](https://quay.io/repository/webdevops/azure-resourcegraph-exporter) 6 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/azure-resourcegraph-exporter)](https://artifacthub.io/packages/search?repo=azure-resourcegraph-exporter) 7 | 8 | Prometheus exporter for Azure ResourceGraph queries with configurable fields and transformations. 9 | 10 | ## Usage 11 | 12 | ``` 13 | Usage: 14 | azure-resourcegraph-exporter [OPTIONS] 15 | 16 | Application Options: 17 | --log.debug debug mode [$LOG_DEBUG] 18 | --log.devel development mode [$LOG_DEVEL] 19 | --log.json Switch log output to json format [$LOG_JSON] 20 | --azure-environment= Azure environment name (default: AZUREPUBLICCLOUD) [$AZURE_ENVIRONMENT] 21 | --azure-subscription= Azure subscription ID [$AZURE_SUBSCRIPTION_ID] 22 | -c, --config= Config path [$CONFIG] 23 | --server.bind= Server address (default: :8080) [$SERVER_BIND] 24 | --server.timeout.read= Server read timeout (default: 5s) [$SERVER_TIMEOUT_READ] 25 | --server.timeout.write= Server write timeout (default: 10s) [$SERVER_TIMEOUT_WRITE] 26 | 27 | Help Options: 28 | -h, --help Show this help message 29 | ``` 30 | 31 | 32 | for Azure API authentication (using ENV vars) see following documentations: 33 | - https://github.com/webdevops/go-common/blob/main/azuresdk/README.md 34 | - https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication 35 | 36 | 37 | ### Configuration file 38 | 39 | * see [example.yaml](example.yaml) 40 | * see [example.azure.yaml](example.azure.yaml) 41 | 42 | ## HTTP Endpoints 43 | 44 | | Endpoint | Description | 45 | |--------------------------------|-------------------------------------------------------------------------------------| 46 | | `/metrics` | Default prometheus golang metrics | 47 | | `/probe` | Execute resourcegraph queries without set module name | 48 | | `/probe?module=xzy` | Execute resourcegraph queries for module `xzy` | 49 | | `/probe?module=xzy&cache=2m` | Execute resourcegraph queries for module `xzy` and enable caching for 2 minutes | 50 | 51 | ## Global metrics 52 | 53 | | Metric | Description | 54 | |--------------------------------------|--------------------------------------------------------------------------------| 55 | | `azure_resourcegraph_query_time` | Summary metric about query execution time (incl. all subqueries) | 56 | | `azure_resourcegraph_query_results` | Number of results from query | 57 | | `azure_resourcegraph_query_requests` | Count of requests (eg paged subqueries) per query | 58 | 59 | ### AzureTracing metrics 60 | 61 | see [armclient tracing documentation](https://github.com/webdevops/go-common/blob/main/azuresdk/README.md#azuretracing-metrics) 62 | 63 | ## Example 64 | 65 | Config file: 66 | ``` 67 | queries: 68 | - metric: azure_resourcestype_count 69 | query: |- 70 | Resources 71 | | summarize count() by type 72 | fields: 73 | - name: count_ 74 | type: value 75 | 76 | ``` 77 | 78 | Metrics: 79 | ``` 80 | # HELP azure_resourcestype_count azure_resourcestype_count 81 | # TYPE azure_resourcestype_count gauge 82 | azure_resourcestype_count{type="microsoft.compute/virtualmachinescalesets"} 2 83 | azure_resourcestype_count{type="microsoft.containerservice/managedclusters"} 1 84 | azure_resourcestype_count{type="microsoft.keyvault/vaults"} 2 85 | azure_resourcestype_count{type="microsoft.managedidentity/userassignedidentities"} 2 86 | azure_resourcestype_count{type="microsoft.network/networksecuritygroups"} 1 87 | azure_resourcestype_count{type="microsoft.network/networkwatchers"} 2 88 | azure_resourcestype_count{type="microsoft.network/routetables"} 3 89 | azure_resourcestype_count{type="microsoft.network/virtualnetworks"} 2 90 | azure_resourcestype_count{type="microsoft.storage/storageaccounts"} 1 91 | ``` 92 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | // azure 18 | Azure struct { 19 | Environment *string `long:"azure-environment" env:"AZURE_ENVIRONMENT" description:"Azure environment name" default:"AZUREPUBLICCLOUD"` 20 | Subscription []string `long:"azure-subscription" env:"AZURE_SUBSCRIPTION_ID" env-delim:" " description:"Azure subscription ID"` 21 | } 22 | 23 | // config 24 | Config struct { 25 | Path string `long:"config" short:"c" env:"CONFIG" description:"Config path" required:"true"` 26 | } 27 | 28 | // general options 29 | Server struct { 30 | // general options 31 | Bind string `long:"server.bind" env:"SERVER_BIND" description:"Server address" default:":8080"` 32 | ReadTimeout time.Duration `long:"server.timeout.read" env:"SERVER_TIMEOUT_READ" description:"Server read timeout" default:"5s"` 33 | WriteTimeout time.Duration `long:"server.timeout.write" env:"SERVER_TIMEOUT_WRITE" description:"Server write timeout" default:"10s"` 34 | } 35 | } 36 | ) 37 | 38 | func (o *Opts) GetJson() []byte { 39 | jsonBytes, err := json.Marshal(o) 40 | if err != nil { 41 | panic(err) 42 | } 43 | return jsonBytes 44 | } 45 | -------------------------------------------------------------------------------- /example.azure.yaml: -------------------------------------------------------------------------------- 1 | tagFields: &tagFields 2 | - name: owner 3 | - name: domain 4 | tagDefaultField: &defaultTagField 5 | type: ignore 6 | 7 | queries: 8 | ######################################################### 9 | ## ResourceType count with SKU list 10 | ######################################################### 11 | - metric: azurerm_resourcestype 12 | query: |- 13 | Resources 14 | | project subscriptionId, type, sku, sku_properties = properties.sku 15 | | join kind=inner ( 16 | Resources | project subscriptionId, type | summarize count() by subscriptionId,type 17 | ) on subscriptionId,type 18 | value: 1 19 | fields: 20 | - 21 | name: type 22 | type: id 23 | - 24 | name: subscriptionId 25 | type: id 26 | - 27 | name: count_ 28 | metric: azurerm_resourcestype_resourcecount 29 | type: value 30 | - 31 | name: sku 32 | metric: azurerm_resourcestype_sku 33 | expand: 34 | value: 1 35 | - 36 | name: sku_properties 37 | metric: azurerm_resourcestype_sku 38 | expand: 39 | value: 1 40 | defaultField: 41 | type: ignore 42 | 43 | ######################################################### 44 | ## Subscriptions 45 | ######################################################### 46 | - metric: azurerm_subscription_info 47 | query: |- 48 | ResourceContainers 49 | | where type == "microsoft.resources/subscriptions" 50 | value: 1 51 | fields: 52 | - 53 | name: name 54 | - 55 | name: subscriptionId 56 | target: subscriptionID 57 | type: id 58 | defaultField: 59 | type: ignore 60 | 61 | 62 | ######################################################### 63 | ## ResourceGroups with resourcecount and tags 64 | ######################################################### 65 | - metric: azurerm_resourcegroup_info 66 | query: |- 67 | ResourceContainers 68 | | where type == "microsoft.resources/subscriptions/resourcegroups" 69 | | join kind=inner ( 70 | Resources | project subscriptionId, resourceGroup | summarize resourceCount=count() by subscriptionId,resourceGroup 71 | ) on subscriptionId,resourceGroup 72 | value: 1 73 | fields: 74 | - 75 | name: resourceGroup 76 | type: id 77 | - 78 | name: subscriptionId 79 | target: subscriptionID 80 | type: id 81 | - 82 | name: resourceCount 83 | metric: azurerm_resourcegroup_resourcecount 84 | type: value 85 | - 86 | name: tags 87 | metric: azurerm_resourcegroup_tags 88 | expand: 89 | value: 1 90 | fields: *tagFields 91 | defaultField: *defaultTagField 92 | - 93 | name: tags 94 | metric: azurerm_resourcegroup_lastupdate 95 | expand: 96 | value: 0 97 | fields: 98 | - name: lastUpdate 99 | filters: [toUnixtime] 100 | type: value 101 | defaultField: 102 | type: ignore 103 | 104 | defaultField: 105 | type: ignore 106 | 107 | ######################################################### 108 | ## AKS 109 | ######################################################### 110 | - metric: azurerm_managedclusters_aks_info 111 | query: |- 112 | Resources 113 | | where type == "microsoft.containerservice/managedclusters" 114 | | where isnotempty(properties.kubernetesVersion) 115 | | project id, name, subscriptionId, location, type, resourceGroup, tags, version = properties.kubernetesVersion, agentPoolProfiles = properties.agentPoolProfiles 116 | value: 1 117 | fields: 118 | - 119 | name: id 120 | target: resourceID 121 | type: id 122 | - 123 | name: name 124 | target: cluster 125 | - 126 | name: subscriptionId 127 | target: subscriptionID 128 | - 129 | name: location 130 | - 131 | name: type 132 | target: provider 133 | - 134 | name: resourceGroup 135 | - 136 | name: kubernetesVersion 137 | - 138 | name: tags 139 | metric: azurerm_managedclusters_tags 140 | expand: 141 | value: 1 142 | fields: *tagFields 143 | defaultField: *defaultTagField 144 | - 145 | name: agentPoolProfiles 146 | metric: azurerm_managedclusters_aks_pool 147 | expand: 148 | value: 1 149 | fields: 150 | - 151 | name: name 152 | target: pool 153 | type: id 154 | - 155 | name: osType 156 | - 157 | name: vmSize 158 | - 159 | name: orchestratorVersion 160 | target: version 161 | - 162 | name: enableAutoScaling 163 | type: boolean 164 | target: autoScaling 165 | - 166 | name: count 167 | metric: azurerm_managedclusters_aks_pool_size 168 | type: value 169 | - 170 | name: minCount 171 | metric: azurerm_managedclusters_aks_pool_size_min 172 | type: value 173 | - 174 | name: maxCount 175 | metric: azurerm_managedclusters_aks_pool_size_max 176 | type: value 177 | - 178 | name: maxPods 179 | metric: azurerm_managedclusters_aks_pool_maxpods 180 | type: value 181 | - 182 | name: osDiskSizeGB 183 | metric: azurerm_managedclusters_aks_pool_os_disksize 184 | type: value 185 | 186 | defaultField: 187 | type: ignore 188 | 189 | defaultField: 190 | type: ignore 191 | 192 | ######################################################### 193 | ## ManagedClusters 194 | ######################################################### 195 | - metric: azurerm_vmss_info 196 | query: |- 197 | Resources 198 | | where type == "microsoft.compute/virtualmachinescalesets" 199 | value: 1 200 | fields: 201 | - 202 | name: id 203 | target: resourceID 204 | type: id 205 | - 206 | name: name 207 | target: cluster 208 | - 209 | name: subscriptionId 210 | target: subscriptionID 211 | - 212 | name: location 213 | - 214 | name: type 215 | target: provider 216 | - 217 | name: resourceGroup 218 | - 219 | name: sku 220 | metric: azurerm_vmss_capacity 221 | expand: 222 | fields: 223 | - name: capacity 224 | type: value 225 | defaultField: 226 | type: ignore 227 | - 228 | name: tags 229 | metric: azurerm_vmss_tags 230 | expand: 231 | value: 1 232 | fields: *tagFields 233 | defaultField: *defaultTagField 234 | 235 | defaultField: 236 | type: ignore 237 | 238 | ######################################################### 239 | ## Resource info with labels 240 | ######################################################### 241 | # be aware that this might exceed the row limit of ResourceGraph queries! 242 | # this example might be better for azure-resourcemanager-exporter 243 | - metric: azurerm_resource_info 244 | query: |- 245 | Resources 246 | value: 1 247 | fields: 248 | - 249 | name: id 250 | target: resourceID 251 | type: id 252 | - 253 | name: subscriptionId 254 | target: subscriptionID 255 | - 256 | name: location 257 | - 258 | name: type 259 | target: provider 260 | - 261 | name: resourceGroup 262 | - 263 | name: tags 264 | metric: azure_resource_tags 265 | expand: 266 | value: 1 267 | fields: *tagFields 268 | defaultField: *defaultTagField 269 | 270 | defaultField: 271 | type: ignore 272 | 273 | -------------------------------------------------------------------------------- /example.yaml: -------------------------------------------------------------------------------- 1 | queries: 2 | 3 | # name of metric 4 | - metric: azure_resources 5 | 6 | # skip metric publishing 7 | # and only publish sub metrics rows (and use configuration only for submetrics) 8 | # publish: false 9 | 10 | # Azure ResourceGraph query 11 | query: |- 12 | Resources 13 | | top 50 by name desc 14 | 15 | ## default value for metric 16 | ## eg for static metrics like informational metrics 17 | value: 1 18 | 19 | ## additional labels (optional) 20 | labels: 21 | scope: resources 22 | 23 | fields: 24 | ## name of the field from the result 25 | - name: id 26 | ## target name for metric label 27 | target: resourceId 28 | ## type of field 29 | ## id: use as identification (added to sub metrics) 30 | ## value: value of metric 31 | ## expand: parse value as sub json structure and create sub metric 32 | ## ignore: do not add this field 33 | type: id 34 | 35 | ## apply filter to value 36 | ## available filters: toLower, toUpper, toTitle 37 | filters: [toLower] 38 | 39 | ## example for regexp manipulation 40 | ## replace microsoft with foobar 41 | - name: type 42 | filters: 43 | - type: regexp 44 | regexp: "microsoft(.*)" 45 | replacement: "foobar$1" 46 | 47 | - name: enableRbacAuthorization 48 | type: ignore 49 | 50 | ## expand tags into own metric 51 | - name: tags 52 | metric: azure_resources_tags 53 | expand: {} 54 | ## additional labels (optional) 55 | labels: 56 | scope: resourcetags 57 | 58 | ## expand properties 59 | - name: properties 60 | metric: azure_resources_props 61 | expand: 62 | fields: 63 | ## ignore this field 64 | - name: enableRbacAuthorization 65 | type: ignore 66 | 67 | defaultField: 68 | type: ignore 69 | 70 | 71 | - metric: azure_resourcestype_count 72 | ## only responds to /probe?module=summary 73 | module: summary 74 | query: |- 75 | Resources 76 | | summarize count() by type 77 | fields: 78 | ## use count_ as metrics value 79 | ## hint: result field must be int or float 80 | - name: count_ 81 | type: value 82 | 83 | - metric: azure_resourcestype 84 | ## only responds to /probe?module=summary 85 | module: summary 86 | query: |- 87 | Resources 88 | | project type, tags 89 | ## only use following subscriptions 90 | subscriptions2: 91 | - axxxx-xxxxx-xxxxxx-xxxxx 92 | - bxxxx-xxxxx-xxxxxx-xxxxx 93 | - cxxxx-xxxxx-xxxxxx-xxxxx 94 | fields: 95 | ## use count_ as metrics value 96 | ## hint: result field must be int or float 97 | - name: count_ 98 | type: value 99 | -------------------------------------------------------------------------------- /global-metrics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | prometheusQueryTime *prometheus.SummaryVec 7 | prometheusQueryResults *prometheus.GaugeVec 8 | prometheusQueryRequests *prometheus.CounterVec 9 | ) 10 | 11 | func initGlobalMetrics() { 12 | prometheusQueryTime = prometheus.NewSummaryVec( 13 | prometheus.SummaryOpts{ 14 | Name: "azure_resourcegraph_query_time", 15 | Help: "Azure ResourceGraph Query time", 16 | }, 17 | []string{ 18 | "module", 19 | "metric", 20 | }, 21 | ) 22 | prometheus.MustRegister(prometheusQueryTime) 23 | 24 | prometheusQueryResults = prometheus.NewGaugeVec( 25 | prometheus.GaugeOpts{ 26 | Name: "azure_resourcegraph_query_results", 27 | Help: "Azure ResourceGraph query results", 28 | }, 29 | []string{ 30 | "module", 31 | "metric", 32 | }, 33 | ) 34 | prometheus.MustRegister(prometheusQueryResults) 35 | 36 | prometheusQueryRequests = prometheus.NewCounterVec( 37 | prometheus.CounterOpts{ 38 | Name: "azure_resourcegraph_query_requests", 39 | Help: "Azure ResourceGraph query request count", 40 | }, 41 | []string{ 42 | "module", 43 | "metric", 44 | }, 45 | ) 46 | prometheus.MustRegister(prometheusQueryRequests) 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webdevops/azure-resourcegraph-exporter 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 9 | github.com/google/uuid v1.6.0 10 | github.com/jessevdk/go-flags v1.6.1 11 | github.com/patrickmn/go-cache v2.1.0+incompatible 12 | github.com/prometheus/client_golang v1.22.0 13 | github.com/webdevops/go-common v0.0.0-20250501164923-7cab87d11d0f 14 | go.uber.org/zap v1.27.0 15 | ) 16 | 17 | require ( 18 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect 19 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect 20 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 21 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect 22 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 // indirect 23 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect 24 | github.com/KimMachineGun/automemlimit v0.7.1 // indirect 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 27 | github.com/dustin/go-humanize v1.0.1 // indirect 28 | github.com/go-logr/logr v1.4.2 // indirect 29 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 30 | github.com/kylelemons/godebug v1.1.0 // indirect 31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 32 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 33 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 34 | github.com/prometheus/client_model v0.6.2 // indirect 35 | github.com/prometheus/common v0.63.0 // indirect 36 | github.com/prometheus/procfs v0.16.1 // indirect 37 | github.com/remeh/sizedwaitgroup v1.0.0 // indirect 38 | go.uber.org/automaxprocs v1.6.0 // indirect 39 | go.uber.org/multierr v1.11.0 // indirect 40 | go.uber.org/zap/exp v0.3.0 // indirect 41 | golang.org/x/crypto v0.37.0 // indirect 42 | golang.org/x/net v0.39.0 // indirect 43 | golang.org/x/sys v0.32.0 // indirect 44 | golang.org/x/text v0.24.0 // indirect 45 | google.golang.org/protobuf v1.36.6 // indirect 46 | k8s.io/apimachinery v0.33.0 // indirect 47 | k8s.io/klog/v2 v2.130.1 // indirect 48 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 49 | sigs.k8s.io/yaml v1.4.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /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/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= 20 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= 21 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 22 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 23 | github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= 24 | github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 25 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 26 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 28 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 32 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 33 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 34 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 35 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 36 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 37 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 38 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 39 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 41 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 42 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 43 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 45 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 46 | github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= 47 | github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= 48 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 49 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 50 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 51 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 52 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 53 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 54 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 55 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 58 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 59 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 60 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 61 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 62 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 63 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 64 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 67 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 68 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 69 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 70 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 71 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 72 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 73 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 74 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 75 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 76 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 77 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 78 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= 79 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= 80 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 81 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 82 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 83 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 84 | github.com/webdevops/go-common v0.0.0-20250501164923-7cab87d11d0f h1:gbTwG6Cp4tYTFXp5FKxThUGKmd+Hi9qHIfrRy8m7dEI= 85 | github.com/webdevops/go-common v0.0.0-20250501164923-7cab87d11d0f/go.mod h1:GzD/xLtTZ5Vh3aHTi02g0OlfDUoiDx44OHeUnqWO2CI= 86 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 87 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 88 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 89 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 90 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 91 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 92 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 93 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 94 | go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= 95 | go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= 96 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 97 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 98 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 99 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 100 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 102 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 103 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 104 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 105 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 106 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 107 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 108 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 109 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 110 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 111 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 112 | k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 113 | k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 114 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 115 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 116 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 117 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 118 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 119 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 120 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "html/template" 9 | "net/http" 10 | "os" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/google/uuid" 15 | "github.com/jessevdk/go-flags" 16 | cache "github.com/patrickmn/go-cache" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | "github.com/webdevops/go-common/azuresdk/armclient" 19 | "github.com/webdevops/go-common/azuresdk/prometheus/tracing" 20 | "github.com/webdevops/go-common/prometheus/kusto" 21 | 22 | "github.com/webdevops/azure-resourcegraph-exporter/config" 23 | ) 24 | 25 | const ( 26 | Author = "webdevops.io" 27 | 28 | UserAgent = "az-rg-exporter/" 29 | ) 30 | 31 | var ( 32 | argparser *flags.Parser 33 | Opts config.Opts 34 | 35 | Config kusto.Config 36 | 37 | AzureClient *armclient.ArmClient 38 | 39 | metricCache *cache.Cache 40 | 41 | //go:embed templates/*.html 42 | templates embed.FS 43 | 44 | // Git version information 45 | gitCommit = "" 46 | gitTag = "" 47 | ) 48 | 49 | func main() { 50 | initArgparser() 51 | initLogger() 52 | 53 | logger.Infof("starting azure-resourcegraph-exporter v%s (%s; %s; by %v)", gitTag, gitCommit, runtime.Version(), Author) 54 | logger.Info(string(Opts.GetJson())) 55 | initSystem() 56 | initGlobalMetrics() 57 | 58 | metricCache = cache.New(120*time.Second, 60*time.Second) 59 | 60 | logger.Infof("loading config") 61 | readConfig() 62 | 63 | logger.Infof("init Azure") 64 | initAzureConnection() 65 | 66 | logger.Infof("starting http server on %s", Opts.Server.Bind) 67 | startHttpServer() 68 | } 69 | 70 | // init argparser and parse/validate arguments 71 | func initArgparser() { 72 | argparser = flags.NewParser(&Opts, flags.Default) 73 | _, err := argparser.Parse() 74 | 75 | // check if there is an parse error 76 | if err != nil { 77 | var flagsErr *flags.Error 78 | if ok := errors.As(err, &flagsErr); ok && flagsErr.Type == flags.ErrHelp { 79 | os.Exit(0) 80 | } else { 81 | fmt.Println() 82 | argparser.WriteHelp(os.Stdout) 83 | os.Exit(1) 84 | } 85 | } 86 | } 87 | 88 | func readConfig() { 89 | Config = kusto.NewConfig(Opts.Config.Path) 90 | 91 | if err := Config.Validate(); err != nil { 92 | logger.Fatal(err) 93 | } 94 | } 95 | 96 | func initAzureConnection() { 97 | var err error 98 | AzureClient, err = armclient.NewArmClientWithCloudName(*Opts.Azure.Environment, logger) 99 | if err != nil { 100 | logger.Fatal(err.Error()) 101 | } 102 | 103 | AzureClient.SetUserAgent(UserAgent + gitTag) 104 | } 105 | 106 | // start and handle prometheus handler 107 | func startHttpServer() { 108 | mux := http.NewServeMux() 109 | 110 | // healthz 111 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 112 | if _, err := fmt.Fprint(w, "Ok"); err != nil { 113 | logger.Error(err) 114 | } 115 | }) 116 | 117 | // readyz 118 | mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { 119 | if _, err := fmt.Fprint(w, "Ok"); err != nil { 120 | logger.Error(err) 121 | } 122 | }) 123 | 124 | // report 125 | tmpl := template.Must(template.ParseFS(templates, "templates/*.html")) 126 | mux.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) { 127 | cspNonce := base64.StdEncoding.EncodeToString([]byte(uuid.New().String())) 128 | 129 | w.Header().Add("Content-Type", "text/html") 130 | w.Header().Add("Referrer-Policy", "same-origin") 131 | w.Header().Add("X-Frame-Options", "DENY") 132 | w.Header().Add("X-XSS-Protection", "1; mode=block") 133 | w.Header().Add("X-Content-Type-Options", "nosniff") 134 | w.Header().Add("Content-Security-Policy", 135 | fmt.Sprintf( 136 | "default-src 'self'; script-src 'nonce-%[1]s'; style-src 'nonce-%[1]s'; img-src 'self' data:", 137 | cspNonce, 138 | ), 139 | ) 140 | 141 | templatePayload := struct { 142 | Nonce string 143 | }{ 144 | Nonce: cspNonce, 145 | } 146 | 147 | if err := tmpl.ExecuteTemplate(w, "query.html", templatePayload); err != nil { 148 | logger.Error(err) 149 | } 150 | }) 151 | 152 | mux.Handle("/metrics", tracing.RegisterAzureMetricAutoClean(promhttp.Handler())) 153 | 154 | mux.HandleFunc("/probe", handleProbeRequest) 155 | 156 | srv := &http.Server{ 157 | Addr: Opts.Server.Bind, 158 | Handler: mux, 159 | ReadTimeout: Opts.Server.ReadTimeout, 160 | WriteTimeout: Opts.Server.WriteTimeout, 161 | } 162 | logger.Fatal(srv.ListenAndServe()) 163 | } 164 | -------------------------------------------------------------------------------- /probe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/prometheus/client_golang/prometheus/promhttp" 12 | "github.com/webdevops/go-common/prometheus/kusto" 13 | "github.com/webdevops/go-common/utils/to" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | const ( 18 | ResourceGraphQueryOptionsTop = 1000 19 | ) 20 | 21 | func handleProbeRequest(w http.ResponseWriter, r *http.Request) { 22 | registry := prometheus.NewRegistry() 23 | 24 | requestTime := time.Now() 25 | 26 | params := r.URL.Query() 27 | moduleName := params.Get("module") 28 | cacheKey := "cache:" + moduleName 29 | 30 | probeLogger := logger.With(zap.String("module", moduleName)) 31 | 32 | cacheTime := 0 * time.Second 33 | cacheTimeDurationStr := params.Get("cache") 34 | if cacheTimeDurationStr != "" { 35 | if v, err := time.ParseDuration(cacheTimeDurationStr); err == nil { 36 | cacheTime = v 37 | } else { 38 | probeLogger.Errorln(err.Error()) 39 | http.Error(w, err.Error(), http.StatusBadRequest) 40 | return 41 | } 42 | } 43 | 44 | ctx := context.Background() 45 | 46 | defaultSubscriptions := []string{} 47 | if subscriptionList, err := AzureClient.ListCachedSubscriptionsWithFilter(ctx, Opts.Azure.Subscription...); err == nil { 48 | for _, subscription := range subscriptionList { 49 | defaultSubscriptions = append(defaultSubscriptions, to.String(subscription.SubscriptionID)) 50 | } 51 | } else { 52 | probeLogger.Panic(err) 53 | } 54 | 55 | // Create and authorize a ResourceGraph client 56 | resourceGraphClient, err := armresourcegraph.NewClient(AzureClient.GetCred(), AzureClient.NewArmClientOptions()) 57 | if err != nil { 58 | probeLogger.Errorln(err.Error()) 59 | http.Error(w, err.Error(), http.StatusBadRequest) 60 | return 61 | } 62 | 63 | metricList := kusto.MetricList{} 64 | metricList.Init() 65 | 66 | // check if value is cached 67 | executeQuery := true 68 | if cacheTime.Seconds() > 0 { 69 | if v, ok := metricCache.Get(cacheKey); ok { 70 | if cacheData, ok := v.([]byte); ok { 71 | if err := json.Unmarshal(cacheData, &metricList); err == nil { 72 | probeLogger.Debug("fetched from cache") 73 | w.Header().Add("X-metrics-cached", "true") 74 | executeQuery = false 75 | } else { 76 | probeLogger.Debug("unable to parse cache data") 77 | } 78 | } 79 | } 80 | } 81 | 82 | if executeQuery { 83 | w.Header().Add("X-metrics-cached", "false") 84 | for _, queryConfig := range Config.Queries { 85 | // check if query matches module name 86 | if queryConfig.Module != moduleName { 87 | continue 88 | } 89 | startTime := time.Now() 90 | 91 | contextLogger := probeLogger.With(zap.String("metric", queryConfig.Metric)) 92 | contextLogger.Debug("starting query") 93 | 94 | querySubscriptions := []*string{} 95 | if queryConfig.Subscriptions != nil { 96 | for _, val := range *queryConfig.Subscriptions { 97 | subscriptionID := val 98 | querySubscriptions = append(querySubscriptions, &subscriptionID) 99 | } 100 | queryConfig.Subscriptions = &defaultSubscriptions 101 | } else { 102 | for _, val := range defaultSubscriptions { 103 | subscriptionID := val 104 | querySubscriptions = append(querySubscriptions, &subscriptionID) 105 | } 106 | } 107 | 108 | requestQueryTop := int32(ResourceGraphQueryOptionsTop) 109 | requestQuerySkip := int32(0) 110 | 111 | // Set options 112 | resultFormat := armresourcegraph.ResultFormatObjectArray 113 | RequestOptions := armresourcegraph.QueryRequestOptions{ 114 | ResultFormat: &resultFormat, 115 | Top: &requestQueryTop, 116 | Skip: &requestQuerySkip, 117 | } 118 | 119 | query := queryConfig.Query 120 | 121 | // Run the query and get the results 122 | resultTotalRecords := int32(0) 123 | for { 124 | // Create the query request 125 | Request := armresourcegraph.QueryRequest{ 126 | Subscriptions: querySubscriptions, 127 | Query: &query, 128 | Options: &RequestOptions, 129 | } 130 | 131 | prometheusQueryRequests.With(prometheus.Labels{"module": moduleName, "metric": queryConfig.Metric}).Inc() 132 | 133 | var results, queryErr = resourceGraphClient.Resources(ctx, Request, nil) 134 | if results.TotalRecords != nil { 135 | resultTotalRecords = int32(*results.TotalRecords) //nolint:gosec // RequestOptions.Skip is int32 136 | } 137 | 138 | if queryErr == nil { 139 | contextLogger.Debug("parsing result") 140 | 141 | if resultList, ok := results.Data.([]interface{}); ok { 142 | // check if we got data, otherwise break the for loop 143 | if len(resultList) == 0 { 144 | break 145 | } 146 | 147 | for _, v := range resultList { 148 | if resultRow, ok := v.(map[string]interface{}); ok { 149 | for metricName, metric := range kusto.BuildPrometheusMetricList(queryConfig.Metric, *queryConfig.QueryMetric, resultRow) { 150 | metricList.Add(metricName, metric...) 151 | } 152 | } 153 | } 154 | } else { 155 | // got invalid or empty data, skipping 156 | break 157 | } 158 | 159 | contextLogger.Debug("metrics parsed") 160 | } else { 161 | contextLogger.Errorln(queryErr.Error()) 162 | http.Error(w, queryErr.Error(), http.StatusBadRequest) 163 | return 164 | } 165 | 166 | *RequestOptions.Skip += requestQueryTop 167 | if *RequestOptions.Skip >= resultTotalRecords { 168 | break 169 | } 170 | } 171 | 172 | elapsedTime := time.Since(startTime) 173 | contextLogger.With(zap.Int32("results", resultTotalRecords)).Debugf("fetched %v results", resultTotalRecords) 174 | prometheusQueryTime.With(prometheus.Labels{"module": moduleName, "metric": queryConfig.Metric}).Observe(elapsedTime.Seconds()) 175 | prometheusQueryResults.With(prometheus.Labels{"module": moduleName, "metric": queryConfig.Metric}).Set(float64(resultTotalRecords)) 176 | } 177 | 178 | // store to cache (if enabeld) 179 | if cacheTime.Seconds() > 0 { 180 | if cacheData, err := json.Marshal(metricList); err == nil { 181 | w.Header().Add("X-metrics-cached-until", time.Now().Add(cacheTime).Format(time.RFC3339)) 182 | metricCache.Set(cacheKey, cacheData, cacheTime) 183 | probeLogger.Debugf("saved metric to cache for %s minutes", cacheTime.String()) 184 | } 185 | } 186 | } 187 | 188 | probeLogger.Debug("building prometheus metrics") 189 | for _, metricName := range metricList.GetMetricNames() { 190 | metricLabelNames := metricList.GetMetricLabelNames(metricName) 191 | 192 | gaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 193 | Name: metricName, 194 | Help: metricName, 195 | }, metricLabelNames) 196 | registry.MustRegister(gaugeVec) 197 | 198 | for _, metric := range metricList.GetMetricList(metricName) { 199 | for _, labelName := range metricLabelNames { 200 | if _, ok := metric.Labels[labelName]; !ok { 201 | metric.Labels[labelName] = "" 202 | } 203 | } 204 | 205 | if metric.Value != nil { 206 | gaugeVec.With(metric.Labels).Set(*metric.Value) 207 | } 208 | } 209 | } 210 | probeLogger.With(zap.String("duration", time.Since(requestTime).String())).Debug("finished request") 211 | 212 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 213 | h.ServeHTTP(w, r) 214 | } 215 | -------------------------------------------------------------------------------- /templates/query.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 114 | azure-resourcegraph-exporter 115 | 116 | 117 | 118 | 126 | 127 |
128 |
129 |

130 | Query settings 131 |

132 | 133 |
134 | 135 |
136 |

General

137 |
138 | 139 |
140 | 141 |
142 | 146 |
azure-resourcegraph-exporter query endpoint
147 |
148 |
149 | 150 |
151 | 152 |
153 | 154 |
Module (query category)
155 |
156 |
157 | 158 |
159 | 160 |
161 | 162 |
Query cache
163 |
164 |
165 | 166 |
167 |
168 | 169 |
170 |
171 |
172 |
173 | 174 |
175 |
Loading...
176 |

Result

177 | 178 |
179 | 180 |
181 | 182 |
183 |
184 | 185 |
186 | 187 |
188 | 189 |
190 |
191 | 192 |
193 | 194 |
195 | 196 |
197 |
198 |
199 | 200 |
201 |

Prometheus scrape_config

202 | 203 |
204 | 205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 |
213 | 214 | 215 | 216 | 217 | 218 | 353 | 354 | 355 | 356 | --------------------------------------------------------------------------------