├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ ├── cleanup-runs.yaml │ ├── codeql.yml │ ├── container.yml │ └── re-release.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── all_available_metrics_6591_7.29.json ├── all_available_metrics_6690_7.57.json ├── all_available_metrics_7590_7.12.json ├── all_available_metrics_7590_7.20.json ├── all_available_metrics_7590_7.25.json ├── all_available_metrics_7590_7.29.json ├── all_available_metrics_7590_7.50.json ├── all_available_metrics_7590_7.57.json ├── all_available_metrics_7590_7.59.json ├── all_available_metrics_7590_8.00.json ├── docker-compose.yml ├── fritzbox_lua ├── README.md └── lua_client.go ├── fritzbox_upnp └── service.go ├── go.mod ├── go.sum ├── grafana ├── Dashboard.json ├── Dashboard_for_grafana6.json └── README.md ├── k8s-fritzbox.yaml ├── luaTest-many.json ├── luaTest.json ├── main.go ├── metrics-lua.json ├── metrics-lua_cable.json ├── metrics.json ├── renovate.json └── systemd └── fritzbox_exporter.service /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "docker" # See documentation for possible values 6 | directory: "/" # Location of package manifests 7 | schedule: 8 | interval: "daily" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "daily" 14 | - package-ecosystem: 'go' 15 | directory: '/' 16 | schedule: 17 | interval: 'daily' 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "build" 2 | 3 | on: 4 | # the 1st condition 5 | workflow_run: 6 | workflows: ["re-release"] 7 | branches: [master] 8 | types: 9 | - completed 10 | 11 | jobs: 12 | build_linux: 13 | permissions: 14 | contents: write 15 | packages: write 16 | 17 | name: Build Go Binary 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 22 | goos: [linux] 23 | goarch: [amd64, arm, arm64] 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: wangyoucao577/go-release-action@v1 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | goos: ${{ matrix.goos }} 30 | goarch: ${{ matrix.goarch }} 31 | release_tag: latest 32 | overwrite: true 33 | extra_files: LICENSE README.md metrics.json metrics-lua.json 34 | -------------------------------------------------------------------------------- /.github/workflows/cleanup-runs.yaml: -------------------------------------------------------------------------------- 1 | name: Cleanup Runs 2 | on: [push] 3 | 4 | jobs: 5 | del_runs: 6 | permissions: 7 | actions: write 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Delete workflow runs 11 | uses: Mattraks/delete-workflow-runs@v2 12 | with: 13 | token: ${{ github.token }} 14 | repository: ${{ github.repository }} 15 | retain_days: 7 16 | keep_minimum_runs: 4 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '42 3 * * 6' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: go 47 | build-mode: autobuild 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.github/workflows/container.yml: -------------------------------------------------------------------------------- 1 | name: Container 2 | 3 | on: 4 | push: 5 | # Publish `main` as Docker `latest` image. 6 | branches: 7 | - main 8 | - master 9 | 10 | # Publish `v1.2.3` tags as releases. 11 | tags: 12 | - '**' # All tags kick off a new container build Save history ad 5.0.x etc 13 | 14 | # Run tests for any PRs. 15 | pull_request: 16 | 17 | env: 18 | BUILD_PLATFORM: | 19 | linux/arm/v6 20 | linux/arm/v7 21 | linux/arm64 22 | linux/amd64 23 | # Enable Docker Buildkit 24 | DOCKER_BUILDKIT: 1 25 | IMAGE_NAME: fritzbox_exporter 26 | 27 | jobs: 28 | lint: 29 | runs-on: ubuntu-latest 30 | if: github.event_name == 'push' 31 | 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Lint Dockerfile 37 | uses: hadolint/hadolint-action@v3.0.0 38 | with: 39 | dockerfile: Dockerfile 40 | 41 | prepare: 42 | runs-on: ubuntu-latest 43 | if: github.event_name == 'push' 44 | needs: lint 45 | # Map a step output to a job output 46 | outputs: 47 | DOCKER_REPOSITORY: ${{ steps.tag_image.outputs.DOCKER_REPOSITORY }} 48 | DOCKER_TAG: ${{ steps.tag_image.outputs.DOCKER_TAG }} 49 | 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | 54 | - name: Tag Image 55 | id: tag_image 56 | run: | 57 | DOCKER_REPOSITORY=ghcr.io/${{ github.repository }} 58 | 59 | # Change all uppercase to lowercase 60 | DOCKER_REPOSITORY=$(echo $DOCKER_REPOSITORY | tr '[A-Z]' '[a-z]') 61 | 62 | DOCKER_TAG=${{ github.ref_name }} 63 | 64 | # Use Docker `latest` tag convention 65 | [ "$DOCKER_TAG" == "master" ] && DOCKER_TAG=latest 66 | [ "$DOCKER_TAG" == "main" ] && DOCKER_TAG=latest 67 | 68 | echo DOCKER_REPOSITORY=$DOCKER_REPOSITORY 69 | echo DOCKER_TAG=$DOCKER_TAG 70 | echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_OUTPUT 71 | echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_OUTPUT 72 | 73 | # Build and push image to GitHub Packages. 74 | # See also https://docs.docker.com/docker-hub/builds/ 75 | build: 76 | runs-on: ubuntu-latest 77 | if: github.event_name == 'push' 78 | needs: [prepare] 79 | 80 | steps: 81 | - name: Checkout 82 | uses: actions/checkout@v4 83 | 84 | - name: Set up QEMU 85 | uses: docker/setup-qemu-action@v3 86 | 87 | - name: Set up Docker Buildx 88 | id: buildx 89 | uses: docker/setup-buildx-action@v3 90 | 91 | - name: Build Image 92 | uses: docker/build-push-action@v5 93 | with: 94 | build-args: REPO=${{ github.repository }} 95 | context: . 96 | cache-from: type=gha,scope=build-${{ github.sha }} 97 | cache-to: type=gha,mode=max,scope=build-${{ github.sha }} 98 | file: Dockerfile 99 | platforms: ${{ env.BUILD_PLATFORM }} 100 | push: false 101 | 102 | # Build and push image to GitHub Packages. 103 | # See also https://docs.docker.com/docker-hub/builds/ 104 | push: 105 | runs-on: ubuntu-latest 106 | if: github.event_name == 'push' 107 | needs: [prepare, build] 108 | 109 | steps: 110 | - name: Checkout 111 | uses: actions/checkout@v4 112 | 113 | - name: Set up QEMU 114 | uses: docker/setup-qemu-action@v3 115 | 116 | - name: Set up Docker Buildx 117 | id: buildx 118 | uses: docker/setup-buildx-action@v3 119 | 120 | - name: Log into registry 121 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 122 | 123 | - name: Build Runtime Image and Push 124 | uses: docker/build-push-action@v5 125 | with: 126 | build-args: REPO=${{ github.repository }} 127 | context: . 128 | cache-from: type=gha 129 | cache-to: type=gha,mode=max 130 | file: Dockerfile 131 | platforms: ${{ env.BUILD_PLATFORM }} 132 | push: true 133 | tags: | 134 | ${{ needs.prepare.outputs.DOCKER_REPOSITORY }}:${{ needs.prepare.outputs.DOCKER_TAG }} 135 | ${{ needs.prepare.outputs.DOCKER_REPOSITORY }}:${{ github.sha }} 136 | target: runtime-image 137 | 138 | - name: Inspect image 139 | if: success() 140 | run: | 141 | docker buildx imagetools inspect ${{ needs.prepare.outputs.DOCKER_REPOSITORY }}:${{ needs.prepare.outputs.DOCKER_TAG }} 142 | docker buildx imagetools inspect ${{ needs.prepare.outputs.DOCKER_REPOSITORY }}:${{ github.sha }} 143 | 144 | test: 145 | runs-on: ubuntu-latest 146 | if: github.event_name == 'push' 147 | needs: [prepare, push] 148 | 149 | steps: 150 | - name: Checkout 151 | uses: actions/checkout@v4 152 | 153 | - name: Run Trivy vulnerability scanner 154 | uses: aquasecurity/trivy-action@master 155 | with: 156 | image-ref: "${{ needs.prepare.outputs.DOCKER_REPOSITORY }}:${{ github.sha }}" 157 | format: 'sarif' 158 | output: 'trivy-results.sarif' 159 | ignore-unfixed: true 160 | vuln-type: 'os,library' 161 | severity: 'MEDIUM,HIGH,CRITICAL' 162 | 163 | - name: Upload Trivy scan results to GitHub Security tab 164 | uses: github/codeql-action/upload-sarif@v3 165 | if: always() 166 | with: 167 | sarif_file: 'trivy-results.sarif' 168 | -------------------------------------------------------------------------------- /.github/workflows/re-release.yaml: -------------------------------------------------------------------------------- 1 | name: "re-release" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | re-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Delete 18 | uses: ame-yu/action-delete-latest-release@v2 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - name: Release 23 | uses: softprops/action-gh-release@v1 24 | with: 25 | name: latest 26 | tag_name: latest 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | fritzbox_exporter 2 | .project 3 | .env 4 | vendor/* 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build Image 4 | FROM golang:1.23-alpine3.21 AS builder 5 | RUN go install github.com/sberk42/fritzbox_exporter@latest \ 6 | && mkdir /app \ 7 | && mv /go/bin/fritzbox_exporter /app 8 | 9 | WORKDIR /app 10 | 11 | COPY metrics.json metrics-lua.json /app/ 12 | 13 | # Runtime Image 14 | FROM alpine:3.21 as runtime-image 15 | 16 | ARG REPO=sberk42/fritzbox_exporter 17 | 18 | LABEL org.opencontainers.image.source https://github.com/${REPO} 19 | 20 | ENV USERNAME username 21 | ENV PASSWORD password 22 | ENV GATEWAY_URL http://fritz.box:49000 23 | ENV GATEWAY_LUAURL http://fritz.box 24 | ENV LISTEN_ADDRESS 0.0.0.0:9042 25 | 26 | RUN mkdir /app \ 27 | && addgroup -S -g 1000 fritzbox \ 28 | && adduser -S -u 1000 -G fritzbox fritzbox \ 29 | && chown -R fritzbox:fritzbox /app 30 | 31 | WORKDIR /app 32 | 33 | COPY --chown=fritzbox:fritzbox --from=builder /app /app 34 | 35 | EXPOSE 9042 36 | 37 | ENTRYPOINT [ "sh", "-c", "/app/fritzbox_exporter" ] 38 | CMD [ "-username", "${USERNAME}", "-password", "${PASSWORD}", "-gateway-url", "${GATEWAY_URL}", "-gateway-luaurl", "${GATEWAY_LUAURL}", "-listen-address", "${LISTEN_ADDRESS}" ] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fritz!Box Upnp statistics exporter for prometheus 2 | 3 | This exporter exports some variables from an 4 | [AVM Fritzbox](http://avm.de/produkte/fritzbox/) 5 | to prometheus. 6 | 7 | This exporter is tested with a Fritzbox 7590 software version 07.12, 07.20, 07.21, 07.25, 07.29, 07.50, 07.57 , 07.59 and 08.00. 8 | 9 | The goal of the fork is: 10 | - [x] allow passing of username / password using evironment variable 11 | - [x] use https instead of http for communitcation with fritz.box 12 | - [x] move config of metrics to be exported to config file rather then code 13 | - [x] add config for additional metrics to collect (especially from TR-064 API) 14 | - [x] create a grafana dashboard consuming the additional metrics 15 | - [x] collect metrics from lua APIs not available in UPNP APIs 16 | 17 | Other changes: 18 | - replaced digest authentication code with own implementation 19 | - improved error messages 20 | - test mode prints details about all SOAP Actions and their parameters 21 | - collect option to directly test collection of results 22 | - additional metrics to collect details about connected hosts and DECT devices 23 | - support to use results like hostname or MAC address as labels to metrics 24 | - support for metrics from lua APIs (e.g. CPU temperature, utilization, ...) 25 | 26 | 27 | ## Building 28 | 29 | go install github.com/sberk42/fritzbox_exporter@latest 30 | 31 | ## Running 32 | 33 | Create a new user account for the exporter on the Fritzbox using the login credentials: 34 | ```bash 35 | USERNAME=your_fritzbox_username 36 | PASSWORD=your_fritzbox_password 37 | ``` 38 | Grant this user access to the following features: 39 | FRITZ!Box settings, voice messages, fax messages, FRITZ!App Fon and call list, 40 | Smart Home, access to NAS content, and VPN. 41 | 42 | In the configuration of the Fritzbox the option "Statusinformationen über UPnP übertragen" in the dialog "Heimnetz > 43 | Heimnetzübersicht > Netzwerkeinstellungen" has to be enabled. 44 | 45 | ### Using docker 46 | 47 | The image is available as package using: 48 | `docker pull ghcr.io/sberk42/fritzbox_exporter/fritzbox_exporter:latest` 49 | or you can build the container yourself: `docker build --tag fritzbox-prometheus-exporter:latest .` 50 | 51 | Then start the container: 52 | 53 | ```bash 54 | $ docker run -e 'USERNAME=your_fritzbox_username' \ 55 | -e 'PASSWORD=your_fritzbox_password' \ 56 | -e 'GATEWAY_URL="http://192.168.0.1:49000"' \ 57 | -e 'LISTEN_ADDRESS="0.0.0.0:9042"' \ 58 | fritzbox-prometheus-exporter:latest 59 | ``` 60 | 61 | I've you're getting `no such host` issues, define your FritzBox as DNS server for your docker container like this: 62 | 63 | ```bash 64 | $ docker run --dns YOUR_FRITZBOX_IP \ 65 | -e 'USERNAME=your_fritzbox_username' \ 66 | -e 'PASSWORD=your_fritzbox_password' \ 67 | -e 'GATEWAY_URL="http://192.168.0.1:49000"' \ 68 | -e 'LISTEN_ADDRESS="0.0.0.0:9042"' \ 69 | fritzbox-prometheus-exporter:latest 70 | ``` 71 | 72 | ### Using docker-compose 73 | 74 | Set your environment variables within the [docker-compose.yml](docker-compose.yml) file. 75 | 76 | Then start up the container using `docker-compose up -d`. 77 | 78 | ### Using the binary 79 | 80 | Usage: 81 | 82 | $GOPATH/bin/fritzbox_exporter -h 83 | Usage of ./fritzbox_exporter: 84 | -gateway-url string 85 | The URL of the FRITZ!Box (default "http://fritz.box:49000") 86 | -gateway-luaurl string 87 | The URL of the FRITZ!Box UI (default "http://fritz.box") 88 | -metrics-file string 89 | The JSON file with the metric definitions. (default "metrics.json") 90 | -lua-metrics-file string 91 | The JSON file with the lua metric definitions. (default "metrics-lua.json") 92 | -test 93 | print all available SOAP calls and their results (if call possible) to stdout 94 | -json-out string 95 | store metrics also to JSON file when running test 96 | -testLua 97 | read luaTest.json file make all contained calls and dump results 98 | -collect 99 | collect metrics once print to stdout and exit 100 | -nolua 101 | disable collecting lua metrics 102 | -username string 103 | The user for the FRITZ!Box UPnP service 104 | -password string 105 | The password for the FRITZ!Box UPnP service 106 | -listen-address string 107 | The address to listen on for HTTP requests. (default "127.0.0.1:9042") 108 | 109 | The password (needed for metrics from TR-064 API) can be passed over environment variables to test in shell: 110 | read -rs PASSWORD && export PASSWORD && ./fritzbox_exporter -username -test; unset PASSWORD 111 | 112 | ## Exported metrics 113 | 114 | start exporter and run 115 | curl -s http://127.0.0.1:9042/metrics 116 | 117 | ## Output of -test 118 | 119 | The exporter prints all available Variables to stdout when called with the -test option. 120 | These values are determined by parsing all services from http://fritz.box:49000/igddesc.xml and http://fritzbox:49000/tr64desc.xml (for TR64 username and password is needed!!!) 121 | 122 | ## Customizing metrics 123 | 124 | The metrics to collect are no longer hard coded, but have been moved to the [metrics.json](metrics.json) and [metrics-lua.json](metrics-lua.json) files, so just adjust to your needs (for cable version also see [metrics-lua_cable.json](metrics-lua_cable.json)). 125 | For a list of all the available metrics just execute the exporter with -test (username and password are needed for the TR-064 API!) 126 | For lua metrics open UI in browser and check the json files used for the various screens. 127 | 128 | For a list of all available metrics, see the dumps below (the format is the same as in the metrics.json file, so it can be used to easily add further metrics to retrieve): 129 | - [FritzBox 6591 v7.29](all_available_metrics_6591_7.29.json) 130 | - [FritzBox 6690 v7.57](all_available_metrics_6690_7.57.json) 131 | - [FritzBox 7590 v7.12](all_available_metrics_7590_7.12.json) 132 | - [FritzBox 7590 v7.20](all_available_metrics_7590_7.20.json) 133 | - [FritzBox 7590 v7.25](all_available_metrics_7590_7.25.json) 134 | - [FritzBox 7590 v7.29](all_available_metrics_7590_7.29.json) 135 | - [FritzBox 7590 v7.50](all_available_metrics_7590_7.50.json) 136 | - [FritzBox 7590 v7.57](all_available_metrics_7590_7.57.json) 137 | - [FritzBox 7590 v7.59](all_available_metrics_7590_7.59.json) - same as 7.57 138 | - [FritzBox 7590 v8.00](all_available_metrics_7590_8.00.json) 139 | ## Grafana Dashboard 140 | 141 | The dashboard is now also published on [Grafana](https://grafana.com/grafana/dashboards/12579). 142 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fritzbox-prometheus-exporter: 4 | hostname: fritzbox-prometheus-exporter 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: fritzbox-prometheus-exporter 9 | # for dns issues like "dial tcp: lookup fritz.box on 127.0.0.11:53: no such host" 10 | # uncomment and fill the following line: 11 | # dns: YOUR_FRITZBOX_IP 12 | ports: 13 | - "9042:9042" 14 | #expose: 15 | # - "9042" 16 | restart: unless-stopped 17 | environment: 18 | USERNAME: your_fritzbox_username 19 | PASSWORD: your_fritzbox_password 20 | GATEWAY_URL: http://192.168.0.1:49000 21 | LISTEN_ADDRESS: 0.0.0.0:9042 22 | -------------------------------------------------------------------------------- /fritzbox_lua/README.md: -------------------------------------------------------------------------------- 1 | # Client for LUA API of FRITZ!Box UI 2 | 3 | **Note:** This client only support calls that return JSON (some seem to return HTML they are not supported) 4 | 5 | There does not seem to be a complete documentation of the API, the authentication and getting a sid (Session ID) is described here: 6 | [https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID.pdf] 7 | 8 | ## Details 9 | Most of the calls seem to be using the data.lua url with a http FORM POST request. As parameters the page and session id are required (e.g.: sid=&page=engery). The result is JSON with the data needed to create the respective UI. 10 | Some calls (like inetstat_monitor.lua) seem to use GET rather than POST, the client also supports them, but prefix GET: is needed, otherwise a post is done. 11 | 12 | Since no public documentation for the JSON format of the various pages seem to exist, you need to observe the calls made by the UI and analyse the JSON result. However the client should be generic enough to get metric and label values from all kind of nested hash and array structures contained in the JSONs. 13 | 14 | ## Compatibility 15 | The client was developed on a Fritzbox 7590 running on 07.21, other models or versions may behave differently so just test and see what works, but again the generic part of the client should still work as long as there is a JSON result. 16 | 17 | ## Translations 18 | Since the API is used to drive the UI, labels are translated and will be returned in the language configured in the Fritzbox. There seems to be a lang parameter but it looks like it is simply ignored. Having translated labels is annoying, therefore the clients also support renaming them based on regex. 19 | Currently the regex are defined for: 20 | - German 21 | 22 | If your Fritzbox is running in another language you need to adjust them or you will receive different labels, that may not work with dashboards using them for filtering! 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /fritzbox_lua/lua_client.go: -------------------------------------------------------------------------------- 1 | // Package lua_client implementes client for fritzbox lua UI API 2 | package lua_client 3 | 4 | // Copyright 2020 Andreas Krebs 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import ( 19 | "bytes" 20 | "crypto/md5" 21 | "crypto/sha256" 22 | "encoding/hex" 23 | "encoding/json" 24 | "encoding/xml" 25 | "errors" 26 | "fmt" 27 | "io" 28 | "net/http" 29 | "net/url" 30 | "regexp" 31 | "strconv" 32 | "strings" 33 | 34 | "github.com/sirupsen/logrus" 35 | "golang.org/x/crypto/pbkdf2" 36 | "golang.org/x/text/encoding/unicode" 37 | "golang.org/x/text/transform" 38 | ) 39 | 40 | // SessionInfo XML from login_sid.lua 41 | type SessionInfo struct { 42 | SID string `xml:"SID"` 43 | Challenge string `xml:"Challenge"` 44 | BlockTime int `xml:"BlockTime"` 45 | Rights string `xml:"Rights"` 46 | } 47 | 48 | // LuaSession for storing connection data and SID 49 | type LuaSession struct { 50 | BaseURL string 51 | Username string 52 | Password string 53 | SID string 54 | ApiVer string 55 | Client http.Client 56 | SessionInfo SessionInfo 57 | } 58 | 59 | // LuaPage identified by path and params 60 | type LuaPage struct { 61 | Path string 62 | Params string 63 | } 64 | 65 | // LuaMetricValueDefinition definition for a single metric 66 | type LuaMetricValueDefinition struct { 67 | Path string 68 | Key string 69 | OkValue string 70 | Labels []string 71 | } 72 | 73 | // LuaMetricValue single value retrieved from lua page 74 | type LuaMetricValue struct { 75 | Name string 76 | Value float64 77 | Labels map[string]string 78 | } 79 | 80 | // LabelRename regex to replace labels to get rid of translations 81 | type LabelRename struct { 82 | Pattern regexp.Regexp 83 | Name string 84 | } 85 | 86 | // regex to remove leading/trailing characters from numbers 87 | var ( 88 | regexNonNumberEnd = regexp.MustCompile(`\D+$`) 89 | ) 90 | 91 | func (lua *LuaSession) v2Login(response string) error { 92 | logrus.Debugln("using LoginApi v2") 93 | res := url.Values{} 94 | res.Set("username", lua.Username) 95 | res.Set("response", response) 96 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/login_sid.lua?version=2", lua.BaseURL), strings.NewReader(res.Encode())) 97 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 98 | if err != nil { 99 | return fmt.Errorf("error forming request: %s", err.Error()) 100 | } 101 | 102 | return lua.doLogin(req) 103 | } 104 | 105 | func (lua *LuaSession) v1Login(response string) error { 106 | logrus.Debugln("using LoginApi v1") 107 | urlParams := fmt.Sprintf("?response=%s&user=%s", response, lua.Username) 108 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/login_sid.lua%s", lua.BaseURL, urlParams), nil) 109 | if err != nil { 110 | return fmt.Errorf("error forming request: %s", err.Error()) 111 | } 112 | 113 | return lua.doLogin(req) 114 | } 115 | 116 | func (lua *LuaSession) doLogin(req *http.Request) error { 117 | resp, err := lua.Client.Do(req) 118 | if err != nil { 119 | return fmt.Errorf("error calling login_sid.lua: %s", err.Error()) 120 | } 121 | 122 | defer resp.Body.Close() 123 | dec := xml.NewDecoder(resp.Body) 124 | 125 | err = dec.Decode(&lua.SessionInfo) 126 | if err != nil { 127 | return fmt.Errorf("error decoding SessionInfo: %s", err.Error()) 128 | } 129 | 130 | if lua.SessionInfo.BlockTime > 0 { 131 | return fmt.Errorf("too many failed logins, login blocked for %d seconds", lua.SessionInfo.BlockTime) 132 | } 133 | return nil 134 | } 135 | 136 | func (lua *LuaSession) initLogin() error { 137 | var version string 138 | switch lua.ApiVer { 139 | case "v1": 140 | version = "" 141 | case "v2": 142 | version = "?version=2" 143 | } 144 | 145 | resp, err := http.Get(fmt.Sprintf("%s/login_sid.lua%s", lua.BaseURL, version)) 146 | if err != nil { 147 | return fmt.Errorf("error calling login_sid.lua: %s", err.Error()) 148 | } 149 | defer resp.Body.Close() 150 | dec := xml.NewDecoder(resp.Body) 151 | 152 | err = dec.Decode(&lua.SessionInfo) 153 | if err != nil { 154 | return fmt.Errorf("error decoding SessionInfo: %s", err.Error()) 155 | } 156 | 157 | if lua.SessionInfo.BlockTime > 0 { 158 | return fmt.Errorf("too many failed logins, login blocked for %d seconds", lua.SessionInfo.BlockTime) 159 | } 160 | return nil 161 | } 162 | 163 | func (lmvDef *LuaMetricValueDefinition) createValue(name string, value float64) LuaMetricValue { 164 | lmv := LuaMetricValue{ 165 | Name: name, 166 | Value: value, 167 | Labels: make(map[string]string), 168 | } 169 | 170 | return lmv 171 | } 172 | 173 | // Login perform loing and get SID 174 | func (lua *LuaSession) Login() error { 175 | err := lua.initLogin() 176 | if err != nil { 177 | return err 178 | } 179 | 180 | challenge := lua.SessionInfo.Challenge 181 | if lua.SessionInfo.SID == "0000000000000000" && challenge != "" { 182 | switch lua.ApiVer { 183 | case "v1": 184 | // no SID, but challenge so calc response 185 | hash := utf16leMd5(fmt.Sprintf("%s-%s", challenge, lua.Password)) 186 | response := fmt.Sprintf("%s-%x", challenge, hash) 187 | err := lua.v1Login(response) 188 | if err != nil { 189 | return err 190 | } 191 | case "v2": 192 | response := calculatePbkdf2Response(challenge, lua.Password) 193 | err := lua.v2Login(response) 194 | if err != nil { 195 | return err 196 | } 197 | } 198 | } 199 | 200 | sid := lua.SessionInfo.SID 201 | if sid == "0000000000000000" || sid == "" { 202 | return errors.New("LUA login failed - no SID received - check username and password") 203 | } 204 | 205 | lua.SID = sid 206 | 207 | return nil 208 | } 209 | 210 | // LoadData load a lua bage and return content 211 | func (lua *LuaSession) LoadData(page LuaPage) ([]byte, error) { 212 | method := "POST" 213 | path := page.Path 214 | 215 | // handle method prefix 216 | pathParts := strings.SplitN(path, ":", 2) 217 | if len(pathParts) > 1 { 218 | method = pathParts[0] 219 | path = pathParts[1] 220 | } 221 | 222 | dataURL := fmt.Sprintf("%s/%s", lua.BaseURL, path) 223 | 224 | callDone := false 225 | var resp *http.Response 226 | var err error 227 | retries := 0 228 | for !callDone { 229 | // perform login if no SID or previous call failed with (403) 230 | if lua.SID == "" || resp != nil { 231 | err = lua.Login() 232 | callDone = true // consider call done, since we tried login 233 | 234 | if err != nil { 235 | return nil, err 236 | } 237 | } 238 | 239 | // send by UI for data.lua: xhr=1&sid=xxxxxxx&lang=de&page=energy&xhrId=all&no_sidrenew= 240 | // but SID and page seem to be enough 241 | params := "sid=" + lua.SID 242 | if page.Params != "" { 243 | params += "&" + page.Params 244 | } 245 | 246 | if method == "POST" { 247 | resp, err = http.Post(dataURL, "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(params))) 248 | } else if method == "GET" { 249 | resp, err = http.Get(dataURL + "?" + params) 250 | } else { 251 | err = fmt.Errorf("method %s is unsupported in path %s", method, page.Path) 252 | } 253 | 254 | if err != nil { 255 | return nil, err 256 | } 257 | defer resp.Body.Close() 258 | 259 | if resp.StatusCode == http.StatusOK { 260 | callDone = true 261 | } else if resp.StatusCode == http.StatusForbidden && !callDone { 262 | // we assume SID is expired, so retry login 263 | } else if retries < 1 { 264 | // unexpected error let's retry (reboot issue ?) 265 | } else { 266 | return nil, fmt.Errorf("%s failed: %s", page.Path, resp.Status) 267 | } 268 | 269 | retries++ 270 | } 271 | 272 | body, err := io.ReadAll(resp.Body) 273 | 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | return body, nil 279 | } 280 | 281 | // ParseJSON generic parser for unmarshalling into map 282 | func ParseJSON(jsonData []byte) (map[string]interface{}, error) { 283 | var data map[string]interface{} 284 | 285 | // Unmarshal or Decode the JSON to the interface. 286 | json.Unmarshal(jsonData, &data) 287 | 288 | return data, nil 289 | } 290 | 291 | func getRenamedLabel(labelRenames *[]LabelRename, label string) string { 292 | if labelRenames != nil { 293 | for _, lblRen := range *labelRenames { 294 | if lblRen.Pattern.MatchString(label) { 295 | return lblRen.Name 296 | } 297 | } 298 | } 299 | 300 | return label 301 | } 302 | 303 | func getValueFromHashOrArray(mapOrArray interface{}, key string, path string) (interface{}, error) { 304 | var value interface{} 305 | 306 | switch moa := mapOrArray.(type) { 307 | case map[string]interface{}: 308 | var exists bool 309 | value, exists = moa[key] 310 | if !exists { 311 | return nil, fmt.Errorf("hash '%s' has no element '%s'", path, key) 312 | } 313 | case []interface{}: 314 | // since type is array there can't be any labels to differentiate values, so only one value supported ! 315 | index, err := strconv.Atoi(key) 316 | if err != nil { 317 | return nil, fmt.Errorf("item '%s' is an array, but index '%s' is not a number", path, key) 318 | } 319 | 320 | if index < 0 { 321 | // this is an index from the end of the values 322 | index += len(moa) 323 | } 324 | 325 | if index < 0 || index >= len(moa) { 326 | return nil, fmt.Errorf("index %d is invalid for array '%s' with length %d", index, path, len(moa)) 327 | } 328 | value = moa[index] 329 | default: 330 | return nil, fmt.Errorf("item '%s' is not a hash or array, can't get value %s", path, key) 331 | } 332 | 333 | return value, nil 334 | } 335 | 336 | // GetMetrics get metrics from parsed lua page for definition and rename labels 337 | func GetMetrics(labelRenames *[]LabelRename, data map[string]interface{}, metricDef LuaMetricValueDefinition) ([]LuaMetricValue, error) { 338 | 339 | var values []interface{} 340 | var err error 341 | if metricDef.Path != "" { 342 | pathItems := strings.Split(metricDef.Path, ".") 343 | values, err = _getValues(data, pathItems, "") 344 | if err != nil { 345 | return nil, err 346 | } 347 | } else { 348 | values = make([]interface{}, 1) 349 | values[0] = data 350 | } 351 | 352 | metrics := make([]LuaMetricValue, 0) 353 | keyItems := strings.Split(metricDef.Key, ".") 354 | 355 | VALUE: 356 | for _, pathVal := range values { 357 | valUntyped := pathVal 358 | path := metricDef.Path 359 | 360 | // now handle if key is also splitted 361 | for _, key := range keyItems { 362 | valUntyped, err = getValueFromHashOrArray(valUntyped, key, path) 363 | if err != nil { 364 | // since we may have other values, we simply continue (should we report it?) 365 | continue VALUE 366 | } 367 | 368 | if path != "" { 369 | path += "." 370 | } 371 | path += key 372 | } 373 | 374 | var sVal = toString(valUntyped) 375 | var floatVal float64 376 | if metricDef.OkValue != "" { 377 | if metricDef.OkValue == sVal { 378 | floatVal = 1 379 | } else { 380 | floatVal = 0 381 | } 382 | } else { 383 | // convert value to float, but first remove all non numbers from begin or end of value 384 | // needed if value contains unit 385 | sNum := regexNonNumberEnd.ReplaceAllString(sVal, "") 386 | 387 | floatVal, err = strconv.ParseFloat(sNum, 64) 388 | if err != nil { 389 | continue VALUE 390 | } 391 | } 392 | 393 | // create metric value 394 | lmv := metricDef.createValue(path, floatVal) 395 | 396 | // add labels if pathVal is a hash 397 | valMap, isType := pathVal.(map[string]interface{}) 398 | if isType { 399 | for _, l := range metricDef.Labels { 400 | lv, exists := valMap[l] 401 | if exists { 402 | lmv.Labels[l] = getRenamedLabel(labelRenames, toString(lv)) 403 | } 404 | } 405 | } 406 | 407 | metrics = append(metrics, lmv) 408 | } 409 | 410 | if len(metrics) == 0 { 411 | if err == nil { 412 | // normal we should already have an error, this is just a fallback 413 | err = fmt.Errorf("no value found for item '%s' with key '%s'", metricDef.Path, metricDef.Key) 414 | } 415 | return nil, err 416 | } 417 | 418 | return metrics, nil 419 | } 420 | 421 | // from https://stackoverflow.com/questions/33710672/golang-encode-string-utf16-little-endian-and-hash-with-md5 422 | func utf16leMd5(s string) []byte { 423 | enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() 424 | hasher := md5.New() 425 | t := transform.NewWriter(hasher, enc) 426 | t.Write([]byte(s)) 427 | return hasher.Sum(nil) 428 | } 429 | 430 | // v2 authentication according to https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf 431 | func calculatePbkdf2Response(challenge, password string) string { 432 | challengeParts := strings.Split(challenge, "$") 433 | iter1, _ := strconv.Atoi(challengeParts[1]) 434 | iter2, _ := strconv.Atoi(challengeParts[3]) 435 | salt1, _ := hex.DecodeString(challengeParts[2]) 436 | salt2, _ := hex.DecodeString(challengeParts[4]) 437 | hash1_raw := pbkdf2.Key([]byte(password), salt1, iter1, 32, sha256.New) 438 | hash2_raw := pbkdf2.Key(hash1_raw, salt2, iter2, 32, sha256.New) 439 | hash2 := hex.EncodeToString(hash2_raw) 440 | raw := fmt.Sprintf("%s$%s", challengeParts[4], hash2) 441 | return raw 442 | } 443 | 444 | // helper for retrieving values from parsed JSON 445 | func _getValues(data interface{}, pathItems []string, parentPath string) ([]interface{}, error) { 446 | 447 | var err error 448 | values := make([]interface{}, 0) 449 | value := data 450 | curPath := parentPath 451 | 452 | for i, p := range pathItems { 453 | if p == "*" { 454 | // handle * case to get all values 455 | var subvals []interface{} 456 | switch vv := value.(type) { 457 | case []interface{}: 458 | for index, u := range vv { 459 | subvals, err = _getValues(u, pathItems[i+1:], fmt.Sprintf("%s.%d", curPath, index)) 460 | 461 | if subvals != nil { 462 | values = append(values, subvals...) 463 | } 464 | } 465 | case map[string]interface{}: 466 | for subK, subV := range vv { 467 | subvals, err = _getValues(subV, pathItems[i+1:], fmt.Sprintf("%s.%s", curPath, subK)) 468 | 469 | if subvals != nil { 470 | values = append(values, subvals...) 471 | } 472 | } 473 | default: 474 | err = fmt.Errorf("item '%s' is neither a hash or array", curPath) 475 | } 476 | 477 | if len(values) == 0 { 478 | if err == nil { 479 | err = fmt.Errorf("item '%s.*' has no values", curPath) 480 | } 481 | 482 | return nil, err 483 | } 484 | 485 | return values, nil 486 | } 487 | 488 | // this is a single value 489 | value, err = getValueFromHashOrArray(value, p, curPath) 490 | if err != nil { 491 | return nil, err 492 | } 493 | 494 | if curPath == "" { 495 | curPath = p 496 | } else { 497 | curPath += "." + p 498 | } 499 | } 500 | 501 | values = append(values, value) 502 | 503 | return values, nil 504 | } 505 | 506 | func toString(value interface{}) string { 507 | // should we better check or simple convert everything ???? 508 | return fmt.Sprintf("%v", value) 509 | } 510 | -------------------------------------------------------------------------------- /fritzbox_upnp/service.go: -------------------------------------------------------------------------------- 1 | // Package fritzbox_upnp Query UPNP variables from Fritz!Box devices. 2 | package fritzbox_upnp 3 | 4 | // Copyright 2016 Nils Decker 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import ( 19 | "bytes" 20 | "crypto/md5" 21 | "crypto/rand" 22 | "crypto/tls" 23 | "encoding/xml" 24 | "errors" 25 | "fmt" 26 | "io" 27 | "net/http" 28 | "strconv" 29 | "strings" 30 | ) 31 | 32 | // curl http://fritz.box:49000/igddesc.xml 33 | // curl http://fritz.box:49000/any.xml 34 | // curl http://fritz.box:49000/igdconnSCPD.xml 35 | // curl http://fritz.box:49000/igdicfgSCPD.xml 36 | // curl http://fritz.box:49000/igddslSCPD.xml 37 | // curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml 38 | 39 | const textXML = `text/xml; charset="utf-8"` 40 | 41 | var errInvalidSOAPResponse = errors.New("invalid SOAP response") 42 | 43 | // Root of the UPNP tree 44 | type Root struct { 45 | BaseURL string 46 | Username string 47 | Password string 48 | Device Device `xml:"device"` 49 | Services map[string]*Service // Map of all services indexed by .ServiceType 50 | } 51 | 52 | // Device an UPNP device 53 | type Device struct { 54 | root *Root 55 | 56 | DeviceType string `xml:"deviceType"` 57 | FriendlyName string `xml:"friendlyName"` 58 | Manufacturer string `xml:"manufacturer"` 59 | ManufacturerURL string `xml:"manufacturerURL"` 60 | ModelDescription string `xml:"modelDescription"` 61 | ModelName string `xml:"modelName"` 62 | ModelNumber string `xml:"modelNumber"` 63 | ModelURL string `xml:"modelURL"` 64 | UDN string `xml:"UDN"` 65 | 66 | Services []*Service `xml:"serviceList>service"` // Service of the device 67 | Devices []*Device `xml:"deviceList>device"` // Sub-Devices of the device 68 | 69 | PresentationURL string `xml:"presentationURL"` 70 | } 71 | 72 | // Service an UPNP Service 73 | type Service struct { 74 | Device *Device 75 | 76 | ServiceType string `xml:"serviceType"` 77 | ServiceID string `xml:"serviceId"` 78 | ControlURL string `xml:"controlURL"` 79 | EventSubURL string `xml:"eventSubURL"` 80 | SCPDUrl string `xml:"SCPDURL"` 81 | 82 | Actions map[string]*Action // All actions available on the service 83 | StateVariables []*StateVariable // All state variables available on the service 84 | } 85 | 86 | type scpdRoot struct { 87 | Actions []*Action `xml:"actionList>action"` 88 | StateVariables []*StateVariable `xml:"serviceStateTable>stateVariable"` 89 | } 90 | 91 | // Action an UPNP action on a service 92 | type Action struct { 93 | service *Service 94 | 95 | Name string `xml:"name"` 96 | Arguments []*Argument `xml:"argumentList>argument"` 97 | ArgumentMap map[string]*Argument // Map of arguments indexed by .Name 98 | } 99 | 100 | // ActionArgument an Inüut Argument to pass to an action 101 | type ActionArgument struct { 102 | Name string 103 | Value interface{} 104 | } 105 | 106 | // SoapEnvelope struct to unmarshal SOAP faults 107 | type SoapEnvelope struct { 108 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 109 | Body SoapBody 110 | } 111 | 112 | // SoapBody struct 113 | type SoapBody struct { 114 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` 115 | Fault SoapFault 116 | } 117 | 118 | // SoapFault struct 119 | type SoapFault struct { 120 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` 121 | FaultCode string `xml:"faultcode"` 122 | FaultString string `xml:"faultstring"` 123 | Detail FaultDetail `xml:"detail"` 124 | } 125 | 126 | // FaultDetail struct 127 | type FaultDetail struct { 128 | UpnpError UpnpError `xml:"UPnPError"` 129 | } 130 | 131 | // UpnpError struct 132 | type UpnpError struct { 133 | ErrorCode int `xml:"errorCode"` 134 | ErrorDescription string `xml:"errorDescription"` 135 | } 136 | 137 | // IsGetOnly Returns if the action seems to be a query for information. 138 | // This is determined by checking if the action has no input arguments and at least one output argument. 139 | func (a *Action) IsGetOnly() bool { 140 | for _, a := range a.Arguments { 141 | if a.Direction == "in" { 142 | return false 143 | } 144 | } 145 | return len(a.Arguments) > 0 146 | } 147 | 148 | // An Argument to an action 149 | type Argument struct { 150 | Name string `xml:"name"` 151 | Direction string `xml:"direction"` 152 | RelatedStateVariable string `xml:"relatedStateVariable"` 153 | StateVariable *StateVariable 154 | } 155 | 156 | // StateVariable a state variable that can be manipulated through actions 157 | type StateVariable struct { 158 | Name string `xml:"name"` 159 | DataType string `xml:"dataType"` 160 | DefaultValue string `xml:"defaultValue"` 161 | } 162 | 163 | // Result The result of a Call() contains all output arguments of the call. 164 | // The map is indexed by the name of the state variable. 165 | // The type of the value is string, uint64 or bool depending of the DataType of the variable. 166 | type Result map[string]interface{} 167 | 168 | // load the whole tree 169 | func (r *Root) load() error { 170 | igddesc, err := http.Get( 171 | fmt.Sprintf("%s/igddesc.xml", r.BaseURL), 172 | ) 173 | 174 | if err != nil { 175 | return err 176 | } 177 | 178 | defer igddesc.Body.Close() 179 | 180 | dec := xml.NewDecoder(igddesc.Body) 181 | 182 | err = dec.Decode(r) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | r.Services = make(map[string]*Service) 188 | return r.Device.fillServices(r) 189 | } 190 | 191 | func (r *Root) loadTr64() error { 192 | igddesc, err := http.Get( 193 | fmt.Sprintf("%s/tr64desc.xml", r.BaseURL), 194 | ) 195 | 196 | if err != nil { 197 | return err 198 | } 199 | 200 | defer igddesc.Body.Close() 201 | 202 | dec := xml.NewDecoder(igddesc.Body) 203 | 204 | err = dec.Decode(r) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | r.Services = make(map[string]*Service) 210 | return r.Device.fillServices(r) 211 | } 212 | 213 | // load all service descriptions 214 | func (d *Device) fillServices(r *Root) error { 215 | d.root = r 216 | 217 | for _, s := range d.Services { 218 | s.Device = d 219 | 220 | response, err := http.Get(r.BaseURL + s.SCPDUrl) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | defer response.Body.Close() 226 | 227 | var scpd scpdRoot 228 | 229 | dec := xml.NewDecoder(response.Body) 230 | err = dec.Decode(&scpd) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | s.Actions = make(map[string]*Action) 236 | for _, a := range scpd.Actions { 237 | s.Actions[a.Name] = a 238 | } 239 | s.StateVariables = scpd.StateVariables 240 | 241 | for _, a := range s.Actions { 242 | a.service = s 243 | a.ArgumentMap = make(map[string]*Argument) 244 | 245 | for _, arg := range a.Arguments { 246 | for _, svar := range s.StateVariables { 247 | if arg.RelatedStateVariable == svar.Name { 248 | arg.StateVariable = svar 249 | } 250 | } 251 | 252 | a.ArgumentMap[arg.Name] = arg 253 | } 254 | } 255 | 256 | r.Services[s.ServiceType] = s 257 | } 258 | for _, d2 := range d.Devices { 259 | err := d2.fillServices(r) 260 | if err != nil { 261 | return err 262 | } 263 | } 264 | return nil 265 | } 266 | 267 | const soapActionXML = `` + 268 | `` + 269 | `%s` + 270 | `` 271 | 272 | const soapActionParamXML = `<%s>%s` 273 | 274 | func (a *Action) createCallHTTPRequest(actionArg *ActionArgument) (*http.Request, error) { 275 | argsString := "" 276 | if actionArg != nil { 277 | var buf bytes.Buffer 278 | sValue := fmt.Sprintf("%v", actionArg.Value) 279 | xml.EscapeText(&buf, []byte(sValue)) 280 | argsString += fmt.Sprintf(soapActionParamXML, actionArg.Name, buf.String(), actionArg.Name) 281 | } 282 | bodystr := fmt.Sprintf(soapActionXML, a.Name, a.service.ServiceType, argsString, a.Name, a.service.ServiceType) 283 | 284 | url := a.service.Device.root.BaseURL + a.service.ControlURL 285 | body := strings.NewReader(bodystr) 286 | 287 | req, err := http.NewRequest("POST", url, body) 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) 293 | 294 | req.Header.Set("Content-Type", textXML) 295 | req.Header.Set("SOAPAction", action) 296 | 297 | return req, nil 298 | } 299 | 300 | // store auth header for reuse 301 | var authHeader = "" 302 | 303 | // Call an action with argument if given 304 | func (a *Action) Call(actionArg *ActionArgument) (Result, error) { 305 | req, err := a.createCallHTTPRequest(actionArg) 306 | 307 | if err != nil { 308 | return nil, err 309 | } 310 | 311 | // reuse prior authHeader, to avoid unnecessary authentication 312 | if authHeader != "" { 313 | req.Header.Set("Authorization", authHeader) 314 | } 315 | 316 | // first try call without auth header 317 | resp, err := http.DefaultClient.Do(req) 318 | 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | wwwAuth := resp.Header.Get("WWW-Authenticate") 324 | if resp.StatusCode == http.StatusUnauthorized { 325 | resp.Body.Close() // close now, since we make a new request below or fail 326 | 327 | if wwwAuth != "" && a.service.Device.root.Username != "" && a.service.Device.root.Password != "" { 328 | // call failed, but we have a password so calculate header and try again 329 | authHeader, err = a.getDigestAuthHeader(wwwAuth, a.service.Device.root.Username, a.service.Device.root.Password) 330 | if err != nil { 331 | return nil, fmt.Errorf("%s: %s", a.Name, err.Error()) 332 | } 333 | 334 | req, err = a.createCallHTTPRequest(actionArg) 335 | if err != nil { 336 | return nil, fmt.Errorf("%s: %s", a.Name, err.Error()) 337 | } 338 | 339 | req.Header.Set("Authorization", authHeader) 340 | 341 | resp, err = http.DefaultClient.Do(req) 342 | 343 | if err != nil { 344 | return nil, fmt.Errorf("%s: %s", a.Name, err.Error()) 345 | } 346 | 347 | } else { 348 | return nil, fmt.Errorf("%s: Unauthorized, but no username and password given", a.Name) 349 | } 350 | } 351 | 352 | defer resp.Body.Close() 353 | 354 | if resp.StatusCode != http.StatusOK { 355 | errMsg := fmt.Sprintf("%s (%d)", http.StatusText(resp.StatusCode), resp.StatusCode) 356 | if resp.StatusCode == 500 { 357 | buf := new(strings.Builder) 358 | io.Copy(buf, resp.Body) 359 | body := buf.String() 360 | //fmt.Println(body) 361 | 362 | var soapEnv SoapEnvelope 363 | err := xml.Unmarshal([]byte(body), &soapEnv) 364 | if err != nil { 365 | errMsg = fmt.Sprintf("error decoding SOAPFault: %s", err.Error()) 366 | } else { 367 | soapFault := soapEnv.Body.Fault 368 | 369 | if soapFault.FaultString == "UPnPError" { 370 | upe := soapFault.Detail.UpnpError 371 | 372 | errMsg = fmt.Sprintf("SAOPFault: %s %d (%s)", soapFault.FaultString, upe.ErrorCode, upe.ErrorDescription) 373 | } else { 374 | errMsg = fmt.Sprintf("SAOPFault: %s", soapFault.FaultString) 375 | } 376 | } 377 | } 378 | return nil, fmt.Errorf("%s: %s", a.Name, errMsg) 379 | } 380 | 381 | return a.parseSoapResponse(resp.Body) 382 | } 383 | 384 | func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password string) (string, error) { 385 | // parse www-auth header 386 | if !strings.HasPrefix(wwwAuth, "Digest ") { 387 | return "", fmt.Errorf("WWW-Authentication header is not Digest: '%s'", wwwAuth) 388 | } 389 | 390 | s := wwwAuth[7:] 391 | d := map[string]string{} 392 | for _, kv := range strings.Split(s, ",") { 393 | parts := strings.SplitN(kv, "=", 2) 394 | if len(parts) != 2 { 395 | continue 396 | } 397 | d[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ") 398 | } 399 | 400 | if d["algorithm"] == "" { 401 | d["algorithm"] = "MD5" 402 | } else if d["algorithm"] != "MD5" { 403 | return "", fmt.Errorf("digest algorithm not supported: %s != MD5", d["algorithm"]) 404 | } 405 | 406 | if d["qop"] != "auth" { 407 | return "", fmt.Errorf("digest qop not supported: %s != auth", d["qop"]) 408 | } 409 | 410 | // calc h1 and h2 411 | ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username+":"+d["realm"]+":"+password))) 412 | 413 | ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:"+a.service.ControlURL))) 414 | 415 | cn := make([]byte, 8) 416 | rand.Read(cn) 417 | cnonce := fmt.Sprintf("%x", cn) 418 | 419 | nCounter := 1 420 | nc := fmt.Sprintf("%08x", nCounter) 421 | 422 | ds := strings.Join([]string{ha1, d["nonce"], nc, cnonce, d["qop"], ha2}, ":") 423 | response := fmt.Sprintf("%x", md5.Sum([]byte(ds))) 424 | 425 | authHeader := fmt.Sprintf("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", cnonce=\"%s\", nc=%s, qop=%s, response=\"%s\", algorithm=%s", 426 | username, d["realm"], d["nonce"], a.service.ControlURL, cnonce, nc, d["qop"], response, d["algorithm"]) 427 | 428 | return authHeader, nil 429 | } 430 | 431 | func (a *Action) parseSoapResponse(r io.Reader) (Result, error) { 432 | res := make(Result) 433 | dec := xml.NewDecoder(r) 434 | 435 | for { 436 | t, err := dec.Token() 437 | if err == io.EOF { 438 | return res, nil 439 | } 440 | 441 | if err != nil { 442 | return nil, err 443 | } 444 | 445 | if se, ok := t.(xml.StartElement); ok { 446 | arg, ok := a.ArgumentMap[se.Name.Local] 447 | 448 | if ok { 449 | t2, err := dec.Token() 450 | if err != nil { 451 | return nil, err 452 | } 453 | 454 | var val string 455 | switch element := t2.(type) { 456 | case xml.EndElement: 457 | val = "" 458 | case xml.CharData: 459 | val = string(element) 460 | default: 461 | return nil, errInvalidSOAPResponse 462 | } 463 | 464 | converted, err := convertResult(val, arg) 465 | if err != nil { 466 | return nil, err 467 | } 468 | res[arg.StateVariable.Name] = converted 469 | } 470 | } 471 | 472 | } 473 | } 474 | 475 | func convertResult(val string, arg *Argument) (interface{}, error) { 476 | switch arg.StateVariable.DataType { 477 | case "string": 478 | return val, nil 479 | case "boolean": 480 | return bool(val == "1"), nil 481 | 482 | case "ui1", "ui2", "ui4": 483 | // type ui4 can contain values greater than 2^32! 484 | res, err := strconv.ParseUint(val, 10, 64) 485 | if err != nil { 486 | return nil, err 487 | } 488 | return uint64(res), nil 489 | case "i4": 490 | res, err := strconv.ParseInt(val, 10, 64) 491 | if err != nil { 492 | return nil, err 493 | } 494 | return int64(res), nil 495 | case "dateTime", "uuid": 496 | // data types we don't convert yet 497 | return val, nil 498 | default: 499 | return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val) 500 | } 501 | } 502 | 503 | // LoadServices loads the services tree from an device. 504 | func LoadServices(baseurl string, username string, password string, verifyTls bool) (*Root, error) { 505 | 506 | if !verifyTls && strings.HasPrefix(baseurl, "https://") { 507 | // disable certificate validation, since fritz.box uses self signed cert 508 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 509 | } 510 | 511 | var root = &Root{ 512 | BaseURL: baseurl, 513 | Username: username, 514 | Password: password, 515 | } 516 | 517 | err := root.load() 518 | if err != nil { 519 | return nil, err 520 | } 521 | 522 | var rootTr64 = &Root{ 523 | BaseURL: baseurl, 524 | Username: username, 525 | Password: password, 526 | } 527 | 528 | err = rootTr64.loadTr64() 529 | if err != nil { 530 | return nil, err 531 | } 532 | 533 | for k, v := range rootTr64.Services { 534 | root.Services[k] = v 535 | } 536 | 537 | return root, nil 538 | } 539 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sberk42/fritzbox_exporter 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/namsral/flag v1.7.4-pre 7 | github.com/prometheus/client_golang v1.20.5 8 | github.com/sirupsen/logrus v1.9.3 9 | golang.org/x/crypto v0.35.0 10 | golang.org/x/text v0.22.0 11 | ) 12 | 13 | require ( 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 16 | github.com/klauspost/compress v1.17.11 // indirect 17 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 18 | github.com/prometheus/client_model v0.6.1 // indirect 19 | github.com/prometheus/common v0.62.0 // indirect 20 | github.com/prometheus/procfs v0.15.1 // indirect 21 | golang.org/x/sys v0.30.0 // indirect 22 | google.golang.org/protobuf v1.36.3 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 11 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 12 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 13 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 14 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 15 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 16 | github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= 17 | github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 21 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 22 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 23 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 24 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 25 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 26 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 27 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 28 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 29 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 33 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 34 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 35 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 36 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 38 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 39 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 40 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 41 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 42 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 46 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | -------------------------------------------------------------------------------- /grafana/Dashboard_for_grafana6.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "panel", 15 | "id": "gauge", 16 | "name": "Gauge", 17 | "version": "" 18 | }, 19 | { 20 | "type": "grafana", 21 | "id": "grafana", 22 | "name": "Grafana", 23 | "version": "6.7.4" 24 | }, 25 | { 26 | "type": "panel", 27 | "id": "graph", 28 | "name": "Graph", 29 | "version": "" 30 | }, 31 | { 32 | "type": "datasource", 33 | "id": "prometheus", 34 | "name": "Prometheus", 35 | "version": "1.0.0" 36 | }, 37 | { 38 | "type": "panel", 39 | "id": "singlestat", 40 | "name": "Singlestat", 41 | "version": "" 42 | }, 43 | { 44 | "type": "panel", 45 | "id": "table", 46 | "name": "Table", 47 | "version": "" 48 | } 49 | ], 50 | "annotations": { 51 | "list": [ 52 | { 53 | "$$hashKey": "object:41", 54 | "builtIn": 1, 55 | "datasource": "-- Grafana --", 56 | "enable": true, 57 | "hide": true, 58 | "iconColor": "rgba(0, 211, 255, 1)", 59 | "name": "Annotations & Alerts", 60 | "type": "dashboard" 61 | } 62 | ] 63 | }, 64 | "description": "Monitor FRITZ!Box routers.", 65 | "editable": true, 66 | "gnetId": 713, 67 | "graphTooltip": 2, 68 | "id": null, 69 | "links": [], 70 | "panels": [ 71 | { 72 | "cacheTimeout": null, 73 | "colorBackground": true, 74 | "colorValue": false, 75 | "colors": [ 76 | "rgba(245, 54, 54, 0.9)", 77 | "rgba(237, 129, 40, 0.89)", 78 | "#1f78c1" 79 | ], 80 | "datasource": "${DS_PROMETHEUS}", 81 | "format": "none", 82 | "gauge": { 83 | "maxValue": 100, 84 | "minValue": 0, 85 | "show": false, 86 | "thresholdLabels": false, 87 | "thresholdMarkers": true 88 | }, 89 | "id": 10, 90 | "interval": null, 91 | "links": [], 92 | "mappingType": 1, 93 | "mappingTypes": [ 94 | { 95 | "name": "value to text", 96 | "value": 1 97 | }, 98 | { 99 | "name": "range to text", 100 | "value": 2 101 | } 102 | ], 103 | "maxDataPoints": 100, 104 | "nullPointMode": "connected", 105 | "nullText": null, 106 | "postfix": "", 107 | "postfixFontSize": "50%", 108 | "prefix": "", 109 | "prefixFontSize": "50%", 110 | "rangeMaps": [ 111 | { 112 | "from": "null", 113 | "text": "N/A", 114 | "to": "null" 115 | } 116 | ], 117 | "sparkline": { 118 | "fillColor": "rgba(31, 118, 189, 0.18)", 119 | "full": false, 120 | "lineColor": "rgb(31, 120, 193)", 121 | "show": false 122 | }, 123 | "tableColumn": "", 124 | "targets": [ 125 | { 126 | "dsType": "prometheus", 127 | "expr": "gateway_wan_connection_status", 128 | "groupBy": [ 129 | { 130 | "params": [ 131 | "$__interval" 132 | ], 133 | "type": "time" 134 | } 135 | ], 136 | "interval": "", 137 | "legendFormat": "", 138 | "measurement": "fritzbox_value", 139 | "orderByTime": "ASC", 140 | "policy": "default", 141 | "refId": "A", 142 | "resultFormat": "time_series", 143 | "select": [ 144 | [ 145 | { 146 | "params": [ 147 | "value" 148 | ], 149 | "type": "field" 150 | }, 151 | { 152 | "params": [], 153 | "type": "last" 154 | } 155 | ] 156 | ], 157 | "tags": [ 158 | { 159 | "key": "type_instance", 160 | "operator": "=", 161 | "value": "constatus" 162 | } 163 | ], 164 | "target": "" 165 | } 166 | ], 167 | "thresholds": "1,1", 168 | "title": "WAN Connection Status", 169 | "type": "singlestat", 170 | "valueFontSize": "80%", 171 | "valueMaps": [ 172 | { 173 | "op": "=", 174 | "text": "N/A", 175 | "value": "null" 176 | }, 177 | { 178 | "op": "=", 179 | "text": "Disconnected", 180 | "value": "0" 181 | }, 182 | { 183 | "op": "=", 184 | "text": "Connected", 185 | "value": "1" 186 | } 187 | ], 188 | "valueName": "current" 189 | }, 190 | { 191 | "cacheTimeout": null, 192 | "colorBackground": true, 193 | "colorValue": false, 194 | "colors": [ 195 | "rgba(245, 54, 54, 0.9)", 196 | "rgba(237, 129, 40, 0.89)", 197 | "#1f78c1" 198 | ], 199 | "datasource": "${DS_PROMETHEUS}", 200 | "format": "none", 201 | "gauge": { 202 | "maxValue": 100, 203 | "minValue": 0, 204 | "show": false, 205 | "thresholdLabels": false, 206 | "thresholdMarkers": true 207 | }, 208 | "gridPos": { 209 | "h": 3, 210 | "w": 6, 211 | "x": 6, 212 | "y": 0 213 | }, 214 | "id": 9, 215 | "interval": null, 216 | "links": [], 217 | "mappingType": 1, 218 | "mappingTypes": [ 219 | { 220 | "name": "value to text", 221 | "value": 1 222 | }, 223 | { 224 | "name": "range to text", 225 | "value": 2 226 | } 227 | ], 228 | "maxDataPoints": 100, 229 | "nullPointMode": "connected", 230 | "nullText": null, 231 | "postfix": "", 232 | "postfixFontSize": "50%", 233 | "prefix": "", 234 | "prefixFontSize": "50%", 235 | "rangeMaps": [ 236 | { 237 | "from": "null", 238 | "text": "N/A", 239 | "to": "null" 240 | } 241 | ], 242 | "sparkline": { 243 | "fillColor": "rgba(31, 118, 189, 0.18)", 244 | "full": false, 245 | "lineColor": "rgb(31, 120, 193)", 246 | "show": false 247 | }, 248 | "tableColumn": "", 249 | "targets": [ 250 | { 251 | "dsType": "prometheus", 252 | "expr": "gateway_wan_layer1_link_status", 253 | "groupBy": [ 254 | { 255 | "params": [ 256 | "$__interval" 257 | ], 258 | "type": "time" 259 | } 260 | ], 261 | "interval": "", 262 | "legendFormat": "", 263 | "measurement": "fritzbox_value", 264 | "orderByTime": "ASC", 265 | "policy": "default", 266 | "refId": "A", 267 | "resultFormat": "time_series", 268 | "select": [ 269 | [ 270 | { 271 | "params": [ 272 | "value" 273 | ], 274 | "type": "field" 275 | }, 276 | { 277 | "params": [], 278 | "type": "last" 279 | } 280 | ] 281 | ], 282 | "tags": [ 283 | { 284 | "key": "type_instance", 285 | "operator": "=", 286 | "value": "dslstatus" 287 | } 288 | ], 289 | "target": "" 290 | } 291 | ], 292 | "thresholds": "1,1", 293 | "title": "DSL Link Status", 294 | "type": "singlestat", 295 | "valueFontSize": "80%", 296 | "valueMaps": [ 297 | { 298 | "op": "=", 299 | "text": "N/A", 300 | "value": "null" 301 | }, 302 | { 303 | "op": "=", 304 | "text": "Disconnected", 305 | "value": "0" 306 | }, 307 | { 308 | "op": "=", 309 | "text": "Connected", 310 | "value": "1" 311 | } 312 | ], 313 | "valueName": "current" 314 | }, 315 | { 316 | "cacheTimeout": null, 317 | "datasource": "${DS_PROMETHEUS}", 318 | "gridPos": { 319 | "h": 6, 320 | "w": 6, 321 | "x": 12, 322 | "y": 0 323 | }, 324 | "id": 11, 325 | "links": [], 326 | "options": { 327 | "fieldOptions": { 328 | "calcs": [ 329 | "lastNotNull" 330 | ], 331 | "defaults": { 332 | "color": { 333 | "mode": "thresholds" 334 | }, 335 | "decimals": 1, 336 | "mappings": [ 337 | { 338 | "id": 0, 339 | "op": "=", 340 | "text": "N/A", 341 | "type": 1, 342 | "value": "null" 343 | } 344 | ], 345 | "max": 116000000, 346 | "min": 0, 347 | "nullValueMode": "connected", 348 | "thresholds": { 349 | "mode": "absolute", 350 | "steps": [ 351 | { 352 | "color": "green", 353 | "value": null 354 | }, 355 | { 356 | "color": "rgba(237, 129, 40, 0.89)", 357 | "value": 50000000 358 | }, 359 | { 360 | "color": "red", 361 | "value": 100000000 362 | } 363 | ] 364 | }, 365 | "unit": "bps" 366 | }, 367 | "overrides": [], 368 | "values": false 369 | }, 370 | "orientation": "horizontal", 371 | "showThresholdLabels": false, 372 | "showThresholdMarkers": true 373 | }, 374 | "pluginVersion": "6.7.4", 375 | "targets": [ 376 | { 377 | "dsType": "prometheus", 378 | "expr": "gateway_wan_bytes_receive_rate", 379 | "groupBy": [ 380 | { 381 | "params": [ 382 | "$__interval" 383 | ], 384 | "type": "time" 385 | } 386 | ], 387 | "interval": "", 388 | "legendFormat": "", 389 | "measurement": "fritzbox_value", 390 | "orderByTime": "ASC", 391 | "policy": "default", 392 | "refId": "A", 393 | "resultFormat": "time_series", 394 | "select": [ 395 | [ 396 | { 397 | "params": [ 398 | "value" 399 | ], 400 | "type": "field" 401 | }, 402 | { 403 | "params": [], 404 | "type": "last" 405 | } 406 | ] 407 | ], 408 | "tags": [ 409 | { 410 | "key": "type_instance", 411 | "operator": "=", 412 | "value": "receiverate" 413 | } 414 | ], 415 | "target": "" 416 | } 417 | ], 418 | "title": "Current Download", 419 | "type": "gauge" 420 | }, 421 | { 422 | "cacheTimeout": null, 423 | "datasource": "${DS_PROMETHEUS}", 424 | "gridPos": { 425 | "h": 6, 426 | "w": 6, 427 | "x": 18, 428 | "y": 0 429 | }, 430 | "id": 12, 431 | "links": [], 432 | "options": { 433 | "fieldOptions": { 434 | "calcs": [ 435 | "lastNotNull" 436 | ], 437 | "defaults": { 438 | "color": { 439 | "mode": "thresholds" 440 | }, 441 | "decimals": 1, 442 | "mappings": [ 443 | { 444 | "id": 0, 445 | "op": "=", 446 | "text": "N/A", 447 | "type": 1, 448 | "value": "null" 449 | } 450 | ], 451 | "max": 40000000, 452 | "min": 0, 453 | "nullValueMode": "connected", 454 | "thresholds": { 455 | "mode": "absolute", 456 | "steps": [ 457 | { 458 | "color": "blue", 459 | "value": null 460 | }, 461 | { 462 | "color": "green", 463 | "value": 5000000 464 | }, 465 | { 466 | "color": "rgba(237, 129, 40, 0.89)", 467 | "value": 10000000 468 | }, 469 | { 470 | "color": "#e24d42", 471 | "value": 30000000 472 | } 473 | ] 474 | }, 475 | "unit": "bps" 476 | }, 477 | "overrides": [], 478 | "values": false 479 | }, 480 | "orientation": "horizontal", 481 | "showThresholdLabels": false, 482 | "showThresholdMarkers": true 483 | }, 484 | "pluginVersion": "6.7.4", 485 | "targets": [ 486 | { 487 | "dsType": "prometheus", 488 | "expr": "gateway_wan_bytes_send_rate", 489 | "groupBy": [ 490 | { 491 | "params": [ 492 | "$__interval" 493 | ], 494 | "type": "time" 495 | } 496 | ], 497 | "interval": "", 498 | "legendFormat": "", 499 | "measurement": "fritzbox_value", 500 | "orderByTime": "ASC", 501 | "policy": "default", 502 | "refId": "A", 503 | "resultFormat": "time_series", 504 | "select": [ 505 | [ 506 | { 507 | "params": [ 508 | "value" 509 | ], 510 | "type": "field" 511 | }, 512 | { 513 | "params": [], 514 | "type": "last" 515 | } 516 | ] 517 | ], 518 | "tags": [ 519 | { 520 | "key": "type_instance", 521 | "operator": "=", 522 | "value": "sendrate" 523 | } 524 | ], 525 | "target": "" 526 | } 527 | ], 528 | "title": "Current Upload", 529 | "type": "gauge" 530 | }, 531 | { 532 | "cacheTimeout": null, 533 | "colorBackground": false, 534 | "colorValue": false, 535 | "colors": [ 536 | "rgba(245, 54, 54, 0.9)", 537 | "rgba(237, 129, 40, 0.89)", 538 | "rgba(50, 172, 45, 0.97)" 539 | ], 540 | "datasource": "${DS_PROMETHEUS}", 541 | "decimals": 1, 542 | "format": "dtdurations", 543 | "gauge": { 544 | "maxValue": 100, 545 | "minValue": 0, 546 | "show": false, 547 | "thresholdLabels": false, 548 | "thresholdMarkers": true 549 | }, 550 | "gridPos": { 551 | "h": 3, 552 | "w": 6, 553 | "x": 0, 554 | "y": 3 555 | }, 556 | "id": 21, 557 | "interval": null, 558 | "links": [], 559 | "mappingType": 1, 560 | "mappingTypes": [ 561 | { 562 | "name": "value to text", 563 | "value": 1 564 | }, 565 | { 566 | "name": "range to text", 567 | "value": 2 568 | } 569 | ], 570 | "maxDataPoints": 100, 571 | "nullPointMode": "connected", 572 | "nullText": null, 573 | "postfix": "", 574 | "postfixFontSize": "50%", 575 | "prefix": "", 576 | "prefixFontSize": "50%", 577 | "rangeMaps": [ 578 | { 579 | "from": "null", 580 | "text": "N/A", 581 | "to": "null" 582 | } 583 | ], 584 | "sparkline": { 585 | "fillColor": "rgba(31, 118, 189, 0.18)", 586 | "full": false, 587 | "lineColor": "rgb(31, 120, 193)", 588 | "show": false 589 | }, 590 | "tableColumn": "", 591 | "targets": [ 592 | { 593 | "dsType": "prometheus", 594 | "expr": "gateway_uptime_seconds", 595 | "groupBy": [ 596 | { 597 | "params": [ 598 | "$__interval" 599 | ], 600 | "type": "time" 601 | } 602 | ], 603 | "interval": "", 604 | "legendFormat": "", 605 | "measurement": "fritzbox_value", 606 | "orderByTime": "ASC", 607 | "policy": "default", 608 | "refId": "A", 609 | "resultFormat": "time_series", 610 | "select": [ 611 | [ 612 | { 613 | "params": [ 614 | "value" 615 | ], 616 | "type": "field" 617 | }, 618 | { 619 | "params": [], 620 | "type": "last" 621 | } 622 | ] 623 | ], 624 | "tags": [ 625 | { 626 | "key": "type", 627 | "operator": "=", 628 | "value": "uptime" 629 | } 630 | ], 631 | "target": "" 632 | } 633 | ], 634 | "thresholds": "1,1", 635 | "title": "Fritzbox Uptime", 636 | "type": "singlestat", 637 | "valueFontSize": "80%", 638 | "valueMaps": [ 639 | { 640 | "op": "=", 641 | "text": "N/A", 642 | "value": "null" 643 | }, 644 | { 645 | "op": "=", 646 | "text": "Disconnected", 647 | "value": "0" 648 | }, 649 | { 650 | "op": "=", 651 | "text": "Connected", 652 | "value": "1" 653 | } 654 | ], 655 | "valueName": "current" 656 | }, 657 | { 658 | "cacheTimeout": null, 659 | "colorBackground": false, 660 | "colorValue": false, 661 | "colors": [ 662 | "rgba(245, 54, 54, 0.9)", 663 | "rgba(237, 129, 40, 0.89)", 664 | "rgba(50, 172, 45, 0.97)" 665 | ], 666 | "datasource": "${DS_PROMETHEUS}", 667 | "decimals": 1, 668 | "format": "dtdurations", 669 | "gauge": { 670 | "maxValue": 100, 671 | "minValue": 0, 672 | "show": false, 673 | "thresholdLabels": false, 674 | "thresholdMarkers": true 675 | }, 676 | "gridPos": { 677 | "h": 3, 678 | "w": 6, 679 | "x": 6, 680 | "y": 3 681 | }, 682 | "id": 13, 683 | "interval": null, 684 | "links": [], 685 | "mappingType": 1, 686 | "mappingTypes": [ 687 | { 688 | "name": "value to text", 689 | "value": 1 690 | }, 691 | { 692 | "name": "range to text", 693 | "value": 2 694 | } 695 | ], 696 | "maxDataPoints": 100, 697 | "nullPointMode": "connected", 698 | "nullText": null, 699 | "postfix": "", 700 | "postfixFontSize": "50%", 701 | "prefix": "", 702 | "prefixFontSize": "50%", 703 | "rangeMaps": [ 704 | { 705 | "from": "null", 706 | "text": "N/A", 707 | "to": "null" 708 | } 709 | ], 710 | "sparkline": { 711 | "fillColor": "rgba(31, 118, 189, 0.18)", 712 | "full": false, 713 | "lineColor": "rgb(31, 120, 193)", 714 | "show": false 715 | }, 716 | "tableColumn": "", 717 | "targets": [ 718 | { 719 | "dsType": "prometheus", 720 | "expr": "gateway_wan_connection_uptime_seconds", 721 | "groupBy": [ 722 | { 723 | "params": [ 724 | "$__interval" 725 | ], 726 | "type": "time" 727 | } 728 | ], 729 | "interval": "", 730 | "legendFormat": "", 731 | "measurement": "fritzbox_value", 732 | "orderByTime": "ASC", 733 | "policy": "default", 734 | "refId": "A", 735 | "resultFormat": "time_series", 736 | "select": [ 737 | [ 738 | { 739 | "params": [ 740 | "value" 741 | ], 742 | "type": "field" 743 | }, 744 | { 745 | "params": [], 746 | "type": "last" 747 | } 748 | ] 749 | ], 750 | "tags": [ 751 | { 752 | "key": "type_instance", 753 | "operator": "=", 754 | "value": "uptime" 755 | } 756 | ], 757 | "target": "" 758 | } 759 | ], 760 | "thresholds": "1,1", 761 | "title": "Connection Uptime", 762 | "type": "singlestat", 763 | "valueFontSize": "80%", 764 | "valueMaps": [ 765 | { 766 | "op": "=", 767 | "text": "N/A", 768 | "value": "null" 769 | } 770 | ], 771 | "valueName": "current" 772 | }, 773 | { 774 | "cacheTimeout": null, 775 | "colorBackground": false, 776 | "colorValue": false, 777 | "colors": [ 778 | "rgba(245, 54, 54, 0.9)", 779 | "rgba(237, 129, 40, 0.89)", 780 | "rgba(50, 172, 45, 0.97)" 781 | ], 782 | "datasource": "${DS_PROMETHEUS}", 783 | "decimals": 1, 784 | "format": "decbytes", 785 | "gauge": { 786 | "maxValue": 100, 787 | "minValue": 0, 788 | "show": false, 789 | "thresholdLabels": false, 790 | "thresholdMarkers": true 791 | }, 792 | "gridPos": { 793 | "h": 4, 794 | "w": 6, 795 | "x": 0, 796 | "y": 6 797 | }, 798 | "id": 3, 799 | "interval": null, 800 | "links": [], 801 | "mappingType": 1, 802 | "mappingTypes": [ 803 | { 804 | "name": "value to text", 805 | "value": 1 806 | }, 807 | { 808 | "name": "range to text", 809 | "value": 2 810 | } 811 | ], 812 | "maxDataPoints": 100, 813 | "nullPointMode": "connected", 814 | "nullText": null, 815 | "postfix": "", 816 | "postfixFontSize": "50%", 817 | "prefix": "", 818 | "prefixFontSize": "50%", 819 | "rangeMaps": [ 820 | { 821 | "from": "null", 822 | "text": "N/A", 823 | "to": "null" 824 | } 825 | ], 826 | "sparkline": { 827 | "fillColor": "rgba(31, 118, 189, 0.18)", 828 | "full": false, 829 | "lineColor": "rgb(31, 120, 193)", 830 | "show": true 831 | }, 832 | "tableColumn": "", 833 | "targets": [ 834 | { 835 | "dsType": "prometheus", 836 | "expr": "gateway_wan_bytes_received", 837 | "groupBy": [], 838 | "interval": "", 839 | "legendFormat": "", 840 | "measurement": "fritzbox_value", 841 | "orderByTime": "ASC", 842 | "policy": "default", 843 | "query": "SELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter GROUP BY time($__interval)", 844 | "rawQuery": false, 845 | "refId": "A", 846 | "resultFormat": "time_series", 847 | "select": [ 848 | [ 849 | { 850 | "params": [ 851 | "value" 852 | ], 853 | "type": "field" 854 | }, 855 | { 856 | "params": [ 857 | "10s" 858 | ], 859 | "type": "non_negative_derivative" 860 | } 861 | ] 862 | ], 863 | "tags": [ 864 | { 865 | "key": "type_instance", 866 | "operator": "=", 867 | "value": "totalbytesreceived" 868 | } 869 | ], 870 | "target": "" 871 | } 872 | ], 873 | "thresholds": "", 874 | "title": "Total Download", 875 | "type": "singlestat", 876 | "valueFontSize": "100%", 877 | "valueMaps": [ 878 | { 879 | "op": "=", 880 | "text": "N/A", 881 | "value": "null" 882 | } 883 | ], 884 | "valueName": "total" 885 | }, 886 | { 887 | "cacheTimeout": null, 888 | "colorBackground": false, 889 | "colorPrefix": true, 890 | "colorValue": false, 891 | "colors": [ 892 | "rgba(245, 54, 54, 0.9)", 893 | "rgba(237, 129, 40, 0.89)", 894 | "rgba(50, 172, 45, 0.97)" 895 | ], 896 | "datasource": "${DS_PROMETHEUS}", 897 | "decimals": 1, 898 | "format": "decbytes", 899 | "gauge": { 900 | "maxValue": 100, 901 | "minValue": 0, 902 | "show": false, 903 | "thresholdLabels": false, 904 | "thresholdMarkers": true 905 | }, 906 | "gridPos": { 907 | "h": 4, 908 | "w": 6, 909 | "x": 6, 910 | "y": 6 911 | }, 912 | "id": 8, 913 | "interval": null, 914 | "links": [], 915 | "mappingType": 1, 916 | "mappingTypes": [ 917 | { 918 | "name": "value to text", 919 | "value": 1 920 | }, 921 | { 922 | "name": "range to text", 923 | "value": 2 924 | } 925 | ], 926 | "maxDataPoints": 100, 927 | "nullPointMode": "connected", 928 | "nullText": null, 929 | "postfix": "", 930 | "postfixFontSize": "50%", 931 | "prefix": "", 932 | "prefixFontSize": "50%", 933 | "rangeMaps": [ 934 | { 935 | "from": "null", 936 | "text": "N/A", 937 | "to": "null" 938 | } 939 | ], 940 | "sparkline": { 941 | "fillColor": "rgba(137, 15, 2, 0.18)", 942 | "full": false, 943 | "lineColor": "#e24d42", 944 | "show": true 945 | }, 946 | "tableColumn": "", 947 | "targets": [ 948 | { 949 | "dsType": "prometheus", 950 | "expr": "gateway_wan_bytes_sent", 951 | "groupBy": [], 952 | "interval": "", 953 | "legendFormat": "", 954 | "measurement": "fritzbox_value", 955 | "orderByTime": "ASC", 956 | "policy": "default", 957 | "query": "SELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter GROUP BY time($__interval)", 958 | "rawQuery": false, 959 | "refId": "A", 960 | "resultFormat": "time_series", 961 | "select": [ 962 | [ 963 | { 964 | "params": [ 965 | "value" 966 | ], 967 | "type": "field" 968 | }, 969 | { 970 | "params": [], 971 | "type": "non_negative_difference" 972 | } 973 | ] 974 | ], 975 | "tags": [ 976 | { 977 | "key": "type_instance", 978 | "operator": "=", 979 | "value": "totalbytessent" 980 | } 981 | ], 982 | "target": "" 983 | } 984 | ], 985 | "thresholds": "", 986 | "title": "Total Upload", 987 | "type": "singlestat", 988 | "valueFontSize": "100%", 989 | "valueMaps": [ 990 | { 991 | "op": "=", 992 | "text": "N/A", 993 | "value": "null" 994 | } 995 | ], 996 | "valueName": "total" 997 | }, 998 | { 999 | "cacheTimeout": null, 1000 | "colorBackground": false, 1001 | "colorValue": false, 1002 | "colors": [ 1003 | "rgba(245, 54, 54, 0.9)", 1004 | "rgba(237, 129, 40, 0.89)", 1005 | "rgba(50, 172, 45, 0.97)" 1006 | ], 1007 | "datasource": "${DS_PROMETHEUS}", 1008 | "decimals": 1, 1009 | "format": "decbytes", 1010 | "gauge": { 1011 | "maxValue": 100, 1012 | "minValue": 0, 1013 | "show": false, 1014 | "thresholdLabels": false, 1015 | "thresholdMarkers": true 1016 | }, 1017 | "gridPos": { 1018 | "h": 4, 1019 | "w": 6, 1020 | "x": 12, 1021 | "y": 6 1022 | }, 1023 | "hideTimeOverride": false, 1024 | "id": 22, 1025 | "interval": "", 1026 | "links": [], 1027 | "mappingType": 1, 1028 | "mappingTypes": [ 1029 | { 1030 | "name": "value to text", 1031 | "value": 1 1032 | }, 1033 | { 1034 | "name": "range to text", 1035 | "value": 2 1036 | } 1037 | ], 1038 | "maxDataPoints": 100, 1039 | "nullPointMode": "connected", 1040 | "nullText": null, 1041 | "postfix": "", 1042 | "postfixFontSize": "50%", 1043 | "prefix": "", 1044 | "prefixFontSize": "50%", 1045 | "rangeMaps": [ 1046 | { 1047 | "from": "null", 1048 | "text": "N/A", 1049 | "to": "null" 1050 | } 1051 | ], 1052 | "sparkline": { 1053 | "fillColor": "rgba(31, 118, 189, 0.18)", 1054 | "full": false, 1055 | "lineColor": "rgb(31, 120, 193)", 1056 | "show": true 1057 | }, 1058 | "tableColumn": "", 1059 | "targets": [ 1060 | { 1061 | "dsType": "prometheus", 1062 | "expr": "gateway_wan_bytes_received", 1063 | "groupBy": [], 1064 | "interval": "", 1065 | "legendFormat": "", 1066 | "measurement": "fritzbox_value", 1067 | "orderByTime": "ASC", 1068 | "policy": "default", 1069 | "query": "SELECT cumulative_sum(max(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter GROUP BY time($__interval) tz('Europe/Berlin')", 1070 | "rawQuery": false, 1071 | "refId": "A", 1072 | "resultFormat": "time_series", 1073 | "select": [ 1074 | [ 1075 | { 1076 | "params": [ 1077 | "value" 1078 | ], 1079 | "type": "field" 1080 | }, 1081 | { 1082 | "params": [], 1083 | "type": "non_negative_difference" 1084 | } 1085 | ] 1086 | ], 1087 | "tags": [ 1088 | { 1089 | "key": "type_instance", 1090 | "operator": "=", 1091 | "value": "totalbytesreceived" 1092 | } 1093 | ], 1094 | "target": "" 1095 | } 1096 | ], 1097 | "thresholds": "", 1098 | "timeFrom": "1d", 1099 | "timeShift": null, 1100 | "title": "Last 24h Download", 1101 | "type": "singlestat", 1102 | "valueFontSize": "100%", 1103 | "valueMaps": [ 1104 | { 1105 | "op": "=", 1106 | "text": "N/A", 1107 | "value": "null" 1108 | } 1109 | ], 1110 | "valueName": "range" 1111 | }, 1112 | { 1113 | "cacheTimeout": null, 1114 | "colorBackground": false, 1115 | "colorValue": false, 1116 | "colors": [ 1117 | "rgba(245, 54, 54, 0.9)", 1118 | "rgba(237, 129, 40, 0.89)", 1119 | "rgba(50, 172, 45, 0.97)" 1120 | ], 1121 | "datasource": "${DS_PROMETHEUS}", 1122 | "decimals": 1, 1123 | "format": "decbytes", 1124 | "gauge": { 1125 | "maxValue": 100, 1126 | "minValue": 0, 1127 | "show": false, 1128 | "thresholdLabels": false, 1129 | "thresholdMarkers": true 1130 | }, 1131 | "gridPos": { 1132 | "h": 4, 1133 | "w": 6, 1134 | "x": 18, 1135 | "y": 6 1136 | }, 1137 | "id": 16, 1138 | "interval": null, 1139 | "links": [], 1140 | "mappingType": 1, 1141 | "mappingTypes": [ 1142 | { 1143 | "name": "value to text", 1144 | "value": 1 1145 | }, 1146 | { 1147 | "name": "range to text", 1148 | "value": 2 1149 | } 1150 | ], 1151 | "maxDataPoints": 100, 1152 | "nullPointMode": "connected", 1153 | "nullText": null, 1154 | "postfix": "", 1155 | "postfixFontSize": "50%", 1156 | "prefix": "", 1157 | "prefixFontSize": "50%", 1158 | "rangeMaps": [ 1159 | { 1160 | "from": "null", 1161 | "text": "N/A", 1162 | "to": "null" 1163 | } 1164 | ], 1165 | "sparkline": { 1166 | "fillColor": "rgba(137, 15, 2, 0.18)", 1167 | "full": false, 1168 | "lineColor": "#e24d42", 1169 | "show": true 1170 | }, 1171 | "tableColumn": "", 1172 | "targets": [ 1173 | { 1174 | "dsType": "prometheus", 1175 | "expr": "gateway_wan_bytes_sent", 1176 | "groupBy": [], 1177 | "interval": "", 1178 | "legendFormat": "", 1179 | "measurement": "fritzbox_value", 1180 | "orderByTime": "ASC", 1181 | "policy": "default", 1182 | "query": "SELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter GROUP BY time($__interval) tz('Europe/Berlin')", 1183 | "rawQuery": false, 1184 | "refId": "A", 1185 | "resultFormat": "time_series", 1186 | "select": [ 1187 | [ 1188 | { 1189 | "params": [ 1190 | "value" 1191 | ], 1192 | "type": "field" 1193 | }, 1194 | { 1195 | "params": [], 1196 | "type": "non_negative_difference" 1197 | } 1198 | ] 1199 | ], 1200 | "tags": [ 1201 | { 1202 | "key": "type_instance", 1203 | "operator": "=", 1204 | "value": "totalbytessent" 1205 | } 1206 | ], 1207 | "target": "" 1208 | } 1209 | ], 1210 | "thresholds": "", 1211 | "timeFrom": "1d", 1212 | "title": "Last 24h Upload", 1213 | "type": "singlestat", 1214 | "valueFontSize": "100%", 1215 | "valueMaps": [ 1216 | { 1217 | "op": "=", 1218 | "text": "N/A", 1219 | "value": "null" 1220 | } 1221 | ], 1222 | "valueName": "range" 1223 | }, 1224 | { 1225 | "aliasColors": {}, 1226 | "bars": false, 1227 | "dashLength": 10, 1228 | "dashes": false, 1229 | "datasource": "${DS_PROMETHEUS}", 1230 | "fill": 1, 1231 | "fillGradient": 0, 1232 | "gridPos": { 1233 | "h": 8, 1234 | "w": 12, 1235 | "x": 0, 1236 | "y": 10 1237 | }, 1238 | "hiddenSeries": false, 1239 | "id": 24, 1240 | "legend": { 1241 | "avg": false, 1242 | "current": false, 1243 | "max": false, 1244 | "min": false, 1245 | "show": true, 1246 | "total": false, 1247 | "values": false 1248 | }, 1249 | "lines": true, 1250 | "linewidth": 1, 1251 | "nullPointMode": "null", 1252 | "options": { 1253 | "dataLinks": [] 1254 | }, 1255 | "percentage": false, 1256 | "pointradius": 2, 1257 | "points": false, 1258 | "renderer": "flot", 1259 | "seriesOverrides": [], 1260 | "spaceLength": 10, 1261 | "stack": false, 1262 | "steppedLine": false, 1263 | "targets": [ 1264 | { 1265 | "expr": "gateway_wlan_current_connections", 1266 | "interval": "", 1267 | "legendFormat": "2.4 GHz", 1268 | "refId": "A" 1269 | }, 1270 | { 1271 | "expr": "gateway_wlan2_current_connections", 1272 | "interval": "", 1273 | "legendFormat": "5 GHz", 1274 | "refId": "B" 1275 | }, 1276 | { 1277 | "expr": "gateway_wlan_current_connections+gateway_wlan2_current_connections", 1278 | "interval": "", 1279 | "legendFormat": "Total", 1280 | "refId": "C" 1281 | } 1282 | ], 1283 | "thresholds": [], 1284 | "timeFrom": null, 1285 | "timeRegions": [], 1286 | "timeShift": null, 1287 | "title": "WLAN Connections", 1288 | "tooltip": { 1289 | "shared": true, 1290 | "sort": 0, 1291 | "value_type": "individual" 1292 | }, 1293 | "type": "graph", 1294 | "xaxis": { 1295 | "buckets": null, 1296 | "mode": "time", 1297 | "name": null, 1298 | "show": true, 1299 | "values": [] 1300 | }, 1301 | "yaxes": [ 1302 | { 1303 | "format": "short", 1304 | "label": null, 1305 | "logBase": 1, 1306 | "max": null, 1307 | "min": null, 1308 | "show": true 1309 | }, 1310 | { 1311 | "format": "short", 1312 | "label": null, 1313 | "logBase": 1, 1314 | "max": null, 1315 | "min": null, 1316 | "show": true 1317 | } 1318 | ], 1319 | "yaxis": { 1320 | "align": false, 1321 | "alignLevel": null 1322 | } 1323 | }, 1324 | { 1325 | "aliasColors": { 1326 | "downstream": "#1F78C1", 1327 | "downstream max": "#0A437C", 1328 | "upstream": "#EA6460", 1329 | "upstream max": "#890F02" 1330 | }, 1331 | "annotate": { 1332 | "enable": false 1333 | }, 1334 | "bars": false, 1335 | "dashLength": 10, 1336 | "dashes": false, 1337 | "datasource": "${DS_PROMETHEUS}", 1338 | "editable": true, 1339 | "fill": 1, 1340 | "fillGradient": 0, 1341 | "grid": {}, 1342 | "gridPos": { 1343 | "h": 7, 1344 | "w": 6, 1345 | "x": 12, 1346 | "y": 10 1347 | }, 1348 | "hiddenSeries": false, 1349 | "id": 18, 1350 | "legend": { 1351 | "alignAsTable": false, 1352 | "avg": false, 1353 | "current": true, 1354 | "max": false, 1355 | "min": false, 1356 | "rightSide": false, 1357 | "show": true, 1358 | "total": false, 1359 | "values": true 1360 | }, 1361 | "lines": true, 1362 | "linewidth": 1, 1363 | "links": [], 1364 | "nullPointMode": "connected", 1365 | "options": { 1366 | "dataLinks": [] 1367 | }, 1368 | "percentage": false, 1369 | "pointradius": 5, 1370 | "points": false, 1371 | "renderer": "flot", 1372 | "resolution": 100, 1373 | "scale": 1, 1374 | "seriesOverrides": [], 1375 | "spaceLength": 10, 1376 | "stack": false, 1377 | "steppedLine": false, 1378 | "targets": [ 1379 | { 1380 | "alias": "downstream", 1381 | "dsType": "prometheus", 1382 | "expr": "gateway_wan_layer1_downstream_max_bitrate", 1383 | "fields": [ 1384 | { 1385 | "func": "mean", 1386 | "name": "value" 1387 | } 1388 | ], 1389 | "groupBy": [ 1390 | { 1391 | "params": [ 1392 | "$__interval" 1393 | ], 1394 | "type": "time" 1395 | }, 1396 | { 1397 | "params": [ 1398 | "null" 1399 | ], 1400 | "type": "fill" 1401 | } 1402 | ], 1403 | "groupByTags": [], 1404 | "interval": "", 1405 | "legendFormat": "Downstream", 1406 | "measurement": "fritzbox_value", 1407 | "orderByTime": "ASC", 1408 | "policy": "default", 1409 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'receiverate' AND $timeFilter GROUP BY time($interval)", 1410 | "refId": "A", 1411 | "resultFormat": "time_series", 1412 | "select": [ 1413 | [ 1414 | { 1415 | "params": [ 1416 | "value" 1417 | ], 1418 | "type": "field" 1419 | }, 1420 | { 1421 | "params": [], 1422 | "type": "mean" 1423 | } 1424 | ] 1425 | ], 1426 | "tags": [ 1427 | { 1428 | "key": "type_instance", 1429 | "operator": "=", 1430 | "value": "receiverate" 1431 | } 1432 | ], 1433 | "target": "alias(collectd.squirrel.fritzbox.bitrate-receiverate,'downstream')" 1434 | }, 1435 | { 1436 | "alias": "downstream max", 1437 | "dsType": "prometheus", 1438 | "expr": "gateway_wan_layer1_upstream_max_bitrate", 1439 | "fields": [ 1440 | { 1441 | "func": "mean", 1442 | "name": "value" 1443 | } 1444 | ], 1445 | "groupBy": [ 1446 | { 1447 | "params": [ 1448 | "$__interval" 1449 | ], 1450 | "type": "time" 1451 | }, 1452 | { 1453 | "params": [ 1454 | "null" 1455 | ], 1456 | "type": "fill" 1457 | } 1458 | ], 1459 | "groupByTags": [], 1460 | "interval": "", 1461 | "legendFormat": "Upstream", 1462 | "measurement": "fritzbox_value", 1463 | "orderByTime": "ASC", 1464 | "policy": "default", 1465 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'downstreammax' AND $timeFilter GROUP BY time($interval)", 1466 | "refId": "B", 1467 | "resultFormat": "time_series", 1468 | "select": [ 1469 | [ 1470 | { 1471 | "params": [ 1472 | "value" 1473 | ], 1474 | "type": "field" 1475 | }, 1476 | { 1477 | "params": [], 1478 | "type": "mean" 1479 | } 1480 | ] 1481 | ], 1482 | "tags": [ 1483 | { 1484 | "key": "type_instance", 1485 | "operator": "=", 1486 | "value": "downstreammax" 1487 | } 1488 | ], 1489 | "target": "alias(collectd.squirrel.fritzbox.bitrate-downstreammax,'downstream max')" 1490 | }, 1491 | { 1492 | "alias": "upstream", 1493 | "dsType": "prometheus", 1494 | "expr": "gateway_wan_bytes_receive_rate", 1495 | "fields": [ 1496 | { 1497 | "func": "mean", 1498 | "name": "value" 1499 | } 1500 | ], 1501 | "groupBy": [ 1502 | { 1503 | "params": [ 1504 | "$__interval" 1505 | ], 1506 | "type": "time" 1507 | }, 1508 | { 1509 | "params": [ 1510 | "null" 1511 | ], 1512 | "type": "fill" 1513 | } 1514 | ], 1515 | "groupByTags": [], 1516 | "interval": "", 1517 | "legendFormat": "Download", 1518 | "measurement": "fritzbox_value", 1519 | "orderByTime": "ASC", 1520 | "policy": "default", 1521 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'sendrate' AND $timeFilter GROUP BY time($interval)", 1522 | "refId": "C", 1523 | "resultFormat": "time_series", 1524 | "select": [ 1525 | [ 1526 | { 1527 | "params": [ 1528 | "value" 1529 | ], 1530 | "type": "field" 1531 | }, 1532 | { 1533 | "params": [], 1534 | "type": "mean" 1535 | } 1536 | ] 1537 | ], 1538 | "tags": [ 1539 | { 1540 | "key": "type_instance", 1541 | "operator": "=", 1542 | "value": "sendrate" 1543 | } 1544 | ], 1545 | "target": "alias(collectd.squirrel.fritzbox.bitrate-sendrate,'upstream')" 1546 | }, 1547 | { 1548 | "alias": "upstream max", 1549 | "dsType": "prometheus", 1550 | "expr": "gateway_wan_bytes_send_rate", 1551 | "fields": [ 1552 | { 1553 | "func": "mean", 1554 | "name": "value" 1555 | } 1556 | ], 1557 | "groupBy": [ 1558 | { 1559 | "params": [ 1560 | "$__interval" 1561 | ], 1562 | "type": "time" 1563 | }, 1564 | { 1565 | "params": [ 1566 | "null" 1567 | ], 1568 | "type": "fill" 1569 | } 1570 | ], 1571 | "groupByTags": [], 1572 | "interval": "", 1573 | "legendFormat": "Upload", 1574 | "measurement": "fritzbox_value", 1575 | "orderByTime": "ASC", 1576 | "policy": "default", 1577 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'upstreammax' AND $timeFilter GROUP BY time($interval)", 1578 | "refId": "D", 1579 | "resultFormat": "time_series", 1580 | "select": [ 1581 | [ 1582 | { 1583 | "params": [ 1584 | "value" 1585 | ], 1586 | "type": "field" 1587 | }, 1588 | { 1589 | "params": [], 1590 | "type": "mean" 1591 | } 1592 | ] 1593 | ], 1594 | "tags": [ 1595 | { 1596 | "key": "type_instance", 1597 | "operator": "=", 1598 | "value": "upstreammax" 1599 | } 1600 | ], 1601 | "target": "alias(collectd.squirrel.fritzbox.bitrate-upstreammax,'upstream max')" 1602 | } 1603 | ], 1604 | "thresholds": [], 1605 | "timeFrom": null, 1606 | "timeRegions": [], 1607 | "timeShift": null, 1608 | "title": "Bandwidth", 1609 | "tooltip": { 1610 | "msResolution": false, 1611 | "query_as_alias": true, 1612 | "shared": true, 1613 | "sort": 0, 1614 | "value_type": "cumulative" 1615 | }, 1616 | "type": "graph", 1617 | "xaxis": { 1618 | "buckets": null, 1619 | "mode": "time", 1620 | "name": null, 1621 | "show": true, 1622 | "values": [] 1623 | }, 1624 | "yaxes": [ 1625 | { 1626 | "decimals": null, 1627 | "format": "bps", 1628 | "label": "", 1629 | "logBase": 1, 1630 | "max": "116000000", 1631 | "min": "0", 1632 | "show": true 1633 | }, 1634 | { 1635 | "format": "short", 1636 | "logBase": 1, 1637 | "max": null, 1638 | "min": null, 1639 | "show": true 1640 | } 1641 | ], 1642 | "yaxis": { 1643 | "align": false, 1644 | "alignLevel": null 1645 | }, 1646 | "zerofill": true 1647 | }, 1648 | { 1649 | "aliasColors": { 1650 | "download": "#1F78C1", 1651 | "upload": "#EA6460" 1652 | }, 1653 | "annotate": { 1654 | "enable": false 1655 | }, 1656 | "bars": false, 1657 | "dashLength": 10, 1658 | "dashes": false, 1659 | "datasource": "${DS_PROMETHEUS}", 1660 | "editable": true, 1661 | "fill": 0, 1662 | "fillGradient": 0, 1663 | "grid": {}, 1664 | "gridPos": { 1665 | "h": 7, 1666 | "w": 6, 1667 | "x": 18, 1668 | "y": 10 1669 | }, 1670 | "hiddenSeries": false, 1671 | "id": 17, 1672 | "interval": "1h", 1673 | "legend": { 1674 | "avg": false, 1675 | "current": true, 1676 | "max": false, 1677 | "min": false, 1678 | "show": true, 1679 | "total": false, 1680 | "values": true 1681 | }, 1682 | "lines": true, 1683 | "linewidth": 1, 1684 | "links": [], 1685 | "nullPointMode": "connected", 1686 | "options": { 1687 | "dataLinks": [] 1688 | }, 1689 | "percentage": false, 1690 | "pointradius": 5, 1691 | "points": false, 1692 | "renderer": "flot", 1693 | "repeat": null, 1694 | "resolution": 100, 1695 | "scale": 1, 1696 | "seriesOverrides": [], 1697 | "spaceLength": 10, 1698 | "stack": false, 1699 | "steppedLine": true, 1700 | "targets": [ 1701 | { 1702 | "alias": "download", 1703 | "dsType": "prometheus", 1704 | "expr": "delta(gateway_wan_bytes_received[1m])/60", 1705 | "fields": [ 1706 | { 1707 | "func": "mean", 1708 | "name": "value" 1709 | } 1710 | ], 1711 | "groupBy": [ 1712 | { 1713 | "params": [ 1714 | "$interval" 1715 | ], 1716 | "type": "time" 1717 | }, 1718 | { 1719 | "params": [ 1720 | "null" 1721 | ], 1722 | "type": "fill" 1723 | } 1724 | ], 1725 | "groupByTags": [], 1726 | "interval": "", 1727 | "legendFormat": "Download", 1728 | "measurement": "fritzbox_value", 1729 | "orderByTime": "ASC", 1730 | "policy": "default", 1731 | "query": "SELECT mean(\"value\") FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter GROUP BY time($interval) fill(null)", 1732 | "rawQuery": true, 1733 | "refId": "A", 1734 | "resultFormat": "time_series", 1735 | "select": [ 1736 | [ 1737 | { 1738 | "params": [ 1739 | "value" 1740 | ], 1741 | "type": "field" 1742 | }, 1743 | { 1744 | "params": [], 1745 | "type": "mean" 1746 | } 1747 | ] 1748 | ], 1749 | "tags": [ 1750 | { 1751 | "key": "type_instance", 1752 | "operator": "=", 1753 | "value": "totalbytesreceived" 1754 | } 1755 | ], 1756 | "target": "alias(summarize(nonNegativeDerivative(collectd.squirrel.fritzbox.bytes-totalbytesreceived, 0), '1h', 'sum'), 'download')" 1757 | }, 1758 | { 1759 | "alias": "upload", 1760 | "dsType": "prometheus", 1761 | "expr": "delta(gateway_wan_bytes_sent[1m])/60", 1762 | "fields": [ 1763 | { 1764 | "func": "mean", 1765 | "name": "value" 1766 | } 1767 | ], 1768 | "fill": "null", 1769 | "groupBy": [ 1770 | { 1771 | "params": [ 1772 | "$interval" 1773 | ], 1774 | "type": "time" 1775 | }, 1776 | { 1777 | "params": [ 1778 | "null" 1779 | ], 1780 | "type": "fill" 1781 | } 1782 | ], 1783 | "groupByTags": [], 1784 | "hide": false, 1785 | "interval": "", 1786 | "legendFormat": "Upload", 1787 | "measurement": "fritzbox_value", 1788 | "orderByTime": "ASC", 1789 | "policy": "default", 1790 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter GROUP BY time($__interval)\n) WHERE $timeFilter GROUP BY time($__interval) tz('Europe/Berlin')", 1791 | "rawQuery": true, 1792 | "refId": "B", 1793 | "resultFormat": "time_series", 1794 | "select": [ 1795 | [ 1796 | { 1797 | "params": [ 1798 | "value" 1799 | ], 1800 | "type": "field" 1801 | }, 1802 | { 1803 | "params": [], 1804 | "type": "mean" 1805 | } 1806 | ] 1807 | ], 1808 | "tags": [ 1809 | { 1810 | "key": "type_instance", 1811 | "operator": "=", 1812 | "value": "totalbytessent" 1813 | } 1814 | ], 1815 | "target": "alias(summarize(nonNegativeDerivative(collectd.squirrel.fritzbox.bytes-totalbytessent,0),'1h','sum'),'upload')" 1816 | } 1817 | ], 1818 | "thresholds": [], 1819 | "timeFrom": null, 1820 | "timeRegions": [], 1821 | "timeShift": null, 1822 | "title": "Current Traffic", 1823 | "tooltip": { 1824 | "msResolution": false, 1825 | "query_as_alias": true, 1826 | "shared": true, 1827 | "sort": 0, 1828 | "value_type": "cumulative" 1829 | }, 1830 | "type": "graph", 1831 | "xaxis": { 1832 | "buckets": null, 1833 | "mode": "time", 1834 | "name": null, 1835 | "show": true, 1836 | "values": [] 1837 | }, 1838 | "yaxes": [ 1839 | { 1840 | "format": "Bps", 1841 | "logBase": 1, 1842 | "max": null, 1843 | "min": 0, 1844 | "show": true 1845 | }, 1846 | { 1847 | "format": "Bps", 1848 | "logBase": 1, 1849 | "max": null, 1850 | "min": null, 1851 | "show": true 1852 | } 1853 | ], 1854 | "yaxis": { 1855 | "align": false, 1856 | "alignLevel": null 1857 | }, 1858 | "zerofill": true 1859 | }, 1860 | { 1861 | "aliasColors": { 1862 | "Download": "#1f78c1", 1863 | "Upload": "#e24d42", 1864 | "fritzbox_value.non_negative_derivative": "#ba43a9", 1865 | "fritzbox_value.non_negative_difference": "#e24d42" 1866 | }, 1867 | "bars": true, 1868 | "dashLength": 10, 1869 | "dashes": false, 1870 | "datasource": "${DS_PROMETHEUS}", 1871 | "fill": 1, 1872 | "fillGradient": 0, 1873 | "gridPos": { 1874 | "h": 7, 1875 | "w": 12, 1876 | "x": 0, 1877 | "y": 18 1878 | }, 1879 | "hiddenSeries": false, 1880 | "id": 2, 1881 | "interval": "10s", 1882 | "legend": { 1883 | "alignAsTable": false, 1884 | "avg": false, 1885 | "current": false, 1886 | "hideEmpty": false, 1887 | "hideZero": false, 1888 | "max": false, 1889 | "min": false, 1890 | "rightSide": false, 1891 | "show": true, 1892 | "total": false, 1893 | "values": false 1894 | }, 1895 | "lines": false, 1896 | "linewidth": 1, 1897 | "links": [], 1898 | "nullPointMode": "null", 1899 | "options": { 1900 | "dataLinks": [] 1901 | }, 1902 | "percentage": false, 1903 | "pointradius": 5, 1904 | "points": false, 1905 | "renderer": "flot", 1906 | "seriesOverrides": [], 1907 | "spaceLength": 10, 1908 | "stack": false, 1909 | "steppedLine": false, 1910 | "targets": [ 1911 | { 1912 | "alias": "Download", 1913 | "dsType": "prometheus", 1914 | "expr": "gateway_wan_bytes_received", 1915 | "format": "time_series", 1916 | "groupBy": [ 1917 | { 1918 | "params": [ 1919 | "24h" 1920 | ], 1921 | "type": "time" 1922 | } 1923 | ], 1924 | "hide": false, 1925 | "instant": false, 1926 | "interval": "", 1927 | "legendFormat": "Download", 1928 | "measurement": "fritzbox_value", 1929 | "orderByTime": "ASC", 1930 | "policy": "default", 1931 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter\n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 1932 | "rawQuery": true, 1933 | "refId": "F", 1934 | "resultFormat": "time_series", 1935 | "select": [ 1936 | [ 1937 | { 1938 | "params": [ 1939 | "value" 1940 | ], 1941 | "type": "field" 1942 | }, 1943 | { 1944 | "params": [], 1945 | "type": "max" 1946 | }, 1947 | { 1948 | "params": [ 1949 | "10s" 1950 | ], 1951 | "type": "non_negative_derivative" 1952 | } 1953 | ] 1954 | ], 1955 | "tags": [ 1956 | { 1957 | "key": "type_instance", 1958 | "operator": "=", 1959 | "value": "totalbytesreceived" 1960 | } 1961 | ], 1962 | "target": "" 1963 | }, 1964 | { 1965 | "alias": "Upload", 1966 | "dsType": "prometheus", 1967 | "expr": "gateway_wan_bytes_sent", 1968 | "groupBy": [ 1969 | { 1970 | "params": [ 1971 | "24h" 1972 | ], 1973 | "type": "time" 1974 | } 1975 | ], 1976 | "hide": false, 1977 | "interval": "", 1978 | "legendFormat": "Upload", 1979 | "measurement": "fritzbox_value", 1980 | "orderByTime": "ASC", 1981 | "policy": "default", 1982 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter \n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 1983 | "rawQuery": true, 1984 | "refId": "A", 1985 | "resultFormat": "time_series", 1986 | "select": [ 1987 | [ 1988 | { 1989 | "params": [ 1990 | "value" 1991 | ], 1992 | "type": "field" 1993 | }, 1994 | { 1995 | "params": [], 1996 | "type": "max" 1997 | }, 1998 | { 1999 | "params": [ 2000 | "10s" 2001 | ], 2002 | "type": "non_negative_derivative" 2003 | } 2004 | ] 2005 | ], 2006 | "tags": [ 2007 | { 2008 | "key": "type_instance", 2009 | "operator": "=", 2010 | "value": "totalbytesreceived" 2011 | } 2012 | ], 2013 | "target": "" 2014 | } 2015 | ], 2016 | "thresholds": [], 2017 | "timeFrom": null, 2018 | "timeRegions": [], 2019 | "timeShift": null, 2020 | "title": "Daily Traffic", 2021 | "tooltip": { 2022 | "shared": true, 2023 | "sort": 0, 2024 | "value_type": "individual" 2025 | }, 2026 | "type": "graph", 2027 | "xaxis": { 2028 | "buckets": null, 2029 | "mode": "time", 2030 | "name": null, 2031 | "show": true, 2032 | "values": [] 2033 | }, 2034 | "yaxes": [ 2035 | { 2036 | "decimals": 0, 2037 | "format": "decbytes", 2038 | "label": "", 2039 | "logBase": 1, 2040 | "max": null, 2041 | "min": null, 2042 | "show": true 2043 | }, 2044 | { 2045 | "format": "decbytes", 2046 | "label": null, 2047 | "logBase": 1, 2048 | "max": null, 2049 | "min": null, 2050 | "show": true 2051 | } 2052 | ], 2053 | "yaxis": { 2054 | "align": false, 2055 | "alignLevel": null 2056 | } 2057 | }, 2058 | { 2059 | "columns": [], 2060 | "datasource": "${DS_PROMETHEUS}", 2061 | "fontSize": "100%", 2062 | "gridPos": { 2063 | "h": 8, 2064 | "w": 12, 2065 | "x": 0, 2066 | "y": 25 2067 | }, 2068 | "id": 14, 2069 | "links": [], 2070 | "pageSize": null, 2071 | "scroll": true, 2072 | "showHeader": true, 2073 | "sort": { 2074 | "col": 0, 2075 | "desc": true 2076 | }, 2077 | "styles": [ 2078 | { 2079 | "alias": "Time", 2080 | "align": "auto", 2081 | "dateFormat": "MMMM D, YYYY LT", 2082 | "pattern": "Time", 2083 | "type": "date" 2084 | }, 2085 | { 2086 | "alias": "", 2087 | "align": "auto", 2088 | "colorMode": null, 2089 | "colors": [ 2090 | "rgba(245, 54, 54, 0.9)", 2091 | "rgba(237, 129, 40, 0.89)", 2092 | "rgba(50, 172, 45, 0.97)" 2093 | ], 2094 | "decimals": 2, 2095 | "pattern": "/.*/", 2096 | "thresholds": [], 2097 | "type": "number", 2098 | "unit": "decbytes" 2099 | } 2100 | ], 2101 | "targets": [ 2102 | { 2103 | "alias": "Download", 2104 | "dsType": "prometheus", 2105 | "expr": "gateway_wan_bytes_received", 2106 | "groupBy": [ 2107 | { 2108 | "params": [ 2109 | "1d" 2110 | ], 2111 | "type": "time" 2112 | } 2113 | ], 2114 | "interval": "", 2115 | "legendFormat": "Download", 2116 | "measurement": "fritzbox_value", 2117 | "orderByTime": "ASC", 2118 | "policy": "default", 2119 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter \n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 2120 | "rawQuery": true, 2121 | "refId": "A", 2122 | "resultFormat": "time_series", 2123 | "select": [ 2124 | [ 2125 | { 2126 | "params": [ 2127 | "value" 2128 | ], 2129 | "type": "field" 2130 | }, 2131 | { 2132 | "params": [], 2133 | "type": "max" 2134 | }, 2135 | { 2136 | "params": [ 2137 | "10s" 2138 | ], 2139 | "type": "non_negative_derivative" 2140 | } 2141 | ] 2142 | ], 2143 | "tags": [ 2144 | { 2145 | "key": "type_instance", 2146 | "operator": "=", 2147 | "value": "totalbytesreceived" 2148 | } 2149 | ], 2150 | "target": "" 2151 | }, 2152 | { 2153 | "alias": "Upload", 2154 | "dsType": "prometheus", 2155 | "expr": "gateway_wan_bytes_sent", 2156 | "groupBy": [ 2157 | { 2158 | "params": [ 2159 | "1d" 2160 | ], 2161 | "type": "time" 2162 | } 2163 | ], 2164 | "interval": "", 2165 | "legendFormat": "Upload", 2166 | "measurement": "fritzbox_value", 2167 | "orderByTime": "ASC", 2168 | "policy": "default", 2169 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter\n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 2170 | "rawQuery": true, 2171 | "refId": "B", 2172 | "resultFormat": "time_series", 2173 | "select": [ 2174 | [ 2175 | { 2176 | "params": [ 2177 | "value" 2178 | ], 2179 | "type": "field" 2180 | }, 2181 | { 2182 | "params": [], 2183 | "type": "max" 2184 | }, 2185 | { 2186 | "params": [ 2187 | "10s" 2188 | ], 2189 | "type": "non_negative_derivative" 2190 | } 2191 | ] 2192 | ], 2193 | "tags": [ 2194 | { 2195 | "key": "type_instance", 2196 | "operator": "=", 2197 | "value": "totalbytesreceived" 2198 | } 2199 | ], 2200 | "target": "" 2201 | } 2202 | ], 2203 | "title": "Daily Traffic", 2204 | "transform": "timeseries_to_columns", 2205 | "type": "table" 2206 | } 2207 | ], 2208 | "refresh": false, 2209 | "schemaVersion": 22, 2210 | "style": "dark", 2211 | "tags": [], 2212 | "templating": { 2213 | "list": [] 2214 | }, 2215 | "time": { 2216 | "from": "now-7d", 2217 | "to": "now" 2218 | }, 2219 | "timepicker": { 2220 | "refresh_intervals": [ 2221 | "5s", 2222 | "10s", 2223 | "30s", 2224 | "1m", 2225 | "5m", 2226 | "15m", 2227 | "30m", 2228 | "1h", 2229 | "2h", 2230 | "1d" 2231 | ], 2232 | "time_options": [ 2233 | "5m", 2234 | "15m", 2235 | "1h", 2236 | "6h", 2237 | "12h", 2238 | "24h", 2239 | "2d", 2240 | "7d", 2241 | "30d" 2242 | ] 2243 | }, 2244 | "timezone": "", 2245 | "title": "FRITZ!Box Status", 2246 | "uid": "000000013", 2247 | "variables": { 2248 | "list": [] 2249 | }, 2250 | "version": 18 2251 | } -------------------------------------------------------------------------------- /grafana/README.md: -------------------------------------------------------------------------------- 1 | # Grafana dashboard working with this exporter 2 | 3 | This dashboard is based on the following dashboard: 4 | https://grafana.com/grafana/dashboards/713 5 | 6 | Instead of influx it uses prometheus and has been modified and enhanced. 7 | 8 | The dashboard is now also published to [Grafana](https://grafana.com/grafana/dashboards/12579) 9 | 10 | The dashboard is now also working with grafana 7, for grafana 6 use [Dashboard_for_grafana6.json](Dashboard_for_grafana6.json) 11 | -------------------------------------------------------------------------------- /k8s-fritzbox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: fritzbox-exporter 6 | name: fritzbox-exporter 7 | namespace: metrics-app 8 | spec: 9 | progressDeadlineSeconds: 600 10 | replicas: 1 11 | revisionHistoryLimit: 10 12 | selector: 13 | matchLabels: 14 | app: fritzbox-exporter 15 | strategy: 16 | rollingUpdate: 17 | maxSurge: 25% 18 | maxUnavailable: 25% 19 | type: RollingUpdate 20 | template: 21 | metadata: 22 | labels: 23 | app: fritzbox-exporter 24 | name: fritzbox-exporter 25 | spec: 26 | containers: 27 | - env: 28 | - name: GWURL 29 | value: 'http://ip-fritzbox:49000' 30 | - name: USERNAME 31 | value: 'username' 32 | - name: PASSWORD 33 | value: 'password' 34 | image: alexxanddr/fritzbox-exporter:latest 35 | imagePullPolicy: Always 36 | name: fritzbox-exporter 37 | resources: {} 38 | securityContext: 39 | privileged: false 40 | terminationMessagePath: /dev/termination-log 41 | terminationMessagePolicy: File 42 | dnsPolicy: ClusterFirst 43 | hostAliases: 44 | - hostnames: 45 | - fritz.box 46 | ip: 'ip-fritzbox' 47 | restartPolicy: Always 48 | schedulerName: default-scheduler 49 | securityContext: {} 50 | terminationGracePeriodSeconds: 30 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | annotations: 56 | prometheus.io/path: /metrics 57 | prometheus.io/port: "9042" 58 | prometheus.io/scrape: "true" 59 | labels: 60 | app: fritzbox-exporter 61 | name: fritzbox-exporter 62 | namespace: metrics-app 63 | spec: 64 | clusterIP: 65 | ports: 66 | - name: tcp 67 | port: 9042 68 | protocol: TCP 69 | targetPort: 9042 70 | selector: 71 | app: fritzbox-exporter 72 | sessionAffinity: None 73 | type: ClusterIP 74 | status: 75 | loadBalancer: {} 76 | -------------------------------------------------------------------------------- /luaTest-many.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "data.lua", 4 | "params": "page=overview" 5 | }, 6 | { 7 | "path": "data.lua", 8 | "params": "page=ipv6" 9 | }, 10 | { 11 | "path": "data.lua", 12 | "params": "page=dnsSrv" 13 | }, 14 | { 15 | "path": "data.lua", 16 | "params": "page=kidLis" 17 | }, 18 | { 19 | "path": "data.lua", 20 | "params": "page=trafapp" 21 | }, 22 | { 23 | "path": "data.lua", 24 | "params": "page=portoverview" 25 | }, 26 | { 27 | "path": "data.lua", 28 | "params": "page=dslOv" 29 | }, 30 | { 31 | "path": "data.lua", 32 | "params": "page=dialLi" 33 | }, 34 | { 35 | "path": "data.lua", 36 | "params": "page=bookLi" 37 | }, 38 | { 39 | "path": "data.lua", 40 | "params": "page=dectSet" 41 | }, 42 | { 43 | "path": "data.lua", 44 | "params": "page=dectMon" 45 | }, 46 | { 47 | "path": "data.lua", 48 | "params": "page=homeNet" 49 | }, 50 | { 51 | "path": "data.lua", 52 | "params": "page=netDev" 53 | }, 54 | { 55 | "path": "data.lua", 56 | "params": "page=netSet" 57 | }, 58 | { 59 | "path": "data.lua", 60 | "params": "page=usbOv" 61 | }, 62 | { 63 | "path": "data.lua", 64 | "params": "page=mServSet" 65 | }, 66 | { 67 | "path": "data.lua", 68 | "params": "page=wSet" 69 | }, 70 | { 71 | "path": "data.lua", 72 | "params": "page=chan" 73 | }, 74 | { 75 | "path": "data.lua", 76 | "params": "page=sh_dev" 77 | }, 78 | { 79 | "path": "data.lua", 80 | "params": "page=energy" 81 | }, 82 | { 83 | "path": "data.lua", 84 | "params": "page=ecoStat" 85 | }, 86 | { 87 | "path": "GET:internet/inetstat_monitor.lua", 88 | "params": "action=get_graphic&useajax=1" 89 | }, 90 | { 91 | "path": "GET:internet/internet_settings.lua", 92 | "params": "multiwan_page=dsl&useajax=1" 93 | }, 94 | { 95 | "path": "GET:internet/dsl_stats_tab.lua", 96 | "params": "update=mainDiv&useajax=1" 97 | }, 98 | { 99 | "path": "GET:net/network.lua", 100 | "params": "useajax=1" 101 | }, 102 | { 103 | "path": "data.lua", 104 | "params": "page=netCnt" 105 | }, 106 | { 107 | "path": "data.lua", 108 | "params": "page=dslStat" 109 | } 110 | ] -------------------------------------------------------------------------------- /luaTest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "data.lua", 4 | "params": "page=overview" 5 | }, 6 | { 7 | "path": "data.lua", 8 | "params": "page=dslOv" 9 | }, 10 | { 11 | "path": "data.lua", 12 | "params": "page=dectMon" 13 | }, 14 | { 15 | "path": "data.lua", 16 | "params": "page=netDev" 17 | }, 18 | { 19 | "path": "data.lua", 20 | "params": "page=usbOv" 21 | }, 22 | { 23 | "path": "data.lua", 24 | "params": "page=sh_dev" 25 | }, 26 | { 27 | "path": "data.lua", 28 | "params": "page=energy" 29 | }, 30 | { 31 | "path": "data.lua", 32 | "params": "page=ecoStat" 33 | }, 34 | { 35 | "path": "GET:webservices/homeautoswitch.lua", 36 | "params": "switchcmd=getdevicelistinfos" 37 | }, 38 | { 39 | "path": "GET:webservices/homeautoswitch.lua", 40 | "params": "switchcmd=getbasicdevicestats" 41 | } 42 | ] -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright 2016 Nils Decker 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "regexp" 27 | "sort" 28 | "strconv" 29 | "strings" 30 | "sync" 31 | "time" 32 | 33 | "github.com/namsral/flag" 34 | "github.com/prometheus/client_golang/prometheus" 35 | "github.com/prometheus/client_golang/prometheus/promhttp" 36 | "github.com/sirupsen/logrus" 37 | 38 | lua "github.com/sberk42/fritzbox_exporter/fritzbox_lua" 39 | upnp "github.com/sberk42/fritzbox_exporter/fritzbox_upnp" 40 | ) 41 | 42 | const serviceLoadRetryTime = 1 * time.Minute 43 | 44 | // minimum TTL for cached results in seconds 45 | const minCacheTTL = 30 46 | 47 | var ( 48 | flagTest = flag.Bool("test", false, "print all available metrics to stdout") 49 | flagLuaTest = flag.Bool("testLua", false, "read luaTest.json file make all contained calls and dump results") 50 | flagCollect = flag.Bool("collect", false, "print configured metrics to stdout and exit") 51 | flagJSONOut = flag.String("json-out", "", "store metrics also to JSON file when running test") 52 | 53 | flagAddr = flag.String("listen-address", "127.0.0.1:9042", "The address to listen on for HTTP requests.") 54 | flagMetricsFile = flag.String("metrics-file", "metrics.json", "The JSON file with the metric definitions.") 55 | flagDisableLua = flag.Bool("nolua", false, "disable collecting lua metrics") 56 | flagLuaMetricsFile = flag.String("lua-metrics-file", "metrics-lua.json", "The JSON file with the lua metric definitions.") 57 | 58 | flagGatewayURL = flag.String("gateway-url", "http://fritz.box:49000", "The URL of the FRITZ!Box") 59 | flagGatewayLuaURL = flag.String("gateway-luaurl", "http://fritz.box", "The URL of the FRITZ!Box UI") 60 | flagUsername = flag.String("username", "", "The user for the FRITZ!Box UPnP service") 61 | flagPassword = flag.String("password", "", "The password for the FRITZ!Box UPnP service") 62 | flagSessionApi = flag.String("sessionapi", "v1", "Use the v1 md5 authentication (default) or the v2 pbkdf2 authentication") 63 | flagGatewayVerifyTLS = flag.Bool("verifyTls", false, "Verify the tls connection when connecting to the FRITZ!Box") 64 | flagLogLevel = flag.String("log-level", "info", "The logging level. Can be error, warn, info, debug or trace") 65 | ) 66 | 67 | var ( 68 | collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ 69 | Name: "fritzbox_exporter_collectErrors", 70 | Help: "Number of collection errors.", 71 | }) 72 | ) 73 | var ( 74 | luaCollectErrors = prometheus.NewCounter(prometheus.CounterOpts{ 75 | Name: "fritzbox_exporter_luaCollectErrors", 76 | Help: "Number of lua collection errors.", 77 | }) 78 | ) 79 | var collectLuaResultsCached = prometheus.NewCounter(prometheus.CounterOpts{ 80 | Name: "fritzbox_exporter_results_cached", 81 | Help: "Number of results taken from cache.", 82 | ConstLabels: prometheus.Labels{"Cache": "LUA"}, 83 | }) 84 | var collectUpnpResultsCached = prometheus.NewCounter(prometheus.CounterOpts{ 85 | Name: "fritzbox_exporter_results_cached", 86 | Help: "Number of results taken from cache.", 87 | ConstLabels: prometheus.Labels{"Cache": "UPNP"}, 88 | }) 89 | var collectLuaResultsLoaded = prometheus.NewCounter(prometheus.CounterOpts{ 90 | Name: "fritzbox_exporter_results_loaded", 91 | Help: "Number of results loaded from fritzbox.", 92 | ConstLabels: prometheus.Labels{"Cache": "LUA"}, 93 | }) 94 | var collectUpnpResultsLoaded = prometheus.NewCounter(prometheus.CounterOpts{ 95 | Name: "fritzbox_exporter_results_loaded", 96 | Help: "Number of results loaded from fritzbox.", 97 | ConstLabels: prometheus.Labels{"Cache": "UPNP"}, 98 | }) 99 | 100 | // JSONPromDesc metric description loaded from JSON 101 | type JSONPromDesc struct { 102 | FqName string `json:"fqName"` 103 | Help string `json:"help"` 104 | VarLabels []string `json:"varLabels"` 105 | FixedLabels map[string]string `json:"fixedLabels"` 106 | fixedLabelValues string // neeeded to create uniq lookup key when reporting 107 | } 108 | 109 | // ActionArg argument for upnp action 110 | type ActionArg struct { 111 | Name string `json:"Name"` 112 | IsIndex bool `json:"IsIndex"` 113 | ProviderAction string `json:"ProviderAction"` 114 | Value string `json:"Value"` 115 | } 116 | 117 | // Metric upnp metric 118 | type Metric struct { 119 | // initialized loading JSON 120 | Service string `json:"service"` 121 | Action string `json:"action"` 122 | ActionArgument *ActionArg `json:"actionArgument"` 123 | Result string `json:"result"` 124 | OkValue string `json:"okValue"` 125 | PromDesc JSONPromDesc `json:"promDesc"` 126 | PromType string `json:"promType"` 127 | CacheEntryTTL int64 `json:"cacheEntryTTL"` 128 | 129 | // initialized at startup 130 | Desc *prometheus.Desc 131 | MetricType prometheus.ValueType 132 | } 133 | 134 | // LuaTest JSON struct for API tests 135 | type LuaTest struct { 136 | Path string `json:"path"` 137 | Params string `json:"params"` 138 | } 139 | 140 | // LuaLabelRename struct 141 | type LuaLabelRename struct { 142 | MatchRegex string `json:"matchRegex"` 143 | RenameLabel string `json:"renameLabel"` 144 | } 145 | 146 | // LuaMetric struct 147 | type LuaMetric struct { 148 | // initialized loading JSON 149 | Path string `json:"path"` 150 | Params string `json:"params"` 151 | ResultPath string `json:"resultPath"` 152 | ResultKey string `json:"resultKey"` 153 | OkValue string `json:"okValue"` 154 | PromDesc JSONPromDesc `json:"promDesc"` 155 | PromType string `json:"promType"` 156 | CacheEntryTTL int64 `json:"cacheEntryTTL"` 157 | 158 | // initialized at startup 159 | Desc *prometheus.Desc 160 | MetricType prometheus.ValueType 161 | LuaPage lua.LuaPage 162 | LuaMetricDef lua.LuaMetricValueDefinition 163 | } 164 | 165 | // LuaMetricsFile json struct 166 | type LuaMetricsFile struct { 167 | LabelRenames []LuaLabelRename `json:"labelRenames"` 168 | Metrics []*LuaMetric `json:"metrics"` 169 | } 170 | 171 | type upnpCacheEntry struct { 172 | Timestamp int64 173 | Result *upnp.Result 174 | } 175 | 176 | type luaCacheEntry struct { 177 | Timestamp int64 178 | Result *map[string]interface{} 179 | } 180 | 181 | var metrics []*Metric 182 | var luaMetrics []*LuaMetric 183 | var upnpCache map[string]*upnpCacheEntry 184 | var luaCache map[string]*luaCacheEntry 185 | 186 | // FritzboxCollector main struct 187 | type FritzboxCollector struct { 188 | URL string 189 | Gateway string 190 | Username string 191 | Password string 192 | VerifyTls bool 193 | 194 | // support for lua collector 195 | LuaSession *lua.LuaSession 196 | LabelRenames *[]lua.LabelRename 197 | 198 | sync.Mutex // protects Root 199 | Root *upnp.Root 200 | 201 | // health checks 202 | ReadyStatus string 203 | Ready bool 204 | LiveStatus string 205 | Live bool 206 | } 207 | 208 | // simple ResponseWriter to collect output 209 | type testResponseWriter struct { 210 | header http.Header 211 | statusCode int 212 | body bytes.Buffer 213 | } 214 | 215 | func (w *testResponseWriter) Header() http.Header { 216 | return w.header 217 | } 218 | 219 | func (w *testResponseWriter) Write(b []byte) (int, error) { 220 | return w.body.Write(b) 221 | } 222 | 223 | func (w *testResponseWriter) WriteHeader(statusCode int) { 224 | w.statusCode = statusCode 225 | } 226 | 227 | func (w *testResponseWriter) String() string { 228 | return w.body.String() 229 | } 230 | 231 | // LoadServices tries to load the service information. Retries until success. 232 | func (fc *FritzboxCollector) LoadServices() { 233 | fc.ReadyStatus = "loading services" 234 | fc.LiveStatus = "waiting for services loading" 235 | for { 236 | root, err := upnp.LoadServices(fc.URL, fc.Username, fc.Password, fc.VerifyTls) 237 | if err != nil { 238 | fc.ReadyStatus = "failed to load services" 239 | logrus.Errorf("cannot load services: %s", err) 240 | 241 | time.Sleep(serviceLoadRetryTime) 242 | continue 243 | } 244 | 245 | logrus.Info("services loaded") 246 | fc.ReadyStatus = "services loaded" 247 | fc.Ready = true 248 | 249 | // set also live 250 | fc.LiveStatus = "ready" 251 | fc.Live = true 252 | 253 | fc.Lock() 254 | fc.Root = root 255 | fc.Unlock() 256 | return 257 | } 258 | } 259 | 260 | // healthcheck functions 261 | func (fc *FritzboxCollector) ReadynessHandler(w http.ResponseWriter, r *http.Request) { 262 | w.Header().Set("Content-Type", "application/json") 263 | if fc.Ready { 264 | w.WriteHeader(http.StatusOK) 265 | } else { 266 | w.WriteHeader(http.StatusServiceUnavailable) 267 | } 268 | 269 | io.WriteString(w, "{ \"status\":\""+fc.ReadyStatus+"\"}") 270 | } 271 | 272 | func (fc *FritzboxCollector) LivenessHandler(w http.ResponseWriter, r *http.Request) { 273 | w.Header().Set("Content-Type", "application/json") 274 | if fc.Live { 275 | w.WriteHeader(http.StatusOK) 276 | } else { 277 | w.WriteHeader(http.StatusServiceUnavailable) 278 | } 279 | 280 | // is there something in promhttp that we could check? 281 | 282 | io.WriteString(w, "{ \"status\":\""+fc.LiveStatus+"\"}") 283 | } 284 | 285 | // Describe describe metric 286 | func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { 287 | for _, m := range metrics { 288 | ch <- m.Desc 289 | } 290 | } 291 | 292 | func (fc *FritzboxCollector) reportMetric(ch chan<- prometheus.Metric, m *Metric, result upnp.Result, dupCache map[string]bool) { 293 | 294 | val, ok := result[m.Result] 295 | if !ok { 296 | logrus.Debugf("%s.%s has no result %s", m.Service, m.Action, m.Result) 297 | collectErrors.Inc() 298 | return 299 | } 300 | 301 | var floatval float64 302 | switch tval := val.(type) { 303 | case uint64: 304 | floatval = float64(tval) 305 | case bool: 306 | if tval { 307 | floatval = 1 308 | } else { 309 | floatval = 0 310 | } 311 | case string: 312 | if tval == m.OkValue { 313 | floatval = 1 314 | } else { 315 | floatval = 0 316 | } 317 | default: 318 | logrus.Warnf("unknown type: %s", val) 319 | collectErrors.Inc() 320 | return 321 | } 322 | 323 | labels := make([]string, len(m.PromDesc.VarLabels)) 324 | for i, l := range m.PromDesc.VarLabels { 325 | if l == "gateway" { 326 | labels[i] = fc.Gateway 327 | } else { 328 | lval, ok := result[l] 329 | if !ok { 330 | logrus.Warnf("%s.%s has no resul for label %s", m.Service, m.Action, l) 331 | lval = "" 332 | } 333 | 334 | // convert hostname and MAC tolower to avoid problems with labels 335 | if l == "HostName" || l == "MACAddress" { 336 | labels[i] = strings.ToLower(fmt.Sprintf("%v", lval)) 337 | } else { 338 | labels[i] = fmt.Sprintf("%v", lval) 339 | } 340 | } 341 | } 342 | 343 | // check for duplicate labels to prevent collection failure 344 | key := m.PromDesc.FqName + ":" + m.PromDesc.fixedLabelValues + strings.Join(labels, ",") 345 | if dupCache[key] { 346 | logrus.Debugf("%s.%s reported before as: %s\n", m.Service, m.Action, key) 347 | collectErrors.Inc() 348 | return 349 | } 350 | dupCache[key] = true 351 | 352 | metric, err := prometheus.NewConstMetric(m.Desc, m.MetricType, floatval, labels...) 353 | if err != nil { 354 | logrus.Errorf("Can not create metric %s.%s: %s", m.Service, m.Action, err.Error()) 355 | } else { 356 | ch <- metric 357 | } 358 | } 359 | 360 | func (fc *FritzboxCollector) getActionResult(metric *Metric, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) { 361 | 362 | key := metric.Service + "|" + actionName 363 | 364 | // for calls with argument also add argument name and value to key 365 | if actionArg != nil { 366 | key += "|" + actionArg.Name + "|" + fmt.Sprintf("%v", actionArg.Value) 367 | } 368 | 369 | now := time.Now().Unix() 370 | 371 | cacheEntry := upnpCache[key] 372 | if cacheEntry == nil { 373 | cacheEntry = &upnpCacheEntry{} 374 | upnpCache[key] = cacheEntry 375 | } else if now-cacheEntry.Timestamp > metric.CacheEntryTTL { 376 | cacheEntry.Result = nil 377 | } 378 | 379 | if cacheEntry.Result == nil { 380 | service, ok := fc.Root.Services[metric.Service] 381 | if !ok { 382 | return nil, fmt.Errorf("service %s not found", metric.Service) 383 | } 384 | 385 | action, ok := service.Actions[actionName] 386 | if !ok { 387 | return nil, fmt.Errorf("action %s not found in service %s", actionName, metric.Service) 388 | } 389 | 390 | data, err := action.Call(actionArg) 391 | 392 | if err != nil { 393 | return nil, err 394 | } 395 | 396 | cacheEntry.Timestamp = now 397 | cacheEntry.Result = &data 398 | collectUpnpResultsCached.Inc() 399 | } else { 400 | collectUpnpResultsLoaded.Inc() 401 | } 402 | 403 | return *cacheEntry.Result, nil 404 | } 405 | 406 | // Collect collect upnp metrics 407 | func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { 408 | fc.Lock() 409 | root := fc.Root 410 | fc.Unlock() 411 | 412 | if root == nil { 413 | // Services not loaded yet 414 | return 415 | } 416 | 417 | // create cache for duplicate lookup, to prevent collection errors 418 | var dupCache = make(map[string]bool) 419 | 420 | for _, m := range metrics { 421 | var actArg *upnp.ActionArgument 422 | if m.ActionArgument != nil { 423 | aa := m.ActionArgument 424 | var value interface{} 425 | value = aa.Value 426 | 427 | if aa.ProviderAction != "" { 428 | provRes, err := fc.getActionResult(m, aa.ProviderAction, nil) 429 | 430 | if err != nil { 431 | logrus.Warnf("Error getting provider action %s result for %s.%s: %s", aa.ProviderAction, m.Service, m.Action, err.Error()) 432 | collectErrors.Inc() 433 | continue 434 | } 435 | 436 | var ok bool 437 | value, ok = provRes[aa.Value] // Value contains the result name for provider actions 438 | if !ok { 439 | logrus.Warnf("provider action %s for %s.%s has no result", m.Service, m.Action, aa.Value) 440 | collectErrors.Inc() 441 | continue 442 | } 443 | } 444 | 445 | if aa.IsIndex { 446 | sval := fmt.Sprintf("%v", value) 447 | count, err := strconv.Atoi(sval) 448 | if err != nil { 449 | logrus.Warn(err.Error()) 450 | collectErrors.Inc() 451 | continue 452 | } 453 | 454 | for i := 0; i < count; i++ { 455 | actArg = &upnp.ActionArgument{Name: aa.Name, Value: i} 456 | result, err := fc.getActionResult(m, m.Action, actArg) 457 | 458 | if err != nil { 459 | logrus.Errorf("can not get result for %s: %s", m.Action, err) 460 | collectErrors.Inc() 461 | continue 462 | } 463 | 464 | fc.reportMetric(ch, m, result, dupCache) 465 | } 466 | 467 | continue 468 | } else { 469 | actArg = &upnp.ActionArgument{Name: aa.Name, Value: value} 470 | } 471 | } 472 | 473 | result, err := fc.getActionResult(m, m.Action, actArg) 474 | 475 | if err != nil { 476 | logrus.Warnf("can not collect metrics: %s", err) 477 | collectErrors.Inc() 478 | continue 479 | } 480 | 481 | fc.reportMetric(ch, m, result, dupCache) 482 | } 483 | 484 | // if lua is enabled now also collect metrics 485 | if fc.LuaSession != nil { 486 | fc.collectLua(ch, dupCache) 487 | } 488 | } 489 | 490 | func (fc *FritzboxCollector) collectLua(ch chan<- prometheus.Metric, dupCache map[string]bool) { 491 | // create a map for caching results 492 | now := time.Now().Unix() 493 | 494 | for _, lm := range luaMetrics { 495 | key := lm.Path + "_" + lm.Params 496 | 497 | cacheEntry := luaCache[key] 498 | if cacheEntry == nil { 499 | cacheEntry = &luaCacheEntry{} 500 | luaCache[key] = cacheEntry 501 | } else if now-cacheEntry.Timestamp > lm.CacheEntryTTL { 502 | cacheEntry.Result = nil 503 | } 504 | 505 | if cacheEntry.Result == nil { 506 | pageData, err := fc.LuaSession.LoadData(lm.LuaPage) 507 | 508 | if err != nil { 509 | logrus.Errorf("Can not load %s for %s.%s: %s", lm.Path, lm.ResultPath, lm.ResultKey, err.Error()) 510 | luaCollectErrors.Inc() 511 | fc.LuaSession.SID = "" // clear SID in case of error, so force reauthentication 512 | continue 513 | } 514 | 515 | var data map[string]interface{} 516 | data, err = lua.ParseJSON(pageData) 517 | if err != nil { 518 | logrus.Errorf("Can not parse JSON from %s for %s.%s: %s", lm.Path, lm.ResultPath, lm.ResultKey, err.Error()) 519 | luaCollectErrors.Inc() 520 | fc.LuaSession.SID = "" // clear SID in case of error, so force reauthentication 521 | continue 522 | } 523 | 524 | cacheEntry.Result = &data 525 | cacheEntry.Timestamp = now 526 | collectLuaResultsLoaded.Inc() 527 | } else { 528 | collectLuaResultsCached.Inc() 529 | } 530 | 531 | metricVals, err := lua.GetMetrics(fc.LabelRenames, *cacheEntry.Result, lm.LuaMetricDef) 532 | 533 | if err != nil { 534 | logrus.Errorf("Can not get metric values for %s.%s: %s", lm.ResultPath, lm.ResultKey, err.Error()) 535 | luaCollectErrors.Inc() 536 | fc.LuaSession.SID = "" // clear SID in case of error, so force reauthentication 537 | cacheEntry.Result = nil // don't use invalid results for cache 538 | continue 539 | } 540 | 541 | for _, mv := range metricVals { 542 | fc.reportLuaMetric(ch, lm, mv, dupCache) 543 | } 544 | } 545 | } 546 | 547 | func (fc *FritzboxCollector) reportLuaMetric(ch chan<- prometheus.Metric, lm *LuaMetric, value lua.LuaMetricValue, dupCache map[string]bool) { 548 | 549 | labels := make([]string, len(lm.PromDesc.VarLabels)) 550 | for i, l := range lm.PromDesc.VarLabels { 551 | if l == "gateway" { 552 | labels[i] = fc.Gateway 553 | } else { 554 | lval, ok := value.Labels[l] 555 | if !ok { 556 | logrus.Warnf("%s.%s from %s?%s has no resul for label %s", lm.ResultPath, lm.ResultKey, lm.Path, lm.Params, l) 557 | lval = "" 558 | } 559 | 560 | // convert hostname and MAC tolower to avoid problems with labels 561 | if l == "HostName" || l == "MACAddress" { 562 | labels[i] = strings.ToLower(fmt.Sprintf("%v", lval)) 563 | } else { 564 | labels[i] = fmt.Sprintf("%v", lval) 565 | } 566 | } 567 | } 568 | 569 | // check for duplicate labels to prevent collection failure 570 | key := lm.PromDesc.FqName + ":" + lm.PromDesc.fixedLabelValues + strings.Join(labels, ",") 571 | if dupCache[key] { 572 | logrus.Errorf("%s.%s reported before as: %s\n", lm.ResultPath, lm.ResultPath, key) 573 | luaCollectErrors.Inc() 574 | return 575 | } 576 | dupCache[key] = true 577 | 578 | metric, err := prometheus.NewConstMetric(lm.Desc, lm.MetricType, value.Value, labels...) 579 | if err != nil { 580 | logrus.Errorf("Can not create metric %s.%s: %s", lm.ResultPath, lm.ResultPath, err.Error()) 581 | } else { 582 | ch <- metric 583 | } 584 | } 585 | 586 | func test() { 587 | root, err := upnp.LoadServices(*flagGatewayURL, *flagUsername, *flagPassword, *flagGatewayVerifyTLS) 588 | if err != nil { 589 | panic(err) 590 | } 591 | 592 | var newEntry = false 593 | var json bytes.Buffer 594 | json.WriteString("[\n") 595 | 596 | serviceKeys := []string{} 597 | for k := range root.Services { 598 | serviceKeys = append(serviceKeys, k) 599 | } 600 | sort.Strings(serviceKeys) 601 | for _, k := range serviceKeys { 602 | s := root.Services[k] 603 | logrus.Infof("Service: %s (Url: %s)\n", k, s.ControlURL) 604 | 605 | actionKeys := []string{} 606 | for l := range s.Actions { 607 | actionKeys = append(actionKeys, l) 608 | } 609 | sort.Strings(actionKeys) 610 | for _, l := range actionKeys { 611 | a := s.Actions[l] 612 | logrus.Debugf("%s - arguments: variable [direction] (soap name, soap type)", a.Name) 613 | for _, arg := range a.Arguments { 614 | sv := arg.StateVariable 615 | logrus.Debugf("%s [%s] (%s, %s)", arg.RelatedStateVariable, arg.Direction, arg.Name, sv.DataType) 616 | } 617 | 618 | if !a.IsGetOnly() { 619 | logrus.Debugf("%s - not calling, since arguments required or no output", a.Name) 620 | continue 621 | } 622 | 623 | // only create JSON for Get 624 | // TODO also create JSON templates for input actionParams 625 | for _, arg := range a.Arguments { 626 | // create new json entry 627 | if newEntry { 628 | json.WriteString(",\n") 629 | } else { 630 | newEntry = true 631 | } 632 | 633 | json.WriteString("\t{\n\t\t\"service\": \"") 634 | json.WriteString(k) 635 | json.WriteString("\",\n\t\t\"action\": \"") 636 | json.WriteString(a.Name) 637 | json.WriteString("\",\n\t\t\"result\": \"") 638 | json.WriteString(arg.RelatedStateVariable) 639 | json.WriteString("\"\n\t}") 640 | } 641 | 642 | logrus.Debugf("%s - calling - results: variable: value", a.Name) 643 | res, err := a.Call(nil) 644 | 645 | if err != nil { 646 | logrus.Warnf("FAILED:%s", err) 647 | continue 648 | } 649 | 650 | for _, arg := range a.Arguments { 651 | logrus.Debugf("%s: %v", arg.RelatedStateVariable, res[arg.StateVariable.Name]) 652 | } 653 | } 654 | } 655 | 656 | json.WriteString("\n]") 657 | 658 | if *flagJSONOut != "" { 659 | err := os.WriteFile(*flagJSONOut, json.Bytes(), 0644) 660 | if err != nil { 661 | logrus.Warnf("Failed writing JSON file '%s': %s\n", *flagJSONOut, err.Error()) 662 | } 663 | } 664 | } 665 | 666 | func testLua() { 667 | 668 | jsonData, err := os.ReadFile("luaTest.json") 669 | if err != nil { 670 | logrus.Error("Can not read luaTest.json: ", err) 671 | return 672 | } 673 | 674 | var luaTests []LuaTest 675 | err = json.Unmarshal(jsonData, &luaTests) 676 | if err != nil { 677 | logrus.Error("Can not parse luaTest JSON: ", err) 678 | return 679 | } 680 | 681 | // create session struct and init params 682 | luaSession := lua.LuaSession{BaseURL: *flagGatewayLuaURL, Username: *flagUsername, Password: *flagPassword, ApiVer: *flagSessionApi} 683 | 684 | for _, test := range luaTests { 685 | page := lua.LuaPage{Path: test.Path, Params: test.Params} 686 | pageData, err := luaSession.LoadData(page) 687 | 688 | if err != nil { 689 | logrus.Errorf("Testing %s (%s) failed: %s", test.Path, test.Params, err.Error()) 690 | } else { 691 | logrus.Infof("Testing %s (%s) successful: %s", test.Path, test.Params, string(pageData)) 692 | } 693 | } 694 | } 695 | 696 | func getValueType(vt string) prometheus.ValueType { 697 | switch vt { 698 | case "CounterValue": 699 | return prometheus.CounterValue 700 | case "GaugeValue": 701 | return prometheus.GaugeValue 702 | case "UntypedValue": 703 | return prometheus.UntypedValue 704 | } 705 | 706 | return prometheus.UntypedValue 707 | } 708 | 709 | func main() { 710 | flag.Parse() 711 | level, e := logrus.ParseLevel(*flagLogLevel) 712 | if e != nil { 713 | logrus.Warnf("Can not parse log level: %s use INFO", e) 714 | level = logrus.InfoLevel 715 | } 716 | logrus.SetLevel(level) 717 | 718 | u, err := url.Parse(*flagGatewayURL) 719 | if err != nil { 720 | logrus.Errorf("invalid URL: %s", err.Error()) 721 | return 722 | } 723 | 724 | // I have tested authentication against both hostname and ipv4 ip. 725 | // It only seems to work against the latter. It doesn't really 726 | // make sense but it is how it is. 727 | // 728 | // Here we try to figure out what ipv4 resolves to fritz.box 729 | lu, err := url.Parse(*flagGatewayLuaURL) 730 | if err != nil && *flagLuaTest { 731 | logrus.Errorf("invalid LUA URL: %s", err.Error()) 732 | return 733 | } 734 | luahost := lu.Hostname() 735 | if strings.Contains(luahost, "fritz.box") { 736 | ips, err := net.LookupIP("fritz.box") 737 | if err != nil { 738 | logrus.Errorf("could not lookup fritz.box: %s", err.Error()) 739 | return 740 | } 741 | for _, ip := range ips { 742 | if ip.To4() != nil { 743 | luahost = ip.String() 744 | logrus.Infoln("Ip: ", ip) 745 | } 746 | } 747 | } 748 | *flagGatewayLuaURL = fmt.Sprintf("%s://%s", lu.Scheme, luahost) 749 | 750 | if *flagTest { 751 | test() 752 | return 753 | } 754 | 755 | if *flagLuaTest { 756 | testLua() 757 | return 758 | } 759 | 760 | // read metrics 761 | jsonData, err := os.ReadFile(*flagMetricsFile) 762 | if err != nil { 763 | logrus.Errorf("error reading metric file: %s", err.Error()) 764 | return 765 | } 766 | 767 | err = json.Unmarshal(jsonData, &metrics) 768 | if err != nil { 769 | logrus.Errorf("error parsing JSON: %s", err.Error()) 770 | return 771 | } 772 | 773 | // create a map for caching results 774 | upnpCache = make(map[string]*upnpCacheEntry) 775 | 776 | var luaSession *lua.LuaSession 777 | var luaLabelRenames *[]lua.LabelRename 778 | if !*flagDisableLua { 779 | jsonData, err := os.ReadFile(*flagLuaMetricsFile) 780 | if err != nil { 781 | logrus.Error("error reading lua metric file:", err) 782 | return 783 | } 784 | 785 | var lmf *LuaMetricsFile 786 | err = json.Unmarshal(jsonData, &lmf) 787 | if err != nil { 788 | logrus.Error("error parsing lua JSON:", err) 789 | return 790 | } 791 | 792 | // create a map for caching results 793 | luaCache = make(map[string]*luaCacheEntry) 794 | 795 | // init label renames 796 | lblRen := make([]lua.LabelRename, 0) 797 | for _, ren := range lmf.LabelRenames { 798 | regex, err := regexp.Compile(ren.MatchRegex) 799 | 800 | if err != nil { 801 | logrus.Error("error compiling lua rename regex:", err) 802 | return 803 | } 804 | 805 | lblRen = append(lblRen, lua.LabelRename{Pattern: *regex, Name: ren.RenameLabel}) 806 | } 807 | luaLabelRenames = &lblRen 808 | 809 | // init metrics 810 | luaMetrics = lmf.Metrics 811 | for _, lm := range luaMetrics { 812 | pd := &lm.PromDesc 813 | 814 | // make labels lower case 815 | labels := make([]string, len(pd.VarLabels)) 816 | for i, l := range pd.VarLabels { 817 | labels[i] = strings.ToLower(l) 818 | } 819 | 820 | // create fixed labels values 821 | pd.fixedLabelValues = "" 822 | for _, flv := range pd.FixedLabels { 823 | pd.fixedLabelValues += flv + "," 824 | } 825 | 826 | lm.Desc = prometheus.NewDesc(pd.FqName, pd.Help, labels, pd.FixedLabels) 827 | lm.MetricType = getValueType(lm.PromType) 828 | 829 | lm.LuaPage = lua.LuaPage{ 830 | Path: lm.Path, 831 | Params: lm.Params, 832 | } 833 | 834 | lm.LuaMetricDef = lua.LuaMetricValueDefinition{ 835 | Path: lm.ResultPath, 836 | Key: lm.ResultKey, 837 | OkValue: lm.OkValue, 838 | Labels: pd.VarLabels, 839 | } 840 | 841 | // init TTL 842 | if lm.CacheEntryTTL < minCacheTTL { 843 | lm.CacheEntryTTL = minCacheTTL 844 | } 845 | } 846 | 847 | luaSession = &lua.LuaSession{ 848 | BaseURL: *flagGatewayLuaURL, 849 | Username: *flagUsername, 850 | Password: *flagPassword, 851 | ApiVer: *flagSessionApi, 852 | } 853 | } 854 | 855 | // init metrics 856 | renw := regexp.MustCompile(`\W+`) 857 | for _, m := range metrics { 858 | pd := &m.PromDesc 859 | 860 | // make labels lower and replace - with _ 861 | labels := make([]string, len(pd.VarLabels)) 862 | for i, l := range pd.VarLabels { 863 | labels[i] = renw.ReplaceAllString(strings.ToLower(strings.ReplaceAll(l, "-", "_")), "") 864 | } 865 | 866 | // create fixed labels values 867 | pd.fixedLabelValues = "" 868 | for _, flv := range pd.FixedLabels { 869 | pd.fixedLabelValues += flv + "," 870 | } 871 | 872 | m.Desc = prometheus.NewDesc(pd.FqName, pd.Help, labels, pd.FixedLabels) 873 | m.MetricType = getValueType(m.PromType) 874 | 875 | // init TTL 876 | if m.CacheEntryTTL < minCacheTTL { 877 | m.CacheEntryTTL = minCacheTTL 878 | } 879 | } 880 | 881 | collector := &FritzboxCollector{ 882 | URL: *flagGatewayURL, 883 | Gateway: u.Hostname(), 884 | Username: *flagUsername, 885 | Password: *flagPassword, 886 | VerifyTls: *flagGatewayVerifyTLS, 887 | 888 | LuaSession: luaSession, 889 | LabelRenames: luaLabelRenames, 890 | } 891 | 892 | if *flagCollect { 893 | collector.LoadServices() 894 | 895 | prometheus.MustRegister(collector) 896 | prometheus.MustRegister(collectErrors) 897 | if luaSession != nil { 898 | prometheus.MustRegister(luaCollectErrors) 899 | } 900 | 901 | logrus.Infof("collecting metrics via http") 902 | 903 | // simulate HTTP request without starting actual http server 904 | writer := testResponseWriter{header: http.Header{}} 905 | request := http.Request{} 906 | promhttp.Handler().ServeHTTP(&writer, &request) 907 | 908 | logrus.Infof("Response:\n\n%s", writer.String()) 909 | 910 | return 911 | } 912 | 913 | go collector.LoadServices() 914 | 915 | prometheus.MustRegister(collector) 916 | prometheus.MustRegister(collectErrors) 917 | prometheus.MustRegister(collectUpnpResultsCached) 918 | prometheus.MustRegister(collectUpnpResultsLoaded) 919 | 920 | if luaSession != nil { 921 | prometheus.MustRegister(luaCollectErrors) 922 | prometheus.MustRegister(collectLuaResultsCached) 923 | prometheus.MustRegister(collectLuaResultsLoaded) 924 | } 925 | 926 | http.Handle("/metrics", promhttp.Handler()) 927 | logrus.Infof("metrics available at http://%s/metrics", *flagAddr) 928 | http.HandleFunc("/ready", collector.ReadynessHandler) 929 | logrus.Infof("readyness check available at http://%s/ready", *flagAddr) 930 | http.HandleFunc("/live", collector.LivenessHandler) 931 | logrus.Infof("liveness check available at http://%s/live", *flagAddr) 932 | 933 | logrus.Error(http.ListenAndServe(*flagAddr, nil)) 934 | } 935 | -------------------------------------------------------------------------------- /metrics-lua.json: -------------------------------------------------------------------------------- 1 | { 2 | "labelRenames": [ 3 | { 4 | "matchRegex": "(?i)^(?:prozessor|processore)", 5 | "renameLabel": "CPU" 6 | }, 7 | { 8 | "matchRegex": "(?i)^(?:system|sistema)", 9 | "renameLabel": "System" 10 | }, 11 | { 12 | "matchRegex": "(?i)DSL", 13 | "renameLabel": "DSL" 14 | }, 15 | { 16 | "matchRegex": "(?i)FON", 17 | "renameLabel": "Phone" 18 | }, 19 | { 20 | "matchRegex": "(?i)WLAN", 21 | "renameLabel": "WLAN" 22 | }, 23 | { 24 | "matchRegex": "(?i)USB", 25 | "renameLabel": "USB" 26 | }, 27 | { 28 | "matchRegex": "(?i)Speicher.*FRITZ", 29 | "renameLabel": "Internal eStorage" 30 | } 31 | ], 32 | "metrics": [ 33 | { 34 | "path": "data.lua", 35 | "params": "page=energy", 36 | "resultPath": "data.drain.*", 37 | "resultKey": "actPerc", 38 | "promDesc": { 39 | "fqName": "gateway_data_energy_consumption", 40 | "help": "percentage of energy consumed from data.lua?page=energy", 41 | "varLabels": [ 42 | "gateway", "name" 43 | ] 44 | }, 45 | "promType": "GaugeValue", 46 | "cacheEntryTTL": 300 47 | }, 48 | { 49 | "path": "data.lua", 50 | "params": "page=energy", 51 | "resultPath": "data.drain.*.lan.*", 52 | "resultKey": "class", 53 | "okValue": "green", 54 | "promDesc": { 55 | "fqName": "gateway_data_energy_lan_status", 56 | "help": "status of LAN connection from data.lua?page=energy (1 = up)", 57 | "varLabels": [ 58 | "gateway", "name" 59 | ] 60 | }, 61 | "promType": "GaugeValue", 62 | "cacheEntryTTL": 300 63 | }, 64 | { 65 | "path": "data.lua", 66 | "params": "page=ecoStat", 67 | "resultPath": "data.cputemp.series.0", 68 | "resultKey": "-1", 69 | "promDesc": { 70 | "fqName": "gateway_data_ecostat_cputemp", 71 | "help": "cpu temperature from data.lua?page=ecoStat", 72 | "varLabels": [ 73 | "gateway" 74 | ] 75 | }, 76 | "promType": "GaugeValue", 77 | "cacheEntryTTL": 300 78 | }, 79 | { 80 | "path": "data.lua", 81 | "params": "page=ecoStat", 82 | "resultPath": "data.cpuutil.series.0", 83 | "resultKey": "-1", 84 | "promDesc": { 85 | "fqName": "gateway_data_ecostat_cpuutil", 86 | "help": "percentage of cpu utilization from data.lua?page=ecoStat", 87 | "varLabels": [ 88 | "gateway" 89 | ] 90 | }, 91 | "promType": "GaugeValue", 92 | "cacheEntryTTL": 300 93 | }, 94 | { 95 | "path": "data.lua", 96 | "params": "page=ecoStat", 97 | "resultPath": "data.ramusage.series.0", 98 | "resultKey": "-1", 99 | "promDesc": { 100 | "fqName": "gateway_data_ecostat_ramusage", 101 | "help": "percentage of RAM utilization from data.lua?page=energy", 102 | "varLabels": [ 103 | "gateway" 104 | ], 105 | "fixedLabels": { 106 | "ram_type" : "Fixed" 107 | } 108 | }, 109 | "promType": "GaugeValue", 110 | "cacheEntryTTL": 300 111 | }, 112 | { 113 | "path": "data.lua", 114 | "params": "page=ecoStat", 115 | "resultPath": "data.ramusage.series.1", 116 | "resultKey": "-1", 117 | "promDesc": { 118 | "fqName": "gateway_data_ecostat_ramusage", 119 | "help": "percentage of RAM utilization from data.lua?page=energy", 120 | "varLabels": [ 121 | "gateway" 122 | ], 123 | "fixedLabels": { 124 | "ram_type" : "Dynamic" 125 | } 126 | }, 127 | "promType": "GaugeValue", 128 | "cacheEntryTTL": 300 129 | }, 130 | { 131 | "path": "data.lua", 132 | "params": "page=ecoStat", 133 | "resultPath": "data.ramusage.series.2", 134 | "resultKey": "-1", 135 | "promDesc": { 136 | "fqName": "gateway_data_ecostat_ramusage", 137 | "help": "percentage of RAM utilization from data.lua?page=energy", 138 | "varLabels": [ 139 | "gateway" 140 | ], 141 | "fixedLabels": { 142 | "ram_type" : "Free" 143 | } 144 | }, 145 | "promType": "GaugeValue", 146 | "cacheEntryTTL": 300 147 | }, 148 | { 149 | "path": "data.lua", 150 | "params": "page=usbOv", 151 | "resultPath": "data.usbOverview.devices.*", 152 | "resultKey": "partitions.0.totalStorageInBytes", 153 | "promDesc": { 154 | "fqName": "gateway_data_usb_storage_total", 155 | "help": "total storage in bytes from data.lua?page=usbOv", 156 | "varLabels": [ 157 | "gateway", "deviceType", "deviceName" 158 | ] 159 | }, 160 | "promType": "GaugeValue", 161 | "cacheEntryTTL": 300 162 | }, 163 | { 164 | "path": "data.lua", 165 | "params": "page=usbOv", 166 | "resultPath": "data.usbOverview.devices.*", 167 | "resultKey": "partitions.0.usedStorageInBytes", 168 | "promDesc": { 169 | "fqName": "gateway_data_usb_storage_used", 170 | "help": "used storage in bytes from data.lua?page=usbOv", 171 | "varLabels": [ 172 | "gateway", "deviceType", "deviceName" 173 | ] 174 | }, 175 | "promType": "GaugeValue", 176 | "cacheEntryTTL": 300 177 | } 178 | ] 179 | } 180 | -------------------------------------------------------------------------------- /metrics-lua_cable.json: -------------------------------------------------------------------------------- 1 | { 2 | "labelRenames": [ 3 | { 4 | "matchRegex": "64QAM", 5 | "renameLabel": "64" 6 | }, 7 | { 8 | "matchRegex": "16QAM", 9 | "renameLabel": "16" 10 | }, 11 | { 12 | "matchRegex": "4096QAM", 13 | "renameLabel": "4096" 14 | }, 15 | { 16 | "matchRegex": "256QAM", 17 | "renameLabel": "256" 18 | }, 19 | { 20 | "matchRegex": "(?i)prozessor", 21 | "renameLabel": "CPU" 22 | }, 23 | { 24 | "matchRegex": "(?i)system", 25 | "renameLabel": "System" 26 | }, 27 | { 28 | "matchRegex": "(?i)DSL", 29 | "renameLabel": "DSL" 30 | }, 31 | { 32 | "matchRegex": "(?i)FON", 33 | "renameLabel": "Phone" 34 | }, 35 | { 36 | "matchRegex": "(?i)WLAN", 37 | "renameLabel": "WLAN" 38 | }, 39 | { 40 | "matchRegex": "(?i)USB", 41 | "renameLabel": "USB" 42 | }, 43 | { 44 | "matchRegex": "(?i)Speicher.*FRITZ", 45 | "renameLabel": "Internal eStorage" 46 | } 47 | ], 48 | "metrics": [ 49 | { 50 | "path": "data.lua", 51 | "params": "page=docInfo", 52 | "resultPath": "data.channelUs.docsis30.*", 53 | "resultKey": "powerLevel", 54 | "promDesc": { 55 | "fqName": "gateway_cable_power_upstream", 56 | "help": "docsis 3.0 power upstream from data.lua?page=docInfo", 57 | "varLabels": [ 58 | "gateway", 59 | "frequency" 60 | ] 61 | }, 62 | "promType": "GaugeValue", 63 | "cacheEntryTTL": 300 64 | }, 65 | { 66 | "path": "data.lua", 67 | "params": "page=docInfo", 68 | "resultPath": "data.channelUs.docsis30.*", 69 | "resultKey": "modulation", 70 | "promDesc": { 71 | "fqName": "gateway_cable_modulation_upstream", 72 | "help": "docsis 3.0 modulation upstream from data.lua?page=docInfo", 73 | "varLabels": [ 74 | "gateway", 75 | "frequency" 76 | ] 77 | }, 78 | "promType": "GaugeValue", 79 | "cacheEntryTTL": 300 80 | }, 81 | { 82 | "path": "data.lua", 83 | "params": "page=docInfo", 84 | "resultPath": "data.channelUs.docsis31.*", 85 | "resultKey": "powerLevel", 86 | "promDesc": { 87 | "fqName": "gateway_cable_power_upstream31", 88 | "help": "docsis 3.1 power upstream from data.lua?page=docInfo", 89 | "varLabels": [ 90 | "gateway", 91 | "frequency" 92 | ] 93 | }, 94 | "promType": "GaugeValue", 95 | "cacheEntryTTL": 300 96 | }, 97 | { 98 | "path": "data.lua", 99 | "params": "page=docInfo", 100 | "resultPath": "data.channelDs.docsis30.*", 101 | "resultKey": "corrErrors", 102 | "promDesc": { 103 | "fqName": "gateway_cable_correctables_downstream", 104 | "help": "docsis 3.0 correctable errors dpwnstream from data.lua?page=docInfo", 105 | "varLabels": [ 106 | "gateway", 107 | "frequency" 108 | ] 109 | }, 110 | "promType": "GaugeValue", 111 | "cacheEntryTTL": 300 112 | }, 113 | { 114 | "path": "data.lua", 115 | "params": "page=docInfo", 116 | "resultPath": "data.channelDs.docsis30.*", 117 | "resultKey": "nonCorrErrors", 118 | "promDesc": { 119 | "fqName": "gateway_cable_uncorrectables_downstream", 120 | "help": "docsis 3.0 uncorrectable errors downstream from data.lua?page=docInfo", 121 | "varLabels": [ 122 | "gateway", 123 | "frequency" 124 | ] 125 | }, 126 | "promType": "GaugeValue", 127 | "cacheEntryTTL": 300 128 | }, 129 | { 130 | "path": "data.lua", 131 | "params": "page=docInfo", 132 | "resultPath": "data.channelDs.docsis30.*", 133 | "resultKey": "mse", 134 | "promDesc": { 135 | "fqName": "gateway_cable_mse_downstream", 136 | "help": "docsis 3.0 mse downstream from data.lua?page=docInfo", 137 | "varLabels": [ 138 | "gateway", 139 | "frequency" 140 | ] 141 | }, 142 | "promType": "GaugeValue", 143 | "cacheEntryTTL": 300 144 | }, 145 | { 146 | "path": "data.lua", 147 | "params": "page=docInfo", 148 | "resultPath": "data.channelDs.docsis30.*", 149 | "resultKey": "powerLevel", 150 | "promDesc": { 151 | "fqName": "gateway_cable_power_downstream", 152 | "help": "docsis 3.0 powerlevel downstream from data.lua?page=docInfo", 153 | "varLabels": [ 154 | "gateway", 155 | "frequency" 156 | ] 157 | }, 158 | "promType": "GaugeValue", 159 | "cacheEntryTTL": 300 160 | }, 161 | { 162 | "path": "data.lua", 163 | "params": "page=docInfo", 164 | "resultPath": "data.channelDs.docsis31.*", 165 | "resultKey": "powerLevel", 166 | "promDesc": { 167 | "fqName": "gateway_cable_power_downstream31", 168 | "help": "docsis 3.1 powerlevel downstream from data.lua?page=docInfo", 169 | "varLabels": [ 170 | "gateway", 171 | "frequency" 172 | ] 173 | }, 174 | "promType": "GaugeValue", 175 | "cacheEntryTTL": 300 176 | }, 177 | { 178 | "path": "data.lua", 179 | "params": "page=energy", 180 | "resultPath": "data.drain.*", 181 | "resultKey": "actPerc", 182 | "promDesc": { 183 | "fqName": "gateway_data_energy_consumption", 184 | "help": "percentage of energy consumed from data.lua?page=energy", 185 | "varLabels": [ 186 | "gateway", 187 | "name" 188 | ] 189 | }, 190 | "promType": "GaugeValue", 191 | "cacheEntryTTL": 300 192 | }, 193 | { 194 | "path": "data.lua", 195 | "params": "page=energy", 196 | "resultPath": "data.drain.*.lan.*", 197 | "resultKey": "class", 198 | "okValue": "green", 199 | "promDesc": { 200 | "fqName": "gateway_data_energy_lan_status", 201 | "help": "status of LAN connection from data.lua?page=energy (1 = up)", 202 | "varLabels": [ 203 | "gateway", 204 | "name" 205 | ] 206 | }, 207 | "promType": "GaugeValue", 208 | "cacheEntryTTL": 300 209 | }, 210 | { 211 | "path": "data.lua", 212 | "params": "page=ecoStat", 213 | "resultPath": "data.cputemp.series.0", 214 | "resultKey": "-1", 215 | "promDesc": { 216 | "fqName": "gateway_data_ecostat_cputemp", 217 | "help": "cpu temperature from data.lua?page=ecoStat", 218 | "varLabels": [ 219 | "gateway" 220 | ] 221 | }, 222 | "promType": "GaugeValue", 223 | "cacheEntryTTL": 300 224 | }, 225 | { 226 | "path": "data.lua", 227 | "params": "page=ecoStat", 228 | "resultPath": "data.cpuutil.series.0", 229 | "resultKey": "-1", 230 | "promDesc": { 231 | "fqName": "gateway_data_ecostat_cpuutil", 232 | "help": "percentage of cpu utilization from data.lua?page=ecoStat", 233 | "varLabels": [ 234 | "gateway" 235 | ] 236 | }, 237 | "promType": "GaugeValue", 238 | "cacheEntryTTL": 300 239 | }, 240 | { 241 | "path": "data.lua", 242 | "params": "page=ecoStat", 243 | "resultPath": "data.ramusage.series.0", 244 | "resultKey": "-1", 245 | "promDesc": { 246 | "fqName": "gateway_data_ecostat_ramusage", 247 | "help": "percentage of RAM utilization from data.lua?page=energy", 248 | "varLabels": [ 249 | "gateway" 250 | ], 251 | "fixedLabels": { 252 | "ram_type": "Fixed" 253 | } 254 | }, 255 | "promType": "GaugeValue", 256 | "cacheEntryTTL": 300 257 | }, 258 | { 259 | "path": "data.lua", 260 | "params": "page=ecoStat", 261 | "resultPath": "data.ramusage.series.1", 262 | "resultKey": "-1", 263 | "promDesc": { 264 | "fqName": "gateway_data_ecostat_ramusage", 265 | "help": "percentage of RAM utilization from data.lua?page=energy", 266 | "varLabels": [ 267 | "gateway" 268 | ], 269 | "fixedLabels": { 270 | "ram_type": "Dynamic" 271 | } 272 | }, 273 | "promType": "GaugeValue", 274 | "cacheEntryTTL": 300 275 | }, 276 | { 277 | "path": "data.lua", 278 | "params": "page=ecoStat", 279 | "resultPath": "data.ramusage.series.2", 280 | "resultKey": "-1", 281 | "promDesc": { 282 | "fqName": "gateway_data_ecostat_ramusage", 283 | "help": "percentage of RAM utilization from data.lua?page=energy", 284 | "varLabels": [ 285 | "gateway" 286 | ], 287 | "fixedLabels": { 288 | "ram_type": "Free" 289 | } 290 | }, 291 | "promType": "GaugeValue", 292 | "cacheEntryTTL": 300 293 | }, 294 | { 295 | "path": "data.lua", 296 | "params": "page=usbOv", 297 | "resultPath": "data.usbOverview.devices.*", 298 | "resultKey": "partitions.0.totalStorageInBytes", 299 | "promDesc": { 300 | "fqName": "gateway_data_usb_storage_total", 301 | "help": "total storage in bytes from data.lua?page=usbOv", 302 | "varLabels": [ 303 | "gateway", 304 | "deviceType", 305 | "deviceName" 306 | ] 307 | }, 308 | "promType": "GaugeValue", 309 | "cacheEntryTTL": 300 310 | }, 311 | { 312 | "path": "data.lua", 313 | "params": "page=usbOv", 314 | "resultPath": "data.usbOverview.devices.*", 315 | "resultKey": "partitions.0.usedStorageInBytes", 316 | "promDesc": { 317 | "fqName": "gateway_data_usb_storage_used", 318 | "help": "used storage in bytes from data.lua?page=usbOv", 319 | "varLabels": [ 320 | "gateway", 321 | "deviceType", 322 | "deviceName" 323 | ] 324 | }, 325 | "promType": "GaugeValue", 326 | "cacheEntryTTL": 300 327 | } 328 | ] 329 | } -------------------------------------------------------------------------------- /metrics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 4 | "action": "GetTotalPacketsReceived", 5 | "result": "TotalPacketsReceived", 6 | "promDesc": { 7 | "fqName": "gateway_traffic", 8 | "help": "traffic on gateway interface", 9 | "varLabels": [ 10 | "gateway" 11 | ], 12 | "fixedLabels": { 13 | "direction": "Received", 14 | "unit": "Packets", 15 | "interface": "WAN" 16 | } 17 | }, 18 | "promType": "CounterValue" 19 | }, 20 | { 21 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 22 | "action": "GetTotalPacketsSent", 23 | "result": "TotalPacketsSent", 24 | "promDesc": { 25 | "fqName": "gateway_traffic", 26 | "help": "traffic on gateway interface", 27 | "varLabels": [ 28 | "gateway" 29 | ], 30 | "fixedLabels": { 31 | "direction": "Sent", 32 | "unit": "Packets", 33 | "interface": "WAN" 34 | } 35 | }, 36 | "promType": "CounterValue" 37 | }, 38 | { 39 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 40 | "action": "GetAddonInfos", 41 | "result": "TotalBytesReceived", 42 | "promDesc": { 43 | "fqName": "gateway_traffic", 44 | "help": "traffic on gateway interface", 45 | "varLabels": [ 46 | "gateway" 47 | ], 48 | "fixedLabels": { 49 | "direction": "Received", 50 | "unit": "Bytes", 51 | "interface": "WAN" 52 | } 53 | }, 54 | "promType": "CounterValue" 55 | }, 56 | { 57 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 58 | "action": "GetAddonInfos", 59 | "result": "TotalBytesSent", 60 | "promDesc": { 61 | "fqName": "gateway_traffic", 62 | "help": "traffic on gateway interface", 63 | "varLabels": [ 64 | "gateway" 65 | ], 66 | "fixedLabels": { 67 | "direction": "Sent", 68 | "unit": "Bytes", 69 | "interface": "WAN" 70 | } 71 | }, 72 | "promType": "CounterValue" 73 | }, 74 | { 75 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 76 | "action": "GetAddonInfos", 77 | "result": "ByteSendRate", 78 | "promDesc": { 79 | "fqName": "gateway_traffic_rate", 80 | "help": "traffic rate on gateway interface", 81 | "varLabels": [ 82 | "gateway" 83 | ], 84 | "fixedLabels": { 85 | "direction": "Sent", 86 | "unit": "Bytes", 87 | "interface": "WAN" 88 | } 89 | }, 90 | "promType": "GaugeValue" 91 | }, 92 | { 93 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 94 | "action": "GetAddonInfos", 95 | "result": "ByteReceiveRate", 96 | "promDesc": { 97 | "fqName": "gateway_traffic_rate", 98 | "help": "traffic rate on gateway interface", 99 | "varLabels": [ 100 | "gateway" 101 | ], 102 | "fixedLabels": { 103 | "direction": "Received", 104 | "unit": "Bytes", 105 | "interface": "WAN" 106 | } 107 | }, 108 | "promType": "GaugeValue" 109 | }, 110 | { 111 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 112 | "action": "GetCommonLinkProperties", 113 | "result": "Layer1UpstreamMaxBitRate", 114 | "promDesc": { 115 | "fqName": "gateway_max_bitrate", 116 | "help": "max bitrate on gateway interface", 117 | "varLabels": [ 118 | "gateway" 119 | ], 120 | "fixedLabels": { 121 | "direction": "Up", 122 | "interface": "WAN" 123 | } 124 | }, 125 | "promType": "GaugeValue", 126 | "cacheEntryTTL": 60 127 | }, 128 | { 129 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 130 | "action": "GetCommonLinkProperties", 131 | "result": "Layer1DownstreamMaxBitRate", 132 | "promDesc": { 133 | "fqName": "gateway_max_bitrate", 134 | "help": "max bitrate on gateway interface", 135 | "varLabels": [ 136 | "gateway" 137 | ], 138 | "fixedLabels": { 139 | "direction": "Down", 140 | "interface": "WAN" 141 | } 142 | }, 143 | "promType": "GaugeValue", 144 | "cacheEntryTTL": 60 145 | }, 146 | { 147 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 148 | "action": "GetCommonLinkProperties", 149 | "result": "PhysicalLinkStatus", 150 | "okValue": "Up", 151 | "promDesc": { 152 | "fqName": "gateway_connection_status", 153 | "help": "Connection status (Connected = 1)", 154 | "varLabels": [ 155 | "gateway" 156 | ], 157 | "fixedLabels": { 158 | "connection": "Physical Link", 159 | "interface": "WAN" 160 | } 161 | }, 162 | "promType": "GaugeValue", 163 | "cacheEntryTTL": 60 164 | }, 165 | { 166 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 167 | "action": "GetTotalPacketsReceived", 168 | "result": "TotalPacketsReceived", 169 | "promDesc": { 170 | "fqName": "gateway_traffic", 171 | "help": "traffic on gateway interface", 172 | "varLabels": [ 173 | "gateway" 174 | ], 175 | "fixedLabels": { 176 | "direction": "Received", 177 | "unit": "Packets", 178 | "interface": "DSL" 179 | } 180 | }, 181 | "promType": "CounterValue" 182 | }, 183 | { 184 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 185 | "action": "GetTotalPacketsSent", 186 | "result": "TotalPacketsSent", 187 | "promDesc": { 188 | "fqName": "gateway_traffic", 189 | "help": "traffic on gateway interface", 190 | "varLabels": [ 191 | "gateway" 192 | ], 193 | "fixedLabels": { 194 | "direction": "Sent", 195 | "unit": "Packets", 196 | "interface": "DSL" 197 | } 198 | }, 199 | "promType": "CounterValue" 200 | }, 201 | { 202 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 203 | "action": "GetCommonLinkProperties", 204 | "result": "Layer1UpstreamMaxBitRate", 205 | "promDesc": { 206 | "fqName": "gateway_max_bitrate", 207 | "help": "max bitrate on gateway interface", 208 | "varLabels": [ 209 | "gateway" 210 | ], 211 | "fixedLabels": { 212 | "direction": "Up", 213 | "interface": "DSL" 214 | } 215 | }, 216 | "promType": "GaugeValue", 217 | "cacheEntryTTL": 60 218 | }, 219 | { 220 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 221 | "action": "GetCommonLinkProperties", 222 | "result": "Layer1DownstreamMaxBitRate", 223 | "promDesc": { 224 | "fqName": "gateway_max_bitrate", 225 | "help": "max bitrate on gateway interface", 226 | "varLabels": [ 227 | "gateway" 228 | ], 229 | "fixedLabels": { 230 | "direction": "Down", 231 | "interface": "DSL" 232 | } 233 | }, 234 | "promType": "GaugeValue", 235 | "cacheEntryTTL": 60 236 | }, 237 | { 238 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 239 | "action": "GetCommonLinkProperties", 240 | "result": "PhysicalLinkStatus", 241 | "okValue": "Up", 242 | "promDesc": { 243 | "fqName": "gateway_connection_status", 244 | "help": "Connection status (Connected = 1)", 245 | "varLabels": [ 246 | "gateway" 247 | ], 248 | "fixedLabels": { 249 | "connection": "Physical Link", 250 | "interface": "DSL" 251 | } 252 | }, 253 | "promType": "GaugeValue", 254 | "cacheEntryTTL": 60 255 | }, 256 | { 257 | "service": "urn:schemas-upnp-org:service:WANIPConnection:1", 258 | "action": "GetStatusInfo", 259 | "result": "ConnectionStatus", 260 | "okValue": "Connected", 261 | "promDesc": { 262 | "fqName": "gateway_connection_status", 263 | "help": "Connection status (Connected = 1)", 264 | "varLabels": [ 265 | "gateway" 266 | ], 267 | "fixedLabels": { 268 | "connection": "IP", 269 | "interface": "WAN" 270 | } 271 | }, 272 | "promType": "GaugeValue", 273 | "cacheEntryTTL": 60 274 | }, 275 | { 276 | "service": "urn:schemas-upnp-org:service:WANIPConnection:1", 277 | "action": "GetStatusInfo", 278 | "result": "Uptime", 279 | "promDesc": { 280 | "fqName": "gateway_connection_uptime_seconds", 281 | "help": "Connection uptime", 282 | "varLabels": [ 283 | "gateway" 284 | ], 285 | "fixedLabels": { 286 | "connection": "IP", 287 | "interface": "WAN" 288 | } 289 | }, 290 | "promType": "GaugeValue" 291 | }, 292 | { 293 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 294 | "action": "GetInfo", 295 | "result": "ConnectionStatus", 296 | "okValue": "Connected", 297 | "promDesc": { 298 | "fqName": "gateway_wan_connection_status", 299 | "help": "WAN Connection status (Connected = 1)", 300 | "varLabels": [ 301 | "gateway", 302 | "ConnectionType", 303 | "ExternalIPAddress", 304 | "MACAddress" 305 | ], 306 | "fixedLabels": { 307 | "connection": "PPP", 308 | "interface": "WAN" 309 | } 310 | }, 311 | "promType": "GaugeValue" 312 | }, 313 | { 314 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 315 | "action": "GetInfo", 316 | "result": "Uptime", 317 | "promDesc": { 318 | "fqName": "gateway_connection_uptime_seconds", 319 | "help": "Connection uptime", 320 | "varLabels": [ 321 | "gateway" 322 | ], 323 | "fixedLabels": { 324 | "connection": "PPP", 325 | "interface": "WAN" 326 | } 327 | }, 328 | "promType": "GaugeValue" 329 | }, 330 | { 331 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 332 | "action": "GetInfo", 333 | "result": "UpstreamMaxBitRate", 334 | "promDesc": { 335 | "fqName": "gateway_connection_max_bitrate", 336 | "help": "connection max bitrate", 337 | "varLabels": [ 338 | "gateway" 339 | ], 340 | "fixedLabels": { 341 | "direction": "Up", 342 | "connection": "PPP", 343 | "interface": "WAN" 344 | } 345 | }, 346 | "promType": "GaugeValue" 347 | }, 348 | { 349 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 350 | "action": "GetInfo", 351 | "result": "DownstreamMaxBitRate", 352 | "promDesc": { 353 | "fqName": "gateway_connection_max_bitrate", 354 | "help": "connection max bitrate", 355 | "varLabels": [ 356 | "gateway" 357 | ], 358 | "fixedLabels": { 359 | "direction": "Down", 360 | "connection": "PPP", 361 | "interface": "WAN" 362 | } 363 | }, 364 | "promType": "GaugeValue" 365 | }, 366 | { 367 | "service": "urn:dslforum-org:service:WLANConfiguration:1", 368 | "action": "GetTotalAssociations", 369 | "result": "TotalAssociations", 370 | "promDesc": { 371 | "fqName": "gateway_wlan_current_connections", 372 | "help": "current WLAN connections", 373 | "varLabels": [ 374 | "gateway" 375 | ], 376 | "fixedLabels": { 377 | "wlan": "2.4 GHz" 378 | } 379 | }, 380 | "promType": "GaugeValue" 381 | }, 382 | { 383 | "service": "urn:dslforum-org:service:WLANConfiguration:2", 384 | "action": "GetTotalAssociations", 385 | "result": "TotalAssociations", 386 | "promDesc": { 387 | "fqName": "gateway_wlan_current_connections", 388 | "help": "current WLAN connections", 389 | "varLabels": [ 390 | "gateway" 391 | ], 392 | "fixedLabels": { 393 | "wlan": "5 Ghz" 394 | } 395 | }, 396 | "promType": "GaugeValue" 397 | }, 398 | { 399 | "service": "urn:dslforum-org:service:WLANConfiguration:1", 400 | "action": "GetInfo", 401 | "result": "Status", 402 | "okValue": "Up", 403 | "promDesc": { 404 | "fqName": "gateway_wlan_status", 405 | "help": "WLAN status (Enabled = 1)", 406 | "varLabels": [ 407 | "gateway" 408 | ], 409 | "fixedLabels": { 410 | "wlan": "2.4 Ghz" 411 | } 412 | }, 413 | "promType": "GaugeValue", 414 | "cacheEntryTTL": 60 415 | }, 416 | { 417 | "service": "urn:dslforum-org:service:WLANConfiguration:2", 418 | "action": "GetInfo", 419 | "result": "Status", 420 | "okValue": "Up", 421 | "promDesc": { 422 | "fqName": "gateway_wlan_status", 423 | "help": "WLAN status (Enabled = 1)", 424 | "varLabels": [ 425 | "gateway" 426 | ], 427 | "fixedLabels": { 428 | "wlan": "5 Ghz" 429 | } 430 | }, 431 | "promType": "GaugeValue", 432 | "cacheEntryTTL": 60 433 | }, 434 | { 435 | "service": "urn:dslforum-org:service:DeviceInfo:1", 436 | "action": "GetInfo", 437 | "result": "UpTime", 438 | "promDesc": { 439 | "fqName": "gateway_uptime_seconds", 440 | "help": "gateway uptime", 441 | "varLabels": [ 442 | "gateway", 443 | "Description" 444 | ] 445 | }, 446 | "promType": "GaugeValue" 447 | }, 448 | { 449 | "service": "urn:dslforum-org:service:DeviceInfo:1", 450 | "action": "GetInfo", 451 | "result": "ModelName", 452 | "promDesc": { 453 | "fqName": "gateway_device_modelname", 454 | "help": "gateway device model name", 455 | "varLabels": [ 456 | "gateway", 457 | "Description", 458 | "ModelName", 459 | "ProductClass", 460 | "SoftwareVersion", 461 | "HardwareVersion" 462 | ] 463 | }, 464 | "promType": "GaugeValue" 465 | }, 466 | { 467 | "service": "urn:dslforum-org:service:LANEthernetInterfaceConfig:1", 468 | "action": "GetStatistics", 469 | "result": "Stats.BytesSent", 470 | "promDesc": { 471 | "fqName": "gateway_traffic", 472 | "help": "traffic on gateway interface", 473 | "varLabels": [ 474 | "gateway" 475 | ], 476 | "fixedLabels": { 477 | "direction": "Sent", 478 | "unit": "Bytes", 479 | "interface": "LAN" 480 | } 481 | }, 482 | "promType": "CounterValue" 483 | }, 484 | { 485 | "service": "urn:dslforum-org:service:LANEthernetInterfaceConfig:1", 486 | "action": "GetStatistics", 487 | "result": "Stats.BytesReceived", 488 | "promDesc": { 489 | "fqName": "gateway_traffic", 490 | "help": "traffic on gateway interface", 491 | "varLabels": [ 492 | "gateway" 493 | ], 494 | "fixedLabels": { 495 | "direction": "Received", 496 | "unit": "Bytes", 497 | "interface": "LAN" 498 | } 499 | }, 500 | "promType": "CounterValue" 501 | }, 502 | { 503 | "service": "urn:dslforum-org:service:Hosts:1", 504 | "action": "GetGenericHostEntry", 505 | "actionArgument": { 506 | "name": "NewIndex", 507 | "isIndex": true, 508 | "providerAction": "GetHostNumberOfEntries", 509 | "value": "HostNumberOfEntries" 510 | }, 511 | "result": "Active", 512 | "promDesc": { 513 | "fqName": "gateway_host_active", 514 | "help": "is host currently active", 515 | "varLabels": [ 516 | "gateway", 517 | "IPAddress", 518 | "MACAddress", 519 | "InterfaceType", 520 | "HostName" 521 | ] 522 | }, 523 | "promType": "GaugeValue", 524 | "cacheEntryTTL": 60 525 | }, 526 | { 527 | "service": "urn:dslforum-org:service:X_AVM-DE_Dect:1", 528 | "action": "GetGenericDectEntry", 529 | "actionArgument": { 530 | "name": "NewIndex", 531 | "isIndex": true, 532 | "providerAction": "GetNumberOfDectEntries", 533 | "value": "NumberOfEntries" 534 | }, 535 | "result": "Active", 536 | "promDesc": { 537 | "fqName": "gateway_dect_active", 538 | "help": "is dect device currently active", 539 | "varLabels": [ 540 | "gateway", 541 | "ID", 542 | "Name", 543 | "Model" 544 | ] 545 | }, 546 | "promType": "GaugeValue", 547 | "cacheEntryTTL": 120 548 | } 549 | ] -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /systemd/fritzbox_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FritzBox Prometheus Exporter 3 | 4 | [Service] 5 | User=fritzbox_exporter 6 | Group=fritzbox_exporter 7 | EnvironmentFile=/opt/fritzbox_exporter/.fritzbox_exporter.env 8 | ExecStart=/opt/fritzbox_exporter/fritzbox_exporter -gateway-url http://fritz.box:49000 -metrics-file /opt/fritzbox_exporter/metrics.json -listen-address 127.0.0.1:9101 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | --------------------------------------------------------------------------------